Youtube - Fix channel links in sidebar recommendations

Fixes the channel links for the "Up next" and recommended videos below it on youtube.

  1. // ==UserScript==
  2. // @name Youtube - Fix channel links in sidebar recommendations
  3. // @namespace 1N07
  4. // @version 0.9
  5. // @description Fixes the channel links for the "Up next" and recommended videos below it on youtube.
  6. // @author 1N07
  7. // @license Unlicense
  8. // @icon https://www.google.com/s2/favicons?domain=youtube.com
  9. // @match https://www.youtube.com/*
  10. // @require https://update.greatest.deepsurf.us/scripts/12036/70722/Mutation%20Summary.js
  11. // @grant GM_registerMenuCommand
  12. // @grant GM_unregisterMenuCommand
  13. // @grant GM_getValue
  14. // @grant GM_setValue
  15. // @grant GM_addStyle
  16. // @compatible firefox v0.9 tested on Firefox v138.0 using Tampermonkey v5.3.3
  17. // @compatible chrome v0.9 tested on Chrome v135.0.7049.115 using Tampermonkey v5.3.3
  18. // @compatible opera Opera untested, but likely works with at least Tampermonkey
  19. // @compatible edge Edge untested, but likely works with at least Tampermonkey
  20. // @compatible safari Safari untested, but likely works with at least Tampermonkey
  21. // ==/UserScript==
  22.  
  23. (() => {
  24. console.log("%cSCRIPT START", "color: green;");
  25. let videoSectionOption;
  26. let videoSection = GM_getValue("videoSection", true);
  27. SetVidSecOption();
  28.  
  29. GM_addStyle(`
  30. ytd-compact-video-renderer .channel-link-blocker:hover ~ a #text.ytd-channel-name {
  31. text-decoration: underline;
  32. }
  33. .channel-link-blocker-parent
  34. {
  35. position: relative;
  36. }
  37. .channel-link-blocker
  38. {
  39. display: inline-block;
  40. position: absolute;
  41. width: 100%;
  42. height: 25px;
  43. background-color: rgba(255, 25, 25, 0);
  44. top: 32px;
  45. left: 0;
  46. z-index: 2019;
  47. }
  48. `);
  49.  
  50. //"Block Youtube Users" compatibility
  51. let byuBlockerStyleAdjustment;
  52. let byuObserver = new MutationSummary({
  53. callback: (summary) => {
  54. console.log(
  55. "%cBlock Youtube Users detected, applying compatibility feature",
  56. "color: green;",
  57. );
  58. summary[0].added[0].addEventListener("click", () => {
  59. setTimeout(() => {
  60. for (const blocker of document.getElementsByClassName("channel-link-blocker")) {
  61. UpdateBlockerSizeAndPositioning(blocker);
  62. }
  63. }, 200);
  64. });
  65. if (byuObserver) {
  66. byuObserver.disconnect();
  67. byuObserver = null;
  68. }
  69. },
  70. queries: [{ element: "#byu-icon" }],
  71. });
  72. setTimeout(() => {
  73. if (byuObserver) {
  74. //console.log("%cBlock Youtube Users not detected", "color: green;");
  75. byuObserver.disconnect();
  76. byuObserver = null;
  77. }
  78. }, 10000);
  79.  
  80. const perVideoObservers = [];
  81. let perVideoObserverIndexTally = 0;
  82. const containerObserver = new MutationSummary({
  83. callback: (containerSummary) => {
  84. console.log(
  85. `%cContainer Observer triggered - Added: ${containerSummary[0].added.length}, Removed: ${containerSummary[0].removed.length}, Reparented: ${containerSummary[0].reparented.length}`,
  86. "color: green",
  87. );
  88.  
  89. // On video added
  90. for (const vid of containerSummary[0].added) {
  91. // Add blocker element
  92. const blockerParent = vid.querySelector(
  93. ".metadata.ytd-compact-video-renderer",
  94. );
  95. blockerParent.classList.add("channel-link-blocker-parent");
  96.  
  97. const blockerElem = document.createElement("a");
  98. blockerElem.className = "channel-link-blocker";
  99. blockerElem.href = "#";
  100. blockerParent.prepend(blockerElem);
  101.  
  102. const channelLink = blockerParent.querySelector(
  103. ".channel-link-blocker",
  104. );
  105.  
  106. UpdateBlockerSizeAndPositioning(channelLink);
  107. UpdateUrl(vid, channelLink);
  108.  
  109. // Add observer id to element so we can clean up the right observer when the element is later removed
  110. vid.setAttribute("data-active-observer-id", perVideoObserverIndexTally);
  111.  
  112. // Add per-video observer for when the video href changes, so we can update the channel link accordingly. Doing this because apparently these days YT just swaps the data in the elements without swapping the elements themselves.
  113. // Also put the observer in an array with an access key for later access
  114. perVideoObservers.push({
  115. key: perVideoObserverIndexTally,
  116. observer: new MutationSummary({
  117. callback: (vidSummary) => {
  118. // console.log("%cPer Video Observer triggered: href changed", "color: green");
  119.  
  120. UpdateBlockerSizeAndPositioning(channelLink);
  121. UpdateUrl(vid, channelLink);
  122. },
  123. rootNode: blockerParent.querySelector("a[href^='/watch']"),
  124. queries: [{ attribute: "href" }],
  125. }),
  126. });
  127. perVideoObserverIndexTally++;
  128. }
  129.  
  130. // On removed
  131. for (const vid of containerSummary[0].removed) {
  132. // Get the observer id/key we stored in the element previously
  133. const id = vid.dataset.activeObserverId;
  134. // console.log("%cAttempting to remove observer: " + id, "color: red");
  135. if (id !== undefined) {
  136. // console.log("id valid");
  137. // Get the observer from the observer array with the key
  138. const index = perVideoObservers.findIndex((o) => o.key === id);
  139. if (index > -1) {
  140. // console.log("observer found");
  141. // Disconnect the observer and remove it from the array
  142. perVideoObservers[index].observer.disconnect();
  143. perVideoObservers.splice(index, 1);
  144. // console.log("%cRemoved observer: " + id, "color: red");
  145. }
  146. }
  147. }
  148.  
  149. //console.log("%cObservers alive: ", "color: yellow");
  150. //console.log(perVideoObservers.map(x => x.key));
  151. },
  152. queries: [
  153. {
  154. element:
  155. "ytd-compact-video-renderer.ytd-item-section-renderer, ytd-compact-video-renderer.ytd-watch-next-secondary-results-renderer",
  156. },
  157. ],
  158. });
  159.  
  160. function UpdateBlockerSizeAndPositioning(blocker, withDelayedRetry = true) {
  161. const parentRect = blocker.parentElement.getBoundingClientRect();
  162. const targetRect = blocker.parentElement.querySelector("#channel-name yt-formatted-string").getBoundingClientRect();
  163.  
  164. // Calculate the blocker's position relative to the parent
  165. // targetRect position is viewport-relative, parentRect is too.
  166. // Subtract parent's top/left from target's top/left
  167. const blockerTop = targetRect.top - parentRect.top;
  168. const blockerLeft = targetRect.left - parentRect.left;
  169.  
  170. // Apply size and position to the blocker
  171. blocker.style.width = `${targetRect.width}px`;
  172. blocker.style.height = `${targetRect.height}px`;
  173. blocker.style.top = `${blockerTop}px`;
  174. blocker.style.left = `${blockerLeft}px`;
  175.  
  176. //Not sure if below is needed anymore. Leaving it here for now, but commented out. Will remove later if he issue donesn't return.
  177. //Adjustment appears to rarely and randomly fail. Attempted fix by additionally reapplying adjustment with a delay, as perhaps the height hasn't been computed yet or something?
  178. // if (withDelayedRetry) {
  179. // setTimeout(() => {
  180. // UpdateBlockerPositioning(blocker, false);
  181. // }, 1000);
  182. // }
  183. }
  184.  
  185. function UpdateUrl(fromElem, toElem) {
  186. //get data source object from element. Newest source used by YT is .polymerController, but older sources that may still be in use if certain flags are in place include .inst or just the element itself
  187. const getVideoDataSource = (o) =>
  188. o ? o.polymerController || o.inst || o || 0 : o || 0;
  189.  
  190. const channelHandle = getVideoDataSource(
  191. fromElem,
  192. )?.data?.longBylineText?.runs?.find((el) =>
  193. el.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl?.match(
  194. /^\/(@|channel)/,
  195. ),
  196. )?.navigationEndpoint.browseEndpoint.canonicalBaseUrl;
  197. if (channelHandle?.length) {
  198. toElem.setAttribute(
  199. "href",
  200. channelHandle + (videoSection ? "/videos" : ""),
  201. );
  202. } else {
  203. console.log("Failed to get channel url");
  204. toElem.addEventListener("click", (e) => {
  205. e.preventDefault();
  206. e.stopPropagation();
  207. alert(
  208. "'Youtube - Fix channel links in sidebar recommendations' failed to get the channel link for this video for some reason. If this happens consistently, please report it at greasyfork.",
  209. );
  210. });
  211. }
  212. }
  213.  
  214. function SetVidSecOption() {
  215. GM_unregisterMenuCommand(videoSectionOption);
  216. videoSectionOption = GM_registerMenuCommand(
  217. `Fix channel links- videos section (${videoSection ? "yes" : "no"}) -click to change-`,
  218. () => {
  219. videoSection = !videoSection;
  220. GM_setValue("videoSection", videoSection);
  221. SetVidSecOption();
  222. },
  223. );
  224. }
  225. })();