// ==UserScript== // @name 555电影网广告过滤 // @name:zh-CN 555电影网广告过滤 // @name:en 555 Movie Ad Block // @namespace https://github.com/luestr/userscripts // @version 1.2.0 // @description 过滤 555k06.com 首页与播放页广告,优先拦截请求、其次清理页面,仅适配 WebKit 引擎环境。 // @description:en Block ads on 555k06.com home and play pages with request blocking first and DOM cleanup as fallback. WebKit only. // @author 可莉 // @homepageURL https://t.me/ibilibili // @updateURL https://raw.githubusercontent.com/luestr/userscripts/main/adblock/555Movie_adblock.user.js // @downloadURL https://raw.githubusercontent.com/luestr/userscripts/main/adblock/555Movie_adblock.user.js // @match https://555k06.com/* // @match https://www.555k06.com/* // @run-at document-start // @grant none // ==/UserScript== (() => { 'use strict'; if (window.__ad555k06) return; window.__ad555k06 = true; const DEBUG = false; const log = (...args) => { if (DEBUG) console.debug('[555-adblock]', ...args); }; /* ------------------------------------------------------------------ * * 一、广告判定(请求拦截为第一优先级) * ------------------------------------------------------------------ */ // 博彩/推广落地域名:图标墙、弹窗、悬浮按钮、播放页横幅都指向这些站点。 const AD_HOST_RE = /(?:^|\.)(?:32841243\.com|10387723\.com|52629942\.com|bihe299\.com|24240575\.com|98516601\.com|91001719\.com|55kp2\.com|55kp30\.com|55dy23\.com|55dy32\.com|55dy75\.com|55dy1\.top)$/i; // 播放页「分享好友观看」指向的微信推广短链域名。 const SHARE_HOST_RE = /(?:^|\.)clb\.kr-tencentclb\.net$/i; // 统计与埋点:Google tag 与站点自建 DF Analytics。 const TRACK_HOST_RE = /(?:^|\.)(?:googletagmanager\.com|google-analytics\.com|ddjzis\.cn)$/i; // 纯广告素材 CDN(该域只承载广告图标)。 const AD_IMAGE_HOST_RE = /(?:^|\.)xstore\.qihu\.com$/i; // 站点资源 CDN:同时承载主题 CSS/JS 与广告素材,只能按「图片路径」精确拦截。 const CDN_HOST = 'eliteres.fanruan.com'; const CDN_AD_IMAGE_RE = /^\/office\/\d{4}\/\d{2}\/\d{2}\/[^/?#]+\.(?:gif|png|jpe?g|webp|svg)$/i; // 命中任一片段才进入字符串清洗,避免全局 innerHTML 开销与误伤。 const SUSPECT_RE = /eliteres|\.xstore\.qihu|32841243|10387723|52629942|bihe299|24240575|98516601|91001719|55kp2|55kp30|55dy23|55dy32|55dy75|small-icon/i; function toURL(raw) { if (raw === null || raw === undefined) return null; try { return new URL(String(raw), location.href); } catch (e) { return null; } } /** URL 是否属于需要拦截的广告/追踪请求。 */ function isBlockedUrl(raw) { const url = toURL(raw); if (!url) return false; if (url.protocol !== 'http:' && url.protocol !== 'https:') return false; const host = url.hostname; if (AD_HOST_RE.test(host) || TRACK_HOST_RE.test(host) || AD_IMAGE_HOST_RE.test(host) || SHARE_HOST_RE.test(host)) return true; if (host === CDN_HOST && CDN_AD_IMAGE_RE.test(url.pathname)) return true; return false; } /** 文本(如 style="background:url(...)")中是否含需拦截的广告资源。 */ function textHasBlockedUrl(text) { if (typeof text !== 'string' || !SUSPECT_RE.test(text)) return false; const matches = text.match(/https?:\/\/[^\s"'()]+/gi); if (!matches) return false; return matches.some(isBlockedUrl); } /* ------------------------------------------------------------------ * * 二、请求拦截:fetch / XHR / sendBeacon / 资源属性 * ------------------------------------------------------------------ */ // 2.1 fetch:返回空 200,避免调用方因网络错误中断。 const nativeFetch = window.fetch; if (typeof nativeFetch === 'function') { window.fetch = function (input, init) { try { const raw = typeof input === 'string' ? input : (input && typeof input.url === 'string' ? input.url : ''); if (raw && isBlockedUrl(raw)) { log('block fetch', raw); return Promise.resolve(new Response('', { status: 200, statusText: 'OK' })); } } catch (e) { /* 放行 */ } return nativeFetch.apply(this, arguments); }; } // 2.2 XMLHttpRequest:合成一个成功但为空的响应。 const XHR = window.XMLHttpRequest; if (XHR && XHR.prototype) { const nativeOpen = XHR.prototype.open; const nativeSend = XHR.prototype.send; XHR.prototype.open = function (method, url) { try { this.__ad555Blocked = isBlockedUrl(url); } catch (e) { /* ignore */ } return nativeOpen.apply(this, arguments); }; XHR.prototype.send = function () { if (!this.__ad555Blocked) return nativeSend.apply(this, arguments); const self = this; const type = self.responseType; const isText = !type || type === 'text'; window.setTimeout(() => { try { Object.defineProperty(self, 'readyState', { value: 4, configurable: true }); Object.defineProperty(self, 'status', { value: 200, configurable: true }); Object.defineProperty(self, 'statusText', { value: 'OK', configurable: true }); // 与原生空响应保持一致:文本类型给空串,其余类型给 null, // 避免调用方把空串当作 JSON 解析而抛错。 Object.defineProperty(self, 'responseText', { value: '', configurable: true }); Object.defineProperty(self, 'response', { value: isText ? '' : null, configurable: true }); } catch (e) { /* ignore */ } for (const handler of ['onreadystatechange', 'onload', 'onloadend']) { if (typeof self[handler] === 'function') { try { self[handler](); } catch (e) { /* ignore */ } } } }, 0); }; } // 2.3 sendBeacon:埋点直接吞掉。 if (navigator && typeof navigator.sendBeacon === 'function') { const nativeBeacon = navigator.sendBeacon.bind(navigator); navigator.sendBeacon = function (url, data) { if (isBlockedUrl(url)) { log('block beacon', url); return true; } return nativeBeacon(url, data); }; } // 2.4 元素属性:脚本/图片/iframe/link 在赋值阶段丢弃广告 URL。 const nativeSetAttribute = Element.prototype.setAttribute; const GUARDED_ATTRS = new Set(['src', 'href', 'srcset', 'poster', 'data-src', 'data-original', 'data-bg', 'data-lazy-src']); Element.prototype.setAttribute = function (name, value) { try { const attr = String(name).toLowerCase(); if (attr === 'style') { if (textHasBlockedUrl(String(value))) { log('block style attr', value); return; } } else if (GUARDED_ATTRS.has(attr) && isBlockedUrl(value)) { log('block setAttribute', attr, value); return; } } catch (e) { /* ignore */ } return nativeSetAttribute.apply(this, arguments); }; // 2.5 属性 setter:覆盖页面直接用 el.src / el.href 赋值的情况。 function guardProperty(proto, prop) { if (!proto) return; const desc = Object.getOwnPropertyDescriptor(proto, prop); if (!desc || typeof desc.set !== 'function') return; const nativeSet = desc.set; Object.defineProperty(proto, prop, { configurable: true, enumerable: desc.enumerable, get: desc.get, set(value) { if (isBlockedUrl(value)) { log('block prop', prop, value); return; } return nativeSet.call(this, value); }, }); } guardProperty(window.HTMLImageElement && window.HTMLImageElement.prototype, 'src'); guardProperty(window.HTMLImageElement && window.HTMLImageElement.prototype, 'srcset'); guardProperty(window.HTMLScriptElement && window.HTMLScriptElement.prototype, 'src'); guardProperty(window.HTMLIFrameElement && window.HTMLIFrameElement.prototype, 'src'); guardProperty(window.HTMLLinkElement && window.HTMLLinkElement.prototype, 'href'); guardProperty(window.HTMLSourceElement && window.HTMLSourceElement.prototype, 'src'); guardProperty(window.HTMLSourceElement && window.HTMLSourceElement.prototype, 'srcset'); // 2.6 innerHTML / insertAdjacentHTML:广告数据是 base64 JSON 拼成 HTML 后注入的, // 在插入前剔除广告卡片,等价于「不发起请求」。 function hasAdHref(tagHtml) { const m = tagHtml.match(/\bhref\s*=\s*["']([^"']+)["']/i); return !!m && isBlockedUrl(m[1]); } function hasAdImgSrc(tagHtml) { const m = tagHtml.match(/\b(?:src|data-src|data-original|data-bg|data-lazy-src)\s*=\s*["']([^"']+)["']/i); return !!m && isBlockedUrl(m[1]); } function scrubHtml(html) { if (typeof html !== 'string' || html.length > 300000) return html; if (!SUSPECT_RE.test(html)) return html; let out = html.replace(/]*>[\s\S]*?<\/a>/gi, (tag) => { // 图标墙:… if (/class\s*=\s*["'][^"']*small-icon-div/i.test(tag)) return ''; // 带广告图片的推广卡片 if (/]*>/gi, (tag) => (hasAdImgSrc(tag) ? '' : tag)); if (out !== html) log('scrubbed injected html'); return out; } const innerHTMLDesc = Object.getOwnPropertyDescriptor(Element.prototype, 'innerHTML'); if (innerHTMLDesc && typeof innerHTMLDesc.set === 'function') { Object.defineProperty(Element.prototype, 'innerHTML', { configurable: true, enumerable: innerHTMLDesc.enumerable, get: innerHTMLDesc.get, set(value) { // 与原生一致:null 等价于空串,其余按字符串清洗后写入。 const html = value === null ? '' : scrubHtml(String(value)); return innerHTMLDesc.set.call(this, html); }, }); } if (typeof Element.prototype.insertAdjacentHTML === 'function') { const nativeInsertAdjacentHTML = Element.prototype.insertAdjacentHTML; Element.prototype.insertAdjacentHTML = function (position, html) { const value = html === null ? '' : scrubHtml(String(html)); return nativeInsertAdjacentHTML.call(this, position, value); }; } /* ------------------------------------------------------------------ * * 三、页面清理(兜底:处理解析器已插入或脚本已渲染的广告节点) * ------------------------------------------------------------------ */ // 明确由广告脚本填充的容器与弹窗。 const REMOVE_SELECTORS = [ '#popup.popup', // 首页 APP 下载弹窗 '.popup-tips', // 弹窗通用容器 '.small-icon-container', // 博彩图标墙(首页 / 播放页 / 侧栏) 'a.fixedGroup-item[href*="55kp2.com"]', // 右下角悬浮下载按钮 'a.fixedGroup-item[href*="55dy23.com"]', 'a.share-btn', // 播放页「分享好友观看」按钮本体 ]; // 站点内联脚本会按 id 回写这两个横幅容器(2s 后再次 innerHTML), // 直接删除会让页面脚本取到 null 而抛错;因此只清空 + 隐藏,保留节点。 const NEUTRALIZE_SELECTORS = [ '#c2f670cd2', // 播放器上方横幅广告 '#c2f670cd', // 播放器下方横幅广告 ]; // 文章列表 / 模块内的广告卡片只在这些范围内按链接域名识别。 const LIST_SCOPE = '.module-items, .module-main, .module-player, .module-player-side, .content > .module, .small-icon-container, .fixedGroup'; const HIDE_CSS = ` #popup.popup, .popup-tips, .small-icon-container, #c2f670cd, #c2f670cd2, a.fixedGroup-item[href*="55kp2.com"], a.fixedGroup-item[href*="55dy23.com"] { display: none !important; } /* 单独成条:不支持 :has() 的引擎只会丢弃这一条,不影响上面的隐藏规则。 */ footer.player-footer > p:has(#player-release) { display: none !important; } `; function injectStyle() { if (document.getElementById('ad555-style') || !document.documentElement) return; const style = document.createElement('style'); style.id = 'ad555-style'; style.textContent = HIDE_CSS; document.documentElement.appendChild(style); } function removeElement(el) { if (el && el.parentNode) el.remove(); } /** * 摘除节点前先切断它的资源请求:对仍在加载的图片 / iframe 移除地址属性, * 可让浏览器放弃未完成的下载(仅删除节点在部分引擎上不会取消请求)。 */ function removeResourceNode(el) { if (!el) return; const tag = el.tagName; try { if (tag === 'IMG' || tag === 'IFRAME' || tag === 'SOURCE' || tag === 'SCRIPT') { el.removeAttribute('src'); el.removeAttribute('srcset'); } else if (tag === 'LINK') { el.removeAttribute('href'); } } catch (e) { /* ignore */ } removeElement(el); } /** 播放器 iframe 页脚的「最新发布页」推广链。 */ function removePlayerRelease(root) { const link = root.querySelector('#player-release'); if (!link) return; const para = link.closest('footer.player-footer > p') || link.parentElement; if (para && /最新发布页/.test(para.textContent || '')) removeElement(para); else removeElement(link); } /** 清空并隐藏,但保留节点本身,供页面脚本继续安全引用。 */ function neutralize(el) { if (!el) return; try { el.textContent = ''; } catch (e) { /* ignore */ } el.style.setProperty('display', 'none', 'important'); } function sweep(root) { for (const selector of REMOVE_SELECTORS) { try { root.querySelectorAll(selector).forEach(removeElement); } catch (e) { log('selector failed', selector, e); } } for (const selector of NEUTRALIZE_SELECTORS) { try { root.querySelectorAll(selector).forEach(neutralize); } catch (e) { log('selector failed', selector, e); } } if (root.querySelector) removePlayerRelease(root); // 已经插入文档的广告资源节点(解析期脚本 / 图片 / iframe / 样式): // 先切断地址属性再摘除,尽快中止未完成的下载。 try { for (const node of document.querySelectorAll('script[src], img[src], iframe[src], link[href]')) { const url = node.getAttribute('src') || node.getAttribute('href'); if (!isBlockedUrl(url)) continue; removeResourceNode(node); } } catch (e) { log('resource sweep failed', e); } const scopes = []; if (root.nodeType === 1 && root.matches(LIST_SCOPE)) scopes.push(root); if (root.querySelectorAll) scopes.push(...root.querySelectorAll(LIST_SCOPE)); for (const scope of scopes) { // 指向广告域名的列表卡片(文章列表 / 推荐列表)。 for (const a of scope.querySelectorAll('a[href]')) { if (a.closest('.navbar')) continue; if (!isBlockedUrl(a.getAttribute('href'))) continue; const card = a.closest('.module-poster-item, .module-topic-item, .module-item, li, .swiper-slide') || a; removeElement(card); } // 图片本身命中广告素材的卡片(放宽到任意容器)。 for (const img of scope.querySelectorAll('img[src], img[data-original]')) { const src = img.getAttribute('src') || img.getAttribute('data-original'); if (!isBlockedUrl(src)) continue; removeElement(img.closest('.module-poster-item, .module-topic-item, .module-item, li, .swiper-slide') || img); } } // 悬浮组:广告按钮移除后若已无内容,整组一起移除。 for (const group of document.querySelectorAll('.fixedGroup')) { if (!group.querySelector('.fixedGroup-item') && !group.querySelector('a, div')) removeElement(group); } // 分享按钮被移除后,清掉留白的空容器。 for (const bar of document.querySelectorAll('.module-player-handle-items')) { if (!bar.textContent.trim() && !bar.querySelector('a, button, div')) removeElement(bar); } } /* ------------------------------------------------------------------ * * 四、生命周期:尽早清理 + 观察动态注入 * ------------------------------------------------------------------ */ let scheduled = false; let observer = null; let extraTimers = []; function scheduleSweep() { if (scheduled) return; scheduled = true; window.setTimeout(() => { scheduled = false; try { sweep(document); } catch (e) { log('sweep failed', e); } }, 60); } function start() { injectStyle(); try { sweep(document); } catch (e) { log('initial sweep failed', e); } try { observer = new MutationObserver((mutations) => { for (const mutation of mutations) { // 解析期插入的广告脚本 / 图片 / iframe:命中即刻摘除, // 尽量在解析线程继续之前切断后续加载(@webRequest 不可用时的兜底)。 // 只处理资源类元素本身,绝不因「包含广告容器」而删除整棵子树。 if (mutation.type === 'childList') { for (const node of mutation.addedNodes) { if (!node || node.nodeType !== 1) continue; const tag = node.tagName; if (tag === 'SCRIPT' || tag === 'IMG' || tag === 'IFRAME' || tag === 'LINK') { if (isBlockedUrl(node.getAttribute('src') || node.getAttribute('href'))) { log('block parsed node', tag, node.getAttribute('src') || node.getAttribute('href')); removeElement(node); } } } } if (mutation.type === 'attributes') { const el = mutation.target; if (!el || el.nodeType !== 1) continue; const attr = mutation.attributeName; const value = el.getAttribute(attr); if (attr === 'href' && isBlockedUrl(value)) { const card = el.closest('.module-poster-item, .module-topic-item, .module-item, li, .swiper-slide'); if (card && !card.closest('.navbar')) { removeElement(card); continue; } } if ((attr === 'src' || attr === 'data-original' || attr === 'style') && value && ( isBlockedUrl(value) || (attr === 'style' && textHasBlockedUrl(value)) )) { removeElement(el); continue; } } } scheduleSweep(); }); observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ['href', 'src', 'srcset', 'data-bg', 'data-original', 'style'], }); } catch (e) { log('observer failed', e); } // 广告在 2s 后才由内联脚本渲染,补几次兜底清理。 for (const delay of [800, 2000, 3500]) { extraTimers.push(window.setTimeout(scheduleSweep, delay)); } } function stop() { if (observer) { observer.disconnect(); observer = null; } for (const timer of extraTimers) window.clearTimeout(timer); extraTimers = []; } if (document.documentElement) start(); else document.addEventListener('readystatechange', start, { once: true }); document.addEventListener('DOMContentLoaded', scheduleSweep, { once: true }); window.addEventListener('load', scheduleSweep, { once: true }); window.addEventListener('pagehide', stop, { once: true }); })();