Blocks YouTube Shorts except from subscribed channels and filters feed to show only subscribed content
Size
10.6 KB
Version
1.0.1
Created
Nov 23, 2025
Updated
23 days ago
1// ==UserScript==
2// @name YouTube Shorts & Feed Filter for Subscriptions
3// @description Blocks YouTube Shorts except from subscribed channels and filters feed to show only subscribed content
4// @version 1.0.1
5// @match https://*.youtube.com/*
6// @icon https://www.youtube.com/s/desktop/2731d6a3/img/favicon_32x32.png
7// @grant GM.xmlhttpRequest
8// @grant GM.getValue
9// @grant GM.setValue
10// @connect www.youtube.com
11// ==/UserScript==
12(function() {
13 'use strict';
14
15 console.log('YouTube Shorts & Feed Filter: Extension started');
16
17 // Cache for subscribed channels
18 let subscribedChannels = new Set();
19 let isLoadingSubscriptions = false;
20 let lastSubscriptionUpdate = 0;
21 const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes
22
23 // Debounce function to prevent excessive calls
24 function debounce(func, wait) {
25 let timeout;
26 return function executedFunction(...args) {
27 const later = () => {
28 clearTimeout(timeout);
29 func(...args);
30 };
31 clearTimeout(timeout);
32 timeout = setTimeout(later, wait);
33 };
34 }
35
36 // Get subscribed channels from YouTube's internal API
37 async function fetchSubscribedChannels() {
38 if (isLoadingSubscriptions) {
39 console.log('Already loading subscriptions, skipping...');
40 return;
41 }
42
43 // Check cache first
44 const now = Date.now();
45 if (now - lastSubscriptionUpdate < CACHE_DURATION && subscribedChannels.size > 0) {
46 console.log('Using cached subscriptions:', subscribedChannels.size, 'channels');
47 return;
48 }
49
50 isLoadingSubscriptions = true;
51 console.log('Fetching subscribed channels...');
52
53 try {
54 // Try to load from storage first
55 const cachedData = await GM.getValue('subscribedChannels', null);
56 const cachedTime = await GM.getValue('lastSubscriptionUpdate', 0);
57
58 if (cachedData && (now - cachedTime < CACHE_DURATION)) {
59 subscribedChannels = new Set(JSON.parse(cachedData));
60 lastSubscriptionUpdate = cachedTime;
61 console.log('Loaded', subscribedChannels.size, 'channels from storage');
62 isLoadingSubscriptions = false;
63 return;
64 }
65
66 // Fetch from YouTube API
67 const response = await GM.xmlhttpRequest({
68 method: 'GET',
69 url: 'https://www.youtube.com/feed/subscriptions',
70 headers: {
71 'Accept': 'text/html'
72 }
73 });
74
75 if (response.status === 200) {
76 // Parse the page to extract channel IDs
77 const parser = new DOMParser();
78 const doc = parser.parseFromString(response.responseText, 'text/html');
79
80 // Extract ytInitialData from the page
81 const scripts = doc.querySelectorAll('script');
82 let ytInitialData = null;
83
84 for (const script of scripts) {
85 const content = script.textContent;
86 if (content.includes('var ytInitialData = ')) {
87 const match = content.match(/var ytInitialData = ({.+?});/);
88 if (match) {
89 ytInitialData = JSON.parse(match[1]);
90 break;
91 }
92 }
93 }
94
95 if (ytInitialData) {
96 extractChannelIds(ytInitialData);
97 }
98
99 // Also try to get channels from current page
100 extractChannelIdsFromCurrentPage();
101
102 // Save to storage
103 await GM.setValue('subscribedChannels', JSON.stringify([...subscribedChannels]));
104 await GM.setValue('lastSubscriptionUpdate', now);
105 lastSubscriptionUpdate = now;
106
107 console.log('Successfully fetched', subscribedChannels.size, 'subscribed channels');
108 }
109 } catch (error) {
110 console.error('Error fetching subscriptions:', error);
111 // Try to extract from current page as fallback
112 extractChannelIdsFromCurrentPage();
113 } finally {
114 isLoadingSubscriptions = false;
115 }
116 }
117
118 // Extract channel IDs from ytInitialData
119 function extractChannelIds(data) {
120 const channelIds = new Set();
121
122 function traverse(obj) {
123 if (!obj || typeof obj !== 'object') return;
124
125 if (obj.channelId) {
126 channelIds.add(obj.channelId);
127 }
128
129 if (obj.browseId && obj.browseId.startsWith('UC')) {
130 channelIds.add(obj.browseId);
131 }
132
133 for (const key in obj) {
134 if (obj.hasOwnProperty(key)) {
135 traverse(obj[key]);
136 }
137 }
138 }
139
140 traverse(data);
141
142 channelIds.forEach(id => subscribedChannels.add(id));
143 console.log('Extracted', channelIds.size, 'channel IDs from data');
144 }
145
146 // Extract channel IDs from current page DOM
147 function extractChannelIdsFromCurrentPage() {
148 // Look for channel links in the page
149 const channelLinks = document.querySelectorAll('a[href*="/channel/"], a[href*="/@"]');
150 let count = 0;
151
152 channelLinks.forEach(link => {
153 const href = link.getAttribute('href');
154 if (href) {
155 // Extract channel ID from /channel/UC... format
156 const channelMatch = href.match(/\/channel\/(UC[\w-]+)/);
157 if (channelMatch) {
158 subscribedChannels.add(channelMatch[1]);
159 count++;
160 }
161
162 // Extract from handle format /@username
163 const handleMatch = href.match(/\/@([\w-]+)/);
164 if (handleMatch) {
165 // Store handle as well (we'll check both)
166 subscribedChannels.add('@' + handleMatch[1]);
167 count++;
168 }
169 }
170 });
171
172 console.log('Extracted', count, 'channel IDs from current page');
173 }
174
175 // Check if a video element is from a subscribed channel
176 function isFromSubscribedChannel(element) {
177 // Look for channel link in the element
178 const channelLink = element.querySelector('a[href*="/channel/"], a[href*="/@"]');
179
180 if (channelLink) {
181 const href = channelLink.getAttribute('href');
182
183 // Check channel ID
184 const channelMatch = href.match(/\/channel\/(UC[\w-]+)/);
185 if (channelMatch && subscribedChannels.has(channelMatch[1])) {
186 return true;
187 }
188
189 // Check handle
190 const handleMatch = href.match(/\/@([\w-]+)/);
191 if (handleMatch && subscribedChannels.has('@' + handleMatch[1])) {
192 return true;
193 }
194 }
195
196 return false;
197 }
198
199 // Block Shorts that are not from subscribed channels
200 function blockNonSubscribedShorts() {
201 // Find all Shorts elements
202 const shortsSelectors = [
203 'ytd-reel-item-renderer',
204 'ytd-rich-item-renderer:has(a[href*="/shorts/"])',
205 'ytd-grid-video-renderer:has(a[href*="/shorts/"])',
206 'ytd-video-renderer:has(a[href*="/shorts/"])'
207 ];
208
209 shortsSelectors.forEach(selector => {
210 const shortsElements = document.querySelectorAll(selector);
211
212 shortsElements.forEach(element => {
213 // Skip if already processed
214 if (element.hasAttribute('data-shorts-processed')) {
215 return;
216 }
217
218 element.setAttribute('data-shorts-processed', 'true');
219
220 // Check if from subscribed channel
221 if (!isFromSubscribedChannel(element)) {
222 console.log('Blocking non-subscribed Short');
223 element.style.display = 'none';
224 element.remove();
225 }
226 });
227 });
228 }
229
230 // Filter feed to show only subscribed content
231 function filterFeedContent() {
232 // Only filter on home page and feed pages
233 const currentPath = window.location.pathname;
234 if (currentPath !== '/' && !currentPath.startsWith('/feed/')) {
235 return;
236 }
237
238 // Find all video elements in the feed
239 const videoSelectors = [
240 'ytd-rich-item-renderer',
241 'ytd-grid-video-renderer',
242 'ytd-video-renderer',
243 'ytd-compact-video-renderer'
244 ];
245
246 videoSelectors.forEach(selector => {
247 const videoElements = document.querySelectorAll(selector);
248
249 videoElements.forEach(element => {
250 // Skip if already processed
251 if (element.hasAttribute('data-feed-processed')) {
252 return;
253 }
254
255 element.setAttribute('data-feed-processed', 'true');
256
257 // Check if from subscribed channel
258 if (!isFromSubscribedChannel(element)) {
259 console.log('Filtering non-subscribed content from feed');
260 element.style.display = 'none';
261 element.remove();
262 }
263 });
264 });
265 }
266
267 // Main processing function
268 const processPage = debounce(() => {
269 blockNonSubscribedShorts();
270 filterFeedContent();
271 }, 500);
272
273 // Initialize the extension
274 async function init() {
275 console.log('Initializing YouTube Shorts & Feed Filter...');
276
277 // Fetch subscribed channels
278 await fetchSubscribedChannels();
279
280 // Initial processing
281 processPage();
282
283 // Watch for DOM changes
284 const observer = new MutationObserver(debounce((mutations) => {
285 processPage();
286 }, 500));
287
288 observer.observe(document.body, {
289 childList: true,
290 subtree: true
291 });
292
293 console.log('YouTube Shorts & Feed Filter: Initialized successfully');
294
295 // Refresh subscriptions periodically
296 setInterval(() => {
297 fetchSubscribedChannels();
298 }, CACHE_DURATION);
299 }
300
301 // Wait for page to be ready
302 if (document.readyState === 'loading') {
303 document.addEventListener('DOMContentLoaded', init);
304 } else {
305 init();
306 }
307
308})();