您需要先安装一款用户样式管理器扩展(如 Stylus )后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus )后才能安装此样式。
您需要先安装一款用户样式管理器扩展(如 Stylus )后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
您需要先安装一款用户样式管理器扩展后才能安装此样式。
(我已经安装了用户样式管理器,让我安装!)
Wrap lines
// ==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