YouTube - Hide Watch Later Videos (v1.9 - Hover Trigger Polished)

Hides WL videos using checkmark or label indicators. Attempts hover simulation if needed. Optimized final version based on user feedback. Remember YT updates might break it.

目前为 2025-04-19 提交的版本。查看 最新版本

// ==UserScript==
// @name         YouTube - Hide Watch Later Videos (v1.9 - Hover Trigger Polished)
// @namespace    http://tampermonkey.net/
// @version      1.9
// @description  Hides WL videos using checkmark or label indicators. Attempts hover simulation if needed. Optimized final version based on user feedback. Remember YT updates might break it.
// @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        GM_info
// @run-at       document-idle
// ==/UserScript==
/*
 * HOW IT WORKS:
 * 1. Initial Check: Scans for existing checkmark icons OR hover label elements. Hides matches.
 * 2. Hover Simulation (If Needed): For remaining videos, simulates a 'mouseover' event
 *    to try and trigger YouTube to add the hover label element to the DOM.
 * 3. Delayed Check: Waits briefly then re-checks only the simulated videos for the hover label.
 * 4. Hides via CSS `display: none !important;`.
 * 5. Uses MutationObserver to re-run checks when the page changes (e.g., scrolling).
 *
 * !! CAVEATS !!
 * - EXPERIMENTAL HOVER: The hover simulation is clever but relies on current YouTube
 *   behavior. It might break with future YouTube updates or impact performance
 *   on some systems.
 * - YOUTUBE UPDATES: Selectors (`watchLaterCheckmarkPath`, `watchLaterLabelSelector`,
 *   `videoSelectors`) may need updating if YT changes its code structure.
 * - LOGIN REQUIRED: Needs you to be logged in for YouTube to identify WL videos.
 * - USERSCRIPT MANAGER: Requires Tampermonkey, Greasemonkey, Violentmonkey, etc.
 */

(function() {
    'use strict';

    // --- Configuration ---
    const SCRIPT_NAME = typeof GM_info !== 'undefined' ? GM_info.script.name : 'YT WL Hider';
    const SCRIPT_VERSION = typeof GM_info !== 'undefined' ? GM_info.script.version : '1.9';

    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"; // Checkmark path
    const watchLaterLabelSelector = 'ytd-thumbnail-overlay-endorsement-renderer'; // Grey hover label element
    const videoSelectors = [
        'ytd-rich-item-renderer',
        'ytd-grid-video-renderer',
        'ytd-compact-video-renderer',
        'ytd-video-renderer',
        'ytd-reel-item-renderer'
    ].join(', '); // Video container selectors
    const hideClassName = 'us-hide-wl-video-hover-final'; // Unique CSS class for hiding
    const hoverCheckPendingClass = 'us-wl-check-pending-final'; // Temp class during hover check

    const DEBOUNCE_DELAY_MS = 75; // Debounce mutations slightly longer (ms)
    const HOVER_SIM_DELAY_MS = 150; // Delay after simulated hover before re-check (ms)
    const INITIAL_HOVER_ATTEMPT_DELAY_MS = 250; // Extra delay before the *first* hover attempt

    let checkTimeoutId = null;      // Stores debounce timer ID
    let isProcessingHover = false; // Prevents overlapping hover simulations
    let initialCheckDone = false; // Flag for applying INITIAL_HOVER_ATTEMPT_DELAY_MS only once


    // --- Styling ---
    const style = document.createElement('style');
    style.textContent = `.${hideClassName} { display: none !important; }`;
    document.head.appendChild(style);
    console.log(`[${SCRIPT_NAME} v${SCRIPT_VERSION}]: Styles injected.`);

    // --- Core Functions ---

    function getTitle(container) { // Helper for debugging logs
        const videoTitleElement = container.querySelector('#video-title');
        return videoTitleElement ? videoTitleElement.textContent.trim()
               : container.querySelector('h3')?.textContent.trim() ||
                 container.querySelector('[id="video-title-link"] yt-formatted-string')?.textContent.trim() ||
                 '[Unknown Video Title]';
    }

    // 1. Check for existing indicators and hide matches
    function checkAndHideExisting() {
        let newlyHiddenCount = 0;
        const videosToProcess = document.querySelectorAll(`${videoSelectors}:not(.${hideClassName})`);

        videosToProcess.forEach(container => {
            if (container.classList.contains(hoverCheckPendingClass)) return; // Skip pending items

            const checkmarkIcon = container.querySelector(`path[d="${watchLaterCheckmarkPath}"]`);
            const hoverLabel = container.querySelector(watchLaterLabelSelector);

            if (checkmarkIcon || hoverLabel) {
                 container.classList.remove(hoverCheckPendingClass); // Remove pending mark if found early
                 container.classList.add(hideClassName);
                 // console.log(`[${SCRIPT_NAME}] HIDING (Initial Check): ${getTitle(container)}`);
                 newlyHiddenCount++;
            }
        });
         if (newlyHiddenCount > 0) {
             // console.log(`[${SCRIPT_NAME}] Initial check hid ${newlyHiddenCount} item(s).`);
         }
        return newlyHiddenCount;
    }

    // 2. Attempt hover simulation on remaining items
    function tryHoverSimulation() {
        if (isProcessingHover) return;

        const videosToHover = document.querySelectorAll(`${videoSelectors}:not(.${hideClassName}):not(.${hoverCheckPendingClass})`);
        if (videosToHover.length === 0) return;

        isProcessingHover = true;
         // console.log(`[${SCRIPT_NAME}] Attempting hover simulation for ${videosToHover.length} video(s).`);

        const targetsForDelayedCheck = [];

        videosToHover.forEach(container => {
            container.classList.add(hoverCheckPendingClass);
            targetsForDelayedCheck.push(container);
            const thumbnailElement = container.querySelector('#thumbnail');
            const dispatchTarget = thumbnailElement || container;

            try {
                 // Only dispatch mouseover, usually sufficient for hover effects
                 dispatchTarget.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true, view: window }));
            } catch (e) {
                 console.warn(`[${SCRIPT_NAME}] Error dispatching hover:`, e, dispatchTarget);
                 container.classList.remove(hoverCheckPendingClass); // Clean up if dispatch failed
                 const index = targetsForDelayedCheck.indexOf(container);
                 if (index > -1) targetsForDelayedCheck.splice(index, 1);
            }
        });

        if (targetsForDelayedCheck.length > 0) {
            setTimeout(() => checkAfterHover(targetsForDelayedCheck), HOVER_SIM_DELAY_MS);
        } else {
            isProcessingHover = false; // Unlock if nothing to check
        }
    }

    // 3. Check for the label after the hover simulation delay
    function checkAfterHover(targets) {
        let newlyHiddenCount = 0;
        targets.forEach(container => {
            container.classList.remove(hoverCheckPendingClass); // Clean up pending class
            if (container.classList.contains(hideClassName)) return; // Skip if already hidden

            // Check specifically for the hover label element NOW
            const hoverLabel = container.querySelector(watchLaterLabelSelector);
            if (hoverLabel) {
                container.classList.add(hideClassName);
                 // console.log(`[${SCRIPT_NAME}] HIDING (Post-Hover): ${getTitle(container)}`);
                newlyHiddenCount++;
            }
            // Optional: Simulate mouseout if needed for cleanup, but often not required
            // const dispatchTarget = container.querySelector('#thumbnail') || container;
            // try { dispatchTarget.dispatchEvent(new MouseEvent('mouseout', { bubbles: true, cancelable: true, view: window })); } catch(e){}
        });
         // if (newlyHiddenCount > 0) console.log(`[${SCRIPT_NAME}] Post-hover check hid ${newlyHiddenCount} item(s).`);
        isProcessingHover = false; // Allow next hover cycle
    }

    // --- Debounced Main Check Orchestrator ---
    function runDetectionCycle(isInitialRun = false) {
        clearTimeout(checkTimeoutId); // Clear pending debounce timer

        checkTimeoutId = setTimeout(() => {
             // console.log(`[${SCRIPT_NAME}] Running detection cycle.`);
            checkAndHideExisting(); // Always run the initial check

            // Determine delay for hover attempt
            const hoverAttemptDelay = isInitialRun ? INITIAL_HOVER_ATTEMPT_DELAY_MS : 0;

             setTimeout(() => {
                tryHoverSimulation();
            }, hoverAttemptDelay);

             if (isInitialRun) {
                initialCheckDone = true; // Mark initial run complete
            }
        }, DEBOUNCE_DELAY_MS);
    }


    // --- Mutation Observer ---
    const observer = new MutationObserver(mutations => {
         // Basic trigger: if nodes were added or removed in relevant areas, queue a check.
         // This is lightweight and lets runDetectionCycle handle the efficient checking.
         let potentialChange = false;
         for (const mutation of mutations) {
            if (mutation.type === 'childList' && (mutation.addedNodes.length > 0 || mutation.removedNodes.length > 0) ) {
                 potentialChange = true;
                 break;
            }
            // Could add basic attribute check if needed: else if (mutation.type === 'attributes') potentialChange = true;
        }
        if(potentialChange) {
             runDetectionCycle(false); // Trigger cycle, it's not the initial run
         }
    });

    // --- Initialization ---
    function initializeScript() {
        const targetNode = document.querySelector('ytd-page-manager');

        if (targetNode) {
            console.log(`[${SCRIPT_NAME}]: Observer target found. Starting observation.`);
            runDetectionCycle(true); // Run initial checks + first hover attempt (with extra delay)
            observer.observe(targetNode, {
                childList: true,
                subtree: true
            });
        } else {
             console.warn(`[${SCRIPT_NAME}]: Target node ('ytd-page-manager') not found. Retrying...`);
            setTimeout(initializeScript, 500);
        }
    }

    // --- Script Entry Point ---
    if (window.location.href.includes('/playlist?list=WL')) {
         console.log(`[${SCRIPT_NAME}]: On Watch Later page. Script inactive.`);
    } else {
         console.log(`[${SCRIPT_NAME} v${SCRIPT_VERSION}]: Initializing...`);
        if (document.readyState === 'interactive' || document.readyState === 'complete') {
             initializeScript();
        } else {
             window.addEventListener('DOMContentLoaded', initializeScript, { once: true });
        }
    }

})();