YouTube Anti-Shorts Script (modified)

A YouTube script that replaces shorts links with regular videos. (based on both Anti-Shorts script and shorts redirect)

  1. // ==UserScript==
  2. // @name YouTube Anti-Shorts Script (modified)
  3. // @namespace https://gf.qytechs.cn/en/users/933798
  4. // @version 1.0.1
  5. // @description A YouTube script that replaces shorts links with regular videos. (based on both Anti-Shorts script and shorts redirect)
  6. // @author YukisCoffee and Fuim (Modified by xX_LegendCraftd_Xx)
  7. // @match *://www.youtube.com/*
  8. // @grant none
  9. // @run-at document-start
  10. // @require https://cdnjs.cloudflare.com/ajax/libs/arrive/2.4.1/arrive.min.js
  11. // @license MIT
  12. // ==/UserScript==
  13. // Add shorts redirect code (works on all browsers that lacks JavaScript ES11 support)
  14. var oldHref = document.location.href;
  15. if (window.location.href.indexOf('youtube.com/shorts') > -1) {
  16. window.location.replace(window.location.toString().replace('/shorts/', '/watch?v='));
  17. }
  18. window.onload = function() {
  19. var bodyList = document.querySelector("body")
  20. var observer = new MutationObserver(function(mutations) {
  21. mutations.forEach(function(mutation) {
  22. if (oldHref != document.location.href) {
  23. oldHref = document.location.href;
  24. console.log('location changed!');
  25. if (window.location.href.indexOf('youtube.com/shorts') > -1) {
  26. window.location.replace(window.location.toString().replace('/shorts/', '/watch?v='));
  27. }
  28. }
  29. });
  30. });
  31. var config = {
  32. childList: true,
  33. subtree: true
  34. };
  35. observer.observe(bodyList, config);
  36. };
  37.  
  38. // Fully replace shorts links with regular videos
  39. /**
  40. * Shorts URL redirect.
  41. *
  42. * This is called on initial visit only. Successive navigations
  43. * are managed by modifying the YouTube Desktop application.
  44. */
  45. (function(){
  46.  
  47. /** @type {string} */
  48. var path = window.location.pathname;
  49.  
  50. if (0 == path.search("/shorts"))
  51. {
  52. // Extract the video ID from the shorts link and redirect.
  53.  
  54. /** @type {string} */
  55. var id = path.replace(/\/|shorts|\?.*/g, "");
  56.  
  57. window.location.replace("https://www.youtube.com/watch?v=" + id);
  58. }
  59.  
  60. })();
  61.  
  62. /**
  63. * YouTube Desktop Shorts remover.
  64. *
  65. * If the initial URL was not a shorts link, traditional redirection
  66. * will not work. This instead modifies video elements to replace them with
  67. * regular links.
  68. */
  69. (function(){
  70.  
  71. /**
  72. * @param {string} selector (CSS-style) of the element
  73. * @return {Promise<Element>}
  74. */
  75. async function querySelectorAsync(selector)
  76. {
  77. while (null == document.querySelector(selector))
  78. {
  79. // Pause for a frame and let other code go on.
  80. await new Promise(r => requestAnimationFrame(r));
  81. }
  82.  
  83. return document.querySelector(selector);
  84. }
  85.  
  86. /**
  87. * Small toolset for interacting with the Polymer
  88. * YouTube Desktop application.
  89. *
  90. * @author Taniko Yamamoto <kirasicecreamm@gmail.com>
  91. * @version 1.0
  92. */
  93. class YtdTools
  94. {
  95. /** @type {string} Page data updated event */
  96. static EVT_DATA_UPDATE = "yt-page-data-updated";
  97.  
  98. /** @type {Element} Main YT Polymer manager */
  99. static YtdApp;
  100.  
  101. /** @type {bool} */
  102. static hasInitialLoaded = false;
  103.  
  104. /** @return {Promise<bool>} */
  105. static async isPolymer()
  106. {
  107. /** @return {Promise<void>} */
  108. function waitForBody() // nice hack lazy ass
  109. {
  110. return new Promise(r => {
  111. document.addEventListener("DOMContentLoaded", function a(){
  112. document.removeEventListener("DOMContentLoaded", a);
  113. r();
  114. });
  115. });
  116. }
  117.  
  118. await waitForBody();
  119.  
  120. if ("undefined" != typeof document.querySelector("ytd-app"))
  121. {
  122. this.YtdApp = document.querySelector("ytd-app");
  123. return true;
  124. }
  125. return false;
  126. }
  127.  
  128. /** @async @return {Promise<void|string>} */
  129. static waitForInitialLoad()
  130. {
  131. var updateEvent = this.EVT_DATA_UPDATE;
  132. return new Promise((resolve, reject) => {
  133. if (!this.isPolymer())
  134. {
  135. reject("Not Polymer :(");
  136. }
  137.  
  138. function _listenerCb()
  139. {
  140. document.removeEventListener(updateEvent, _listenerCb);
  141. resolve();
  142. }
  143.  
  144. document.addEventListener(updateEvent, _listenerCb);
  145. });
  146. }
  147.  
  148. /** @return {string} */
  149. static getPageType()
  150. {
  151. return this.YtdApp.data.page;
  152. }
  153. }
  154.  
  155. class ShortsTools
  156. {
  157. /** @type {MutationObserver} */
  158. static mo = new MutationObserver(muts => {
  159. muts.forEach(mut => {
  160. Array.from(mut.addedNodes).forEach(node => {
  161. if (node instanceof HTMLElement) {
  162. this.onMutation(node);
  163. }
  164. });
  165. });
  166. });
  167.  
  168. /** @return {void} */
  169. static watchForShorts()
  170. {
  171. /*
  172. this.mo.observe(YtdTools.YtdApp, {
  173. childList: true,
  174. subtree: true
  175. });
  176. */
  177. var me = this;
  178. YtdTools.YtdApp.arrive("ytd-video-renderer, ytd-grid-video-renderer", function() {
  179. me.onMutation(this);
  180.  
  181. // This is literally the worst hack I ever wrote, but it works ig...
  182. (new MutationObserver(function(){
  183. if (me.isShortsRenderer(this))
  184. {
  185. me.onMutation(this);
  186. }
  187. }.bind(this))).observe(this, {"subtree": true, "childList": true, "characterData": "true"});
  188. });
  189. }
  190.  
  191. /** @return {void} */
  192. static stopWatchingForShorts()
  193. {
  194. this.mo.disconnect();
  195. }
  196.  
  197. /**
  198. * @param {HTMLElement} node
  199. * @return {void}
  200. */
  201. static onMutation(node)
  202. {
  203. if (node.tagName.search("VIDEO-RENDERER") > -1 && this.isShortsRenderer(node))
  204. {
  205. this.transformShortsRenderer(node);
  206. }
  207. }
  208.  
  209. /** @return {bool} */
  210. static isShortsRenderer(videoRenderer)
  211. {
  212. return "WEB_PAGE_TYPE_SHORTS" == videoRenderer?.data?.navigationEndpoint?.commandMetadata?.webCommandMetadata?.webPageType;
  213. }
  214.  
  215. /** @return {string} */
  216. static extractLengthFromA11y(videoData)
  217. {
  218. // A11y = {title} by {creator} {date} {*length*} {viewCount} - play Short
  219. // tho hopefully this works in more than just English
  220. var a11yTitle = videoData.title.accessibility.accessibilityData.label;
  221.  
  222. var publishedTimeText = videoData.publishedTimeText.simpleText;
  223. var viewCountText = videoData.viewCountText.simpleText;
  224.  
  225. var isolatedLengthStr = a11yTitle.split(publishedTimeText)[1].split(viewCountText)[0]
  226. .replace(/\s/g, "");
  227.  
  228. var numbers = isolatedLengthStr.split(/\D/g);
  229.  
  230. var string = "";
  231.  
  232. // Remove all empties before iterating it
  233. for (var i = 0; i < numbers.length; i++)
  234. {
  235. if ("" === numbers[i])
  236. {
  237. numbers.splice(i, 1);
  238. i--;
  239. }
  240. }
  241.  
  242. for (var i = 0; i < numbers.length; i++)
  243. {
  244. // Lazy 0 handling idc im tired
  245. if (1 == numbers.length)
  246. {
  247. string += "0:";
  248. if (1 == numbers[i].length)
  249. {
  250. string += "0" + numbers[i];
  251. }
  252. else
  253. {
  254. string += numbers[i];
  255. }
  256.  
  257. break;
  258. }
  259.  
  260. if (0 != i) string += ":";
  261. if (0 != i && 1 == numbers[i].length) string += "0";
  262. string += numbers[i];
  263. }
  264.  
  265. return string;
  266. }
  267.  
  268. /**
  269. * @param {HTMLElement} videoRenderer
  270. * @return {void}
  271. */
  272. static transformShortsRenderer(videoRenderer)
  273. {
  274.  
  275. /** @type {string} */
  276. var originalOuterHTML = videoRenderer.outerHTML;
  277.  
  278. /** @type {string} */
  279. var lengthText = videoRenderer.data?.lengthText?.simpleText ?? this.extractLengthFromA11y(videoRenderer.data);
  280.  
  281. /** @type {string} */
  282. var lengthA11y = videoRenderer.data?.lengthText?.accessibility?.accessibilityData?.label ?? "";
  283.  
  284. /** @type {string} */
  285. var originalHref = videoRenderer.data.navigationEndpoint.commandMetadata.webCommandMetadata.url;
  286. var href = "/watch?v=" + originalHref.replace(/\/|shorts|\?.*/g, "");
  287.  
  288. var reelWatchEndpoint = videoRenderer.data.navigationEndpoint.reelWatchEndpoint;
  289.  
  290. var i;
  291. videoRenderer.data.thumbnailOverlays.forEach((a, index) =>{
  292. if ("thumbnailOverlayTimeStatusRenderer" in a)
  293. {
  294. i = index;
  295. }
  296. });
  297.  
  298. // Set the thumbnail overlay style
  299. videoRenderer.data.thumbnailOverlays[i].thumbnailOverlayTimeStatusRenderer.style = "DEFAULT";
  300.  
  301. delete videoRenderer.data.thumbnailOverlays[i].thumbnailOverlayTimeStatusRenderer.icon;
  302.  
  303. // Set the thumbnail overlay text
  304. videoRenderer.data.thumbnailOverlays[i].thumbnailOverlayTimeStatusRenderer.text.simpleText = lengthText;
  305.  
  306. // Set the thumbnail overlay accessibility label
  307. videoRenderer.data.thumbnailOverlays[i].thumbnailOverlayTimeStatusRenderer.text.accessibility.accessibilityData.label = lengthA11y;
  308.  
  309. // Set the navigation endpoint metadata (used for middle click)
  310. videoRenderer.data.navigationEndpoint.commandMetadata.webCommandMetadata.webPageType = "WEB_PAGE_TYPE_WATCH";
  311. videoRenderer.data.navigationEndpoint.commandMetadata.webCommandMetadata.url = href;
  312.  
  313. videoRenderer.data.navigationEndpoint.watchEndpoint = {
  314. "videoId": reelWatchEndpoint.videoId,
  315. "playerParams": reelWatchEndpoint.playerParams,
  316. "params": reelWatchEndpoint.params
  317. };
  318. delete videoRenderer.data.navigationEndpoint.reelWatchEndpoint;
  319.  
  320. //var _ = videoRenderer.data; videoRenderer.data = {}; videoRenderer.data = _;
  321.  
  322. // Sometimes the old school data cycle trick fails,
  323. // however this always works.
  324. var _ = videoRenderer.cloneNode();
  325. _.data = videoRenderer.data;
  326. for (var i in videoRenderer.properties)
  327. {
  328. _[i] = videoRenderer[i];
  329. }
  330. videoRenderer.insertAdjacentElement("afterend", _);
  331. videoRenderer.remove();
  332. }
  333. }
  334.  
  335. /**
  336. * Sometimes elements are reused on page updates, so fix that
  337. *
  338. * @return {void}
  339. */
  340. function onDataUpdate()
  341. {
  342. var videos = document.querySelectorAll("ytd-video-renderer, ytd-grid-video-renderer");
  343.  
  344. for (var i = 0, l = videos.length; i < l; i++) if (ShortsTools.isShortsRenderer(videos[i]))
  345. {
  346. ShortsTools.transformShortsRenderer(videos[i]);
  347. }
  348. }
  349.  
  350. /**
  351. * I hope she makes lotsa spaghetti :D
  352. * @async @return {Promise<void>}
  353. */
  354. async function main()
  355. {
  356. // If not Polymer, nothing happens
  357. if (await YtdTools.isPolymer())
  358. {
  359. ShortsTools.watchForShorts();
  360.  
  361. document.addEventListener("yt-page-data-updated", onDataUpdate);
  362. }
  363. }
  364.  
  365. main();
  366.  
  367. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址