// ==UserScript== // @name B站自动开启中文字幕 // @version 1.2.0 // @description 自动打开B站视频的中文字幕,支持按粉丝数过滤 // @author ShuiYun // @match *://*.bilibili.com/video/* // @match *://*.bilibili.com/bangumi/play/* // @match *://*.bilibili.com/list/* // @grant GM_setValue // @grant GM_getValue // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @connect api.bilibili.com // @run-at document-start // ==/UserScript== (function() { 'use strict'; // 默认配置 const DEFAULT_CONFIG = { // 中文字幕的语言代码(按优先级排序) chineseLangCodes: ['zh-CN', 'zh-TW', 'ai-zh', 'zh-Hans', 'zh-Hant'], // 轮询间隔(毫秒) pollInterval: 100, // 最大等待时间(毫秒) maxWait: 15000, // 粉丝过滤配置 followerFilter: { enabled: true, // 功能开关,默认开启 threshold: 100000, // 粉丝数阈值,默认10万 }, }; // 读取配置(合并默认值和存储值) function loadConfig() { const saved = GM_getValue('config', {}); return { ...DEFAULT_CONFIG, ...saved, followerFilter: { ...DEFAULT_CONFIG.followerFilter, ...(saved.followerFilter || {}), }, }; } // 保存配置 function saveConfig(config) { GM_setValue('config', config); } // 获取当前配置 let CONFIG = loadConfig(); // 选择器 const SELECTORS = { subtitleBtn: '.bpx-player-ctrl-subtitle', subtitleBtnInner: '.bpx-player-ctrl-subtitle > div > span', closeSwitch: '.bpx-player-ctrl-subtitle-close-switch', langItem: '.bpx-player-ctrl-subtitle-language-item[data-lan]', activeLangItem: '.bpx-player-ctrl-subtitle-language-item.bpx-state-active', }; // 快速轮询等待元素 function waitForElement(selector, timeout = CONFIG.maxWait) { return new Promise((resolve, reject) => { const start = Date.now(); function check() { const el = document.querySelector(selector); if (el) { resolve(el); return; } if (Date.now() - start > timeout) { reject(new Error(`超时: ${selector}`)); return; } requestAnimationFrame(check); } check(); }); } // 检查字幕是否已开启 function isSubtitleEnabled() { const closeSwitch = document.querySelector(SELECTORS.closeSwitch); if (!closeSwitch) return null; return !closeSwitch.classList.contains('bpx-state-active'); } // 开启字幕 function enableSubtitle() { const closeSwitch = document.querySelector(SELECTORS.closeSwitch); if (!closeSwitch) return false; if (closeSwitch.classList.contains('bpx-state-active')) { closeSwitch.click(); return true; } return false; // 已经开启 } // 选择中文字幕 function selectChineseSubtitle() { const langItems = document.querySelectorAll(SELECTORS.langItem); if (!langItems || langItems.length === 0) return false; // 按优先级匹配中文字幕 for (const langCode of CONFIG.chineseLangCodes) { for (const item of langItems) { if (item.dataset.lan === langCode) { if (!item.classList.contains('bpx-state-active')) { item.click(); } return true; } } } // 模糊匹配 for (const item of langItems) { const lan = (item.dataset.lan || '').toLowerCase(); if (lan.includes('zh')) { if (!item.classList.contains('bpx-state-active')) { item.click(); } return true; } } // 没有找到中文字幕,关闭字幕 const closeSwitch = document.querySelector(SELECTORS.closeSwitch); if (closeSwitch && !closeSwitch.classList.contains('bpx-state-active')) { closeSwitch.click(); } return false; } // 获取当前视频的UP主uid function getUploaderUid() { try { // 方法1:从 __INITIAL_STATE__ 获取(B站视频页面的标准数据源) if (window.__INITIAL_STATE__ && window.__INITIAL_STATE__.videoData) { return window.__INITIAL_STATE__.videoData.owner?.mid; } // 方法2:从页面元素获取(备用方案) const upLink = document.querySelector('.up-name[href*="/space.bilibili.com/"]'); if (upLink) { const match = upLink.href.match(/space\.bilibili\.com\/(\d+)/); return match ? parseInt(match[1]) : null; } } catch (e) { return null; } return null; } // 获取UP主粉丝数 function getFollowerCount(uid) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: 'GET', url: `https://api.bilibili.com/x/relation/stat?vmid=${uid}`, onload: (response) => { try { const data = JSON.parse(response.responseText); if (data.code === 0) { resolve(data.data.follower); } else { reject(new Error('API返回错误')); } } catch (e) { reject(e); } }, onerror: reject, }); }); } // 检查是否应该跳过字幕开启 async function shouldSkipByFollower() { // 如果功能未开启,不跳过 if (!CONFIG.followerFilter.enabled) { return false; } const uid = getUploaderUid(); if (!uid) { return false; // 无法获取uid,不跳过 } try { const followerCount = await getFollowerCount(uid); return followerCount > CONFIG.followerFilter.threshold; } catch (e) { return false; // 获取失败,不跳过 } } // 核心逻辑:一次性完成所有操作 async function run() { try { // 粉丝数过滤检查 const shouldSkip = await shouldSkipByFollower(); if (shouldSkip) { console.log('[B站字幕] UP主粉丝数超过阈值,跳过自动开启字幕'); return; } // 等待字幕按钮出现 await waitForElement(SELECTORS.subtitleBtn); // 点击字幕按钮打开菜单 const btn = document.querySelector(SELECTORS.subtitleBtnInner) || document.querySelector(SELECTORS.subtitleBtn); if (!btn) return; btn.click(); // 等待菜单元素出现(使用高频轮询) await waitForElement(SELECTORS.closeSwitch, 3000); // 检查字幕状态 const enabled = isSubtitleEnabled(); if (enabled === null) { // 无字幕 btn.click(); // 关闭菜单 return; } if (!enabled) { // 字幕关闭,开启它 enableSubtitle(); // 等待字幕开启后语言选项出现 await waitForElement(SELECTORS.langItem, 2000); } // 选择中文字幕 selectChineseSubtitle(); // 关闭菜单 setTimeout(() => { const player = document.querySelector('.bpx-player-container') || document.querySelector('.bpx-player'); if (player) player.click(); }, 50); } catch (e) { // 静默处理 } } // 延迟执行函数 function debounce(fn, delay) { let timer; return function(...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } // 页面切换时重新执行 const debouncedRun = debounce(run, 300); // 监听URL变化(SPA页面) let lastUrl = location.href; const observeUrl = () => { const url = location.href; if (url !== lastUrl) { lastUrl = url; debouncedRun(); } }; // 使用更高效的监听方式 const pushState = history.pushState; history.pushState = function() { pushState.apply(this, arguments); observeUrl(); }; const replaceState = history.replaceState; history.replaceState = function() { replaceState.apply(this, arguments); observeUrl(); }; window.addEventListener('popstate', observeUrl); // 监听视频加载 function attachVideoListener() { const video = document.querySelector('video'); if (video && !video._subtitleAttached) { video._subtitleAttached = true; video.addEventListener('loadeddata', () => { debouncedRun(); }, { once: false }); } } // 使用 MutationObserver 监听视频元素 const videoObserver = new MutationObserver(() => { attachVideoListener(); }); // 打开设置对话框 function openSettings() { const current = loadConfig(); const statusText = current.followerFilter.enabled ? '开启' : '关闭'; const statusIcon = current.followerFilter.enabled ? '✅' : '❌'; const input = prompt( `B站字幕设置\n\n` + `${statusIcon} 粉丝过滤:${statusText}\n` + `📊 粉丝阈值:${current.followerFilter.threshold}\n\n` + `请输入设置(输入后按确定):\n` + `• 输入数字 → 设置阈值(如 100000)\n` + `• 输入 on/off → 开启/关闭过滤`, '100000' ); if (input === null) return; // 用户取消 const trimmed = input.trim().toLowerCase(); // 判断是开关命令还是阈值命令 if (trimmed === 'on' || trimmed === 'off') { current.followerFilter.enabled = (trimmed === 'on'); saveConfig(current); CONFIG = current; alert(`粉丝过滤已${trimmed === 'on' ? '开启' : '关闭'}`); } else { const threshold = parseInt(trimmed); if (isNaN(threshold) || threshold < 0) { alert('请输入有效的正整数或 on/off'); return; } current.followerFilter.threshold = threshold; saveConfig(current); CONFIG = current; alert(`阈值已设置为:${threshold}`); } // 刷新页面以应用新设置 location.reload(); } // 注册Tampermonkey菜单 function registerMenu() { const current = loadConfig(); const statusIcon = current.followerFilter.enabled ? '✅' : '❌'; GM_registerMenuCommand(`${statusIcon} 字幕设置`, openSettings); } // 页面加载后启动 function init() { registerMenu(); // 注册设置菜单 attachVideoListener(); videoObserver.observe(document.body, { childList: true, subtree: true }); run(); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } })();