// ==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();
})();