Redirect to Invidious

Redirects YouTube videos to an Invidious instance.

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

  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.0
  8. // @match *://*.youtube.com/
  9. // @match *://*.youtube.com/*
  10. // @run-at document-start
  11. // @noframes
  12. // @homepageURL https://greasyfork.org/scripts/477967-redirect-to-invidious
  13. // ==/UserScript==
  14. (() => {
  15. // instances.ts
  16. var instances_default = {
  17. "grwp24hodrefzvjjuccrkw3mjq4tzhaaq32amf33dzpmuxe7ilepcmad.onion": "US \u{1F1FA}\u{1F1F8}",
  18. "http://pjsfhqamc7k6htnumrvn4cwqqdoggeepj7u5viyimgnxg3gar72q.b32.i2p": "FR \u{1F1EB}\u{1F1F7}",
  19. "http://pjsfi2szfkb4guqzmfmlyq4no46fayertjrwt4h2uughccrh2lvq.b32.i2p": "LU \u{1F1F1}\u{1F1FA}",
  20. "inv.n8pjl.ca": "CA \u{1F1E8}\u{1F1E6}",
  21. "inv.nadeko.net": "CL \u{1F1E8}\u{1F1F1}",
  22. "inv.nadekonw7plitnjuawu6ytjsl7jlglk2t6pyq6eftptmiv3dvqndwvyd.onion": "CL \u{1F1E8}\u{1F1F1}",
  23. "inv.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion": "FR \u{1F1EB}\u{1F1F7}",
  24. "inv.tux.pizza": "US \u{1F1FA}\u{1F1F8}",
  25. "inv.us.projectsegfau.lt": "US \u{1F1FA}\u{1F1F8}",
  26. "invidious.drgns.space": "US \u{1F1FA}\u{1F1F8}",
  27. "invidious.einfachzocken.eu": "DE \u{1F1E9}\u{1F1EA}",
  28. "invidious.fdn.fr": "FR \u{1F1EB}\u{1F1F7}",
  29. "invidious.flokinet.to": "RO \u{1F1F7}\u{1F1F4}",
  30. "invidious.g4c3eya4clenolymqbpgwz3q3tawoxw56yhzk4vugqrl6dtu3ejvhjid.onion": "FR \u{1F1EB}\u{1F1F7}",
  31. "invidious.jing.rocks": "JP \u{1F1EF}\u{1F1F5}",
  32. "invidious.lunar.icu": "DE \u{1F1E9}\u{1F1EA}",
  33. "invidious.nerdvpn.de": "DE \u{1F1E9}\u{1F1EA}",
  34. "invidious.perennialte.ch": "AU \u{1F1E6}\u{1F1FA}",
  35. "invidious.privacydev.net": "FR \u{1F1EB}\u{1F1F7}",
  36. "invidious.private.coffee": "AT \u{1F1E6}\u{1F1F9}",
  37. "invidious.projectsegfau.lt": "FR \u{1F1EB}\u{1F1F7}",
  38. "invidious.protokolla.fi": "DE \u{1F1E9}\u{1F1EA}",
  39. "iv.datura.network": "DE \u{1F1E9}\u{1F1EA}",
  40. "iv.ggtyler.dev": "US \u{1F1FA}\u{1F1F8}",
  41. "iv.melmac.space": "DE \u{1F1E9}\u{1F1EA}",
  42. "iv.nboeck.de": "FI \u{1F1EB}\u{1F1EE}",
  43. "jemgkaq2xibfu37hm2xojsxoi7djtwb25w6krhl63lhn52xfzgeyc2ad.onion": "DE \u{1F1E9}\u{1F1EA}",
  44. "ng27owmagn5amdm7l5s3rsqxwscl5ynppnis5dqcasogkyxcfqn7psid.onion": "DE \u{1F1E9}\u{1F1EA}",
  45. "vid.puffyan.us": "US \u{1F1FA}\u{1F1F8}",
  46. "yewtu.be": "DE \u{1F1E9}\u{1F1EA}",
  47. "youtube.owacon.moe": "JP \u{1F1EF}\u{1F1F5}",
  48. "yt.artemislena.eu": "DE \u{1F1E9}\u{1F1EA}",
  49. "yt.cdaut.de": "DE \u{1F1E9}\u{1F1EA}",
  50. "yt.drgnz.club": "CZ \u{1F1E8}\u{1F1FF}",
  51. "zzlsbhhfvwg3oh36tcvx4r7n6jrw7zibvyvfxqlodcwn3mfrvzuq.b32.i2p": "CL \u{1F1E8}\u{1F1F1}"
  52. };
  53.  
  54. // select-instance.ts
  55. function getStyle() {
  56. const style = document.createElement("style");
  57. style.textContent = `
  58. #invidious-instance-container {
  59. font-family: mono;
  60. font-size: 12px;
  61. position: fixed;
  62. top: 0;
  63. left: 0;
  64. right: 0;
  65. bottom: 0;
  66. background-color: rgba(0, 0, 0, 0.5);
  67. backdrop-filter: blur(5px);
  68. display: grid;
  69. place-content: center;
  70. z-index: 10000;
  71. }
  72.  
  73. #invidious-instance-table {
  74. background-color: white;
  75. padding: 10px 15px;
  76. box-shadow: 2px 2px 10px rgba(0, 0, 0, 0.5);
  77. }
  78.  
  79. #invidious-instance-table td {
  80. padding-left: 10px;
  81. }
  82.  
  83. #invidious-instance-table td:first-child {
  84. padding-left: 0;
  85. }
  86.  
  87. #invidious-instance-table button {
  88. font-family: mono;
  89. font-size: 12px;
  90. padding: 4px 5px;
  91. border: none;
  92. background-color: #007bff;
  93. color: white;
  94. cursor: pointer;
  95. }
  96.  
  97. #invidious-instance-table .selected {
  98. font-weight: bold;
  99. }
  100. `;
  101. return style;
  102. }
  103. function addProtocol(uri) {
  104. if (uri.startsWith("http"))
  105. return uri;
  106. else
  107. return `https://${uri}`;
  108. }
  109. function getTable(current) {
  110. let sorted = Array.from(Object.keys(instances_default)).sort((a, b) => {
  111. const region_a = instances_default[a];
  112. const region_b = instances_default[b];
  113. if (region_a !== region_b)
  114. return region_a.localeCompare(region_b);
  115. else
  116. return a.localeCompare(b);
  117. }).map((uri) => [uri, instances_default[uri]]);
  118. return sorted.map(([uri, region]) => [uri, region]).filter(([uri]) => !uri.endsWith(".i2p") && !uri.endsWith(".onion")).map(([uri, region]) => {
  119. let url = addProtocol(uri);
  120. uri = uri.replace(/^https?:\/\//, "");
  121. return `
  122. <tr data-url="${url}" class=${current == url ? "selected" : ""}>
  123. <td><a href="${url}" target="_blank">${uri}</a></td>
  124. <td>${region.toLowerCase()}</td>
  125. <td><button>select</button></td>
  126. </tr>
  127. `;
  128. }).join("");
  129. }
  130. function showTable(current) {
  131. const table = document.createElement("div");
  132. table.id = "invidious-instance-container";
  133. table.innerHTML = `<table id="invidious-instance-table">${getTable(current)}</table>`;
  134. table.appendChild(getStyle());
  135. document.body.appendChild(table);
  136. return new Promise((resolve) => {
  137. table.querySelectorAll("button").forEach((button) => {
  138. button.addEventListener("click", (e) => {
  139. const tr = e.target.closest("tr");
  140. if (tr) {
  141. const url = tr.getAttribute("data-url");
  142. if (url) {
  143. table.remove();
  144. resolve(url);
  145. }
  146. }
  147. });
  148. });
  149. });
  150. }
  151.  
  152. // invidious-redirect.ts
  153. var instanceKey = "invidious-redirect-instance";
  154. var defaultInstance = "https://yewtu.be";
  155. localStorage.getItem(instanceKey) || localStorage.setItem(instanceKey, defaultInstance);
  156. function makeUrl(videoId) {
  157. const instance = localStorage.getItem(instanceKey);
  158. return new URL(`${instance}/watch?v=${videoId}`).href;
  159. }
  160. function getVideoId(href) {
  161. const url = new URL(href, window.location.href);
  162. if (url.pathname === "/watch") {
  163. return url.searchParams.get("v");
  164. } else {
  165. const videoId = url.pathname.match(/^\/shorts\/([a-zA-Z0-9_-]+)$/)?.[1];
  166. if (videoId)
  167. return videoId;
  168. }
  169. throw new Error(`Unable to parse URL: ${href}`);
  170. }
  171. document.addEventListener("click", (event) => {
  172. if (event.target instanceof HTMLElement) {
  173. try {
  174. const href = event.target.closest("a")?.getAttribute("href");
  175. if (href) {
  176. event.preventDefault();
  177. event.stopPropagation();
  178. window.location.assign(makeUrl(getVideoId(href)));
  179. }
  180. } catch (e) {
  181. }
  182. }
  183. }, true);
  184. var currentUrl = window.location.href;
  185. setInterval(() => {
  186. if (window.location.href !== currentUrl) {
  187. currentUrl = window.location.href;
  188. try {
  189. window.location.replace(makeUrl(getVideoId(currentUrl)));
  190. } catch (e) {
  191. }
  192. }
  193. }, 150);
  194. try {
  195. window.location.replace(makeUrl(getVideoId(currentUrl)));
  196. } catch (e) {
  197. }
  198. ((fn) => {
  199. if (document.readyState !== "loading")
  200. fn();
  201. else
  202. document.addEventListener("DOMContentLoaded", fn);
  203. })(() => {
  204. const css = document.createElement("style");
  205. css.textContent = "#set-invidious-url:hover { opacity: 1 !important; }";
  206. document.head.appendChild(css);
  207. const button = document.createElement("img");
  208. button.id = "set-invidious-url";
  209. button.tabIndex = -1;
  210. button.src = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAJFBMVEXv8e7
  211. Z4ePn6+kWt/CZzvChpKFrbWrT1dJVV1WJjIm2uLXCxMH33HXYAAAAp0lEQVR4AeXNIQ7CMABG4ceSsXSYIXFVFaCAC5
  212. BwgblNV4HDkMwiIA0YDMnkDMHWoHY5PPwGSfjsE4+fNbZIyXIBOszR1iu+lICWFmiuRGsOaPURbXOyKINb6FDyR/AoZ
  213. lefURyNnuwxelKR6YmHVk2yK3qSd+iJKdATB9Be+PAEPakATIi8STzISVaiJ2lET4YFejIBPbmDnEy3ETmZ9REARr3l
  214. P7wAXHImU2sAU14AAAAASUVORK5CYII=`.replace(/\s/g, "");
  215. Object.assign(button.style, {
  216. "position": "fixed",
  217. "bottom": 0,
  218. "right": 0,
  219. "height": "48px",
  220. "width": "48px",
  221. "z-index": 99999,
  222. "margin": "1rem",
  223. "cursor": "pointer",
  224. "border-radius": "50%",
  225. "box-shadow": "0px 0px 3px black",
  226. "opacity": 0.5
  227. });
  228. button.addEventListener("click", async () => {
  229. const current = localStorage.getItem(instanceKey) ?? defaultInstance;
  230. let instance = await showTable(current);
  231. try {
  232. new URL(instance);
  233. localStorage.setItem(instanceKey, instance);
  234. } catch (e) {
  235. alert(`The URL you entered is invalid: ${instance}`);
  236. }
  237. });
  238. document.body.appendChild(button);
  239. });
  240. })();