YouTube Shorts & Feed Filter for Subscriptions

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

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 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();
    }

})();