Youtube Bilibili Video Player Enhancer Tools

Adds more speed buttons and more settings to YouTube and Bilibili video players.

// ==UserScript==
// @name              Youtube Bilibili Video Player Enhancer Tools
// @name:zh           油管哔哩哔哩视频播放器增强工具
// @name:zh-CN        油管哔哩哔哩视频播放器增强工具
// @name:en           Youtube Bilibili Video Player Enhancer Tools
// @name:en-US        Youtube Bilibili Video Player Enhancer Tools
// @description       Adds more speed buttons and more settings to YouTube and Bilibili video players.
// @description:en    Adds more speed buttons and more settings to YouTube and Bilibili video players.
// @description:en-US Adds more speed buttons and more settings to YouTube and Bilibili video players.
// @description:zh    油管哔哩哔哩视频播放器下添加更多倍速播放按钮及更多配置。
// @description:zh-CN 油管哔哩哔哩视频播放器下添加更多倍速播放按钮及更多配置。
// @namespace         com.julong.tampermonkey.TubeBiliVideoPlayerEnhancerTools
// @version           1.0.3
// @author            [email protected]
// @homepage          https://github.com/julong111/tampermonkey-TubeBili
// @supportURL        https://github.com/julong111/tampermonkey-TubeBili/issues

// @match             *://*.youtube.com*
// @match             *://*.bilibili.com*
// @include           *://*.youtube.com*
// @include           *://*.bilibili.com*

// @grant             GM_addStyle
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_registerMenuCommand
// @icon              https://www.youtube.com/s/desktop/3748dff5/img/favicon_48.png
// @charset		      UTF-8
// @license           MIT
// ==/UserScript==

(function () {
    'use strict';
    const i18nConfig = {
        // 中文配置
        zh: {
            menu_settings: "设置面板",
            menu_save: "保存",
            menu_close: "关闭",

            Youtube_AutoTheaterMode: "Youtube - 自动视频网页全屏",
            Youtube_AutoRate2x: "Youtube - 自动2倍速播放",
            Youtube_AutoRemoveMiniplayer: "Youtube - 自动移除MiniPlayer按钮",

            Bilibili_AutoWebFullscreen: "Bilibili - 自动视频网页全屏",
            Bilibili_AutoRate2x: "Bilibili - 自动2倍速播放",
            Bilibili_AutoRemovePip: "Bilibili - 自动移除画中画按钮",
            Bilibili_AutoRemoveWide: "Bilibili - 自动移除宽屏按钮",
            Bilibili_AutoRemoveSpeed: "Bilibili - 自动移除原始倍速按钮",
            Bilibili_AutoRemoveComments: "Bilibili - 自动移除评论输入区",
            Bilibili_AutoRemoveSettings: "Bilibili - 自动移除设置按钮",
        },
        // 英文配置
        en: {
            menu_settings: "Settings Panel",
            menu_save: "Save",
            menu_close: "Close",

            Youtube_AutoTheaterMode: "Youtube - Auto Theater Mode",
            Youtube_AutoRate2x: "Youtube - Auto 2x Playback",
            Youtube_AutoRemoveMiniplayer: "Youtube - Auto Remove MiniPlayer Button",

            Bilibili_AutoWebFullscreen: "Bilibili - Auto Web Fullscreen",
            Bilibili_AutoRate2x: "Bilibili - Auto 2x Playback",
            Bilibili_AutoRemovePip: "Bilibili - Auto Remove Picture-in-Picture Button",
            Bilibili_AutoRemoveWide: "Bilibili - Auto Remove Wide Button",
            Bilibili_AutoRemoveSpeed: "Bilibili - Auto Remove Original Speed Button",
            Bilibili_AutoRemoveComments: "Bilibili - Auto Remove Comments Input Area",
            Bilibili_AutoRemoveSettings: "Bilibili - Auto Remove Settings Button",
        }
    };

    const settingPanelStyles = `
        #minimalSettingsPanel {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 350px;
            padding: 15px;
            background-color: #f9f9f9;
            border: 1px solid #ccc;
            border-radius: 5px;
            box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
            z-index: 99999;
            font-family: sans-serif;
            display: none;
        }
        #minimalSettingsPanel.show {
            display: block;
        }
        #minimalSettingsPanel h2 {
            margin: 0 0 10px;
            font-size: 1.1em;
            text-align: center;
        }
        #minimalSettingsPanel .setting-item {
            margin-bottom: 10px;
        }
        #minimalSettingsPanel .buttons {
            margin-top: 15px;
            text-align: right;
        }
        #minimalSettingsPanel button {
            padding: 5px 10px;
            cursor: pointer;
            border: 1px solid #ccc;
            background-color: #eee;
            border-radius: 3px;
        }`;

    let isYoutubeListenerRegistered = false;
    let youtubeLiveStreamCheck = null;

    const Common = {
        speeds: [0.5, 1.0, 1.5, 2.0],
        colors: ['#072525', '#287F54', '#C22544'],
        currentLang: 'en',
        settingPanelItems: [],
        initSpeedBtnFlag: false,
        settingPanelInitialized: false,
        settingPanelElement: null,
        detectLanguage: function () {
            let userLang = navigator.language.toLowerCase();
            if (userLang.startsWith('zh')) {
                return 'zh';
            }
            if (userLang.startsWith('en')) {
                return 'en';
            }
            return 'en';
        },
        geti18nText: function (key) {
            return i18nConfig[Common.currentLang][key];
        },
        initializePanel: function () {
            let panel = document.createElement("div");
            panel.id = "minimalSettingsPanel";
            let title = document.createElement("h2");
            title.textContent = Common.geti18nText("menu_settings");
            panel.appendChild(title);
            for (const [key, item] of Object.entries(Common.settingPanelItems)) {
                let functionDiv = document.createElement("div");
                functionDiv.className = "setting-item";
                panel.appendChild(functionDiv);
                let functionValue = GM_getValue(item.dataKey, false);
                let input1 = document.createElement("input");
                input1.type = "checkbox";
                input1.checked = functionValue;
                input1.id = item.classId;
                functionDiv.appendChild(input1);
                let label1 = document.createElement("label");
                label1.setAttribute("for", item.classId);
                label1.textContent = item.text;
                functionDiv.appendChild(label1);
            }
            let buttons = document.createElement("div");
            buttons.className = "buttons";
            let saveBtn = document.createElement("button");
            saveBtn.id = "saveBtn";
            saveBtn.textContent = Common.geti18nText("menu_save");
            saveBtn.addEventListener("click", () => {
                Common.saveSettings();
            });
            let closeBtn = document.createElement("button");
            closeBtn.id = "closeBtn";
            closeBtn.textContent = Common.geti18nText("menu_close");
            closeBtn.addEventListener("click", () => {
                Common.togglePanel();
            });
            buttons.appendChild(saveBtn);
            buttons.appendChild(closeBtn);
            panel.appendChild(buttons);
            document.body.appendChild(panel);
            Common.settingPanelElement = panel;
            Common.settingPanelInitialized = true;
        },
        saveSettings: function () {
            for (const [key, item] of Object.entries(Common.settingPanelItems)) {
                const isChecked = document.getElementById(item.classId).checked;
                GM_setValue(item.dataKey, isChecked);
            }
            Common.settingPanelElement.classList.toggle('show');
        },
        togglePanel: function () {
            if (!Common.settingPanelInitialized) {
                Common.initializePanel();
            }
            Common.settingPanelElement.classList.toggle('show');
        },
        initSettingItems: function (currentUrl) {
            if (currentUrl.includes("youtube.com")) {
                Common.settingPanelItems = {
                    // Youtube_AutoTheaterMode: {
                    //     classId: "Youtube-AutoTheaterMode",
                    //     text: geti18nText("Youtube_AutoTheaterMode"),
                    //     dataKey: "Youtube-AutoTheaterMode",
                    // },
                    Youtube_AutoRate2x: {
                        classId: "Youtube-AutoRate2x",
                        text: Common.geti18nText("Youtube_AutoRate2x"),
                        dataKey: "Youtube-AutoRate2x",
                    },
                    Youtube_AutoRemoveMiniplayer: {
                        classId: "Youtube-AutoRemoveMiniplayer",
                        text: Common.geti18nText("Youtube_AutoRemoveMiniplayer"),
                        dataKey: "Youtube-AutoRemoveMiniplayer",
                    },
                };
            } else if (currentUrl.includes("bilibili.com")) {
                Common.settingPanelItems = {
                    Bilibili_AutoWebFullscreen: {
                        classId: "Bilibili-AutoWebFullscreen",
                        text: Common.geti18nText("Bilibili_AutoWebFullscreen"),
                        dataKey: "Bilibili-AutoWebFullscreen",
                    },
                    Bilibili_AutoRate2x: {
                        classId: "Bilibili-AutoRate2x",
                        text: Common.geti18nText("Bilibili_AutoRate2x"),
                        dataKey: "Bilibili-AutoRate2x",
                    },
                    Bilibili_AutoRemovePip: {
                        classId: "Bilibili-AutoRemovePip",
                        text: Common.geti18nText("Bilibili_AutoRemovePip"),
                        dataKey: "Bilibili-AutoRemovePip",
                    },
                    Bilibili_AutoRemoveWide: {
                        classId: "Bilibili-AutoRemoveWide",
                        text: Common.geti18nText("Bilibili_AutoRemoveWide"),
                        dataKey: "Bilibili-AutoRemoveWide",
                    },
                    Bilibili_AutoRemoveSpeed: {
                        classId: "Bilibili-AutoRemoveSpeed",
                        text: Common.geti18nText("Bilibili_AutoRemoveSpeed"),
                        dataKey: "Bilibili-AutoRemoveSpeed",
                    },
                    Bilibili_AutoRemoveComments: {
                        classId: "Bilibili-AutoRemoveComments",
                        text: Common.geti18nText("Bilibili_AutoRemoveComments"),
                        dataKey: "Bilibili-AutoRemoveComments",
                    },
                    Bilibili_AutoRemoveSettings: {
                        classId: "Bilibili-AutoRemoveSettings",
                        text: Common.geti18nText("Bilibili_AutoRemoveSettings"),
                        dataKey: "Bilibili-AutoRemoveSettings",
                    },
                };
            };
        },
        createSpeedButtons: function (selector, callback) {
            if (Common.initSpeedBtnFlag) {
                return;
            }
            let bgColor = Common.colors[0];
            let speedListDiv = document.createElement('div');
            speedListDiv.id = 'speedButtons';
            speedListDiv.style.display = 'flex';
            speedListDiv.style.alignItems = 'center';
            speedListDiv.style.justifyContent = 'center';
            speedListDiv.style.height = '100%';
            const handleButtonClick = (speed) => {
                document.getElementsByTagName('video')[0].playbackRate = speed;
            };
            for (let i = 0; i < Common.speeds.length; i++) {
                if (Common.speeds[i] >= 1) { bgColor = Common.colors[1]; }
                if (Common.speeds[i] >= 1.5) { bgColor = Common.colors[2]; }
                let btn = document.createElement('button');
                btn.style.backgroundColor = bgColor;
                btn.style.marginRight = '1px';
                btn.style.border = '1px solid #D3D3D3';
                btn.style.borderRadius = '2px';
                btn.style.color = '#ffffff';
                btn.style.cursor = 'pointer';
                btn.style.fontFamily = 'Arial, "Helvetica Neue", Helvetica, sans-serif';
                btn.style.display = 'flex';
                btn.style.justifyContent = 'center';
                btn.style.alignItems = 'center';
                btn.style.width = '38px';
                btn.style.height = '24px';
                btn.style.fontSize = '14px';
                btn.textContent = Common.speeds[i].toString() + '×';
                btn.addEventListener('click', () => {
                    handleButtonClick(Common.speeds[i]);
                });
                speedListDiv.appendChild(btn);
            }
            callback(speedListDiv);
            Common.initSpeedBtnFlag = true;
        },
        waitForElement: function (selector, callback, interval = 200) {
            if (typeof selector !== 'string') {
                throw new TypeError('selector must be a string.');
            }
            if (typeof callback !== 'function') {
                throw new TypeError('callback must be a function.');
            }
            let attempts = 0;
            const options = {
                interval: interval,
                maxAttempts: -1
            };
            const checkProcess = () => {
                let element = null;
                if (typeof selector === 'string') {
                    element = document.querySelector(selector);
                } else {
                    throw new TypeError('selector must be a string.');
                }
                if (element) {
                    callback(element);
                    return;
                }
                attempts++;
                if (options.maxAttempts !== -1 && attempts >= options.maxAttempts) {
                    console.error(`Common.waitForElement: Reached max attempts (${options.maxAttempts}), element not found.`);
                    return;
                }
                setTimeout(checkProcess, options.interval);
            };
            checkProcess();
        },
        removeSelector: function (selector) {
            let ele = document.querySelector(selector);
            if (ele) {
                ele.remove();
            }
        },
        setPlaybackRate: function (rate) {
            document.getElementsByTagName('video')[0].playbackRate = rate;
        }
    };
    const WebSite = {
        selectors: {
            youtube: {
                // YouTube selectors listeners
                videoPanel: '#movie_player > div.ytp-chrome-bottom > div.ytp-chrome-controls > div.ytp-right-controls',
                liveStreamIcon: '#movie_player > div.ytp-chrome-bottom > div.ytp-chrome-controls > div.ytp-left-controls > div.ytp-time-display.notranslate.ytp-live > button', // Youtube Live Stream check
                miniPlayerBtn: '#movie_player > div.ytp-chrome-bottom > div.ytp-chrome-controls > div.ytp-right-controls > button.ytp-miniplayer-button.ytp-button',
                finishListener: 'yt-navigate-finish',
                liveStreamClass: 'ytp-live-badge-is-livehead',
            },
            bilibili: {
                /// Bilibili selectors 
                playerContainer: '#bilibili-player',
                webFullClass: 'mode-webscreen',
                speedBtn: '.bpx-player-control-bottom-left',
                videoPanel: '.bilibili-player, .bpx-player-container, #bilibiliPlayer',
                commentsPanel: '#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-video-area > div.bpx-player-control-wrap > div.bpx-player-control-entity > div.bpx-player-control-bottom > div.bpx-player-control-bottom-center',
                webFullBtn: '#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-video-area > div.bpx-player-control-wrap > div.bpx-player-control-entity > div.bpx-player-control-bottom > div.bpx-player-control-bottom-right > div.bpx-player-ctrl-btn.bpx-player-ctrl-web',
                pipBtn: '#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-video-area > div.bpx-player-control-wrap > div.bpx-player-control-entity > div.bpx-player-control-bottom > div.bpx-player-control-bottom-right > div.bpx-player-ctrl-btn.bpx-player-ctrl-pip',
                wideBtn: '#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-video-area > div.bpx-player-control-wrap > div.bpx-player-control-entity > div.bpx-player-control-bottom > div.bpx-player-control-bottom-right > div.bpx-player-ctrl-btn.bpx-player-ctrl-wide',
                speedsListBtn: '#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-video-area > div.bpx-player-control-wrap > div.bpx-player-control-entity > div.bpx-player-control-bottom > div.bpx-player-control-bottom-right > div.bpx-player-ctrl-btn.bpx-player-ctrl-playbackrate',
                settingsBtn: '#bilibili-player > div > div > div.bpx-player-primary-area > div.bpx-player-video-area > div.bpx-player-control-wrap > div.bpx-player-control-entity > div.bpx-player-control-bottom > div.bpx-player-control-bottom-right > div.bpx-player-ctrl-btn.bpx-player-ctrl-setting',
            }
        },
        youtube: function () {
            const handleYoutubePage = () => {
                if (!Common.initSpeedBtnFlag) {
                    // 创建速度按钮
                    Common.waitForElement(
                        WebSite.selectors.youtube.videoPanel,
                        (item) => {
                            Common.createSpeedButtons(WebSite.selectors.youtube.videoPanel, (moreSpeedsDiv) => {
                                item.before(moreSpeedsDiv);
                            });
                        }
                    );

                    let autoRate2x = GM_getValue(Common.settingPanelItems.Youtube_AutoRate2x.dataKey, false);
                    if (autoRate2x) {
                        Common.waitForElement(
                            WebSite.selectors.youtube.videoPanel,
                            (item) => {
                                Common.setPlaybackRate(2.0);
                            }
                        );
                    }

                    let removeMiniplayer = GM_getValue(Common.settingPanelItems.Youtube_AutoRemoveMiniplayer.dataKey, false);
                    if (removeMiniplayer) {
                        Common.waitForElement(
                            WebSite.selectors.youtube.miniPlayerBtn,
                            (item) => {
                                item.remove();
                            }
                        );
                    }

                    // TheaterMode无法监听事件,待开发 
                }
            };

            if (!isYoutubeListenerRegistered) {
                window.addEventListener(WebSite.selectors.youtube.finishListener, handleYoutubePage);
                isYoutubeListenerRegistered = true;
            }
            // 启动直播状态检测
            youtubeLiveStreamCheck = setInterval(() => {
                let element = document.querySelector(WebSite.selectors.youtube.liveStreamIcon);
                if (element) {
                    if (element.classList.contains(WebSite.selectors.youtube.liveStreamClass)) {
                        document.getElementsByTagName('video')[0].playbackRate = 1.0;
                        console.log('已检测到直播,重置播放速度为1.0');
                    }
                }
            }, 1000);
            handleYoutubePage();
        },
        bilibili: function () {
            const handleBilibiliPage = () => {
                // 清理旧的按钮,防止重复创建
                Common.removeSelector('#speedButtons');
                Common.initSpeedBtnFlag = false;

                // 创建速度按钮
                Common.waitForElement(
                    WebSite.selectors.bilibili.videoPanel,
                    (item) => {
                        Common.createSpeedButtons(WebSite.selectors.bilibili.videoPanel, (moreSpeedsDiv) => {
                            let ele = document.querySelector(WebSite.selectors.bilibili.speedBtn);
                            if (ele) {
                                ele.after(moreSpeedsDiv);
                            }
                        });
                    }, 1000
                );


                // 自动移除的元素
                const removalConfigs = {
                    Bilibili_AutoRemoveComments: WebSite.selectors.bilibili.commentsPanel,
                    Bilibili_AutoRemovePip: WebSite.selectors.bilibili.pipBtn,
                    Bilibili_AutoRemoveWide: WebSite.selectors.bilibili.wideBtn,
                    Bilibili_AutoRemoveSpeed: WebSite.selectors.bilibili.speedsListBtn,
                    Bilibili_AutoRemoveSettings: WebSite.selectors.bilibili.settingsBtn,
                };
                for (const key in removalConfigs) {
                    if (GM_getValue(Common.settingPanelItems[key].dataKey, false)) {
                        Common.waitForElement(
                            removalConfigs[key],
                            (item) => {
                                item.remove();
                            }, 1000
                        );
                    }
                }

                let autoRate2x = GM_getValue(Common.settingPanelItems.Bilibili_AutoRate2x.dataKey, false);
                if (autoRate2x) {
                    Common.setPlaybackRate(2.0);
                }

                let autoWebFullscreen = GM_getValue(Common.settingPanelItems.Bilibili_AutoWebFullscreen.dataKey, false);
                if (autoWebFullscreen) {
                    let playerContainerMode = document.querySelector(WebSite.selectors.bilibili.playerContainer);
                    if (playerContainerMode && playerContainerMode.classList.contains(WebSite.selectors.bilibili.webFullClass)) {
                        return;
                    }
                    Common.waitForElement(
                        WebSite.selectors.bilibili.webFullBtn,
                        (item) => {
                            item.click();
                        }, 1000
                    );
                }
            };

            const observerConfig = { childList: true, subtree: true };
            const observer = new MutationObserver((mutationsList, observer) => {
                for (const mutation of mutationsList) {
                    if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                        const videoPlayer = document.querySelector(WebSite.selectors.bilibili.videoPanel);
                        if (videoPlayer) {
                            handleBilibiliPage();
                            observer.disconnect();
                        }
                    }
                }
            });
            observer.observe(document.body, observerConfig);
            handleBilibiliPage();
        }
    }

    function main() {
        // 每次页面加载时,都先清理所有可能存在的定时器
        if (youtubeLiveStreamCheck !== null) {
            clearInterval(youtubeLiveStreamCheck);
            youtubeLiveStreamCheck = null;
        }
        const currentUrl = window.location.href;
        Common.currentLang = Common.detectLanguage();
        Common.initSettingItems(currentUrl);
        GM_addStyle(settingPanelStyles);
        GM_registerMenuCommand(Common.geti18nText("menu_settings"), Common.togglePanel);

        if (currentUrl.includes("youtube.com")) {
            WebSite.youtube();
        } else if (currentUrl.includes("bilibili.com")) {
            WebSite.bilibili();
        }
    }
    main();
})();