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), and optionally uncollapse all updated links which may extend the non collapsible links. All features can be enabled/disabled from the script code. For new YouTube layout only.

当前为 2021-08-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Change YouTube Leftbar Subscription Links To Channel/User Video Page
  3. // @namespace ChangeYouTubeLeftbarSubscriptionLinksToChannelUserVideoPage
  4. // @version 1.2.18
  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), and optionally uncollapse all updated links which may extend the non collapsible links. All features can be enabled/disabled from the script code. For new YouTube layout only.
  8. // @match *://www.youtube.com/*
  9. // @grant none
  10. // @run-at document-start
  11. // ==/UserScript==
  12.  
  13. (yigd => {
  14.  
  15. //===== Configuration Start ===
  16.  
  17. var changeLinksURL = true; //Change links' URLs from the Home tab, to the Videos tab.
  18. var moveUpdatedLinksToTop = true; //Move updated links to top of list without uncollapsing them.
  19. var UncollapseUpdatedLinks = true; //Uncollapse updated links. This may extend the length of uncollapsed links.
  20. //If enabled, it will implicitly enable moveUpdatedLinksToTop.
  21.  
  22. //===== Configuration End ===
  23.  
  24. function changeUrl(url, m) {
  25. if (m = url.match(/^\/feed\/subscriptions\/([^\/]+)$/)) {
  26. return "/channel/" + m[1] + "/videos";
  27. } else return url + "/videos";
  28. }
  29.  
  30. function updateEndpoint(ep) {
  31. ep.commandMetadata.webCommandMetadata.url = changeUrl(ep.commandMetadata.webCommandMetadata.url);
  32. if (ep.webNavigationEndpointData) ep.webNavigationEndpointData.url = changeUrl(ep.webNavigationEndpointData.url);
  33. (function waitGD(gd) {
  34. if ((gd = window["guide-renderer"].__data) && (gd = gd.data) && (gd = gd.items) && (gd = gd.reduce(
  35. (r, v) => {
  36. if (!r && (v = v.guideSubscriptionsSectionRenderer) && (v = v.items)) {
  37. r = v.reduce((r, e) => {
  38. if (e.guideCollapsibleEntryRenderer) {
  39. e.guideCollapsibleEntryRenderer.expandableItems.forEach(t => {
  40. if (!t.guideEntryRenderer.icon || (t.guideEntryRenderer.icon.iconType !== "ADD_CIRCLE")) r.push(t.guideEntryRenderer.navigationEndpoint)
  41. });
  42. } else r.push(e.guideEntryRenderer.navigationEndpoint);
  43. return r;
  44. }, []);
  45. }
  46. return r;
  47. }, null
  48. ))) {
  49. fetch(location.protocol + "//" + location.host + ep.commandMetadata.webCommandMetadata.url, {credentials: "omit"}).then(r => r.text().then((h, o) => {
  50. if ((h = h.match(/window\["ytInitialData"\] = (\{.*?)\};\n/)) && (h = JSON.parse(h[1] + "}").contents.twoColumnBrowseResultsRenderer.tabs)) {
  51. h.some(a => {
  52. if ((/^\/[^\/]+\/[^\/]+\/videos$/).test((a = a.tabRenderer.endpoint).commandMetadata.webCommandMetadata.url)) {
  53. ep.browseEndpoint.params = a.browseEndpoint.params;
  54. ep.commandMetadata.webCommandMetadata.url = a.commandMetadata.webCommandMetadata.url;
  55. if (ep.webNavigationEndpointData) ep.webNavigationEndpointData.url = a.commandMetadata.webCommandMetadata.url;
  56. gd.some(d => {
  57. if (d.browseEndpoint.browseId === a.browseEndpoint.browseId) {
  58. d.browseEndpoint.params = a.browseEndpoint.params;
  59. d.commandMetadata.webCommandMetadata.url = a.commandMetadata.webCommandMetadata.url;
  60. if (d.webNavigationEndpointData) d.webNavigationEndpointData.url = a.commandMetadata.webCommandMetadata.url;
  61. return true;
  62. }
  63. });
  64. return true;
  65. }
  66. })
  67. }
  68. }))
  69. } else setTimeout(waitGD, 100)
  70. })()
  71. }
  72.  
  73. function patchGuide(guide, z) {
  74. if (guide && guide.items && !guide.cysl_done) try {
  75. guide.cysl_done = 1;
  76. guide.items.forEach((v, vc, l, w, c, i, u) => {
  77. if (v.guideSubscriptionsSectionRenderer) {
  78. //change links' URL
  79. if (changeLinksURL) {
  80. v.guideSubscriptionsSectionRenderer.items.forEach(w => {
  81. if (w.guideCollapsibleEntryRenderer) {
  82. w.guideCollapsibleEntryRenderer.expandableItems.forEach(x => {
  83. if (x.guideEntryRenderer.badges && (x = x.guideEntryRenderer.navigationEndpoint)) updateEndpoint(x);
  84. });
  85. } else if (w = w.guideEntryRenderer.navigationEndpoint) updateEndpoint(w);
  86. });
  87. }
  88. //move links with new uploads to top
  89. if (moveUpdatedLinksToTop) {
  90. v = v.guideSubscriptionsSectionRenderer.items; //v = main links container. includes collapsed links container wrapper at end
  91. vc = v.length - 1; //vc = main links count
  92. w = v[vc].guideCollapsibleEntryRenderer; //w = collapsed links container wrapper
  93. l = v.splice(0, vc); //l = list1. move main links into list1. list1 now: mainLinks
  94. c = -1;
  95. if (w) { //has collapsed links container? w = collapsed links container
  96. (w = w.expandableItems).some((e, i) => { //count collapsed links
  97. if (!e.guideEntryRenderer.entryData) {
  98. c = i;
  99. return true;
  100. }
  101. });
  102. l.push.apply(l, w.splice(0, c)); //append collapsed links to list1. list1 now: mainLinks, collapsedLinks
  103. }
  104. c = []; //c = list2 = collapsed new links
  105. for (i = l.length - 1; i >= 0; i--) { //move new links in list1 into main links container
  106. u = l[i].guideEntryRenderer.presentationStyle === "GUIDE_ENTRY_PRESENTATION_STYLE_NEW_CONTENT";
  107. if (l[i].guideEntryRenderer.count || u) {
  108. if ((u = UncollapseUpdatedLinks && u) || vc--) {
  109. v.unshift(l.splice(i, 1)[0]);
  110. if (u) vc--;
  111. } else c.unshift(l.splice(i, 1)[0]);
  112. }
  113. }
  114. c.push.apply(c, l); //append any remaining list1 links (non updated links) into list2
  115. if (vc > 0) { //original-length main links container still has free slot?
  116. l = c.splice(0, vc); //move collapsed links in list2 into list1. same count as main links container free slots
  117. l.unshift.apply(l, [v.length - 1, 0]); //prepare arguments for below task
  118. v.splice.apply(v, l); //move collapsed links in list1 into main links container
  119. }
  120. if (w) w.unshift.apply(w, c); //if has collapsed links container, move remaining collapsed links in list2 into it
  121. }
  122. }
  123. });
  124. return true;
  125. } catch(z) {}
  126. return false;
  127. }
  128.  
  129. Object.defineProperty(window, "ytInitialGuideData", {
  130. get(v) {
  131. return yigd;
  132. },
  133. set(v) {
  134. delete window.ytInitialGuideData;
  135. patchGuide(v);
  136. return yigd = v;
  137. }
  138. });
  139.  
  140. JSON.parse_cylslcvp = JSON.parse;
  141. JSON.parse = function(txt) {
  142. var res = JSON.parse_cylslcvp.apply(this, arguments);
  143. if ((/\/youtubei\/v1\/guide\?/).test(JSON.url_cylslcvp)) patchGuide(res);
  144. return res;
  145. };
  146.  
  147. var fetch_ = window.fetch;
  148. window.fetch = function(opts) {
  149. var fres = fetch_.apply(this, arguments), fthen = fres.then;
  150. if ((/\/youtubei\/v1\/guide\?/).test(opts.url)) {
  151. JSON.url_cylslcvp = opts.url;
  152. fres.then = function(fn) {
  153. var fthenfn = fn;
  154. fn = function(fresp) {
  155. var frtext = fresp.text;
  156. fresp.text = function() {
  157. var tres = frtext.apply(this, arguments), tthen = tres.then;
  158. tres.then = function(tfn) {
  159. var tthenfn = tfn;
  160. tfn = function(tresp) {
  161. var z, tr;
  162. try {
  163. tr = JSON.parse_cylslcvp(tresp);
  164. patchGuide(tr);
  165. tresp = JSON.stringify(tr);
  166. } catch(z) {}
  167. return tthenfn.apply(this, arguments);
  168. };
  169. return tthen.apply(this, arguments);
  170. };
  171. return tres;
  172. }
  173. var frjson = fresp.json;
  174. fresp.json = function() {
  175. var jres = frjson.apply(this, arguments), jthen = jres.then;
  176. jres.then = function(jfn) {
  177. var jthenfn = jfn;
  178. jfn = function(jresp) {
  179. patchGuide(jresp);
  180. return jthenfn.apply(this, arguments);
  181. };
  182. return jthen.apply(this, arguments);
  183. };
  184. return jres;
  185. }
  186. return fthenfn.apply(this, arguments);
  187. };
  188. return fthen.apply(this, arguments);
  189. };
  190. } else JSON.url_cylslcvp = "";
  191. return fres;
  192. };
  193.  
  194. addEventListener("click", (ev, a) => {
  195. if ((a = ev.target) && a.matches && a.matches("ytd-guide-section-renderer:nth-child(2) ytd-guide-entry-renderer *")) {
  196. while (a.tagName !== "A") a = a.parentNode;
  197. if (/^\/(c|channel|u|user)\//.test(a.pathname)) {
  198. a = a.__dataHost.__data.data.navigationEndpoint;
  199. ev.stopImmediatePropagation();
  200. ev.preventDefault();
  201. a.browseEndpoint.params = encodeURIComponent(btoa("\x12\x06videos"));
  202. a.browseEndpoint.canonicalBaseUrl = a.commandMetadata.webCommandMetadata.url.substring(0, a.commandMetadata.webCommandMetadata.url.lastIndexOf("/"));
  203. nav.navigate(a, false, undefined, {});
  204. }
  205. }
  206. }, true);
  207.  
  208. })();