// ==UserScript==
// @name embyLaunchPotplayer
// @name:en embyLaunchPotplayer
// @name:zh embyLaunchPotplayer
// @name:zh-CN embyLaunchPotplayer
// @namespace http://tampermonkey.net/
// @version 1.1.16
// @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 = {
// 隐藏异构平台的播放器图标
hideByOS: true,
// 启用后将只显示图标,不显示文字
iconOnly: false,
// 图标来源,以下三选一,注释为只留一个,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 中
};
// 启用后将修改直接串流链接为真实文件名,方便第三方播放器友好显示和匹配,
// 默认不启用,强依赖 nginx-emby2Alist location two rewrite,如发现原始链接播放失败,请关闭此选项
const useRealFileName = false;
// 以下为内部使用变量,请勿更改
let isEmby = "";
function init() {
const playBtnsId = "ExternalPlayersBtns";
let playBtns = document.getElementById(playBtnsId);
if (playBtns) {
playBtns.remove();
}
let mainDetailButtons = document.querySelector("div[is='emby-scroller']:not(.hide) .mainDetailButtons");
const buttons = [
{ 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, }
];
function generateButtonHTML({ id, title, iconId }) {
return `
<button
id="${id}"
type="button"
class="detailButton emby-button emby-button-backdropfilter raised-backdropfilter detailButton-primary"
title="${title}"
>
<div class="detailButton-content">
<i class="md-icon detailButton-icon button-icon ${!iconConfig.iconOnly ? 'button-icon-left' : ''}"
id="${iconId}"> </i>
${!iconConfig.iconOnly ? `<span class="button-text">${title}</span>` : ''}
</div>
</button>
`;
}
let buttonHtml = `<div id="${playBtnsId}" class="detailButtons flex align-items-flex-start flex-wrap-wrap">`;
buttons.forEach(btn => {
if (!iconConfig.hideByOS || !btn.osCheck || btn.osCheck.some(check => check())) {
buttonHtml += generateButtonHTML(btn);
}
});
buttonHtml += `</div>`;
if (!isEmby) {
// jellfin
mainDetailButtons = document.querySelector("div.itemDetailPage:not(.hide) div.detailPagePrimaryContainer");
}
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
buttons.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};
`;
}
});
}
// 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() {
// itemMiscInfo-primary
// 评分,上映日期信息栏
let mediaInfoPrimary = document.querySelector("div[is='emby-scroller']:not(.hide) .mediaInfoPrimary:not(.hide)");
// 创建录制按钮
let btnManualRecording = document.querySelector("div[is='emby-scroller']:not(.hide) .btnManualRecording:not(.hide)");
if (!isEmby) {
mediaInfoPrimary = document.querySelector(".itemMiscInfo-primary:not(.hide)");
// 停止录制按钮
btnManualRecording = document.querySelector(".btnCancelTimer:not(.hide)");
}
return !!mediaInfoPrimary || !!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("div[is='emby-scroller']:not(.hide) select.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("div[is='emby-scroller']:not(.hide) select.selectSource:not([disabled])");
if (selectSource && selectSource.value.length > 0) {
mediaSourceId = selectSource.value;
}
// let selectAudio = document.querySelector("div[is='emby-scroller']:not(.hide) select.selectAudio:not([disabled])");
let mediaSource = itemInfo.MediaSources.find(m => m.Id == mediaSourceId);
let uri = isEmby ? "/emby/videos" : "/Items";
let domain = `${ApiClient._serverAddress}${uri}/${itemInfo.Id}`;
let subPath = getSubPath(mediaSource);
let subUrl = subPath.length > 0 ? `${domain}${subPath}?api_key=${ApiClient.accessToken()}` : '';
let streamUrl = `${domain}/`;
let fileName = mediaSource.IsInfiniteStream ? `master.m3u8` : mediaSource.Path.replace(/.*[\\/]/, "");
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=${ApiClient.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
: mediaSource.Path.split('/').pop();
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
};
}
// URL with "intent" scheme 只支持
// 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;
let potUrl = `potplayer://${encodeURI(mediaInfo.streamUrl)} /sub=${encodeURI(mediaInfo.subUrl)} /current /seek=${getSeek(intent.position)} /title="${intent.title}"`;
await writeClipboard(potUrl);
console.log("成功写入剪切板真实深度链接: ", potUrl);
// 测试出无空格也行,potplayer 对于 DeepLink 会自动转换为命令行参数,全量参数: PotPlayer 关于 => 命令行选项
potUrl = `potplayer:///current/clipboard`;
window.open(potUrl, "_self");
}
/**
* 这是一个临时解决方案,所以此段判断仅在 Google Chrome 浏览器下使用,区别 {brand: 'Microsoft Edge', version: '130'}
* 非 Chrome 内核无 userAgentData 对象, Chrome 内核套壳的没添加 brands 品牌元素
*/
// function geGoogleChrome130() {
// if (!navigator.userAgentData) { return false; }
// const googleBrand = navigator.userAgentData.brands.find(b => b.brand === "Google Chrome");
// if (!googleBrand) { return false; }
// return parseInt(googleBrand.version) >= 130;
// }
// 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()) {
// 桌面端需要额外设置,参考这个项目: 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");
}
// 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");
}
// MPV
async function embyMPV() {
let mediaInfo = await getEmbyMediaInfo();
//桌面端需要额外设置,使用这个项目: 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)}`;
}
console.log(MPVUrl);
window.open(MPVUrl, "_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");
}
async function embyCopyUrl() {
const mediaInfo = await getEmbyMediaInfo();
const streamUrl = encodeURI(mediaInfo.streamUrl);
if (await writeClipboard(streamUrl)) {
console.log(`decodeURI for show copyUrl = ${mediaInfo.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;
}
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()),
};
// 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,
})
}
});
})();