Redirect to Invidious

Redirects YouTube videos to an Invidious instance.

当前为 2024-03-08 提交的版本,查看 最新版本

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

You will need to install an extension such as Tampermonkey to install this script.

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

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

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

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

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

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

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

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

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

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

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

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

// ==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.1
// @match       *://*.youtube.com/
// @match       *://*.youtube.com/*
// @run-at      document-start
// @noframes
// @grant       GM_xmlhttpRequest
// @grant       GM.xmlhttpRequest
// @homepageURL https://greasyfork.org/scripts/477967-redirect-to-invidious
// ==/UserScript==
(() => {
  // select-instance.ts
  function getStyle() {
    const style = document.createElement("style");
    style.textContent = `
        #invidious-instance-container {
            font-family: mono;
            font-size: 12px;
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background-color: rgba(0, 0, 0, 0.5);
            backdrop-filter: blur(5px);
            display: grid;
            place-content: center;
            z-index: 10000;
        }

        #invidious-instance-table {
            background-color: white;
            padding: 10px 15px;
            box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5);
        }

        #invidious-instance-table td {
            padding-left: 10px;
        }

        #invidious-instance-table td:first-child {
            padding-left: 0;
        }

        #invidious-instance-table button {
            font-family: mono;
            font-size: 12px;
            padding: 4px 5px;
            border: none;
            background-color: #007bff;
            color: white;
            cursor: pointer;
        }

        #invidious-instance-table .selected {
            font-weight: bold;
        }
    `;
    return style;
  }
  function addProtocol(uri) {
    if (uri.startsWith("http"))
      return uri;
    else
      return `https://${uri}`;
  }
  async function getInstances() {
    const instancesLastUpdatedKey = "invidious-redirect-instances-last-updated";
    const instancesKey = "invidious-redirect-instances";
    const now = Date.now();
    const lastUpdated = parseInt(localStorage.getItem(instancesLastUpdatedKey) ?? "0");
    if (lastUpdated && now - lastUpdated < 864e5) {
      return JSON.parse(localStorage.getItem(instancesKey));
    } else {
      const instances = await new Promise((resolve) => {
        (GM.xmlHttpRequest || GM_xmlhttpRequest)({
          method: "GET",
          headers: { "Content-Type": "application/json" },
          url: "https://raw.githubusercontent.com/kugland/invidious-redirect/master/instances.json",
          onload: function(response) {
            resolve(JSON.parse(response.responseText));
          }
        });
      });
      localStorage.setItem(instancesKey, JSON.stringify(instances));
      localStorage.setItem(instancesLastUpdatedKey, now.toString());
      return instances;
    }
  }
  async function getTable(current) {
    const instances = await getInstances();
    let sorted = Array.from(Object.keys(instances)).sort((a, b) => instances[a] !== instances[b] ? instances[a].localeCompare(instances[b]) : a.localeCompare(b)).map((uri) => [uri, instances[uri]]);
    return sorted.map(([uri, region]) => [uri, region]).filter(([uri]) => !uri.endsWith(".i2p") && !uri.endsWith(".onion")).map(([uri, region]) => {
      let url = addProtocol(uri);
      uri = uri.replace(/^https?:\/\//, "");
      return `
                <tr data-url="${url}" class=${current == url ? "selected" : ""}>
                    <td><a href="${url}" target="_blank">${uri}</a></td>
                    <td>${region.toLowerCase()}</td>
                    <td><button>select</button></td>
                </tr>
            `;
    }).join("");
  }
  async function showTable(current) {
    const table = document.createElement("div");
    table.id = "invidious-instance-container";
    table.innerHTML = `<table id="invidious-instance-table">${await getTable(current)}</table>`;
    table.appendChild(getStyle());
    document.body.appendChild(table);
    return await new Promise((resolve) => {
      table.querySelectorAll("button").forEach((button) => {
        button.addEventListener("click", (e) => {
          const tr = e.target.closest("tr");
          if (tr) {
            const url = tr.getAttribute("data-url");
            if (url) {
              table.remove();
              resolve(url);
            }
          }
        });
      });
    });
  }

  // invidious-redirect.ts
  var instanceKey = "invidious-redirect-instance";
  var defaultInstance = "https://yewtu.be";
  localStorage.getItem(instanceKey) || localStorage.setItem(instanceKey, defaultInstance);
  function makeUrl(videoId) {
    const instance = localStorage.getItem(instanceKey);
    return new URL(`${instance}/watch?v=${videoId}`).href;
  }
  function getVideoId(href) {
    const url = new URL(href, window.location.href);
    if (url.pathname === "/watch") {
      return url.searchParams.get("v") ?? "";
    } else {
      const videoId = url.pathname.match(/^\/shorts\/([a-zA-Z0-9_-]+)$/)?.[1];
      if (videoId)
        return videoId;
    }
    throw new Error(`Unable to parse URL: ${href}`);
  }
  document.addEventListener("click", (event) => {
    if (event.target instanceof HTMLElement) {
      try {
        const href = event.target.closest("a")?.getAttribute("href");
        if (href) {
          event.preventDefault();
          event.stopPropagation();
          window.location.assign(makeUrl(getVideoId(href)));
        }
      } catch (e) {
      }
    }
  }, true);
  var currentUrl = window.location.href;
  setInterval(() => {
    if (window.location.href !== currentUrl) {
      currentUrl = window.location.href;
      try {
        window.location.replace(makeUrl(getVideoId(currentUrl)));
      } catch (e) {
      }
    }
  }, 150);
  try {
    window.location.replace(makeUrl(getVideoId(currentUrl)));
  } catch (e) {
  }
  ((fn) => {
    if (document.readyState !== "loading")
      fn();
    else
      document.addEventListener("DOMContentLoaded", fn);
  })(() => {
    const css = document.createElement("style");
    css.textContent = "#set-invidious-url:hover { opacity: 1 !important; }";
    document.head.appendChild(css);
    const button = document.createElement("img");
    button.id = "set-invidious-url";
    button.tabIndex = -1;
    button.src = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAJFBMVEXv8e7
        Z4ePn6+kWt/CZzvChpKFrbWrT1dJVV1WJjIm2uLXCxMH33HXYAAAAp0lEQVR4AeXNIQ7CMABG4ceSsXSYIXFVFaCAC5
        BwgblNV4HDkMwiIA0YDMnkDMHWoHY5PPwGSfjsE4+fNbZIyXIBOszR1iu+lICWFmiuRGsOaPURbXOyKINb6FDyR/AoZ
        lefURyNnuwxelKR6YmHVk2yK3qSd+iJKdATB9Be+PAEPakATIi8STzISVaiJ2lET4YFejIBPbmDnEy3ETmZ9REARr3l
        P7wAXHImU2sAU14AAAAASUVORK5CYII=`.replace(/\s/g, "");
    Object.assign(button.style, {
      "position": "fixed",
      "bottom": 0,
      "right": 0,
      "height": "48px",
      "width": "48px",
      "z-index": 99999,
      "margin": "1rem",
      "cursor": "pointer",
      "border-radius": "50%",
      "box-shadow": "0px 0px 3px black",
      "opacity": 0.5
    });
    button.addEventListener("click", async () => {
      const current = localStorage.getItem(instanceKey) ?? defaultInstance;
      let instance = await showTable(current);
      try {
        new URL(instance);
        localStorage.setItem(instanceKey, instance);
      } catch (e) {
        alert(`The URL you entered is invalid: ${instance}`);
      }
    });
    document.body.appendChild(button);
  });
})();