Python flask emby直链反代

前言

本文使用的Emby版本为4.5.4

这是魔改某大佬的方法,但是博主却无法做到更完善,因为本人在JavaScript方面如同一个门外汉,无法解决网页端调用失败的问题,以及直链页面加密后获取直链失败的问题;此外,每次换小鸡后安装njs插件也是个麻烦的问题。

最终用Python撸了一个反代工具:
主要有以下特点:

  •  解决网页端播放失败的问题
  •  支持设置密码的直链页面(目前仅支持Onemanager)
  •  支持源码运行或二进制文件运行
  •  多文件夹多路径替换
  •  下载视频时走直链

网页端播放问题解决思路

与使用无关,不感兴趣的直接往下翻。在重定向过程中,刚开始得到的是类似 https://xxx.mkv 这样的文件,但是在使用这个直链的时候,会再次重定向(301)到一个新的微软的文件地址,是平时下载微软文件是那一长串有你微软账户域名和api新的的长串链接,而emby网页端只支持一次301重定向,所以需要在301时直接指向最终的那个链接。

使用

Readme Card

方法一:打包文件直接使用

releases下载最新的打包文件,上传到服务器

在同目录新建config.json文件,编辑如下内容

{
    "main_site": "http://127.0.0.1:8096/",
    "main_port": "8096",
    "new_port": "2333",
    "api_key": "1fb2477b5cb14d32843753xxxxxxxx",
    "redirects": "True",
    "password_key": "True",
    "password_value": "xxxx",
    "replace_list": [
        {
            "from": "/media/video/",
            "to": "http://xxxx.weinb.top/shi/"
        },
        {
            "from": "/media/adult/",
            "to": "http://xxxx.weinb.top/adult/emby/"
        }
    ]
}

配置解释:
main_site:emby的原本地址,后面需要加/
main_port:emby原端口
new_port:反代后的端口
api_key:emby的api,需要自己在api中设置
redirects:是否需要获取直链后再次获取真实文件链接,od需要开启此项,True为开启,False为关闭
password_key:直链的目录界面是否设置有密码,目前仅支持Onemanager
password_value:密码的值,开启前一项此项才生效,关闭时可留空或任意值
replace_list:替换的规则此处效果如下

/media/video/newanime/奇巧计程车/Season 01/[Lilith-Raws] Odd Taxi - S01E01 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4].mp4
上边路径的文件会被替换为
http://xxxx.weinb.top/shi/newanime/奇巧计程车/Season 01/[Lilith-Raws] Odd Taxi - S01E01 [Baha][WEB-DL][1080p][AVC AAC][CHT][MP4].mp4

支持无限条规则,注意格式即可,例如三条规则的格式

"replace_list": [
    {
        "from": "/media/video/",
        "to": "http://xxxx.weinb.top/shi/"
    },
    {
        "from": "/media/adult/",
        "to": "http://xxxx.weinb.top/adult/emby/"
    }, 
    {
        "from": "/media/test/",
        "to": "http://xxxx.weinb.top/test/emby/"
    }
]

修改后最好进行格式化检查看格式是否正确:点击检验

配置完成后,运行二进制文件(此处为Linux环境)

chmod +x web & ./web

即可完成,注意,此方法运行断开SSH连接后程序也会停止,请配置Screen之类的程序保持运行

方法二:运行源码

环境要求:Python3.6+

按方法一的要求设置config.json文件

安装依赖

pip3 install flask
pip3 install requests

获取源码

wget https://raw.githubusercontent.com/666wcy/emby-python/main/web.py

运行程序

python3 web.py

此方法默认你拥有部分Python基础,不做过多赘述。

增加功能(网页版)

在网页版原本功能上加入调用外部播放器,此方法来自Tg大佬 @baipiaoking ,也是原方法的作者,自己只是做了小部分修改(修改软件包名和路径替换部分)。

/opt/emby-server/system/dashboard-ui路径找到index.html

编辑index.html文件,找到以下代码

<script src="apploader.js" defer></script>

在此行代码下添加如下代码

<script type="text/javascript" src="./externalPlayer.js"></script>

保存后退出。在同文件夹下新建externalPlayer.js文件,添加如下内容

const api_key = "1fb2477b5cb14d32843753977a60cb17";

const reg = /\/[a-z]{2,}\/\S*?id=/;

let timer = setInterval(function() {
    let potplayer = document.querySelectorAll("div[is='emby-scroller']:not(.hide) .potplayer")[0];
    if(!potplayer){
        let mainDetailButtons = document.querySelectorAll("div[is='emby-scroller']:not(.hide) .mainDetailButtons")[0];
        if(mainDetailButtons){
            let buttonhtml = `<div class ="flex">
                <button id="cloudPot" type="button" class="detailButton emby-button potplayer" title="Cloud"> <div class="detailButton-content"> <i class="md-icon detailButton-icon"></i> <div class="detailButton-text">PotPlayer</div> </div> </button>
                <button id="embyIINA" type="button" class="detailButton emby-button iina" title="IINA"> <div class="detailButton-content"> <i class="md-icon detailButton-icon"></i> <div class="detailButton-text">IINA</div> </div> </button>
                <button id="nPlayer" type="button" class="detailButton emby-button nPlayer" title="nPlayer"> <div class="detailButton-content"> <i class="md-icon detailButton-icon"></i> <div class="detailButton-text">NPlayer</div> </div> </button>
                <button id="mxPlayer" type="button" class="detailButton emby-button mxPlayer" title="MxPlayer"> <div class="detailButton-content"> <i class="md-icon detailButton-icon"></i> <div class="detailButton-text">MXPlayer Pro</div> </div> </button>
                <button id="mxPlayerad" type="button" class="detailButton emby-button mxPlayerad" title="MxPlayerad"> <div class="detailButton-content"> <i class="md-icon detailButton-icon"></i> <div class="detailButton-text">MXPlayer Free</div> </div> </button>
            </div>`
            mainDetailButtons.insertAdjacentHTML('afterend', buttonhtml)
            document.querySelector("div[is='emby-scroller']:not(.hide) #cloudPot").onclick = cloudPot;
            document.querySelector("div[is='emby-scroller']:not(.hide) #embyIINA").onclick = embyIINA;
            //document.querySelector("div[is='emby-scroller']:not(.hide) #mxPlayer").onclick = mxPlayer;
            document.querySelector("div[is='emby-scroller']:not(.hide) #nPlayer").onclick = nPlayer;
            document.querySelector("div[is='emby-scroller']:not(.hide) #mxPlayer").onclick = mxPlayer;
            document.querySelector("div[is='emby-scroller']:not(.hide) #mxPlayerad").onclick = mxPlayerad;
        }
    }
}, 1000)

async function getItemInfo(){
    let itemInfoUrl = window.location.href.replace(reg, "/emby/Items/").split('&')[0] + "/PlaybackInfo?api_key=" + api_key;
    console.log("itemInfo:" + itemInfoUrl);
    let response = await fetch(itemInfoUrl);
    if(response.ok) {
        return await response.json();
    }else{
        alert("获取视频信息失败,检查api_key是否设置正确 "+response.status+" "+response.statusText);
        throw new Error(response.statusText);
    }
}
function getSeek(){
    //.resumeButtonText
    let resumeButton = document.querySelector(".resumeButtonText");
    let seek = '';
    let temp = `播放时间信息:${resumeButton.innerText}`;
    console.log(temp);
    if (resumeButton) {
        if (resumeButton.innerText.includes('恢复播放')) {
            seek = resumeButton.innerText.replace(' 恢复播放', '');
            seek = seek.replace('从 ', '');
        }
    } return seek;
}
function getSubUrl(itemInfo, MediaSourceIndex){
    let selectSubtitles = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectSubtitles");
    let subTitleUrl = '';
    if (selectSubtitles) {
        if (selectSubtitles.value > 0) {
            if (itemInfo.MediaSources[MediaSourceIndex].MediaStreams[selectSubtitles.value].IsExternal) {
                let subtitleCodec = itemInfo.MediaSources[MediaSourceIndex].MediaStreams[selectSubtitles.value].Codec;
                let MediaSourceId = itemInfo.MediaSources[MediaSourceIndex].Id;
                let domain = window.location.href.replace(reg, "/emby/videos/").split('&')[0];
                subTitleUrl = `${domain}/${MediaSourceId}/Subtitles/${selectSubtitles.value}/${MediaSourceIndex}/Stream.${subtitleCodec}?api_key=${api_key}`;
                console.log(subTitleUrl);
            }
        }
    } return subTitleUrl;
}
async function getCloudSubUrl(itemInfo, MediaSourceIndex){
    let selectSubtitles = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectSubtitles");
    let subTitleUrl = '';
    if (selectSubtitles) { 
        if (selectSubtitles.value > 0) {
            if (itemInfo.MediaSources[MediaSourceIndex].MediaStreams[selectSubtitles.value].IsExternal) {
                let embySubPath = itemInfo.MediaSources[MediaSourceIndex].MediaStreams[selectSubtitles.value].Path;
                subTitleUrl = await url2webdrive(embySubPath);
            }
        }
    }
    return subTitleUrl;
} 

//emby路径转换为网盘路径
async function url2webdrive(embyVideoPath){
    if(embyVideoPath.includes("/media/video")){
        return embyVideoPath.replace("/media/video", "http://xxx.weinb.top/shi");
    }
    if(embyVideoPath.includes("/media/adult")){
        return embyVideoPath.replace("/media/adult", "http://xxx.weinb.top/adult/emby");
    }
}

async function getCloudMediaUrl(){
    let selectSource = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectSource");
    //let selectAudio = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectAudio");
    let itemInfo = await getItemInfo();
    let MediaSourceIndex = 0;
    for(let i = 0; i< itemInfo.MediaSources.length; i++){
        if(itemInfo.MediaSources[i].Id == selectSource.value){ MediaSourceIndex = i;
        };
    }
    let embyVideoPath = itemInfo.MediaSources[MediaSourceIndex].Path;
    
    let cloudVideoUrl = await url2webdrive(embyVideoPath);
    let subUrl = await getSubUrl(itemInfo, MediaSourceIndex);
    console.log(cloudVideoUrl, subUrl);
    return Array(cloudVideoUrl,subUrl);
}
async function cloudPot(){
    let CloudMediaUrl = await getCloudMediaUrl();
    let poturl = `potplayer://${encodeURI(CloudMediaUrl[0])} /sub=${encodeURI(CloudMediaUrl[1])} /current /seek=${getSeek()}`;
    console.log(poturl);
    window.open(poturl, "_blank");
}
async function mxPlayer(){
    let CloudMediaUrl = await getCloudMediaUrl();
    let poturl = `intent:${encodeURI(CloudMediaUrl[0])}#Intent;package=com.mxtech.videoplayer.pro;S.title=undefined;S.subs=${encodeURI(CloudMediaUrl[1])};end`;
    console.log(poturl);
    window.open(poturl, "_blank");
}
async function mxPlayerad(){
    let CloudMediaUrl = await getCloudMediaUrl();
    let poturl = `intent:${encodeURI(CloudMediaUrl[0])}#Intent;package=com.mxtech.videoplayer.ad;S.title=undefined;end`;
    console.log(poturl); window.open(poturl, "_blank");
}
async function nPlayer(){
    let CloudMediaUrl = await getCloudMediaUrl();
    let poturl = `nplayer-${encodeURI(CloudMediaUrl[0])}`;
    console.log(poturl);
    window.open(poturl, "_blank");
}
async function embyIINA(){
    let mediaUrl = await getCloudMediaUrl();
    let iinaUrl = `iina://weblink?url=${escape(mediaUrl[0])}&new_window=1`;
    console.log(`iinaUrl= ${iinaUrl}`);
    window.open(iinaUrl, "_blank");
}

修改第一行的api为自己的api

修改约91行的url2webdrive函数中的路径为自己的路径规则

保存后退出,主要检查js的文件访问权限。

重启Emby

最终实现效果如下

图片[2]-Python flask emby直链反代-随笔

THE END
喜欢就支持一下吧
点赞8
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片