Redirect to Invidious

Redirects YouTube videos to an Invidious instance.

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

  1. // ==UserScript==
  2. // @name Redirect to Invidious
  3. // @author André Kugland
  4. // @description Redirects YouTube videos to an Invidious instance.
  5. // @namespace https://github.com/kugland
  6. // @license MIT
  7. // @version 0.3.1
  8. // @match *://*.youtube.com/
  9. // @match *://*.youtube.com/*
  10. // @run-at document-start
  11. // @noframes
  12. // @grant GM_xmlhttpRequest
  13. // @grant GM.xmlhttpRequest
  14. // @homepageURL https://greasyfork.org/scripts/477967-redirect-to-invidious
  15. // ==/UserScript==
  16. (() => {
  17. // select-instance.ts
  18. function getStyle() {
  19. const style = document.createElement("style");
  20. style.textContent = `
  21. #invidious-instance-container {
  22. font-family: mono;
  23. font-size: 12px;
  24. position: fixed;
  25. top: 0;
  26. left: 0;
  27. right: 0;
  28. bottom: 0;
  29. background-color: rgba(0, 0, 0, 0.5);
  30. backdrop-filter: blur(5px);
  31. display: grid;
  32. place-content: center;
  33. z-index: 10000;
  34. }
  35.  
  36. #invidious-instance-table {
  37. background-color: white;
  38. padding: 10px 15px;
  39. box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5);
  40. }
  41.  
  42. #invidious-instance-table td {
  43. padding-left: 10px;
  44. }
  45.  
  46. #invidious-instance-table td:first-child {
  47. padding-left: 0;
  48. }
  49.  
  50. #invidious-instance-table button {
  51. font-family: mono;
  52. font-size: 12px;
  53. padding: 4px 5px;
  54. border: none;
  55. background-color: #007bff;
  56. color: white;
  57. cursor: pointer;
  58. }
  59.  
  60. #invidious-instance-table .selected {
  61. font-weight: bold;
  62. }
  63. `;
  64. return style;
  65. }
  66. function addProtocol(uri) {
  67. if (uri.startsWith("http"))
  68. return uri;
  69. else
  70. return `https://${uri}`;
  71. }
  72. async function getInstances() {
  73. const instancesLastUpdatedKey = "invidious-redirect-instances-last-updated";
  74. const instancesKey = "invidious-redirect-instances";
  75. const now = Date.now();
  76. const lastUpdated = parseInt(localStorage.getItem(instancesLastUpdatedKey) ?? "0");
  77. if (lastUpdated && now - lastUpdated < 864e5) {
  78. return JSON.parse(localStorage.getItem(instancesKey));
  79. } else {
  80. const instances = await new Promise((resolve) => {
  81. (GM.xmlHttpRequest || GM_xmlhttpRequest)({
  82. method: "GET",
  83. headers: { "Content-Type": "application/json" },
  84. url: "https://raw.githubusercontent.com/kugland/invidious-redirect/master/instances.json",
  85. onload: function(response) {
  86. resolve(JSON.parse(response.responseText));
  87. }
  88. });
  89. });
  90. localStorage.setItem(instancesKey, JSON.stringify(instances));
  91. localStorage.setItem(instancesLastUpdatedKey, now.toString());
  92. return instances;
  93. }
  94. }
  95. async function getTable(current) {
  96. const instances = await getInstances();
  97. 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]]);
  98. return sorted.map(([uri, region]) => [uri, region]).filter(([uri]) => !uri.endsWith(".i2p") && !uri.endsWith(".onion")).map(([uri, region]) => {
  99. let url = addProtocol(uri);
  100. uri = uri.replace(/^https?:\/\//, "");
  101. return `
  102. <tr data-url="${url}" class=${current == url ? "selected" : ""}>
  103. <td><a href="${url}" target="_blank">${uri}</a></td>
  104. <td>${region.toLowerCase()}</td>
  105. <td><button>select</button></td>
  106. </tr>
  107. `;
  108. }).join("");
  109. }
  110. async function showTable(current) {
  111. const table = document.createElement("div");
  112. table.id = "invidious-instance-container";
  113. table.innerHTML = `<table id="invidious-instance-table">${await getTable(current)}</table>`;
  114. table.appendChild(getStyle());
  115. document.body.appendChild(table);
  116. return await new Promise((resolve) => {
  117. table.querySelectorAll("button").forEach((button) => {
  118. button.addEventListener("click", (e) => {
  119. const tr = e.target.closest("tr");
  120. if (tr) {
  121. const url = tr.getAttribute("data-url");
  122. if (url) {
  123. table.remove();
  124. resolve(url);
  125. }
  126. }
  127. });
  128. });
  129. });
  130. }
  131.  
  132. // invidious-redirect.ts
  133. var instanceKey = "invidious-redirect-instance";
  134. var defaultInstance = "https://yewtu.be";
  135. localStorage.getItem(instanceKey) || localStorage.setItem(instanceKey, defaultInstance);
  136. function makeUrl(videoId) {
  137. const instance = localStorage.getItem(instanceKey);
  138. return new URL(`${instance}/watch?v=${videoId}`).href;
  139. }
  140. function getVideoId(href) {
  141. const url = new URL(href, window.location.href);
  142. if (url.pathname === "/watch") {
  143. return url.searchParams.get("v") ?? "";
  144. } else {
  145. const videoId = url.pathname.match(/^\/shorts\/([a-zA-Z0-9_-]+)$/)?.[1];
  146. if (videoId)
  147. return videoId;
  148. }
  149. throw new Error(`Unable to parse URL: ${href}`);
  150. }
  151. document.addEventListener("click", (event) => {
  152. if (event.target instanceof HTMLElement) {
  153. try {
  154. const href = event.target.closest("a")?.getAttribute("href");
  155. if (href) {
  156. event.preventDefault();
  157. event.stopPropagation();
  158. window.location.assign(makeUrl(getVideoId(href)));
  159. }
  160. } catch (e) {
  161. }
  162. }
  163. }, true);
  164. var currentUrl = window.location.href;
  165. setInterval(() => {
  166. if (window.location.href !== currentUrl) {
  167. currentUrl = window.location.href;
  168. try {
  169. window.location.replace(makeUrl(getVideoId(currentUrl)));
  170. } catch (e) {
  171. }
  172. }
  173. }, 150);
  174. try {
  175. window.location.replace(makeUrl(getVideoId(currentUrl)));
  176. } catch (e) {
  177. }
  178. ((fn) => {
  179. if (document.readyState !== "loading")
  180. fn();
  181. else
  182. document.addEventListener("DOMContentLoaded", fn);
  183. })(() => {
  184. const css = document.createElement("style");
  185. css.textContent = "#set-invidious-url:hover { opacity: 1 !important; }";
  186. document.head.appendChild(css);
  187. const button = document.createElement("img");
  188. button.id = "set-invidious-url";
  189. button.tabIndex = -1;
  190. button.src = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAJFBMVEXv8e7
  191. Z4ePn6+kWt/CZzvChpKFrbWrT1dJVV1WJjIm2uLXCxMH33HXYAAAAp0lEQVR4AeXNIQ7CMABG4ceSsXSYIXFVFaCAC5
  192. BwgblNV4HDkMwiIA0YDMnkDMHWoHY5PPwGSfjsE4+fNbZIyXIBOszR1iu+lICWFmiuRGsOaPURbXOyKINb6FDyR/AoZ
  193. lefURyNnuwxelKR6YmHVk2yK3qSd+iJKdATB9Be+PAEPakATIi8STzISVaiJ2lET4YFejIBPbmDnEy3ETmZ9REARr3l
  194. P7wAXHImU2sAU14AAAAASUVORK5CYII=`.replace(/\s/g, "");
  195. Object.assign(button.style, {
  196. "position": "fixed",
  197. "bottom": 0,
  198. "right": 0,
  199. "height": "48px",
  200. "width": "48px",
  201. "z-index": 99999,
  202. "margin": "1rem",
  203. "cursor": "pointer",
  204. "border-radius": "50%",
  205. "box-shadow": "0px 0px 3px black",
  206. "opacity": 0.5
  207. });
  208. button.addEventListener("click", async () => {
  209. const current = localStorage.getItem(instanceKey) ?? defaultInstance;
  210. let instance = await showTable(current);
  211. try {
  212. new URL(instance);
  213. localStorage.setItem(instanceKey, instance);
  214. } catch (e) {
  215. alert(`The URL you entered is invalid: ${instance}`);
  216. }
  217. });
  218. document.body.appendChild(button);
  219. });
  220. })();