Restore YouTube Username from Handle to Custom

将 YouTube 用户名从 Handle 恢复为自定义名称

当前为 2023-06-30 提交的版本,查看 最新版本

  1. /*
  2.  
  3. MIT License
  4.  
  5. Copyright 2023 CY Fung
  6.  
  7. Permission is hereby granted, free of charge, to any person obtaining a copy
  8. of this software and associated documentation files (the "Software"), to deal
  9. in the Software without restriction, including without limitation the rights
  10. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. copies of the Software, and to permit persons to whom the Software is
  12. furnished to do so, subject to the following conditions:
  13.  
  14. The above copyright notice and this permission notice shall be included in all
  15. copies or substantial portions of the Software.
  16.  
  17. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  23. SOFTWARE.
  24.  
  25. */
  26. // ==UserScript==
  27. // @name Restore YouTube Username from Handle to Custom
  28. // @namespace http://tampermonkey.net/
  29. // @version 0.6.3
  30. // @license MIT License
  31.  
  32. // @author CY Fung
  33. // @match https://www.youtube.com/*
  34. // @match https://m.youtube.com/*
  35. // @exclude /^https?://\S+\.(txt|png|jpg|jpeg|gif|xml|svg|manifest|log|ini)[^\/]*$/
  36. // @icon https://github.com/cyfung1031/userscript-supports/raw/main/icons/general-icon.png
  37. // @supportURL https://github.com/cyfung1031/userscript-supports
  38. // @run-at document-start
  39. // @grant none
  40. // @unwrap
  41. // @allFrames true
  42. // @inject-into page
  43.  
  44. // @compatible firefox 55
  45. // @compatible chrome 61
  46. // @compatible opera 48
  47. // @compatible safari 11.1
  48. // @compatible edge 16
  49.  
  50. // @description To restore YouTube Username to the traditional custom name
  51. // @description:ja YouTubeのユーザー名を伝統的なカスタム名に復元するために。
  52. // @description:zh-TW 將 YouTube 使用者名稱從 Handle 恢復為自訂名稱
  53. // @description:zh-CN 将 YouTube 用户名从 Handle 恢复为自定义名称
  54.  
  55. // @description:ko YouTube 사용자 이름을 전통적인 사용자 지정 이름으로 복원합니다.
  56. // @description:ru Восстановление имени пользователя YouTube с помощью обычного настраиваемого имени
  57. // @description:af Herstel YouTube-gebruikersnaam vanaf Handvat na Aangepaste Naam
  58. // @description:az YouTube İstifadəçi Adını Özəl Adından İstifadə Etmək
  59. // @description:id Mengembalikan Nama Pengguna YouTube dari Handle ke Kustom
  60. // @description:ms Pulihkan Nama Pengguna YouTube dari Handle ke Tersuai
  61. // @description:bs Vrati YouTube korisničko ime sa ručke na prilagođeno ime
  62. // @description:ca Restaurar el nom d'usuari de YouTube de la màniga al personalitzat
  63. // @description:cs Obnovte uživatelské jméno YouTube z rukojeti na vlastní
  64. // @description:da Gendan YouTube-brugernavn fra Håndtag til Brugerdefineret
  65. // @description:de Stellt den YouTube-Benutzernamen von Handle auf Benutzerdefiniert wieder her
  66. // @description:et Taasta YouTube'i kasutajanimi käepidemelt kohandatud nimeks
  67. // @description:es Restaurar el nombre de usuario de YouTube de la manija al personalizado
  68. // @description:eu Berrezarri YouTube Erabiltzaile-izena Manipulatik Pertsonalizatuera
  69. // @description:fr Restaurer le nom d'utilisateur YouTube de la poignée au nom personnalisé
  70. // @description:gl Restaura o nome de usuario de YouTube da manexa ao personalizado
  71. // @description:hr Vrati YouTube korisničko ime s ručke na prilagođeno ime
  72. // @description:zu Hlanganisa Igama Lokusebenza lwe-YouTube kusuka kwi-Handle kuze kube kusebenza kakhulu
  73. // @description:is Endurheimtu YouTube Notandanafn frá Handfangi til Sérsniðins
  74. // @description:it Ripristina il nome utente di YouTube da Handle a Personalizzato
  75. // @description:sw Rejesha Jina la Mtumiaji wa YouTube kutoka kwa Kishughulikia hadi Desturi
  76. // @description:lv Atjaunot YouTube lietotāja vārdu no roktura uz pielāgotu
  77. // @description:lt Atkurti „YouTube“ naudotojo vardą iš rankenos į tinkamą
  78. // @description:hu Visszaállítja a YouTube felhasználónevet a fogantyútól a testreszabottra
  79. // @description:nl Herstel YouTube-gebruikersnaam van Handle naar Aangepaste naam
  80. // @description:uz YouTube foydalanuvchining nomini Hurda dan Shaxsiylashtirilgan nomga qaytarish
  81. // @description:pl Przywróć nazwę użytkownika YouTube z uchwytu na niestandardową
  82. // @description:pt Restaurar o nome de usuário do YouTube de Handle para Personalizado
  83. // @description:pt-BR Restaurar o nome de usuário do YouTube de Handle para Personalizado
  84. // @description:ro Restaurați Numele de Utilizator YouTube de la Mâner la Personalizat
  85. // @description:sq Rikthe emrin e përdoruesit të YouTube nga Maja në Personalizuar
  86. // @description:sk Obnovte používateľské meno YouTube z rukoväte na vlastné
  87. // @description:sl Obnovite uporabniško ime YouTube iz ročaja v prilagojeno ime
  88. // @description:sr Вратите YouTube корисничко име са ручке на прилагођено име
  89. // @description:fi Palauta YouTube-käyttäjänimi kahvasta räätälöityyn nimeen
  90. // @description:sv Återställ YouTube-användarnamnet från handtaget till anpassat namn
  91. // @description:vi Khôi phục Tên người dùng YouTube từ Tay cầm thành Tùy chỉnh
  92. // @description:tr Özel İsme Kullanıcı Adını İade Etme
  93. // @description:be Вярнуць імя карыстальніка YouTube з ручкі на наладжваецца
  94. // @description:bg Възстановяване на потребителско име на YouTube от дръжка до персонализирано име
  95. // @description:ky YouTube колдонуучунун атты боюнча атын тамашалау
  96. // @description:kk YouTube пайдаланушының атын Handle танымасынан Жеке атауға қайта түсіру
  97. // @description:mk Врати го името на YouTube корисникот од држачот во прилагодено име
  98. // @description:mn YouTube хэрэглэгчийн нэрийг хүчирхийлэгчидээс Байгууллагад нь солих
  99. // @description:uk Відновлення імені користувача YouTube з ручки на налаштоване ім'я
  100. // @description:el Επαναφορά Ονόματος Χρήστη YouTube από τη Λαβή σε Προσαρμοσμένο
  101. // @description:hy Վերականգնել YouTube-ի Օգտագործողի Անունը Ձեռքից Հատուցվածությանով
  102. // @description:ur ہینڈل سے یوٹیوب یوزر نیم کو کسٹم میں بحال کریں
  103. // @description:ar استعادة اسم مستخدم YouTube من المقبض إلى مخصص
  104. // @description:fa بازیابی نام کاربری یوتیوب از دستگیره به سفارشی
  105. // @description:ne ह्यान्डलबाट यूट्युब प्रयोगकर्तानाम अनुकूलमा उपयोग गर्नुहोस्
  106. // @description:mr हॅंडलमधून यूट्यूब वापरकर्तानाव अनुकूलित करा
  107. // @description:hi हैंडल से यूट्यूब यूजरनेम को कस्टम में बदलें
  108. // @description:as কাস্টম হতে হ্যান্ডেল পৰা ইউটিউব ব্যৱহাৰকাৰী নাম পুনৰ্স্থাপন কৰক
  109. // @description:bn হ্যান্ডেল থেকে কাস্টম করে ইউটিউব ব্যবহারকারীর নাম পুনরুদ্ধার করুন
  110. // @description:pa ਯੂਟਿਊਬ ਯੂਜਰਨਾਂ ਦਾ ਨਾਂ ਹੈਂਡਲ ਤੋਂ ਕਸਟਮ ਵਿੱਚ ਮੁੜ ਲਾਓ
  111. // @description:gu હેન્ડલથી કસ્ટમમાંથી YouTube વપરાશકર્તાનું નામ પુનર્સ્થાપિત કરો
  112. // @description:or କଷ୍ଟମରେ ହେନ୍ଡେଲରୁ YouTube ବ୍ୟବହାରକାରୀଙ୍କ ନାମ ପୁନର୍ପ୍ରାପ୍ତ କରନ୍ତୁ
  113. // @description:ta ஹேண்டில் இருந்து கஸ்டம் ஆக்கும் யூடியூப் பயனர்பெயரை மீட்டமைக்கவும்
  114. // @description:te హాండును నుంచి YouTube వాడుకరి పేరును కస్టమ్‌కు పునరుద్ధరించండి
  115. // @description:kn ಹ್ಯಾಂಡಲ್‌ನಿಂದ YouTube ಬಳಕೆದಾರ ಹೆಸರನ್ನು ಅನುಕೂಲಿತ ಮಾಡಿ
  116. // @description:ml ഹാന്റിൽ നിന്ന് കസ്റ്റം ആക്കാംമെന്ന് യുട്യൂബ് ഉപയോക്തൃനാമം മറ്റുള്ളവരെ മാറ്റുക
  117. // @description:si හැන්ඩල් සහිතව YouTube භාවිතයේ පරිශීලක නාමය ස්වයං කරයි
  118. // @description:th กู้คืนชื่อผู้ใช้ YouTube จากแฮนดิลไปเป็นชื่อที่กำหนดเอง
  119. // @description:lo ເຮັດຊະນິດຊື່ຜູ້ໃຊ້ YouTube ຈາກ Handle ຈົດໄວ້ເປັນຊື່ປັດຈຸບັນ
  120. // @description:my Handle မှ YouTube အသုံးပြုသူအမည်ကို စတင်ပြန်စစ်ဆေးပါ
  121. // @description:ka YouTube მომხმარებლის სახელის აღდგენა ხანდლიდან მორგებულ სახელში
  122. // @description:am ስለ YouTube የተጠቃሚ ስም ማስቀመጫዎቹን ከ Handle ወደ Custom ውስጥ እንደሚመጣ ይረዳሉ
  123. // @description:km កំណត់ YouTube ពាក្យឈ្មោះពី Handle ទៅជាឈ្មោះផ្ទាល់ខ្លួន
  124.  
  125. // ==/UserScript==
  126.  
  127. /* jshint esversion:8 */
  128.  
  129.  
  130. /**
  131. @typedef DisplayNameResultObject
  132. @type {Object}
  133. @property {string} title
  134. @property {string?} externalId
  135. @property {string[]} ownerUrls
  136. @property {string} channelUrl
  137. @property {string} vanityChannelUrl
  138. @property {boolean|undefined} verified123
  139.  
  140. */
  141.  
  142. /**
  143. @typedef ChannelIdToHandleResult
  144. @type {Object}
  145. @property {string} handleText
  146. @property {boolean} justPossible
  147. */
  148.  
  149. (function (__CONTEXT__) {
  150. 'use strict';
  151.  
  152. const USE_RSS_FETCHER = true;
  153. const USE_TIMEOUT_SIGNAL = false;
  154. const USE_CHANNEL_META = true;
  155.  
  156. const { Promise, fetch, JSON, Request, AbortController, setTimeout, clearTimeout } = __CONTEXT__; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  157.  
  158. const timeoutSignal = AbortController && USE_TIMEOUT_SIGNAL ? (timeout) => {
  159. let controller = new AbortController();
  160. const r = controller.signal;
  161. let cid = setTimeout(() => {
  162. if (cid >= 1) {
  163. cid = 0;
  164. controller && controller.abort();
  165. controller = null;
  166. }
  167. }, timeout);
  168. r.clearTimeout = () => {
  169. if (cid >= 1) {
  170. clearTimeout(cid);
  171. cid = 0;
  172. }
  173. }
  174. return r;
  175. } : () => { };
  176.  
  177. const fxOperator = (proto, propertyName) => {
  178. let propertyDescriptorGetter = null;
  179. try {
  180. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  181. // https://github.com/cyfung1031/userscript-supports/issues/9
  182. // .parentNode becomes DocumentFragment
  183. /**
  184. *
  185. * Issue Description: YtCustomElements - Custom DOM Operations overrided in FireFox
  186. *
  187. * e.g. ytd-comment-renderer#comment > div#body
  188. *
  189. * ${div#body}.parentNode = DocumentFragment <Node.parentNode>
  190. * ${ytd-comment-renderer#comment}.firstElementChild <Element.firstElementChild>
  191. *
  192. * Cofirmed Affected: parentNode, firstChild, firstElementChild, lastChild, lastElementChild, querySelector, & querySelectorAll
  193. *
  194. * Alternative way: ytCustomElement.$.xxxxxxx
  195. *
  196. */
  197. } catch (e) { }
  198. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  199. };
  200.  
  201. const fxAPI = (proto, propertyName) => {
  202. const methodFunc = proto[propertyName];
  203. return typeof methodFunc === 'function' ? (e, ...args) => methodFunc.apply(e, args) : (e, ...args) => e[propertyName](...args);
  204. };
  205.  
  206. /** @type { (node: Node)=>Node | null } */
  207. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  208. /** @type { (node: Node)=>Node | null } */
  209. const nodeFirstChild = fxOperator(Node.prototype, 'firstChild');
  210. /** @type { (node: Node)=>Node | null } */
  211. const nodeNextSibling = fxOperator(Node.prototype, 'nextSibling');
  212.  
  213. /** @type { (node: ParentNode, selector: string)=>Element | null } */
  214. const elementQS = fxAPI(Element.prototype, 'querySelector');
  215. /** @type { (node: ParentNode, selector: string)=>Element[] } */
  216. const elementQSA = fxAPI(Element.prototype, 'querySelectorAll');
  217.  
  218. /*
  219.  
  220.  
  221. init
  222.  
  223. initialdata
  224. state-progress
  225. state-responsereceived
  226. player-autonav-pause
  227. state-change
  228. state-navigateend
  229. player-initialized
  230. renderer-module-load-start
  231. video-data-change
  232. player-state-change
  233. updateui
  234. renderer-module-load-end -> channel owner DOM available
  235. player-autonav-pause
  236. updateui
  237. renderer-module-load-start
  238. updateui
  239. renderer-module-load-end
  240.  
  241.  
  242. playing
  243.  
  244. h5player.video-progress
  245. h5player.video-progress
  246. h5player.video-progress
  247. h5player.video-progress
  248. ...
  249.  
  250.  
  251.  
  252.  
  253. navigate new video
  254.  
  255. state-navigatestart
  256. state-change => channel owener DOM vanished
  257. state-progress
  258. ...
  259. h5player.player-state-change
  260. h5player.video-data-change
  261. h5player.player-state-change
  262. h5player.muted-autoplay-change
  263. h5player.volume-change
  264. h5player.video-data-change
  265. h5player.volume-change
  266. ...
  267.  
  268. state-progress
  269. ...
  270.  
  271. state-progress => channel owner DOM appear [just before state-responsereceived]
  272. state-responsereceived
  273.  
  274. video-data-change
  275. state-change
  276. state-navigateend
  277.  
  278.  
  279.  
  280.  
  281. UX interaction
  282.  
  283. user-activity
  284. player-autonav-pause
  285.  
  286.  
  287.  
  288.  
  289.  
  290. EventTarget.prototype.uhfsE = EventTarget.prototype.dispatchEvent
  291.  
  292. EventTarget.prototype.dispatchEvent = function (ev) {
  293. if (ev instanceof Event) {
  294. if (ev.type === 'video-progress') { } else {
  295. console.log(ev.type, this, ev.target)
  296. if (ev.target === null) {
  297. console.log(ev.type, (document.querySelector('ytm-app') || 0).data)
  298. console.log(ev.type, (document.querySelector('ytm-slim-owner-renderer') || 0).textContent)
  299. }
  300. }
  301. }
  302. return this.uhfsE.apply(this, arguments);
  303. }
  304.  
  305.  
  306.  
  307. */
  308.  
  309. const isMobile = location.hostname === 'm.youtube.com';
  310.  
  311. const cfg = {};
  312. class Mutex {
  313.  
  314. constructor() {
  315. this.p = Promise.resolve()
  316. }
  317.  
  318. /**
  319. * @param {(lockResolve: () => void)} f
  320. */
  321. lockWith(f) {
  322. this.p = this.p.then(() => new Promise(f).catch(console.warn))
  323. }
  324.  
  325. }
  326.  
  327. class OrderedMutex extends Mutex {
  328. constructor() {
  329. super();
  330. this.nextIndex = 0;
  331. this.arr = [];
  332. this.lockFunc = resolve => {
  333. if (this.arr.length >= 1) {
  334. let f = this.arr[0];
  335. if (typeof f === 'function') {
  336. this.arr.shift();
  337. if (this.nextIndex > 0) this.nextIndex--;
  338. f(resolve);
  339. return;
  340. } else {
  341. // reset if unknown error
  342. this.arr.length = 0;
  343. this.nextIndex = 0;
  344. }
  345. }
  346. resolve();
  347. };
  348. }
  349. /**
  350. * @param {(lockResolve: () => void)} f
  351. */
  352. add(f) {
  353. if (this.nextIndex === this.arr.length) {
  354. this.arr.push(f); this.nextIndex++;
  355. } else {
  356. this.arr.splice(this.nextIndex++, 0, f);
  357. }
  358. this.lockWith(this.lockFunc);
  359. }
  360. }
  361. // usage: run network request one by one
  362. const mutex = new OrderedMutex();
  363.  
  364. /**
  365. * @typedef { AsyncValue<T> | T} AsyncOrResolvedValue<T>
  366. * @template T
  367. */
  368. //
  369. /**
  370. * usage: cache the network result per web application instance.
  371. * @type {Map<string, AsyncOrResolvedValue<DisplayNameResultObject>>} */
  372. const displayNameResStore = new Map();
  373.  
  374. /**
  375. * usage: mapping the .dataChanged() to the wrapped function - only few entries as most are {ytd-comment-renderer}.dataChanged().
  376. * @type {WeakMap<Function, Function>} */
  377. const dataChangedFuncStore = new WeakMap();
  378.  
  379. /**
  380. * usage: for \@Mention inside comments in YouTube Mobile that without hyperlinks for channelId.
  381. * @type {Map<string, AsyncOrResolvedValue<string>>} */
  382. const handleToChannelId = new Map();
  383.  
  384. /**
  385. * usage: in RSS fetching, no handle text will be obtained from the response. inject the handle into the response.
  386. * @type {Map<string, ChannelIdToHandleResult>} */
  387. const channelIdToHandle = new Map();
  388.  
  389.  
  390. /**
  391. * AsyncValue
  392. * @class
  393. * @template T
  394. */
  395. class AsyncValue {
  396. constructor() {
  397. /** @type {T | null} */
  398. this.__value__ = null;
  399. /** @type {((value: any)=>void) | null} */
  400. this.__resolve__ = null;
  401. this.__promise__ = new Promise(resolve => {
  402. this.__resolve__ = resolve;
  403. });
  404. }
  405. /**
  406. *
  407. * @param {T} value
  408. */
  409. setValue(value) {
  410. const promise = this.__promise__;
  411. if (promise === null) throw 'Value has already been set.';
  412. this.__value__ = value;
  413. this.__promise__ = null;
  414. this.__resolve__ ? this.__resolve__() : Promise.resolve().then(() => this.__resolve__());
  415. }
  416. /**
  417. *
  418. * @returns {Promise<T>}
  419. */
  420. async getValue() {
  421. const promise = this.__promise__;
  422. if (promise === null) return this.__value__;
  423. await promise; // promise become null
  424. this.__promise__ = null; // just in case
  425. this.__resolve__ = null;
  426. return this.__value__;
  427. }
  428. }
  429.  
  430. /**
  431. *
  432. * @param {DisplayNameResultObject} resultInfo
  433. * @param {string} channelId
  434. */
  435. const cacheHandleToChannel = (resultInfo, channelId) => {
  436.  
  437. const { vanityChannelUrl, ownerUrls } = resultInfo;
  438.  
  439. let handleText = urlToHandle(vanityChannelUrl || '');
  440.  
  441. if (handleText) {
  442. // match = true;
  443. } else if ((ownerUrls || 0).length >= 1) {
  444. for (const ownerUrl of ownerUrls) {
  445. handleText = urlToHandle(ownerUrl || '');
  446.  
  447. if (handleText) {
  448. // match = true;
  449. break;
  450. }
  451. }
  452. }
  453. if (handleText) {
  454.  
  455. const asyncValue = handleToChannelId.get(handleText); // nothing if no pending promise
  456. if (asyncValue instanceof AsyncValue) {
  457. asyncValue.setValue(channelId);
  458. }
  459. handleToChannelId.set(handleText, channelId);
  460. }
  461.  
  462.  
  463. }
  464.  
  465. /**
  466. *
  467. * @param {string} channelId
  468. * @param {Function} onDownloaded
  469. * @param {Function} onResulted
  470. * @param {Function} onError
  471. */
  472. const fetcherBrowseAPI = (channelId, onDownloaded, onResulted, onError) => {
  473.  
  474. let signal = timeoutSignal(4000);
  475.  
  476. fetch(new Request(`/youtubei/v1/browse?key=${cfg.INNERTUBE_API_KEY}&prettyPrint=false`, {
  477. "method": "POST",
  478. "mode": "same-origin",
  479. "credentials": "omit",
  480.  
  481. referrerPolicy: "no-referrer",
  482. cache: "default", // no effect on POST request
  483. // cache: "force-cache",
  484. redirect: "error", // there shall be no redirection in this API request
  485. integrity: "",
  486. keepalive: false,
  487. signal,
  488.  
  489. "headers": {
  490. "Content-Type": "application/json", // content type of the body data in POST request
  491. "Accept-Encoding": "gzip, deflate, br", // YouTube Response - br
  492. // X-Youtube-Bootstrap-Logged-In: false,
  493. // X-Youtube-Client-Name: 1, // INNERTUBE_CONTEXT_CLIENT_NAME
  494. // X-Youtube-Client-Version: "2.20230622.06.00" // INNERTUBE_CONTEXT_CLIENT_VERSION
  495.  
  496. "Accept": "application/json",
  497. },
  498. "body": JSON.stringify({
  499. "context": {
  500. "client": {
  501. "visitorData": "Cgs0aVg0VjFWM0U0USi0jvOkBg%3D%3D", // [optional] fake visitorData to avoid dynamic visitorData generated in response
  502. "clientName": "MWEB", // "WEB", "MWEB"
  503. "clientVersion": `${cfg.INNERTUBE_CLIENT_VERSION || '2.20230614.01.00'}`, // same as WEB version
  504. "originalUrl": `https://m.youtube.com/channel/${channelId}`,
  505. "playerType": "UNIPLAYER",
  506. "platform": "MOBILE", // "DESKTOP", "MOBILE", "TABLET"
  507. "clientFormFactor": "SMALL_FORM_FACTOR", // "LARGE_FORM_FACTOR", "SMALL_FORM_FACTOR"
  508. "acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
  509. "mainAppWebInfo": {
  510. "graftUrl": `/channel/${channelId}`,
  511. "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER",
  512. "isWebNativeShareAvailable": true
  513. }
  514. },
  515. "user": {
  516. "lockedSafetyMode": false
  517. },
  518. "request": {
  519. "useSsl": true,
  520. "internalExperimentFlags": [],
  521. "consistencyTokenJars": []
  522. }
  523. },
  524. "browseId": `${channelId}`
  525. })
  526. })).then(res => {
  527. signal && signal.clearTimeout && signal.clearTimeout();
  528. signal = null;
  529. onDownloaded();
  530. onDownloaded = null;
  531. return res.json();
  532. }).then(resJson => {
  533. let resultInfo = ((resJson || 0).metadata || 0).channelMetadataRenderer;
  534. onResulted(resultInfo);
  535. onResulted = null;
  536. }).catch(onError);
  537.  
  538. };
  539.  
  540. /**
  541. *
  542. * @param {string} channelId
  543. * @param {Function} onDownloaded
  544. * @param {Function} onResulted
  545. * @param {Function} onError
  546. */
  547. const fetcherRSS = location.origin !== 'https://www.youtube.com' ? null : (channelId, onDownloaded, onResulted, onError) => {
  548.  
  549. let signal = timeoutSignal(4000);
  550. fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`, {
  551. // YouTube RSS Response - public, max-age=900
  552.  
  553. "method": "GET",
  554. "mode": "same-origin",
  555. "credentials": "omit",
  556.  
  557. referrerPolicy: "no-referrer",
  558. cache: "default",
  559. // cache: "no-cache",
  560. redirect: "error", // there shall be no redirection in this API request
  561. integrity: "",
  562. keepalive: false,
  563. signal,
  564.  
  565. "headers": {
  566. "Cache-Control": "public, max-age=900, stale-while-revalidate=1800",
  567. // refer "Cache-Control Use Case Examples" in https://www.koyeb.com/blog/using-cache-control-and-cdns-to-improve-performance-and-reduce-latency
  568. // seems YouTube RSS Feeds server insists its own Cache-Control.
  569.  
  570. // "Content-Type": "text/xml; charset=UTF-8",
  571. "Accept-Encoding": "gzip, deflate, br", // YouTube Response - gzip
  572. // X-Youtube-Bootstrap-Logged-In: false,
  573. // X-Youtube-Client-Name: 1, // INNERTUBE_CONTEXT_CLIENT_NAME
  574. // X-Youtube-Client-Version: "2.20230622.06.00" // INNERTUBE_CONTEXT_CLIENT_VERSION
  575.  
  576. "Accept": "text/xml",
  577. }
  578. }).then(res => {
  579. signal && signal.clearTimeout && signal.clearTimeout();
  580. signal = null;
  581. onDownloaded();
  582. onDownloaded = null;
  583. return res.text();
  584. }).then(resText => {
  585. let eIdx = resText.indexOf('<entry>');
  586. let mText = (eIdx > 0) ? `${resText.substring(0, eIdx).trim()}</feed>` : resText;
  587.  
  588. // simple: https://www.youtube.com/feeds/videos.xml?channel_id=UC-MUu72gixoYlhV5_mWa36g
  589. // standard: https://www.youtube.com/feeds/videos.xml?channel_id=UCGSfK5HeiIhuFfXoKE47TzA
  590. // long: https://www.youtube.com/feeds/videos.xml?channel_id=UC8cSGjKxDuh2mWG1hDOTdBg
  591. // special: http://www.youtube.com/feeds/videos.xml?channel_id=UCmZ2-GUxmdWFKfXA5IN0x-w
  592.  
  593.  
  594. let name, uri, mt;
  595.  
  596. // ===============================================================
  597.  
  598. /* removed in 2023.06.30 */
  599.  
  600.  
  601. // ===============================================================
  602.  
  603.  
  604. const wIdx1 = mText.indexOf('<author>');
  605. const wIdx2 = wIdx1 > 0 ? mText.indexOf('</author>', wIdx1 + 8) : -1;
  606.  
  607. if (wIdx1 > 0 && wIdx2 > 0) {
  608. let qText = mText.substring(wIdx1, wIdx2 + '</author>'.length);
  609. const template = document.createElement('template');
  610. template.innerHTML = qText;
  611. /** @type {HTMLElement | null} */
  612. let authorChild = ((template.content || 0).firstElementChild || 0).firstElementChild;
  613. for (; authorChild !== null; authorChild = authorChild.nextElementSibling) {
  614. const nodeName = authorChild.nodeName;
  615. if (nodeName === 'NAME') name = authorChild.textContent;
  616. else if (nodeName === 'URI') {
  617. uri = authorChild.textContent;
  618. mt = obtainChannelId(uri);
  619. }
  620. }
  621. template.innerHTML = '';
  622. }
  623.  
  624. if (!name && !uri) {
  625.  
  626. // fallback
  627.  
  628. const parser = new DOMParser();
  629. const xmlDoc = parser.parseFromString(mText, "text/xml");
  630.  
  631. // const author = xmlDoc.querySelector("author");
  632.  
  633. // https://extendsclass.com/xpath-tester.html
  634.  
  635. const authorChilds = xmlDoc.evaluate("//*[name()='author']/*", xmlDoc, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
  636.  
  637. let authorChild;
  638. while ((authorChild = authorChilds.iterateNext()) !== null) {
  639. if (authorChild.nodeName === 'name') name = authorChild.textContent;
  640. else if (authorChild.nodeName === 'uri') {
  641. uri = authorChild.textContent;
  642. mt = obtainChannelId(uri)
  643. }
  644. }
  645.  
  646. try {
  647. xmlDoc.firstChild.remove();
  648. } catch (e) { }
  649.  
  650. }
  651.  
  652.  
  653.  
  654.  
  655. let res = null;
  656.  
  657. if (name && uri && mt && mt === channelId) {
  658.  
  659.  
  660. let object = channelIdToHandle.get(mt);
  661.  
  662. if (object) {
  663.  
  664. if (object.handleText === name) channelIdToHandle.delete(mt);
  665. else {
  666. object.justPossible = false; // ignore @handle checking
  667.  
  668. res = {
  669. "title": name,
  670. "externalId": mt,
  671. "ownerUrls": [
  672. `http://www.youtube.com/${object.handleText}`
  673. ],
  674. "channelUrl": uri,
  675. "vanityChannelUrl": `http://www.youtube.com/${object.handleText}`
  676. };
  677.  
  678. }
  679. }
  680. }
  681.  
  682. onResulted(res);
  683. onResulted = null;
  684.  
  685. // let resultInfo = ((res || 0).metadata || 0).channelMetadataRenderer;
  686. // onResulted(resultInfo);
  687. }).catch(onError);
  688.  
  689.  
  690.  
  691. }
  692.  
  693. /**
  694. *
  695. * @param {string} channelId
  696. */
  697. function stackNewRequest(channelId) {
  698.  
  699. mutex.add(lockResolve => {
  700.  
  701. let bResult = displayNameResStore.get(channelId);
  702. if (!(bResult instanceof AsyncValue)) {
  703. // resolved or removed
  704. lockResolve(); lockResolve = null;
  705. return;
  706. }
  707.  
  708. let setResult = (result) => {
  709. setResult = null;
  710. if (!result) {
  711. bResult.fetchingState = 0;
  712. bResult.setValue(null);
  713. displayNameResStore.delete(channelId); // create another network response in the next request
  714. } else {
  715. bResult.fetchingState = 4;
  716. bResult.setValue(result);
  717. displayNameResStore.set(channelId, result); // update store result to resolved value
  718. }
  719. bResult = null;
  720. }
  721.  
  722. if (bResult.fetchingState >= 2) { // fetchingState == 3 or 4
  723. // request is already done. no need to stack any request
  724. lockResolve(); lockResolve = null;
  725. return;
  726. }
  727.  
  728. if (!document.querySelector(`[jkrgy="${channelId}"]`)) {
  729. // element has already been removed
  730. lockResolve(); lockResolve = null;
  731. setResult(null);
  732. return;
  733. }
  734.  
  735. //INNERTUBE_API_KEY = ytcfg.data_.INNERTUBE_API_KEY
  736.  
  737. bResult.fetchingState = 2;
  738.  
  739.  
  740. // note: when setResult(null), the fetch will be requested again if the same username appears. (multiple occurrences)
  741. // consider the network problem might be fixed in the 2nd attempt, the name will be changed in the 2nd attempt but ignore 1st attempt.
  742. const fetcher = USE_RSS_FETCHER && fetcherRSS && channelIdToHandle.has(channelId) ? fetcherRSS : fetcherBrowseAPI;
  743. fetcher(channelId, () => {
  744. bResult.fetchingState = 3;
  745. lockResolve();
  746. lockResolve = null;
  747. }, resultInfo => {
  748.  
  749. if (!resultInfo) {
  750. // invalid json format
  751. setResult(null);
  752. return;
  753. }
  754.  
  755. cacheHandleToChannel(resultInfo, channelId);
  756.  
  757. const { title, externalId, ownerUrls, channelUrl, vanityChannelUrl } = resultInfo;
  758.  
  759. const displayNameRes = { title, externalId, ownerUrls, channelUrl, vanityChannelUrl, verified123: false };
  760.  
  761. setResult(displayNameRes);
  762. }, e => {
  763. lockResolve && lockResolve();
  764. lockResolve = null;
  765. console.warn(e);
  766. setResult && setResult(null);
  767. });
  768.  
  769. });
  770.  
  771. }
  772.  
  773. /**
  774. *
  775. * @param {string} url Example: _www\.youtube\.com/\@yr__kd_
  776. * @returns Example: _\@yr__kd_
  777. */
  778. function urlToHandle(url) {
  779.  
  780. if (typeof url !== 'string') return '';
  781. let i = url.indexOf('.youtube.com/');
  782. if (i >= 1) url = url.substring(i + '.youtube.com/'.length);
  783. else if (url.charAt(0) === '/') url = url.substring(1);
  784. return isDisplayAsHandle(url) ? url : '';
  785.  
  786. }
  787.  
  788. /**
  789. *
  790. * @param {string} channelId Example: UC0gmRdmpDWJ4dt7DAeRaawA
  791. * @returns {Promise<DisplayNameResultObject | null>}
  792. */
  793. async function getDisplayName(channelId) {
  794.  
  795. let cache = displayNameResStore.get(channelId);
  796. let isStackNewRequest = false;
  797.  
  798. /** @type {AsyncValue<DisplayNameResultObject> | null | undefined} */
  799. let aResult;
  800.  
  801. if (cache instanceof AsyncValue) {
  802. aResult = cache;
  803. if (aResult.fetchingState >= 2) {
  804. // aResult.fetchingState
  805. // 2: network fetch started
  806. // 3: network fetch ended
  807. // isStackNewRequest = false;
  808. } else {
  809. // 0: invalid before
  810. // 1: scheduled but not yet fetch
  811. isStackNewRequest = true;
  812. }
  813. } else if (cache) {
  814. // resolved result
  815. return cache;
  816. } else {
  817. // else no cached value found
  818. /** @type {AsyncValue<DisplayNameResultObject>} */
  819. aResult = new AsyncValue();
  820. displayNameResStore.set(channelId, aResult);
  821. isStackNewRequest = true;
  822. }
  823.  
  824. if (isStackNewRequest) {
  825. aResult.fetchingState = 1;
  826. stackNewRequest(channelId);
  827. }
  828. return await aResult.getValue();
  829. }
  830.  
  831.  
  832. /**
  833. *
  834. * @param {string} href Example: https\:\/\/www\.youtube\.com/channel/UC0gmRdmpDWJ4dt7DAeRaawA
  835. * @returns
  836. */
  837. const obtainChannelId = (href) => {
  838. let m = /\/channel\/(UC[-_a-zA-Z0-9+=.]+)/.exec(`/${href}`);
  839. // let m = /\/channel\/([^/?#\s]+)/.exec(`/${href}`);
  840. return !m ? '' : (m[1] || '');
  841. };
  842.  
  843.  
  844. /**
  845. *
  846. * @param {Element} ytElm
  847. */
  848. const resetWhenDataChanged = (ytElm) => {
  849. if (ytElm instanceof Element) {
  850. const anchors = elementQSA(ytElm, 'a[id][href*="channel/"][jkrgy]');
  851. if ((anchors || 0).length >= 1 && (ytElm.data || 0).jkrgx !== 1) {
  852. for (const anchor of anchors) {
  853. anchor.removeAttribute('jkrgy');
  854. }
  855. }
  856. }
  857. }
  858.  
  859. /**
  860. *
  861. * @param {Function} dataChanged original dataChanged function
  862. * @returns new dataChanged function
  863. */
  864. const dataChangeFuncProducer = (dataChanged) => {
  865. /** @this HTMLElement */
  866. return function () {
  867. resetWhenDataChanged(this);
  868. return dataChanged.apply(this, arguments);
  869. }
  870. };
  871.  
  872. /**
  873. *
  874. * @param {HTMLAnchorElement} anchor Example: https\:\/\/www\.youtube\.com/channel/UCRmLncxsQFcOOC8OhzUIfxQ/videos
  875. * @param {string} channelHref Example: /channel/UCRmLncxsQFcOOC8OhzUIfxQ
  876. * @param {string} channelId Example: UCRmLncxsQFcOOC8OhzUIfxQ
  877. * @returns
  878. */
  879. const anchorIntegrityCheck = (anchor, channelHref, channelId) => {
  880.  
  881. let currentHref = anchor.getAttribute('href');
  882. if (currentHref === channelHref) return true; // /channel/UCRmLncxsQFcOOC8OhzUIfxQ // /channel/UCRmLncxsQFcOOC8OhzUIfxQ
  883.  
  884. return (currentHref + '/').indexOf(channelHref + '/') >= 0;
  885.  
  886. };
  887.  
  888. /**
  889. *
  890. * @param {string} currentDisplayed
  891. * @param {DisplayNameResultObject} fetchResult
  892. * @returns
  893. */
  894. const verifyAndConvertHandle = (currentDisplayed, fetchResult) => {
  895.  
  896. const { title, externalId, ownerUrls, channelUrl, vanityChannelUrl, verified123 } = fetchResult;
  897.  
  898. const currentDisplayTrimmed = currentDisplayed.trim();
  899. let match = false;
  900. if (verified123) {
  901. match = true;
  902. } else if ((vanityChannelUrl || '').endsWith(`/${currentDisplayTrimmed}`)) {
  903. match = true;
  904. } else if ((ownerUrls || 0).length >= 1) {
  905. for (const ownerUrl of ownerUrls) {
  906. if ((ownerUrl || '').endsWith(`/${currentDisplayTrimmed}`)) {
  907. match = true;
  908. break;
  909. }
  910. }
  911. }
  912. if (match) {
  913. return currentDisplayTrimmed;
  914. }
  915. return '';
  916.  
  917. };
  918.  
  919. /**
  920. *
  921. *
  922.  
  923. ### [Handle naming guidelines](https://support.google.com/youtube/answer/11585688?hl=en&co=GENIE.Platform%3DAndroid)
  924.  
  925. - Is between 3-30 characters
  926. - Is made up of alphanumeric characters (A–Z, a–z, 0–9)
  927. - Your handle can also include: underscores (_), hyphens (-), dots (.)
  928. - Is not URL-like or phone number-like
  929. - Is not already being used
  930. - Follows YouTube's Community Guidelines
  931.  
  932. ### Handle automatically generated by YouTube
  933. - `@user-XXXX`
  934. - without dot (.)
  935. *
  936. * @param {string} text
  937. * @returns
  938. */
  939. const isDisplayAsHandle = (text) => {
  940.  
  941. if (typeof text !== 'string') return false;
  942. if (text.length < 4) return false;
  943. if (text.indexOf('@') < 0) return false;
  944. return /^\s*@[-_a-zA-Z0-9.]{3,30}\s*$/.test(text);
  945.  
  946. };
  947.  
  948. /**
  949. *
  950. * @param {Object[]} contentTexts
  951. * @param {number} idx
  952. * @returns
  953. */
  954. const contentTextProcess = (contentTexts, idx) => {
  955. const contentText = contentTexts[idx];
  956. const text = (contentText || 0).text;
  957. const url = (((contentText.navigationEndpoint || 0).commandMetadata || 0).webCommandMetadata || 0).url;
  958. if (typeof url === 'string' && typeof text === 'string') {
  959.  
  960. if (!isDisplayAsHandle(text)) return null;
  961. const channelId = obtainChannelId(url);
  962.  
  963. return getDisplayName(channelId).then(fetchResult => {
  964. let resolveResult = null;
  965. if (fetchResult) {
  966. // note: if that user shown is not found in `a[id]`, the hyperlink would not change
  967.  
  968. const textTrimmed = verifyAndConvertHandle(text, fetchResult);
  969. if (textTrimmed) {
  970. resolveResult = (md) => {
  971. let runs = ((md || 0).contentText || 0).runs;
  972. if (!runs || !runs[idx]) return;
  973. if (runs[idx].text !== text) return;
  974. runs[idx].text = text.replace(textTrimmed, `@${fetchResult.title}`); // HyperLink always @SomeOne
  975. md.contentText = Object.assign({}, md.contentText);
  976. };
  977. }
  978. }
  979. return resolveResult; // function as a Promise
  980. });
  981. }
  982.  
  983. return null;
  984. };
  985.  
  986. /**
  987. *
  988. * @param {ParentNode} parentNode
  989. * @param {Function} callback
  990. */
  991. function findTextNodes(parentNode, callback) {
  992. /**
  993. *
  994. * @param {Node} node
  995. * @returns
  996. */
  997. function traverse(node) {
  998. if (node.nodeType === Node.TEXT_NODE) {
  999. let r = callback(node);
  1000. if (r === true) return true;
  1001. }
  1002.  
  1003. let childNode = nodeFirstChild(node);
  1004. while (childNode) {
  1005. let r = traverse(childNode);
  1006. if (r === true) return true;
  1007. childNode = nodeNextSibling(childNode);
  1008. }
  1009. }
  1010.  
  1011. traverse(parentNode);
  1012. }
  1013.  
  1014. /**
  1015. *
  1016. * @param {ParentNode} parentNode
  1017. * @param {Object[]} contentTexts
  1018. * @param {number} aidx
  1019. * @returns
  1020. */
  1021. const mobileContentHandleAsync = async (parentNode, contentTexts, aidx) => {
  1022.  
  1023.  
  1024. let commentText = elementQS(parentNode, '.comment-text');
  1025. if (!commentText) return;
  1026. commentText = elementQS(commentText, '.yt-core-attributed-string') || commentText;
  1027. let currentText = commentText.textContent;
  1028. let textMatch = commentText.textContent === contentTexts.map(e => e.text).join('');
  1029. if (!textMatch) return;
  1030.  
  1031. const contentText = contentTexts[aidx];
  1032.  
  1033. /** @type {string} */
  1034. const handleText = contentText.text.trim();
  1035. /** @type {string | null | undefined} */
  1036. let channelId;
  1037.  
  1038. let cache = handleToChannelId.get(handleText);
  1039.  
  1040. if (typeof cache === 'string') {
  1041. channelId = cache;
  1042. } else if (cache instanceof AsyncValue) {
  1043. channelId = await cache.getValue();
  1044. } else {
  1045. /** @type {AsyncValue<string>} */
  1046. const asyncValue = new AsyncValue();
  1047. handleToChannelId.set(handleText, asyncValue);
  1048. channelId = await asyncValue.getValue(); // note: it could be never resolved
  1049. }
  1050.  
  1051. if (!channelId) return; // just in case
  1052.  
  1053. if (commentText.isConnected !== true) return; // already removed
  1054. if (commentText.textContent !== currentText) return; // already changed
  1055.  
  1056. const fetchResult = await getDisplayName(channelId);
  1057.  
  1058. if (fetchResult === null) return;
  1059.  
  1060. const { title, externalId } = fetchResult;
  1061. if (externalId !== channelId) return; // channel id must be the same
  1062.  
  1063. let search = contentText.text;
  1064. if (typeof search === 'string') {
  1065.  
  1066. const searchTrimmed = search.trim();
  1067. if (searchTrimmed === handleText) { // ensure integrity after getDisplayName
  1068. contentText.text = search.replace(searchTrimmed, "@" + title);
  1069.  
  1070. findTextNodes(commentText, (textnode) => {
  1071. if (textnode.nodeValue.indexOf(search) >= 0) {
  1072. textnode.nodeValue = textnode.nodeValue.replace(search, contentText.text);
  1073. return true;
  1074. }
  1075. });
  1076. }
  1077.  
  1078. }
  1079.  
  1080. }
  1081.  
  1082. /**
  1083. * Process Checking when there is new (unprocessed) anchor DOM element with hyperlink of channel
  1084. * @type { (anchor: HTMLElement, channelHref: string?, mt: string?) => Promise<void> }
  1085. */
  1086. const domCheckAsync = isMobile ? async (anchor, channelHref, mt) => {
  1087.  
  1088. try {
  1089. if (!channelHref || !mt) return;
  1090. let parentNode = nodeParent(anchor);
  1091. while (parentNode instanceof Node) {
  1092. if (parentNode.nodeName === 'YTM-COMMENT-RENDERER' || ('_commentData' in parentNode)) break;
  1093. parentNode = nodeParent(parentNode);
  1094. }
  1095. if (parentNode instanceof Node) { } else return;
  1096.  
  1097. let displayTextDOM = elementQS(parentNode, '.comment-header .comment-title');
  1098. if (!displayTextDOM) return;
  1099. displayTextDOM = elementQS(displayTextDOM, '.yt-core-attributed-string') || displayTextDOM;
  1100. let airaLabel = anchor.getAttribute('aria-label')
  1101. const parentNodeData = parentNode.data;
  1102. let runs = ((parentNodeData || 0).authorText || 0).runs;
  1103.  
  1104. if (displayTextDOM && airaLabel && displayTextDOM.textContent.trim() === airaLabel.trim() && isDisplayAsHandle(airaLabel) && runs && (runs[0] || 0).text === airaLabel) {
  1105.  
  1106. if (!channelIdToHandle.has(mt)) {
  1107. channelIdToHandle.set(mt, {
  1108. handleText: airaLabel.trim(),
  1109. justPossible: true
  1110. })
  1111. }
  1112.  
  1113. const fetchResult = await getDisplayName(mt);
  1114.  
  1115. if (fetchResult === null) return;
  1116.  
  1117. const { title, externalId } = fetchResult;
  1118.  
  1119. if (externalId !== mt) return; // channel id must be the same
  1120.  
  1121. // anchor href might be changed by external
  1122. if (!anchorIntegrityCheck(anchor, channelHref, externalId)) return;
  1123.  
  1124. let found = false;
  1125.  
  1126. findTextNodes(displayTextDOM, (textnode) => {
  1127. if (textnode.nodeValue === airaLabel) {
  1128. textnode.nodeValue = title;
  1129. found = true;
  1130. return true;
  1131. }
  1132. });
  1133.  
  1134. if (!found) return;
  1135. anchor.setAttribute('aria-label', title);
  1136. runs[0].text = title;
  1137.  
  1138.  
  1139. const contentTexts = (parentNodeData.contentText || 0).runs;
  1140. if (contentTexts && contentTexts.length >= 2) {
  1141. for (let aidx = 0; aidx < contentTexts.length; aidx++) {
  1142. const contentText = contentTexts[aidx] || 0;
  1143. if (contentText.italics !== true) {
  1144. let isHandle = isDisplayAsHandle(contentText.text);
  1145. if (isHandle) {
  1146. mobileContentHandleAsync(parentNode, contentTexts, aidx);
  1147. }
  1148. }
  1149. }
  1150. }
  1151.  
  1152. }
  1153. } catch (e) {
  1154. console.warn(e);
  1155. }
  1156.  
  1157. } : async (anchor, channelHref, mt) => {
  1158.  
  1159. try {
  1160. if (!channelHref || !mt) return;
  1161. let parentNode = nodeParent(anchor);
  1162. while (parentNode instanceof Node) {
  1163. if (typeof parentNode.is === 'string' && typeof parentNode.dataChanged === 'function') break;
  1164. parentNode = nodeParent(parentNode);
  1165. }
  1166. if (parentNode instanceof Node) { } else return;
  1167. const authorText = (parentNode.data || 0).authorText;
  1168. const currentDisplayed = (authorText || 0).simpleText;
  1169. if (typeof currentDisplayed !== 'string') return;
  1170. if (!isDisplayAsHandle(currentDisplayed)) return;
  1171.  
  1172. if (!channelIdToHandle.has(mt)) {
  1173. channelIdToHandle.set(mt, {
  1174. handleText: currentDisplayed.trim(),
  1175. justPossible: true
  1176. })
  1177. }
  1178.  
  1179. const oldDataChanged = parentNode.dataChanged;
  1180. if (typeof oldDataChanged === 'function' && !oldDataChanged.jkrgx) {
  1181. let newDataChanged = dataChangedFuncStore.get(oldDataChanged);
  1182. if (!newDataChanged) {
  1183. newDataChanged = dataChangeFuncProducer(oldDataChanged);
  1184. newDataChanged.jkrgx = 1;
  1185. dataChangedFuncStore.set(oldDataChanged, newDataChanged);
  1186. }
  1187. parentNode.dataChanged = newDataChanged;
  1188. }
  1189.  
  1190. const fetchResult = await getDisplayName(mt);
  1191.  
  1192. if (fetchResult === null) return;
  1193.  
  1194. const { title, externalId } = fetchResult;
  1195.  
  1196. if (externalId !== mt) return; // channel id must be the same
  1197.  
  1198. // anchor href might be changed by external
  1199. if (!anchorIntegrityCheck(anchor, channelHref, externalId)) return;
  1200.  
  1201. const parentNodeData = parentNode.data;
  1202. const funcPromises = [];
  1203.  
  1204. if (parentNode.isAttached === true && parentNode.isConnected === true && typeof parentNodeData === 'object' && parentNodeData && parentNodeData.authorText === authorText) {
  1205.  
  1206. if (authorText.simpleText !== currentDisplayed) return;
  1207. const currentDisplayTrimmed = verifyAndConvertHandle(currentDisplayed, fetchResult);
  1208. const cSimpleText = ((parentNodeData.authorText || 0).simpleText || '');
  1209. if (currentDisplayTrimmed && currentDisplayed !== title && cSimpleText === currentDisplayed) {
  1210.  
  1211. // the inside hyperlinks will be only converted if its parent author name is handle
  1212. const contentTexts = (parentNodeData.contentText || 0).runs;
  1213. if (contentTexts && contentTexts.length >= 1) {
  1214. for (let aidx = 0; aidx < contentTexts.length; aidx++) {
  1215. const r = contentTextProcess(contentTexts, aidx);
  1216. if (r instanceof Promise) funcPromises.push(r);
  1217. }
  1218. }
  1219.  
  1220. const md = Object.assign({}, parentNodeData);
  1221. let setBadge = false;
  1222. if (((((md.authorCommentBadge || 0).authorCommentBadgeRenderer || 0).authorText || 0).simpleText || '').trim() === cSimpleText.trim()) {
  1223. setBadge = true;
  1224. }
  1225. // parentNode.data = Object.assign({}, { jkrgx: 1 });
  1226. md.authorText = Object.assign({}, md.authorText, { simpleText: currentDisplayed.replace(currentDisplayTrimmed, title) });
  1227. if (setBadge) {
  1228. md.authorCommentBadge = Object.assign({}, md.authorCommentBadge);
  1229. md.authorCommentBadge.authorCommentBadgeRenderer = Object.assign({}, md.authorCommentBadge.authorCommentBadgeRenderer);
  1230. md.authorCommentBadge.authorCommentBadgeRenderer.authorText = Object.assign({}, md.authorCommentBadge.authorCommentBadgeRenderer.authorText, { simpleText: title });
  1231.  
  1232. }
  1233. if (funcPromises.length >= 1) {
  1234. let funcs = await Promise.all(funcPromises);
  1235.  
  1236. for (const func of funcs) {
  1237. if (typeof func === 'function') {
  1238. func(md);
  1239. }
  1240. }
  1241. }
  1242. parentNode.data = Object.assign({}, md, { jkrgx: 1 });
  1243. }
  1244.  
  1245. }
  1246. } catch (e) {
  1247. console.warn(e);
  1248. }
  1249.  
  1250. }
  1251.  
  1252. /**
  1253. *
  1254. * @param {string} url
  1255. * @returns
  1256. */
  1257. const channelUrlUnify = (url) => {
  1258.  
  1259. if (typeof url !== 'string') return url; // type unchanged
  1260. let c0 = url.charAt(0);
  1261. if (c0 === '/') return url;
  1262. let i = url.indexOf('.youtube.com/');
  1263. if (i >= 1) url = url.substring(i + '.youtube.com/'.length - 1);
  1264. else if (url.charAt(0) !== '/') url = '/' + url;
  1265. return url
  1266.  
  1267. }
  1268.  
  1269. let firstDOMCheck = false;
  1270. const firstDOMChecker = isMobile ? () => {
  1271.  
  1272. const channelNameDOM = document.querySelector('ytm-slim-owner-renderer');
  1273. const channelNameDOMData = (channelNameDOM || 0).data;
  1274.  
  1275. if (channelNameDOMData) {
  1276. let mainChannelUrl = null;
  1277. let mainFormattedNameUrl = null;
  1278. let mainChannelText = null;
  1279. let mainFormattedNameText = null;
  1280.  
  1281. try {
  1282. mainChannelUrl = channelUrlUnify(channelNameDOMData.channelUrl)
  1283. mainFormattedNameUrl = channelUrlUnify(channelNameDOMData.title.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl)
  1284. mainChannelText = channelNameDOMData.channelName
  1285. mainFormattedNameText = channelNameDOMData.title.runs[0].text
  1286. } catch (e) { }
  1287.  
  1288. if (mainChannelUrl === mainFormattedNameUrl && mainChannelText === mainFormattedNameText && typeof mainChannelUrl === 'string' && typeof mainChannelText === 'string') {
  1289.  
  1290.  
  1291. let channelId = obtainChannelId(mainChannelUrl);
  1292. if (channelId) {
  1293. if (mainChannelText.startsWith('@')) return;
  1294. if (mainChannelText.trim() !== mainChannelText) return;
  1295.  
  1296. displayNameResStore.set(channelId, {
  1297. channelUrl: `https://www.youtube.com/channel/${channelId}`,
  1298. externalId: `${channelId}`,
  1299. ownerUrls: [],
  1300. title: mainChannelText,
  1301. vanityChannelUrl: null,
  1302. verified123: true
  1303. });
  1304. }
  1305.  
  1306.  
  1307. }
  1308. }
  1309.  
  1310. } : () => {
  1311.  
  1312. const channelNameDOM = document.querySelector('ytd-channel-name.ytd-video-owner-renderer');
  1313. const channelNameDOMData = (channelNameDOM || 0).__data;
  1314. if (channelNameDOMData) {
  1315. let mainChannelUrl = null;
  1316. let mainFormattedNameUrl = null;
  1317. let mainChannelText = null;
  1318. let mainFormattedNameText = null;
  1319.  
  1320. try {
  1321. mainChannelUrl = channelNameDOMData.channelName.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl
  1322. mainFormattedNameUrl = channelNameDOMData.formattedName.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl
  1323. mainChannelText = channelNameDOMData.channelName.runs[0].text
  1324. mainFormattedNameText = channelNameDOMData.formattedName.runs[0].text
  1325. } catch (e) { }
  1326.  
  1327. if (mainChannelUrl === mainFormattedNameUrl && mainChannelText === mainFormattedNameText && typeof mainChannelUrl === 'string' && typeof mainChannelText === 'string') {
  1328.  
  1329. let m = /^\/(@[-_a-zA-Z0-9.]{3,30})$/.test(mainChannelUrl);
  1330.  
  1331. if (m && m[1] && mainChannelText !== m[1]) {
  1332.  
  1333. let channelId = obtainChannelId(mainChannelUrl);
  1334. if (channelId) {
  1335. if (mainChannelText.startsWith('@')) return;
  1336. if (mainChannelText.trim() !== mainChannelText) return;
  1337.  
  1338. displayNameResStore.set(channelId, {
  1339. channelUrl: `https://www.youtube.com/channel/${channelId}`,
  1340. externalId: `${channelId}`,
  1341. ownerUrls: [],
  1342. title: mainChannelText,
  1343. vanityChannelUrl: null,
  1344. verified123: true
  1345. });
  1346. }
  1347.  
  1348. }
  1349. }
  1350. }
  1351.  
  1352.  
  1353.  
  1354.  
  1355. };
  1356.  
  1357. // let newAnchorAdded = false;
  1358. /*
  1359.  
  1360. const intersectionobserver = new IntersectionObserver((entries) => {
  1361. let anchorAppear = false;
  1362. for (const entry of entries) {
  1363. if (entry.isIntersecting === true) {
  1364. anchorAppear = true;
  1365. break;
  1366. }
  1367. }
  1368. if (anchorAppear && newAnchorAdded) {
  1369. newAnchorAdded = false; // stacking will be only reset when one or more anchor added to DOM.
  1370. mutex.nextIndex = 0; // higher pirority for new elements being shown
  1371. }
  1372. }, {
  1373. rootMargin:"0px 0px 0px 0px",
  1374. threshold:1
  1375. })
  1376. */
  1377.  
  1378.  
  1379. /* globals WeakRef:false */
  1380.  
  1381. /** @type {(o: Object | null) => WeakRef | null} */
  1382. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  1383.  
  1384. /** @type {(wr: Object | null) => Object | null} */
  1385. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  1386.  
  1387. let lastNewAnchorLastWR = null;
  1388.  
  1389. let domCheckTimeStamp = 0;
  1390.  
  1391. const mutexForDOMMutations = new Mutex();
  1392. const domCheckSelector = isMobile ? 'a[aria-label^="@"][href*="channel/"]:not([jkrgy])' : 'a[id][href*="channel/"]:not([jkrgy])';
  1393.  
  1394. let domCheckScheduled = false;
  1395. const domChecker = () => {
  1396.  
  1397. if (domCheckScheduled) return;
  1398. if (document.querySelector(domCheckSelector) === null) {
  1399. return;
  1400. }
  1401. domCheckScheduled = true;
  1402.  
  1403. mutexForDOMMutations.lockWith(lockResolve => {
  1404. domCheckScheduled = false;
  1405.  
  1406. const newAnchors = document.querySelectorAll(domCheckSelector);
  1407. if (newAnchors.length === 0) {
  1408. lockResolve();
  1409. return;
  1410. }
  1411.  
  1412. const cNewAnchorFirst = newAnchors[0]; // non-null
  1413. const cNewAnchorLast = newAnchors[newAnchors.length - 1]; // non-null
  1414. /** @type {HTMLElement | null} */
  1415. const lastNewAnchorLast = kRef(lastNewAnchorLastWR); // HTMLElement | null
  1416. lastNewAnchorLastWR = mWeakRef(cNewAnchorLast);
  1417.  
  1418. const ltDOMCheck = domCheckTimeStamp;
  1419. const ctDOMCheck = Date.now();
  1420. domCheckTimeStamp = ctDOMCheck;
  1421.  
  1422. if (!firstDOMCheck) {
  1423. firstDOMCheck = true;
  1424. USE_CHANNEL_META && firstDOMChecker();
  1425. }
  1426.  
  1427. const isDocumentModifying = ctDOMCheck - ltDOMCheck < 230;
  1428.  
  1429. Promise.resolve()
  1430. .then(() => {
  1431. if (lastNewAnchorLast && mutex.nextIndex >= 1) {
  1432.  
  1433. if (isDocumentModifying) {
  1434.  
  1435. } else {
  1436. if ((lastNewAnchorLast.compareDocumentPosition(cNewAnchorFirst) & 2) === 2) {
  1437. mutex.nextIndex = 0;
  1438. }
  1439. }
  1440.  
  1441. /*
  1442. if (mutex.nextIndex === 0) {
  1443. // no change
  1444. } else if ((lastNewAnchorLast.compareDocumentPosition(cNewAnchorLast) & 2) === 2) { // when "XX replies" clicked
  1445. mutex.nextIndex = 0; // highest priority
  1446. } else if (cNewAnchorLast !== cNewAnchorFirst && (lastNewAnchorLast.compareDocumentPosition(cNewAnchorFirst) & 2) === 1) { // rarely
  1447. mutex.nextIndex = Math.floor(mutex.nextIndex / 2); // relatively higher priority
  1448. }
  1449. */
  1450. }
  1451. }).then(() => {
  1452.  
  1453. // newAnchorAdded = true;
  1454. for (const anchor of newAnchors) {
  1455. // author-text or name
  1456. // normal url: /channel/xxxxxxx
  1457. // Improve YouTube! - https://www.youtube.com/channel/xxxxxxx/videos
  1458. const href = anchor.getAttribute('href');
  1459. const channelId = obtainChannelId(href); // string, can be empty
  1460. anchor.setAttribute('jkrgy', channelId);
  1461. // intersectionobserver.unobserve(anchor);
  1462. // intersectionobserver.observe(anchor); // force first occurance
  1463. domCheckAsync(anchor, href, channelId);
  1464. }
  1465.  
  1466. }).then(lockResolve).catch(e => {
  1467. lockResolve();
  1468. console.warn(e);
  1469. });
  1470.  
  1471. });
  1472.  
  1473.  
  1474. };
  1475.  
  1476.  
  1477. /** @type {MutationObserver | null} */
  1478. let domObserver = null;
  1479.  
  1480. /** @type {(callback: (event: Event)=>{})} */
  1481. const onPageFetched = isMobile ? (callback) => {
  1482.  
  1483. let state = 0;
  1484.  
  1485. window.addEventListener('state-change', function (evt) {
  1486. if (state === 1) return; // ignore intermediate events
  1487. state = 1; // before value: 0 or 2;
  1488. callback(evt);
  1489. }, false);
  1490.  
  1491. window.addEventListener('state-navigateend', function (evt) { // fallback
  1492. let inSeq = state === 1;
  1493. state = 2; // 2 when loaded
  1494. if (!inSeq) callback(evt);
  1495. }, false);
  1496.  
  1497. } : (callback) => {
  1498.  
  1499. let state = 0;
  1500.  
  1501. document.addEventListener('yt-page-data-fetched', function (evt) {
  1502. if (state === 1) return; // ignore intermediate events if any
  1503. state = 1; // before value: 0 or 2;
  1504. callback(evt);
  1505. }, false);
  1506.  
  1507. document.addEventListener('yt-navigate-finish', function (evt) { // fallback
  1508. let inSeq = state === 1;
  1509. state = 2; // 2 when loaded
  1510. if (!inSeq) callback(evt);
  1511. }, false);
  1512.  
  1513. };
  1514.  
  1515. /**
  1516. *
  1517. * @param {Node} app
  1518. * @returns
  1519. */
  1520. function setupOnPageFetched(app) {
  1521.  
  1522. firstDOMCheck = false;
  1523.  
  1524. if (!cfg['INNERTUBE_API_KEY']) {
  1525. console.warn("Userscript Error: INNERTUBE_API_KEY is not found.");
  1526. return;
  1527. }
  1528.  
  1529. if (!domObserver) {
  1530. domObserver = new MutationObserver(domChecker);
  1531. } else {
  1532. domObserver.takeRecords();
  1533. domObserver.disconnect();
  1534. }
  1535.  
  1536. domObserver.observe(app, { childList: true, subtree: true });
  1537. domChecker();
  1538.  
  1539. }
  1540.  
  1541. /**
  1542. *
  1543. * @param {Object|null} dest
  1544. * @param {Object} ytcfg
  1545. * @param {string[]} keys
  1546. * @returns
  1547. */
  1548. function getYtConfig(dest, ytcfg, keys) {
  1549. dest = dest || {};
  1550. if (typeof ytcfg.get === 'function') { // mobile
  1551. for (const key of keys) {
  1552. dest[key] = ytcfg.get(key);
  1553. }
  1554. } else {
  1555. const cfgData = ytcfg.data_ || ytcfg.__data || ytcfg._data || ytcfg.data || ytcfg; // dekstop - ytcfg.data_
  1556. for (const key of keys) {
  1557. dest[key] = cfgData[key];
  1558. }
  1559. }
  1560. return dest;
  1561. }
  1562.  
  1563. /** @type { (evt: Object) => Element | null } */
  1564. const getPageApp = isMobile
  1565. ? ((evt) => (document.querySelector('ytm-app#app') || document.querySelector('ytm-app')))
  1566. : ((evt) => ((evt || 0).target || document.querySelector('ytd-app')));
  1567.  
  1568. onPageFetched(async (evt) => {
  1569. const app = getPageApp(evt);
  1570. console.assert(app);
  1571. const ytcfg = ((window || 0).ytcfg || 0);
  1572. getYtConfig(cfg, ytcfg, ['INNERTUBE_API_KEY', 'INNERTUBE_CLIENT_VERSION']);
  1573. setupOnPageFetched(app);
  1574. });
  1575.  
  1576. })({ Promise, fetch, JSON, Request, AbortController, setTimeout, clearTimeout });