// ==UserScript== // @name Pure Douyu 斗鱼纯净版 // @namespace PureDouyu // @version 0.1.5 // @description 净化斗鱼网页版,屏蔽广告/无用模块,提供清爽流畅的观看体验。 *部分代码参考了douyuEx // @author DreamNya // @match https://www.douyu.com/beta/* // @icon https://www.douyu.com/favicon.ico // @grant unsafeWindow // @run-at doucment-start // @license MIT // @noframes // ==/UserScript== // @grant unsafeWindow 能够防止网页原生API被斗鱼污染、顺便兼容未来可能加入的GM_xmlhttpRequest // unsafeWindow.console = console; const blackList = [ ".ToolbarCardModule.ToollBarCardModule", // 播放器下方互动玩法 ".interactEntry", // 互动玩法展开按钮 ".InteractItem", // 互动玩法按钮 ".ToolbarGiftArea-giftShowList", // 播放器下方付费礼物 ".ToolbarGiftArea-arrowMoreEnter", // 付费礼物展开按钮 "button:has(+#js-player-toolbar)", // 全屏展开礼物按钮 "#js-player-controlbar div[class^='right-'] > div.wonderful-da6c1a", // 播放器录像按钮 "#js-player-controlbar div[class^='right-'] > i:has(path[d='M18 8H8v16h16V14'])", // 播放器问题反馈按钮 "#js-player-controlbar div[class^='right-'] > div.msrceen-14bd72", // 播放器小屏观看按钮 "#js-player-controlbar div[class^='right-'] > div:has(path[d='M24 14.357V7H6v18h7.143'])", // 播放器画中画按钮 ".react-draggable", // 所有可拖动广告 "#js-room-activity", // 右侧超级粉丝团悬浮广告 ".NewGiftBarrageBanner", // 右侧礼物横幅 ".VideoBarrageBanner", // 播放区礼物横幅 "div:has(>.customBarrage)", // 超级弹幕头 "#player-marvel-controller~div[class^='watermark']", //房间号水印 ".Header-menu-game", // header 游戏 ".Header-menu-match", // header 赛事 ".Header-download-wrap", // header 下载 ".Header-broadcast-wrap", // header 开播 ".Header-createcenter-wrap", // header 创作中心 ".Header-taskentry-wrap", // header 任务 ".bc-wrapper", // 暴雪战网绑定入口 ".wm-general", // 推广横幅 "#room-html5-player img[class^='super-user-icon-']", // 超级弹幕头像 "#room-html5-player img[class^='super-noble-icon-']", // 超级弹幕头像框 "#room-html5-player img[class^='super-tail-']", // 超级弹幕尾巴 "#room-html5-player img[class^='vip-icon-']", // 钻粉弹幕尾巴图标 "#room-html5-player img[class^='vipIcon-']", // 钻粉弹幕尾巴图标 "#room-html5-player [class^='super-text-'] img", // 超级弹幕图标 "#room-html5-player img[class^='headpic-']", // 普通弹幕图标 ".dy-Modal-mask", // 模态框蒙版 "#js-layout-fixed-buff", // 半悬浮广告 "#yuba-bottom-region", // 直播下方鱼吧 ".DfsRankGivingActBanner", // 钻粉横幅 "#js-player-main div[class^='activeContainer']", // 房间信息右侧横幅 ]; const removeList = [ "iframe", "#yuba-bottom-region", // 直播下方鱼吧 "#js-room-activity", // 右侧超级粉丝团悬浮广告 "#player-marvel-controller~div[class^='watermark']", //房间号水印 ".bc-wrapper", // 暴雪战网绑定入口 ".wm-general", // 推广横幅 ".DfsRankGivingActBanner", // 钻粉横幅 "#js-player-main div[class^='activeContainer']", // 房间信息右侧横幅 ]; const whiteList = [ ".InteractItem[dataname='粉丝钓鱼']", ".InteractItem[dataname='互动预言']", ".InteractItem[dataname='互动抽奖']", ".guessDealerPopup > .react-draggable", // 我要预言 ".high_energy_barrage > .react-draggable", // 高能弹幕 ]; const otherCSS = ` /* 互动玩法按钮 */ .InteractItem { margin: 0 !important; padding: 0 !important; } /* 播放器顶部白边 */ #root { margin: 0 !important; } /* 播放器底部白边 */ #js-player-main > div:first-child:before { padding-top: 120px !important; } /* 超级弹幕背景 */ #room-html5-player div[class^='super-text-'] { color: rgb(255, 255, 255) !important; background-image: unset !important; } `; /* let times = 0; // 将拓展子按钮移动直接显示 const timer = setInterval(() => { const toolbar = document.querySelector("#js-toolbar-interact"); const toolbarExpand = document.querySelector(".InteractEntryPanelRecent > .InteractEntryPanelList"); if (times++ > 10) { clearInterval(timer); return; } if (toolbar && toolbarExpand.childElementCount > 20) { clearInterval(timer); //document.querySelectorAll(".ToolbarCardModule.ToollBarCardModule").forEach((node) => node.remove()); toolbar.append(...toolbarExpand.childNodes); } }, 5000); */ // document-start时立即运行 const featureStart = { // 屏蔽各种广告元素 blockAds: () => { // css相同级别规则后覆盖前 document.head.insertAdjacentHTML( "beforeend", `` ); }, // 禁用P2P上传 disableP2P: () => { const P2Plist = ["RTCPeerConnection", "webkitRTCPeerConnection", "mozRTCPeerConnection", "msRTCPeerConnectio"]; unsafeWindow.testHook = {}; P2Plist.forEach((fuc) => { // delete unsafeWindow[fuc]; if (unsafeWindow[fuc]) { unsafeWindow[fuc] = new Proxy(unsafeWindow[fuc], { construct: function (...args) { console.log(`[disableP2P] construct ${fuc}`, ...args); throw new Error(`[disableP2P] construct ${fuc}`); }, apply: function (...args) { console.log(`[disableP2P] apply ${fuc}`, ...args); throw new Error(`[disableP2P] apply ${fuc}`); }, }); } }); }, // 防止页面冻结 documentVisibility: () => { Object.defineProperty(document, "hidden", { value: false, writable: false }); Object.defineProperty(document, "visibilityState", { value: "visible", writable: false }); Object.defineProperty(document, "webkitVisibilityState", { value: "visible", writable: false }); document.dispatchEvent(new Event("visibilitychange")); document.hasFocus = () => true; document.addEventListener("visibilitychange", (e) => e.stopImmediatePropagation(), true); }, }; // document-end时等网页初始化完毕后再执行 const featureEnd = { // 自动最高画质 highestQuality: { selector: "#js-player-controlbar input[readonly][value^='画质'] ~ ul > li:first-child", on: function () { WaitFor.Element(this.selector) .then((node) => { node.click(); }) .catch((err) => { console.error(err); }); }, }, // 自动全屏 *存在浏览器限制 video元素必须触发用户交互事件才能全屏 fullscreen: { selectorVideo: "video", selector: "#js-player-controlbar div[class^='right']", on: function () { WaitFor.Element(this.selectorVideo).then((video) => { video.addEventListener( "play", () => { WaitFor.Element(this.selector) .then((node) => { this.callbacks.online(node); }) .catch((err) => { console.error(err); }); }, { once: true } ); }); }, callbacks: { // 直播中 → 自动全屏 online: function (node) { // 非全屏状态下自动全屏 // fixed→非全屏 small→网页全屏 large→全屏 *部分直播间非全屏为small if (!document.querySelector("#js-player-main").parentElement.className.includes("large")) { const fullscreenButton = node.querySelector(":scope > i:last-child"); fullscreenButton.click(); let flag = false; document.addEventListener( "click", (event) => { if ( !flag && event.target != fullscreenButton && event.target != fullscreenButton.previousElementSibling && navigator.userActivation.hasBeenActive ) { console.log(navigator.userActivation); document.documentElement.requestFullscreen(); } }, { once: true } ); document.addEventListener( "keydown", () => { flag = true; }, { once: true } ); } }, }, }, // +1按钮 & 快速回复 commentEnhancer: { selector: "#comment-dzjy-container", node: void 0, commentNode: void 0, callback: function (comment, reply = false) { const danmu = comment.data.text; const user = comment.data.extraData.sendName; // console.log(danmu, user); if (danmu && (!reply || user)) { document.querySelector("div.ChatSend-txt").innerText = reply ? `@${user}:${danmu}` : danmu; document.querySelector("button.ChatSend-button").click(); } else { throw new Error(`Invaild danmu: ${danmu}; user: ${user}`); } }, render: function () { this.node .querySelector(".btnscontainer-4e2ed0 > .btnscontainer-4e2ed0") .insertAdjacentHTML( "afterbegin", `
+1

|

` ); }, on: function () { WaitFor.Element(this.selector) .then((node) => { this.node = node; const observer = new MutationObserver((mutations) => { if (!mutations?.[0]?.addedNodes?.length) { return; } this.render(); }); const observerChecker = new MutationObserver((mutations) => { if (!mutations?.[0]?.addedNodes?.length) { return; } observerChecker.disconnect(); this.commentNode = document.querySelector("#comment-higher-container"); const btn = node.querySelector(".thirdBtn-06cde5"); // 委托回复/+1右击事件 node.addEventListener("contextmenu", (event) => { event.preventDefault(); // event.stopImmediatePropagation(); event.stopPropagation(); const comment = this.commentNode.querySelector(":scope > div").comment; if (event.target == comment.回复) { this.callback(comment, true); } else if (event.target?.id == "plus1-btn") { this.callback(comment); } }); // 如果不存在原生+1按钮 则模拟一个+1按钮 if (!btn) { // 委托+1点击事件 node.addEventListener("click", (event) => { if (event.target?.id == "plus1-btn") { const comment = this.commentNode.querySelector(":scope > div").comment; this.callback(comment); } }); this.render(); observer.observe(node, { childList: true }); } }); observerChecker.observe(node, { childList: true }); }) .catch((err) => { console.error(err); }); }, }, // 禁止显示新观众 noNewViwer: { selector: "#js-barrage-extendList", on: function () { WaitFor.Element(this.selector) .then((node) => { node.appendChild = () => {}; node.replaceChild = () => {}; node.textContent = ""; }) .catch((err) => { console.error(err); }); }, }, // 移除高耗能元素 removeElements: { on: function () { let times = 0; const timer = setInterval(() => { removeList.forEach((selector) => document.querySelector(selector)?.remove()); if (++times > 5) { clearInterval(timer); } }, 5000); }, }, }; const WaitFor = { // 默认超时 timeout: 15000, // 上下文记录 contexts: new Map(), // 只要命中任意selector 则立即返回结果 _waitEngine: function (selectors, root, timeout) { // 合并选择器 快速命中 const fastMatch = root.querySelector(selectors.join(", ")); if (fastMatch) { return Promise.resolve(fastMatch); } // 初始化上下文 let ctx = this.contexts.get(root); if (!ctx) { ctx = { tasks: new Map(), observer: new MutationObserver((mutations) => this._handleMutations(mutations, root)), }; ctx.observer.observe(root, { childList: true, subtree: true }); this.contexts.set(root, ctx); } return new Promise((resolve, reject) => { const cleanup = () => { selectors.forEach((selector) => { const queue = ctx.tasks.get(selector); if (queue) { const idx = queue.findIndex((w) => w == waiter); if (idx > -1) { queue.splice(idx, 1); } if (queue.length == 0) { ctx.tasks.delete(selector); } } }); if (ctx.tasks.size == 0) { ctx.observer.disconnect(); this.contexts.delete(root); } }; const timerId = setTimeout(() => { cleanup(); reject(new Error(`timeout: [${selectors.join(", ")}]`)); }, timeout); const onSuccess = (node) => { clearTimeout(timerId); cleanup(); resolve(node); }; const waiter = { resolve: onSuccess }; selectors.forEach((selector) => { if (!ctx.tasks.has(selector)) ctx.tasks.set(selector, []); ctx.tasks.get(selector).push(waiter); }); }); }, // Mutation 事件分发器 _handleMutations: function (mutations, root) { const ctx = this.contexts.get(root); if (!ctx || !ctx.tasks.size) { return; } for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!(node instanceof Element)) { continue; } for (const [selector, waiters] of ctx.tasks) { let match = null; try { if (node.matches(selector)) { match = node; } else { match = node.querySelector(selector); } } catch (e) { console.error(`WaitFor selector error: "${selector}"`, e); ctx.tasks.delete(selector); continue; } if (match) { // 浅拷贝,避免resolve修改数组导致遍历问题 for (const { resolve } of [...waiters]) { resolve(match); } } } } } }, Body: function () { return new Promise((resolve) => { if (document.body) { resolve(); } else { document.addEventListener("DOMContentLoaded", () => { resolve(); }); } }); }, /** * 等待单个元素 * @param {string} selector 选择器 * @param {Element} [root=document.body] 根节点 * @param {number} [timeout=this.timeout] timeout 超时时间 * @returns {Promise} */ Element: function (selector, root = document.body, timeout = this.timeout) { return this._waitEngine([selector], root, timeout).catch(() => { throw new Error(`WaitFor.Element timeout (${timeout}ms): ${selector}`); }); }, /** * 等待多个元素 * @param {string[]} selectors 选择器数组 * @param {Element} [root=document.body] 根节点 * @param {number} [timeout=this.timeout] timeout 超时时间 * @param {boolean} race 竞速模式 (true=任意命中即返回,false=全部命中才返回) * @returns {Promise} */ Elements: function (selectors, root = document.body, timeout = this.timeout, race = false) { if (race) { return this._waitEngine(selectors, root, timeout).catch((err) => { throw new Error(`WaitFor.Elements(Race) ${err.message}`); }); } const tasks = selectors.map((selector) => this.Element(selector, root, timeout) .then((node) => ({ status: "fulfilled", selector, node })) .catch((error) => ({ status: "rejected", selector, error })) ); return Promise.all(tasks).then((results) => { const failed = results.filter((r) => r.status == "rejected"); if (failed.length) { const messages = failed .map((f) => { const msg = f.error && f.error.message ? f.error.message : "Unknown error"; return `${f.selector}: ${msg}`; }) .join("\n - "); throw new Error(`WaitFor.Elements(All) timeout (${timeout}ms).\n Failed selectors:\n - ${messages}`); } return results.map((r) => r.node); }); }, }; unsafeWindow.WaitFor = WaitFor; Object.values(featureStart).forEach((feature) => feature()); await WaitFor.Body(); Object.values(featureEnd).forEach((feature) => feature.on());