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

前言

  国内网盘通过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/ ,输入密码,进入后台,选择账号-添加:

图片[1]-[Emby]基于网盘挂载的emby服务端并实现直链播放-DoubleWorld’s


这边我rclone挂载的是名为 sp01 的世纪互联 sharepoint ,参考我的设置,其 中客户端ID , 客户端密钥刷新令牌(refresh token) 均可以在 rclone 配置中找到, sharepoint站点ID ,填你创建的site id,如果你不知道这个是什么的话,访问 获取SharePoint网站site-id 。
完成设置后,点击右下角的首页,进入 sp01 目录:

图片[2]-[Emby]基于网盘挂载的emby服务端并实现直链播放-DoubleWorld’s



随便点击一部电影并试试播放速度:

图片[3]-[Emby]基于网盘挂载的emby服务端并实现直链播放-DoubleWorld’s



速度不错,并且 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 -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]-[Emby]基于网盘挂载的emby服务端并实现直链播放-DoubleWorld’s
© 版权声明
THE END
喜欢就支持一下吧
点赞13 分享
评论 共2条
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片
    • 头像xiaomeng0