//author: @bpking https://github.com/bpking1/embyExternalUrl let serverAddr = 'http://192.168.101.13:8097'; const tags = ['BluRay', 'REMUX', 'WEB-DL']; //添加视频tag const groups = ['CMCT', 'WIKI', 'Z0N3', 'EbP', 'PTer', 'EPSILON', 'FRDS', 'SMURF']; //添加制作组 let api_key = ''; let domain = ''; let oriData = ''; const redirectKey = 'redirect2external'; const addExternalUrl = async (r, data, flags) => { api_key = r.args['X-Emby-Token'] ? r.args['X-Emby-Token'] : r.headersIn['X-Emby-Token']; api_key = api_key ? api_key : r.args.api_key; //外链地址默认http,如果是https,则需要将这里的 http 改为 https,如果是反代服务器两种都有,可以将这一行注释掉,统一使用第一行填写的地址 serverAddr = r.headersIn.Host ? `http://${r.headersIn.Host}` : serverAddr; domain = `${serverAddr}/emby/videos/${r.uri.split('Items/')[1]}`; r.error(`api_key: ${api_key}`); r.error(`domain: ${domain}`); if (flags.last === false) { oriData += data; r.error(`flags.last: ${flags.last} , data.length: ${data.length}`); return; } else { r.error(`flags.last: ${flags.last}`); data = JSON.parse(oriData); r.error(`data.length: ${JSON.stringify(data).length}`); } if (data.MediaSources && data.MediaSources.length > 0) { try { data = addUrl(r, data); } catch (error) { r.error(`addUrl error: ${error}`); } } r.error(`addUrldata.length: ${JSON.stringify(data).length}`) r.sendBuffer(JSON.stringify(data), flags); r.done(); } const addUrl = (r, data) => { data.MediaSources.map(mediaSource => { const streamUrl = `${domain}/stream.${mediaSource.Container}?api_key=${api_key}&Static=true&MediaSourceId=${mediaSource.Id}`; //get subtitle let subUrl = ''; try { subUrl = getSubUrl(r, mediaSource); } catch (error) { r.error(`get sub url error: ${error}`); } //get displayTitle let displayTitle = ''; try { displayTitle = mediaSource.MediaStreams.find(s => s.Type === 'Video').DisplayTitle; displayTitle = typeof displayTitle === 'undefined' ? '' : displayTitle; } catch (error) { r.error(`get displayTitle error: ${error}`); } //get position const position = parseInt(data.UserData.PlaybackPositionTicks / 10000); //get tagName let tagName = ''; try { tagName = tags.find(t => mediaSource.Name.toUpperCase().includes(t.toUpperCase())); tagName = typeof tagName === 'undefined' ? '' : tagName; } catch (error) { r.error(`get tagName error: ${mediaSource.Name}`); } //get groupName let groupName = ''; try { groupName = groups.find(g => mediaSource.Name.toUpperCase().includes(g.toUpperCase())); groupName = typeof groupName === 'undefined' ? '' : groupName; } catch (error) { r.error(`get groupName error: ${mediaSource.Name}`); } const mediaInfo = { title: data.Name, streamUrl, subUrl, position, displayTitle, mediaSourceName: (tagName + groupName).length > 1 ? `${tagName}-${groupName}` : mediaSource.Name } data.ExternalUrls.push(getPotUrl(mediaInfo)); data.ExternalUrls.push(getVlcUrl(mediaInfo)); data.ExternalUrls.push(getIinaUrl(mediaInfo)); data.ExternalUrls.push(getMXUrl(mediaInfo)); data.ExternalUrls.push(getNPlayerUrl(mediaInfo)); data.ExternalUrls.push(getInfuseUrl(mediaInfo)); }); return data; } const getPotUrl = (mediaInfo) => { return { Name: `potplayer-${mediaInfo.mediaSourceName}-${mediaInfo.displayTitle}`, Url: `potplayer://${encodeURI(mediaInfo.streamUrl)} /sub=${encodeURI(mediaInfo.subUrl)} /seek=${getSeek(mediaInfo.position)}` //双引号不能直接放,可能要base64编码一下 //Url: `potplayer://${encodeURI(mediaInfo.streamUrl)} /sub="${encodeURI(mediaInfo.subUrl)}" /current /title="${encodeURI(mediaInfo.title)}" /seek=${getSeek(mediaInfo.position)}` } } //https://wiki.videolan.org/Android_Player_Intents/ const getVlcUrl = (mediaInfo) => { //安卓: //android subtitles: https://code.videolan.org/videolan/vlc-android/-/issues/1903 //const vlcUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=org.videolan.vlc;type=video/*;S.subtitles_location=${encodeURI(mediaInfo.subUrl)};S.title=${encodeURI(mediaInfo.title)};i.position=${mediaInfo.position};end`; //const vlcUrl64 = Buffer.from(vlcUrl, 'utf8').toString('base64'); //PC: //PC端需要额外设置,参考这个项目,MPV也是类似的方法: https://github.com/stefansundin/vlc-protocol //const vlcUrl = `vlc://${encodeURI(mediaInfo.streamUrl)}`; //ios: //https://code.videolan.org/videolan/vlc-ios/-/commit/55e27ed69e2fce7d87c47c9342f8889fda356aa9 const vlcUrl = `vlc-x-callback://x-callback-url/stream?url=${encodeURIComponent(mediaInfo.streamUrl)}&sub=${encodeURIComponent(mediaInfo.subUrl)}`; const vlcUrl64 = Buffer.from(vlcUrl, 'utf8').toString('base64'); return { Name: `vlc-${mediaInfo.mediaSourceName}-${mediaInfo.displayTitle}`, Url: `${serverAddr}/${redirectKey}?link=${vlcUrl64}` } } //https://github.com/iina/iina/issues/1991 const getIinaUrl = (mediaInfo) => { return { Name: `IINA-${mediaInfo.mediaSourceName}-${mediaInfo.displayTitle}`, Url: `iina://weblink?url=${encodeURIComponent(mediaInfo.streamUrl)}&new_window=1` } } //infuse const getInfuseUrl = (mediaInfo) => { const infuseUrl = `infuse://x-callback-url/play?url=${encodeURIComponent(mediaInfo.streamUrl)}`; const infuseUrl64 = Buffer.from(infuseUrl, 'utf8').toString('base64'); return { Name: `Infuse-${mediaInfo.mediaSourceName}-${mediaInfo.displayTitle}`, Url: `${serverAddr}/${redirectKey}?link=${infuseUrl64}` } } //https://sites.google.com/site/mxvpen/api const getMXUrl = (mediaInfo) => { //mxPlayer free const mxUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=com.mxtech.videoplayer.ad;S.title=${encodeURI(mediaInfo.title)};i.position=${mediaInfo.position};end`; const mxUrl64 = Buffer.from(mxUrl, 'utf8').toString('base64'); //mxPlayer Pro //const mxUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=com.mxtech.videoplayer.pro;S.title=${encodeURI(mediaInfo.title)};i.position=${mediaInfo.position};end`; return { Name: `mxPlayer-${mediaInfo.mediaSourceName}-${mediaInfo.displayTitle}`, Url: `${serverAddr}/${redirectKey}?link=${mxUrl64}` } } const getNPlayerUrl = (mediaInfo) => { const nplayerUrl = `nplayer-${encodeURI(mediaInfo.streamUrl)}`; const nplayerUrl64 = Buffer.from(nplayerUrl, 'utf8').toString('base64'); return { Name: `nplayer-${mediaInfo.mediaSourceName}-${mediaInfo.displayTitle}`, Url: `${serverAddr}/${redirectKey}?link=${nplayerUrl64}` } } const getSeek = (position) => { let ticks = position * 10000; let parts = [] , hours = ticks / 36e9; (hours = Math.floor(hours)) && parts.push(hours); let minutes = (ticks -= 36e9 * hours) / 6e8; ticks -= 6e8 * (minutes = Math.floor(minutes)), minutes < 10 && hours && (minutes = "0" + minutes), parts.push(minutes); let seconds = ticks / 1e7; return (seconds = Math.floor(seconds)) < 10 && (seconds = "0" + seconds), parts.push(seconds), parts.join(":") } const getSubUrl = (r, mediaSource) => { let subTitleUrl = ''; //尝试返回第一个外挂中字 const chiSubIndex = mediaSource.MediaStreams.findIndex(m => m.Language == "chi" && m.IsExternal); r.error('chisubINdex: ' + chiSubIndex); if (chiSubIndex > -1) { const subtitleCodec = mediaSource.MediaStreams[chiSubIndex].Codec; subTitleUrl = `${domain}/${mediaSource.Id}/Subtitles/${chiSubIndex}/Stream.${subtitleCodec}?api_key=${api_key}`; } else { //尝试返回第一个外挂字幕 const externalSubIndex = mediaSource.MediaStreams.findIndex(m => m.IsExternal); r.error('subIndex: ' + externalSubIndex); if (externalSubIndex > -1) { const subtitleCodec = mediaSource.MediaStreams[externalSubIndex].Codec; subTitleUrl = `${domain}/${mediaSource.Id}/Subtitles/${externalSubIndex}/Stream.${subtitleCodec}?api_key=${api_key}`; } } return subTitleUrl; } function HeaderFilter(r) { r.headersOut['Content-Length'] = null; } const redirectUrl = (r) => { const baseLink = r.args.link; r.error(`baseLink: ${baseLink}`); const link = Buffer.from(baseLink, 'base64').toString('utf8'); r.return(302, link); } const rewritePlaybackInfo = async (r, data, flags) => { //const rewritePlaybackInfo = (r) => { // 获取响应体 //let data = r.responseText; //r.error("Original response body: " + r.responseText); if (flags.last === false) { oriData += data; r.error(`flags.last: ${flags.last} , data.length: ${data.length}`); return; } else { r.error(`flags.last: ${flags.last}`); data = JSON.parse(oriData); r.error(`data.length: ${JSON.stringify(data).length}`); } // try { // 解析 JSON 响应 //let data = JSON.parse(body); // 检查是否存在 MediaSources 数组 if (data.MediaSources && Array.isArray(data.MediaSources)) { // 遍历 MediaSources 数组,替换 DirectStreamUrl 为 Path data.MediaSources.forEach(source => { if (source.Path && typeof source.Path === 'string') { //source.DirectStreamUrl = source.Path; ngx.shared.path_cache.set(source.Id, source.Path); let tmppath=ngx.shared.path_cache.get(source.Id); //r.error(`set cache item:${source.Id}=>${tmppath}`); } }); } // 返回修改后的 JSON //r.return(200, JSON.stringify(data)); // } catch (e) { // // 如果 JSON 解析失败,返回原始响应并记录错误 // r.warn('Failed to parse JSON: ' + e); // r.return(200, body); // } // r.error(`addUrldata.length: ${JSON.stringify(data).length}`); // // Remove Content-Length header delete r.headersOut['Content-Length']; // Set Transfer-Encoding to chunked r.headersOut['Transfer-Encoding'] = 'chunked'; r.sendBuffer(JSON.stringify(data), flags); //r.sendBuffer(JSON.stringify(data)); r.done(); } function cacheRedirect(r) { // Extract MediaSourceId from query parameters let mediaSourceId = r.variables.arg_MediaSourceId; if (!mediaSourceId) { r.return(400, "Missing MediaSourceId parameter"); return; } // Look up the path in the shared dictionary let cachedPath = ngx.shared.path_cache.get(mediaSourceId); if (cachedPath) { // Return 302 redirect to the cached path r.return(302, cachedPath); } else { // Return 404 if no path is found r.return(404, "No path found for MediaSourceId: " + mediaSourceId); } } export default { addExternalUrl, redirectUrl, HeaderFilter, rewritePlaybackInfo, cacheRedirect };