基于网盘挂载的emby服务端并实现直链播放
侧边栏壁纸
  • 累计撰写 17 篇文章
  • 累计收到 761 条评论

基于网盘挂载的emby服务端并实现直链播放

syqman
2022-06-03 / 520 评论 / 13,265 阅读 / 正在检测是否收录...
温馨提示:
本文最后更新于2024年04月09日,已超过584天没有更新,若内容或图片失效,请留言反馈。
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/ ,输入密码,进入后台,选择账号-添加:

l3xtl27v.png
这边我rclone挂载的是名为 sp01 的世纪互联 sharepoint ,参考我的设置,其 中客户端ID , 客户端密钥, 刷新令牌(refresh token) 均可以在 rclone 配置中找到, sharepoint站点ID ,填你创建的site id,如果你不知道这个是什么的话,访问 获取SharePoint网站site-id
完成设置后,点击右下角的首页,进入 sp01 目录:
l3xtzhyr.png
随便点击一部电影并试试播放速度:
l3xu0xo2.png
速度不错,并且 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 服务器。
l3xvie2i.png

4. 关于直链成功的一些补充说明

这里直接引用 @bpking 大哥的原话

直链播放不支持转码,转码的话只能走emby server
所以最好 在emby设置中将 播放 –> 视频 –> 互联网质量 设置为最高 ,并且将用户的转码权限关掉,确保走直链
web端各大浏览器对音频和视频编码支持情况不一,碰到不支持的情况emby会强制走转码而不会走直链 ``
3

评论

博主关闭了当前页面的评论