// 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';
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | // --- 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);
|
})();