您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Redirects YouTube videos to an Invidious instance.
当前为
// ==UserScript== // @name Redirect to Invidious // @author André Kugland // @description Redirects YouTube videos to an Invidious instance. // @namespace https://github.com/kugland // @license MIT // @version 0.3.3 // @match https://www.youtube.com/* // @match https://m.youtube.com/* // @exclude *://music.youtube.com/* // @exclude *://*.music.youtube.com/* // @run-at document-start // @noframes // @grant GM_xmlhttpRequest // @grant GM.xmlhttpRequest // @homepageURL https://greasyfork.org/scripts/477967-redirect-to-invidious // ==/UserScript== /* This script is transpiled from TypeScript, that’s why it looks a bit weird. For the original source code, see https://github.com/kugland/invidious-redirect/. */ (() => { // src/domhelper.ts function onload(callback) { if (document.readyState !== "loading") { callback(); } else { document.addEventListener("DOMContentLoaded", callback); } } function element(html) { const template = document.createElement("template"); template.innerHTML = html.trim(); return template.content.firstChild; } // src/config.ts var DEFAULT_INSTANCE_URL = "https://invidious.protokolla.fi"; var INSTANCES_JSON_URL = "https://raw.githubusercontent.com/kugland/invidious-redirect/master/instances.json"; var INSTANCE_URL_KEY = "invidious-redirect--instance"; var INSTANCES_KEY = "invidious-redirect--public-instances"; var UPDATED_KEY = "invidious-redirect--public-instances-updated"; var instanceUrl = localStorage.getItem(INSTANCE_URL_KEY) || DEFAULT_INSTANCE_URL; var publicInstances = JSON.parse(localStorage.getItem(INSTANCES_KEY) || "{}"); var instancesUpdated = parseInt(localStorage.getItem(UPDATED_KEY) || "0"); function getInstanceUrl() { return instanceUrl.replace(/\/$/, "").replace(/^https:\/\//, ""); } function getFullInstanceUrl() { if (instanceUrl.startsWith("http://")) { return instanceUrl; } else { return `https://${instanceUrl}`; } } function setInstanceUrl(url) { instanceUrl = url.replace(/\/$/, "").replace(/^https:\/\//, ""); localStorage.setItem(INSTANCE_URL_KEY, url); } async function getInstances() { const now = Date.now(); const expired = now - instancesUpdated > 864e5; if (Object.keys(publicInstances).length !== 0 && !expired) { return publicInstances; } else { publicInstances = await loadInstances(); instancesUpdated = now; localStorage.setItem(INSTANCES_KEY, JSON.stringify(publicInstances)); localStorage.setItem(UPDATED_KEY, instancesUpdated.toString()); return publicInstances; } } async function loadInstances() { const options = { method: "GET", headers: { "Content-Type": "application/json" } }; return new Promise((resolve) => { const gm_xmlHttpRequest = (typeof GM !== "undefined" ? GM?.xmlHttpRequest : null) || (typeof GM_xmlhttpRequest !== "undefined" ? GM_xmlhttpRequest : null); if (gm_xmlHttpRequest) { gm_xmlHttpRequest({ ...options, nocache: true, url: INSTANCES_JSON_URL, onload: (response) => resolve(JSON.parse(response.responseText)) }); } else if (false) { fetch("/instances.json", { cache: "no-cache", ...options }).then(async (response) => resolve(await response.json())); } else { throw new Error( "Unable to load instances.json. Is the script running in a userscript manager?" ); } }).then((instances) => instances); } function clearInstances() { publicInstances = {}; instancesUpdated = 0; localStorage.removeItem(INSTANCES_KEY); localStorage.removeItem(UPDATED_KEY); } // assets/refresh.svg var refresh_default = '<svg width="13" height="13" viewBox="0 0 130 130"><path d="M22 63a8 8 0 0 1-16 0 59 59 0 0 1 97.1-45v-6.3a8 8 0 1 1 16.1 0V37a8 8 0 0 1-8 8l-24.4 2.2a8 8 0 1 1-1.4-16l8-.7A43 43 0 0 0 22 63zm22.8 19.6a8 8 0 0 1 1.5 16l-10 .9a43 43 0 0 0 71.7-32 8 8 0 0 1 16 0 59 59 0 0 1-95.6 46.2v4.2a8 8 0 0 1-16 0v-25a8 8 0 0 1 7.2-8l25.3-2.4z" style="fill:currentColor"/></svg>\n'; // assets/new-tab.svg var new_tab_default = '<svg width="16" height="16" viewBox="0 0 96 96"><path d="M83 13 44 52m16-40h23v23M45 22H12v62h62l-0-32" style="fill:none;stroke:currentColor;stroke-width:var(--stroke-width);stroke-linecap:round;stroke-linejoin:round"/></svg>\n'; // src/select.ts var INVIDIOUS_INSTANCE_CONTAINER = "invidious-instance-container"; async function showDialog() { const instances = await getInstances(); const tableHtml = Object.keys(instances).map((uri) => ` <tr data-url="https://${uri}"> <td>${uri}</td> <td>${instances[uri].toLowerCase()}</td> <td><a href="https://${uri}" target="_blank">${new_tab_default}</a></td> </tr> `).join(""); const dialog = element(` <div id="${INVIDIOUS_INSTANCE_CONTAINER}" ondragstart="return false;"> <div> <header> <span>Select an Invidious instance</span> <span class="refresh">${refresh_default}</span> <span class="close">\u2715</span> <a class="rateme" href="https://greasyfork.org/en/scripts/477967-redirect-to-invidious/feedback" target="_blank"> <div> Rate this script! <span class="emoji">\u{1F60A}</span> </div> </a> </header> <table>${tableHtml}</table> <footer> <div class="input-container"> <span class="input-helper">Add http:// if it\u2019s not an https:// URL.</span> <input type="text" /> </div> <button>Save</button> </footer> </div> </div> `); const input = dialog.querySelector("footer input"); if (!input) return; input.value = getInstanceUrl() || ""; input.placeholder = "invidious.snopyta.org"; document.body.appendChild(dialog); dialog.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const dialog2 = target.closest(`#${INVIDIOUS_INSTANCE_CONTAINER}`); const input2 = dialog2?.querySelector("footer input"); if (!dialog2 || !input2) return; if (target.tagName != "A") { event.preventDefault(); event.stopPropagation(); } if (target.matches(".close")) { dialog2.remove(); } else if (target.matches(".refresh")) { clearInstances(); dialog2.remove(); showDialog(); } else if (target.matches("tr[data-url] *:not(a)")) { const url = target.closest("tr")?.getAttribute("data-url"); if (url) { input2.value = url.replace(/^https:\/\//, ""); } } else if (target.matches("footer button")) { try { new URL(`https://${input2.value}`); setInstanceUrl(input2.value); dialog2.remove(); } catch (e) { alert("Invalid URL"); } } }, true); } // assets/button.png var button_default = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAJFBMVEXv8e7Z4ePn6+kWt/CZzvChpKFrbWrT1dJVV1WJjIm2uLXCxMH33HXYAAAAp0lEQVR4AeXNIQ7CMABG4ceSsXSYIXFVFaCAC5BwgblNV4HDkMwiIA0YDMnkDMHWoHY5PPwGSfjsE4+fNbZIyXIBOszR1iu+lICWFmiuRGsOaPURbXOyKINb6FDyR/AoZlefURyNnuwxelKR6YmHVk2yK3qSd+iJKdATB9Be+PAEPakATIi8STzISVaiJ2lET4YFejIBPbmDnEy3ETmZ9REARr3lP7wAXHImU2sAU14AAAAASUVORK5CYII="; // css/style.css var style_default = '#set-invidious-url{position:fixed;bottom:0;right:0;height:48px;width:48px;z-index:99998;margin:1rem;cursor:pointer;border-radius:50%;box-shadow:0px 0px 3px #000;opacity:.5}#set-invidious-url:hover{opacity:1 !important}#invidious-instance-container{position:fixed;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,.2);backdrop-filter:blur(2px);display:grid;place-content:center;z-index:99999;overflow-y:auto}#invidious-instance-container,#invidious-instance-container *{font-family:mono;font-size:12px;box-sizing:border-box}#invidious-instance-container>div{background-color:#fff;box-shadow:10px 10px 20px rgba(0,0,0,.5);border-radius:5px;user-select:none}#invidious-instance-container header,#invidious-instance-container footer{background-color:#eee}#invidious-instance-container header{border-radius:5px 5px 0 0;display:grid;grid-template:"title refresh close" auto "rateme rateme rateme"/1fr auto auto;align-items:center;border-bottom:1px solid #ccc}#invidious-instance-container header>span{padding-left:10px}#invidious-instance-container footer{border-radius:0 0 5px 5px;padding:10px;display:grid;grid-template-columns:1fr auto;gap:10px;border-top:1px solid #ccc}#invidious-instance-container footer input{padding:5px;border:1px solid #ccc;border-radius:5px;width:100%}#invidious-instance-container footer button{padding:5px 10px;border:1px solid #ccc;border-radius:5px;cursor:pointer;background-color:#eee}#invidious-instance-container footer button:hover,#invidious-instance-container footer button:focus{background-color:#ddd}#invidious-instance-container footer button:active{background-color:#ccc}#invidious-instance-container table{border-collapse:collapse;margin:3px 0}#invidious-instance-container td{padding:0 10px;cursor:pointer}#invidious-instance-container td a{position:relative;top:1px;color:#888;text-decoration:none;--stroke-width: 8}#invidious-instance-container td a:hover{color:#000;--stroke-width: 12}#invidious-instance-container tr:hover td{background-color:#eee}#invidious-instance-container .input-helper{opacity:0;font-size:12px;position:absolute;background-color:rgba(0,0,0,.8);color:#fff;bottom:20px;left:0;right:0;padding:5px 10px;margin:0 25px;pointer-events:none;border-radius:5px;text-align:center;transition:.5s ease all}#invidious-instance-container .input-helper::after{content:"";position:absolute;top:100%;left:50%;border:solid rgba(0,0,0,0);height:0;width:0;border-top-color:#000;border-width:8px;margin-left:-8px}#invidious-instance-container .input-container{position:relative}#invidious-instance-container .input-container:hover .input-helper{opacity:1;bottom:30px}#invidious-instance-container .refresh,#invidious-instance-container .close{cursor:pointer;color:#000;text-decoration:none;font-size:20px;padding:5px 10px;border-top-right-radius:5px}#invidious-instance-container .refresh:hover,#invidious-instance-container .refresh:focus,#invidious-instance-container .close:hover,#invidious-instance-container .close:focus{font-weight:bold}#invidious-instance-container .refresh:hover,#invidious-instance-container .close:hover{color:#fff;background-color:rgba(255,0,0,.5)}#invidious-instance-container .refresh{border-top-right-radius:0}#invidious-instance-container .refresh:hover{background-color:rgba(0,192,0,.5)}#invidious-instance-container .rateme{justify-self:stretch;grid-area:rateme;display:flex;justify-content:center;background-color:#ddd;padding:5px 10px;color:#000;text-decoration:none}#invidious-instance-container .rateme .emoji{font-variant-emoji:emoji}#invidious-instance-container .rateme:hover,#invidious-instance-container .rateme:focus{font-weight:bold;background-color:#ccc}\n'; // src/videourl.ts function getVideoId(url) { try { const baseUrl = true ? window.location.origin : "https://www.youtube.com"; const urlObj = new URL(url, baseUrl); let videoId = null; if (urlObj.pathname === "/watch") { videoId = urlObj.searchParams.get("v"); } else if (urlObj.pathname.startsWith("/shorts/")) { videoId = urlObj.pathname.slice(8); } else if (urlObj.pathname.startsWith("/live/")) { videoId = urlObj.pathname.slice(6); } else if (urlObj.hostname === "youtu.be") { videoId = urlObj.pathname.slice(1); } if (videoId) { return videoId; } } catch (e) { } const error = new Error(`Unable to parse URL: ${url}`); throw error; } // src/redirect.ts var currentUrl = window.location.href; function tryNavigate(href, replace = true) { try { const url = `${getFullInstanceUrl()}/watch?v=${getVideoId(href)}`; if (replace) { window.location.replace(url); } else { window.location.assign(url); } return true; } catch (e) { } return false; } tryNavigate(window.location.href, true); document.addEventListener("click", (event) => { if (event.target instanceof HTMLElement) { const href = event.target.closest("a")?.getAttribute("href"); if (href && tryNavigate(href, false)) { event.preventDefault(); event.stopPropagation(); } } }, true); setInterval(() => { if (window.location.href !== currentUrl) { currentUrl = window.location.href; tryNavigate(window.location.href, true); } }, 150); // src/main.ts onload(() => { if (true) { let styles = element(`<style>${style_default}</style>`); document.head.appendChild(styles); } const button = element(`<img id="set-invidious-url" src="${button_default}" tabindex="-1">`); button.addEventListener("click", () => showDialog()); document.body.appendChild(button); if (false) { localStorage.clear(); showDialog(); } }); })();