B站/哔哩哔哩/bilibili视频增强脚本-(中文字幕+倍速控制)

自动开启中文字幕,提供倍速控制功能(Z/X/C键),记忆用户选择

// ==UserScript==
// @name         B站/哔哩哔哩/bilibili视频增强脚本-(中文字幕+倍速控制)
// @namespace    http://tampermonkey.net/
// @version      2.0.2
// @description  自动开启中文字幕,提供倍速控制功能(Z/X/C键),记忆用户选择
// @author       anyphasy
// @icon         https://play-lh.googleusercontent.com/C1tISqYgtW_ejAmnGzvepbaYt7NJLagPelCZ_lzNv06RJPQgBx1_q3VX67z9wc48EgY=s1024
// @match        *://www.bilibili.com/video/*
// @match        *://www.bilibili.com/list/watchlater*
// @grant        GM_setValue
// @grant        GM_getValue
// @license MIT
// @grant        GM_addStyle
// ==/UserScript==

(function() {
    'use strict';

    // 配置参数
    const MAX_ATTEMPTS = 10;
    const INITIAL_DELAY = 800;
    const RETRY_DELAY = 2000;
    let attemptCount = 0;
    let isActive = false;
    let isEnabled = GM_getValue('bilibili_subtitle_enabled', true);
    let observer = null;
    let timeoutId = null;
    let playbackRates = [2, 1.5, 1.25, 1, 0.75, 0.5]; // 支持的倍速选项
    let currentRate = GM_getValue('bilibili_playback_rate', 1); // 当前倍速

    // 添加Element Plus样式
    GM_addStyle(`
       .bpx-player-dm-notice {
      position: absolute;
      top: 10%;
      /* 将提示位置稍微下移,避免遮挡视频内容 */
      left: 50%;
      transform: translate(-50%, -50%);
      background-color: rgba(0, 0, 0, 0.8);
      /* 增加背景不透明度 */
      color: white;
      padding: 12px 20px;
      /* 增加内边距,让提示更大 */
      border-radius: 8px;
      /* 增加圆角 */
      font-size: 18px;
      /* 增大字体大小 */
      font-weight: bold;
      /* 加粗字体 */
      z-index: 9999;
      pointer-events: none;
      animation: fadeInOut 2.5s ease-in-out forwards;
      /* 稍微延长动画时间 */
      min-width: 120px;
      /* 设置最小宽度 */
      text-align: center;
      /* 文字居中 */
      box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
      /* 添加阴影效果 */
    }
        @keyframes fadeInOut {
            0% { opacity: 0; }
            10% { opacity: 1; }
            90% { opacity: 1; }
            100% { opacity: 0; }
        }
    `);

    // 初始化
    function init() {
        console.log(`B站增强脚本已加载,字幕状态: ${isEnabled ? '开启' : '关闭'}, 当前倍速: ${currentRate}x`);

        // 清理函数
        const cleanup = () => {
            if (timeoutId) {
                clearTimeout(timeoutId);
                timeoutId = null;
            }
            if (observer) {
                observer.disconnect();
                observer = null;
            }
            document.removeEventListener('keydown', handleKeyPress);
        };

        // 添加页面卸载时的清理
        window.addEventListener('beforeunload', cleanup);

        // 添加键盘监听器
        document.addEventListener('keydown', handleKeyPress);
        console.log('快捷键监听器已添加 (Shift+A:字幕, Z/X/C:倍速)');

        // 设置SPA路由变化监听
        setupSPAObserver();

        // 立即开始尝试(减少延迟)
        if (isEnabled) {
            timeoutId = setTimeout(tryClickChineseSubtitle, INITIAL_DELAY);
        }

        // 设置倍速控制
        timeoutId = setTimeout(applySavedPlaybackRate, INITIAL_DELAY);
    }

    // 设置SPA路由监听
    function setupSPAObserver() {
        let lastUrl = location.href;
        observer = new MutationObserver(() => {
            const currentUrl = location.href;
            if (currentUrl !== lastUrl) {
                lastUrl = currentUrl;
                resetState();
                if (isEnabled) {
                    timeoutId = setTimeout(tryClickChineseSubtitle, INITIAL_DELAY);
                }
                timeoutId = setTimeout(applySavedPlaybackRate, INITIAL_DELAY);
            }
        });
        observer.observe(document, { subtree: true, childList: true });
    }

    // 重置状态
    function resetState() {
        if (timeoutId) {
            clearTimeout(timeoutId);
            timeoutId = null;
        }
        attemptCount = 0;
        isActive = false;
    }

    // 快捷键处理
    function handleKeyPress(event) {
        // Shift+C 切换字幕
        if (event.shiftKey && event.key === 'A') {
            console.log("按下了Shift+A");
            event.preventDefault();
            toggleSubtitles();
        }

        // 倍速控制
        if (!event.ctrlKey && !event.altKey && !event.metaKey) {
            switch (event.key.toLowerCase()) {
                case 'z':
                    console.log("按下了Z键 - 重置倍速");
                    event.preventDefault();
                    setPlaybackRate(1);
                    break;
                case 'x':
                    console.log("按下了X键 - 减小倍速");
                    event.preventDefault();
                    decreasePlaybackRate();
                    break;
                case 'c':
                    console.log("按下了C键 - 增加倍速");
                    event.preventDefault();
                    increasePlaybackRate();
                    break;
            }
        }
    }

    // 切换字幕开关
    function toggleSubtitles() {
        isEnabled = !isEnabled;
        GM_setValue('bilibili_subtitle_enabled', isEnabled);
        console.log(`字幕功能已${isEnabled ? '开启' : '关闭'}`);

        resetState();

        if (isEnabled) {
            timeoutId = setTimeout(tryClickChineseSubtitle, INITIAL_DELAY);
        } else {
            closeSubtitles();
        }
    }

    // 尝试点击中文字幕(优化版)
    function tryClickChineseSubtitle() {
        timeoutId = null;

        if (!isEnabled || isActive || attemptCount >= MAX_ATTEMPTS) {
            return;
        }

        attemptCount++;
        console.log(`尝试点击中文字幕 (第 ${attemptCount} 次)`);

        // 使用更高效的选择器
        const subtitleItems = document.querySelectorAll('.bpx-player-ctrl-subtitle-language-item');
        let foundChinese = false;

        for (const item of subtitleItems) {
            const textElement = item.querySelector('.bpx-player-ctrl-subtitle-language-item-text');
            if (textElement && textElement.textContent.trim() === '中文') {
                foundChinese = true;
                console.log('找到中文字幕按钮');

                if (!item.classList.contains('bpx-state-active')) {
                    console.log('中文字幕未激活,执行点击');
                    textElement.click();

                    // 使用更快的验证机制
                    timeoutId = setTimeout(() => verifyActivation(item), 300);
                } else {
                    console.log('中文字幕已激活');
                    isActive = true;
                }
                break;
            }
        }

        if (!foundChinese && attemptCount < MAX_ATTEMPTS) {
            console.log('未找到中文字幕按钮,稍后重试');
            timeoutId = setTimeout(tryClickChineseSubtitle, RETRY_DELAY);
        } else if (!foundChinese) {
            console.log(`已达到最大尝试次数(${MAX_ATTEMPTS}),可能此视频无中文字幕`);
        }
    }

    // 验证激活状态
    function verifyActivation(item) {
        timeoutId = null;

        if (item.classList.contains('bpx-state-active')) {
            console.log('中文字幕激活成功');
            isActive = true;
        } else {
            console.log('中文字幕激活失败');
            if (attemptCount < MAX_ATTEMPTS) {
                timeoutId = setTimeout(tryClickChineseSubtitle, RETRY_DELAY);
            }
        }
    }

    // 关闭字幕
    function closeSubtitles() {
        const closeButton = document.querySelector('.bpx-player-ctrl-subtitle-close-switch[data-action="close"]');
        if (closeButton && !closeButton.classList.contains('bpx-state-active')) {
            console.log('找到关闭字幕按钮,执行点击');
            closeButton.click();
            isActive = false;
        } else {
            console.log('关闭字幕按钮已激活或未找到');
        }
    }

    // 倍速控制相关函数
    function applySavedPlaybackRate() {
        const rateMenu = document.querySelector('.bpx-player-ctrl-playbackrate-menu');
        if (!rateMenu) {
            if (attemptCount < MAX_ATTEMPTS) {
                timeoutId = setTimeout(applySavedPlaybackRate, RETRY_DELAY);
                attemptCount++;
                return;
            }
            console.log('未找到倍速菜单');
            return;
        }

        const currentActive = rateMenu.querySelector('.bpx-state-active');
        if (currentActive) {
            const activeRate = parseFloat(currentActive.dataset.value);
            if (Math.abs(activeRate - currentRate) > 0.01) {
                console.log(`当前倍速(${activeRate}x)与保存倍速(${currentRate}x)不一致,正在调整`);
                setPlaybackRate(currentRate);
            } else {
                console.log(`当前倍速(${activeRate}x)与保存倍速一致,无需调整`);
            }
        } else {
            console.log('未找到激活的倍速选项,尝试设置');
            setPlaybackRate(currentRate);
        }
    }

    function setPlaybackRate(rate) {
        // 确保rate是支持的倍速
        const supportedRate = playbackRates.find(r => Math.abs(r - rate) < 0.01) || 1;
        currentRate = supportedRate;
        GM_setValue('bilibili_playback_rate', currentRate);

        const rateMenu = document.querySelector('.bpx-player-ctrl-playbackrate-menu');
        if (rateMenu) {
            const items = rateMenu.querySelectorAll('.bpx-player-ctrl-playbackrate-menu-item');
            let found = false;

            items.forEach(item => {
                const itemRate = parseFloat(item.dataset.value);
                if (Math.abs(itemRate - currentRate) < 0.01) {
                    found = true;
                    if (!item.classList.contains('bpx-state-active')) {
                        item.click();
                        console.log(`已设置倍速为 ${currentRate}x`);
                        showRateMessage(`倍速已设为 ${currentRate}x`);
                    }
                }
            });

            if (!found) {
                console.log(`未找到 ${currentRate}x 的倍速选项`);
            }
        } else {
            console.log('未找到倍速菜单');
            if (attemptCount < MAX_ATTEMPTS) {
                timeoutId = setTimeout(() => setPlaybackRate(rate), RETRY_DELAY);
                attemptCount++;
            }
        }
    }

    function increasePlaybackRate() {
        const currentIndex = playbackRates.findIndex(r => Math.abs(r - currentRate) < 0.01);
        if (currentIndex > 0) {
            const newRate = playbackRates[currentIndex - 1];
            setPlaybackRate(newRate);
        } else {
            showRateMessage('已经是最大倍速 (2x)');
            console.log('已经是最大倍速 (2x)');
        }
    }

    function decreasePlaybackRate() {
        const currentIndex = playbackRates.findIndex(r => Math.abs(r - currentRate) < 0.01);
        if (currentIndex < playbackRates.length - 1) {
            const newRate = playbackRates[currentIndex + 1];
            setPlaybackRate(newRate);
        } else {
            showRateMessage('已经是最小倍速 (0.5x)');
            console.log('已经是最小倍速 (0.5x)');
        }
    }

    function showRateMessage(message) {
        // 移除旧的消息
        const oldNotice = document.querySelector('.bpx-player-dm-notice');
        if (oldNotice) oldNotice.remove();

        // 创建新消息元素
        const notice = document.createElement('div');
        notice.className = 'bpx-player-dm-notice';
        notice.textContent = message;

        // 添加到播放器容器
        const playerContainer = document.querySelector('.bpx-player-container') || document.body;
        playerContainer.appendChild(notice);

        // 2秒后自动移除
        setTimeout(() => {
            if (notice.parentNode) {
                notice.parentNode.removeChild(notice);
            }
        }, 2000);
    }

    // 启动脚本
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();