YouTube Shorts & Feed Filter for Subscriptions

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

3 months 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})();
YouTube Shorts & Feed Filter for Subscriptions | Robomonkey