Redirect to Invidious

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.4
// @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.lunar.icu";
  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();
    }
  });
})();