// ==UserScript== // @name Make Bilibili Better(海外优化版) // @namespace local.make-bilibili-better // @version 1.2.2 // @description 优化哔哩哔哩网页体验,支持海外播放地址优化,适合留学生和海外用户观看 B 站。 // @author ArcherEmiya // @license MIT // @match https://*.bilibili.com/* // @match https://*.bilibili.tv/* // @run-at document-start // @grant unsafeWindow // @grant GM_addStyle // @grant GM_notification // ==/UserScript== (function () { 'use strict'; const CONFIG = { hosts: { homepage: 'www.bilibili.com', dynamic: 't.bilibili.com', live: 'live.bilibili.com', }, paths: { homepage: '/', article: '/read/cv', bangumi: '/bangumi/play/', video: '/video/', }, storage: { dynamicWideDisabled: 'mbb-dynamic-wide-disabled', videoCropEnabled: 'mbb-video-crop-enabled', playbackFlag: 'bilibili_player_force_DolbyAtmos&8K&HDR', hdrFlag: 'bilibili_player_force_hdr', hevcError: 'enableHEVCError', }, playbackCapabilityPaths: [ '/watchlater', '/watchroom', '/medialist', '/list/', '/festival/', '/blackboard/', ], trackingParamRules: [ 'buvid', 'from_spmid', 'is_story_h5', 'launch_id', 'live_from', 'mid', 'session_id', 'timestamp', 'unique_k', 'up_id', 'vd_source', /^share/i, /^spm/i, ], userAgent: { safariMac: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6_1) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Safari/605.1.15', platform: 'MacIntel', }, }; const page = { isHomepage() { return location.host === CONFIG.hosts.homepage && location.pathname === CONFIG.paths.homepage; }, isDynamic() { return location.host === CONFIG.hosts.dynamic; }, isArticle() { return location.host === CONFIG.hosts.homepage && location.pathname.startsWith(CONFIG.paths.article); }, isLive() { return location.host === CONFIG.hosts.live; }, isBangumi() { return location.host === CONFIG.hosts.homepage && location.pathname.startsWith(CONFIG.paths.bangumi); }, isVideo() { return location.host === CONFIG.hosts.homepage && location.pathname.startsWith(CONFIG.paths.video); }, needsPlaybackCapabilityPatch() { if (this.isVideo() || this.isBangumi() || this.isLive()) { return true; } if (location.host !== CONFIG.hosts.homepage) { return false; } return CONFIG.playbackCapabilityPaths.some(prefix => location.pathname.startsWith(prefix)); }, }; const utils = { toArray(listLike) { return Array.from(listLike || []); }, uniqueElements(list) { return this.toArray(new Set(list)); }, getPageWindow() { return typeof unsafeWindow !== 'undefined' ? unsafeWindow : window; }, onBodyReady(callback) { if (document.body) { callback(); return; } let hasRun = false; const runOnce = () => { if (hasRun || !document.body) { return; } hasRun = true; callback(); }; document.addEventListener('DOMContentLoaded', runOnce, { once: true }); const intervalId = setInterval(() => { if (hasRun) { clearInterval(intervalId); return; } if (document.body) { clearInterval(intervalId); runOnce(); } }, 50); }, observeAddedElements(onElement) { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node instanceof HTMLElement) { onElement(node); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); return observer; }, setNavigatorValue(propertyName, value) { const descriptor = Object.getOwnPropertyDescriptor(window.navigator, propertyName); if (descriptor && descriptor.configurable === false) { return; } Object.defineProperty(window.navigator, propertyName, { configurable: true, get() { return value; }, }); }, safeNotification(options) { if (typeof GM_notification === 'function') { GM_notification(options); } }, }; const urlTools = { shouldRemoveTrackingParam(paramName) { return CONFIG.trackingParamRules.some(rule => { if (rule instanceof RegExp) { return rule.test(paramName); } return rule === paramName; }); }, removeTrackingParams(inputUrl) { if (!inputUrl) { return inputUrl; } let parsedUrl; try { parsedUrl = new URL(String(inputUrl), location.href); } catch (error) { return inputUrl; } let changed = false; utils.toArray(parsedUrl.searchParams.keys()).forEach(paramName => { if (this.shouldRemoveTrackingParam(paramName)) { parsedUrl.searchParams.delete(paramName); changed = true; } }); if (!changed) { return inputUrl; } if (/^https?:/i.test(String(inputUrl))) { return parsedUrl.toString(); } return `${parsedUrl.pathname}${parsedUrl.search}${parsedUrl.hash}`; }, }; const modules = { playbackCdnAccelerator: { storageKey: 'biliAccelerator.config.v1', mediaPathPattern: /\.(m4s|mp4|flv|m3u8)(?:$|[?#])/i, ipPattern: /^(?:\d{1,3}\.){3}\d{1,3}$/, xyMcdnPattern: /^xy(?:\d+x){3}\d+xy\.mcdn\.bilivideo\.(?:cn|com|net)$/i, defaultConfig: Object.freeze({ enabled: true, autoAdapt: true, adaptiveLevel: 0, mode: 'bad-only', pcdnHost: 'upos-sz-mirrorcos.bilivideo.com', mcdnStrategy: 'proxy-all', proxyHost: 'proxy-tf-all-ws.bilivideo.com', rewriteAkamai: false, maxDepth: 20, }), state: { rewrites: [], rewriteCount: 0, lastSource: '', playbackIssues: [], adaptiveReason: '', lastAdaptiveAt: 0, }, config: null, nativeJsonParse: null, shouldRun() { return /(^|\.)bilibili\.(com|tv)$/i.test(location.hostname); }, shouldMonitorPlayback() { return page.needsPlaybackCapabilityPatch() || location.hostname.endsWith('.bilibili.tv'); }, getBufferedAhead(video) { if (!(video instanceof HTMLVideoElement) || !video.buffered) { return 0; } const currentTime = video.currentTime || 0; for (let index = 0; index < video.buffered.length; index += 1) { const start = video.buffered.start(index); const end = video.buffered.end(index); if (currentTime >= start && currentTime <= end) { return Math.max(0, end - currentTime); } } return 0; }, isLikelyRealPlaybackIssue(video, reason) { if (!(video instanceof HTMLVideoElement)) { return false; } if (video.paused || video.ended || video.seeking || video.currentTime < 1) { return false; } if (reason === 'error') { return true; } const remaining = Number.isFinite(video.duration) ? video.duration - video.currentTime : 999; if (remaining < 8) { return false; } return video.readyState < HTMLMediaElement.HAVE_FUTURE_DATA || this.getBufferedAhead(video) < 2.5; }, recordPlaybackIssue(root, video, reason) { if (!this.config?.enabled || !this.config.autoAdapt || !this.shouldMonitorPlayback()) { return; } if (!this.isLikelyRealPlaybackIssue(video, reason)) { return; } const now = Date.now(); this.state.playbackIssues.push({ at: now, reason, bufferedAhead: this.getBufferedAhead(video), readyState: video.readyState, }); this.state.playbackIssues = this.state.playbackIssues.filter(item => now - item.at <= 45000); const issueCount = this.state.playbackIssues.length; const hasHardFailure = reason === 'error'; if (issueCount >= 3 || hasHardFailure) { this.adaptPlaybackStrategy(root, `${reason}:${issueCount}`); } }, adaptPlaybackStrategy(root, reason) { const now = Date.now(); if (now - this.state.lastAdaptiveAt < 60000) { return; } if (this.config.adaptiveLevel >= 1 && this.config.mode === 'force' && this.config.rewriteAkamai) { return; } this.state.lastAdaptiveAt = now; this.state.adaptiveReason = reason; this.saveConfig(root, Object.assign({}, this.config, { adaptiveLevel: 1, mode: 'force', mcdnStrategy: 'proxy-all', rewriteAkamai: true, })); console.info('[Make Bilibili Better] playback CDN strategy escalated after stutter', { reason, config: this.config, }); this.reloadPlaybackOnce(root, reason); }, reloadPlaybackOnce(root, reason) { let markerKey = ''; try { markerKey = `mbb-accelerator-adapted:${location.origin}${location.pathname}${location.search}`; if (root.sessionStorage.getItem(markerKey) === '1') { return; } root.sessionStorage.setItem(markerKey, '1'); } catch (error) { // Continue without loop protection if sessionStorage is blocked. } setTimeout(() => { try { root.location.reload(); } catch (error) { console.warn('[Make Bilibili Better] playback reload after CDN strategy change failed', reason, error); } }, 800); }, collectVideos(root = document) { const videos = []; if (root instanceof HTMLVideoElement) { videos.push(root); } if (typeof root.querySelectorAll === 'function') { videos.push(...root.querySelectorAll('video')); } return utils.uniqueElements(videos); }, attachPlaybackMonitor(root, target = document) { this.collectVideos(target).forEach(video => { if (!(video instanceof HTMLVideoElement) || video.dataset.mbbCdnMonitorBound === '1') { return; } video.dataset.mbbCdnMonitorBound = '1'; video.addEventListener('waiting', () => this.recordPlaybackIssue(root, video, 'waiting')); video.addEventListener('stalled', () => this.recordPlaybackIssue(root, video, 'stalled')); video.addEventListener('error', () => this.recordPlaybackIssue(root, video, 'error')); setInterval(() => { if (!video.isConnected) { return; } if (!video.paused && !video.ended && this.getBufferedAhead(video) < 1.5 && video.readyState < HTMLMediaElement.HAVE_ENOUGH_DATA) { this.recordPlaybackIssue(root, video, 'low-buffer'); } }, 4000); }); }, normalizeConfig(config) { return Object.assign({}, this.defaultConfig, config || {}); }, loadConfig(root) { try { const stored = root.localStorage.getItem(this.storageKey); return this.normalizeConfig(stored ? JSON.parse(stored) : null); } catch (error) { return this.normalizeConfig(); } }, saveConfig(root, nextConfig) { this.config = this.normalizeConfig(nextConfig); root.localStorage.setItem(this.storageKey, JSON.stringify(this.config)); }, hasBiliMediaSignal(value) { return typeof value === 'string' && ( value.includes('bilivideo') || value.includes('akamaized.net') || value.includes('szbdyd.com') || value.includes('/upgcxcode/') || value.includes('/v1/resource/') ); }, parseUrl(value) { if (!this.hasBiliMediaSignal(value)) { return null; } try { const url = new URL(value); if (url.protocol !== 'http:' && url.protocol !== 'https:') { return null; } return url; } catch (error) { return null; } }, isMediaUrl(url) { return this.mediaPathPattern.test(url.pathname + url.search) || url.pathname.startsWith('/upgcxcode/') || url.pathname.startsWith('/v1/resource/'); }, isMcdnHost(hostname) { return /\.mcdn\.bilivideo\.(?:cn|com|net)$/i.test(hostname); }, isPcdnHost(url) { return this.ipPattern.test(url.hostname) || this.xyMcdnPattern.test(url.hostname) || this.isMcdnHost(url.hostname); }, isBiliCdnHost(hostname) { return hostname.endsWith('.bilivideo.com') || hostname.endsWith('.bilivideo.cn') || hostname.endsWith('.bilivideo.net') || hostname.endsWith('.akamaized.net'); }, isKnownSlowHost(url, config) { const hostname = url.hostname.toLowerCase(); if (this.isPcdnHost(url) || hostname.endsWith('.szbdyd.com')) { return true; } if (hostname.includes('mirroraliov') || hostname.includes('mirrorcosov') || hostname.includes('mirrorhwov')) { return true; } return config.rewriteAkamai && hostname.endsWith('.akamaized.net'); }, cleanHost(host) { const trimmed = String(host || '').trim(); return trimmed.replace(/^https?:\/\//i, '').replace(/\/.*$/, ''); }, replaceHost(url, host) { const cleanedHost = this.cleanHost(host); if (!cleanedHost) { return url.toString(); } const next = new URL(url.toString()); next.protocol = 'https:'; next.host = cleanedHost; if (!cleanedHost.includes(':')) { next.port = ''; } return next.toString(); }, proxyUrl(url, config) { const next = new URL(`https://${this.cleanHost(config.proxyHost)}/`); next.searchParams.set('url', url.toString()); return next.toString(); }, shouldProxyMcdn(url, config) { if (!this.isMcdnHost(url.hostname)) { return false; } if (config.mcdnStrategy === 'proxy-all') { return true; } return config.mcdnStrategy === 'proxy-v1' && url.pathname.startsWith('/v1/resource/'); }, rewriteUrlDetail(value, rawConfig) { const config = this.normalizeConfig(rawConfig); const original = String(value || ''); const url = this.parseUrl(original); if (!config.enabled || !url || !this.isMediaUrl(url) || url.hostname === this.cleanHost(config.proxyHost)) { return { changed: false, original, url: original, reason: 'ignored', }; } if (url.hostname.endsWith('.szbdyd.com')) { const source = url.searchParams.get('xy_usource'); if (source) { const rewritten = this.replaceHost(url, source); return { changed: rewritten !== original, original, url: rewritten, reason: 'szbdyd-source', targetHost: this.cleanHost(source), }; } } if (this.shouldProxyMcdn(url, config)) { const rewritten = this.proxyUrl(url, config); return { changed: rewritten !== original, original, url: rewritten, reason: 'mcdn-proxy', targetHost: this.cleanHost(config.proxyHost), }; } const force = config.mode === 'force'; if (this.isKnownSlowHost(url, config) || (force && this.isBiliCdnHost(url.hostname))) { const rewritten = this.replaceHost(url, config.pcdnHost); return { changed: rewritten !== original, original, url: rewritten, reason: this.isPcdnHost(url) ? 'pcdn-host' : 'cdn-host', targetHost: this.cleanHost(config.pcdnHost), }; } return { changed: false, original, url: original, reason: 'ok', }; }, rewriteObject(value, rawConfig, state, depth, seen) { const config = this.normalizeConfig(rawConfig); const tracker = state || { changed: false, rewrites: [] }; const level = depth || 0; const visited = seen || new WeakSet(); if (!config.enabled || value == null || level > config.maxDepth) { return value; } if (typeof value === 'string') { const detail = this.rewriteUrlDetail(value, config); if (detail.changed) { tracker.changed = true; tracker.rewrites.push(detail); } return detail.url; } if (typeof value !== 'object') { return value; } if (visited.has(value)) { return value; } visited.add(value); if (Array.isArray(value)) { for (let index = 0; index < value.length; index += 1) { value[index] = this.rewriteObject(value[index], config, tracker, level + 1, visited); } return value; } Object.keys(value).forEach(key => { value[key] = this.rewriteObject(value[key], config, tracker, level + 1, visited); }); return value; }, rewritePayload(payload, source) { const tracker = { changed: false, rewrites: [] }; try { const rewritten = this.rewriteObject(payload, this.config, tracker); this.record(tracker.rewrites, source); return rewritten; } catch (error) { console.warn('[Make Bilibili Better] playback CDN rewrite failed', error); return payload; } }, record(rewrites, source) { if (!rewrites || rewrites.length === 0) { return; } this.state.lastSource = source; this.state.rewriteCount += rewrites.length; this.state.rewrites = this.state.rewrites.concat(rewrites.map(item => ({ at: new Date().toISOString(), source, reason: item.reason, targetHost: item.targetHost, from: item.original, to: item.url, }))).slice(-50); this.renderStatus(); }, isInterestingFetch(input) { const url = typeof input === 'string' ? input : input?.url; return typeof url === 'string' && ( url.includes('/x/player') || url.includes('/pgc/player') || url.includes('playurl') || url.includes('bilivideo') ); }, patchJsonParse(root) { if (!root.JSON || root.JSON.parse.__mbbAcceleratorPatched === '1') { return; } const accelerator = this; root.JSON.parse = function patchedJsonParse(text) { const parsed = accelerator.nativeJsonParse.apply(this, arguments); if (typeof text === 'string' && text.includes('bilivideo')) { return accelerator.rewritePayload(parsed, 'JSON.parse'); } return parsed; }; Object.defineProperty(root.JSON.parse, '__mbbAcceleratorPatched', { configurable: true, value: '1', }); }, patchFetch(root) { if (!root.fetch || root.fetch.__mbbAcceleratorPatched === '1') { return; } const accelerator = this; const nativeFetch = root.fetch; root.fetch = function patchedFetch() { const args = arguments; return nativeFetch.apply(this, args).then(response => { if (!accelerator.config.enabled || !accelerator.isInterestingFetch(args[0])) { return response; } const contentType = response.headers?.get('content-type'); if (contentType && !contentType.includes('json') && !contentType.includes('text')) { return response; } return response.clone().text().then(text => { if (!text || !text.includes('bilivideo')) { return response; } let parsed; const tracker = { changed: false, rewrites: [] }; try { parsed = accelerator.nativeJsonParse(text); accelerator.rewriteObject(parsed, accelerator.config, tracker); } catch (error) { return response; } if (!tracker.changed) { return response; } accelerator.record(tracker.rewrites, 'fetch'); const HeadersCtor = root.Headers || Headers; const ResponseCtor = root.Response || Response; const headers = new HeadersCtor(response.headers); headers.delete('content-length'); return new ResponseCtor(JSON.stringify(parsed), { status: response.status, statusText: response.statusText, headers, }); }).catch(() => response); }); }; Object.defineProperty(root.fetch, '__mbbAcceleratorPatched', { configurable: true, value: '1', }); }, patchGlobalPlayInfo(root, name) { let currentValue; const existing = Object.getOwnPropertyDescriptor(root, name); if (existing && existing.configurable === false) { return; } if (existing && 'value' in existing) { currentValue = this.rewritePayload(existing.value, name); } try { Object.defineProperty(root, name, { configurable: true, enumerable: true, get() { return currentValue; }, set: value => { currentValue = this.rewritePayload(value, name); }, }); } catch (error) { if (root[name]) { root[name] = this.rewritePayload(root[name], name); } } }, renderStatus() { // No UI is installed; status is available through MBBPlaybackAccelerator.getStats(). }, install() { const root = utils.getPageWindow(); if (root.__MBB_BILI_ACCELERATOR_INSTALLED__) { return; } root.__MBB_BILI_ACCELERATOR_INSTALLED__ = true; this.config = this.loadConfig(root); this.nativeJsonParse = root.JSON?.parse || JSON.parse; this.patchJsonParse(root); this.patchFetch(root); this.patchGlobalPlayInfo(root, '__playinfo__'); this.patchGlobalPlayInfo(root, '__INITIAL_STATE__'); root.MBBPlaybackAccelerator = { getConfig: () => Object.assign({}, this.config), setConfig: nextConfig => { this.saveConfig(root, Object.assign({}, this.config, nextConfig || {})); this.renderStatus(); return Object.assign({}, this.config); }, getStats: () => JSON.parse(JSON.stringify(this.state)), rewriteUrl: url => this.rewriteUrlDetail(url, this.config).url, }; if (this.shouldMonitorPlayback()) { utils.onBodyReady(() => { this.attachPlaybackMonitor(root); utils.observeAddedElements(node => this.attachPlaybackMonitor(root, node)); }); } console.info('[Make Bilibili Better] adaptive playback CDN accelerator installed', root.MBBPlaybackAccelerator.getConfig()); }, }, trackingCleanup: { shouldRun() { return true; }, install() { const pageWindow = utils.getPageWindow(); const { history } = pageWindow; const cleanedCurrentUrl = urlTools.removeTrackingParams(location.href); if (typeof cleanedCurrentUrl === 'string' && cleanedCurrentUrl !== location.href) { history.replaceState(history.state, '', cleanedCurrentUrl); } const originalPushState = history.pushState.bind(history); history.pushState = function (state, unused, url) { const cleanedUrl = typeof url === 'string' ? urlTools.removeTrackingParams(url) : url; return originalPushState(state, unused, cleanedUrl); }; const originalReplaceState = history.replaceState.bind(history); history.replaceState = function (state, unused, url) { const cleanedUrl = typeof url === 'string' ? urlTools.removeTrackingParams(url) : url; return originalReplaceState(state, unused, cleanedUrl); }; }, }, playbackCapabilityUnlocks: { shouldRun() { return page.needsPlaybackCapabilityPatch(); }, install() { try { localStorage.setItem(CONFIG.storage.playbackFlag, '1'); localStorage.setItem(CONFIG.storage.hdrFlag, '1'); } catch (error) { // Ignore storage write failures on restricted pages. } const storageProto = Object.getPrototypeOf(sessionStorage); if (storageProto && storageProto.__mbbHevcPatched !== '1') { const originalGetItem = storageProto.getItem; storageProto.getItem = function (key) { if (this === sessionStorage && key === CONFIG.storage.hevcError) { return undefined; } return originalGetItem.call(this, key); }; Object.defineProperty(storageProto, '__mbbHevcPatched', { configurable: true, value: '1', }); } utils.setNavigatorValue('userAgent', CONFIG.userAgent.safariMac); utils.setNavigatorValue('platform', CONFIG.userAgent.platform); }, }, homepageCleanup: { shouldRun() { return page.isHomepage(); }, collectFeedCards(root = document) { const cards = []; if (root instanceof HTMLElement && root.matches('.feed-card, .bili-feed-card')) { cards.push(root); } if (typeof root.querySelectorAll === 'function') { cards.push(...root.querySelectorAll('.feed-card, .bili-feed-card')); } return utils.uniqueElements(cards); }, hasAdMarkers(card) { return Boolean( card.querySelector('a[href*="cm.bilibili.com"]') || card.querySelector('.ad-report') ); }, isEmptyRecommendationCard(card) { const videoCard = card.matches('.bili-video-card') ? card : card.querySelector('.bili-video-card.is-rcmd'); if (!(videoCard instanceof HTMLElement) || !videoCard.matches('.bili-video-card.is-rcmd')) { return false; } if (videoCard.querySelector('.bili-video-card__wrap, .bili-video-card__info, .bili-video-card__image--link, .bili-video-card__skeleton')) { return false; } const elementChildren = utils.toArray(videoCard.children); return elementChildren.length === 1 && elementChildren[0].tagName === 'DIV'; }, cleanup(root = document) { this.collectFeedCards(root).forEach(card => { if (this.hasAdMarkers(card) || this.isEmptyRecommendationCard(card)) { card.remove(); } }); }, install() { GM_addStyle([ '.feed2 .container > * { margin-top: 0 !important; }', '.feed-card:has(a[href*="cm.bilibili.com"]) { display: none !important; }', '.feed-card:has(.ad-report) { display: none !important; }', ].join('\n')); this.cleanup(); let attempts = 0; const intervalId = setInterval(() => { this.cleanup(); attempts += 1; if (attempts >= 100) { clearInterval(intervalId); } }, 300); utils.observeAddedElements(node => this.cleanup(node)); }, }, dynamicWideMode: { shouldRun() { return page.isDynamic(); }, isEnabled() { return localStorage.getItem(CONFIG.storage.dynamicWideDisabled) !== '1'; }, applyState(enabled) { document.documentElement.toggleAttribute('data-mbb-wide', enabled); }, toggle() { const nextEnabled = !this.isEnabled(); if (nextEnabled) { localStorage.removeItem(CONFIG.storage.dynamicWideDisabled); } else { localStorage.setItem(CONFIG.storage.dynamicWideDisabled, '1'); } this.applyState(nextEnabled); }, createButton() { const button = document.createElement('button'); button.id = 'mbb-wide-mode-switch'; button.type = 'button'; button.className = 'mbb-wide-mode-switch'; button.textContent = '宽屏'; button.addEventListener('click', () => this.toggle()); return button; }, install() { GM_addStyle([ 'html[data-mbb-wide] #app { display: flex; }', 'html[data-mbb-wide] .bili-dyn-home--member { box-sizing: border-box; width: 100%; flex: 1; padding: 0 10px; }', 'html[data-mbb-wide] .bili-dyn-content { width: auto; }', 'html[data-mbb-wide] main { width: auto; flex: 1; margin: 0 8px; overflow: hidden; }', '.mbb-wide-mode-switch { position: fixed; right: 16px; bottom: 24px; z-index: 9999; box-sizing: border-box; min-width: 76px; padding: 8px 14px; border: 1px solid #e3e5e7; border-radius: 6px; background: #fff; color: #18191c; font: inherit; line-height: 20px; text-align: center; cursor: pointer; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08); }', '.mbb-wide-mode-switch:hover { border-color: #c9ccd0; background: #fff; color: #18191c; }', 'html[data-mbb-wide] .mbb-wide-mode-switch { border-color: transparent; background: #fb7299; color: #fff; box-shadow: 0 6px 16px rgba(251, 114, 153, 0.28); }', 'html[data-mbb-wide] .mbb-wide-mode-switch:hover { background: #fc86a8; color: #fff; }', '.bili-dyn-list__item:has(.bili-dyn-card-goods), .bili-dyn-list__item:has(.bili-rich-text-module.goods) { display: none !important; }', ].join('\n')); this.applyState(this.isEnabled()); if (!document.getElementById('mbb-wide-mode-switch')) { document.body.appendChild(this.createButton()); } }, }, articleCopy: { shouldRun() { return page.isArticle(); }, unlock(root = document) { const articleHolders = []; if (root instanceof HTMLElement && root.matches('.article-holder')) { articleHolders.push(root); } if (typeof root.querySelectorAll === 'function') { articleHolders.push(...root.querySelectorAll('.article-holder')); } articleHolders.forEach(holder => { holder.classList.remove('unable-reprint'); }); }, install() { document.addEventListener('copy', event => { if (event.target instanceof Node && event.target.parentElement?.closest('.article-holder')) { event.stopImmediatePropagation(); } }, true); this.unlock(); utils.observeAddedElements(node => this.unlock(node)); }, }, videoCropMode: { shouldRun() { return page.isVideo(); }, isEnabled() { return localStorage.getItem(CONFIG.storage.videoCropEnabled) === '1'; }, applyState(enabled) { document.body.toggleAttribute('data-mbb-video-crop', enabled); }, setEnabled(enabled) { if (enabled) { localStorage.setItem(CONFIG.storage.videoCropEnabled, '1'); } else { localStorage.removeItem(CONFIG.storage.videoCropEnabled); } this.applyState(enabled); }, updateInput(input) { if (input instanceof HTMLInputElement) { input.checked = this.isEnabled(); } }, createSwitchItem() { const item = document.createElement('div'); item.className = 'bpx-player-ctrl-setting-fit-mode bui bui-switch'; item.innerHTML = ''; const input = item.querySelector('input'); this.updateInput(input); input?.addEventListener('change', event => { this.setEnabled(Boolean(event.target?.checked)); }); return item; }, mountSwitch() { const existingInput = document.querySelector('.bpx-player-ctrl-setting-fit-mode input'); if (existingInput) { this.updateInput(existingInput); return true; } const parent = document.querySelector('.bpx-player-ctrl-setting-menu-left'); if (!(parent instanceof HTMLElement)) { return false; } const item = this.createSwitchItem(); const moreButton = document.querySelector('.bpx-player-ctrl-setting-more'); if (moreButton instanceof HTMLElement && moreButton.parentElement === parent) { parent.insertBefore(item, moreButton); } else { parent.appendChild(item); } const panelItem = document.querySelector('.bpx-player-ctrl-setting-box .bui-panel-item'); if (panelItem instanceof HTMLElement) { panelItem.style.height = ''; } return true; }, install() { GM_addStyle([ 'body[data-mbb-video-crop] #bilibili-player video { object-fit: cover !important; }', '.bpx-player-ctrl-setting-fit-mode { display: flex; width: 100%; height: 32px; line-height: 32px; }', '.bpx-player-ctrl-setting-box .bui-panel-wrap, .bpx-player-ctrl-setting-box .bui-panel-item { min-height: 172px !important; }', ].join('\n')); this.applyState(this.isEnabled()); let attempts = 0; const timer = setInterval(() => { attempts += 1; if (this.mountSwitch() || attempts >= 200) { clearInterval(timer); } }, 200); }, }, liveEnhancements: { shouldRun() { return page.isLive(); }, install() { GM_addStyle([ '#welcome-area-bottom-vm { display: none !important; }', '.web-player-icon-roomStatus { display: none !important; }', ].join('\n')); const errorTimestamps = []; let hasShownFailureNotice = false; const recordPlaybackFailure = () => { const now = Date.now(); errorTimestamps.push(now); while (errorTimestamps.length && now - errorTimestamps[0] > 30000) { errorTimestamps.shift(); } if (hasShownFailureNotice || errorTimestamps.length < 5) { return; } hasShownFailureNotice = true; utils.safeNotification({ title: '直播播放异常', text: '检测到直播多次播放失败,最高清晰度可能不可用,建议尝试切换到较低画质。', timeout: 4000, silent: true, }); }; const attachVideoWatchers = root => { const videos = []; if (root instanceof HTMLVideoElement) { videos.push(root); } if (typeof root.querySelectorAll === 'function') { videos.push(...root.querySelectorAll('video')); } utils.uniqueElements(videos).forEach(video => { if (!(video instanceof HTMLVideoElement) || video.dataset.mbbLiveBound === '1') { return; } video.dataset.mbbLiveBound = '1'; video.addEventListener('error', recordPlaybackFailure); video.addEventListener('stalled', recordPlaybackFailure); video.addEventListener('abort', recordPlaybackFailure); }); }; attachVideoWatchers(document); utils.observeAddedElements(node => attachVideoWatchers(node)); }, }, bangumiEnhancements: { shouldRun() { return page.isBangumi(); }, install() { const attachVideoRecovery = root => { const videos = []; if (root instanceof HTMLVideoElement) { videos.push(root); } if (typeof root.querySelectorAll === 'function') { videos.push(...root.querySelectorAll('video')); } utils.uniqueElements(videos).forEach(video => { if (!(video instanceof HTMLVideoElement) || video.dataset.mbbBangumiBound === '1') { return; } video.dataset.mbbBangumiBound = '1'; let hasRetried = false; video.addEventListener('error', () => { if (hasRetried) { return; } hasRetried = true; const currentTime = Number.isFinite(video.currentTime) ? video.currentTime : 0; try { video.load(); video.addEventListener('loadedmetadata', () => { if (currentTime > 0) { try { video.currentTime = currentTime; } catch (error) { // Ignore seek failures on recovery. } } video.play().catch(() => { }); }, { once: true }); } catch (error) { // Ignore recovery failures and let the player handle the final state. } }); }); }; attachVideoRecovery(document); utils.observeAddedElements(node => attachVideoRecovery(node)); }, }, }; const startup = { earlyModules: [ modules.playbackCdnAccelerator, modules.trackingCleanup, modules.playbackCapabilityUnlocks, ], bodyModules: [ modules.homepageCleanup, modules.dynamicWideMode, modules.articleCopy, modules.videoCropMode, modules.liveEnhancements, modules.bangumiEnhancements, ], run(list) { list.forEach(module => { if (module.shouldRun()) { module.install(); } }); }, boot() { this.run(this.earlyModules); utils.onBodyReady(() => this.run(this.bodyModules)); }, }; startup.boot(); })();