Greasy Fork is available in English.
隐藏 Shorts、广告、低观看数视频与杂讯,并绕过反广告拦截。20+ 过滤规则,完全可自定义。
// ==UserScript==
// @name YouTube Cleaner - Block Shorts, Ads & Clutter
// @name:zh-TW YouTube Cleaner - 隱藏 Shorts、廣告與雜訊
// @name:zh-CN YouTube Cleaner - 隐藏 Shorts、广告与杂讯
// @name:ja YouTube Cleaner - Shorts・広告・雑音をブロック
// @namespace http://tampermonkey.net/
// @version 1.6.1
// @description Hide YouTube Shorts, ads, low-view videos, clutter & bypass anti-adblock. 20+ filter rules, fully customizable.
// @description:zh-TW 隱藏 Shorts、廣告、低觀看數影片與雜訊,並繞過反廣告攔截。20+ 過濾規則,完全可自訂。
// @description:zh-CN 隐藏 Shorts、广告、低观看数视频与杂讯,并绕过反广告拦截。20+ 过滤规则,完全可自定义。
// @description:ja Shorts、広告、低視聴数動画、雑音を非表示。アンチ広告ブロック回避対応。20以上のフィルタールール。
// @author Benny & AI Collaborators
// @match https://www.youtube.com/*
// @exclude https://www.youtube.com/embed/*
// @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';
/**
* 🏛️ Architecture Overview (v2.0)
* 0. SELECTORS: Centralized selector management for easy maintenance.
* 1. ConfigManager: Unified state management for settings.
* 2. Utils: Stateless helper functions (parsing, debouncing).
* 3. Logger: centralized logging wrapper.
* 4. FilterStats: Statistics tracking for filtered content.
* 5. StyleManager: Handles CSS injection for high-performance static filtering.
* 6. AdBlockGuard: Specialized module for anti-adblock popup removal.
* 7. VideoFilter: The core engine for Dynamic Filtering (View counts, etc).
* 8. CustomRuleManager: Extensible rule system for easy adding of new text-based filters.
* 9. InteractionEnhancer: Open in new tab functionality.
* 10. UIManager: Handles the Tampermonkey menu interface.
* 11. App: Application entry point and orchestrator.
*/
// --- 0. Centralized Selectors (Easy maintenance when YouTube updates) ---
const SELECTORS = {
// 頂層容器 (用於過濾)
VIDEO_CONTAINERS: [
'ytd-rich-item-renderer',
'ytd-video-renderer',
'ytd-compact-video-renderer', // 播放頁側邊欄
'ytd-grid-video-renderer',
'yt-lockup-view-model',
'ytd-compact-radio-renderer', // 播放頁自動播放清單
'ytd-playlist-panel-video-renderer' // 播放清單面板
],
SECTION_CONTAINERS: [
'ytd-rich-section-renderer',
'ytd-rich-shelf-renderer',
'ytd-reel-shelf-renderer',
'grid-shelf-view-model',
'ytd-watch-next-secondary-results-renderer' // 播放頁推薦區塊
],
// Metadata 選擇器 (新舊版相容)
METADATA: {
// 觀看數/時間
TEXT: '.inline-metadata-item, #metadata-line span, .yt-content-metadata-view-model__metadata-text, yt-content-metadata-view-model .yt-core-attributed-string',
// 標題連結 (用於 aria-label 提取)
TITLE_LINKS: [
'a#video-title-link[aria-label]',
'a#thumbnail[aria-label]',
'a.yt-lockup-metadata-view-model__title[aria-label]',
'a[href*="/watch?"][aria-label]'
],
// 時長
DURATION: 'ytd-thumbnail-overlay-time-status-renderer, span.ytd-thumbnail-overlay-time-status-renderer, badge-shape .yt-badge-shape__text, yt-thumbnail-badge-view-model .yt-badge-shape__text',
// 頻道名稱
CHANNEL: 'ytd-channel-name, .ytd-channel-name, a[href^="/@"]',
// 標題文字
TITLE: '#video-title, #title, .yt-lockup-metadata-view-model__title, h3'
},
// 會員/廣告標記
BADGES: {
MEMBERS: '.badge-style-type-members-only, [aria-label*="會員專屬"], [aria-label*="Members only"]',
AD: '[aria-label*="廣告"], [aria-label*="Sponsor"], ad-badge-view-model, feed-ad-metadata-view-model',
SHORTS: 'a[href*="/shorts/"]',
MIX: 'a[aria-label*="合輯"], a[aria-label*="Mix"]'
},
// 互動排除
INTERACTION_EXCLUDE: 'button, yt-icon-button, #menu, ytd-menu-renderer, ytd-toggle-button-renderer, yt-chip-cloud-chip-renderer, .yt-spec-button-shape-next, .yt-core-attributed-string__link, #subscribe-button, .ytp-progress-bar, .ytp-chrome-bottom',
// 可點擊容器
CLICKABLE: [
'ytd-rich-item-renderer', 'ytd-video-renderer', 'ytd-compact-video-renderer',
'yt-lockup-view-model', 'ytd-playlist-renderer', 'ytd-compact-playlist-renderer',
'ytd-video-owner-renderer', 'ytd-grid-video-renderer'
],
// 內嵌預覽
PREVIEW_PLAYER: 'ytd-video-preview',
// 連結候選
LINK_CANDIDATES: [
'a#thumbnail[href*="/watch?"]', 'a#thumbnail[href*="/shorts/"]', 'a#thumbnail[href*="/playlist?"]',
'a#video-title-link', 'a#video-title', 'a.yt-simple-endpoint#video-title', 'a.yt-lockup-view-model-wiz__title'
],
// 生成組合選擇器
get allContainers() {
return [...this.VIDEO_CONTAINERS, ...this.SECTION_CONTAINERS].join(', ');
},
get videoContainersStr() {
return this.VIDEO_CONTAINERS.join(', ');
}
};
// --- 0.1 Filter Statistics ---
const FilterStats = {
counts: {},
session: { total: 0, byRule: {} },
record(reason) {
this.counts[reason] = (this.counts[reason] || 0) + 1;
this.session.total++;
this.session.byRule[reason] = (this.session.byRule[reason] || 0) + 1;
},
getSummary() {
return `已過濾 ${this.session.total} 個項目\n` +
Object.entries(this.session.byRule)
.sort((a, b) => b[1] - a[1])
.map(([k, v]) => ` ${k}: ${v}`)
.join('\n');
},
reset() {
this.session = { total: 0, byRule: {} };
}
};
// --- 0.3 Internationalization (i18n) ---
const I18N = {
_lang: null,
// 語言字典
strings: {
'zh-TW': {
title: 'YouTube 淨化大師',
menu_rules: '📂 設定過濾規則',
menu_low_view: '低觀看數過濾 (含直播)',
menu_threshold: '🔢 設定閾值',
menu_advanced: '🚫 進階過濾',
menu_new_tab: '強制新分頁',
menu_debug: 'Debug',
menu_reset: '🔄 恢復預設',
menu_stats: '📊 過濾統計',
menu_export: '💾 匯出/匯入設定',
menu_lang: '🌐 語言',
menu_input: '輸入選項:',
stats_title: '【 過濾統計 】',
stats_empty: '尚未過濾任何內容',
stats_filtered: '已過濾 {0} 個項目',
export_title: '【 設定管理 】',
export_export: '📤 匯出設定',
export_import: '📥 匯入設定',
export_success: '✅ 設定已複製到剪貼簿!',
export_copy: '請複製以下設定 (Ctrl+C):',
import_prompt: '請貼上設定 JSON:',
import_success: '✅ 設定已成功匯入!',
import_fail: '❌ 匯入失敗: ',
rules_title: '【 過濾規則 】',
rules_back: '(0 返回)',
threshold_prompt: '閾值:',
reset_confirm: '重設?',
lang_title: '【 選擇語言 】',
back: '返回',
adv_keyword_filter: '關鍵字過濾',
adv_keyword_list: '✏️ 關鍵字清單',
adv_channel_filter: '頻道過濾',
adv_channel_list: '✏️ 頻道清單',
adv_duration_filter: '長度過濾',
adv_duration_set: '⏱️ 設定長度',
adv_min: '最短(分):',
adv_max: '最長(分):',
adv_add: '新增',
adv_remove: '刪除',
adv_clear: '清空'
},
'zh-CN': {
title: 'YouTube 净化大师',
menu_rules: '📂 设置过滤规则',
menu_low_view: '低观看数过滤 (含直播)',
menu_threshold: '🔢 设置阈值',
menu_advanced: '🚫 高级过滤',
menu_new_tab: '强制新标签页',
menu_debug: 'Debug',
menu_reset: '🔄 恢复默认',
menu_stats: '📊 过滤统计',
menu_export: '💾 导出/导入设置',
menu_lang: '🌐 语言',
menu_input: '输入选项:',
stats_title: '【 过滤统计 】',
stats_empty: '尚未过滤任何内容',
stats_filtered: '已过滤 {0} 个项目',
export_title: '【 设置管理 】',
export_export: '📤 导出设置',
export_import: '📥 导入设置',
export_success: '✅ 设置已复制到剪贴板!',
export_copy: '请复制以下设置 (Ctrl+C):',
import_prompt: '请粘贴设置 JSON:',
import_success: '✅ 设置已成功导入!',
import_fail: '❌ 导入失败: ',
rules_title: '【 过滤规则 】',
rules_back: '(0 返回)',
threshold_prompt: '阈值:',
reset_confirm: '重置?',
lang_title: '【 选择语言 】',
back: '返回',
adv_keyword_filter: '关键字过滤',
adv_keyword_list: '✏️ 关键字列表',
adv_channel_filter: '频道过滤',
adv_channel_list: '✏️ 频道列表',
adv_duration_filter: '时长过滤',
adv_duration_set: '⏱️ 设置时长',
adv_min: '最短(分):',
adv_max: '最长(分):',
adv_add: '新增',
adv_remove: '删除',
adv_clear: '清空'
},
'en': {
title: 'YouTube Cleaner',
menu_rules: '📂 Filter Rules',
menu_low_view: 'Low View Filter (incl. Live)',
menu_threshold: '🔢 Set Threshold',
menu_advanced: '🚫 Advanced Filters',
menu_new_tab: 'Force New Tab',
menu_debug: 'Debug',
menu_reset: '🔄 Reset to Default',
menu_stats: '📊 Filter Stats',
menu_export: '💾 Export/Import Settings',
menu_lang: '🌐 Language',
menu_input: 'Enter option:',
stats_title: '【 Filter Statistics 】',
stats_empty: 'No content filtered yet',
stats_filtered: 'Filtered {0} items',
export_title: '【 Settings Management 】',
export_export: '📤 Export Settings',
export_import: '📥 Import Settings',
export_success: '✅ Settings copied to clipboard!',
export_copy: 'Copy settings (Ctrl+C):',
import_prompt: 'Paste settings JSON:',
import_success: '✅ Settings imported successfully!',
import_fail: '❌ Import failed: ',
rules_title: '【 Filter Rules 】',
rules_back: '(0 Back)',
threshold_prompt: 'Threshold:',
reset_confirm: 'Reset?',
lang_title: '【 Select Language 】',
back: 'Back',
adv_keyword_filter: 'Keyword Filter',
adv_keyword_list: '✏️ Keyword List',
adv_channel_filter: 'Channel Filter',
adv_channel_list: '✏️ Channel List',
adv_duration_filter: 'Duration Filter',
adv_duration_set: '⏱️ Set Duration',
adv_min: 'Min (min):',
adv_max: 'Max (min):',
adv_add: 'Add',
adv_remove: 'Remove',
adv_clear: 'Clear'
},
'ja': {
title: 'YouTube クリーナー',
menu_rules: '📂 フィルタルール',
menu_low_view: '低視聴数フィルター (ライブ含む)',
menu_threshold: '🔢 閾値設定',
menu_advanced: '🚫 詳細フィルター',
menu_new_tab: '新しいタブで開く',
menu_debug: 'デバッグ',
menu_reset: '🔄 初期化',
menu_stats: '📊 フィルター統計',
menu_export: '💾 設定のエクスポート/インポート',
menu_lang: '🌐 言語',
menu_input: 'オプションを入力:',
stats_title: '【 フィルター統計 】',
stats_empty: 'まだフィルターされたコンテンツはありません',
stats_filtered: '{0} 件をフィルターしました',
export_title: '【 設定管理 】',
export_export: '📤 設定をエクスポート',
export_import: '📥 設定をインポート',
export_success: '✅ 設定をクリップボードにコピーしました!',
export_copy: '設定をコピー (Ctrl+C):',
import_prompt: '設定JSONを貼り付け:',
import_success: '✅ 設定をインポートしました!',
import_fail: '❌ インポート失敗: ',
rules_title: '【 フィルタールール 】',
rules_back: '(0 戻る)',
threshold_prompt: '閾値:',
reset_confirm: 'リセットしますか?',
lang_title: '【 言語選択 】',
back: '戻る',
adv_keyword_filter: 'キーワードフィルター',
adv_keyword_list: '✏️ キーワードリスト',
adv_channel_filter: 'チャンネルフィルター',
adv_channel_list: '✏️ チャンネルリスト',
adv_duration_filter: '長さフィルター',
adv_duration_set: '⏱️ 長さ設定',
adv_min: '最短(分):',
adv_max: '最長(分):',
adv_add: '追加',
adv_remove: '削除',
adv_clear: 'クリア'
}
},
// 規則名稱翻譯
ruleNames: {
'zh-TW': {
ad_block_popup: '廣告阻擋彈窗',
ad_sponsor: '廣告/贊助',
members_only: '會員專屬',
shorts_item: 'Shorts 項目',
mix_only: '合輯',
premium_banner: 'Premium 橫幅',
news_block: '新聞區塊',
shorts_block: 'Shorts 區塊',
posts_block: '社群貼文',
playables_block: '可玩內容',
fundraiser_block: '募款活動',
shorts_grid_shelf: 'Shorts 網格',
movies_shelf: '電影推薦',
youtube_featured_shelf: 'YouTube 精選',
popular_gaming_shelf: '熱門遊戲',
more_from_game_shelf: '更多遊戲內容',
trending_playlist: '熱門播放清單',
inline_survey: '問卷調查',
clarify_box: '資訊框',
explore_topics: '探索主題',
recommended_playlists: '推薦播放清單',
members_early_access: '會員搶先看'
},
'zh-CN': {
ad_block_popup: '广告拦截弹窗',
ad_sponsor: '广告/赞助',
members_only: '会员专属',
shorts_item: 'Shorts 项目',
mix_only: '合辑',
premium_banner: 'Premium 横幅',
news_block: '新闻区块',
shorts_block: 'Shorts 区块',
posts_block: '社区帖子',
playables_block: '可玩内容',
fundraiser_block: '募款活动',
shorts_grid_shelf: 'Shorts 网格',
movies_shelf: '电影推荐',
youtube_featured_shelf: 'YouTube 精选',
popular_gaming_shelf: '热门游戏',
more_from_game_shelf: '更多游戏内容',
trending_playlist: '热门播放列表',
inline_survey: '问卷调查',
clarify_box: '信息框',
explore_topics: '探索主题',
recommended_playlists: '推荐播放列表',
members_early_access: '会员抢先看'
},
'en': {
ad_block_popup: 'Ad-block Popup',
ad_sponsor: 'Ads / Sponsors',
members_only: 'Members Only',
shorts_item: 'Shorts Items',
mix_only: 'Mix Playlists',
premium_banner: 'Premium Banner',
news_block: 'News Section',
shorts_block: 'Shorts Section',
posts_block: 'Community Posts',
playables_block: 'Playables',
fundraiser_block: 'Fundraiser',
shorts_grid_shelf: 'Shorts Grid',
movies_shelf: 'Movies Shelf',
youtube_featured_shelf: 'YouTube Featured',
popular_gaming_shelf: 'Popular Gaming',
more_from_game_shelf: 'More from Games',
trending_playlist: 'Trending Playlist',
inline_survey: 'Surveys',
clarify_box: 'Clarify Box',
explore_topics: 'Explore Topics',
recommended_playlists: 'Recommended Playlists',
members_early_access: 'Members Early Access'
},
'ja': {
ad_block_popup: '広告ブロックポップアップ',
ad_sponsor: '広告/スポンサー',
members_only: 'メンバー限定',
shorts_item: 'Shorts アイテム',
mix_only: 'ミックス',
premium_banner: 'Premium バナー',
news_block: 'ニュースセクション',
shorts_block: 'Shorts セクション',
posts_block: 'コミュニティ投稿',
playables_block: 'プレイアブル',
fundraiser_block: '募金活動',
shorts_grid_shelf: 'Shorts グリッド',
movies_shelf: '映画のおすすめ',
youtube_featured_shelf: 'YouTube おすすめ',
popular_gaming_shelf: '人気ゲーム',
more_from_game_shelf: 'ゲーム関連',
trending_playlist: '急上昇プレイリスト',
inline_survey: 'アンケート',
clarify_box: '情報ボックス',
explore_topics: 'トピックを探す',
recommended_playlists: 'おすすめプレイリスト',
members_early_access: 'メンバー先行'
}
},
// 取得規則顯示名稱
getRuleName(ruleKey) {
return this.ruleNames[this.lang]?.[ruleKey] || this.ruleNames['en'][ruleKey] || ruleKey;
},
// 自動偵測語言
detectLanguage() {
const ytLang = document.documentElement.lang || navigator.language || 'zh-TW';
if (ytLang.startsWith('zh-CN') || ytLang.startsWith('zh-Hans')) return 'zh-CN';
if (ytLang.startsWith('zh')) return 'zh-TW';
if (ytLang.startsWith('ja')) return 'ja';
return 'en';
},
get lang() {
if (!this._lang) {
this._lang = GM_getValue('ui_language', null) || this.detectLanguage();
}
return this._lang;
},
set lang(value) {
this._lang = value;
GM_setValue('ui_language', value);
},
// 取得翻譯字串
t(key, ...args) {
const str = this.strings[this.lang]?.[key] || this.strings['en'][key] || key;
return str.replace(/\{(\d+)\}/g, (_, i) => args[i] ?? '');
},
// 語言清單
get availableLanguages() {
return {
'zh-TW': '繁體中文',
'zh-CN': '简体中文',
'en': 'English',
'ja': '日本語'
};
}
};
// --- 1. Core: Configuration Management ---
class ConfigManager {
constructor() {
this.defaults = {
LOW_VIEW_THRESHOLD: 1000,
ENABLE_LOW_VIEW_FILTER: true,
DEBUG_MODE: false,
OPEN_IN_NEW_TAB: true,
ENABLE_KEYWORD_FILTER: false,
KEYWORD_BLACKLIST: [],
ENABLE_CHANNEL_FILTER: false,
CHANNEL_BLACKLIST: [],
ENABLE_DURATION_FILTER: false,
DURATION_MIN: 0,
DURATION_MAX: 0,
GRACE_PERIOD_HOURS: 4,
// These connect to simple toggle switches
RULE_ENABLES: {
ad_block_popup: true, ad_sponsor: true, members_only: true, shorts_item: true,
mix_only: true, premium_banner: true, news_block: true, shorts_block: true,
posts_block: true, playables_block: true, fundraiser_block: true,
shorts_grid_shelf: true, movies_shelf: true,
youtube_featured_shelf: true, popular_gaming_shelf: true,
more_from_game_shelf: true, trending_playlist: true,
inline_survey: true, clarify_box: true, explore_topics: true,
recommended_playlists: true, members_early_access: true
}
};
this.state = this._load();
}
_load() {
const get = (k, d) => GM_getValue(k, d);
const snake = str => str.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`);
const loaded = {};
for (const key in this.defaults) {
if (key === 'RULE_ENABLES') {
const saved = get('ruleEnables', {});
loaded[key] = { ...this.defaults.RULE_ENABLES, ...saved };
} else {
loaded[key] = get(snake(key), this.defaults[key]);
if (Array.isArray(this.defaults[key]) && !Array.isArray(loaded[key])) {
loaded[key] = [...this.defaults[key]];
}
}
}
return loaded;
}
get(key) { return this.state[key]; }
set(key, value) {
this.state[key] = value;
const snake = str => str.replace(/[A-Z]/g, l => `_${l.toLowerCase()}`);
if (key === 'RULE_ENABLES') GM_setValue('ruleEnables', value);
else GM_setValue(snake(key), value);
}
toggleRule(ruleId) {
this.state.RULE_ENABLES[ruleId] = !this.state.RULE_ENABLES[ruleId];
this.set('RULE_ENABLES', this.state.RULE_ENABLES);
}
}
// --- 2. Core: Utilities (Enhanced i18n Support) ---
const Utils = {
debounce: (func, delay) => {
let t;
return (...args) => { clearTimeout(t); t = setTimeout(() => func(...args), delay); };
},
// 國際化數字解析 (支援多語言)
parseNumeric: (text, type = 'any') => {
if (!text) return null;
const clean = text.replace(/,/g, '').toLowerCase().trim();
// 排除時間字串
if (type === 'view' && /(ago|前|hour|minute|day|week|month|year|秒|分|時|天|週|月|年|時間|전|日|ヶ月|年前)/.test(clean)) return null;
// 支援各種語言的數字單位
const match = clean.match(/([\d.]+)\s*([kmb千萬万億亿]|천|만|억|lakh|crore)?/i);
if (!match) return null;
let num = parseFloat(match[1]);
const unit = match[2]?.toLowerCase();
if (unit) {
const unitMap = {
// 英文
'k': 1e3, 'm': 1e6, 'b': 1e9,
// 繁體中文
'千': 1e3, '萬': 1e4, '億': 1e8,
// 簡體中文
'万': 1e4, '亿': 1e8,
// 日文 (同中文)
// 韓文
'천': 1e3, '만': 1e4, '억': 1e8,
// 印度
'lakh': 1e5, 'crore': 1e7
};
num *= (unitMap[unit] || 1);
}
return Math.floor(num);
},
parseDuration: (text) => {
if (!text) return null;
const parts = text.trim().split(':').map(Number);
if (parts.some(isNaN)) return null;
return parts.length === 3
? parts[0] * 3600 + parts[1] * 60 + parts[2]
: (parts.length === 2 ? parts[0] * 60 + parts[1] : null);
},
// 國際化時間解析 (支援多語言)
parseTimeAgo: (text) => {
if (!text) return null;
const raw = text.toLowerCase();
// 秒
if (/second|秒|초|วินาที/.test(raw)) return 0;
const match = raw.match(/(\d+)/);
if (!match) return null;
const val = parseInt(match[1], 10);
// 分鐘
if (/minute|分鐘|分钟|分|분|นาที/.test(raw)) return val;
// 小時
if (/hour|小時|小时|時間|시간|ชั่วโมง/.test(raw)) return val * 60;
// 天
if (/day|天|日|일|วัน/.test(raw)) return val * 1440;
// 週
if (/week|週|周|주|สัปดาห์/.test(raw)) return val * 10080;
// 月
if (/month|月|ヶ月|개월|เดือน/.test(raw)) return val * 43200;
// 年
if (/year|年|년|ปี/.test(raw)) return val * 525600;
return null;
},
// 解析直播觀看數 (支援「正在觀看」「觀眾」等關鍵字)
parseLiveViewers: (text) => {
if (!text) return null;
const liveKeywords = /(正在觀看|觀眾|watching|viewers)/i;
if (!liveKeywords.test(text)) return null;
return Utils.parseNumeric(text, 'any');
},
// 從 aria-label 提取觀看數資訊
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 '';
}
};
// --- 3. Core: Logger ---
const Logger = {
enabled: false,
prefix: `[Purifier]`,
info(msg, ...args) { if (this.enabled) console.log(`%c${this.prefix} ${msg}`, 'color:#3498db;font-weight:bold', ...args); },
warn(msg, ...args) { if (this.enabled) console.warn(`${this.prefix} ${msg}`, ...args); }
};
// --- 4. Module: Custom Rule Manager (Extensibility) ---
/**
* Designed to make adding new simple text-based rules easy.
* Add new entries to the `definitions` array here.
*/
class CustomRuleManager {
constructor(config) {
this.config = config;
// ★ ADD NEW RULES HERE ★
// Format: { key: 'config_key_name', rules: [/Regex/i, 'String'], type: 'text' (default) }
this.definitions = [
// 從 v1.4.0 還原的文字匹配規則 (作為 CSS 的備援)
{ key: 'members_only', rules: [/頻道會員專屬|Members only/i] },
{ key: 'mix_only', rules: [/^(合輯|Mix)[\s\-–]/i] },
// 區塊/Shelf 類規則
{ key: 'news_block', rules: [/新聞快報|Breaking News|ニュース/i] },
{ key: 'posts_block', rules: [/貼文|Posts|投稿|Publicaciones|最新 YouTube 貼文/i] },
{ key: 'playables_block', rules: [/Playables|遊戲角落/i] },
{ key: 'fundraiser_block', rules: [/Fundraiser|募款/i] },
{ key: 'popular_gaming_shelf', rules: [/熱門遊戲直播/i] },
{ key: 'explore_topics', rules: [/探索更多主題|Explore more topics/i] },
{ key: 'movies_shelf', rules: [/為你推薦的特選電影|featured movies|YouTube 精選/i] },
{ key: 'trending_playlist', rules: [/發燒影片|Trending/i] },
{ key: 'youtube_featured_shelf', rules: [/YouTube 精選/i] },
{ key: 'shorts_block', rules: [/^Shorts$/i] },
{ key: 'shorts_grid_shelf', rules: [/^Shorts$/i] },
{ key: 'more_from_game_shelf', rules: [/^更多此遊戲相關內容$/i] },
{ key: 'members_early_access', rules: [/會員優先|Members Early Access|Early access for members/i] }
];
}
check(element, textContent) {
const enables = this.config.get('RULE_ENABLES');
for (const def of this.definitions) {
if (enables[def.key]) { // Only check if enabled in config
for (const rule of def.rules) {
if (rule instanceof RegExp) {
if (rule.test(textContent)) return def.key;
} else if (textContent.includes(rule)) {
return def.key;
}
}
}
}
return null;
}
}
// --- 5. Module: Style Manager (CSS) ---
class StyleManager {
constructor(config) { this.config = config; }
apply() {
const rules = [];
const enables = this.config.get('RULE_ENABLES');
// 5.1 Global Fixes
rules.push('body, html { font-family: "YouTube Noto", Roboto, Arial, "PingFang SC", "Microsoft YaHei", sans-serif !important; }');
// 5.2 Anti-Adblock (完整還原 v1.4.0)
if (enables.ad_block_popup) {
rules.push(`
tp-yt-paper-dialog:has(ytd-enforcement-message-view-model),
ytd-enforcement-message-view-model,
#immersive-translate-browser-popup,
tp-yt-iron-overlay-backdrop:has(~ tp-yt-paper-dialog ytd-enforcement-message-view-model),
tp-yt-iron-overlay-backdrop.opened,
yt-playability-error-supported-renderers:has(ytd-enforcement-message-view-model) { display: none !important; }
ytd-app:has(ytd-enforcement-message-view-model), body:has(ytd-enforcement-message-view-model), html:has(ytd-enforcement-message-view-model) {
overflow: auto !important; overflow-y: auto !important; position: static !important;
pointer-events: auto !important; height: auto !important; top: 0 !important;
margin-right: 0 !important; overscroll-behavior: auto !important;
}
ytd-app[aria-hidden="true"]:has(ytd-enforcement-message-view-model) {
aria-hidden: false !important; display: block !important;
}
ytd-app { --ytd-app-scroll-offset: 0 !important; }
`);
}
// 5.3 Simple Selection (CSS)
// ★ Add new Selector-based rules here
const map = {
ad_sponsor: [
'ytd-ad-slot-renderer',
'ytd-promoted-sparkles-text-search-renderer',
'#masthead-ad',
'ytd-rich-item-renderer:has(.ytd-ad-slot-renderer)',
'feed-ad-metadata-view-model',
'ad-badge-view-model'
],
premium_banner: ['ytd-statement-banner-renderer', 'ytd-rich-section-renderer:has(ytd-statement-banner-renderer)'],
clarify_box: ['ytd-info-panel-container-renderer'],
inline_survey: ['ytd-rich-section-renderer:has(ytd-inline-survey-renderer)'],
playables_block: ['ytd-rich-section-renderer:has(ytd-rich-shelf-renderer[is-playables])', 'ytd-game-card-renderer']
};
for (const [key, selectors] of Object.entries(map)) {
if (enables[key]) rules.push(`${selectors.join(', ')} { display: none !important; }`);
}
// 5.4 Advanced :has() Rules
// ★ Add new Container rules here
const VIDEO_CONTAINERS = 'ytd-rich-item-renderer, ytd-video-renderer, ytd-compact-video-renderer, ytd-grid-video-renderer, yt-lockup-view-model';
const hasRules = [
{ key: 'ad_sponsor', selector: '[aria-label*="廣告"], [aria-label*="Sponsor"], [aria-label="贊助商廣告"], ad-badge-view-model, feed-ad-metadata-view-model' },
{ key: 'members_only', selector: '[aria-label*="會員專屬"]' },
{ key: 'shorts_item', selector: 'a[href*="/shorts/"]' },
{ key: 'mix_only', selector: 'a[aria-label*="合輯"], a[aria-label*="Mix"]' }
];
hasRules.forEach(({ key, selector }) => {
if (enables[key]) {
const containers = VIDEO_CONTAINERS.split(',').map(s => s.trim());
containers.forEach(c => rules.push(`${c}:has(${selector}) { display: none !important; }`));
}
});
// 5.5 首頁推薦播放清單 (不影響頻道頁面)
if (enables.recommended_playlists) {
rules.push(`
ytd-browse[page-subtype="home"] ytd-rich-item-renderer:has(a[href^="/playlist?list="]),
ytd-browse[page-subtype="home"] ytd-rich-item-renderer:has([content-id^="PL"]) { display: none !important; }
`);
}
GM_addStyle(rules.join('\n'));
Logger.info('Static CSS rules injected');
}
}
// --- 6. Module: AdBlock Guard (Enhanced with Whitelist) ---
class AdBlockGuard {
constructor() {
// 多語言關鍵字偵測
this.keywords = [
'Ad blockers', '廣告攔截器', '广告拦截器', '広告ブロッカー', '광고 차단기',
'Video player will be blocked', '影片播放器將被封鎖', '视频播放器将被封锁',
'Allow YouTube', '允許 YouTube', '允许 YouTube',
'You have an ad blocker', '您使用了廣告攔截器',
'YouTube 禁止使用廣告攔截器', "YouTube doesn't allow ad blockers"
];
// 白名單選擇器 - 這些對話框絕不是廣告警告
this.whitelistSelectors = [
'ytd-sponsorships-offer-renderer', // 會員加入視窗
'ytd-about-channel-renderer', // 頻道資訊視窗
'ytd-report-form-modal-renderer', // 檢舉視窗
'ytd-multi-page-menu-renderer', // 通用選單
'ytd-playlist-add-to-option-renderer' // 加入播放清單視窗
];
this.lastTrigger = 0;
}
start() {
const beat = () => {
this.checkAndClean();
setTimeout(() => requestAnimationFrame(beat), 800);
};
beat();
}
isWhitelisted(dialog) {
for (const sel of this.whitelistSelectors) {
if (dialog.querySelector(sel)) {
Logger.info(`✅ Whitelist dialog detected: ${sel}`);
return true;
}
}
return false;
}
isAdBlockPopup(dialog) {
// ytd-enforcement-message-view-model 是廣告攔截專屬標籤,直接判定
if (dialog.tagName === 'YTD-ENFORCEMENT-MESSAGE-VIEW-MODEL') {
return true;
}
// 檢查是否包含廣告攔截專屬標籤
if (dialog.querySelector('ytd-enforcement-message-view-model')) {
return true;
}
// 深度關鍵字檢查
if (dialog.innerText && this.keywords.some(k => dialog.innerText.includes(k))) {
return true;
}
return false;
}
checkAndClean() {
// 更積極的彈窗選擇器
const popupSelectors = [
'tp-yt-paper-dialog',
'ytd-enforcement-message-view-model',
'yt-playability-error-supported-renderers',
'ytd-popup-container tp-yt-paper-dialog',
'[role="dialog"]:has(ytd-enforcement-message-view-model)'
];
const dialogs = document.querySelectorAll(popupSelectors.join(', '));
let detected = false;
for (const dialog of dialogs) {
// ★ 白名單優先檢查 - 避免誤殺會員視窗等
if (this.isWhitelisted(dialog)) continue;
if (this.isAdBlockPopup(dialog)) {
// 嘗試點擊關閉按鈕
const dismissBtns = dialog.querySelectorAll('[aria-label="Close"], #dismiss-button, [aria-label="可能有風險"], .yt-spec-button-shape-next--call-to-action');
dismissBtns.forEach(btn => btn.click());
dialog.remove();
detected = true;
Logger.info(`🚫 Removed AdBlock Popup: ${dialog.tagName}`);
}
}
if (detected) {
// 移除背景遮罩 (包含所有可能的遮罩)
document.querySelectorAll('tp-yt-iron-overlay-backdrop, .ytd-popup-container, [style*="z-index: 9999"]').forEach(b => {
if (b.classList.contains('opened') || b.style.display !== 'none') {
b.style.display = 'none';
b.remove();
}
});
this.unlockScroll();
this.resumeVideo();
}
}
unlockScroll() {
const css = (el, props) => {
if (!el) return;
for (const [key, val] of Object.entries(props)) {
el.style.setProperty(key, val, 'important');
}
};
const allowScrollProps = {
'overflow-y': 'auto',
'overflow-x': 'hidden',
'position': 'static',
'pointer-events': 'auto',
'top': 'auto',
'display': 'block'
};
css(document.body, allowScrollProps);
css(document.documentElement, allowScrollProps);
const ytdApp = document.querySelector('ytd-app');
if (ytdApp) {
css(ytdApp, allowScrollProps);
ytdApp.removeAttribute('aria-hidden');
}
// 移除播放器模糊效果
const watchPage = document.querySelector('ytd-watch-flexy');
if (watchPage) {
watchPage.style.removeProperty('filter');
}
}
resumeVideo() {
// 只有剛偵測到彈窗時才強制播放,避免過度積極
if (Date.now() - this.lastTrigger > 3000) {
this.lastTrigger = Date.now();
const video = document.querySelector('video');
if (video && video.paused && !video.ended) {
video.play().catch(() => { });
}
}
}
}
// --- 7. Module: Video Filter (Lazy Evaluator) ---
class LazyVideoData {
constructor(element) {
this.el = element;
this._title = null;
this._channel = null;
this._viewCount = undefined;
this._liveViewers = undefined;
this._timeAgo = undefined;
this._duration = undefined;
}
get title() {
if (this._title === null) this._title = this.el.querySelector(SELECTORS.METADATA.TITLE)?.textContent?.trim() || '';
return this._title;
}
get channel() {
if (this._channel === null) this._channel = this.el.querySelector(SELECTORS.METADATA.CHANNEL)?.textContent?.trim() || '';
return this._channel;
}
_parseMetadata() {
if (this._viewCount !== undefined) return;
// 使用集中管理的選擇器
const texts = Array.from(this.el.querySelectorAll(SELECTORS.METADATA.TEXT));
// 嘗試從 aria-label 提取
let aria = '';
for (const sel of SELECTORS.METADATA.TITLE_LINKS) {
const el = this.el.querySelector(`:scope ${sel}`);
if (el?.ariaLabel) { aria = el.ariaLabel; break; }
}
if (texts.length === 0 && aria) {
this._viewCount = Utils.parseNumeric(aria, 'view');
this._liveViewers = Utils.parseLiveViewers(aria);
this._timeAgo = Utils.parseTimeAgo(aria);
return;
}
this._viewCount = null;
this._liveViewers = null;
this._timeAgo = null;
for (const t of texts) {
const text = t.textContent;
// 直播觀看數優先檢查
if (this._liveViewers === null) this._liveViewers = Utils.parseLiveViewers(text);
// 一般觀看數
if (this._viewCount === null && /view|觀看|次/i.test(text)) this._viewCount = Utils.parseNumeric(text, 'view');
// 時間
if (this._timeAgo === null && /ago|前/i.test(text)) this._timeAgo = Utils.parseTimeAgo(text);
}
}
get viewCount() { this._parseMetadata(); return this._viewCount; }
get liveViewers() { this._parseMetadata(); return this._liveViewers; }
get timeAgo() { this._parseMetadata(); return this._timeAgo; }
get duration() {
if (this._duration === undefined) {
const el = this.el.querySelector(SELECTORS.METADATA.DURATION);
this._duration = el ? Utils.parseDuration(el.textContent) : null;
}
return this._duration;
}
get isShorts() { return !!this.el.querySelector(SELECTORS.BADGES.SHORTS); }
get isLive() { return this._liveViewers !== null; }
get isMembers() {
return this.el.querySelector(SELECTORS.BADGES.MEMBERS) ||
this.el.innerText.includes('會員專屬') ||
this.el.innerText.includes('Members only');
}
}
class VideoFilter {
constructor(config) {
this.config = config;
this.customRules = new CustomRuleManager(config);
}
// 使用 requestIdleCallback 分批處理以優化效能
processPage() {
const elements = Array.from(document.querySelectorAll(SELECTORS.allContainers));
const unprocessed = elements.filter(el => !el.dataset.ypChecked);
if (unprocessed.length === 0) return;
// 如果瀏覽器支援 requestIdleCallback,使用分批處理
if ('requestIdleCallback' in window) {
this._processBatch(unprocessed, 0);
} else {
// Fallback: 直接處理
for (const el of unprocessed) this.processElement(el);
}
}
_processBatch(elements, startIndex, batchSize = 20) {
requestIdleCallback((deadline) => {
let i = startIndex;
// 在空閒時間內處理盡可能多的元素
while (i < elements.length && (deadline.timeRemaining() > 0 || deadline.didTimeout)) {
this.processElement(elements[i]);
i++;
// 每批最多處理 batchSize 個
if (i - startIndex >= batchSize) break;
}
// 如果還有未處理的元素,繼續排程
if (i < elements.length) {
this._processBatch(elements, i, batchSize);
}
}, { timeout: 500 }); // 500ms 超時保證
}
processElement(element) {
if (element.dataset.ypChecked) return;
if (element.offsetParent === null) return;
// 7.2 Custom Text Rules Check (Extensible)
const textRule = this.customRules.check(element, element.innerText);
if (textRule) return this._hide(element, textRule);
// 7.3 Base Logic
if (element.tagName.includes('VIDEO') || element.tagName.includes('LOCKUP') || element.tagName.includes('RICH-ITEM')) {
const item = new LazyVideoData(element);
// Advanced Filters
// Advanced Filters
if (this.config.get('ENABLE_KEYWORD_FILTER') && item.title) {
if (this.config.get('KEYWORD_BLACKLIST').some(k => item.title.toLowerCase().includes(k.toLowerCase()))) return this._hide(element, 'keyword_blacklist');
}
if (this.config.get('ENABLE_CHANNEL_FILTER') && item.channel) {
if (this.config.get('CHANNEL_BLACKLIST').some(k => item.channel.toLowerCase().includes(k.toLowerCase()))) return this._hide(element, 'channel_blacklist');
}
// 強化會員過濾 (JS補刀):若開啟成員過濾且偵測到是會員影片,直接隱藏
if (this.config.get('RULE_ENABLES').members_only && item.isMembers) {
return this._hide(element, 'members_only_js');
}
if (this.config.get('ENABLE_LOW_VIEW_FILTER') && !item.isShorts) {
const th = this.config.get('LOW_VIEW_THRESHOLD');
const grace = this.config.get('GRACE_PERIOD_HOURS') * 60;
// 直播觀看數過濾 (不受豁免期限制)
if (item.isLive && item.liveViewers !== null && item.liveViewers < th) {
return this._hide(element, 'low_viewer_live');
}
// 一般影片觀看數過濾 (受豁免期限制)
if (!item.isLive && item.viewCount !== null && item.timeAgo !== null && item.timeAgo > grace && item.viewCount < th) {
return this._hide(element, 'low_view');
}
}
if (this.config.get('ENABLE_DURATION_FILTER') && !item.isShorts && item.duration !== null) {
const min = this.config.get('DURATION_MIN');
const max = this.config.get('DURATION_MAX');
if ((min > 0 && item.duration < min) || (max > 0 && item.duration > max)) return this._hide(element, 'duration_filter');
}
}
element.dataset.ypChecked = 'true';
}
_hide(element, reason) {
element.style.display = 'none';
element.dataset.ypHidden = reason;
FilterStats.record(reason); // 記錄統計
Logger.info(`Hidden [${reason}]`, element);
}
reset() {
document.querySelectorAll('[data-yp-hidden]').forEach(el => {
el.style.display = '';
delete el.dataset.ypHidden;
delete el.dataset.ypChecked;
});
FilterStats.reset(); // 重設統計
}
}
// --- 8. Module: Interaction Enhancer (使用集中選擇器) ---
class InteractionEnhancer {
constructor(config) {
this.config = config;
}
findPrimaryLink(container) {
if (!container) return null;
for (const sel of SELECTORS.LINK_CANDIDATES) {
const a = container.querySelector(sel);
if (a?.href) return a;
}
return container.querySelector('a[href*="/watch?"], a[href*="/shorts/"], a[href*="/playlist?"]');
}
init() {
document.addEventListener('click', (e) => {
if (!this.config.get('OPEN_IN_NEW_TAB')) return;
if (e.button !== 0 || e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
// 使用集中管理的排除清單
if (e.target.closest(SELECTORS.INTERACTION_EXCLUDE)) return;
let targetLink = null;
const previewPlayer = e.target.closest(SELECTORS.PREVIEW_PLAYER);
if (previewPlayer) {
targetLink = this.findPrimaryLink(previewPlayer) || this.findPrimaryLink(previewPlayer.closest(SELECTORS.CLICKABLE.join(',')));
} else {
const container = e.target.closest(SELECTORS.CLICKABLE.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 : this.findPrimaryLink(container);
}
if (!targetLink) return;
try {
const hostname = new URL(targetLink.href, location.origin).hostname;
const isValidTarget = targetLink.href && /(^|\.)youtube\.com$/.test(hostname);
if (isValidTarget) {
e.preventDefault();
e.stopImmediatePropagation();
window.open(targetLink.href, '_blank');
}
} catch (err) { }
}, { capture: true });
}
}
// --- 9. Module: UI Manager (Enhanced with i18n) ---
class UIManager {
constructor(config, onRefresh) { this.config = config; this.onRefresh = onRefresh; }
t(key, ...args) { return I18N.t(key, ...args); }
showMainMenu() {
const i = (k) => this.config.get(k) ? '✅' : '❌';
const statsInfo = FilterStats.session.total > 0 ? ` (${FilterStats.session.total})` : '';
const langName = I18N.availableLanguages[I18N.lang];
const choice = prompt(
`【 ${this.t('title')} v1.6.0 】\n\n` +
`1. ${this.t('menu_rules')}\n` +
`2. ${i('ENABLE_LOW_VIEW_FILTER')} ${this.t('menu_low_view')}\n` +
`3. ${this.t('menu_threshold')} (${this.config.get('LOW_VIEW_THRESHOLD')})\n` +
`4. ${this.t('menu_advanced')}\n` +
`5. ${i('OPEN_IN_NEW_TAB')} ${this.t('menu_new_tab')}\n` +
`6. ${i('DEBUG_MODE')} ${this.t('menu_debug')}\n` +
`7. ${this.t('menu_reset')}\n` +
`8. ${this.t('menu_stats')}${statsInfo}\n` +
`9. ${this.t('menu_export')}\n` +
`10. ${this.t('menu_lang')} [${langName}]\n\n` +
this.t('menu_input')
);
if (choice) this.handleMenu(choice);
}
handleMenu(c) {
switch (c.trim()) {
case '1': this.showRuleMenu(); break;
case '2': this.toggle('ENABLE_LOW_VIEW_FILTER'); break;
case '3': const v = prompt(this.t('threshold_prompt')); if (v) this.update('LOW_VIEW_THRESHOLD', Number(v)); break;
case '4': this.showAdvancedMenu(); break;
case '5': this.toggle('OPEN_IN_NEW_TAB'); break;
case '6': this.toggle('DEBUG_MODE'); break;
case '7': if (confirm(this.t('reset_confirm'))) { Object.keys(this.config.defaults).forEach(k => this.config.set(k, this.config.defaults[k])); this.update('', null); } break;
case '8': this.showStats(); break;
case '9': this.showExportImportMenu(); break;
case '10': this.showLanguageMenu(); break;
}
}
showStats() {
const summary = FilterStats.getSummary();
alert(`${this.t('stats_title')}\n\n${summary || this.t('stats_empty')}`);
this.showMainMenu();
}
showLanguageMenu() {
const langs = I18N.availableLanguages;
const keys = Object.keys(langs);
const current = I18N.lang;
const menu = keys.map((k, i) => `${i + 1}. ${k === current ? '✅' : '⬜'} ${langs[k]}`).join('\n');
const c = prompt(`${this.t('lang_title')}\n\n${menu}\n\n0. ${this.t('back')}`);
if (c && c !== '0') {
const idx = parseInt(c) - 1;
if (keys[idx]) {
I18N.lang = keys[idx];
alert(`✅ ${langs[keys[idx]]}`);
}
}
this.showMainMenu();
}
showExportImportMenu() {
const c = prompt(`${this.t('export_title')}\n\n1. ${this.t('export_export')}\n2. ${this.t('export_import')}\n0. ${this.t('back')}`);
if (c === '1') this.exportSettings();
else if (c === '2') this.importSettings();
else if (c === '0') this.showMainMenu();
}
exportSettings() {
const exportData = {
version: '1.6.0',
timestamp: new Date().toISOString(),
settings: this.config.state,
language: I18N.lang
};
const json = JSON.stringify(exportData, null, 2);
navigator.clipboard.writeText(json).then(() => {
alert(this.t('export_success'));
}).catch(() => {
prompt(this.t('export_copy'), json);
});
this.showExportImportMenu();
}
importSettings() {
const json = prompt(this.t('import_prompt'));
if (!json) { this.showExportImportMenu(); return; }
try {
const data = JSON.parse(json);
if (!data.settings) throw new Error('Invalid format');
for (const key in data.settings) {
if (key in this.config.defaults) {
this.config.set(key, data.settings[key]);
}
}
if (data.language) I18N.lang = data.language;
alert(this.t('import_success'));
this.onRefresh();
} catch (e) {
alert(this.t('import_fail') + e.message);
}
this.showExportImportMenu();
}
showRuleMenu() {
const r = this.config.get('RULE_ENABLES'); const k = Object.keys(r);
const c = prompt(`${this.t('rules_title')} ${this.t('rules_back')}\n` + k.map((key, i) => `${i + 1}. [${r[key] ? '✅' : '❌'}] ${I18N.getRuleName(key)}`).join('\n'));
if (c && c !== '0') { this.config.toggleRule(k[parseInt(c) - 1]); this.onRefresh(); this.showRuleMenu(); } else if (c === '0') this.showMainMenu();
}
showAdvancedMenu() {
const i = (k) => this.config.get(k) ? '✅' : '❌';
const c = prompt(
`1. ${i('ENABLE_KEYWORD_FILTER')} ${this.t('adv_keyword_filter')}\n` +
`2. ${this.t('adv_keyword_list')}\n` +
`3. ${i('ENABLE_CHANNEL_FILTER')} ${this.t('adv_channel_filter')}\n` +
`4. ${this.t('adv_channel_list')}\n` +
`5. ${i('ENABLE_DURATION_FILTER')} ${this.t('adv_duration_filter')}\n` +
`6. ${this.t('adv_duration_set')}\n` +
`0. ${this.t('back')}`
);
if (c === '1' || c === '3' || c === '5') this.toggle(c === '1' ? 'ENABLE_KEYWORD_FILTER' : c === '3' ? 'ENABLE_CHANNEL_FILTER' : 'ENABLE_DURATION_FILTER', true);
else if (c === '2') this.manage('KEYWORD_BLACKLIST', this.t('adv_keyword_filter'));
else if (c === '4') this.manage('CHANNEL_BLACKLIST', this.t('adv_channel_filter'));
else if (c === '6') {
const min = prompt(this.t('adv_min')); const max = prompt(this.t('adv_max'));
if (min) this.config.set('DURATION_MIN', min * 60);
if (max) this.config.set('DURATION_MAX', max * 60);
this.onRefresh(); this.showAdvancedMenu();
} else if (c === '0') this.showMainMenu();
}
manage(k, n) {
const l = this.config.get(k);
const c = prompt(`[${l.join(', ')}]\n1.${this.t('adv_add')} 2.${this.t('adv_remove')} 3.${this.t('adv_clear')} 0.${this.t('back')}`);
if (c === '1') { const v = prompt(`${this.t('adv_add')}:`); if (v) this.config.set(k, [...l, ...v.split(',')]); }
if (c === '2') { const v = prompt(`${this.t('adv_remove')}:`); if (v) this.config.set(k, l.filter(i => i !== v)); }
if (c === '3') this.config.set(k, []);
this.onRefresh(); this.showAdvancedMenu();
}
toggle(k, adv) { this.config.set(k, !this.config.get(k)); this.onRefresh(); adv ? this.showAdvancedMenu() : this.showMainMenu(); }
update(k, v) { if (k) this.config.set(k, v); this.onRefresh(); this.showMainMenu(); }
}
// --- 10. App Entry ---
class App {
constructor() {
this.config = new ConfigManager();
this.styleManager = new StyleManager(this.config);
this.adGuard = new AdBlockGuard();
this.filter = new VideoFilter(this.config);
this.enhancer = new InteractionEnhancer(this.config);
this.ui = new UIManager(this.config, () => this.refresh());
}
// **ANTI-ADBLOCK PATCH**: 透過 YouTube 自身的配置對象來阻止偵測
patchYouTubeConfig() {
try {
const config = window.yt?.config_ || window.ytcfg?.data_;
if (config?.openPopupConfig?.supportedPopups?.adBlockMessageViewModel) {
config.openPopupConfig.supportedPopups.adBlockMessageViewModel = false;
}
if (config?.EXPERIMENT_FLAGS) {
config.EXPERIMENT_FLAGS.ad_blocker_notifications_disabled = true;
config.EXPERIMENT_FLAGS.web_enable_adblock_detection_block_playback = false;
}
} catch (e) {
// 忽略錯誤
}
}
init() {
Logger.enabled = this.config.get('DEBUG_MODE');
// 先嘗試 patch YouTube 配置
this.patchYouTubeConfig();
this.styleManager.apply();
this.adGuard.start();
this.enhancer.init();
GM_registerMenuCommand('⚙️ 淨化大師設定', () => this.ui.showMainMenu());
const obs = new MutationObserver(Utils.debounce(() => this.filter.processPage(), 100));
obs.observe(document.body, { childList: true, subtree: true });
window.addEventListener('yt-navigate-finish', () => {
this.patchYouTubeConfig(); // 每次導航後重新 patch
this.filter.processPage();
this.adGuard.checkAndClean();
});
this.filter.processPage();
Logger.info(`🚀 YouTube 淨化大師 v1.6.0 啟動`);
}
refresh() {
Logger.enabled = this.config.get('DEBUG_MODE');
this.filter.reset();
this.styleManager.apply();
this.filter.processPage();
}
}
// 防止腳本重複初始化
if (window.ytPurifierInitialized) return;
window.ytPurifierInitialized = true;
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => new App().init());
else new App().init();
})();