// UserScript
// @name Universal YouTube Mirror Links
// @namespace http://tampermonkey.net/
// @version 1.0
// @description Robustly adds Invidious mirrors to YouTube links on Google, DuckDuckGo, and StartPage by handling redirects and volatile selectors.
// @author You
// @match ://.google.com/
// @match
://duckduckgo.com/
// @match
://.startpage.com/
// @grant none
// @run-at document-end
// /UserScript

(function() {
'use strict';

// --- CONFIGURATION ---
const MIRROR_DOMAINS = [
    "inv.nadeko.net",
    "invidious.nerdvpn.de"
];
const FORCE_LANG = "en-US";

// --- UTILITIES ---

/**
 * 1. Robust Video ID Extraction
 * Matches standard watch, shorts, embed, live, and youtu.be formats.
 * @param {string} url - The URL to scan.
 * @returns {string|null} - The 11-character video ID or null.
 */
function extractVideoId(url) {
    if (!url) return null;
    // Regex to find YouTube ID from various formats
    const regex = /(?:youtube\.com\/(?:watch\?v=|shorts\/|embed\/|live\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/;
    const match = url.match(regex);
    return match ? match[1] : null;
}

/**
 * 2. URL De-Redirecting
 * Extracts the real URL from Google or DuckDuckGo redirect links.
 * @param {string} href - The raw href from the anchor tag.
 * @returns {string} - The clean destination URL.
 */
function getRealUrl(href) {
    try {
        const url = new URL(href);

        // Google Redirect Handling
        if (url.hostname.includes("google.com")) {
            if (url.pathname.includes("/url")) {
                // Standard search result redirect
                return url.searchParams.get("q") || url.searchParams.get("url");
            }
            if (url.pathname.includes("/imgres")) {
                // Image search result
                return url.searchParams.get("imgurl");
            }
        }

        // DuckDuckGo Redirect Handling
        if (url.hostname.includes("duckduckgo.com") && url.pathname.includes("/l/")) {
            return url.searchParams.get("uddg");
        }

    } catch (e) {
        // Invalid URL structure, ignore
    }
    return href; // Return original if no redirect detected
}

// --- CORE LOGIC ---

function processLink(anchor) {
    // 1. Avoid reprocessing links we've already handled
    if (anchor.dataset.mirrorProcessed) return;
    anchor.dataset.mirrorProcessed = "true";

    // 2. Get the real URL (peeling away Google/DDG redirects)
    const realUrl = getRealUrl(anchor.href);
    if (!realUrl) return;

    // 3. Check if it's a YouTube URL and get the ID
    const videoId = extractVideoId(realUrl);
    if (!videoId) return;

    // 4. Create the container for mirrors
    const container = document.createElement('div');
    container.style.cssText = `
        display: flex; 
        flex-wrap: wrap; 
        margin-top: 4px; 
        font-size: 13px; 
        font-family: system-ui, sans-serif;
    `;

    // 5. Generate links for each mirror domain
    MIRROR_DOMAINS.forEach(domain => {
        const link = document.createElement('a');
        link.href = `https://${domain}/watch?v=${videoId}&hl=${FORCE_LANG}`;
        link.textContent = domain;
        link.target = "_blank"; // Open in new tab
        link.style.cssText = `
            color: #1967D2; 
            margin-right: 12px; 
            text-decoration: none; 
            padding: 2px 4px; 
            border-radius: 3px;
        `;
        // Hover effects
        link.onmouseenter = () => link.style.backgroundColor = "#f1f3f4";
        link.onmouseleave = () => link.style.backgroundColor = "transparent";

        container.appendChild(link);
    });

    // 6. Insert into DOM
    // We try to find the main container for the search result to append the links.
    // We prioritize specific containers, then fall back to the parent element.
    let targetContainer = anchor.closest('div.g') || // Google Standard
                          anchor.closest('div[data-snc]') || // Google New Layout
                          anchor.closest('article[data-testid="result"]') || // DuckDuckGo
                          anchor.closest('div.result') || // StartPage / Generic
                          anchor.parentElement; // Fallback

    // If the anchor is inside a title (h3), we usually want to step out 
    // to the main container to append below the result snippet.
    if (anchor.querySelector('h3') && targetContainer === anchor.parentElement) {
        targetContainer = targetContainer.parentElement;
    }

    if (targetContainer) {
        // Double check we haven't already added links to this specific container
        if (!targetContainer.querySelector('.yt-mirror-links')) {
            container.classList.add('yt-mirror-links');
            targetContainer.appendChild(container);
        }
    }
}

// --- OBSERVER ---

// Use MutationObserver to handle dynamic loading (Infinite scroll, SPA navigation)
const observer = new MutationObserver((mutations) => {
    // Scan all anchors in the document. 
    // (Note: For extremely heavy pages, iterating mutations is better, 
    // but querySelectorAll is fast enough for typical search result counts).
    const links = document.querySelectorAll('a[href]');
    links.forEach(processLink);
});

// Initial Run: Process links already on the page
document.querySelectorAll('a[href]').forEach(processLink);

// Start Observing: Watch for future changes
observer.observe(document.body, {
    childList: true,
    subtree: true
});

// Fallback Interval: Catch any edge cases or late-loading elements
setInterval(() => {
    document.querySelectorAll('a[href]:not([data-mirror-processed])').forEach(processLink);
}, 2000);

})();

Edit

Pub: 21 Feb 2026 19:51 UTC

Views: 39