Plex External Player PotPlayer

插件用于激活本地PotPlayer 播放器使用。

目前為 2021-07-30 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Plex External Player PotPlayer
// @namespace    https://github.com/Tosslog/PlexMediaServer/tree/main/Web%20Plugin/Plex%20External%20Player%20PotPlayer
// @version      1.1.0
// @description  插件用于激活本地PotPlayer 播放器使用。
// @author       北京土著 [email protected]
// @include     /^https?://.*:32400/web.*
// @include     http://*:32400/web/index.html*
// @include     https://*:32400/web/index.html*
// @include     https://app.plex.tv/*
// @require     http://code.jquery.com/jquery-3.2.1.min.js
// @connect     *
// @require     https://cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/js/toastr.min.js
// @grant       GM_xmlhttpRequest
// ==/UserScript==

$("head").append(
    '<link href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/css/toastr.min.css" rel="stylesheet" type="text/css">'
);

// 消息设定
toastr.options = {
    "closeButton": true,
    "debug": false,
    "newestOnTop": true,
    "progressBar": true,
    "positionClass": "toast-bottom-right",
    "preventDuplicates": false,
    "onclick": null,
    "showDuration": "300",
    "hideDuration": "1000",
    "timeOut": "5000",
    "extendedTimeOut": "5000",
    "showEasing": "swing",
    "hideEasing": "linear",
    "showMethod": "fadeIn",
    "hideMethod": "fadeOut"
};

// 输出消息
var showToast = function (msg, error) {
    var title = 'Plex External PotPlayer';
    if (error) {
        toastr.error(msg, title, { timeOut: 10000 });
        logMessage(msg, 'Error');
    }
    else {
        toastr.success(msg, title);
    }
};

// 控制台输出
var logMessage = function (msg, _type) {
    console.log('[Plex External PotPlayer ' + _type + ' ] ' + msg);
};

//得到 Plex Servers
var makeRequest = function (url, serverId) {
    return new Promise(function (resolve, reject) {
        var origAccessToken = localStorage.myPlexAccessToken;
        var serverNode = {};
        if (localStorage.users) {
            serverNode = JSON.parse(localStorage.users);
            //console.log(serverNode);
        } else {
            logMessage('未找到用户详细信息', 'Error');
        }
        var tokenToTry = origAccessToken;
        if (serverNode === undefined) {
            serverNode = {
                users: []
            };
        }
        // logMessage('寻找 serverId 的令牌:' + serverId)
        let tokenFound = false
        if (serverId !== undefined) {
            serverLoop:
            for (var i = 0; i < serverNode.users.length; i++) {
                // logMessage('Checking server list (' + serverNode.users[i].servers.length + ') for user:' + serverNode.users[i].username)
                for (var j = 0; j < serverNode.users[i].servers.length; j++) {
                    // logMessage('Checking server with id ' + serverNode.users[i].servers[j].machineIdentifier)
                    if (serverNode.users[i].servers[j].machineIdentifier == serverId) {
                        tokenToTry = serverNode.users[i].servers[j].accessToken;
                        // logMessage('Token found:' + tokenToTry);
                        tokenFound = true
                        break serverLoop;
                    }
                }
            }
            token = tokenToTry;
            if (!tokenFound) {
                showToast('找不到身份验证信息', 1);
                reject();
                return;
            }
        }
        var authedUrl = url + '&X-Plex-Token=' + tokenToTry;
        // logMessage('调用 ' + authedUrl, 'Log');
        GM_xmlhttpRequest({
            method: "GET",

            url: authedUrl,
            onload: function (state) {
                if (state.status === 200) {
                    //logMessage('成功调用' + url, 'Log');
                    resolve(state);
                }
            },
            onreadystatechange: function (state) {
                if (state.readyState === 4) {

                    if (state.status === 401) {
                        logMessage('未授权 ' + url, 'Error');
                    } else if (state.status !== 200) {
                        logMessage('请求返回 ' + state.status, 'Log');
                        showToast('Error calling: ' + url + '. Response: ' + state.responseText + ' Code:' + state.status + ' Message: ' + state.statusText, 1);
                    }
                }
            },
        });
    });
};

var getHosts = function () {
    makeRequest('https://plex.tv/api/resources?includeHttps=1&X-Plex-Token=' + localStorage.myPlexAccessToken)
        .then(function (response) {
            //console.log(response)
            let parts = response.responseXML.getElementsByTagName('Device');
            for (let i = 0; i < parts.length; i++) {
                if (parts[i].getAttribute('product') == 'Plex Media Server') {
                    let connections = parts[i].getElementsByTagName('Connection');
                    for (let j = 0; j < connections.length; j++) {
                        if (connections[j].getAttribute('local') == parts[i].getAttribute('publicAddressMatches')) {
                            pmsUrls.set(parts[i].getAttribute('clientIdentifier'), 'http://' + connections[j].getAttribute('address') + ':' + connections[j].getAttribute('port'));
                            break;
                        }
                    }
                }
            }
        }).catch(function () {
            showToast('Failed to get PMS URLs', 1);
        });
}

var pmsUrls = new Map();
var token = '';
var title = '';
setTimeout(function () {
    getHosts();
    //console.log(token)
}, 1000);


// 单击侦听器
var clickListener = function (e) {
    e.preventDefault();
    e.stopPropagation();
    var a = jQuery(e.target).closest('a');
    var link = a.attr('href');
    var url = link;
    if (link === '#' || link === undefined || link === 'javascript:void(0)') {
        url = window.location.hash;
    }

    if (url.indexOf('/server/') > -1) {
        var serverId = url.split('/')[2];
    }

    if (url.indexOf('%2Fmetadata%2F') > -1) {
        var idx = url.indexOf('%2Fmetadata%2F');
        var mediaId = url.substr(idx + 14);
        var idToken = mediaId.indexOf('&');
        if (idToken > -1) {
            mediaId = mediaId.substr(0, idToken);
        }
    }

    var metaDataPath = pmsUrls.get(serverId) + '/library/metadata/' + mediaId + '?includeConcerts=1&includeExtras=1&includeOnDeck=1&includePopularLeaves=1&includePreferences=1&includeChapters=1&asyncCheckFiles=0&asyncRefreshAnalysis=0&asyncRefreshLocalMediaAgent=0&X-Plex-Token=' + localStorage.myPlexAccessToken;
    //console.log('metaDataPath = ' + metaDataPath)
    makeRequest(metaDataPath, serverId)
        .then((response) => {
            let mediaurl = '';
            let subtitlelist = [];
            let Video = response.responseXML.getElementsByTagName('Video');
            for (let i = 0; i < Video.length; i++) {
                if (Video[i].attributes['title'] !== undefined) {
                    title = ' [' + Video[i].attributes['title'].value + '] ';
                    break;
                }
            }


            let parts = response.responseXML.getElementsByTagName('Part');
            for (let i = 0; i < parts.length; i++) {
                if (parts[i].attributes['file'] !== undefined) {
                    mediaurl = pmsUrls.get(serverId) + parts[i].attributes['key'].value + '?download=1';
                }
            }

            let Stream = response.responseXML.getElementsByTagName('Stream');
            //console.log(Stream)
            for (let i = 0; i < Stream.length; i++) {
                if (Stream[i].attributes['key'] !== undefined) {
                    let subtitlekey = Stream[i].attributes['key'].value;
                    if (subtitlekey !== undefined) {
                        subtitlelist.push(subtitlekey);
                    }
                    else {
                        logMessage('未找到本地外挂字幕文件', 'Error');
                    }
                }
            }

            let subtitleUrl = [];
            for (let i = 0; i < subtitlelist.length; i++) {
                subtitleUrl.push(pmsUrls.get(serverId) + subtitlelist[i]);
            };

            var authedUrl = mediaurl + '&X-Plex-Token=' + token;
            let poturl = "potplayer://" + authedUrl + " /sub=" + subtitleUrl[0] + '?X-Plex-Token=' + token;
            //console.log(poturl);
            showToast('成功解析影片 ' + title + ' 路径,正在激活本地PotPlayer播放器。', 0)
            window.open(poturl, "_blank");
        });

};

// 绑定按钮
var bindClicks = function () {
    var hasBtn = false;
    var toolBar = jQuery("#plex-icon-toolbar-play-560").parent().parent();
    toolBar.children('button').each(function (i, e) {
        if (jQuery(e).hasClass('plexextplayer'))
            hasBtn = true;
    });


    if (!hasBtn) {
        var template = jQuery('<button class="play-btn media-poster-btn btn-link plexextplayer" tabindex="-1" title="外部播放器"><i class="glyphicon play plexextplayer plexextplayerico"></i></button>');
        toolBar.prepend(template);
        template.click(clickListener);

    }

    // Cover page
    jQuery('[class^=MetadataPosterCardOverlay-link]').each(function (i, e) {
        e = jQuery(e);
        let poster = e.parent();
        if (poster.length === 1 && poster[0].className.trim().startsWith('MetadataPosterCardOverlay')) {
            let existingButton = poster.find('.plexextplayerico');
            if (existingButton.length === 0) {
                let url = poster.find('a').attr('href');
                let template = jQuery('<a href="' + url + '" aria-haspopup="false"  aria-role="button" class="" type="button"><i class="glyphicon play plexextplayer plexextplayerico plexextplayericocover"></i></button>');
                let newButton = template.appendTo(poster);
                newButton.click(clickListener);
                poster.mouseenter(function () {
                    newButton.find('i').css('display', 'block');
                });
                poster.mouseleave(function () {
                    newButton.find('i').css('display', 'none');
                });
            }
        }
    });

    //Thumbnails
    jQuery('[class^=PosterCardLink-link]').each(function (i, e) {
        e = jQuery(e);
        let thumb = e.parent();
        if (thumb.length === 1 && thumb[0].className.trim().startsWith('MetadataPosterListItem-card')) {
            let existingButton = thumb.find('.plexextplayerico');
            if (existingButton.length === 0) {
                let url = thumb.find('a').attr('href');
                let template = jQuery('<a href="' + url + '" aria-haspopup="false"  aria-role="button" class="" type="button"><i class="glyphicon play plexextplayer plexextplayerico plexextplayericocover"></i></button>');
                let thumbButton = template.appendTo(thumb);
                thumbButton.click(clickListener);
                thumb.mouseenter(function () {
                    thumbButton.find('i').css('display', 'block');
                });
                thumb.mouseleave(function () {
                    thumbButton.find('i').css('display', 'none');
                });
            }
        }
    });

    // Playlist
    jQuery("span[class^=' MetadataPosterTitle-singleLineTitle']").each(function (i, e) {
        e = jQuery(e);
        let playlistItem = e.closest("[class^='PlaylistItemRow-overlay']").find("[class^='PlaylistItemMetadata-indexContainer']");
        if (playlistItem.length === 1) {
            let existingButton = playlistItem.find('.plexextplayerico');
            if (existingButton.length === 0) {
                let url = e.find('a').attr('href');
                let template = jQuery('<a href="' + url + '" aria-haspopup="false"  aria-role="button" class="" type="button"><i class="glyphicon play plexextplayer plexextplayerico plexextplayericocover"></i></button>');
                let newButton = template.appendTo(playlistItem);
                newButton.click(clickListener);
                playlistItem.closest("[class^='PlaylistItemRow-overlay']").mouseenter(function () {
                    newButton.find('i').css('display', 'block');
                });
                playlistItem.closest("[class^='PlaylistItemRow-overlay']").mouseleave(function () {
                    newButton.find('i').css('display', 'none');
                });
            }
        }
    });
};


// Make buttons smaller
jQuery('body').append('<style>.plexextplayericocover {right: 10px; top: 10px; position:absolute; display:none;font-size:15px;} .glyphicon.plexfolderextplayerico:before {  content: "\\e145";   } .glyphicon.plexextplayerico:before {  content: "\\e161";   }</style>');


// 绑定按钮并每 100 毫秒检查一次新按钮
// 放置脚本最后
setInterval(bindClicks, 100);
bindClicks();