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会强制走转码而不会走直链 ``
好!很棒的笔记记录!
学习一下,谢谢大佬
学习一下~
学习一下直连。我现在都搭建完了,就是挂载不上
挂载可以看下rclone笔记那篇的一键脚本
6
学习一下!!
感谢大佬
大佬,这个适合jellyfin么?
最近刚好需要谢谢
来大佬这学习看看
学习学习!
研究下emby直链,感谢大神分享
学习ing
学习一下
学习一下,找下自己问题出在哪
学习一下
学习一下
学习一下
观摩
学习下经验
感谢分享
学习下
膜拜大佬
学习一下,谢谢大佬
鼠标轨迹看的难受
不错
现在被速度折磨的难受,试试您的方案
6666666
谢谢大佬的教程
好!很棒的笔记记录!表情
学习一下,谢谢大佬
学习一下,谢谢大佬
学习相关11
谢谢作者的分享
不错的笔记,很详细,正好要用。
感谢大佬,终于不用耗费vps,希望开通捐赠
好文章~
本来想反代的,现在好像直链舒服一点
巧了~感谢有你,解决我的 问题
好!很棒的笔记记录!表情
感谢!
学习学习!
非常感谢.
真的可以?
向大佬学习
很棒的笔记
谢谢~~~~~~~
感谢分享
回复学习一下
帮大忙了
学生优惠的阿里云服务器只有1M带宽,这个真的救命了
前来学习
6666
辛苦了
感谢分享!
来看看怎么样
学习一下
很棒,继续学习
大佬,emby.js里的infuse api_key在哪里找,我想用infuse直链
666666666
很棒的教程 这就试试
牛逼牛逼
学习学习
看一看
好帅的笔记
正好需要这个
学习一下,正在研究哈哈哈
好!很棒的笔记记录!表情
学习学习
下载链接过期
好!很棒的笔记记录
优秀
看看怎么弄的
学习一下
可以
这个方法已经失效了
看看还能不能用
666666666666666
学习一下直链
学习学习
厉害,这就去试试
学习一下直连.问问alist直链那里可以换成rclone serve的吗?
alist v3怎么办呢,大佬?
研究研究
试了很多次没有成功,不知道是不是阿里网盘改策略来
学习
学习一下,谢谢大佬
来了
555555555555555555555
学习到了,谢谢大佬
大佬想问下 如果alist挂载gd或者od的话这样也能获取到alist配置的代理地址播放么
厉害了!
学习学习
學一下
学习一下,谢谢大佬
谢谢分享看看 vu
谢谢分享啊
很好的教程
学习以下
非常受教
好!很棒的笔记记录!
好!很棒的笔记记录!表情
学习一下核心技术emby.js怎么实现的
学习以下
学习一下,谢谢大佬
非常好的技术文章
hao,feichanghao
学习一下
啊啊啊啊啊啊
感谢大佬的指路,这对我真的帮助很大
学习下
看看行不行
学习一下
这个好,最近正需要
学习一下
好!很棒的笔记记录!表情
windows版本的有教程吗?
学习一下
感谢楼主!另外想问一下大佬的世纪互联是在哪买的~
好!很棒的笔记记录!
我找了好多地方 在学习 ,只有这里有
太强了。学习学习
好!很棒的笔记记录!
感谢大佬
das
学习了
学习一下,谢谢大佬
牛逼牛逼,学习试试
很棒的笔记!Mark一下。硬盘不够用的时候就试一下。
plex是不是同理能实现?
666666
大佬牛逼,整起来
学习了大佬
1111
这个是值支持web播放吗?emby或者infuse支持吗
感谢分享
//查看日志: "docker logs -f -n 10 emby-nginx 2>&1 | grep js:"
async function redirect3Pan(r) {
//下面4个设置,通常来说保持默认即可,根据实际情况修改
const embyHost = 'http://127.0.0.1:8096'; //这里默认emby的地址是宿主机,要注意iptables给容器放行端口
const embyMountPath = '/'; // rclone 的挂载目录, 例如将od, gd挂载到/mnt目录下: /mnt/onedrive /mnt/gd ,那么这里就填写 /mnt
const alistToken = ''; //alist Token
const alistApiPath = 'http://127.0.0.1:5244/api/fs/get'; //访问宿主机上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, alistToken);
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, '/', alistToken);
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}`, alistToken);
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, alistToken) {
const alistRequestBody = {
"path": alistFilePath,
"password": ""
}
try {
const response = await ngx.fetch(alistApiPath, {
method: 'POST',
headers: {
'Content-Type': 'application/json;charset=utf-8',
'Authorization': alistToken
},
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 == 2) {
return result.data.raw_url;
}
else {
return 'error502: alist_path_api type_error!'
}
}
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 { redirect3Pan};
自己稍微修改了下,适用于alist v3版本
不错~
alistToken哪里获取?
就是密码
你的alist 令牌 在管理面板->设置->其他里面
学习一下
学习一下,谢谢大佬
看看
学习下
学习一下,谢谢大佬
感谢分享
学习学习,生命在于折腾
最近刚好需要谢谢
谢谢大佬
插个锚点
学习一下
学习一下
学习学习.感谢
你好,8095端口播放,日志报错[error] 20#20: *847 js: error500: alist_path_api 401 Guest user is disabled, login please是哪里问题呢
好!很棒的笔记记录!表情
谢谢博主
谢谢大佬
谢谢分享,学习一下
学习一下
学习一下,谢谢大佬
学习一下,谢谢大佬
谢谢分享
好!很棒的笔记记录
谢谢大佬,谢谢
学习一下
感谢大佬
我挂载完了,看到大佬的笔记,真是不错!
谢谢分享
学习学习
好文章 学习学习
mark!学习学习
学习一下
学习
感觉有点难
看看看看
感谢大佬
8888888
学习一下了,谢谢
没想到还有这种操作,学习一下
学习一下,感谢大佬!
学习学习
查看隐藏内容
学习学
6666
感谢分享
感谢好大哥的详细介绍
学习一下
头像
xxxx
Windows 10 · Google Chrome
感谢分享
一句话,大佬牛逼 请问plex能实现直链播放吗?