// ==UserScript== // @name TabulaBili Userscript // @namespace https://github.com/tjsky/TabulaBili // @homepageURL https://github.com/Unintendedz/tabulabili-userscript // @supportURL https://github.com/Unintendedz/tabulabili-userscript/issues // @updateURL https://raw.githubusercontent.com/Unintendedz/tabulabili-userscript/main/TabulaBili.user.js // @downloadURL https://raw.githubusercontent.com/Unintendedz/tabulabili-userscript/main/TabulaBili.user.js // @version 1.1.4-userscript.1 // @description 让 B 站首页推荐接口尽量以无 Cookie 方式请求,并自动点击首页“换一换”以替换首屏个性化推荐。 // @author wangdaodao, tjsky; userscript conversion by Codex // @license MIT // @match *://www.bilibili.com/* // @run-at document-start // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant unsafeWindow // ==/UserScript== (function () { 'use strict'; const STORAGE_KEY = 'tabulaBiliEnabled'; const enabled = GM_getValue(STORAGE_KEY, true); GM_registerMenuCommand(enabled ? '关闭 TabulaBili' : '开启 TabulaBili', () => { GM_setValue(STORAGE_KEY, !enabled); location.reload(); }); GM_registerMenuCommand('立即换一换', () => { clickRollButton(true); }); if (!enabled) { return; } injectNetworkPatch(); if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', startAutoRoll, { once: true }); } else { startAutoRoll(); } function injectNetworkPatch() { const pageWindow = typeof unsafeWindow === 'object' ? unsafeWindow : null; if (pageWindow) { try { pagePatch(pageWindow); return; } catch (error) { console.warn('[TabulaBili] unsafeWindow patch failed; falling back to script injection.', error); } } const script = document.createElement('script'); script.textContent = `(${pagePatch.toString()})(window);`; const root = document.documentElement || document.head || document.body; if (root) { root.appendChild(script); script.remove(); } } function pagePatch(win) { 'use strict'; const TAG = '[TabulaBili]'; if (!win || win.__tabulaBiliPatched) { return; } const nativeFetch = win.fetch; const XHR = win.XMLHttpRequest; const RequestCtor = win.Request; const nativeOpen = XHR.prototype.open; const nativeSend = XHR.prototype.send; win.__tabulaBiliPatched = true; function isTargetUrl(input) { try { const url = new win.URL(String(input), win.location.href); if (url.hostname !== 'api.bilibili.com') { return false; } const path = url.pathname; return ( path.includes('/x/web-interface') && path.includes('/index/top') && path.includes('/rcmd') ); } catch (_) { return false; } } function requestUrl(input) { if (input && typeof input === 'object' && 'url' in input) { return input.url; } return input; } if (typeof nativeFetch === 'function') { win.fetch = function tabulaBiliFetch(input, init) { if (!isTargetUrl(requestUrl(input))) { return nativeFetch.apply(this, arguments); } try { if (RequestCtor && input instanceof RequestCtor) { return nativeFetch.call(this, new RequestCtor(input, Object.assign({}, init, { credentials: 'omit' }))); } return nativeFetch.call(this, input, Object.assign({}, init, { credentials: 'omit' })); } catch (error) { win.console.warn(TAG, 'fetch patch failed; falling back to original request.', error); return nativeFetch.apply(this, arguments); } }; } XHR.prototype.open = function tabulaBiliOpen(method, url) { this.__tabulaBiliTarget = isTargetUrl(url); return nativeOpen.apply(this, arguments); }; XHR.prototype.send = function tabulaBiliSend() { if (this.__tabulaBiliTarget) { try { this.withCredentials = false; } catch (_) {} } return nativeSend.apply(this, arguments); }; win.console.info(TAG, 'recommendation feed requests will omit credentials.'); } function startAutoRoll() { if (!isHomePage()) { return; } if (clickRollButton(false)) { return; } const root = document.body || document.documentElement; const observer = new MutationObserver((_, obs) => { if (clickRollButton(false)) { obs.disconnect(); } }); observer.observe(root, { childList: true, subtree: true }); window.setTimeout(() => observer.disconnect(), 8000); } function isHomePage() { return location.hostname === 'www.bilibili.com' && ( location.pathname === '/' || location.pathname === '/index.html' ); } function clickRollButton(force) { const rollButton = document.querySelector('.roll-btn'); if (!rollButton) { return false; } if (!force && window.scrollY >= 100) { console.info('[TabulaBili] roll button loaded after scrolling; skip auto click.'); return true; } rollButton.click(); window.scrollTo({ top: 0, behavior: 'smooth' }); return true; } })();