您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
emby/jellfin launch extetnal player
// ==UserScript== // @name embyLaunchPotplayer // @name:en embyLaunchPotplayer // @name:zh embyLaunchPotplayer // @name:zh-CN embyLaunchPotplayer // @namespace http://tampermonkey.net/ // @version 1.1.22 // @description emby/jellfin launch extetnal player // @description:zh-cn emby/jellfin 调用外部播放器 // @description:en emby/jellfin to external player // @license MIT // @author @bpking // @github https://github.com/bpking1/embyExternalUrl // @match *://*/web/index.html // @match *://*/web/ // ==/UserScript== (function () { 'use strict'; const iconConfig = { // 图标来源,以下三选一,注释为只留一个,3 的优先级最高 // 1.add icons from jsdelivr, network baseUrl: "https://emby-external-url.7o7o.cc/embyWebAddExternalUrl/icons", // baseUrl: "https://fastly.jsdelivr.net/gh/bpking1/embyExternalUrl@main/embyWebAddExternalUrl/icons", // 2.server local icons, same as /emby-server/system/dashboard-ui/icons // baseUrl: "icons", // 3.add icons from Base64, script inner, this script size 22.5KB to 74KB, // 自行复制 ./iconsExt.js 内容到此脚本的 getIconsExt 中 // 移除最后几个冗余的自定义开关 removeCustomBtns: false, }; // 启用后将修改直接串流链接为真实文件名,方便第三方播放器友好显示和匹配, // 默认不启用,强依赖 nginx-emby2Alist location two rewrite,如发现原始链接播放失败,请关闭此选项 const useRealFileName = false; // 以下为内部使用变量,请勿更改 let isEmby = ""; const mark = "embyLaunchPotplayer"; const playBtnsWrapperId = "ExternalPlayersBtns"; const lsKeys = { iconOnly: `${mark}-iconOnly`, hideByOS: `${mark}-hideByOS`, notCurrentPot: `${mark}-notCurrentPot`, strmDirect: `${mark}-strmDirect`, }; const OS = { isAndroid: () => /android/i.test(navigator.userAgent), isIOS: () => /iPad|iPhone|iPod/i.test(navigator.userAgent), isMacOS: () => /Macintosh|MacIntel/i.test(navigator.userAgent), isApple: () => OS.isMacOS() || OS.isIOS(), isWindows: () => /compatible|Windows/i.test(navigator.userAgent), isMobile: () => OS.isAndroid() || OS.isIOS(), isUbuntu: () => /Ubuntu/i.test(navigator.userAgent), // isAndroidEmbyNoisyX: () => OS.isAndroid() && ApiClient.appVersion().includes('-'), // isEmbyNoisyX: () => ApiClient.appVersion().includes('-'), isOthers: () => Object.entries(OS).filter(([key, val]) => key !== 'isOthers').every(([key, val]) => !val()), }; const playBtns = [ { id: "embyPot", title: "Potplayer", iconId: "icon-PotPlayer" , onClick: embyPot, osCheck: [OS.isWindows], }, { id: "embyVlc", title: "VLC", iconId: "icon-VLC", onClick: embyVlc, }, { id: "embyIINA", title: "IINA", iconId: "icon-IINA" , onClick: embyIINA, osCheck: [OS.isMacOS], }, { id: "embyNPlayer", title: "NPlayer", iconId: "icon-NPlayer", onClick: embyNPlayer, }, { id: "embyMX", title: "MXPlayer", iconId: "icon-MXPlayer" , onClick: embyMX, osCheck: [OS.isAndroid], }, { id: "embyMXPro", title: "MXPlayerPro", iconId: "icon-MXPlayerPro" , onClick: embyMXPro, osCheck: [OS.isAndroid], }, { id: "embyInfuse", title: "Infuse", iconId: "icon-infuse" , onClick: embyInfuse, osCheck: [OS.isApple], }, { id: "embyStellarPlayer", title: "恒星播放器", iconId: "icon-StellarPlayer" , onClick: embyStellarPlayer, osCheck: [OS.isWindows, OS.isMacOS, OS.isAndroid], }, { id: "embyMPV", title: "MPV", iconId: "icon-MPV", onClick: embyMPV, }, { id: "embyDDPlay", title: "弹弹Play", iconId: "icon-DDPlay" , onClick: embyDDPlay, osCheck: [OS.isWindows, OS.isAndroid], }, { id: "embyFileball", title: "Fileball", iconId: "icon-Fileball" , onClick: embyFileball, osCheck: [OS.isApple], }, { id: "embyOmniPlayer", title: "OmniPlayer", iconId: "icon-OmniPlayer" , onClick: embyOmniPlayer, osCheck: [OS.isMacOS], }, { id: "embyFigPlayer", title: "FigPlayer", iconId: "icon-FigPlayer" , onClick: embyFigPlayer, osCheck: [OS.isMacOS], }, { id: "embySenPlayer", title: "SenPlayer", iconId: "icon-SenPlayer" , onClick: embySenPlayer, osCheck: [OS.isIOS], }, { id: "embyCopyUrl", title: "复制串流地址", iconId: "icon-Copy", onClick: embyCopyUrl, }, ]; // Jellyfin Icons: https://marella.github.io/material-icons/demo // Emby Icons: https://fonts.google.com/icons const customBtns = [ { id: "hideByOS", title: "异构播放器", iconName: "more", onClick: hideByOSHandler, }, { id: "iconOnly", title: "显示模式", iconName: "open_in_full", onClick: iconOnlyHandler, }, { id: "notCurrentPot", title: "多开Potplayer", iconName: "window", onClick: notCurrentPotHandler, }, { id: "strmDirect", title: "STRM直通", desc: "AList注意关sign,否则不要开启此选项,任然由服务端处理sign" , iconName: "link", onClick: strmDirectHandler, }, ]; if (!iconConfig.removeCustomBtns) { playBtns.push(...customBtns); } const fileNameReg = /.*[\\/]|(\?.*)?$/g; const selectors = { // 详情页评分,上映日期信息栏 embyMediaInfoDiv: "div[is='emby-scroller']:not(.hide) .mediaInfo:not(.hide)", jellfinMediaInfoDiv: ".itemMiscInfo-primary:not(.hide)", // 电视直播详情页创建录制按钮 embyBtnManualRecording: "div[is='emby-scroller']:not(.hide) .btnManualRecording:not(.hide)", // 电视直播详情页停止录制按钮 jellfinBtnCancelTimer: ".btnCancelTimer:not(.hide)", // 详情页播放收藏那排按钮 embyMainDetailButtons: "div[is='emby-scroller']:not(.hide) .mainDetailButtons", jellfinMainDetailButtons: "div.itemDetailPage:not(.hide) div.detailPagePrimaryContainer", // 详情页字幕选择下拉框 selectSubtitles: "div[is='emby-scroller']:not(.hide) select.selectSubtitles", // 详情页多版本选择下拉框 selectSource: "div[is='emby-scroller']:not(.hide) select.selectSource:not([disabled])", }; function init() { let playBtnsWrapper = document.getElementById(playBtnsWrapperId); if (playBtnsWrapper) { playBtnsWrapper.remove(); } let mainDetailButtons = document.querySelector(selectors.embyMainDetailButtons); function generateButtonHTML({ id, title, desc, iconId, iconName }) { // jellyfin icon class: material-icons return ` <button id="${id}" type="button" class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary" title="${desc ? desc : title}" > <div class="detailButton-content"> <i class="md-icon detailButton-icon button-icon button-icon-left material-icons" id="${iconId}"> ${iconName ? iconName : ' '} </i> <span class="button-text">${title}</span> </div> </button> `; } let buttonHtml = `<div id="${playBtnsWrapperId}" class="detailButtons flex align-items-flex-start flex-wrap-wrap detail-lineItem">`; playBtns.forEach(btn => { buttonHtml += generateButtonHTML(btn); }); buttonHtml += `</div>`; if (!isEmby) { // jellfin mainDetailButtons = document.querySelector(selectors.jellfinMainDetailButtons); } mainDetailButtons.insertAdjacentHTML("afterend", buttonHtml); if (!isEmby) { // jellfin add class, detailPagePrimaryContainer、button-flat let playBtnsWrapper = document.getElementById("ExternalPlayersBtns"); // style to cover .layout-mobile playBtnsWrapper.style.display = "flex"; // playBtnsWrapper.style["justifyContent"] = "center"; playBtnsWrapper.classList.add("detailPagePrimaryContainer"); let btns = playBtnsWrapper.getElementsByTagName("button"); for (let i = 0; i < btns.length; i++) { btns[i].classList.add("button-flat"); } } // add event playBtns.forEach(btn => { const btnEle = document.querySelector(`#${btn.id}`); if (btnEle) { btnEle.onclick = btn.onClick; } }); const iconBaseUrl = iconConfig.baseUrl; const icons = [ // if url exists, use url property, if id diff icon name, use name property { id: "icon-PotPlayer", name: "icon-PotPlayer.webp", fontSize: "1.4em" }, { id: "icon-VLC", fontSize: "1.3em" }, { id: "icon-IINA", fontSize: "1.4em" }, { id: "icon-NPlayer", fontSize: "1.3em" }, { id: "icon-MXPlayer", fontSize: "1.4em" }, { id: "icon-MXPlayerPro", fontSize: "1.4em" }, { id: "icon-infuse", fontSize: "1.4em" }, { id: "icon-StellarPlayer", fontSize: "1.4em" }, { id: "icon-MPV", fontSize: "1.4em" }, { id: "icon-DDPlay", fontSize: "1.4em" }, { id: "icon-Fileball", fontSize: "1.4em" }, { id: "icon-SenPlayer", fontSize: "1.4em" }, { id: "icon-OmniPlayer", fontSize: "1.4em" }, { id: "icon-FigPlayer", fontSize: "1.4em" }, { id: "icon-Copy", fontSize: "1.4em" }, ]; const iconsExt = getIconsExt(); icons.map((icon, index) => { const element = document.querySelector(`#${icon.id}`); if (element) { // if url exists, use url property, if id diff icon name, use name property icon.url = typeof iconsExt !== 'undefined' && iconsExt && iconsExt[index] ? iconsExt[index].url : undefined; const url = icon.url || `${iconBaseUrl}/${icon.name || `${icon.id}.webp`}`; element.style.cssText += ` background-image: url(${url}); background-repeat: no-repeat; background-size: 100% 100%; font-size: ${icon.fontSize}; `; } }); if (!iconConfig.removeCustomBtns) { hideByOSHandler(); iconOnlyHandler(); notCurrentPotHandler(); strmDirectHandler(); } } // copy from ./iconsExt,如果更改了以下内容,请同步更改 ./iconsExt.js function getIconsExt() { // base64 data total size 72.5 KB from embyWebAddExternalUrl/icons/min, sync modify const iconsExt = []; return iconsExt; } function showFlag() { let mediaInfoDiv = document.querySelector(selectors.embyMediaInfoDiv); let btnManualRecording = document.querySelector(selectors.embyBtnManualRecording); if (!isEmby) { mediaInfoDiv = document.querySelector(selectors.jellfinMediaInfoDiv); btnManualRecording = document.querySelector(selectors.jellfinBtnCancelTimer); } return !!mediaInfoDiv || !!btnManualRecording; } async function getItemInfo() { let userId = ApiClient._serverInfo.UserId; let itemId = /\?id=([A-Za-z0-9]+)/.exec(window.location.hash)[1]; let response = await ApiClient.getItem(userId, itemId); // 继续播放当前剧集的下一集 if (response.Type == "Series") { let seriesNextUpItems = await ApiClient.getNextUpEpisodes({ SeriesId: itemId, UserId: userId }); if (seriesNextUpItems.Items.length > 0) { console.log("nextUpItemId: " + seriesNextUpItems.Items[0].Id); return await ApiClient.getItem(userId, seriesNextUpItems.Items[0].Id); } } // 播放当前季season的第一集 if (response.Type == "Season") { let seasonItems = await ApiClient.getItems(userId, { parentId: itemId }); console.log("seasonItemId: " + seasonItems.Items[0].Id); return await ApiClient.getItem(userId, seasonItems.Items[0].Id); } // 播放当前集或电影 if (response.MediaSources?.length > 0) { console.log("itemId: " + itemId); return response; } // 默认播放第一个,集/播放列表第一个媒体 let firstItems = await ApiClient.getItems(userId, { parentId: itemId, Recursive: true, IsFolder: false, Limit: 1 }); console.log("firstItemId: " + firstItems.Items[0].Id); return await ApiClient.getItem(userId, firstItems.Items[0].Id); } function 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(":") } function getSubPath(mediaSource) { let selectSubtitles = document.querySelector(selectors.selectSubtitles); let subTitlePath = ''; //返回选中的外挂字幕 if (selectSubtitles && selectSubtitles.value > 0) { let SubIndex = mediaSource.MediaStreams.findIndex(m => m.Index == selectSubtitles.value && m.IsExternal); if (SubIndex > -1) { let subtitleCodec = mediaSource.MediaStreams[SubIndex].Codec; subTitlePath = `/${mediaSource.Id}/Subtitles/${selectSubtitles.value}/Stream.${subtitleCodec}`; } } else { //默认尝试返回第一个外挂中文字幕 let chiSubIndex = mediaSource.MediaStreams.findIndex(m => m.Language == "chi" && m.IsExternal); if (chiSubIndex > -1) { let subtitleCodec = mediaSource.MediaStreams[chiSubIndex].Codec; subTitlePath = `/${mediaSource.Id}/Subtitles/${chiSubIndex}/Stream.${subtitleCodec}`; } else { //尝试返回第一个外挂字幕 let externalSubIndex = mediaSource.MediaStreams.findIndex(m => m.IsExternal); if (externalSubIndex > -1) { let subtitleCodec = mediaSource.MediaStreams[externalSubIndex].Codec; subTitlePath = `/${mediaSource.Id}/Subtitles/${externalSubIndex}/Stream.${subtitleCodec}`; } } } return subTitlePath; } async function getEmbyMediaInfo() { let itemInfo = await getItemInfo(); let mediaSourceId = itemInfo.MediaSources[0].Id; let selectSource = document.querySelector(selectors.selectSource); if (selectSource && selectSource.value.length > 0) { mediaSourceId = selectSource.value; } // let selectAudio = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectAudio:not([disabled])"); const accessToken = ApiClient.accessToken(); let mediaSource = itemInfo.MediaSources.find(m => m.Id == mediaSourceId); let uri = isEmby ? "/emby/videos" : "/Items"; let baseUrl = `${ApiClient._serverAddress}${uri}/${itemInfo.Id}`; let subPath = getSubPath(mediaSource); let subUrl = subPath.length > 0 ? `${baseUrl}${subPath}?api_key=${accessToken}` : ""; let streamUrl = `${baseUrl}/`; if (mediaSource.Path.startsWith("http") && localStorage.getItem(lsKeys.strmDirect) === "1") { streamUrl = decodeURIComponent(mediaSource.Path); } else { let fileName = mediaSource.IsInfiniteStream ? `master.m3u8` : decodeURIComponent(mediaSource.Path.replace(fileNameReg, "")); if (isEmby) { if (mediaSource.IsInfiniteStream) { streamUrl += useRealFileName && mediaSource.Name ? `${mediaSource.Name}.m3u8` : fileName; } else { // origin link: /emby/videos/401929/stream.xxx?xxx // modify link: /emby/videos/401929/stream/xxx.xxx?xxx // this is not important, hit "/emby/videos/401929/" path level still worked streamUrl += useRealFileName ? `stream/${fileName}` : `stream.${mediaSource.Container}`; } } else { streamUrl += `Download`; streamUrl += useRealFileName ? `/${fileName}` : ""; } streamUrl += `?api_key=${accessToken}&Static=true&MediaSourceId=${mediaSourceId}&DeviceId=${ApiClient._deviceId}`; } let position = parseInt(itemInfo.UserData.PlaybackPositionTicks / 10000); let intent = await getIntent(mediaSource, position); console.log(streamUrl, subUrl, intent); return { streamUrl: streamUrl, subUrl: subUrl, intent: intent, } } async function getIntent(mediaSource, position) { // 直播节目查询items接口没有path let title = mediaSource.IsInfiniteStream ? mediaSource.Name : decodeURIComponent(mediaSource.Path.replace(fileNameReg, "")); let externalSubs = mediaSource.MediaStreams.filter(m => m.IsExternal == true); let subs = ''; // 要求是android.net.uri[] ? let subs_name = ''; let subs_filename = ''; let subs_enable = ''; if (externalSubs) { subs_name = externalSubs.map(s => s.DisplayTitle); subs_filename = externalSubs.map(s => s.Path.split('/').pop()); } return { title: title, position: position, subs: subs, subs_name: subs_name, subs_filename: subs_filename, subs_enable: subs_enable, path: mediaSource.Path, }; } // URL with "intent" scheme only support // String => 'S' // Boolean =>'B' // Byte => 'b' // Character => 'c' // Double => 'd' // Float => 'f' // Integer => 'i' // Long => 'l' // Short => 's' async function embyPot() { const mediaInfo = await getEmbyMediaInfo(); const intent = mediaInfo.intent; const notCurrentPotArg = localStorage.getItem(lsKeys.notCurrentPot) === "1" ? "" : "/current"; let potUrl = `potplayer://${encodeURI(mediaInfo.streamUrl)} /sub=${encodeURI(mediaInfo.subUrl)} ${notCurrentPotArg} /seek=${getSeek(intent.position)} /title="${intent.title}"`; await writeClipboard(potUrl); console.log("成功写入剪切板真实深度链接: ", potUrl); // 测试出无空格也行,potplayer 对于 DeepLink 会自动转换为命令行参数,全量参数: PotPlayer 关于 => 命令行选项 potUrl = `potplayer://${notCurrentPotArg}/clipboard`; window.open(potUrl, "_self"); } // async function embyPot() { // let mediaInfo = await getEmbyMediaInfo(); // let intent = mediaInfo.intent; // let potUrl = `potplayer://${encodeURI(mediaInfo.streamUrl)} /sub=${encodeURI(mediaInfo.subUrl)} /current /seek=${getSeek(intent.position)}`; // potUrl += useRealFileName ? '' : ` /title="${intent.title}"`; // console.log(potUrl); // window.open(potUrl, "_self"); // } // https://wiki.videolan.org/Android_Player_Intents/ async function embyVlc() { let mediaInfo = await getEmbyMediaInfo(); let intent = mediaInfo.intent; // android subtitles: https://code.videolan.org/videolan/vlc-android/-/issues/1903 let vlcUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=org.videolan.vlc;type=video/*;S.subtitles_location=${encodeURI(mediaInfo.subUrl)};S.title=${encodeURI(intent.title)};i.position=${intent.position};end`; if (OS.isWindows() || OS.isMacOS()) { // 桌面端需要额外设置,参考这个项目: // new: https://github.com/northsea4/vlc-protocol // old: https://github.com/stefansundin/vlc-protocol vlcUrl = `vlc://${encodeURI(mediaInfo.streamUrl)}`; } if (OS.isIOS()) { // https://wiki.videolan.org/Documentation:IOS/#x-callback-url // https://code.videolan.org/videolan/vlc-ios/-/commit/55e27ed69e2fce7d87c47c9342f8889fda356aa9 vlcUrl = `vlc-x-callback://x-callback-url/stream?url=${encodeURIComponent(mediaInfo.streamUrl)}&sub=${encodeURIComponent(mediaInfo.subUrl)}`; } console.log(vlcUrl); window.open(vlcUrl, "_self"); } // MPV async function embyMPV() { let mediaInfo = await getEmbyMediaInfo(); // 桌面端需要额外设置,参考这个项目: // new: https://github.com/northsea4/mpvplay-protocol // old: https://github.com/akiirui/mpv-handler let streamUrl64 = btoa(String.fromCharCode.apply(null, new Uint8Array(new TextEncoder().encode(mediaInfo.streamUrl)))) .replace(/\//g, "_").replace(/\+/g, "-").replace(/\=/g, ""); let MPVUrl = `mpv://play/${streamUrl64}`; if (mediaInfo.subUrl.length > 0) { let subUrl64 = btoa(mediaInfo.subUrl).replace(/\//g, "_").replace(/\+/g, "-").replace(/\=/g, ""); MPVUrl = `mpv://play/${streamUrl64}/?subfile=${subUrl64}`; } if (OS.isIOS() || OS.isAndroid()) { MPVUrl = `mpv://${encodeURI(mediaInfo.streamUrl)}`; } if (OS.isMacOS()) { MPVUrl = `mpvplay://${encodeURI(mediaInfo.streamUrl)}`; } console.log(MPVUrl); window.open(MPVUrl, "_self"); } // https://github.com/iina/iina/issues/1991 async function embyIINA() { let mediaInfo = await getEmbyMediaInfo(); let iinaUrl = `iina://weblink?url=${encodeURIComponent(mediaInfo.streamUrl)}&new_window=1`; console.log(`iinaUrl= ${iinaUrl}`); window.open(iinaUrl, "_self"); } // https://sites.google.com/site/mxvpen/api // https://mx.j2inter.com/api // https://support.mxplayer.in/support/solutions/folders/43000574903 async function embyMX() { const mediaInfo = await getEmbyMediaInfo(); const intent = mediaInfo.intent; // mxPlayer free const packageName = "com.mxtech.videoplayer.ad"; const url = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=${packageName};S.title=${encodeURI(intent.title)};i.position=${intent.position};end`; console.log(url); window.open(url, "_self"); } async function embyMXPro() { const mediaInfo = await getEmbyMediaInfo(); const intent = mediaInfo.intent; // mxPlayer Pro const packageName = "com.mxtech.videoplayer.pro"; const url = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=${packageName};S.title=${encodeURI(intent.title)};i.position=${intent.position};end`; console.log(url); window.open(url, "_self"); } async function embyNPlayer() { let mediaInfo = await getEmbyMediaInfo(); let nUrl = OS.isMacOS() ? `nplayer-mac://weblink?url=${encodeURIComponent(mediaInfo.streamUrl)}&new_window=1` : `nplayer-${encodeURI(mediaInfo.streamUrl)}`; console.log(nUrl); window.open(nUrl, "_self"); } async function embyInfuse() { let mediaInfo = await getEmbyMediaInfo(); // sub 参数限制: 播放带有外挂字幕的单个视频文件(Infuse 7.6.2 及以上版本) // see: https://support.firecore.com/hc/zh-cn/articles/215090997 let infuseUrl = `infuse://x-callback-url/play?url=${encodeURIComponent(mediaInfo.streamUrl)}&sub=${encodeURIComponent(mediaInfo.subUrl)}`; console.log(`infuseUrl= ${infuseUrl}`); window.open(infuseUrl, "_self"); } // StellarPlayer async function embyStellarPlayer() { let mediaInfo = await getEmbyMediaInfo(); let stellarPlayerUrl = `stellar://play/${encodeURI(mediaInfo.streamUrl)}`; console.log(`stellarPlayerUrl= ${stellarPlayerUrl}`); window.open(stellarPlayerUrl, "_self"); } // see https://gf.qytechs.cn/zh-CN/scripts/443916 async function embyDDPlay() { // 检查是否windows本地路径 const fullPathEle = document.querySelector(".mediaSources .mediaSource .sectionTitle > div:not([class]):first-child"); let fullPath = fullPathEle ? fullPathEle.innerText : ""; let ddplayUrl; if (new RegExp('^[a-zA-Z]:').test(fullPath)) { ddplayUrl = `ddplay:${encodeURIComponent(fullPath)}`; } else { console.log("文件路径不是本地路径,将使用串流播放"); const mediaInfo = await getEmbyMediaInfo(); const intent = mediaInfo.intent; if (!fullPath) { fullPath = intent.title; } const urlPart = mediaInfo.streamUrl + `|filePath=${fullPath}`; ddplayUrl = `ddplay:${encodeURIComponent(urlPart)}`; if (OS.isAndroid()) { // Subtitles Not Supported: https://github.com/kaedei/dandanplay-libraryindex/blob/master/api/ClientProtocol.md ddplayUrl = `intent:${encodeURI(urlPart)}#Intent;package=com.xyoye.dandanplay;type=video/*;end`; } } console.log(`ddplayUrl= ${ddplayUrl}`); window.open(ddplayUrl, "_self"); } async function embyFileball() { const mediaInfo = await getEmbyMediaInfo(); // see: app 关于, URL Schemes const url = `filebox://play?url=${encodeURIComponent(mediaInfo.streamUrl)}`; console.log(`FileballUrl= ${url}`); window.open(url, "_self"); } async function embyOmniPlayer() { const mediaInfo = await getEmbyMediaInfo(); // see: https://github.com/AlistGo/alist-web/blob/main/src/pages/home/previews/video_box.tsx const url = `omniplayer://weblink?url=${encodeURIComponent(mediaInfo.streamUrl)}`; console.log(`OmniPlayerUrl= ${url}`); window.open(url, "_self"); } async function embyFigPlayer() { const mediaInfo = await getEmbyMediaInfo(); // see: https://github.com/AlistGo/alist-web/blob/main/src/pages/home/previews/video_box.tsx const url = `figplayer://weblink?url=${encodeURIComponent(mediaInfo.streamUrl)}`; console.log(`FigPlayerUrl= ${url}`); window.open(url, "_self"); } async function embySenPlayer() { const mediaInfo = await getEmbyMediaInfo(); // see: app 关于, URL Schemes const url = `SenPlayer://x-callback-url/play?url=${encodeURIComponent(mediaInfo.streamUrl)}`; console.log(`SenPlayerUrl= ${url}`); window.open(url, "_self"); } function lsCheckSetBoolean(event, lsKeyName) { let flag = localStorage.getItem(lsKeyName) === "1"; if (event) { flag = !flag; localStorage.setItem(lsKeyName, flag ? "1" : "0"); } return flag; } function hideByOSHandler(event) { const btn = document.getElementById("hideByOS"); if (!btn) { return; } const flag = lsCheckSetBoolean(event, lsKeys.hideByOS); const playBtnsWrapper = document.getElementById(playBtnsWrapperId); const buttonEleArr = playBtnsWrapper.querySelectorAll("button"); buttonEleArr.forEach(btnEle => { const btn = playBtns.find(btn => btn.id === btnEle.id); const shouldHide = flag && btn.osCheck && !btn.osCheck.some(check => check()); console.log(`${btn.id} Should Hide: ${shouldHide}`); btnEle.style.display = shouldHide ? 'none' : 'block'; }); btn.classList.toggle("button-submit", flag); } function iconOnlyHandler(event) { const btn = document.getElementById("iconOnly"); if (!btn) { return; } const flag = lsCheckSetBoolean(event, lsKeys.iconOnly); const playBtnsWrapper = document.getElementById(playBtnsWrapperId); const spans = playBtnsWrapper.querySelectorAll("span"); spans.forEach(span => { span.hidden = flag; }); const iArr = playBtnsWrapper.querySelectorAll("i"); iArr.forEach(iEle => { iEle.classList.toggle("button-icon-left", !flag); }); btn.classList.toggle("button-submit", flag); } function notCurrentPotHandler(event) { const btn = document.getElementById("notCurrentPot"); if (!btn) { return; } const flag = lsCheckSetBoolean(event, lsKeys.notCurrentPot); btn.classList.toggle("button-submit", flag); } function strmDirectHandler(event) { const btn = document.getElementById("strmDirect"); if (!btn) { return; } const flag = lsCheckSetBoolean(event, lsKeys.strmDirect); btn.classList.toggle("button-submit", flag); } async function embyCopyUrl() { const mediaInfo = await getEmbyMediaInfo(); const streamUrl = encodeURI(mediaInfo.streamUrl); if (await writeClipboard(streamUrl)) { console.log(`copyUrl = ${streamUrl}`); this.innerText = '复制成功'; } } async function writeClipboard(text) { let flag = false; if (navigator.clipboard) { // 火狐上 need https try { await navigator.clipboard.writeText(text); flag = true; console.log("成功使用 navigator.clipboard 现代剪切板实现"); } catch (error) { console.error('navigator.clipboard 复制到剪贴板时发生错误:', error); } } else { flag = writeClipboardLegacy(text); console.log("不存在 navigator.clipboard 现代剪切板实现,使用旧版实现"); } return flag; } function writeClipboardLegacy(text) { let textarea = document.createElement('textarea'); document.body.appendChild(textarea); textarea.style.position = 'absolute'; textarea.style.clip = 'rect(0 0 0 0)'; textarea.value = text; textarea.select(); if (document.execCommand('copy', true)) { return true; } return false; } // emby/jellyfin CustomEvent // see: https://github.com/MediaBrowser/emby-web-defaultskin/blob/822273018b82a4c63c2df7618020fb837656868d/nowplaying/videoosd.js#L691 // monitor dom changements document.addEventListener("viewbeforeshow", function (e) { console.log("viewbeforeshow", e); if (isEmby === "") { isEmby = !!e.detail.contextPath; } let isItemDetailPage; if (isEmby) { isItemDetailPage = e.detail.contextPath.startsWith("/item?id="); } else { isItemDetailPage = e.detail.params && e.detail.params.id; } if (isItemDetailPage) { const mutation = new MutationObserver(function() { if (showFlag()) { init(); mutation.disconnect(); } }) mutation.observe(document.body, { childList: true, characterData: true, subtree: true, }) } }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址