Greasy Fork

Greasy Fork is available in English.

jellyfinLaunchPotplayerHideButton

jellyfin launch external player hide play button

当前为 2025-07-05 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         jellyfinLaunchPotplayerHideButton
// @name:en      jellyfinLaunchPotplayerHideButton
// @name:zh      jellyfinLaunchPotplayerHideButton
// @name:zh-CN   jellyfinLaunchPotplayerHideButton
// @namespace    http://tampermonkey.net/
// @version      1.0.3
// @description jellyfin launch external player hide play button
// @description:en  jellyfin launch external player hide play button
// @description:zh-cn   jellyfin调用外部播放器,隐藏多余的播放按钮
// @license      MIT
// @author       @Myisking
// @include      */web/index.html
// ==/UserScript==

(function () {
    'use strict';
    const style = document.createElement('style');
    style.type = 'text/css';
    style.innerHTML = `
        .player-buttons-container {
            display: flex;
            overflow-x: auto;
            scrollbar-width: none;
            -ms-overflow-style: none;
            padding-bottom: 8px;
        }
        .player-buttons-container::-webkit-scrollbar {
            display: none;
        }
        .player-buttons-wrapper {
            display: flex;
            flex-wrap: nowrap;
            min-width: min-content;
            gap: 6px;
        }
        /* 更多按钮样式 */
        .more-button-wrapper {
            position: relative;
            display: inline-block;
        }
        .more-button {
            background: #2b2b2b;
            border: 1px solid #444;
            color: #ddd;
            padding: 5px 15px;
            border-radius: 4px;
            cursor: pointer;
        }
        .more-dropdown {
            display: none;
            position: absolute;
            top: 100%;
            right: 0;
            background: #333;
            border: 1px solid #444;
            border-radius: 4px;
            z-index: 100;
            min-width: 120px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.5);
        }
        .more-dropdown button {
            display: block;
            width: 100%;
            text-align: left;
            padding: 8px 12px;
            background: none;
            border: none;
            color: #ddd;
            cursor: pointer;
            white-space: nowrap;
        }
        .more-dropdown button:hover {
            background: #444;
        }
        .more-button-wrapper:hover .more-dropdown {
            display: block;
        }
    `;
    document.head.appendChild(style);
    setInterval(function () {
        let potplayer = document.querySelectorAll("div#itemDetailPage:not(.hide) #embyPot")[0];
        if (!potplayer) {
            let mainDetailButtons = document.querySelectorAll("div#itemDetailPage:not(.hide) .mainDetailButtons .detailButton[title='播放']")[0];
            if (mainDetailButtons) {
                // 创建滚动容器和按钮包装器
                const container = document.createElement('div');
                container.className = 'player-buttons-container';
                
                const buttonsWrapper = document.createElement('div');
                buttonsWrapper.className = 'player-buttons-wrapper';
                
                // 创建按钮元素(只创建前三个)
                buttonsWrapper.innerHTML = `
                  <button id="embyPot" type="button" class="button-flat btnPlay detailButton emby-button" title="Potplayer"> 
                    <div class="detailButton-content"> 
                      <span class="material-icons detailButton-icon icon-PotPlayer"> </span> 
                    </div> 
                  </button>

                  <button id="embyMX" type="button" class="button-flat btnPlay detailButton emby-button" title="MXPlayer">MXPlayer</button>
                    <div class="detailButton-content"> 
                      <span class="material-icons detailButton-icon icon-MX"> </span> 
                    </div> 
                  </button>
                  <button id="embyIINA" type="button" class="button-flat btnPlay detailButton emby-button" title="IINA"> 
                    <div class="detailButton-content"> 
                      <span class="material-icons detailButton-icon icon-IINA"> </span> 
                    </div> 
                  </button>
                  <!-- 更多按钮 -->
                  <div class="more-button-wrapper">
                    <button class="more-button">更多</button>
                    <div class="more-dropdown">
                      <button id="embyVlc" title="VLC"> 
                      <button id="embyNPlayer" title="NPlayer">NPlayer</button>
                      <button id="embyInfuse" title="InfusePlayer">Infuse</button>
                    </div>
                  </div>
                `;
                
                // 组装滚动容器结构
                container.appendChild(buttonsWrapper);
                
                // 在播放按钮后插入滚动容器
                mainDetailButtons.insertAdjacentHTML('afterend', '<div class="player-buttons-gap"></div>');
                mainDetailButtons.nextElementSibling.replaceWith(container);
                
                // 设置按钮事件
                const detailPage = document.querySelector("div#itemDetailPage:not(.hide)");
                detailPage.querySelector("#embyPot").onclick = embyPot;
                detailPage.querySelector("#embyIINA").onclick = embyIINA;
                detailPage.querySelector("#embyVlc").onclick = embyVlc;
                detailPage.querySelector("#embyNPlayer").onclick = embyNPlayer;
                detailPage.querySelector("#embyMX").onclick = embyMX;
                detailPage.querySelector("#embyInfuse").onclick = embyInfuse;

                // 添加图标
                detailPage.querySelector(".icon-PotPlayer").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/[email protected]/embyWebAddExternalUrl/icons/icon-PotPlayer.webp)no-repeat;background-size: 100% 100%';
                detailPage.querySelector(".icon-VLC").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/[email protected]/embyWebAddExternalUrl/icons/icon-VLC.webp)no-repeat;background-size: 100% 100%';
                detailPage.querySelector(".icon-IINA").style.cssText += 'background: url(https://cdn.jsdelivr.net/gh/bpking1/[email protected]/embyWebAddExternalUrl/icons/icon-IINA.webp)no-repeat;background-size: 100% 100%';
            }
        }
    }, 1000);

    async function getItemInfo() {
        let userId = ApiClient._serverInfo.UserId;
        let itemId = /\?id=(\w*)/.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 });
            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);
        }
        //播放当前集或电影
        console.log("itemId:  " + itemId);
        return response;
    }

    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("select[is='emby-select']:not(.hide).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("select[is='emby-select']:not(.hide).selectSource");
        if (selectSource && selectSource.value.length > 0) {
            mediaSourceId = selectSource.value;
        }
        //let selectAudio = document.querySelector("select[is='emby-select']:not(.hide).selectAudio");
        let mediaSource = itemInfo.MediaSources.find(m => m.Id == mediaSourceId);
        let domain = `${ApiClient._serverAddress}/emby/videos/${itemInfo.Id}`;
        let subPath = getSubPath(mediaSource);
        let subUrl = subPath.length > 0 ? `${domain}${subPath}?api_key=${ApiClient.accessToken()}` : '';
        let streamUrl = `${domain}/stream.${mediaSource.Container}?api_key=${ApiClient.accessToken()}&Static=true&MediaSourceId=${mediaSourceId}`;
        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) {
        let title = 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
        };
    }

    async function embyPot() {
        let mediaInfo = await getEmbyMediaInfo();
        let intent = mediaInfo.intent;
        let poturl = `potplayer://${encodeURI(mediaInfo.streamUrl)} /sub=${encodeURI(mediaInfo.subUrl)} /current /title="${intent.title}" /seek=${getSeek(intent.position)}`;
        console.log(poturl);
        window.open(poturl, "_blank");
    }

    //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 (getOS() == "windows") {
            //桌面端需要额外设置,参考这个项目,MPV也是类似的方法:  https://github.com/stefansundin/vlc-protocol
            vlcUrl = `vlc://${encodeURI(mediaInfo.streamUrl)}`;
        }
        if (getOS() == 'ios') {
            //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, "_blank");
    }

    //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, "_blank");
    }

    //https://sites.google.com/site/mxvpen/api
    async function embyMX() {
        let mediaInfo = await getEmbyMediaInfo();
        let intent = mediaInfo.intent;
        //mxPlayer free
        let mxUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=com.mxtech.videoplayer.ad;S.title=${encodeURI(intent.title)};i.position=${intent.position};end`;
        //mxPlayer Pro
        //let mxUrl = `intent:${encodeURI(mediaInfo.streamUrl)}#Intent;package=com.mxtech.videoplayer.pro;S.title=${encodeURI(intent.title)};i.position=${intent.position};end`;
        console.log(mxUrl);
        window.open(mxUrl, "_blank");
    }

    async function embyNPlayer() {
        let mediaInfo = await getEmbyMediaInfo();
        let nUrl = getOS() == 'macOS' ? `nplayer-mac://weblink?url=${encodeURIComponent(mediaInfo.streamUrl)}&new_window=1` : `nplayer-${encodeURI(mediaInfo.streamUrl)}`;
        console.log(nUrl);
        window.open(nUrl, "_blank");
    }

    //infuse
     async function embyInfuse() {
         let mediaInfo = await getEmbyMediaInfo();
         let infuseUrl = `infuse://x-callback-url/play?url=${encodeURIComponent(mediaInfo.streamUrl)}`;
         console.log(`infuseUrl= ${infuseUrl}`);
         window.open(infuseUrl, "_blank");
     }

    function getOS() {
        let u = navigator.userAgent
        if (!!u.match(/compatible/i) || u.match(/Windows/i)) {
            return 'windows'
        } else if (!!u.match(/Macintosh/i) || u.match(/MacIntel/i)) {
            return 'macOS'
        } else if (!!u.match(/iphone/i) || u.match(/Ipad/i)) {
            return 'ios'
        } else if (u.match(/android/i)) {
            return 'android'
        } else if (u.match(/Ubuntu/i)) {
            return 'Ubuntu'
        } else {
            return 'other'
        }
    }

})();