2024.4.9补充说明
随着emby版本和alist版本的更新,本文的脚本不具备一定及时性,由于这篇文章一直热度还算可以,为了使兄弟们少走弯路,有一定动手能力的,请移步作者脚本主页 emby2Alist ,聪明的朋友其实只要替换下conf.d目录下的脚本,然后按提示做修改即可,本文后续不在更新,因为笔者不玩直链好久了 😂 。
前言
国内网盘通过rclone挂载搭建的emby服务端本身速度尚可,如果你的服务器是家里的nas,这方案算是比较合适的方案。为啥要用网盘搭建emby影视库,因为网盘提供了家里硬盘所没有的大容量,如果你的影视资源比较多的话,网盘挂载应该是你的选择。
这里我提供了另一个思路,rclone挂载国内网盘,实现emby的直链播放,什么意思呢,即实现播放电影走的是网盘的cdn服务器,不走服务器流量,这样你播放电影不受限于家里nas的上传带宽,或者不影响vps的流量额度,此外由于是国内直链播放,速度相当的快,如果emby服务端是放在vps,vps到你家里的速度影响的只是前端的速度,即海报刷新的速度,不影响视频播放的速度。
当然,要实现这个方案,离不开大佬的智慧结晶,我这里其实起了一个抛砖引玉的作用,贴一下群里 bpking大佬的脚本及教程,写得比较简单,适合稍微有点基础的人。我这里记录下我自己的折腾过程,本文内容可能会比较长,请做好心里准备
。
原理
搭建 alist多种存储的目录文件列表程序 ,将需要挂载的网盘添加上去,如阿里云盘,世纪互联等,然后使用 nginx 及其 njs 模块将 emby 视频播放地址劫持到 alist 直链。
准备工作
准备一台vps,系统推荐 Debian11 ,并搭建好 emby 服务端,解决 rclone 挂载国内网盘,这里不再赘述,网上教程很多。最终访问 http://vps-ip:8096/ ,可以正常访问 emby 并正常播放视频,视为完成准备工作。
1. 安装alist并创建网盘列表
alsit 项目地址: alist项目
参照 alist文档 的安装教程,我这里采用直装版。
安装完成后,打开 http://vps-ip:5244/ ,输入密码,进入后台,选择账号-添加:

这边我rclone挂载的是名为 sp01 的世纪互联 sharepoint ,参考我的设置,其 中客户端ID , 客户端密钥, 刷新令牌(refresh token) 均可以在 rclone 配置中找到, sharepoint站点ID ,填你创建的site id,如果你不知道这个是什么的话,访问 获取SharePoint网站site-id 。
完成设置后,点击右下角的首页,进入 sp01 目录:
随便点击一部电影并试试播放速度:
速度不错,并且 vps 无瞬时的大流量上传的话, alist 安装完成。
2. 安装nginx
如果你的 nginx 无其他用途,仅用来反代 emby ,推荐用 前言 大佬教程里的 docker版 ,省却了很多折腾步骤,这里我决定采用安装版。要求 nginx 版本大于 1.20 ,如果你已经安装过 nginx ,可以通过以下命令查看版本:
nginx -v
版本如大于 1.20 即可,但是由于 debian 默认的 nginx 源版本往往比较低,所以我们要采用官方的安装方式: nginx官方最新版debian安装教程 。安装过程不重复了,自行参照下官网步骤。
安装njs模块
apt install nginx-module-njs安装完成后,进入 nginx 的配置目录:
cd /etc/nginx/conf.d创建你域名的配置,如 yourdomain.com.conf ,添加如下内容
# Load the njs script
js_path /etc/nginx/conf.d/;
js_import emby2Pan from emby.js;
#Cache images
proxy_cache_path /var/cache/nginx/emby levels=1:2 keys_zone=emby:100m max_size=1g inactive=30d use_temp_path=off;
proxy_cache_path /var/cache/nginx/emby/subs levels=1:2 keys_zone=embysubs:10m max_size=1g inactive=30d use_temp_path=off;
server{
gzip on;
listen 80;
server_name yourdomain.com;
# aliDrive direct stream need no-referrer
add_header 'Referrer-Policy' 'no-referrer';
set $emby http://127.0.0.1:8096; #emby address
# Proxy sockets traffic for jellyfin-mpv-shim and webClient
location ~ /(socket|embywebsocket) {
# Proxy Emby Websockets traffic
proxy_pass $emby;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
}
# Redirect the stream to njs
location ~* /videos/(\d+)/stream {
js_content emby2Pan.redirect2Pan;
}
# for webClient download ,android is SyncService api
location ~* /Items/(\d+)/Download {
js_content emby2Pan.redirect2Pan;
}
#Cache the Subtitles
location ~* /videos/(.*)/Subtitles {
proxy_pass $emby;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_cache embysubs;
proxy_cache_revalidate on;
proxy_cache_lock_timeout 10s;
proxy_cache_lock on;
proxy_cache_valid 200 30d;
proxy_cache_key $proxy_host$uri;
#add_header X-Cache-Status $upstream_cache_status; # This is only to check if cache is working
}
# Cache the images
location ~ /Items/(.*)/Images {
proxy_pass $emby;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_cache emby;
proxy_cache_revalidate on;
proxy_cache_lock_timeout 10s;
proxy_cache_lock on;
# add_header X-Cache-Status $upstream_cache_status; # This is only to check if cache is working
}
location / {
# Proxy main Emby traffic
proxy_pass $emby;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Protocol $scheme;
proxy_set_header X-Forwarded-Host $http_host;
# Disable buffering when the nginx proxy gets very resource heavy upon streaming
proxy_buffering off;
}
}同目录下创建 emby.js ,添加如下内容:
//查看日志: "docker logs -f -n 10 emby-nginx 2>&1 | grep js:"
async function redirect2Pan(r) {
//下面4个设置,通常来说保持默认即可,根据实际情况修改
const embyHost = 'http://127.0.0.1:8096'; //这里默认emby的地址是宿主机,要注意iptables给容器放行端口
const embyMountPath = '/mnt'; // rclone 的挂载目录, 例如将od, gd挂载到/mnt目录下: /mnt/onedrive /mnt/gd ,那么这里就填写 /mnt
const alistPwd = 'alist'; //alist password
const alistApiPath = 'http://127.0.0.1:5244/api/public/path'; //访问宿主机上5244端口的alist api, 要注意iptables给容器放行端口
//fetch mount emby file path
const itemId = /[\d]+/.exec(r.uri)[0];
const mediaSourceId = r.args.MediaSourceId;
const api_key = r.args.api_key;
//infuse用户需要填写下面的api_key, 感谢@amwamw968
if ((api_key === null) || (api_key === undefined)) {
api_key = 'b19cde886a384fc097750b412345678';//这里填自己的API KEY
r.error(`api key for Infuse: ${api_key}`);
}
const itemInfoUri = `${embyHost}/emby/Items/${itemId}/PlaybackInfo?api_key=${api_key}`;
r.error(`itemInfoUri: ${itemInfoUri}`);
const embyRes = await fetchEmbyFilePath(itemInfoUri, mediaSourceId);
if (embyRes.startsWith('error')) {
r.error(embyRes);
r.return(500, embyRes);
return;
}
r.error(`mount emby file path: ${embyRes}`);
//fetch alist direct link
const alistFilePath = embyRes.replace(embyMountPath, '');
const alistRes = await fetchAlistPathApi(alistApiPath, alistFilePath, alistPwd);
if (!alistRes.startsWith('error')) {
r.error(`redirect to: ${alistRes}`);
r.return(302, alistRes);
return;
}
if (alistRes.startsWith('error401')) {
r.error(alistRes);
r.return(401, alistRes);
return;
}
if (alistRes.startsWith('error404')) {
const filePath = alistFilePath.substring(alistFilePath.indexOf('/', 1));
const foldersRes = await fetchAlistPathApi(alistApiPath, '/', alistPwd);
if (foldersRes.startsWith('error')) {
r.error(foldersRes);
r.return(500, foldersRes);
return;
}
const folders = foldersRes.split(',').sort();
for (let i = 0; i < folders.length; i++) {
r.error(`try to fetch alist path from /${folders[i]}${filePath}`);
const driverRes = await fetchAlistPathApi(alistApiPath, `/${folders[i]}${filePath}`, alistPwd);
if (!driverRes.startsWith('error')) {
r.error(`redirect to: ${driverRes}`);
r.return(302, driverRes);
return;
}
}
r.error(alistRes);
r.return(404, alistRes);
return;
}
r.error(alistRes);
r.return(500, alistRes);
return;
}
async function fetchAlistPathApi(alistApiPath, alistFilePath, alistPwd) {
const alistRequestBody = {
"path": alistFilePath,
"password": alistPwd
}
try {
const response = await ngx.fetch(alistApiPath, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8'
},
max_response_body_size: 65535,
body: JSON.stringify(alistRequestBody)
})
if (response.ok) {
const result = await response.json();
if (result === null || result === undefined) {
return `error: alist_path_api response is null`;
}
if (result.message == 'success') {
if (result.data.type == 'file') {
return result.data.files[0].url;
}
if (result.data.type == 'folder') {
return result.data.files.map(item => item.name).join(',');
}
}
if (result.code == 401) {
return `error401: alist_path_api ${result.message}`;
}
if (result.message.includes('account')) {
return `error404: alist_path_api ${result.code} ${result.message}`;
}
if (result.message == 'path not found') {
return `error404: alist_path_api ${result.message}`;
}
return `error: alist_path_api ${result.code} ${result.message}`;
}
else {
return `error: alist_path_api ${response.status} ${response.statusText}`;
}
} catch (error) {
return (`error: alist_path_api fetchAlistFiled ${error}`);
}
}
async function fetchEmbyFilePath(itemInfoUri, mediaSourceId) {
try {
const res = await ngx.fetch(itemInfoUri, { max_response_body_size: 65535 });
if (res.ok) {
const result = await res.json();
if (result === null || result === undefined) {
return `error: emby_api itemInfoUri response is null`;
}
const mediaSource = result.MediaSources.find(m => m.Id == mediaSourceId);
if (mediaSource === null || mediaSource === undefined) {
return `error: emby_api mediaSourceId ${mediaSourceId} not found`;
}
return mediaSource.Path;
}
else {
return (`error: emby_api ${res.status} ${res.statusText}`);
}
}
catch (error) {
return (`error: emby_api fetch mediaItemInfo failed, ${error}`);
}
}
export default { redirect2Pan };根据注释的地方自行调整相应的配置。
修改 /etc/nginx/nginx.conf ,在首行添加如下内容:
load_module modules/ngx_http_js_module.so;验证 nginx 配置是否问题:
nginx -c /etc/nginx/nginx.conf
nginx -t如无报错,重启 nginx
nginx -s reload打开上述 nginx 配置的域名,如http://yourdomain.com , 注意这里不要访问默认的 8096 端口,如果能正常访问 emby 界面, nginx 安装工作完成。
3. 验证直链播放是否成功
随机打开一部电影,验证播放、拖曳速度。
查看 nginx js 日志:
tail -f -n 10 /var/log/nginx/access.log /var/log/nginx/error.log | grep js:如出现以下直链地址,表示直链成功,并且此时流量不经过 vps 服务器。
4. 关于直链成功的一些补充说明
这里直接引用 @bpking 大哥的原话
直链播放不支持转码,转码的话只能走emby server
所以最好 在emby设置中将 播放 –> 视频 –> 互联网质量 设置为最高 ,并且将用户的转码权限关掉,确保走直链
web端各大浏览器对音频和视频编码支持情况不一,碰到不支持的情况emby会强制走转码而不会走直链 ``
评论