YouTube - Hide Watch Later Videos (Combined Detection - Final)

Hides videos on YouTube browsing pages (homepage, subscriptions, search results, channel pages, etc.) that are detected as being in your "Watch Later" playlist. It attempts to hide them immediately upon page load or update, making your browsing experience cleaner by removing videos you've already saved.

目前為 2025-04-19 提交的版本,檢視 最新版本

// ==UserScript==
// @name         YouTube - Hide Watch Later Videos (Combined Detection - Final)
// @namespace    http://tampermonkey.net/
// @version      1.7
// @description  Hides videos on YouTube browsing pages (homepage, subscriptions, search results, channel pages, etc.) that are detected as being in your "Watch Later" playlist. It attempts to hide them immediately upon page load or update, making your browsing experience cleaner by removing videos you've already saved.
// @author       Anonymous
// @license MIT
// @match        https://www.youtube.com/*
// @exclude      https://www.youtube.com/playlist?list=WL*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// @run-at       document-idle
//
// ==/UserScript==
/*
 * HOW IT WORKS:
 * ============================================================================
 * This script checks video containers on various YouTube pages. It looks for
 * two main indicators that suggest a video is in your Watch Later list:
 *
 * 1. The Checkmark Icon Path: YouTube's internal HTML structure for video
 *    menus sometimes includes a specific SVG <path> element (with a unique 'd'
 *    attribute) when a video is marked as "Added to Watch Later". This script
 *    searches for this specific path within each video's container. This
 *    indicator is often present even if the menu itself isn't visible.
 *
 * 2. The Hover Label Element: When hovering over some recommended Watch Later
 *    videos (especially on the homepage), YouTube may display a grey label
 *    element below the thumbnail (e.g., containing text like "Videos from
 *    Watch Later playlist"). This script looks for the presence of the specific
 *    HTML element (`ytd-thumbnail-overlay-endorsement-renderer`) that holds
 *    this label. It can often find this element even if it's initially hidden
 *    by YouTube's CSS before you hover.
 *
 * If EITHER of these indicators is found within a video's container element
 * in the page's HTML, the script adds a specific CSS class (`us-hide-wl-video-235f`)
 * to that container. A CSS rule injected by this script then immediately hides
 * any element with that class using `display: none !important;`.
 *
 * It uses a MutationObserver to efficiently detect when new videos are loaded
 * onto the page (e.g., by scrolling down) and runs the check again on the
 * new content.
 *
 * LIMITATIONS & IMPORTANT NOTES:
 * ============================================================================
 * - Requires a Userscript Manager: You need a browser extension like
 *   Tampermonkey, Greasemonkey, or Violentmonkey to install and run this script.
 * - YouTube Updates: YouTube frequently changes its website code (HTML structure,
 *   CSS class names, element IDs). If this script stops working correctly after
 *   a YouTube update, the selectors used within the script (specifically
 *   `watchLaterCheckmarkPath`, `watchLaterLabelSelector`, and `videoSelectors`)
 *   may need to be updated by inspecting the new page elements using your
 *   browser's Developer Tools.
 * - Immediate vs. Delayed Hiding: This script aims for immediate hiding.
 *   However, if YouTube loads the indicator elements (either the checkmark
 *   or the hover label element) *after* the script's initial checks and DOM
 *   updates (which can sometimes happen, especially with complex page loads
 *   or very late dynamic content injection), those specific videos might not
 *   be hidden immediately or, in rare cases, might be missed. The script *cannot*
 *   detect indicators that are *only* added to the HTML code *after* a real
 *   user mouse hover interaction occurs – it relies on the indicator elements
 *   existing in the DOM structure when it runs its checks.
 * - Performance: While optimized, monitoring page changes can consume some
 *   browser resources, especially on very long pages or slower computers.
 * - Login State: The script relies on YouTube showing the correct Watch Later
 *   indicators, which generally requires you to be logged into your account.
 *
 * ============================================================================
 */

(function() {
    'use strict';

    // --- Configuration ---

    // 1. CSS selector for the checkmark icon's SVG path data (found within the video's menu structure)
    const watchLaterCheckmarkPath = "M4.6,11.4 L10,16.7 L19.4,7.3 L18,5.9 L10,13.9 L6,10 L4.6,11.4 Z";

    // 2. CSS selector for the hover overlay label element (the grey bar below thumbnail)
    const watchLaterLabelSelector = 'ytd-thumbnail-overlay-endorsement-renderer';

    // 3. CSS selectors for the main container elements of individual videos in grids/lists.
    const videoSelectors = [
        'ytd-rich-item-renderer',       // Home feed, channel videos grid, search grid
        'ytd-grid-video-renderer',      // Older Grid view?
        'ytd-compact-video-renderer',   // Sidebar recommendations, playlist items (outside WL page)
        'ytd-video-renderer',           // Search results list view
        'ytd-reel-item-renderer'        // Shorts feed items
    ].join(', '); // Combine selectors into a single efficient query string

    // 4. CSS class to add to videos that should be hidden
    const hideClassName = 'us-hide-wl-video-235f'; // Unique class name

    // --- Styling ---
    const style = document.createElement('style');
    style.textContent = `
        .${hideClassName} {
             display: none !important; /* Hide the element completely */
        }
    `;

    // --- Core Logic ---

    let checkQueued = false; // Flag to prevent multiple queued checks

    function hideWatchedLaterVideos() {
        checkQueued = false; // Reset the queue flag as the check is now running

        // Find video containers that *haven't* already been hidden by this script
        // This is more efficient than finding all videos and then checking the class inside the loop.
        const videosToProcess = document.querySelectorAll(`${videoSelectors}:not(.${hideClassName})`);

        if (videosToProcess.length === 0) {
            // console.log("YouTube Watch Later Hider: No new videos to process.");
            return; // No new/unhidden videos found
        }

        // console.log(`YouTube Watch Later Hider: Processing ${videosToProcess.length} video items.`);

        videosToProcess.forEach(container => {
            // Check if EITHER the checkmark path exists OR the hover label element exists within this container
            const checkmarkIcon = container.querySelector(`path[d="${watchLaterCheckmarkPath}"]`);
            const hoverLabel = container.querySelector(watchLaterLabelSelector);

            if (checkmarkIcon || hoverLabel) {
                // Add the class to hide the video via the CSS rule
                container.classList.add(hideClassName);

                /* // Optional: Uncomment for debugging which videos are being hidden and why
                const videoTitleElement = container.querySelector('#video-title');
                const videoTitle = videoTitleElement ? videoTitleElement.textContent.trim() : container.querySelector('h3')?.textContent.trim() || container.querySelector('[id="video-title-link"] yt-formatted-string')?.textContent.trim() || '[Unknown Video Title]';
                console.log(`YouTube Watch Later Hider: HIDING "${videoTitle}" (Indicator: ${checkmarkIcon ? 'Checkmark' : ''}${checkmarkIcon && hoverLabel ? ' +' : ''}${hoverLabel ? ' Label' : ''})`);
                */
            }
        });
    }

    // Queues a check to run on the next animation frame, preventing redundant checks
    function queueCheck() {
        if (!checkQueued) {
            checkQueued = true;
            requestAnimationFrame(hideWatchedLaterVideos);
        }
    }

    // --- Mutation Observer ---
    // Observes the page for changes (like new videos loading) and triggers checks.
    const observer = new MutationObserver(mutations => {
        // Trigger a check if relevant node changes occur.
        // Use requestAnimationFrame via queueCheck to avoid redundant checks on rapid mutations.
        let relevantChange = false;
         for (const mutation of mutations) {
             if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                // Quick check if *any* added node might be or contain a video we care about
                for (const node of mutation.addedNodes) {
                    if (node.nodeType === Node.ELEMENT_NODE && node.matches && (node.matches(videoSelectors) || node.querySelector(videoSelectors))) {
                        relevantChange = true;
                        break;
                    }
                 }
             }
            // Optionally, if needed later, could re-add minimal attribute checks,
            // but childList is the most important for new content.
             if (relevantChange) break;
         }

         if (relevantChange) {
             queueCheck();
        }
    });

    // --- Initialization ---
    function initializeScript() {
        // Inject the CSS rule into the page head
        document.head.appendChild(style);
        console.log(`[${GM_info.script.name} v${GM_info.script.version}]: Styles injected.`); // Use GM_info for script details

        // Find the main content area to observe
        const targetNode = document.querySelector('ytd-page-manager');

        if (targetNode) {
            console.log(`[${GM_info.script.name}]: Observer target found. Starting observation.`);
            // Run an initial check for any videos already present
            queueCheck();
            // Start observing the target node for changes
            observer.observe(targetNode, {
                childList: true,  // Detect when new video elements are added/removed
                subtree: true     // Monitor changes within descendants of the target
                // attributes: false // Generally not needed for this logic
            });
        } else {
            // If the main content area isn't ready yet, wait and try again
             console.warn(`[${GM_info.script.name}]: Target node ('ytd-page-manager') not found yet. Retrying in 500ms.`);
             setTimeout(initializeScript, 500);
        }
    }

    // --- Script Entry Point ---
    // Ensure we don't run on the actual Watch Later playlist page
    if (window.location.href.includes('/playlist?list=WL')) {
        console.log(`[${GM_info.script.name}]: On Watch Later page. Script inactive.`);
    } else {
         console.log(`[${GM_info.script.name} v${GM_info.script.version}]: Initializing...`);
         // Ensure the DOM is reasonably ready before starting
        if (document.readyState === 'interactive' || document.readyState === 'complete') {
             initializeScript();
        } else {
             window.addEventListener('DOMContentLoaded', initializeScript, { once: true });
        }
    }

})();