// ==UserScript== // @name Pure Douyu 斗鱼纯净版 // @namespace PureDouyu // @version 0.1.1 // @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^='wonderful']", // 播放器录像按钮 "#js-player-controlbar div[class^='wonderful']+i", // 播放器问题反馈按钮 ".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", // 推广横幅 ".super-user-icon-574f31", // 超级弹幕头像 ".super-noble-icon-9aacaf ", // 超级弹幕头像框 ".super-tail-bffa58", // 超级弹幕尾巴 ".super-text-0281ca img", // 超级弹幕图标 ".headpic-dda332", // 普通弹幕图标 ]; 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; } /* 超级弹幕背景 */ .super-text-0281ca { 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"]; P2Plist.forEach((fuc) => { delete unsafeWindow[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: { selector: "#js-player-controlbar div[class^='right']", on: function () { WaitFor.Element(this.selector) .then((node) => { // 非全屏状态下自动全屏 // fixed→非全屏 small→网页全屏 large→全屏 *部分直播间非全屏为small if (!document.querySelector("#js-player-main").parentElement.className.includes("large")) { node.querySelector("i:last-child").click(); } }) .catch((err) => { console.error(err); }); }, }, // +1按钮 plus1: { selector: "#comment-dzjy-container", callback: function () { const danmu = ( document.querySelector("#comment-higher-container .text-879f3e") || document.querySelector("#comment-higher-container .super-text-0281ca") ).innerText; // console.log(danmu); if (danmu) { document.querySelector("div.ChatSend-txt").innerText = danmu; document.querySelector("button.ChatSend-button").click(); } }, render: function () { document .querySelector(".btnscontainer-4e2ed0 > .btnscontainer-4e2ed0") .insertAdjacentHTML( "afterbegin", `
+1

|

` ); }, on: function () { WaitFor.Element(this.selector) .then((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; } const btn = document.querySelector("#comment-dzjy-container .thirdBtn-06cde5"); observerChecker.disconnect(); // 如果不存在原生+1按钮 则模拟一个+1按钮 if (!btn) { // 委托点击事件 node.addEventListener("click", (event) => { if (event.target?.id == "plus1-btn") { this.callback(); } }); 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); }); }, }, }; const WaitFor = { tasks: new Map(), // 超时自动reject timeout: 15000, observer: null, initObserver: function (root = document.body) { if (this.observer) { return; } this.observer = new MutationObserver((mutations) => { if (!this.tasks.size) { return; } for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (!(node instanceof Element)) { continue; } this.checkNode(node); } } }); this.observer.observe(root, { childList: true, subtree: true, }); }, checkNode: function (node) { for (const [selector, waiters] of this.tasks) { let match; if (node.matches(selector)) { match = node; } else { match = node.querySelector(selector); } if (match) { // 命中后通知所有订阅者 waiters.forEach(({ resolve, timerId }) => { clearTimeout(timerId); resolve(match); }); this.tasks.delete(selector); } } if (!this.tasks.size) { this.stopObserver(); } }, stopObserver: function () { if (this.observer) { this.observer.disconnect(); this.observer = null; } else { throw new Error("Invaild stopObserver caller"); } }, Body: function () { return new Promise((resolve) => { if (document.body) { resolve(); } else { document.addEventListener("DOMContentLoaded", () => { resolve(); }); } }); }, /** * 等待单个元素出现 * @param {string} selector * @param {Element|Document} [root=document.body] * @param {number} [timeout=this.timeout] */ Element: function (selector, root = document.body, timeout = this.timeout) { // 存在则直接返回 const target = root.querySelector(selector); if (target) { return Promise.resolve(target); } this.initObserver(root); return new Promise((resolve, reject) => { // 超时定时器 const timerId = setTimeout(() => { // 超时后从 tasks 中移除当前 waiter const waiters = this.tasks.get(selector); if (waiters) { const index = waiters.findIndex((waiter) => waiter.resolve === resolve); if (index !== -1) { waiters.splice(index, 1); } if (!waiters.length) { this.tasks.delete(selector); } } if (!this.tasks.size) { this.stopObserver(); } reject(new Error(`WaitFor.Element timeout (${timeout}ms): ${selector}`)); }, timeout); const waiter = { resolve, reject, timerId }; if (this.tasks.has(selector)) { this.tasks.get(selector).push(waiter); } else { this.tasks.set(selector, [waiter]); } }); }, /** * 等待多个元素出现 * @param {string[]} selectors * @param {Element|Document} [root=document.body] * @param {number} [timeout=this.timeout] 单个 selector 的超时时间 */ Elements: function (selectors, root = document.body, timeout = this.timeout) { const results = selectors.map((selector) => root.querySelector(selector)); // 如果全部存在,直接 resolve if (results.every((r) => r)) { return Promise.resolve(results); } return Promise.all( selectors.map((selector, index) => { if (results[index]) { return Promise.resolve(results[index]); } else { return this.Element(selector, root, timeout); } }) ); }, }; Object.values(featureStart).forEach((feature) => feature()); await WaitFor.Body(); Object.values(featureEnd).forEach((feature) => feature.on());