YouTube Channel Blocker with Menu and Import/Export

Hides YouTube videos from specified channels (by handle or channel ID), allows adding via button, and manages list via menu with import/export

当前为 2025-05-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Channel Blocker with Menu and Import/Export
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.7
  5. // @description Hides YouTube videos from specified channels (by handle or channel ID), allows adding via button, and manages list via menu with import/export
  6. // @author DoctorEye
  7. // @license MIT
  8. // @match https://www.youtube.com/*
  9. // @grant GM_addStyle
  10. // @grant GM_registerMenuCommand
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // Initial list of blocked channels (handles or channel IDs)
  17. const initialBlockedChannels = [
  18. '@AutoVortex-01',
  19. '@RevReviewtrend',
  20. '@TurboFusion-35'
  21. ].map(item => item.toLowerCase());
  22.  
  23. // Load blocked channels from localStorage or use initial list
  24. let blockedChannels = JSON.parse(localStorage.getItem('blockedChannels')) || initialBlockedChannels;
  25. if (!localStorage.getItem('blockedChannels')) {
  26. localStorage.setItem('blockedChannels', JSON.stringify(blockedChannels));
  27. }
  28.  
  29. // CSS for Block Channel button and UI
  30. GM_addStyle(`
  31. .block-channel-btn {
  32. background-color: #ff4444;
  33. color: white;
  34. border: none;
  35. padding: 5px 10px;
  36. margin-left: 10px;
  37. cursor: pointer;
  38. border-radius: 3px;
  39. font-size: 12px;
  40. }
  41. .block-channel-btn:hover {
  42. background-color: #cc0000;
  43. }
  44. #blocker-ui {
  45. position: fixed;
  46. top: 20%;
  47. left: 50%;
  48. transform: translateX(-50%);
  49. background: white;
  50. border: 1px solid #ccc;
  51. padding: 20px;
  52. z-index: 1000;
  53. box-shadow: 0 0 10px rgba(0,0,0,0.5);
  54. display: none;
  55. }
  56. #blocker-ui textarea {
  57. width: 300px;
  58. height: 150px;
  59. }
  60. #blocker-ui button {
  61. margin: 10px 5px;
  62. }
  63. `);
  64.  
  65. // Save blocked channels to localStorage
  66. function saveBlockedChannels() {
  67. localStorage.setItem('blockedChannels', JSON.stringify(blockedChannels));
  68. }
  69.  
  70. // Function to hide videos and add Block button
  71. function hideVideos() {
  72. const videoElements = document.querySelectorAll(
  73. 'ytd-video-renderer, ytd-grid-video-renderer, ytd-rich-item-renderer, ytd-compact-video-renderer, ytd-reel-item-renderer, ytd-playlist-renderer'
  74. );
  75.  
  76. videoElements.forEach(video => {
  77. // Check for channel links
  78. const channelElements = video.querySelectorAll('a[href*="/@"], a[href*="/channel/"], a[href*="/user/"], ytd-channel-name a');
  79. let foundIdentifier = null;
  80.  
  81. for (const element of channelElements) {
  82. const href = element.href || '';
  83. // Extract handle from @ links
  84. const handleMatch = href.match(/\/@[^\/]+/)?.[0]?.toLowerCase();
  85. // Extract channel ID from /channel/ or /user/ links
  86. const channelIdMatch = href.match(/\/(channel|user)\/([^\/?]+)/)?.[2]?.toLowerCase();
  87.  
  88. if (handleMatch && blockedChannels.includes(handleMatch)) {
  89. foundIdentifier = handleMatch;
  90. video.style.display = 'none';
  91. console.log(`Blocked video from handle: ${handleMatch}`);
  92. break;
  93. } else if (channelIdMatch && blockedChannels.includes(channelIdMatch)) {
  94. foundIdentifier = channelIdMatch;
  95. video.style.display = 'none';
  96. console.log(`Blocked video from channel ID: ${channelIdMatch}`);
  97. break;
  98. } else {
  99. // Log for debugging
  100. if (handleMatch) {
  101. console.log(`Detected handle (not blocked): ${handleMatch}`);
  102. } else if (channelIdMatch) {
  103. console.log(`Detected channel ID (not blocked): ${channelIdMatch}`);
  104. }
  105. }
  106. }
  107.  
  108. // Add Block Channel button if not already present
  109. if (!video.querySelector('.block-channel-btn')) {
  110. const channelLink = video.querySelector('a[href*="/@"], a[href*="/channel/"], a[href*="/user/"]');
  111. if (channelLink) {
  112. const handle = channelLink.href.match(/\/@[^\/]+/)?.[0];
  113. const channelId = channelLink.href.match(/\/(channel|user)\/([^\/?]+)/)?.[2];
  114. const identifier = handle || channelId;
  115.  
  116. if (identifier) {
  117. const button = document.createElement('button');
  118. button.className = 'block-channel-btn';
  119. button.textContent = 'Block Channel';
  120. button.onclick = () => {
  121. if (!blockedChannels.includes(identifier.toLowerCase())) {
  122. blockedChannels.push(identifier.toLowerCase());
  123. saveBlockedChannels();
  124. hideVideos();
  125. alert(`Blocked: ${identifier}`);
  126. } else {
  127. alert(`${identifier} is already blocked.`);
  128. }
  129. };
  130. const metaContainer = video.querySelector('#meta') || video;
  131. metaContainer.appendChild(button);
  132. } else {
  133. console.log(`No identifier found for channel link: ${channelLink.href}`);
  134. }
  135. }
  136. }
  137. });
  138. }
  139.  
  140. // Create UI for managing blocked channels
  141. function createManageUI() {
  142. let ui = document.getElementById('blocker-ui');
  143. if (!ui) {
  144. ui = document.createElement('div');
  145. ui.id = 'blocker-ui';
  146. ui.innerHTML = `
  147. <h3>Manage Blocked Channels</h3>
  148. <textarea id="blocked-channels-list">${blockedChannels.join('\n')}</textarea>
  149. <br>
  150. <input type="file" id="import-channels" accept=".json" style="margin: 10px 0;">
  151. <br>
  152. <button id="save-channels">Save</button>
  153. <button id="export-channels">Export</button>
  154. <button id="clear-channels">Clear List</button>
  155. <button id="close-ui">Close</button>
  156. `;
  157. document.body.appendChild(ui);
  158.  
  159. // Event listeners for buttons
  160. document.getElementById('save-channels').onclick = () => {
  161. const newList = document.getElementById('blocked-channels-list').value
  162. .split('\n')
  163. .map(line => line.trim().toLowerCase())
  164. .filter(line => line.startsWith('@') || line.match(/^[a-z0-9_-]+$/i));
  165. blockedChannels = [...new Set(newList)];
  166. saveBlockedChannels();
  167. hideVideos();
  168. alert('List updated!');
  169. ui.style.display = 'none';
  170. };
  171.  
  172. document.getElementById('export-channels').onclick = () => {
  173. const data = JSON.stringify(blockedChannels, null, 2);
  174. const blob = new Blob([data], { type: 'application/json' });
  175. const url = URL.createObjectURL(blob);
  176. const a = document.createElement('a');
  177. a.href = url;
  178. a.download = 'blocked_channels.json';
  179. a.click();
  180. URL.revokeObjectURL(url);
  181. };
  182.  
  183. document.getElementById('import-channels').onchange = (event) => {
  184. const file = event.target.files[0];
  185. if (!file) {
  186. alert('No file selected.');
  187. return;
  188. }
  189.  
  190. const reader = new FileReader();
  191. reader.onload = (e) => {
  192. try {
  193. const importedList = JSON.parse(e.target.result);
  194. if (Array.isArray(importedList) && importedList.every(item => typeof item === 'string' && (item.startsWith('@') || item.match(/^[a-z0-9_-]+$/i)))) {
  195. blockedChannels = [...new Set(importedList.map(item => item.toLowerCase()))];
  196. saveBlockedChannels();
  197. document.getElementById('blocked-channels-list').value = blockedChannels.join('\n');
  198. hideVideos();
  199. alert('Channels imported successfully!');
  200. } else {
  201. alert('Invalid file format. Must be a JSON array of channel handles (starting with @) or channel IDs.');
  202. }
  203. } catch (error) {
  204. alert('Error importing file: ' + error.message);
  205. }
  206. };
  207. reader.onerror = () => {
  208. alert('Error reading file.');
  209. };
  210. reader.readAsText(file);
  211. };
  212.  
  213. document.getElementById('clear-channels').onclick = () => {
  214. if (confirm('Are you sure you want to clear the blocked channels list?')) {
  215. blockedChannels = [];
  216. saveBlockedChannels();
  217. document.getElementById('blocked-channels-list').value = '';
  218. hideVideos();
  219. alert('List cleared!');
  220. }
  221. };
  222.  
  223. document.getElementById('close-ui').onclick = () => {
  224. ui.style.display = 'none';
  225. };
  226. }
  227. ui.style.display = 'block';
  228. }
  229.  
  230. // Register menu command for managing blocked channels
  231. GM_registerMenuCommand('Manage Blocked Channels', createManageUI, 'm');
  232.  
  233. // Initial execution
  234. hideVideos();
  235.  
  236. // Observe DOM changes
  237. const observer = new MutationObserver(hideVideos);
  238. observer.observe(document.body, { childList: true, subtree: true });
  239.  
  240. // Re-run every 0.5 seconds for late-loaded content
  241. setInterval(hideVideos, 500);
  242. })();