更新文件: runwebdavsim.sh, externalPlayer.js, install.sh, runembysim.sh, embysim.tar.xz
This commit is contained in:
BIN
embysim.tar.xz
BIN
embysim.tar.xz
Binary file not shown.
700
externalPlayer.js
Normal file
700
externalPlayer.js
Normal file
@ -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 `
|
||||
<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://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,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
@ -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"
|
||||
|
||||
|
@ -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|<script src=\"externalPlayer.js\" defer></script></body>|</body>|g' \"/system/dashboard-ui/index.html\";sed -i 's|</body>|<script src=\"externalPlayer.js\" defer></script></body>|g' \"/system/dashboard-ui/index.html\""
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user