- // ==UserScript==
- // @name Web Skipper for Plex
- // @namespace https://github.com/x1ao4/web-skipper-for-plex
- // @version 1.2
- // @description Automatically skip intros, credits, and auto-play the next item on Plex Web.
- // @description:zh-CN 在 Plex Web 上实现自动跳过片头、片尾和自动播放下一个项目功能。
- // @description:zh-HK 在 Plex Web 上實現自動跳過片頭、片尾和自動播放下一個項目功能。
- // @description:zh-TW 在 Plex Web 上實現自動跳過介紹、名單和自動播放下一個項目功能。
- // @author x1ao4
- // @match https://app.plex.tv/*
- // @match http://localhost:32400/*
- // @match http://127.0.0.1:32400/*
- // @license MIT
- // @grant GM_setValue
- // @grant GM_getValue
- // @grant GM_registerMenuCommand
- // @grant GM_unregisterMenuCommand
- // ==/UserScript==
-
- (function() {
- 'use strict';
-
- // 设置检查按钮和 “播放下一个” 元素的间隔
- const interval = 1000;
-
- // 设置按钮的选择器
- const buttonSelector = 'button.AudioVideoFullPlayer-overlayButton-D2xSex';
-
- // 保存菜单命令的 ID
- let skipIntroCmd, skipCreditsCmd, autoPlayNextCmd;
-
- // 获取用户语言
- const userLang = navigator.language || navigator.userLanguage;
-
- // 根据用户语言设置按钮文本
- const buttonTexts = {
- 'skipIntro': {
- 'af-ZA': 'Slaan inleiding oor',
- 'bg-BG': 'Прескачане на интродукцията',
- 'ca-ES': 'Salteu la Introducció',
- 'cs-CZ': 'Přeskočit úvod',
- 'da-DK': 'Spring over intro',
- 'de-DE': 'Überspringe Intro',
- 'el-GR': 'Παράλειψη Προλόγου',
- 'en-US': 'Skip Intro',
- 'es-419': 'Omitir Opening',
- 'es-ES': 'Saltar intro',
- 'et-EE': 'Jäta intro vahele',
- 'fi-FI': 'Ohita intro',
- 'fr-CA': 'Passer l\'intro',
- 'fr-FR': 'Passer l\'intro',
- 'he-IL': 'דלג על הפתיח',
- 'hr-HR': 'Preskakanje uvoda',
- 'hu-HU': 'Intro átugrása',
- 'is-IS': 'Sleppa inngangi',
- 'it-IT': 'Salta Intro',
- 'ja-JP': 'イントロをスキップ',
- 'ko-KR': '인트로 건너뛰기',
- 'lt-LT': 'Praleisti intro',
- 'nl-NL': 'Intro overslaan',
- 'nb-NO': 'Hopp over intro',
- 'pl-PL': 'Pomiń wstęp',
- 'pt-BR': 'Pular Intro',
- 'pt-PT': 'Saltar introdução',
- 'ro-RO': 'Sari peste introducere',
- 'ru-RU': 'Пропустить интро',
- 'sk-SK': 'Preskočiť intro',
- 'sl-SI': 'Preskoči predstavitev',
- 'sv-SE': 'Hoppa över intro',
- 'th-TH': 'ข้ามตอนต้น',
- 'tr-TR': 'Jeneriği Atla',
- 'uk-UA': 'Пропустити вступ',
- 'zh-TW': '略過介紹',
- 'zh-CN': '跳过片头',
- },
- 'skipCredits': {
- 'af-ZA': 'Slaan eindkrediete oor',
- 'cs-CZ': 'Přeskočit titulky',
- 'da-DK': 'Spring rulletekster over',
- 'de-DE': 'Abspann überspringen',
- 'el-GR': 'Παράλειψη των τίτλων τέλους',
- 'en-US': 'Skip Credits',
- 'es-ES': 'Saltar créditos',
- 'fi-FI': 'Ohita lopputekstit',
- 'fr-CA': 'Sauter le générique',
- 'fr-FR': 'Passer le générique',
- 'he-IL': 'דלג על כותרות סיום',
- 'hr-HR': 'Preskoči odjavnu špicu',
- 'hu-HU': 'Stáblista Átugrása',
- 'it-IT': 'Salta Crediti',
- 'ja-JP': 'クレジットをスキップ',
- 'ko-KR': '크레딧 건너뛰기',
- 'lt-LT': 'Praleisti subtitrus',
- 'nl-NL': 'Credits overslaan',
- 'nb-NO': 'Hopp over rulleteksten',
- 'pl-PL': 'Pomiń napisy końcowe',
- 'pt-BR': 'Pular créditos',
- 'pt-PT': 'Saltar créditos',
- 'ro-RO': 'Sari peste credite',
- 'ru-RU': 'Пропустить титры',
- 'sk-SK': 'Preskočiť titulky',
- 'sl-SI': 'Preskoči napise',
- 'sv-SE': 'Hoppa över eftertexter',
- 'tr-TR': 'Kredileri Atla',
- 'uk-UA': 'Пропустити титри',
- 'zh-TW': '跳過名單',
- 'zh-CN': '跳过片尾',
- },
- 'autoSkipIntro': {
- 'en-US': 'Auto Skip Intro',
- 'zh-TW': '自動跳過介紹',
- 'zh-CN': '自动跳过片头',
- },
- 'autoSkipCredits': {
- 'en-US': 'Auto Skip Credits',
- 'zh-TW': '自動跳過名單',
- 'zh-CN': '自动跳过片尾',
- },
- 'autoPlayNext': {
- 'en-US': 'Auto Play Next',
- 'zh-TW': '自動播放下一個',
- 'zh-CN': '自动播放下一个',
- },
- 'enabled': {
- 'en-US': 'On',
- 'zh-TW': '開',
- 'zh-CN': '开',
- },
- 'disabled': {
- 'en-US': 'Off',
- 'zh-TW': '關',
- 'zh-CN': '关',
- }
- };
-
- // 获取对应语言的文本,如果没有找到,则使用英语作为默认语言
- function getText(textType) {
- return buttonTexts[textType][userLang] || buttonTexts[textType]['en-US'];
- }
-
- // 获取按钮类型
- function getButtonType(buttonText) {
- for (let lang in buttonTexts['skipIntro']) {
- if (buttonTexts['skipIntro'][lang] === buttonText) {
- return 'skipIntro';
- }
- }
- for (let lang in buttonTexts['skipCredits']) {
- if (buttonTexts['skipCredits'][lang] === buttonText) {
- return 'skipCredits';
- }
- }
- return null;
- }
-
- // 更新菜单命令
- function updateMenuCommands() {
- // 移除旧的菜单命令
- GM_unregisterMenuCommand(skipIntroCmd);
- GM_unregisterMenuCommand(skipCreditsCmd);
- GM_unregisterMenuCommand(autoPlayNextCmd);
-
- // 为每个功能注册(不可用)新的菜单命令
- skipIntroCmd = GM_registerMenuCommand(getText('autoSkipIntro') + ' · ' + (GM_getValue('skipIntro', true) ? getText('enabled') : getText('disabled')), function() {
- GM_setValue('skipIntro', !GM_getValue('skipIntro', true));
- updateMenuCommands();
- });
-
- skipCreditsCmd = GM_registerMenuCommand(getText('autoSkipCredits') + ' · ' + (GM_getValue('skipCredits', true) ? getText('enabled') : getText('disabled')), function() {
- GM_setValue('skipCredits', !GM_getValue('skipCredits', true));
- updateMenuCommands();
- });
-
- autoPlayNextCmd = GM_registerMenuCommand(getText('autoPlayNext') + ' · ' + (GM_getValue('autoPlayNext', true) ? getText('enabled') : getText('disabled')), function() {
- GM_setValue('autoPlayNext', !GM_getValue('autoPlayNext', true));
- updateMenuCommands();
- });
- }
-
- // 初始化菜单命令
- updateMenuCommands();
-
- // 如果存在按钮,则点击按钮
- function clickButton(selector, isEnabled) {
- if (isEnabled) {
- const buttons = document.querySelectorAll(selector);
- for (const button of buttons) {
- let buttonType = getButtonType(button.innerText);
- if (buttonType && GM_getValue(buttonType, true)) {
- button.click();
- break;
- }
- }
- }
- }
-
- // 检查元素是否存在
- function elementExists(selector) {
- return document.querySelector(selector) !== null;
- }
-
- // 设置一个间隔来检查按钮和 “播放下一个” 元素,如果它们存在,则点击它们或按空格键
- setInterval(() => {
- clickButton(buttonSelector, true);
- if (GM_getValue('autoPlayNext', true) && elementExists('label.AudioVideoUpNext-autoPlayOn-FMTHL1')) {
- document.dispatchEvent(new KeyboardEvent('keydown', {key: ' ', keyCode: 32}));
- }
- }, interval);
- })();