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

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