您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds block/whitelist buttons and a tabbed management UI for YouTube channels, with video duration filtering and export/import functionality.
// ==UserScript== // @name YouTube Gatekeeper // @namespace http://tampermonkey.net/ // @version 0.1.1-optimized-en // @description Adds block/whitelist buttons and a tabbed management UI for YouTube channels, with video duration filtering and export/import functionality. // @author MayoHu // @match https://www.youtube.com/* // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @run-at document-end // @license MIT // ==/UserScript== (function() { 'use strict'; // Helper functions for GM_storage const getBlockedChannels = () => GM_getValue('blocked_channels', []); const getWhitelistedChannels = () => GM_getValue('whitelisted_channels', []); const getKeywords = () => GM_getValue('blocked_keywords', []); const getWhitelistMode = () => GM_getValue('whitelist_mode', false); const getMinDuration = () => GM_getValue('min_duration', 30); const getDurationFilterEnabled = () => GM_getValue('duration_filter_enabled', false); const setWhitelistMode = (mode) => GM_setValue('whitelist_mode', mode); const setMinDuration = (duration) => GM_setValue('min_duration', duration); const setDurationFilterEnabled = (enabled) => GM_setValue('duration_filter_enabled', enabled); const addBlockedChannel = (channelId, channelName) => { const blocked = getBlockedChannels(); if (!blocked.some(c => c.id === channelId)) { blocked.push({ id: channelId, name: channelName }); GM_setValue('blocked_channels', blocked); return true; } return false; }; const removeBlockedChannel = (channelId) => { const blocked = getBlockedChannels().filter(c => c.id !== channelId); GM_setValue('blocked_channels', blocked); }; const addWhitelistedChannel = (channelId, channelName) => { const whitelisted = getWhitelistedChannels(); if (!whitelisted.some(c => c.id === channelId)) { whitelisted.push({ id: channelId, name: channelName }); GM_setValue('whitelisted_channels', whitelisted); return true; } return false; }; const removeWhitelistedChannel = (channelId) => { const whitelisted = getWhitelistedChannels().filter(c => c.id !== channelId); GM_setValue('whitelisted_channels', whitelisted); }; const addBlockedKeyword = (keyword) => { const keywords = getKeywords(); if (!keywords.includes(keyword)) { keywords.push(keyword); GM_setValue('blocked_keywords', keywords); return true; } return false; }; const removeBlockedKeyword = (keyword) => { const keywords = getKeywords().filter(k => k !== keyword); GM_setValue('blocked_keywords', keywords); }; // UI creation and management function createManagementUI() { if (document.getElementById('block-channel-ui')) { return; } const ui = document.createElement('div'); ui.id = 'block-channel-ui'; ui.style.cssText = ` position: fixed; top: 56px; right: 20px; width: 700px; max-width: 90%; max-height: 90vh; background-color: white; color: black; border: 1px solid #ccc; box-shadow: 0 4px 10px rgba(0,0,0,0.2); z-index: 10000; display: none; flex-direction: column; border-radius: 8px; padding: 20px; font-family: 'Arial', sans-serif; font-size: 16px; `; ui.innerHTML = ` <style> #block-channel-ui h2 { font-size: 20px; margin-bottom: 10px; } #block-channel-ui p { font-size: 16px; } #block-channel-ui a { color: black; text-decoration: none; } #block-channel-ui button { background-color: #f2f2f2; border: 1px solid #ccc; padding: 10px 14px; cursor: pointer; border-radius: 4px; margin: 4px; color: black; font-size: 16px; } #block-channel-ui button:hover { background-color: #e6e6e6; } #block-channel-ui input[type="text"], #block-channel-ui input[type="number"] { border: 1px solid #ccc; padding: 8px; border-radius: 4px; font-size: 16px; } #block-channel-ui .tab-buttons { display: flex; border-bottom: 1px solid #ccc; margin-bottom: 15px; } #block-channel-ui .tab-button { background: none; border: none; padding: 10px 15px; cursor: pointer; font-size: 16px; color: #555; border-bottom: 2px solid transparent; } #block-channel-ui .tab-button.active { color: #000; border-bottom: 2px solid #337ab7; } #block-channel-ui .tab-content { display: none; overflow-y: auto; flex-grow: 1; padding-right: 8px; } #block-channel-ui .tab-content.active { display: block; } #block-channel-ui .list-item { display: flex; justify-content: space-between; align-items: center; padding: 10px 0; border-bottom: 1px solid #eee; } #block-channel-ui .list-item:last-child { border-bottom: none; } #block-channel-ui .list-item .remove-btn { background-color: #d9534f; color: white; padding: 6px 10px; margin: 0; } #block-channel-ui .list-item .remove-btn:hover { background-color: #c9302c; } #block-channel-ui .item-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-grow: 1; margin-right: 8px; font-size: 16px; } #block-channel-ui .mode-toggle { display: flex; align-items: center; gap: 10px; margin: 10px 0; font-size: 16px; } #block-channel-ui .slider { position: relative; display: inline-block; width: 40px; height: 20px; } #block-channel-ui .slider input { opacity: 0; width: 0; height: 0; } #block-channel-ui .slider-round { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 20px; } #block-channel-ui .slider-round:before { position: absolute; content: ""; height: 16px; width: 16px; left: 2px; bottom: 2px; background-color: white; transition: .4s; border-radius: 50%; } #block-channel-ui input:checked + .slider-round { background-color: #337ab7; } #block-channel-ui input:checked + .slider-round:before { transform: translateX(20px); } #block-channel-ui .export-import-container { display: flex; gap: 10px; margin-top: 20px; } #block-channel-ui .export-import-container button { flex: 1; } .channel-name-long { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; max-width: calc(100% - 100px); } #channel-lists-container { display: flex; gap: 20px; } #blocked-channels-section, #whitelisted-channels-section { flex: 1; min-width: 0; } #blocked-channels-list, #whitelisted-channels-list, #blocked-keywords-list { max-height: 250px; overflow-y: auto; padding-right: 8px; } .keyword-tag { display: inline-flex; align-items: center; background-color: #f2f2f2; border: 1px solid #ccc; border-radius: 4px; padding: 6px 10px; font-size: 16px; color: black; margin-bottom: 10px; margin-right: 10px; } .keyword-tag-remove { margin-left: 8px; cursor: pointer; font-weight: bold; color: #555; background: none; border: none; font-size: 18px; line-height: 1; } </style> <h2>YouTube Gatekeeper</h2> <div class="tab-buttons"> <button class="tab-button active" data-tab="channel-management">Channel Management</button> <button class="tab-button" data-tab="blocked-keywords">Keyword Filter</button> </div> <div id="channel-management" class="tab-content active"> <div class="mode-toggle"> <span>Whitelist Mode (Show only whitelisted channels)</span> <label class="slider"> <input type="checkbox" id="whitelist-mode-toggle"> <span class="slider-round"></span> </label> </div> <div class="mode-toggle"> <span>Enable Video Duration Filter</span> <label class="slider"> <input type="checkbox" id="duration-filter-toggle"> <span class="slider-round"></span> </label> </div> <div style="margin: 10px 0;"> <label for="min-duration-input">Minimum Duration (seconds):</label> <input type="number" id="min-duration-input" value="30" min="0" style="width: 80px;"> </div> <div id="channel-lists-container"> <div id="blocked-channels-section"> <p id="blocked-channels-title">Blocked Channels: (Total <span id="blocked-count">0</span>)</p> <div id="blocked-channels-list"></div> </div> <div id="whitelisted-channels-section"> <p id="whitelisted-channels-title">Whitelisted Channels: (Total <span id="whitelisted-count">0</span>)</p> <div id="whitelisted-channels-list"></div> </div> </div> <div class="export-import-container"> <button id="export-channels-btn">Export Channel List</button> <button id="import-channels-btn">Import Channel List</button> </div> <input type="file" id="import-channels-file-input" style="display: none;"> </div> <div id="blocked-keywords" class="tab-content"> <div style="display: flex; gap: 10px; margin-bottom: 15px;"> <input type="text" id="keyword-input" placeholder="Enter keyword to block..." style="flex-grow: 1;"> <button id="add-keyword-btn">Add</button> </div> <div id="blocked-keywords-list-container"> <div id="blocked-keywords-list"></div> </div> <div class="export-import-container"> <button id="export-keywords-btn">Export Keyword List</button> <button id="import-keywords-btn">Import Keyword List</button> </div> <input type="file" id="import-keywords-file-input" style="display: none;"> </div> `; document.body.appendChild(ui); setupUIEventHandlers(); refreshUI(); } function setupUIEventHandlers() { const ui = document.getElementById('block-channel-ui'); if (!ui) return; ui.querySelectorAll('.tab-button').forEach(button => { button.addEventListener('click', () => { const tab = button.dataset.tab; ui.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active')); ui.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active')); button.classList.add('active'); document.getElementById(tab).classList.add('active'); refreshUI(); }); }); const whitelistToggle = document.getElementById('whitelist-mode-toggle'); if (whitelistToggle) { whitelistToggle.addEventListener('change', (e) => { setWhitelistMode(e.target.checked); hideBlockedContent(); }); } const durationToggle = document.getElementById('duration-filter-toggle'); if (durationToggle) { durationToggle.addEventListener('change', (e) => { setDurationFilterEnabled(e.target.checked); hideBlockedContent(); }); } const minDurationInput = document.getElementById('min-duration-input'); if (minDurationInput) { minDurationInput.addEventListener('change', (e) => { setMinDuration(parseInt(e.target.value, 10)); hideBlockedContent(); }); } const addKeywordBtn = document.getElementById('add-keyword-btn'); if (addKeywordBtn) { addKeywordBtn.addEventListener('click', () => { const keywordInput = document.getElementById('keyword-input'); const keyword = keywordInput.value.trim(); if (keyword && addBlockedKeyword(keyword)) { keywordInput.value = ''; refreshUI(); hideBlockedContent(); } }); } const exportChannelsBtn = document.getElementById('export-channels-btn'); if (exportChannelsBtn) { exportChannelsBtn.addEventListener('click', exportChannelsData); } const importChannelsBtn = document.getElementById('import-channels-btn'); if (importChannelsBtn) { importChannelsBtn.addEventListener('click', () => { document.getElementById('import-channels-file-input').click(); }); } const importChannelsFile = document.getElementById('import-channels-file-input'); if (importChannelsFile) { importChannelsFile.addEventListener('change', importChannelsData); } const exportKeywordsBtn = document.getElementById('export-keywords-btn'); if (exportKeywordsBtn) { exportKeywordsBtn.addEventListener('click', exportKeywordsData); } const importKeywordsBtn = document.getElementById('import-keywords-btn'); if (importKeywordsBtn) { importKeywordsBtn.addEventListener('click', () => { document.getElementById('import-keywords-file-input').click(); }); } const importKeywordsFile = document.getElementById('import-keywords-file-input'); if (importKeywordsFile) { importKeywordsFile.addEventListener('change', importKeywordsData); } } function refreshUI() { const blockedChannels = getBlockedChannels(); const whitelistedChannels = getWhitelistedChannels(); const blockedKeywords = getKeywords(); const blockedCountEl = document.getElementById('blocked-count'); const whitelistedCountEl = document.getElementById('whitelisted-count'); const blockedListEl = document.getElementById('blocked-channels-list'); const whitelistedListEl = document.getElementById('whitelisted-channels-list'); const keywordListEl = document.getElementById('blocked-keywords-list'); if (blockedCountEl) blockedCountEl.textContent = blockedChannels.length; if (whitelistedCountEl) whitelistedCountEl.textContent = whitelistedChannels.length; if (blockedListEl) { blockedListEl.innerHTML = ''; blockedChannels.forEach(c => { const div = document.createElement('div'); div.className = 'list-item'; div.innerHTML = `<span class="item-text"><a href="/channel/${c.id}" target="_blank">${c.name}</a></span> <button class="remove-btn">Remove</button>`; div.querySelector('.remove-btn').addEventListener('click', () => { removeBlockedChannel(c.id); refreshUI(); hideBlockedContent(); }); blockedListEl.appendChild(div); }); } if (whitelistedListEl) { whitelistedListEl.innerHTML = ''; whitelistedChannels.forEach(c => { const div = document.createElement('div'); div.className = 'list-item'; div.innerHTML = `<span class="item-text"><a href="/channel/${c.id}" target="_blank">${c.name}</a></span> <button class="remove-btn">Remove</button>`; div.querySelector('.remove-btn').addEventListener('click', () => { removeWhitelistedChannel(c.id); refreshUI(); hideBlockedContent(); }); whitelistedListEl.appendChild(div); }); } if (keywordListEl) { keywordListEl.innerHTML = ''; blockedKeywords.forEach(k => { const span = document.createElement('span'); span.className = 'keyword-tag'; span.textContent = k; const removeBtn = document.createElement('button'); removeBtn.textContent = '×'; removeBtn.className = 'keyword-tag-remove'; removeBtn.addEventListener('click', () => { removeBlockedKeyword(k); refreshUI(); hideBlockedContent(); }); span.appendChild(removeBtn); keywordListEl.appendChild(span); }); } const whitelistToggle = document.getElementById('whitelist-mode-toggle'); if (whitelistToggle) whitelistToggle.checked = getWhitelistMode(); const durationToggle = document.getElementById('duration-filter-toggle'); if (durationToggle) durationToggle.checked = getDurationFilterEnabled(); const minDurationInput = document.getElementById('min-duration-input'); if (minDurationInput) minDurationInput.value = getMinDuration(); } function addUIButtons() { const endActions = document.querySelector('ytd-masthead #end'); if (!endActions || document.getElementById('block-channel-manager-button')) { return; } const managerButton = document.createElement('button'); managerButton.id = 'block-channel-manager-button'; managerButton.textContent = 'Channel Management'; managerButton.style.cssText = ` background-color: #f2f2f2; border: 1px solid #ccc; color: black; padding: 8px 12px; border-radius: 4px; font-weight: 500; cursor: pointer; margin-right: 12px; `; managerButton.addEventListener('click', () => { const ui = document.getElementById('block-channel-ui'); if (ui) { if (ui.style.display === 'none') { ui.style.display = 'flex'; refreshUI(); } else { ui.style.display = 'none'; } } }); endActions.insertBefore(managerButton, endActions.firstChild); } function hideBlockedContent() { const blockedChannels = getBlockedChannels(); const whitelistedChannels = getWhitelistedChannels(); const blockedKeywords = getKeywords().map(k => k.toLowerCase()); const whitelistMode = getWhitelistMode(); const durationFilterEnabled = getDurationFilterEnabled(); const minDuration = getMinDuration(); const items = document.querySelectorAll('ytd-rich-item-renderer, ytd-compact-video-renderer, ytd-video-renderer, ytd-playlist-video-renderer, ytd-grid-video-renderer, yt-lockup-view-model, ytd-reel-item-renderer, ytd-rich-grid-media'); items.forEach(item => { let shouldHide = false; let channelId = null; let channelName = null; let title = null; let duration = null; const channelElement = item.querySelector('yt-lockup-metadata-view-model .yt-content-metadata-view-model__metadata-row span, #channel-name a, ytd-channel-name a, .ytd-channel-name a, yt-formatted-string[channel-name], #channel-title, .yt-lockup-metadata-view-model__metadata-row a'); if (channelElement) { channelName = channelElement.textContent.trim(); let channelUrl = channelElement.href; if (!channelUrl && channelElement.parentElement && channelElement.parentElement.tagName === 'A') { channelUrl = channelElement.parentElement.href; } if (channelUrl) { const match = channelUrl.match(/(@[a-zA-Z0-9_-]+|channel\/[a-zA-Z0-9_-]+)/); if (match) { channelId = match[0]; } } } const titleElement = item.querySelector('#video-title, #video-title-link, .yt-lockup-metadata-view-model__title, #video-title-text, a#video-title, span.title, yt-formatted-string.ytd-rich-grid-media'); if (titleElement) { title = titleElement.textContent.trim().toLowerCase(); } const durationElement = item.querySelector('ytd-thumbnail-overlay-time-status-renderer, span.ytd-thumbnail-overlay-time-status-renderer, .yt-badge-shape__text, yt-formatted-string.ytd-thumbnail-overlay-time-status-renderer'); if (durationElement) { duration = parseDuration(durationElement.textContent.trim()); } if (!channelId && !channelName && item.getAttribute('is-live-stream') === 'true') { return; } let isBlocked = false; let isWhitelisted = false; if (channelId) { isBlocked = blockedChannels.some(c => c.id === channelId); isWhitelisted = whitelistedChannels.some(c => c.id === channelId); } if (!isBlocked && !isWhitelisted && channelName) { isBlocked = blockedChannels.some(c => c.name === channelName); isWhitelisted = whitelistedChannels.some(c => c.name === channelName); } if (whitelistMode) { shouldHide = !isWhitelisted; } else { shouldHide = isBlocked || (title && blockedKeywords.some(keyword => title.includes(keyword))); } if (durationFilterEnabled && duration !== null && duration < minDuration) { shouldHide = true; } if (isWhitelisted) { shouldHide = false; } item.style.display = shouldHide ? 'none' : ''; }); } function addActionButtons(item) { if (item.querySelector('.block-channel-btn') || item.querySelector('.whitelist-channel-btn')) { return; } let channelLink = null; let channelName = null; let insertPoint = null; const selectors = [ '#byline-container', '#channel-name', '.yt-lockup-metadata-view-model__text-container', '.yt-content-metadata-view-model', '.yt-lockup-metadata-view-model__metadata', '#meta', '#info', '.ytd-video-meta-block', '.yt-lockup-metadata-view-model', 'yt-lockup-metadata-view-model' ]; for (const selector of selectors) { const container = item.querySelector(selector); if (container) { channelLink = container.querySelector('a[href*="/@"], a[href*="/channel/"], a[href*="/user/"], .yt-content-metadata-view-model__metadata-row a'); if (!channelLink) { const nameSpan = container.querySelector('.yt-content-metadata-view-model__metadata-row span, yt-formatted-string'); if (nameSpan) { channelName = nameSpan.textContent.trim(); channelLink = nameSpan; // Use as placeholder } } if (channelLink || channelName) { insertPoint = container; break; } } } if (!channelLink && !channelName) { return; } if (!channelName) { channelName = channelLink.textContent.trim(); } let channelId = null; let channelUrl = channelLink.href; if (!channelUrl && channelLink.parentElement && channelLink.parentElement.tagName === 'A') { channelUrl = channelLink.parentElement.href; } if (channelUrl) { const match = channelUrl.match(/(@[a-zA-Z0-9_-]+|channel\/[a-zA-Z0-9_-]+)/); if (match) { channelId = match[0]; } } if (!channelId) { channelId = channelName; // Fallback to name if no ID found } const buttonContainer = document.createElement('div'); buttonContainer.style.cssText = ` display: inline-flex; flex-wrap: nowrap; gap: 4px; font-size: 12px; margin-left: 8px; `; const blockBtn = document.createElement('button'); blockBtn.textContent = 'Block'; blockBtn.className = 'block-channel-btn'; blockBtn.style.cssText = ` background-color: #f2f2f2; border: 1px solid #ccc; color: black; padding: 2px 6px; border-radius: 4px; cursor: pointer; `; const whitelistBtn = document.createElement('button'); whitelistBtn.textContent = 'Whitelist'; whitelistBtn.className = 'whitelist-channel-btn'; whitelistBtn.style.cssText = ` background-color: #f2f2f2; border: 1px solid #ccc; color: black; padding: 2px 6px; border-radius: 4px; cursor: pointer; `; blockBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (addBlockedChannel(channelId, channelName)) { buttonContainer.style.display = 'none'; hideBlockedContent(); } }); whitelistBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); if (addWhitelistedChannel(channelId, channelName)) { buttonContainer.style.display = 'none'; hideBlockedContent(); } }); buttonContainer.appendChild(blockBtn); buttonContainer.appendChild(whitelistBtn); insertPoint.appendChild(buttonContainer); } function parseDuration(durationStr) { if (!durationStr) return null; let parts = durationStr.split(':').map(Number); let duration = 0; if (parts.length === 3) { duration = parts[0] * 3600 + parts[1] * 60 + parts[2]; } else if (parts.length === 2) { duration = parts[0] * 60 + parts[1]; } else if (parts.length === 1) { duration = parts[0]; } return duration; } function exportData(data, filename) { const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); const now = new Date(); const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hour = String(now.getHours()).padStart(2, '0'); const minute = String(now.getMinutes()).padStart(2, '0'); a.href = url; a.download = `${filename}_${year}_${month}_${day}_${hour}_${minute}.json`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); alert('Data exported successfully!'); } function importData(event, key) { const file = event.target.files[0]; if (!file) { alert('Import failed: No file selected.'); return; } const reader = new FileReader(); reader.onload = (e) => { try { const data = JSON.parse(e.target.result); if (key === 'channels') { if (!data.blocked_channels || !data.whitelisted_channels) { throw new Error('Invalid channels file format.'); } GM_setValue('blocked_channels', data.blocked_channels); GM_setValue('whitelisted_channels', data.whitelisted_channels); } else if (key === 'keywords') { if (!Array.isArray(data)) { throw new Error('Invalid keywords file format.'); } GM_setValue('blocked_keywords', data); } alert('Data imported successfully!'); refreshUI(); hideBlockedContent(); } catch (error) { alert('Import failed, please check the file format.'); console.error('Import failed:', error); } }; reader.readAsText(file); } function exportChannelsData() { const data = { blocked_channels: getBlockedChannels(), whitelisted_channels: getWhitelistedChannels() }; exportData(data, 'youtube_channels'); } function importChannelsData(event) { importData(event, 'channels'); } function exportKeywordsData() { const data = getKeywords(); exportData(data, 'youtube_keywords'); } function importKeywordsData(event) { importData(event, 'keywords'); } // Main execution function function initializeScript() { const observer = new MutationObserver(() => { const items = document.querySelectorAll('ytd-rich-item-renderer, ytd-compact-video-renderer, ytd-video-renderer, ytd-playlist-video-renderer, ytd-grid-video-renderer, yt-lockup-view-model, ytd-reel-item-renderer, ytd-rich-grid-media'); items.forEach(item => { addActionButtons(item); }); hideBlockedContent(); }); // Use a more specific observer to improve performance const mainContent = document.querySelector('ytd-page-manager'); if (mainContent) { observer.observe(mainContent, { childList: true, subtree: true }); } else { observer.observe(document.body, { childList: true, subtree: true }); } // Initial setup createManagementUI(); addUIButtons(); } // Run the script initializeScript(); })();