diff --git a/embysim.tar.xz b/embysim.tar.xz index 753eb57..54ccd35 100644 Binary files a/embysim.tar.xz and b/embysim.tar.xz differ diff --git a/externalPlayer.js b/externalPlayer.js new file mode 100644 index 0000000..9be02c8 --- /dev/null +++ b/externalPlayer.js @@ -0,0 +1,700 @@ +// ==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 ` + + `; + } + let buttonHtml = `
`; + playBtns.forEach(btn => { + buttonHtml += generateButtonHTML(btn); + }); + buttonHtml += `
`; + + 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://greasyfork.org/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, + }) + } + }); + +})(); diff --git a/install.sh b/install.sh index de05896..b7b6a8e 100644 --- a/install.sh +++ b/install.sh @@ -47,8 +47,8 @@ if [ ! -e emby.config/config/system.xml ]; then fi echo "copy scripts..." -cp -a ../webdav_simulator.amd64 ../htdocs ../runwebdavsim.sh ../killwebdavsim.sh ../tmuxrunwebdavsim.sh ../attach.sh ../rclonemount.sh ../runembysim.sh ../rclone.conf ../xy*.txt.xz . -rm -f ../webdav_simulator.amd64 ../htdocs ../runwebdavsim.sh ../killwebdavsim.sh ../tmuxrunwebdavsim.sh ../attach.sh ../rclonemount.sh ../runembysim.sh ../rclone.conf ../xy*.txt.xz +cp -a ../webdav_simulator.amd64 ../htdocs ../runwebdavsim.sh ../killwebdavsim.sh ../tmuxrunwebdavsim.sh ../attach.sh ../rclonemount.sh ../runembysim.sh ../rclone.conf ../xy*.txt.xz ../externalPlayer.js . +rm -f ../webdav_simulator.amd64 ../htdocs ../runwebdavsim.sh ../killwebdavsim.sh ../tmuxrunwebdavsim.sh ../attach.sh ../rclonemount.sh ../runembysim.sh ../rclone.conf ../xy*.txt.xz ../externalPlayer.js chmod u+x *.sh echo "copy scripts finish" diff --git a/runembysim.sh b/runembysim.sh index a6cf0e0..0c40e4d 100644 --- a/runembysim.sh +++ b/runembysim.sh @@ -16,6 +16,15 @@ progdir=`dirname "${prog}"` cd "${progdir}" progdir=$(pwd) +# 获取当前脚本的名称 +SCRIPT_NAME=$(basename "$0") + +# 检查是否有相同的脚本在运行(排除当前进程) +if pgrep -f "$SCRIPT_NAME" | grep -v "$$" > /dev/null; then + echo "脚本已在运行,退出..." + exit 1 +fi + mkdir -p emby.tmp emby.cache >/dev/null 2>&1 chmod 777 emby.tmp emby.cache @@ -104,3 +113,7 @@ docker run -d \ emby/embyserver:latest #pandagroove/embysim:1.0.0 + + +docker cp "externalPlayer.js" "embysim":"/system/dashboard-ui/externalPlayer.js" +docker exec "embysim" sh -c "sed -i -e 's|||g' \"/system/dashboard-ui/index.html\";sed -i 's|||g' \"/system/dashboard-ui/index.html\"" diff --git a/runwebdavsim.sh b/runwebdavsim.sh index b95094f..5fa2ee5 100644 --- a/runwebdavsim.sh +++ b/runwebdavsim.sh @@ -16,6 +16,15 @@ progdir=`dirname "${prog}"` cd "${progdir}" progdir=$(pwd) +# 获取当前脚本的名称 +SCRIPT_NAME=$(basename "$0") + +# 检查是否有相同的脚本在运行(排除当前进程) +if pgrep -f "$SCRIPT_NAME" | grep -v "$$" > /dev/null; then + echo "脚本已在运行,退出..." + exit 1 +fi + if [ "$SELFIP" == "" ]; then interface=$(ip route show | grep default | awk '{print $5}') export SELFIP=$(ip addr show $interface | grep -w inet | awk '{print $2}' | cut -d/ -f1|head -1) @@ -40,7 +49,7 @@ while true do curl -v -L "http://127.0.0.1:${WEBDAV_PORT}/dav" if [ $? -ne 0 ]; then - ./webdav_simulator.$arch --selfip $SELFIP --port ${WEBDAV_PORT} --noindex --alist_config alistservers.txt --username guest --password guest_Api789 --proxymode 1 --blackwords '抖音,短剧,音乐22,mp3,wav' --fake_media_file fake.mkv --strmmode 'xy115-all.txt.xz#xy-dy.txt.xz#xy-dsj.txt.xz#xy115-music.txt.xz' + ./webdav_simulator.$arch --selfip $SELFIP --port ${WEBDAV_PORT} --noindex --alist_config alistservers.txt --username guest --password guest_Api789 --proxymode 1 --blackwords '抖音,短剧,音乐22,mp3,wav,flac,BACKUP,BDMV,txt' --fake_media_file fake.mkv --strmmode 'xy115-all.txt.xz#xy-dy.txt.xz#xy-dsj.txt.xz#xy115-music.txt.xz' echo "webdavsim exit, sleep 5s and restart" else break