Google Cookie Consent Remover

Remove Google and Youtube's annoying cookie consent questions (especially annoying for Incognito windows)

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

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

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Google Cookie Consent Remover
// @namespace    https://gist.github.com/WoLpH/985a6072b35926eb4a3f9d2cdd0a2dad
// @version      0.8
// @description  Remove Google and Youtube's annoying cookie consent questions (especially annoying for Incognito windows)
// @author       Wolph
// @include      /https:\/\/(www|consent)\.(google|youtube)\.*\w+\/.*$/
// @include      https://consent.google.com/*
// @match        *://*.google.*/*
// @match        *://*.youtube.*/*
// @homepage     https://gist.github.com/WoLpH/985a6072b35926eb4a3f9d2cdd0a2dad
// @grant        none
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const SCRIPT_NAME = "GoogleCookieConsentRemover";
    const DEBUG = true; // Set to false to reduce console output

    function log(message) {
        if (DEBUG) {
            console.log(`[${SCRIPT_NAME}] ${message}`);
        }
    }

    function warn(message) {
        console.warn(`[${SCRIPT_NAME}] ${message}`);
    }

    function error(err, context = "") {
        console.error(`[${SCRIPT_NAME}] Error ${context}:`, err);
    }

    // XPaths for common light DOM structures
    const XPATHS = [
        // English
        '//button[.//span[contains(text(),"Reject all")]]', // YouTube new structure (if in light DOM)
        '//button[.//div[contains(text(),"Reject all")]]',
        '//button[contains(., "Reject all")]',
        '//input[@type="submit" and contains(@value,"Reject all")]',
        // Dutch ("Alles afwijzen")
        '//button[.//span[contains(text(),"Alles afwijzen")]]',
        '//button[.//div[contains(text(),"Alles afwijzen")]]',
        '//button[contains(., "Alles afwijzen")]',
        '//input[@type="submit" and contains(@value,"Alles afwijzen")]',
        // German ("Alle ablehnen")
        '//button[.//span[contains(text(),"Alle ablehnen")]]',
        '//button[.//div[contains(text(),"Alle ablehnen")]]',
        '//button[contains(., "Alle ablehnen")]',
        '//input[@type="submit" and contains(@value,"Alle ablehnen")]',
        // French ("Tout refuser")
        '//button[.//span[contains(text(),"Tout refuser")]]',
        '//button[.//div[contains(text(),"Tout refuser")]]',
        '//button[contains(., "Tout refuser")]',
        '//input[@type="submit" and contains(@value,"Tout refuser")]',
        // Spanish ("Rechazar todo")
        '//button[.//span[contains(text(),"Rechazar todo")]]',
        '//button[.//div[contains(text(),"Rechazar todo")]]',
        '//button[contains(., "Rechazar todo")]',
        '//input[@type="submit" and contains(@value,"Rechazar todo")]',
        // Italian ("Rifiuta tutto")
        '//button[.//span[contains(text(),"Rifiuta tutto")]]',
        '//button[.//div[contains(text(),"Rifiuta tutto")]]',
        '//button[contains(., "Rifiuta tutto")]',
        '//input[@type="submit" and contains(@value,"Rifiuta tutto")]',
        // Generic form-based fallback
        '//form[contains(@action, "consent") or contains(@action, "reject")]/button[not(@jsname) or @jsname="LgbsSe" or @jsname="ZUkOIc"]',
        '//form[contains(@action, "consent") or contains(@action, "reject")]//button[translate(normalize-space(.), "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz") = "reject all"]',
        // YouTube specific aria-labels (usually for tp-yt-paper-button or button)
        // Note: The deep search below is better for aria-labels in Shadow DOM
        '//button[contains(translate(@aria-label, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "reject all")]',
        '//tp-yt-paper-button[contains(translate(@aria-label, "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz"), "reject all")]',
    ];

    // Keywords for deep search (aria-label or text content)
    // Order by likely prevalence or specificity.
    const DEEP_SEARCH_KEYWORDS = [
        "Reject the use of cookies and other data for the purposes described", // Exact YouTube aria-label
        "Reject all", "Alles afwijzen", "Alle ablehnen", "Tout refuser", "Rechazar todo", "Rifiuta tutto"
    ];


    let isHandled = false;
    let observer = null;
    let observerTimeoutId = null;
    let periodicCheckIntervalId = null;
    let periodicCheckMasterTimeoutId = null;

    const OBSERVER_MAX_DURATION_MS = 30000;
    const PERIODIC_CHECK_INTERVAL_MS = 1000;
    const PERIODIC_CHECK_DURATION_MS = 10000;

    function cleanup(reason) {
        log(`Cleanup called. Reason: ${reason}`);
        if (periodicCheckIntervalId) { clearInterval(periodicCheckIntervalId); periodicCheckIntervalId = null; log("Periodic check interval cleared."); }
        if (periodicCheckMasterTimeoutId) { clearTimeout(periodicCheckMasterTimeoutId); periodicCheckMasterTimeoutId = null; log("Periodic check master timeout cleared."); }
        if (observer) { observer.disconnect(); observer = null; log("MutationObserver disconnected."); }
        if (observerTimeoutId) { clearTimeout(observerTimeoutId); observerTimeoutId = null; log("MutationObserver master timeout cleared."); }
    }

    /**
     * Recursively searches for elements matching the selector, piercing Shadow DOM.
     * @param {string} selector - The CSS selector to match.
     * @param {Element|ShadowRoot} rootNode - The node to start searching from.
     * @returns {Element[]} An array of found elements.
     */
    function deepQuerySelectorAll(selector, rootNode = document.documentElement) {
        const results = [];
        const elementsToSearchIn = [rootNode];

        while (elementsToSearchIn.length > 0) {
            const currentElement = elementsToSearchIn.shift();
            if (!currentElement || typeof currentElement.querySelectorAll !== 'function') {
                continue;
            }

            // Search in the light DOM of the current element
            results.push(...Array.from(currentElement.querySelectorAll(selector)));

            // Search in the shadow DOM of all children of currentElement
            const shadowHosts = currentElement.querySelectorAll('*');
            for (const host of shadowHosts) {
                if (host.shadowRoot) {
                    // Add shadowRoot itself to search inside it.
                    // Its children's shadowRoots will be processed from shadowHosts.
                    results.push(...Array.from(host.shadowRoot.querySelectorAll(selector)));
                    elementsToSearchIn.push(host.shadowRoot); // Also queue the shadowRoot for deeper traversal of its children's shadowRoots
                }
            }
        }
        return results;
    }


    /**
     * Checks visibility and attempts to click the element.
     * @param {HTMLElement} element - The element to click.
     * @param {string} foundByType - Description of how the element was found (for logging).
     * @returns {boolean} True if clicked successfully, false otherwise.
     */
    function attemptClickOnElement(element, foundByType) {
        log(`Found potential button by ${foundByType}. Details: Tag=${element.tagName}, Text="${element.textContent?.trim().substring(0, 50)}", Aria="${element.getAttribute('aria-label')?.substring(0, 50)}"`);

        const style = window.getComputedStyle(element);
        if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0' || element.disabled || element.getAttribute('aria-hidden') === 'true') {
            log(`Button seems non-interactable (display: ${style.display}, visibility: ${style.visibility}, opacity: ${style.opacity}, disabled: ${element.disabled}, aria-hidden: ${element.getAttribute('aria-hidden')}). Skipping.`);
            return false;
        }

        try {
            log("Attempting to click the button...");
            element.click();
            log(`Button clicked successfully (found by ${foundByType}).`);
            return true;
        } catch (e) {
            error(e, `while clicking button found by ${foundByType}`);
            return false;
        }
    }


    function findAndClickButtonOnly() {
        log("findAndClickButtonOnly: Searching for consent button...");

        // Phase 1: XPath search (primarily for Light DOM, faster)
        log("Phase 1: XPath search (Light DOM).");
        for (const xpath of XPATHS) {
            let buttonNode = null;
            try {
                buttonNode = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
            } catch (e) {
                error(e, `while evaluating XPath: ${xpath}`);
                continue;
            }
            if (buttonNode && (buttonNode instanceof HTMLElement)) {
                if (attemptClickOnElement(buttonNode, `XPath: ${xpath}`)) return true;
            }
        }
        log("Phase 1: XPath search did not find a clickable button.");

        // Phase 2: Deep search (piercing Shadow DOMs)
        log("Phase 2: Deep search (Shadow DOM piercing).");
        const clickableElementSelectors = 'button, input[type="submit"], tp-yt-paper-button'; // Common clickable elements
        let allPotentialClickableElements = [];
        try {
            allPotentialClickableElements = deepQuerySelectorAll(clickableElementSelectors);
        } catch (e) {
            error(e, "during deepQuerySelectorAll execution");
            return false; // Critical failure in deep search
        }

        log(`Deep search found ${allPotentialClickableElements.length} potential elements using selector: "${clickableElementSelectors}".`);

        for (const element of allPotentialClickableElements) {
            // 1. Check ARIA label
            const ariaLabel = element.getAttribute('aria-label');
            if (ariaLabel) {
                for (const keyword of DEEP_SEARCH_KEYWORDS) {
                    if (ariaLabel.toLowerCase().includes(keyword.toLowerCase())) {
                        if (attemptClickOnElement(element, `Deep search - ARIA label ("${ariaLabel.substring(0,50)}") matching keyword "${keyword}"`)) return true;
                        // Don't break here, element might also match text or a more specific aria keyword
                    }
                }
            }

            // 2. Check Text Content (or value for inputs)
            let textContent = "";
            const tagName = element.tagName.toLowerCase();

            if (tagName === 'input') {
                textContent = element.value || "";
            } else { // For <button> or <tp-yt-paper-button>
                // Standard way to get text for tp-yt-paper-button label
                const paperButtonLabel = element.querySelector('#text, #label'); // #text is common in yt-formatted-string inside paper-button
                if (paperButtonLabel && paperButtonLabel.textContent) {
                    textContent = paperButtonLabel.textContent.trim();
                } else if (element.textContent) { // General text content
                     textContent = element.textContent.trim();
                }
                // For specific YouTube buttons, text is in a nested span
                const ytButtonSpan = element.querySelector('span.yt-core-attributed-string, .button-renderer span, .yt-spec-button-shape-next__button-text-content span');
                if (ytButtonSpan && ytButtonSpan.textContent && ytButtonSpan.textContent.trim().length > (textContent.length / 2) ) { // Prefer specific span if its text is substantial
                    textContent = ytButtonSpan.textContent.trim();
                }
            }

            if (textContent) {
                for (const keyword of DEEP_SEARCH_KEYWORDS) {
                    if (textContent.toLowerCase().includes(keyword.toLowerCase())) { // Using includes for partial matches like "Reject all cookies"
                        if (attemptClickOnElement(element, `Deep search - Text ("${textContent.substring(0,50)}") matching keyword "${keyword}"`)) return true;
                    }
                }
            }
        }
        log("Phase 2: Deep search did not find a clickable button.");
        log("No suitable consent button found after all search phases.");
        return false;
    }

    function attemptToHandleConsent(triggerSource) {
        if (isHandled) return true;
        log(`Attempting to handle consent (triggered by: ${triggerSource}).`);
        if (findAndClickButtonOnly()) {
            isHandled = true;
            cleanup(`Consent handled (source: ${triggerSource})`);
            log(`Consent successfully handled by ${triggerSource}. All checks stopped.`);
            return true;
        }
        return false;
    }

    // --- Main Execution ---
    log("Script started.");

    const MAIN_CONSENT_PAGE_HOST_REGEX = /^(www|consent)\.(google|youtube)\./i;
    const GENERAL_GOOGLE_YOUTUBE_HOST_REGEX = /(\.google\.|\.youtube\.)/i;
    const isLikelyFullConsentPage = MAIN_CONSENT_PAGE_HOST_REGEX.test(window.location.hostname);
    const isAnyGoogleYouTubeDomain = GENERAL_GOOGLE_YOUTUBE_HOST_REGEX.test(window.location.hostname);
    const pathMightHaveConsentPopup = /\/(consent|watch|results|search|embed|$)/i.test(window.location.pathname); // Includes root "/"
    const isRootPath = window.location.pathname === "/";

    if (isAnyGoogleYouTubeDomain && !isLikelyFullConsentPage && !pathMightHaveConsentPopup && !isRootPath) {
        log(`Current page (${window.location.href}) is a Google/YouTube domain but unlikely for full consent. Performing a single quick check.`);
        if (attemptToHandleConsent("initial check on less-likely page")) {
            log("Consent dialog handled on less-likely page type.");
        } else {
            log("No consent dialog found on less-likely page type. Script will now exit for this page.");
        }
        return;
    }

    if (attemptToHandleConsent("initial synchronous check")) {
        log("Dialog handled on initial synchronous check. Script finished.");
        return;
    }

    if (!isHandled) {
        log(`Starting periodic checks every ${PERIODIC_CHECK_INTERVAL_MS / 1000}s for up to ${PERIODIC_CHECK_DURATION_MS / 1000}s.`);
        periodicCheckIntervalId = setInterval(() => {
            if (isHandled || document.hidden) {
                if (document.hidden && !isHandled) log("Tab is hidden, skipping periodic check's attemptToHandleConsent.");
                return;
            }
            attemptToHandleConsent("periodic check");
        }, PERIODIC_CHECK_INTERVAL_MS);

        periodicCheckMasterTimeoutId = setTimeout(() => {
            if (periodicCheckIntervalId) {
                clearInterval(periodicCheckIntervalId); periodicCheckIntervalId = null;
                log("Periodic checks completed their duration without success.");
            }
        }, PERIODIC_CHECK_DURATION_MS);
    }

    if (!isHandled) {
        log("Setting up MutationObserver.");
        observer = new MutationObserver((mutationsList, obs) => {
            if (isHandled) {
                obs.disconnect(); observer = null;
                log("MutationObserver: Consent already handled. Disconnecting self.");
                return;
            }
            log(`DOM changes detected by MutationObserver. Mutations: ${mutationsList.length}`);
            let potentiallyRelevantChange = false;
            for (const mutation of mutationsList) {
                if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                    potentiallyRelevantChange = true; break;
                }
                if (mutation.type === 'attributes') { // Sometimes dialog appears by changing attributes
                    potentiallyRelevantChange = true; break;
                }
            }
            if (!potentiallyRelevantChange) return;
            attemptToHandleConsent("MutationObserver");
        });

        observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
        log("MutationObserver started. Waiting for DOM changes or observer timeout.");

        observerTimeoutId = setTimeout(() => {
            if (!isHandled) {
                warn(`MutationObserver timed out after ${OBSERVER_MAX_DURATION_MS / 1000}s.`);
                cleanup(`Max observation duration reached for observer.`);
            }
        }, OBSERVER_MAX_DURATION_MS);
    }
    log("Initial setup complete. Waiting for consent dialog or timeouts.");
})();