- // ==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,
- })
- }
- });
-
- })();