YouTube Shorts & Feed Filter for Subscriptions

Blocks YouTube Shorts except from subscribed channels and filters feed to show only subscribed content

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name		YouTube Shorts & Feed Filter for Subscriptions
// @description		Blocks YouTube Shorts except from subscribed channels and filters feed to show only subscribed content
// @version		1.0.1
// @match		https://*.youtube.com/*
// @icon		https://www.youtube.com/s/desktop/2731d6a3/img/favicon_32x32.png
// @grant		GM.xmlhttpRequest
// @grant		GM.getValue
// @grant		GM.setValue
// @connect		www.youtube.com
// @namespace https://greasyfork.org/users/1536954
// ==/UserScript==
(function() {
    'use strict';

    console.log('YouTube Shorts & Feed Filter: Extension started');

    // Cache for subscribed channels
    let subscribedChannels = new Set();
    let isLoadingSubscriptions = false;
    let lastSubscriptionUpdate = 0;
    const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes

    // Debounce function to prevent excessive calls
    function debounce(func, wait) {
        let timeout;
        return function executedFunction(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }

    // Get subscribed channels from YouTube's internal API
    async function fetchSubscribedChannels() {
        if (isLoadingSubscriptions) {
            console.log('Already loading subscriptions, skipping...');
            return;
        }

        // Check cache first
        const now = Date.now();
        if (now - lastSubscriptionUpdate < CACHE_DURATION && subscribedChannels.size > 0) {
            console.log('Using cached subscriptions:', subscribedChannels.size, 'channels');
            return;
        }

        isLoadingSubscriptions = true;
        console.log('Fetching subscribed channels...');

        try {
            // Try to load from storage first
            const cachedData = await GM.getValue('subscribedChannels', null);
            const cachedTime = await GM.getValue('lastSubscriptionUpdate', 0);
            
            if (cachedData && (now - cachedTime < CACHE_DURATION)) {
                subscribedChannels = new Set(JSON.parse(cachedData));
                lastSubscriptionUpdate = cachedTime;
                console.log('Loaded', subscribedChannels.size, 'channels from storage');
                isLoadingSubscriptions = false;
                return;
            }

            // Fetch from YouTube API
            const response = await GM.xmlhttpRequest({
                method: 'GET',
                url: 'https://www.youtube.com/feed/subscriptions',
                headers: {
                    'Accept': 'text/html'
                }
            });

            if (response.status === 200) {
                // Parse the page to extract channel IDs
                const parser = new DOMParser();
                const doc = parser.parseFromString(response.responseText, 'text/html');
                
                // Extract ytInitialData from the page
                const scripts = doc.querySelectorAll('script');
                let ytInitialData = null;
                
                for (const script of scripts) {
                    const content = script.textContent;
                    if (content.includes('var ytInitialData = ')) {
                        const match = content.match(/var ytInitialData = ({.+?});/);
                        if (match) {
                            ytInitialData = JSON.parse(match[1]);
                            break;
                        }
                    }
                }

                if (ytInitialData) {
                    extractChannelIds(ytInitialData);
                }

                // Also try to get channels from current page
                extractChannelIdsFromCurrentPage();

                // Save to storage
                await GM.setValue('subscribedChannels', JSON.stringify([...subscribedChannels]));
                await GM.setValue('lastSubscriptionUpdate', now);
                lastSubscriptionUpdate = now;

                console.log('Successfully fetched', subscribedChannels.size, 'subscribed channels');
            }
        } catch (error) {
            console.error('Error fetching subscriptions:', error);
            // Try to extract from current page as fallback
            extractChannelIdsFromCurrentPage();
        } finally {
            isLoadingSubscriptions = false;
        }
    }

    // Extract channel IDs from ytInitialData
    function extractChannelIds(data) {
        const channelIds = new Set();
        
        function traverse(obj) {
            if (!obj || typeof obj !== 'object') return;
            
            if (obj.channelId) {
                channelIds.add(obj.channelId);
            }
            
            if (obj.browseId && obj.browseId.startsWith('UC')) {
                channelIds.add(obj.browseId);
            }

            for (const key in obj) {
                if (obj.hasOwnProperty(key)) {
                    traverse(obj[key]);
                }
            }
        }
        
        traverse(data);
        
        channelIds.forEach(id => subscribedChannels.add(id));
        console.log('Extracted', channelIds.size, 'channel IDs from data');
    }

    // Extract channel IDs from current page DOM
    function extractChannelIdsFromCurrentPage() {
        // Look for channel links in the page
        const channelLinks = document.querySelectorAll('a[href*="/channel/"], a[href*="/@"]');
        let count = 0;
        
        channelLinks.forEach(link => {
            const href = link.getAttribute('href');
            if (href) {
                // Extract channel ID from /channel/UC... format
                const channelMatch = href.match(/\/channel\/(UC[\w-]+)/);
                if (channelMatch) {
                    subscribedChannels.add(channelMatch[1]);
                    count++;
                }
                
                // Extract from handle format /@username
                const handleMatch = href.match(/\/@([\w-]+)/);
                if (handleMatch) {
                    // Store handle as well (we'll check both)
                    subscribedChannels.add('@' + handleMatch[1]);
                    count++;
                }
            }
        });
        
        console.log('Extracted', count, 'channel IDs from current page');
    }

    // Check if a video element is from a subscribed channel
    function isFromSubscribedChannel(element) {
        // Look for channel link in the element
        const channelLink = element.querySelector('a[href*="/channel/"], a[href*="/@"]');
        
        if (channelLink) {
            const href = channelLink.getAttribute('href');
            
            // Check channel ID
            const channelMatch = href.match(/\/channel\/(UC[\w-]+)/);
            if (channelMatch && subscribedChannels.has(channelMatch[1])) {
                return true;
            }
            
            // Check handle
            const handleMatch = href.match(/\/@([\w-]+)/);
            if (handleMatch && subscribedChannels.has('@' + handleMatch[1])) {
                return true;
            }
        }
        
        return false;
    }

    // Block Shorts that are not from subscribed channels
    function blockNonSubscribedShorts() {
        // Find all Shorts elements
        const shortsSelectors = [
            'ytd-reel-item-renderer',
            'ytd-rich-item-renderer:has(a[href*="/shorts/"])',
            'ytd-grid-video-renderer:has(a[href*="/shorts/"])',
            'ytd-video-renderer:has(a[href*="/shorts/"])'
        ];

        shortsSelectors.forEach(selector => {
            const shortsElements = document.querySelectorAll(selector);
            
            shortsElements.forEach(element => {
                // Skip if already processed
                if (element.hasAttribute('data-shorts-processed')) {
                    return;
                }
                
                element.setAttribute('data-shorts-processed', 'true');
                
                // Check if from subscribed channel
                if (!isFromSubscribedChannel(element)) {
                    console.log('Blocking non-subscribed Short');
                    element.style.display = 'none';
                    element.remove();
                }
            });
        });
    }

    // Filter feed to show only subscribed content
    function filterFeedContent() {
        // Only filter on home page and feed pages
        const currentPath = window.location.pathname;
        if (currentPath !== '/' && !currentPath.startsWith('/feed/')) {
            return;
        }

        // Find all video elements in the feed
        const videoSelectors = [
            'ytd-rich-item-renderer',
            'ytd-grid-video-renderer',
            'ytd-video-renderer',
            'ytd-compact-video-renderer'
        ];

        videoSelectors.forEach(selector => {
            const videoElements = document.querySelectorAll(selector);
            
            videoElements.forEach(element => {
                // Skip if already processed
                if (element.hasAttribute('data-feed-processed')) {
                    return;
                }
                
                element.setAttribute('data-feed-processed', 'true');
                
                // Check if from subscribed channel
                if (!isFromSubscribedChannel(element)) {
                    console.log('Filtering non-subscribed content from feed');
                    element.style.display = 'none';
                    element.remove();
                }
            });
        });
    }

    // Main processing function
    const processPage = debounce(() => {
        blockNonSubscribedShorts();
        filterFeedContent();
    }, 500);

    // Initialize the extension
    async function init() {
        console.log('Initializing YouTube Shorts & Feed Filter...');
        
        // Fetch subscribed channels
        await fetchSubscribedChannels();
        
        // Initial processing
        processPage();
        
        // Watch for DOM changes
        const observer = new MutationObserver(debounce((mutations) => {
            processPage();
        }, 500));
        
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
        
        console.log('YouTube Shorts & Feed Filter: Initialized successfully');
        
        // Refresh subscriptions periodically
        setInterval(() => {
            fetchSubscribedChannels();
        }, CACHE_DURATION);
    }

    // Wait for page to be ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();