您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
v27.4 "Aeterna-Final-Fix": 究極修正!重寫核心解析器,徹底解決因全形冒號(:)等符號導致的觀看數解析失敗問題。
// ==UserScript== // @name YouTube 淨化大師 (Pantheon) // @namespace http://tampermonkey.net/ // @version 27.4.0 // @description v27.4 "Aeterna-Final-Fix": 究極修正!重寫核心解析器,徹底解決因全形冒號(:)等符號導致的觀看數解析失敗問題。 // @author Benny, AI Collaborators & The Final Optimizer // @match https://www.youtube.com/* // @grant GM_info // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @run-at document-start // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // ==/UserScript== (function () { 'use strict'; // --- 設定與常數 --- const SCRIPT_INFO = GM_info?.script || { name: 'YouTube Purifier Pantheon', version: '27.4.0' }; const ATTRS = { PROCESSED: 'data-yt-pantheon-processed', HIDDEN_REASON: 'data-yt-pantheon-hidden-reason', WAIT_COUNT: 'data-yt-pantheon-wait-count', }; const State = { HIDE: 'HIDE', KEEP: 'KEEP', WAIT: 'WAIT' }; const DEFAULT_RULE_ENABLES = { ad_sponsor: true, members_only: true, shorts_item: true, mix_only: true, premium_banner: true, news_block: true, shorts_block: true, posts_block: true, shorts_grid_shelf: true, movies_shelf: true, }; const DEFAULT_LOW_VIEW_THRESHOLD = 1000; const CONFIG = { ENABLE_LOW_VIEW_FILTER: GM_getValue('enableLowViewFilter', true), LOW_VIEW_THRESHOLD: GM_getValue('lowViewThreshold', DEFAULT_LOW_VIEW_THRESHOLD), DEBUG_MODE: GM_getValue('debugMode', false), RULE_ENABLES: GM_getValue('ruleEnables', { ...DEFAULT_RULE_ENABLES }), DEBOUNCE_DELAY: 50, PERIODIC_INTERVAL: 350, WAIT_MAX_RETRY: 5, }; // 主要選擇器 const SELECTORS = { TOP_LEVEL_FILTERS: [ 'ytd-rich-item-renderer', 'ytd-rich-section-renderer', 'ytd-rich-shelf-renderer', 'ytd-video-renderer', 'ytd-compact-video-renderer', 'ytd-reel-shelf-renderer', 'ytd-ad-slot-renderer', 'yt-lockup-view-model', 'ytd-statement-banner-renderer', 'grid-shelf-view-model', 'ytd-playlist-renderer', 'ytd-compact-playlist-renderer' ], CLICKABLE_CONTAINERS: [ 'ytd-rich-item-renderer', 'ytd-video-renderer', 'ytd-compact-video-renderer', 'yt-lockup-view-model', 'ytd-playlist-renderer', 'ytd-compact-playlist-renderer' ], INLINE_PREVIEW_PLAYER: 'ytd-video-preview', init() { this.UNPROCESSED = this.TOP_LEVEL_FILTERS.map(s => `${s}:not([${ATTRS.PROCESSED}])`).join(', '); return this; } }.init(); // --- 工具函數 --- const utils = { debounce: (func, delay) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => func(...a), delay); }; }, injectCSS: () => GM_addStyle('ytd-ad-slot-renderer, ytd-promoted-sparkles-text-search-renderer { display: none !important; }'), unitMultiplier: (u) => { if (!u) return 1; const m = { 'k': 1e3, 'm': 1e6, 'b': 1e9, '千': 1e3, '萬': 1e4, '万': 1e4, '億': 1e8, '亿': 1e8 }; return m[u.toLowerCase()] || 1; }, // [v27.4] 究極強化版解析器 parseNumeric: (text, type) => { if (!text) return null; const keywords = { live: /(正在觀看|觀眾|watching|viewers)/i, view: /(view|觀看|次)/i, }; const antiKeywords = /(分鐘|小時|天|週|月|年|ago|minute|hour|day|week|month|year)/i; const raw = text.replace(/,/g, '').toLowerCase().trim(); // 1. 檢查是否包含必要的關鍵字 if (!keywords[type].test(raw)) return null; // 2. 如果是計數類型,確保它不是純粹的時間描述 if (type === 'view' && antiKeywords.test(raw) && !keywords.view.test(raw)) return null; // 3. 使用更強健的Regex從字串中任何位置提取數字 const m = raw.match(/([\d.]+)\s*([kmb千萬万億亿])?/i); if (!m) return null; const num = parseFloat(m[1]); if (isNaN(num)) return null; return Math.floor(num * utils.unitMultiplier(m[2])); }, parseLiveViewers: (text) => utils.parseNumeric(text, 'live'), parseViewCount: (text) => utils.parseNumeric(text, 'view'), extractAriaTextForCounts(container) { const a1 = container.querySelector(':scope a#video-title-link[aria-label]'); if (a1?.ariaLabel) return a1.ariaLabel; const a2 = container.querySelector(':scope a#thumbnail[aria-label]'); if (a2?.ariaLabel) return a2.ariaLabel; return ''; }, findPrimaryLink(container) { if (!container) return null; const candidates = [ 'a#thumbnail[href*="/watch?"]', 'a#thumbnail[href*="/shorts/"]', 'a#thumbnail[href*="/playlist?"]', 'a#video-title-link', 'a.yt-simple-endpoint#video-title', 'a.yt-lockup-view-model-wiz__title' ]; for (const sel of candidates) { const a = container.querySelector(sel); if (a?.href) return a; } return container.querySelector('a[href*="/watch?"], a[href*="/shorts/"], a[href*="/playlist?"]'); } }; // --- 日誌記錄器 --- const logger = { _batch: [], prefix: `[${SCRIPT_INFO.name}]`, style: (color) => `color:${color}; font-weight:bold;`, info: (msg, color = '#3498db') => CONFIG.DEBUG_MODE && console.log(`%c${logger.prefix} [INFO] ${msg}`, logger.style(color)), startBatch() { this._batch = []; }, hide(source, ruleName, reason, element) { if (!CONFIG.DEBUG_MODE) return; this._batch.push({ ruleName, reason, element, source }); }, flushBatch() { if (!CONFIG.DEBUG_MODE || this._batch.length === 0) return; const summary = this._batch.reduce((acc, item) => { acc[item.ruleName] = (acc[item.ruleName] || 0) + 1; return acc; }, {}); const summaryString = Object.entries(summary).map(([name, count]) => `${name}: ${count}`).join(', '); console.groupCollapsed(`%c${this.prefix} [HIDE BATCH] Hiding ${this._batch.length} items from ${this._batch[0].source} | ${summaryString}`, this.style('#e74c3c')); this._batch.forEach(item => console.log(`Rule:"${item.ruleName}" | Reason:${item.reason}`, item.element)); console.groupEnd(); }, logStart: () => console.log(`%c🏛️ ${SCRIPT_INFO.name} v${SCRIPT_INFO.version} "Aeterna" 啟動. (Debug: ${CONFIG.DEBUG_MODE})`, 'color:#8e44ad; font-weight:bold; font-size: 1.2em;'), }; // --- 功能增強模組 --- const Enhancer = { initGlobalClickListener() { document.addEventListener('pointerdown', (e) => { if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return; const exclusions = 'button, yt-icon-button, #menu, ytd-menu-renderer, ytd-toggle-button-renderer, yt-chip-cloud-chip-renderer, .yt-spec-button-shape-next'; if (e.target.closest(exclusions)) return; let targetLink = null; const previewPlayer = e.target.closest(SELECTORS.INLINE_PREVIEW_PLAYER); if (previewPlayer) { targetLink = utils.findPrimaryLink(previewPlayer) || utils.findPrimaryLink(previewPlayer.closest(SELECTORS.CLICKABLE_CONTAINERS.join(','))); } else { const container = e.target.closest(SELECTORS.CLICKABLE_CONTAINERS.join(', ')); if (!container) return; const channelLink = e.target.closest('a#avatar-link, .ytd-channel-name a, a[href^="/@"], a[href^="/channel/"]'); targetLink = channelLink?.href ? channelLink : utils.findPrimaryLink(container); } try { const isValidTarget = targetLink?.href && (new URL(targetLink.href, location.origin)).hostname.includes('youtube.com'); if (isValidTarget) { e.preventDefault(); e.stopImmediatePropagation(); const clickBlocker = (eClick) => { eClick.preventDefault(); eClick.stopImmediatePropagation(); }; document.addEventListener('click', clickBlocker, { capture: true, once: true }); window.open(targetLink.href, '_blank'); } } catch (err) {} }, { capture: true }); } }; // --- 統一規則引擎 --- const RuleEngine = { ruleCache: new Map(), globalRules: [], rawRuleDefinitions: [], init() { this.ruleCache.clear(); this.globalRules = []; this.rawRuleDefinitions = [ { id: 'ad_sponsor', name: '廣告/促銷', conditions: { any: [{ type: 'selector', value: '[aria-label*="廣告"], [aria-label*="Sponsor"], [aria-label="贊助商廣告"], ytd-ad-slot-renderer' }] } }, { id: 'members_only', name: '會員專屬', conditions: { any: [ { type: 'selector', value: '[aria-label*="會員專屬"]' }, { type: 'text', selector: '.badge-shape-wiz__text', keyword: /頻道會員專屬|Members only/i } ] } }, { id: 'shorts_item', name: 'Shorts (單個)', conditions: { any: [{ type: 'selector', value: 'a[href*="/shorts/"]' }] } }, { id: 'mix_only', name: '合輯 (Mix)', conditions: { any: [{ type: 'text', selector: '.badge-shape-wiz__text, ytd-thumbnail-overlay-side-panel-renderer', keyword: /(^|\s)(合輯|Mix)(\s|$)/i }] } }, { id: 'premium_banner', name: 'Premium 推廣', scope: 'ytd-statement-banner-renderer', conditions: { any: [{ type: 'selector', value: 'ytd-button-renderer' }] } }, { id: 'news_block', name: '新聞區塊', scope: 'ytd-rich-shelf-renderer, ytd-rich-section-renderer', conditions: { any: [{ type: 'text', selector: 'h2 #title', keyword: /新聞快報|Breaking News|ニュース/i }] } }, { id: 'shorts_block', name: 'Shorts 區塊', scope: 'ytd-rich-shelf-renderer, ytd-reel-shelf-renderer, ytd-rich-section-renderer', conditions: { any: [{ type: 'text', selector: '#title, h2 #title', keyword: /^Shorts$/i }] } }, { id: 'posts_block', name: '貼文區塊', scope: 'ytd-rich-shelf-renderer, ytd-rich-section-renderer', conditions: { any: [{ type: 'text', selector: 'h2 #title', keyword: /貼文|Posts|投稿|Publicaciones/i }] } }, { id: 'shorts_grid_shelf', name: 'Shorts 區塊 (Grid)', scope: 'grid-shelf-view-model', conditions: { any: [{ type: 'text', selector: 'h2.shelf-header-layout-wiz__title', keyword: /^Shorts$/i }] } }, { id: 'movies_shelf', name: '電影推薦區塊', scope: 'ytd-rich-shelf-renderer, ytd-rich-section-renderer', conditions: { any: [ { type: 'text', selector: 'h2 #title', keyword: /為你推薦的特選電影|featured movies/i }, { type: 'text', selector: 'p.ytd-badge-supported-renderer', keyword: /YouTube 精選/i } ] } }, ]; const activeRules = this.rawRuleDefinitions.filter(rule => CONFIG.RULE_ENABLES[rule.id] !== false); if (CONFIG.ENABLE_LOW_VIEW_FILTER) { const lowViewScope = 'ytd-rich-item-renderer, ytd-video-renderer, ytd-compact-video-renderer, yt-lockup-view-model'; activeRules.push( { id: 'low_viewer_live', name: '低觀眾直播', scope: lowViewScope, isConditional: true, conditions: { any: [{ type: 'liveViewers', threshold: CONFIG.LOW_VIEW_THRESHOLD }] } }, { id: 'low_view_video', name: '低觀看影片', scope: lowViewScope, isConditional: true, conditions: { any: [{ type: 'viewCount', threshold: CONFIG.LOW_VIEW_THRESHOLD }] } } ); } activeRules.forEach(rule => { const scopes = rule.scope ? rule.scope.split(',') : [null]; scopes.forEach(scope => { const target = scope ? scope.trim().toUpperCase() : 'GLOBAL'; if (target === 'GLOBAL') { this.globalRules.push(rule); } else { if (!this.ruleCache.has(target)) this.ruleCache.set(target, []); this.ruleCache.get(target).push(rule); } }); }); }, checkCondition(container, condition) { try { switch (condition.type) { case 'selector': return container.querySelector(`:scope ${condition.value}`) ? { state: State.HIDE, reason: `Selector: ${condition.value}` } : { state: State.KEEP }; case 'text': { const elements = container.querySelectorAll(`:scope ${condition.selector}`); for (const el of elements) { if (condition.keyword.test(el.textContent)) { return { state: State.HIDE, reason: `Text: "${el.textContent.trim()}"` }; } } return { state: State.KEEP }; } case 'liveViewers': case 'viewCount': return this.checkNumericMetadata(container, condition); default: return { state: State.KEEP }; } } catch (e) { return { state: State.KEEP }; } }, checkNumericMetadata(container, condition) { const parser = condition.type === 'liveViewers' ? utils.parseLiveViewers : utils.parseViewCount; const textSources = [ ...Array.from(container.querySelectorAll('#metadata-line .inline-metadata-item, .yt-content-metadata-view-model-wiz__metadata-text'), el => el.textContent), utils.extractAriaTextForCounts(container) ]; for (const text of textSources) { const count = parser(text); if (count !== null) return count < condition.threshold ? { state: State.HIDE, reason: `${condition.type}: ${count} < ${condition.threshold}` } : { state: State.KEEP }; } return container.tagName.includes('PLAYLIST') ? { state: State.KEEP } : { state: State.WAIT }; }, checkRule(container, rule) { if (rule.scope && !container.matches(rule.scope)) return { state: State.KEEP }; let requiresWait = false; for (const condition of rule.conditions.any) { const result = this.checkCondition(container, condition); if (result.state === State.HIDE) return { ...result, ruleId: rule.id }; if (result.state === State.WAIT) requiresWait = true; } return requiresWait ? { state: State.WAIT } : { state: State.KEEP }; }, processContainer(container, source) { if (container.hasAttribute(ATTRS.PROCESSED)) return; const relevantRules = (this.ruleCache.get(container.tagName) || []).concat(this.globalRules); let finalState = State.KEEP; for (const rule of relevantRules) { const result = this.checkRule(container, rule); if (result.state === State.HIDE) { container.style.setProperty('display', 'none', 'important'); container.setAttribute(ATTRS.PROCESSED, 'hidden'); container.setAttribute(ATTRS.HIDDEN_REASON, result.ruleId); logger.hide(source, rule.name, result.reason, container); return; } if (result.state === State.WAIT) finalState = State.WAIT; } if (finalState === State.WAIT) { const count = +(container.getAttribute(ATTRS.WAIT_COUNT) || 0) + 1; const maxRetries = container.tagName === 'YT-LOCKUP-VIEW-MODEL' ? 2 : CONFIG.WAIT_MAX_RETRY; if (count >= maxRetries) container.setAttribute(ATTRS.PROCESSED, 'checked-wait-expired'); else container.setAttribute(ATTRS.WAIT_COUNT, String(count)); } else { container.setAttribute(ATTRS.PROCESSED, 'checked'); } } }; // --- 主執行流程與菜單管理 --- const Main = { menuIds: [], scanPage: (source) => { logger.startBatch(); for (const sel of SELECTORS.TOP_LEVEL_FILTERS) { try { document.querySelectorAll(`${sel}:not([${ATTRS.PROCESSED}])`).forEach(el => RuleEngine.processContainer(el, source)); } catch (e) {} } logger.flushBatch(); }, resetAndRescan(message) { logger.info(message); document.querySelectorAll(`[${ATTRS.PROCESSED}]`).forEach(el => { el.style.display = ''; el.removeAttribute(ATTRS.PROCESSED); el.removeAttribute(ATTRS.HIDDEN_REASON); el.removeAttribute(ATTRS.WAIT_COUNT); }); RuleEngine.init(); this.scanPage('settings-changed'); this.setupMenu(); }, setupMenu() { this.menuIds.forEach(id => { try { GM_unregisterMenuCommand(id); } catch (e) {} }); this.menuIds = []; const addCmd = (text, func) => this.menuIds.push(GM_registerMenuCommand(text, func)); const s = (key) => CONFIG[key] ? '✅' : '❌'; addCmd(`${s('ENABLE_LOW_VIEW_FILTER')} 低觀看數過濾 (閾值: ${CONFIG.LOW_VIEW_THRESHOLD})`, () => { CONFIG.ENABLE_LOW_VIEW_FILTER = !CONFIG.ENABLE_LOW_VIEW_FILTER; GM_setValue('enableLowViewFilter', CONFIG.ENABLE_LOW_VIEW_FILTER); this.resetAndRescan(`低觀看數過濾 已${s('ENABLE_LOW_VIEW_FILTER') === '✅' ? '啟用' : '停用'}`); }); addCmd(`🔧 修改觀看數過濾閾值`, () => { const newThreshold = parseInt(prompt('請輸入新的低觀看數過濾閾值(純數字):', CONFIG.LOW_VIEW_THRESHOLD)); if (!isNaN(newThreshold) && newThreshold >= 0) { CONFIG.LOW_VIEW_THRESHOLD = newThreshold; GM_setValue('lowViewThreshold', newThreshold); this.resetAndRescan(`觀看數過濾閾值已更新為 ${newThreshold}`); } }); addCmd('--- 過濾規則開關 ---', () => {}); RuleEngine.rawRuleDefinitions.forEach(rule => { const mark = CONFIG.RULE_ENABLES[rule.id] !== false ? '✅' : '❌'; addCmd(`${mark} 過濾:${rule.name}`, () => { const isEnabled = CONFIG.RULE_ENABLES[rule.id] !== false; CONFIG.RULE_ENABLES[rule.id] = !isEnabled; GM_setValue('ruleEnables', CONFIG.RULE_ENABLES); this.resetAndRescan(`規則「${rule.name}」已${!isEnabled ? '啟用' : '停用'}`); }); }); addCmd('--- 系統 ---', () => {}); addCmd(`${s('DEBUG_MODE')} Debug 模式`, () => { CONFIG.DEBUG_MODE = !CONFIG.DEBUG_MODE; GM_setValue('debugMode', CONFIG.DEBUG_MODE); logger.info(`Debug 模式 已${s('DEBUG_MODE') === '✅' ? '啟用' : '停用'}`); this.setupMenu(); }); addCmd('🔄 恢復預設設定', () => { if (confirm('確定要將所有過濾規則和設定恢復為預設值嗎?')) { GM_setValue('ruleEnables', { ...DEFAULT_RULE_ENABLES }); GM_setValue('lowViewThreshold', DEFAULT_LOW_VIEW_THRESHOLD); CONFIG.RULE_ENABLES = { ...DEFAULT_RULE_ENABLES }; CONFIG.LOW_VIEW_THRESHOLD = DEFAULT_LOW_VIEW_THRESHOLD; this.resetAndRescan('所有設定已恢復為預設值。'); } }); }, init() { if (window.ytPantheonInitialized) return; window.ytPantheonInitialized = true; logger.logStart(); utils.injectCSS(); RuleEngine.init(); this.setupMenu(); Enhancer.initGlobalClickListener(); const debouncedScan = utils.debounce(() => this.scanPage('observer'), CONFIG.DEBOUNCE_DELAY); const observer = new MutationObserver(debouncedScan); const onReady = () => { if (!document.body) return; observer.observe(document.querySelector('ytd-app') || document.body, { childList: true, subtree: true }); window.addEventListener('yt-navigate-finish', () => this.scanPage('navigate')); this.scanPage('initial'); setInterval(() => { try { this.scanPage('periodic'); } catch(e){} }, CONFIG.PERIODIC_INTERVAL); }; document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', onReady, { once: true }) : onReady(); } }; Main.init(); })();