Change YouTube Leftbar Subscription Links To Channel/User Video Page

Change YouTube leftbar's subscription links to channel/user video page. This script can optionally also move updated links to top of the list (if there's enough space), optionally uncollapse all updated links which may extend the non collapsible links, and optionally display the number of updated & total links following the "SUBSCRIPTION" section label. All features can be enabled/disabled from the script code. For new YouTube layout only.

  1. // ==UserScript==
  2. // @name Change YouTube Leftbar Subscription Links To Channel/User Video Page
  3. // @namespace ChangeYouTubeLeftbarSubscriptionLinksToChannelUserVideoPage
  4. // @version 1.5.37
  5. // @license AGPL v3
  6. // @author jcunews
  7. // @description Change YouTube leftbar's subscription links to channel/user video page. This script can optionally also move updated links to top of the list (if there's enough space), optionally uncollapse all updated links which may extend the non collapsible links, and optionally display the number of updated & total links following the "SUBSCRIPTION" section label. All features can be enabled/disabled from the script code. For new YouTube layout only.
  8. // @website https://greasyfork.org/en/users/85671-jcunews
  9. // @match *://www.youtube.com/*
  10. // @grant none
  11. // @run-at document-start
  12. // ==/UserScript==
  13.  
  14. //Note: Users of Violentmonkey under Firefox should disable Violenmonkey's `Synchronous page mode` setting, and enable `Alternative page mode in Firefox` setting; for consistent result.
  15.  
  16. (yigd => {
  17. //===== Configuration Start ===
  18.  
  19. var changeLinksURL = true; //Change links' URLs from the Home tab, to the Videos tab.
  20. var moveUpdatedLinksToTop = true; //Move updated links to top of list without uncollapsing them.
  21. var UncollapseUpdatedLinks = true; //Uncollapse updated links. This may extend the length of uncollapsed links.
  22. //If enabled, it will implicitly enable moveUpdatedLinksToTop.
  23. var showLinksCount = true; //Display number of updated links and total links following the "SUBSCRIPTION" section label.
  24. var autoExpandCollapsed = true; //Auto expand collapsed links.
  25.  
  26. //===== Configuration End ===
  27.  
  28. var to = {createHTML: s => s}, tp = window.trustedTypes?.createPolicy ? trustedTypes.createPolicy("", to) : to, html = s => tp.createHTML(s);
  29.  
  30. function changeUrl(url, m) {
  31. return url;
  32. if (m = url.match(/^\/feed\/subscriptions\/([^\/]+)$/)) {
  33. return "/channel/" + m[1] + "/videos";
  34. } else return url + "/videos";
  35. }
  36.  
  37. function updateSubscriptionCount(a, b, u, t) {
  38. function count(v, w) {
  39. if ((w = v.guideEntryRenderer) && w.entryData) {
  40. t++;
  41. if (w.presentationStyle === "GUIDE_ENTRY_PRESENTATION_STYLE_NEW_CONTENT") u++;
  42. } else if ((w = v.guideCollapsibleEntryRenderer) && (w = w.expandableItems) && w.forEach) {
  43. w.forEach(count)
  44. }
  45. }
  46. if ((a = document.querySelector("ytd-guide-section-renderer:nth-child(2) #guide-section-title")) && !window.ytlssc_ujs) {
  47. a.insertAdjacentHTML("beforeend", html(' (<span id=ytlssc_ujs>-/-</span>)'))
  48. }
  49. if (window.ytlssc_ujs) {
  50. u = 0; t = 0;
  51. if (a && (a = a.parentNode.__shady_native_parentNode.__data || a.parentNode.__dataHost?.__data) && (a = a.shownItems) && a.forEach) a.forEach(count);
  52. ytlssc_ujs.textContent = u + "/" + t
  53. } else setTimeout(updateSubscriptionCount, 500)
  54. }
  55.  
  56. function updateEndpoint(ep, gd, si) {
  57. function processPage(h, url, o) {
  58. if ((h = h.match(/var ytInitialData = (\{.*?\});/)) && (h = JSON.parse(h[1]).contents.twoColumnBrowseResultsRenderer.tabs)) {
  59. h.some((a, i, b) => {
  60. if (!(b = a.tabRenderer).content && (/^\/(@|[^\/]+\/)[^\/]+\/videos$/).test((b = b.endpoint).commandMetadata.webCommandMetadata.url)) {
  61. ep.browseEndpoint = b.browseEndpoint;
  62. ep.clickTrackingParams = b.clickTrackingParams;
  63. ep.commandMetadata = b.commandMetadata;
  64. if (url) b.commandMetadata.webCommandMetadata.url = url + "/videos";
  65. gd.some((d, i) => {
  66. if (d.browseEndpoint.browseId === b.browseEndpoint.browseId) {
  67. d.browseEndpoint = b.browseEndpoint;
  68. d.clickTrackingParams = b.clickTrackingParams;
  69. d.commandMetadata = b.commandMetadata;
  70. if (i = document.querySelector(`ytd-guide-section-renderer:nth-child(${si + 1}) a[href="${d.browseEndpoint.canonicalBaseUrl}"]`)) {
  71. i.href = b.commandMetadata.webCommandMetadata.url
  72. }
  73. return true
  74. }
  75. });
  76. return true
  77. }
  78. })
  79. }
  80. }
  81. function processResponse(r, url) {
  82. if ((r.status >= 400) && !url) { //yt bugfix. STOOPEED YOUTUBE!
  83. if (ep.commandMetadata.webCommandMetadata.webPageType === "WEB_PAGE_TYPE_CHANNEL") {
  84. fetch(url = "/channel/" + ep.browseEndpoint.browseId, {credentials: "omit"}).then(r => processResponse(r, url))
  85. }
  86. return
  87. } else r.text().then(h => processPage(h, url))
  88. }
  89. ep.commandMetadata.webCommandMetadata.url = changeUrl(ep.commandMetadata.webCommandMetadata.url);
  90. if (ep.webNavigationEndpointData) ep.webNavigationEndpointData.url = changeUrl(ep.webNavigationEndpointData.url);
  91. (function waitGD() {
  92. if ((gd = window["guide-renderer"].__data || document.querySelector("#guide-renderer #sections")?.__dataHost) && (gd = gd.data) && (gd = gd.items) && (gd = gd.reduce(
  93. (r, v, i) => {
  94. if (!r && (v = v.guideSubscriptionsSectionRenderer) && (v = v.items)) {
  95. si = i;
  96. r = v.reduce((r, e) => {
  97. if (e.guideCollapsibleEntryRenderer) {
  98. e.guideCollapsibleEntryRenderer.expandableItems.forEach(t => {
  99. if (!t.guideEntryRenderer.icon || (t.guideEntryRenderer.icon.iconType !== "ADD_CIRCLE")) r.push(t.guideEntryRenderer.navigationEndpoint)
  100. });
  101. } else r.push(e.guideEntryRenderer.navigationEndpoint);
  102. return r;
  103. }, []);
  104. }
  105. return r;
  106. }, null
  107. ))) {
  108. fetch(ep.commandMetadata.webCommandMetadata.url, {credentials: "omit"}).then(processResponse)
  109. } else setTimeout(waitGD, 100)
  110. })()
  111. }
  112.  
  113. function patchGuide(guide, z) {
  114. if (guide && guide.items && !guide.cysl_done) try {
  115. guide.cysl_done = 1;
  116. guide.items.forEach((v, vc, l, w, x, c, i, u) => {
  117. if (v.guideSubscriptionsSectionRenderer) {
  118. //change links' URL
  119. if (changeLinksURL) {
  120. v.guideSubscriptionsSectionRenderer.items.forEach(w => {
  121. if (w.guideCollapsibleEntryRenderer) {
  122. w.guideCollapsibleEntryRenderer.expandableItems.forEach(x => {
  123. if (x.guideEntryRenderer.badges && (x = x.guideEntryRenderer.navigationEndpoint)) updateEndpoint(x);
  124. });
  125. } else if (w = w.guideEntryRenderer.navigationEndpoint) updateEndpoint(w);
  126. });
  127. }
  128. //move links with new uploads to top
  129. if (moveUpdatedLinksToTop) {
  130. v = v.guideSubscriptionsSectionRenderer.items; //v = main links container. includes collapsed links container wrapper at end
  131. vc = v.length - 1; //vc = main links count
  132. x = w = v[vc].guideCollapsibleEntryRenderer; //w = collapsed links container wrapper
  133. l = v.splice(0, vc); //l = list1. move main links into list1. list1 now: mainLinks
  134. c = -1;
  135. if (w) { //has collapsed links container? w = collapsed links container
  136. (w = w.expandableItems).some((e, i) => { //count collapsed links
  137. if (!e.guideEntryRenderer.entryData) {
  138. c = i;
  139. return true;
  140. }
  141. });
  142. l.push.apply(l, w.splice(0, c)); //append collapsed links to list1. list1 now: mainLinks, collapsedLinks
  143. }
  144. c = []; //c = list2 = collapsed new links
  145. for (i = l.length - 1; i >= 0; i--) { //move new links in list1 into main links container
  146. u = l[i].guideEntryRenderer.presentationStyle === "GUIDE_ENTRY_PRESENTATION_STYLE_NEW_CONTENT";
  147. if (l[i].guideEntryRenderer.count || u) {
  148. if ((u = UncollapseUpdatedLinks && u) || vc--) {
  149. v.unshift(l.splice(i, 1)[0]);
  150. if (u) vc--;
  151. } else c.unshift(l.splice(i, 1)[0]);
  152. }
  153. }
  154. c.push.apply(c, l); //append any remaining list1 links (non updated links) into list2
  155. if (vc > 0) { //original-length main links container still has free slot?
  156. l = c.splice(0, vc); //move collapsed links in list2 into list1. same count as main links container free slots
  157. l.unshift.apply(l, [v.length - 1, 0]); //prepare arguments for below task
  158. v.splice.apply(v, l); //move collapsed links in list1 into main links container
  159. }
  160. if (w) w.unshift.apply(w, c); //if has collapsed links container, move remaining collapsed links in list2 into it
  161. if ((w = x.expandableItems) && (x = x.expanderItem?.guideEntryRenderer)) { //update collapsed link count
  162. if (x?.formattedTitle?.simpleText) x.formattedTitle.simpleText = x.formattedTitle.simpleText.replace(/\d+/, w.length - 1);
  163. if (x?.accessibility?.accessibilityData?.label) x.accessibility.accessibilityData.label = x.accessibility.accessibilityData.label.replace(/\d+/, w.length - 1);
  164. }
  165. }
  166. }
  167. });
  168. if (showLinksCount) updateSubscriptionCount();
  169. return true;
  170. } catch(z) {}
  171. return false;
  172. }
  173.  
  174. Object.defineProperty(window, "ytInitialGuideData", {
  175. get(v) {
  176. return yigd;
  177. },
  178. set(v) {
  179. delete window.ytInitialGuideData;
  180. patchGuide(v);
  181. return yigd = v;
  182. }
  183. });
  184. JSON.parse_cylslcvp = JSON.parse;
  185. JSON.parse = function(txt) {
  186. var res = JSON.parse_cylslcvp.apply(this, arguments);
  187. if ((/\/youtubei\/v1\/guide\?/).test(JSON.url_cylslcvp)) patchGuide(res);
  188. return res;
  189. };
  190. var fetch_ = window.fetch;
  191. window.fetch = async function(opts) {
  192. var fres = await fetch_.apply(this, arguments);
  193. if ((/\/youtubei\/v1\/guide\?/).test(opts.url)) {
  194. JSON.url_cylslcvp = opts.url;
  195. var frtext = fres.text;
  196. fres.text = async function() {
  197. var tres = await frtext.apply(this, arguments), z, tr;
  198. try {
  199. tr = JSON.parse_cylslcvp(tres);
  200. patchGuide(tr);
  201. tres = JSON.stringify(tr)
  202. } catch(z) {}
  203. return tres
  204. }
  205. var frjson = fres.json;
  206. fres.json = async function() {
  207. var jres = await frjson.apply(this, arguments);
  208. patchGuide(jres);
  209. return jres
  210. }
  211. } else JSON.url_cylslcvp = "";
  212. return fres
  213. };
  214. var nac = Node.prototype.appendChild, ge, gs;
  215. Node.prototype.appendChild = function(e) {
  216. var z;
  217. if ((this.tagName === "BODY") && (e?.tagName === "IFRAME")) {
  218. var r = nac.apply(this, arguments);
  219. try {
  220. if (/^about:blank\b/.test(e.contentWindow.location.href)) e.contentWindow.fetch = fetch
  221. } catch(z) {}
  222. return r
  223. } else {
  224. if (autoExpandCollapsed && e.matches?.('ytd-guide-collapsible-entry-renderer')) {
  225. (function we(a, t) {
  226. if (a = e.querySelector('#expander-item')) {
  227. gs = (ge = e.closest('#guide-inner-content')).scrollTop;
  228. ge.onscroll = ev => {
  229. ge.onscroll = null;
  230. ge.scrollTop = gs
  231. };
  232. a.click();
  233. t = Date.now();
  234. (function wa() {
  235. if (document.activeElement && (document.activeElement.tagName !== "BODY")) {
  236. document.activeElement.blur()
  237. } else if ((Date.now() - t) < 1000) requestAnimationFrame(wa)
  238. })()
  239. } else setTimeout(we, 20)
  240. })()
  241. }
  242. return nac.apply(this, arguments)
  243. }
  244. };
  245.  
  246. addEventListener("click", (ev, a, b) => {
  247. if ((a = ev.target) && a.matches && a.matches("ytd-guide-section-renderer:nth-child(2) ytd-guide-entry-renderer *")) {
  248. while (a.tagName !== "A") a = a.parentNode;
  249. if (/^\/((c|channel|u|user)|@[^\\]+)\//.test(a.pathname)) {
  250. if (a.__dataHost) {
  251. (b = a.__dataHost.__data.data).navigationEndpoint;
  252. if (b.presentationStyle === "GUIDE_ENTRY_PRESENTATION_STYLE_NEW_CONTENT") {
  253. (function usc() {
  254. if (b.presentationStyle !== "GUIDE_ENTRY_PRESENTATION_STYLE_NEW_CONTENT") {
  255. updateSubscriptionCount()
  256. } else setTimeout(usc, 500)
  257. })();
  258. }
  259. } else if (b = a.closest('ytd-guide-entry-renderer[line-end-style="dot"]')) {
  260. (function usc2() {
  261. if (b.getAttribute("line-end-style") !== "dot") {
  262. updateSubscriptionCount()
  263. } else setTimeout(usc2, 500)
  264. })();
  265. }
  266. }
  267. }
  268. }, true);
  269.  
  270. })()