YouTube - Remove Watch Later Videos (Exp. Hover)

Removes WL videos from the DOM based on the 'Added to Watch later' button/label. EXPERIMENTALLY simulates hover for edge cases. MAY IMPACT PERFORMANCE/BREAK.

// ==UserScript==
// @name         YouTube - Remove Watch Later Videos (Exp. Hover)
// @namespace    http://tampermonkey.net/
// @version      1.9.3-remove  // Changed version slightly to indicate modification
// @description  Removes WL videos from the DOM based on the 'Added to Watch later' button/label. EXPERIMENTALLY simulates hover for edge cases. MAY IMPACT PERFORMANCE/BREAK.
// @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==

/*
 * !! EXPERIMENTAL HOVER SIMULATION & ELEMENT REMOVAL !!
 * This script REMOVES videos marked as Watch Later (WL) by checking for the button
 * state (aria-label="Added to Watch later") or the explicit "Video from Watch Later" label.
 * If neither is found, it EXPERIMENTALLY attempts to simulate mouse hover to trigger
 * YouTube's code to potentially add the label, then checks again.
 *
 * !! CAVEATS !!
 * - PERFORMANCE: Programmatically triggering hover events can impact browser performance.
 * - RELIABILITY: The hover simulation method is FRAGILE.
 * - TIMING: Success depends on HOVER_SIM_DELAY_MS.
 * - REMOVAL SIDE EFFECTS: Removing elements might conflict with YT updates or other scripts.
 *
 * Consider this version experimental.
 */

(function() {
    'use strict';

    // --- Configuration ---
    const DEBUG = false; // Set to true for verbose console logging
    const watchLaterButtonSelector = 'ytd-toggle-button-renderer button[aria-label="Added to Watch later"]';
    const watchLaterLabelSelector = 'ytd-thumbnail-overlay-endorsement-renderer';
    const videoSelectors = [
        'ytd-rich-item-renderer',
        'ytd-grid-video-renderer',
        'ytd-compact-video-renderer',
        'ytd-video-renderer',
        'ytd-playlist-panel-video-renderer',
        'ytd-reel-item-renderer'
    ].join(', ');

    // REMOVED: hideClassName no longer needed as elements are removed.
    const hoverCheckPendingClass = 'us-wl-check-pending-235f-v2'; // Unique class for pending hover checks
    const HOVER_SIM_DELAY_MS = 200;
    const CHECK_DEBOUNCE_MS = 150;

    let checkTimeoutId = null;
    let isProcessingHover = false;
    let observer = null;

    // --- Logging Utility ---
    const log = DEBUG ? (...args) => console.log(`[WL Remover ${GM_info?.script?.version ?? 'Dev'}]`, ...args) : () => {};
    const warn = (...args) => console.warn(`[WL Remover ${GM_info?.script?.version ?? 'Dev'}]`, ...args);
    const error = (...args) => console.error(`[WL Remover ${GM_info?.script?.version ?? 'Dev'}]`, ...args);

    // REMOVED: addStyles function is no longer needed.

    // --- Core Logic Functions ---

    function getTitle(container) {
        try {
            const videoTitleElement = container.querySelector('#video-title');
            if (videoTitleElement?.textContent) return videoTitleElement.textContent.trim();
            const gridTitle = container.querySelector('h3.ytd-grid-video-renderer a#video-title')?.textContent;
            if (gridTitle) return gridTitle.trim();
            const compactTitle = container.querySelector('span#video-title.ytd-compact-video-renderer')?.textContent;
            if (compactTitle) return compactTitle.trim();
            const generalTitleLink = container.querySelector('[id="video-title-link"] yt-formatted-string')?.textContent;
            if (generalTitleLink) return generalTitleLink.trim();
             const ariaLabelTitle = container.querySelector('a#thumbnail')?.getAttribute('aria-label');
            if(ariaLabelTitle) return ariaLabelTitle.split(".")[0].trim();
            return '[Unknown Video Title]';
        } catch (e) {
            warn("Error getting video title:", e, container);
            return '[Error getting title]';
        }
    }

    /**
     * Checks videos currently in the DOM for existing WL indicators and removes them.
     * @returns {number} Count of newly removed videos in this pass.
     */
    function checkAndRemoveExisting() { // Renamed function slightly for clarity
        let newlyRemovedCount = 0;
        // Select videos that are not pending a hover check
        const videosToProcess = document.querySelectorAll(`${videoSelectors}:not(.${hoverCheckPendingClass})`); // Removed .hideClassName check
        log(`checkAndRemoveExisting: Found ${videosToProcess.length} candidates.`);

        videosToProcess.forEach(container => {
            try {
                const watchLaterButton = container.querySelector(watchLaterButtonSelector);
                const watchLaterLabel = container.querySelector(watchLaterLabelSelector);

                if (watchLaterButton || watchLaterLabel) {
                    // Check if it hasn't already been removed (robustness check)
                    if (container.parentNode) { // Check if it's still in the DOM
                        log(`REMOVING (Initial): ${getTitle(container)} (Button: ${!!watchLaterButton}, Label: ${!!watchLaterLabel})`);
                        container.remove(); // <<<--- MODIFIED: Remove the element
                        newlyRemovedCount++;
                    }
                }
            } catch (e) {
                 error("Error during initial check/removal on container:", e, container);
            }
        });
        if (newlyRemovedCount > 0) log(`checkAndRemoveExisting: Removed ${newlyRemovedCount} items.`);
        return newlyRemovedCount;
    }

    function tryHoverSimulation() {
        if (isProcessingHover) {
            log("Hover simulation skipped: Already in progress.");
            return;
        }

        // Select videos not removed and not already marked for a pending hover check
        const videosToHover = document.querySelectorAll(`${videoSelectors}:not(.${hoverCheckPendingClass})`); // Removed .hideClassName check

        if (videosToHover.length === 0) {
            log("No videos remaining for hover simulation.");
            return;
        }

        isProcessingHover = true;
        log(`Attempting hover simulation for ${videosToHover.length} videos.`);

        const targetsForDelayedCheck = [];

        videosToHover.forEach(container => {
             try {
                // Double-check it wasn't removed just before this loop iteration
                if (!container.parentNode) return;

                 container.classList.add(hoverCheckPendingClass);
                 targetsForDelayedCheck.push(container);

                 const thumbnailElement = container.querySelector('#thumbnail');
                 const dispatchTarget = thumbnailElement || container;

                 if(dispatchTarget) {
                    log(`Simulating hover on: ${getTitle(container)}`, dispatchTarget);
                    dispatchTarget.dispatchEvent(new MouseEvent('mouseover', { bubbles: true, cancelable: true, view: window }));
                    dispatchTarget.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true, view: window }));
                 } else {
                    warn("Could not find dispatch target for hover simulation on:", container);
                     container.classList.remove(hoverCheckPendingClass);
                     const index = targetsForDelayedCheck.indexOf(container);
                     if (index > -1) targetsForDelayedCheck.splice(index, 1);
                 }
            } catch (e) {
                 error("Error dispatching hover events:", e, container);
                 // Clean up if dispatching failed and container still exists
                 if(container.parentNode) container.classList.remove(hoverCheckPendingClass);
                 const index = targetsForDelayedCheck.indexOf(container);
                 if (index > -1) targetsForDelayedCheck.splice(index, 1);
             }
        });

        if (targetsForDelayedCheck.length > 0) {
            log(`Scheduling post-hover check for ${targetsForDelayedCheck.length} videos in ${HOVER_SIM_DELAY_MS}ms.`);
             setTimeout(() => checkAfterHover(targetsForDelayedCheck), HOVER_SIM_DELAY_MS);
        } else {
             log("No targets left for post-hover check, likely due to dispatch errors or prior removal.");
            isProcessingHover = false;
        }
    }

    function checkAfterHover(targets) {
        log(`Checking ${targets.length} videos after hover delay.`);
        let newlyRemovedCount = 0;

        targets.forEach(container => {
            // Check if the container element is still part of the document BEFORE removing pending class or doing checks.
            // It might have been removed by `checkAndRemoveExisting` running again.
            if (!container.parentNode || !document.body.contains(container)) {
                log(`Skipping post-hover check, container no longer in DOM: ${getTitle(container)} (Assumed Title)`);
                // If the title was needed before removal, you might need to store it earlier.
                return; // Exit if the element is gone
            }

            // Ensure pending class is removed from elements that still exist.
             container.classList.remove(hoverCheckPendingClass);

            try {
                // REMOVED check for hideClassName - not relevant anymore.

                 // Check for the label element after potential hover simulation
                 const hoverLabel = container.querySelector(watchLaterLabelSelector);
                if (hoverLabel) {
                    log(`REMOVING (Post-Hover): ${getTitle(container)}`);
                    container.remove(); // <<<--- MODIFIED: Remove the element
                    newlyRemovedCount++;
                } else {
                     log(`Post-hover label not found for: ${getTitle(container)}`);
                }

            } catch(e) {
                // Check parentNode again in catch block before logging, as the error itself might relate to the node being gone.
                const titleOnError = container.parentNode ? getTitle(container) : "[Removed Element - Title Unavailable]";
                error(`Error during post-hover check on container: ${titleOnError}`, e, container);
            }
        });

        if (newlyRemovedCount > 0) log(`Post-hover check removed ${newlyRemovedCount} items.`);
        isProcessingHover = false;
    }

    function runDetectionCycle() {
        clearTimeout(checkTimeoutId);
        checkTimeoutId = setTimeout(() => {
            log("Running detection cycle...");
            try {
                const initiallyRemoved = checkAndRemoveExisting(); // Run removal check first
                tryHoverSimulation(); // Then attempt hover simulation for remaining items
            } catch(e) {
                error("Error within detection cycle:", e);
                isProcessingHover = false;
            }
        }, CHECK_DEBOUNCE_MS);
    }

    function startObserver() {
        if (observer) {
             log("Observer already running.");
             return;
        }
        const targetNode = document.querySelector('ytd-page-manager');
        if (!targetNode) {
             warn("Target node ('ytd-page-manager') not found. Retrying initialization soon...");
             setTimeout(initializeScript, 500);
            return;
        }
        log("Target node found. Starting MutationObserver.");
        // REMOVED: Call to addStyles() is gone.

        observer = new MutationObserver(mutations => {
             let potentiallyRelevantChange = false;
             for (const mutation of mutations) {
                 if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                      for (const node of mutation.addedNodes) {
                          if (node.nodeType === Node.ELEMENT_NODE) {
                             if (node.matches?.(videoSelectors) || node.querySelector?.(videoSelectors)) {
                                potentiallyRelevantChange = true;
                                break;
                             }
                         }
                     }
                 }
                 if (potentiallyRelevantChange) break;
             }
             if (potentiallyRelevantChange) {
                 log("Relevant mutation detected. Scheduling detection cycle.");
                 runDetectionCycle();
             }
        });

        observer.observe(targetNode, { childList: true, subtree: true });
         log("Running initial detection cycle.");
         runDetectionCycle();
    }

    function initializeScript() {
        const scriptId = `${GM_info?.script?.name ?? 'WL Remover'} v${GM_info?.script?.version ?? 'Dev'}`;
        log(`Initializing ${scriptId} (Hover Simulation: ${DEBUG ? 'ON+LOGGING' : 'ON'}, Mode: REMOVE).`);
        startObserver();
    }

    // --- Script Entry Point ---
    if (window.location.pathname === '/playlist' && new URLSearchParams(window.location.search).get('list') === 'WL') {
         const scriptId = `${GM_info?.script?.name ?? 'WL Remover'} v${GM_info?.script?.version ?? 'Dev'}`;
         console.log(`[${scriptId}] On Watch Later page. Script inactive.`);
    } else {
         if (document.readyState === 'interactive' || document.readyState === 'complete') {
             initializeScript();
         } else {
            window.addEventListener('DOMContentLoaded', initializeScript, { once: true });
         }
    }

})(); // End of IIFE