Restore YouTube Username from Handle to Custom

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

当前为 2023-09-22 提交的版本,查看 最新版本

  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.9.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 Violentmonkey
  45. // @compatible firefox Tampermonkey
  46. // @compatible firefox Firemonkey
  47. // @compatible chrome Violentmonkey
  48. // @compatible chrome Tampermonkey
  49. // @compatible opera Violentmonkey
  50. // @compatible opera Tampermonkey
  51. // @compatible safari Stay
  52. // @compatible edge Violentmonkey
  53. // @compatible edge Tampermonkey
  54. // @compatible brave Violentmonkey
  55. // @compatible brave Tampermonkey
  56.  
  57. // @description To restore YouTube Username to the traditional custom name
  58. // @description:ja YouTubeのユーザー名を伝統的なカスタム名に復元するために。
  59. // @description:zh-TW 將 YouTube 使用者名稱從 Handle 恢復為自訂名稱
  60. // @description:zh-CN 将 YouTube 用户名从 Handle 恢复为自定义名称
  61.  
  62. // @description:ko YouTube 사용자 이름을 전통적인 사용자 지정 이름으로 복원합니다.
  63. // @description:ru Восстановление имени пользователя YouTube с помощью обычного настраиваемого имени
  64. // @description:af Herstel YouTube-gebruikersnaam vanaf Handvat na Aangepaste Naam
  65. // @description:az YouTube İstifadəçi Adını Özəl Adından İstifadə Etmək
  66. // @description:id Mengembalikan Nama Pengguna YouTube dari Handle ke Kustom
  67. // @description:ms Pulihkan Nama Pengguna YouTube dari Handle ke Tersuai
  68. // @description:bs Vrati YouTube korisničko ime sa ručke na prilagođeno ime
  69. // @description:ca Restaurar el nom d'usuari de YouTube de la màniga al personalitzat
  70. // @description:cs Obnovte uživatelské jméno YouTube z rukojeti na vlastní
  71. // @description:da Gendan YouTube-brugernavn fra Håndtag til Brugerdefineret
  72. // @description:de Stellt den YouTube-Benutzernamen von Handle auf Benutzerdefiniert wieder her
  73. // @description:et Taasta YouTube'i kasutajanimi käepidemelt kohandatud nimeks
  74. // @description:es Restaurar el nombre de usuario de YouTube de la manija al personalizado
  75. // @description:eu Berrezarri YouTube Erabiltzaile-izena Manipulatik Pertsonalizatuera
  76. // @description:fr Restaurer le nom d'utilisateur YouTube de la poignée au nom personnalisé
  77. // @description:gl Restaura o nome de usuario de YouTube da manexa ao personalizado
  78. // @description:hr Vrati YouTube korisničko ime s ručke na prilagođeno ime
  79. // @description:zu Hlanganisa Igama Lokusebenza lwe-YouTube kusuka kwi-Handle kuze kube kusebenza kakhulu
  80. // @description:is Endurheimtu YouTube Notandanafn frá Handfangi til Sérsniðins
  81. // @description:it Ripristina il nome utente di YouTube da Handle a Personalizzato
  82. // @description:sw Rejesha Jina la Mtumiaji wa YouTube kutoka kwa Kishughulikia hadi Desturi
  83. // @description:lv Atjaunot YouTube lietotāja vārdu no roktura uz pielāgotu
  84. // @description:lt Atkurti „YouTube“ naudotojo vardą iš rankenos į tinkamą
  85. // @description:hu Visszaállítja a YouTube felhasználónevet a fogantyútól a testreszabottra
  86. // @description:nl Herstel YouTube-gebruikersnaam van Handle naar Aangepaste naam
  87. // @description:uz YouTube foydalanuvchining nomini Hurda dan Shaxsiylashtirilgan nomga qaytarish
  88. // @description:pl Przywróć nazwę użytkownika YouTube z uchwytu na niestandardową
  89. // @description:pt Restaurar o nome de usuário do YouTube de Handle para Personalizado
  90. // @description:pt-BR Restaurar o nome de usuário do YouTube de Handle para Personalizado
  91. // @description:ro Restaurați Numele de Utilizator YouTube de la Mâner la Personalizat
  92. // @description:sq Rikthe emrin e përdoruesit të YouTube nga Maja në Personalizuar
  93. // @description:sk Obnovte používateľské meno YouTube z rukoväte na vlastné
  94. // @description:sl Obnovite uporabniško ime YouTube iz ročaja v prilagojeno ime
  95. // @description:sr Вратите YouTube корисничко име са ручке на прилагођено име
  96. // @description:fi Palauta YouTube-käyttäjänimi kahvasta räätälöityyn nimeen
  97. // @description:sv Återställ YouTube-användarnamnet från handtaget till anpassat namn
  98. // @description:vi Khôi phục Tên người dùng YouTube từ Tay cầm thành Tùy chỉnh
  99. // @description:tr Özel İsme Kullanıcı Adını İade Etme
  100. // @description:be Вярнуць імя карыстальніка YouTube з ручкі на наладжваецца
  101. // @description:bg Възстановяване на потребителско име на YouTube от дръжка до персонализирано име
  102. // @description:ky YouTube колдонуучунун атты боюнча атын тамашалау
  103. // @description:kk YouTube пайдаланушының атын Handle танымасынан Жеке атауға қайта түсіру
  104. // @description:mk Врати го името на YouTube корисникот од држачот во прилагодено име
  105. // @description:mn YouTube хэрэглэгчийн нэрийг хүчирхийлэгчидээс Байгууллагад нь солих
  106. // @description:uk Відновлення імені користувача YouTube з ручки на налаштоване ім'я
  107. // @description:el Επαναφορά Ονόματος Χρήστη YouTube από τη Λαβή σε Προσαρμοσμένο
  108. // @description:hy Վերականգնել YouTube-ի Օգտագործողի Անունը Ձեռքից Հատուցվածությանով
  109. // @description:ur ہینڈل سے یوٹیوب یوزر نیم کو کسٹم میں بحال کریں
  110. // @description:ar استعادة اسم مستخدم YouTube من المقبض إلى مخصص
  111. // @description:fa بازیابی نام کاربری یوتیوب از دستگیره به سفارشی
  112. // @description:ne ह्यान्डलबाट यूट्युब प्रयोगकर्तानाम अनुकूलमा उपयोग गर्नुहोस्
  113. // @description:mr हॅंडलमधून यूट्यूब वापरकर्तानाव अनुकूलित करा
  114. // @description:hi हैंडल से यूट्यूब यूजरनेम को कस्टम में बदलें
  115. // @description:as কাস্টম হতে হ্যান্ডেল পৰা ইউটিউব ব্যৱহাৰকাৰী নাম পুনৰ্স্থাপন কৰক
  116. // @description:bn হ্যান্ডেল থেকে কাস্টম করে ইউটিউব ব্যবহারকারীর নাম পুনরুদ্ধার করুন
  117. // @description:pa ਯੂਟਿਊਬ ਯੂਜਰਨਾਂ ਦਾ ਨਾਂ ਹੈਂਡਲ ਤੋਂ ਕਸਟਮ ਵਿੱਚ ਮੁੜ ਲਾਓ
  118. // @description:gu હેન્ડલથી કસ્ટમમાંથી YouTube વપરાશકર્તાનું નામ પુનર્સ્થાપિત કરો
  119. // @description:or କଷ୍ଟମରେ ହେନ୍ଡେଲରୁ YouTube ବ୍ୟବହାରକାରୀଙ୍କ ନାମ ପୁନର୍ପ୍ରାପ୍ତ କରନ୍ତୁ
  120. // @description:ta ஹேண்டில் இருந்து கஸ்டம் ஆக்கும் யூடியூப் பயனர்பெயரை மீட்டமைக்கவும்
  121. // @description:te హాండును నుంచి YouTube వాడుకరి పేరును కస్టమ్‌కు పునరుద్ధరించండి
  122. // @description:kn ಹ್ಯಾಂಡಲ್‌ನಿಂದ YouTube ಬಳಕೆದಾರ ಹೆಸರನ್ನು ಅನುಕೂಲಿತ ಮಾಡಿ
  123. // @description:ml ഹാന്റിൽ നിന്ന് കസ്റ്റം ആക്കാംമെന്ന് യുട്യൂബ് ഉപയോക്തൃനാമം മറ്റുള്ളവരെ മാറ്റുക
  124. // @description:si හැන්ඩල් සහිතව YouTube භාවිතයේ පරිශීලක නාමය ස්වයං කරයි
  125. // @description:th กู้คืนชื่อผู้ใช้ YouTube จากแฮนดิลไปเป็นชื่อที่กำหนดเอง
  126. // @description:lo ເຮັດຊະນິດຊື່ຜູ້ໃຊ້ YouTube ຈາກ Handle ຈົດໄວ້ເປັນຊື່ປັດຈຸບັນ
  127. // @description:my Handle မှ YouTube အသုံးပြုသူအမည်ကို စတင်ပြန်စစ်ဆေးပါ
  128. // @description:ka YouTube მომხმარებლის სახელის აღდგენა ხანდლიდან მორგებულ სახელში
  129. // @description:am ስለ YouTube የተጠቃሚ ስም ማስቀመጫዎቹን ከ Handle ወደ Custom ውስጥ እንደሚመጣ ይረዳሉ
  130. // @description:km កំណត់ YouTube ពាក្យឈ្មោះពី Handle ទៅជាឈ្មោះផ្ទាល់ខ្លួន
  131.  
  132. // ==/UserScript==
  133.  
  134. /* jshint esversion:8 */
  135.  
  136. /**
  137. @typedef {string} HandleText
  138. * \@[-_a-zA-Z0-9.]{3,30}
  139. */
  140.  
  141. /**
  142. @typedef {string} ChannelId
  143. UC[-_a-zA-Z0-9+=.]+
  144. */
  145.  
  146. /**
  147. @typedef {string} DisplayName
  148. * Display Name
  149. */
  150.  
  151. /**
  152. @typedef DisplayNameResultObject
  153. @type {Object}
  154. @property {DisplayName} title
  155. @property {DisplayName} langTitle
  156. @property {ChannelId?} externalId
  157. @property {string[]} ownerUrls
  158. @property {string} channelUrl
  159. @property {string} vanityChannelUrl
  160. @property {boolean|undefined} verified123
  161. */
  162.  
  163. /**
  164. @typedef ChannelIdToHandleResult
  165. @type {Object}
  166. @property {HandleText} handleText
  167. @property {boolean} justPossible
  168. */
  169.  
  170. (function (__CONTEXT__) {
  171. 'use strict';
  172.  
  173. const USE_RSS_FETCHER = true;
  174. const USE_TIMEOUT_SIGNAL = false;
  175. const USE_CHANNEL_META = true;
  176. const CHANGE_FOR_SHORTS_CHANNEL_NAME = false;
  177. const USE_LANG_SPECIFIC_NAME = true;
  178. const UPDATE_PIN_NAME = true; // for USE_LANG_SPECIFIC_NAME
  179. const FIX_RTL_ISSUE = true;
  180.  
  181. /** @type {globalThis.PromiseConstructor} */
  182. const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  183.  
  184. const { fetch, JSON, Request, AbortController, setTimeout, clearTimeout } = __CONTEXT__; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  185.  
  186. const timeoutSignal = AbortController && USE_TIMEOUT_SIGNAL ? (timeout) => {
  187. let controller = new AbortController();
  188. const r = controller.signal;
  189. let cid = setTimeout(() => {
  190. if (cid >= 1) {
  191. cid = 0;
  192. controller && controller.abort();
  193. controller = null;
  194. }
  195. }, timeout);
  196. r.clearTimeout = () => {
  197. if (cid >= 1) {
  198. clearTimeout(cid);
  199. cid = 0;
  200. }
  201. }
  202. return r;
  203. } : () => { };
  204.  
  205. const fxOperator = (proto, propertyName) => {
  206. let propertyDescriptorGetter = null;
  207. try {
  208. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  209. // https://github.com/cyfung1031/userscript-supports/issues/9
  210. // .parentNode becomes DocumentFragment
  211. /**
  212. *
  213. * Issue Description: YtCustomElements - Custom DOM Operations overrided in FireFox
  214. *
  215. * e.g. ytd-comment-renderer#comment > div#body
  216. *
  217. * ${div#body}.parentNode = DocumentFragment <Node.parentNode>
  218. * ${ytd-comment-renderer#comment}.firstElementChild <Element.firstElementChild>
  219. *
  220. * Cofirmed Affected: parentNode, firstChild, firstElementChild, lastChild, lastElementChild, querySelector, & querySelectorAll
  221. *
  222. * Alternative way: ytCustomElement.$.xxxxxxx
  223. *
  224. */
  225. } catch (e) { }
  226. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  227. };
  228.  
  229. const fxAPI = (proto, propertyName) => {
  230. const methodFunc = proto[propertyName];
  231. return typeof methodFunc === 'function' ? (e, ...args) => methodFunc.apply(e, args) : (e, ...args) => e[propertyName](...args);
  232. };
  233.  
  234. /** @type { (node: Node)=>Node | null } */
  235. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  236. /** @type { (node: Node)=>Node | null } */
  237. const nodeFirstChild = fxOperator(Node.prototype, 'firstChild');
  238. /** @type { (node: Node)=>Node | null } */
  239. const nodeNextSibling = fxOperator(Node.prototype, 'nextSibling');
  240.  
  241. /** @type { (node: ParentNode, selector: string)=>Element | null } */
  242. const elementQS = fxAPI(Element.prototype, 'querySelector');
  243. /** @type { (node: ParentNode, selector: string)=>Element[] } */
  244. const elementQSA = fxAPI(Element.prototype, 'querySelectorAll');
  245.  
  246. /*
  247.  
  248.  
  249. init
  250.  
  251. initialdata
  252. state-progress
  253. state-responsereceived
  254. player-autonav-pause
  255. state-change
  256. state-navigateend
  257. player-initialized
  258. renderer-module-load-start
  259. video-data-change
  260. player-state-change
  261. updateui
  262. renderer-module-load-end -> channel owner DOM available
  263. player-autonav-pause
  264. updateui
  265. renderer-module-load-start
  266. updateui
  267. renderer-module-load-end
  268.  
  269.  
  270. playing
  271.  
  272. h5player.video-progress
  273. h5player.video-progress
  274. h5player.video-progress
  275. h5player.video-progress
  276. ...
  277.  
  278.  
  279.  
  280.  
  281. navigate new video
  282.  
  283. state-navigatestart
  284. state-change => channel owener DOM vanished
  285. state-progress
  286. ...
  287. h5player.player-state-change
  288. h5player.video-data-change
  289. h5player.player-state-change
  290. h5player.muted-autoplay-change
  291. h5player.volume-change
  292. h5player.video-data-change
  293. h5player.volume-change
  294. ...
  295.  
  296. state-progress
  297. ...
  298.  
  299. state-progress => channel owner DOM appear [just before state-responsereceived]
  300. state-responsereceived
  301.  
  302. video-data-change
  303. state-change
  304. state-navigateend
  305.  
  306.  
  307.  
  308.  
  309. UX interaction
  310.  
  311. user-activity
  312. player-autonav-pause
  313.  
  314.  
  315.  
  316.  
  317.  
  318. EventTarget.prototype.uhfsE = EventTarget.prototype.dispatchEvent
  319.  
  320. EventTarget.prototype.dispatchEvent = function (ev) {
  321. if (ev instanceof Event) {
  322. if (ev.type === 'video-progress') { } else {
  323. console.log(ev.type, this, ev.target)
  324. if (ev.target === null) {
  325. console.log(ev.type, (document.querySelector('ytm-app') || 0).data)
  326. console.log(ev.type, (document.querySelector('ytm-slim-owner-renderer') || 0).textContent)
  327. }
  328. }
  329. }
  330. return this.uhfsE.apply(this, arguments);
  331. }
  332.  
  333.  
  334.  
  335. */
  336.  
  337. const isMobile = location.hostname === 'm.youtube.com';
  338.  
  339. const cfg = {};
  340. class Mutex {
  341.  
  342. constructor() {
  343. this.p = Promise.resolve()
  344. }
  345.  
  346. /**
  347. * @param {(lockResolve: () => void)} f
  348. */
  349. lockWith(f) {
  350. this.p = this.p.then(() => new Promise(f).catch(console.warn))
  351. }
  352.  
  353. }
  354.  
  355. class OrderedMutex extends Mutex {
  356. constructor() {
  357. super();
  358. this.nextIndex = 0;
  359. this.arr = [];
  360. this.lockFunc = resolve => {
  361. if (this.arr.length >= 1) {
  362. let f = this.arr[0];
  363. if (typeof f === 'function') {
  364. this.arr.shift();
  365. if (this.nextIndex > 0) this.nextIndex--;
  366. f(resolve);
  367. return;
  368. } else {
  369. // reset if unknown error
  370. this.arr.length = 0;
  371. this.nextIndex = 0;
  372. }
  373. }
  374. resolve();
  375. };
  376. }
  377. /**
  378. * @param {(lockResolve: () => void)} f
  379. */
  380. add(f) {
  381. if (this.nextIndex === this.arr.length) {
  382. this.arr.push(f); this.nextIndex++;
  383. } else {
  384. this.arr.splice(this.nextIndex++, 0, f);
  385. }
  386. this.lockWith(this.lockFunc);
  387. }
  388. }
  389. // usage: run network request one by one
  390. const mutex = new OrderedMutex();
  391.  
  392. /**
  393. * @typedef { AsyncValue<T> | T} AsyncOrResolvedValue<T>
  394. * @template T */
  395.  
  396. /**
  397. * usage: cache the network result per web application instance.
  398. * @type {Map<ChannelId, AsyncOrResolvedValue<DisplayNameResultObject>>} */
  399. const displayNameResStore = new Map();
  400.  
  401. /**
  402. * usage: mapping the .dataChanged() to the wrapped function - only few entries as most are {ytd-comment-renderer}.dataChanged().
  403. * @type {WeakMap<Function, Function>} */
  404. const dataChangedFuncStore = new WeakMap();
  405.  
  406. /**
  407. * usage: for \@Mention inside comments in YouTube Mobile that without hyperlinks for channelId.
  408. * @type {Map<HandleText, AsyncOrResolvedValue<ChannelId>>} */
  409. const handleToChannelId = new Map();
  410.  
  411. /**
  412. * usage: in RSS fetching, no handle text will be obtained from the response. inject the handle into the response.
  413. * @type {Map<ChannelId, ChannelIdToHandleResult>} */
  414. const channelIdToHandle = new Map();
  415.  
  416. /**
  417. * usage: replace handle to lang-specific display name
  418. * @type {Map<HandleText, DisplayName>} */
  419. const langPreferredDisplayNameMap = new Map();
  420.  
  421. /**
  422. * AsyncValue
  423. * @class
  424. * @template T
  425. */
  426. class AsyncValue {
  427. constructor() {
  428. /** @type {T | null} */
  429. this.__value__ = null;
  430. /** @type {((value: any)=>void) | null} */
  431. this.__resolve__ = null;
  432. this.__promise__ = new Promise(resolve => {
  433. this.__resolve__ = resolve;
  434. });
  435. }
  436. /**
  437. *
  438. * @param {T} value
  439. */
  440. setValue(value) {
  441. const promise = this.__promise__;
  442. if (promise === null) throw 'Value has already been set.';
  443. this.__value__ = value;
  444. this.__promise__ = null;
  445. this.__resolve__ ? this.__resolve__() : Promise.resolve().then(() => this.__resolve__());
  446. }
  447. /**
  448. *
  449. * @returns {Promise<T>}
  450. */
  451. async getValue() {
  452. const promise = this.__promise__;
  453. if (promise === null) return this.__value__;
  454. await promise; // promise become null
  455. this.__promise__ = null; // just in case
  456. this.__resolve__ = null;
  457. return this.__value__;
  458. }
  459. }
  460.  
  461. let isCSSAdded = false;
  462. const addCSSRulesIfRequired = () => {
  463. if (isCSSAdded) return;
  464. isCSSAdded = true;
  465. const style = document.createElement('style')
  466. let nonce = document.querySelector('head style[nonce]');
  467. nonce = nonce ? nonce.getAttribute('nonce') : null;
  468.  
  469. /* #contenteditable-root.yt-formatted-string:lang(he) */ /* Hebrew (LTR, when mixed with LTR languages) */
  470. /* #contenteditable-root.yt-formatted-string:lang(he) */ /* Hebrew (pure) */
  471.  
  472. const cssText01_FIX_RTL_ISSUE = FIX_RTL_ISSUE ? `
  473.  
  474. /* Left-to-Right (LTR) Languages */
  475. #contenteditable-root.yt-formatted-string:lang(en) /* English */,
  476. #contenteditable-root.yt-formatted-string:lang(es) /* Spanish */,
  477. #contenteditable-root.yt-formatted-string:lang(fr) /* French */,
  478. #contenteditable-root.yt-formatted-string:lang(de) /* German */,
  479. #contenteditable-root.yt-formatted-string:lang(it) /* Italian */,
  480. #contenteditable-root.yt-formatted-string:lang(pt) /* Portuguese */,
  481. #contenteditable-root.yt-formatted-string:lang(ru) /* Russian */,
  482. #contenteditable-root.yt-formatted-string:lang(zh) /* Chinese */,
  483. #contenteditable-root.yt-formatted-string:lang(ja) /* Japanese */,
  484. #contenteditable-root.yt-formatted-string:lang(ko) /* Korean */,
  485. #contenteditable-root.yt-formatted-string:lang(nl) /* Dutch */,
  486. #contenteditable-root.yt-formatted-string:lang(sv) /* Swedish */,
  487. #contenteditable-root.yt-formatted-string:lang(fi) /* Finnish */,
  488. #contenteditable-root.yt-formatted-string:lang(da) /* Danish */,
  489. #contenteditable-root.yt-formatted-string:lang(no) /* Norwegian */,
  490. #contenteditable-root.yt-formatted-string:lang(pl) /* Polish */,
  491. #contenteditable-root.yt-formatted-string:lang(cs) /* Czech */,
  492. #contenteditable-root.yt-formatted-string:lang(sk) /* Slovak */,
  493. #contenteditable-root.yt-formatted-string:lang(hu) /* Hungarian */,
  494. #contenteditable-root.yt-formatted-string:lang(tr) /* Turkish */,
  495. #contenteditable-root.yt-formatted-string:lang(el) /* Greek */,
  496. #contenteditable-root.yt-formatted-string:lang(id) /* Indonesian */,
  497. #contenteditable-root.yt-formatted-string:lang(ms) /* Malay */,
  498. #contenteditable-root.yt-formatted-string:lang(th) /* Thai */,
  499. #contenteditable-root.yt-formatted-string:lang(vi) /* Vietnamese */
  500. {
  501. direction: ltr;
  502. }
  503.  
  504. /* Right-to-Left (RTL) Languages */
  505. #contenteditable-root.yt-formatted-string:lang(ar) /* Arabic */,
  506. #contenteditable-root.yt-formatted-string:lang(fa) /* Persian */,
  507. #contenteditable-root.yt-formatted-string:lang(ur) /* Urdu */,
  508. #contenteditable-root.yt-formatted-string:lang(dv) /* Divehi */
  509. {
  510. direction: rtl;
  511. }
  512.  
  513. ` : '';
  514. style.textContent = `
  515. ${cssText01_FIX_RTL_ISSUE}
  516. `.trim();
  517. if (nonce) style.setAttribute('nonce', nonce);
  518. document.head.appendChild(style);
  519. }
  520.  
  521. /**
  522. *
  523. * @param {DisplayNameResultObject} resultInfo
  524. * @param {string} channelId
  525. */
  526. const cacheHandleToChannel = (resultInfo, channelId) => {
  527.  
  528. const { vanityChannelUrl, ownerUrls } = resultInfo;
  529.  
  530. let handleText = urlToHandle(vanityChannelUrl || '');
  531.  
  532. if (handleText) {
  533. // match = true;
  534. } else if ((ownerUrls || 0).length >= 1) {
  535. for (const ownerUrl of ownerUrls) {
  536. handleText = urlToHandle(ownerUrl || '');
  537.  
  538. if (handleText) {
  539. // match = true;
  540. break;
  541. }
  542. }
  543. }
  544. if (handleText) {
  545.  
  546. const asyncValue = handleToChannelId.get(handleText); // nothing if no pending promise
  547. if (asyncValue instanceof AsyncValue) {
  548. asyncValue.setValue(channelId);
  549. }
  550. handleToChannelId.set(handleText, channelId);
  551. }
  552.  
  553.  
  554. }
  555.  
  556. /**
  557. *
  558. * @param {string} channelId
  559. * @param {Function} onDownloaded
  560. * @param {Function} onResulted
  561. * @param {Function} onError
  562. */
  563. const fetcherBrowseAPI = (channelId, onDownloaded, onResulted, onError) => {
  564.  
  565. let signal = timeoutSignal(4000);
  566.  
  567. const requestBody = {
  568. "context": {
  569. "client": {
  570. "visitorData": "Cgs0aVg0VjFWM0U0USi0jvOkBg%3D%3D", // [optional] fake visitorData to avoid dynamic visitorData generated in response
  571. "clientName": "MWEB", // "WEB", "MWEB"
  572. "clientVersion": `${cfg.INNERTUBE_CLIENT_VERSION || '2.20230614.01.00'}`, // same as WEB version
  573. "originalUrl": `https://m.youtube.com/channel/${channelId}`,
  574. "playerType": "UNIPLAYER",
  575. "platform": "MOBILE", // "DESKTOP", "MOBILE", "TABLET"
  576. "clientFormFactor": "SMALL_FORM_FACTOR", // "LARGE_FORM_FACTOR", "SMALL_FORM_FACTOR"
  577. "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",
  578. "mainAppWebInfo": {
  579. "graftUrl": `/channel/${channelId}`,
  580. "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER",
  581. "isWebNativeShareAvailable": true
  582. }
  583. },
  584. "user": {
  585. "lockedSafetyMode": false
  586. },
  587. "request": {
  588. "useSsl": true,
  589. "internalExperimentFlags": [],
  590. "consistencyTokenJars": []
  591. }
  592. },
  593. "browseId": `${channelId}`
  594. };
  595.  
  596. if (USE_LANG_SPECIFIC_NAME && cfg.HL && cfg.GL) {
  597. Object.assign(requestBody.context.client, {
  598. "hl": cfg.HL,
  599. "gl": cfg.GL,
  600. });
  601. }
  602.  
  603. fetch(new Request(`/youtubei/v1/browse?key=${cfg.INNERTUBE_API_KEY}&prettyPrint=false`, {
  604. "method": "POST",
  605. "mode": "same-origin",
  606. "credentials": "omit",
  607.  
  608. referrerPolicy: "no-referrer",
  609. cache: "default", // no effect on POST request
  610. // cache: "force-cache",
  611. redirect: "error", // there shall be no redirection in this API request
  612. integrity: "",
  613. keepalive: false,
  614. signal,
  615.  
  616. "headers": {
  617. "Content-Type": "application/json", // content type of the body data in POST request
  618. "Accept-Encoding": "gzip, deflate, br", // YouTube Response - br
  619. // X-Youtube-Bootstrap-Logged-In: false,
  620. // X-Youtube-Client-Name: 1, // INNERTUBE_CONTEXT_CLIENT_NAME
  621. // X-Youtube-Client-Version: "2.20230622.06.00" // INNERTUBE_CONTEXT_CLIENT_VERSION
  622.  
  623. "Accept": "application/json",
  624. },
  625. "body": JSON.stringify(requestBody)
  626. })).then(res => {
  627. signal && signal.clearTimeout && signal.clearTimeout();
  628. signal = null;
  629. onDownloaded();
  630. onDownloaded = null;
  631. return res.json();
  632. }).then(resJson => {
  633. let resultInfo = ((resJson || 0).metadata || 0).channelMetadataRenderer;
  634. let title = resultInfo.title;
  635. if (title) {
  636.  
  637. resultInfo.title = '';
  638. resultInfo.langTitle = title;
  639.  
  640. if (cfg.ownerProfileUrl && cfg.ownerChannelName && cfg.ownerChannelName !== title) {
  641. let matched = false;
  642.  
  643. if (cfg.ownerProfileUrl.endsWith(`/channel/${channelId}`)) matched = true;
  644. else {
  645.  
  646. for (const ownerUrl of resultInfo.ownerUrls) {
  647. if (ownerUrl.endsWith(cfg.ownerProfileUrl)) {
  648. matched = true;
  649. break;
  650. }
  651. }
  652.  
  653.  
  654. }
  655.  
  656.  
  657. if (matched) {
  658. resultInfo.title = cfg.ownerChannelName;
  659. }
  660. }
  661.  
  662. }
  663.  
  664. onResulted(resultInfo);
  665. onResulted = null;
  666. }).catch(onError);
  667.  
  668. };
  669.  
  670. /**
  671. *
  672. * @param {string} channelId
  673. * @param {Function} onDownloaded
  674. * @param {Function} onResulted
  675. * @param {Function} onError
  676. */
  677. const fetcherRSS = location.origin !== 'https://www.youtube.com' ? null : (channelId, onDownloaded, onResulted, onError) => {
  678.  
  679. let signal = timeoutSignal(4000);
  680. fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`, {
  681. // YouTube RSS Response - public, max-age=900
  682.  
  683. "method": "GET",
  684. "mode": "same-origin",
  685. "credentials": "omit",
  686.  
  687. referrerPolicy: "no-referrer",
  688. cache: "default",
  689. // cache: "no-cache",
  690. redirect: "error", // there shall be no redirection in this API request
  691. integrity: "",
  692. keepalive: false,
  693. signal,
  694.  
  695. "headers": {
  696. "Cache-Control": "public, max-age=900, stale-while-revalidate=1800",
  697. // refer "Cache-Control Use Case Examples" in https://www.koyeb.com/blog/using-cache-control-and-cdns-to-improve-performance-and-reduce-latency
  698. // seems YouTube RSS Feeds server insists its own Cache-Control.
  699.  
  700. // "Content-Type": "text/xml; charset=UTF-8",
  701. "Accept-Encoding": "gzip, deflate, br", // YouTube Response - gzip
  702. // X-Youtube-Bootstrap-Logged-In: false,
  703. // X-Youtube-Client-Name: 1, // INNERTUBE_CONTEXT_CLIENT_NAME
  704. // X-Youtube-Client-Version: "2.20230622.06.00" // INNERTUBE_CONTEXT_CLIENT_VERSION
  705.  
  706. "Accept": "text/xml",
  707. }
  708. }).then(res => {
  709. signal && signal.clearTimeout && signal.clearTimeout();
  710. signal = null;
  711. onDownloaded();
  712. onDownloaded = null;
  713. return res.text();
  714. }).then(resText => {
  715. let eIdx = resText.indexOf('<entry>');
  716. let mText = (eIdx > 0) ? `${resText.substring(0, eIdx).trim()}</feed>` : resText;
  717.  
  718. // simple: https://www.youtube.com/feeds/videos.xml?channel_id=UC-MUu72gixoYlhV5_mWa36g
  719. // standard: https://www.youtube.com/feeds/videos.xml?channel_id=UCGSfK5HeiIhuFfXoKE47TzA
  720. // long: https://www.youtube.com/feeds/videos.xml?channel_id=UC8cSGjKxDuh2mWG1hDOTdBg
  721. // special: http://www.youtube.com/feeds/videos.xml?channel_id=UCmZ2-GUxmdWFKfXA5IN0x-w
  722.  
  723.  
  724. let name, uri, mt;
  725.  
  726. // ===============================================================
  727.  
  728. /* removed in 2023.06.30 */
  729.  
  730.  
  731. // ===============================================================
  732.  
  733.  
  734. const wIdx1 = mText.indexOf('<author>');
  735. const wIdx2 = wIdx1 > 0 ? mText.indexOf('</author>', wIdx1 + 8) : -1;
  736.  
  737. if (wIdx1 > 0 && wIdx2 > 0) {
  738. let qText = mText.substring(wIdx1, wIdx2 + '</author>'.length);
  739. const template = document.createElement('template');
  740. template.innerHTML = qText;
  741. /** @type {HTMLElement | null} */
  742. let authorChild = ((template.content || 0).firstElementChild || 0).firstElementChild;
  743. for (; authorChild !== null; authorChild = authorChild.nextElementSibling) {
  744. const nodeName = authorChild.nodeName;
  745. if (nodeName === 'NAME') name = authorChild.textContent;
  746. else if (nodeName === 'URI') {
  747. uri = authorChild.textContent;
  748. mt = obtainChannelId(uri);
  749. }
  750. }
  751. template.innerHTML = '';
  752. }
  753.  
  754. if (!name && !uri) {
  755.  
  756. // fallback
  757.  
  758. const parser = new DOMParser();
  759. const xmlDoc = parser.parseFromString(mText, "text/xml");
  760.  
  761. // const author = xmlDoc.querySelector("author");
  762.  
  763. // https://extendsclass.com/xpath-tester.html
  764.  
  765. const authorChilds = xmlDoc.evaluate("//*[name()='author']/*", xmlDoc, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
  766.  
  767. let authorChild;
  768. while ((authorChild = authorChilds.iterateNext()) !== null) {
  769. if (authorChild.nodeName === 'name') name = authorChild.textContent;
  770. else if (authorChild.nodeName === 'uri') {
  771. uri = authorChild.textContent;
  772. mt = obtainChannelId(uri)
  773. }
  774. }
  775.  
  776. try {
  777. xmlDoc.firstChild.remove();
  778. } catch (e) { }
  779.  
  780. }
  781.  
  782.  
  783.  
  784.  
  785. let res = null;
  786.  
  787. if (name && uri && mt && mt === channelId) {
  788.  
  789.  
  790. let object = channelIdToHandle.get(mt);
  791.  
  792. if (object) {
  793.  
  794. const handleText = object.handleText;
  795.  
  796. if (handleText === name) channelIdToHandle.delete(mt);
  797. else {
  798. object.justPossible = false; // ignore @handle checking
  799.  
  800. res = {
  801. "title": name,
  802. "externalId": mt,
  803. "ownerUrls": [
  804. `http://www.youtube.com/${handleText}`
  805. ],
  806. "channelUrl": uri,
  807. "vanityChannelUrl": `http://www.youtube.com/${handleText}`
  808. };
  809.  
  810. if (USE_LANG_SPECIFIC_NAME) {
  811. const langPreferredDisplayName = langPreferredDisplayNameMap.get(handleText);
  812. if (langPreferredDisplayName && name !== langPreferredDisplayName) res.langTitle = langPreferredDisplayName;
  813. }
  814.  
  815. }
  816. }
  817. }
  818.  
  819. onResulted(res);
  820. onResulted = null;
  821.  
  822. // let resultInfo = ((res || 0).metadata || 0).channelMetadataRenderer;
  823. // onResulted(resultInfo);
  824. }).catch(onError);
  825.  
  826.  
  827.  
  828. }
  829.  
  830. /**
  831. *
  832. * @param {string} channelId
  833. */
  834. function stackNewRequest(channelId) {
  835.  
  836. mutex.add(lockResolve => {
  837.  
  838. let bResult = displayNameResStore.get(channelId);
  839. if (!(bResult instanceof AsyncValue)) {
  840. // resolved or removed
  841. lockResolve(); lockResolve = null;
  842. return;
  843. }
  844.  
  845. let setResult = (result) => {
  846. setResult = null;
  847. if (!result) {
  848. bResult.fetchingState = 0;
  849. bResult.setValue(null);
  850. displayNameResStore.delete(channelId); // create another network response in the next request
  851. } else {
  852. bResult.fetchingState = 4;
  853. bResult.setValue(result);
  854. displayNameResStore.set(channelId, result); // update store result to resolved value
  855. }
  856. bResult = null;
  857. }
  858.  
  859. if (bResult.fetchingState >= 2) { // fetchingState == 3 or 4
  860. // request is already done. no need to stack any request
  861. lockResolve(); lockResolve = null;
  862. return;
  863. }
  864.  
  865. if (!document.querySelector(`[jkrgy="${channelId}"]`)) {
  866. // element has already been removed
  867. lockResolve(); lockResolve = null;
  868. setResult(null);
  869. return;
  870. }
  871.  
  872. //INNERTUBE_API_KEY = ytcfg.data_.INNERTUBE_API_KEY
  873.  
  874. bResult.fetchingState = 2;
  875.  
  876.  
  877. // note: when setResult(null), the fetch will be requested again if the same username appears. (multiple occurrences)
  878. // consider the network problem might be fixed in the 2nd attempt, the name will be changed in the 2nd attempt but ignore 1st attempt.
  879. const fetcher = USE_RSS_FETCHER && fetcherRSS && channelIdToHandle.has(channelId) ? fetcherRSS : fetcherBrowseAPI;
  880. fetcher(channelId, () => {
  881. bResult.fetchingState = 3;
  882. lockResolve();
  883. lockResolve = null;
  884. }, resultInfo => {
  885.  
  886. if (!resultInfo) {
  887. // invalid json format
  888. setResult(null);
  889. return;
  890. }
  891.  
  892. cacheHandleToChannel(resultInfo, channelId);
  893.  
  894. const { title, langTitle, externalId, ownerUrls, channelUrl, vanityChannelUrl } = resultInfo;
  895.  
  896. const displayNameRes = { title, langTitle, externalId, ownerUrls, channelUrl, vanityChannelUrl, verified123: false };
  897.  
  898. setResult(displayNameRes);
  899. }, e => {
  900. lockResolve && lockResolve();
  901. lockResolve = null;
  902. console.warn(e);
  903. setResult && setResult(null);
  904. });
  905.  
  906. });
  907.  
  908. }
  909.  
  910. /**
  911. *
  912. * @param {string} url Example: _www\.youtube\.com/\@yr__kd_
  913. * @returns Example: _\@yr__kd_
  914. */
  915. function urlToHandle(url) {
  916.  
  917. if (typeof url !== 'string') return '';
  918. let i = url.indexOf('.youtube.com/');
  919. if (i >= 1) url = url.substring(i + '.youtube.com/'.length);
  920. else if (url.charAt(0) === '/') url = url.substring(1);
  921. return isDisplayAsHandle(url) ? url : '';
  922.  
  923. }
  924.  
  925. /**
  926. *
  927. * @param {string} channelId Example: UC0gmRdmpDWJ4dt7DAeRaawA
  928. * @returns {Promise<DisplayNameResultObject | null>}
  929. */
  930. async function getDisplayName(channelId) {
  931.  
  932. let cache = displayNameResStore.get(channelId);
  933. let isStackNewRequest = false;
  934.  
  935. /** @type {AsyncValue<DisplayNameResultObject> | null | undefined} */
  936. let aResult;
  937.  
  938. if (cache instanceof AsyncValue) {
  939. aResult = cache;
  940. if (aResult.fetchingState >= 2) {
  941. // aResult.fetchingState
  942. // 2: network fetch started
  943. // 3: network fetch ended
  944. // isStackNewRequest = false;
  945. } else {
  946. // 0: invalid before
  947. // 1: scheduled but not yet fetch
  948. isStackNewRequest = true;
  949. }
  950. } else if (cache) {
  951. // resolved result
  952. return cache;
  953. } else {
  954. // else no cached value found
  955. /** @type {AsyncValue<DisplayNameResultObject>} */
  956. aResult = new AsyncValue();
  957. displayNameResStore.set(channelId, aResult);
  958. isStackNewRequest = true;
  959. }
  960.  
  961. if (isStackNewRequest) {
  962. aResult.fetchingState = 1;
  963. stackNewRequest(channelId);
  964. }
  965. return await aResult.getValue();
  966. }
  967.  
  968.  
  969. /**
  970. *
  971. * @param {string} href Example: https\:\/\/www\.youtube\.com/channel/UC0gmRdmpDWJ4dt7DAeRaawA
  972. * @returns
  973. */
  974. const obtainChannelId = (href) => {
  975. let m = /\/channel\/(UC[-_a-zA-Z0-9+=.]+)/.exec(`/${href}`);
  976. // let m = /\/channel\/([^/?#\s]+)/.exec(`/${href}`);
  977. return !m ? '' : (m[1] || '');
  978. };
  979.  
  980.  
  981. /**
  982. *
  983. * @param {Element} ytElm
  984. */
  985. const resetWhenDataChanged = (ths) => {
  986. const cnt = ths.inst || ths || 0;
  987. const ytElm = cnt.hostElement instanceof Element ? cnt.hostElement : cnt instanceof Element ? cnt : ths;
  988. if (ytElm instanceof Element) {
  989. const anchors = elementQSA(ytElm, 'a[id][href*="channel/"][jkrgy]');
  990. if ((anchors || 0).length >= 1 && ((ytElm.inst || ytElm).data || 0).jkrgx !== 1) {
  991. for (const anchor of anchors) {
  992. anchor.removeAttribute('jkrgy');
  993. }
  994. }
  995. }
  996. }
  997.  
  998. /**
  999. *
  1000. * @param {Function} dataChanged original dataChanged function
  1001. * @returns new dataChanged function
  1002. */
  1003. const dataChangeFuncProducer = (dataChanged) => {
  1004. /** @this HTMLElement */
  1005. return function () {
  1006. resetWhenDataChanged(this);
  1007. return dataChanged.apply(this, arguments);
  1008. }
  1009. };
  1010.  
  1011. /**
  1012. *
  1013. * @param {HTMLAnchorElement} anchor Example: https\:\/\/www\.youtube\.com/channel/UCRmLncxsQFcOOC8OhzUIfxQ/videos
  1014. * @param {string} channelHref Example: /channel/UCRmLncxsQFcOOC8OhzUIfxQ
  1015. * @param {string} channelId Example: UCRmLncxsQFcOOC8OhzUIfxQ
  1016. * @returns
  1017. */
  1018. const anchorIntegrityCheck = (anchor, channelHref, channelId) => {
  1019.  
  1020. let currentHref = anchor.getAttribute('href');
  1021. if (currentHref === channelHref) return true; // /channel/UCRmLncxsQFcOOC8OhzUIfxQ // /channel/UCRmLncxsQFcOOC8OhzUIfxQ
  1022.  
  1023. return (currentHref + '/').indexOf(channelHref + '/') >= 0;
  1024.  
  1025. };
  1026.  
  1027. /**
  1028. *
  1029. * @param {string} currentDisplayed
  1030. * @param {DisplayNameResultObject} fetchResult
  1031. * @returns
  1032. */
  1033. const verifyAndConvertHandle = (currentDisplayed, fetchResult) => {
  1034.  
  1035. const { title, langTitle, externalId, ownerUrls, channelUrl, vanityChannelUrl, verified123 } = fetchResult;
  1036.  
  1037. const currentDisplayTrimmed = currentDisplayed.trim();
  1038. let match = false;
  1039. if (verified123) {
  1040. match = true;
  1041. } else if ((vanityChannelUrl || '').endsWith(`/${currentDisplayTrimmed}`)) {
  1042. match = true;
  1043. } else if ((ownerUrls || 0).length >= 1) {
  1044. for (const ownerUrl of ownerUrls) {
  1045. if ((ownerUrl || '').endsWith(`/${currentDisplayTrimmed}`)) {
  1046. match = true;
  1047. break;
  1048. }
  1049. }
  1050. }
  1051. if (match) {
  1052. return currentDisplayTrimmed;
  1053. }
  1054. return '';
  1055.  
  1056. };
  1057.  
  1058. /**
  1059. *
  1060. *
  1061.  
  1062. ### [Handle naming guidelines](https://support.google.com/youtube/answer/11585688?hl=en&co=GENIE.Platform%3DAndroid)
  1063.  
  1064. - Is between 3-30 characters
  1065. - Is made up of alphanumeric characters (A–Z, a–z, 0–9)
  1066. - Your handle can also include: underscores (_), hyphens (-), dots (.)
  1067. - Is not URL-like or phone number-like
  1068. - Is not already being used
  1069. - Follows YouTube's Community Guidelines
  1070.  
  1071. ### Handle automatically generated by YouTube
  1072. - `@user-XXXX`
  1073. - without dot (.)
  1074. *
  1075. * @param {string} text
  1076. * @returns
  1077. */
  1078. const isDisplayAsHandle = (text) => {
  1079.  
  1080. if (typeof text !== 'string') return false;
  1081. if (text.length < 4) return false;
  1082. if (text.indexOf('@') < 0) return false;
  1083. return /^\s*@[-_a-zA-Z0-9.]{3,30}\s*$/.test(text);
  1084.  
  1085. };
  1086.  
  1087. /**
  1088. *
  1089. * @param {Object[]} contentTexts
  1090. * @param {number} idx
  1091. * @returns
  1092. */
  1093. const contentTextProcess = (contentTexts, idx) => {
  1094. const contentText = contentTexts[idx];
  1095. const text = (contentText || 0).text;
  1096. const url = (((contentText.navigationEndpoint || 0).commandMetadata || 0).webCommandMetadata || 0).url;
  1097. if (typeof url === 'string' && typeof text === 'string') {
  1098.  
  1099. if (!isDisplayAsHandle(text)) return null;
  1100. const channelId = obtainChannelId(url);
  1101.  
  1102. return getDisplayName(channelId).then(fetchResult => {
  1103. let resolveResult = null;
  1104. if (fetchResult) {
  1105. // note: if that user shown is not found in `a[id]`, the hyperlink would not change
  1106.  
  1107. const textTrimmed = verifyAndConvertHandle(text, fetchResult);
  1108. if (textTrimmed) {
  1109. resolveResult = (md) => {
  1110. let runs = ((md || 0).contentText || 0).runs;
  1111. if (!runs || !runs[idx]) return;
  1112. if (runs[idx].text !== text) return;
  1113. runs[idx].text = text.replace(textTrimmed, `@${fetchResult.title}`); // HyperLink always @SomeOne
  1114. md.contentText = Object.assign({}, md.contentText);
  1115. };
  1116. }
  1117. }
  1118. return resolveResult; // function as a Promise
  1119. });
  1120. }
  1121.  
  1122. return null;
  1123. };
  1124.  
  1125. const editableTextProcess = (editableTexts, idx) => {
  1126. const editableText = editableTexts[idx];
  1127. const text = (editableText || 0).text;
  1128. const url = (((editableText.navigationEndpoint || 0).commandMetadata || 0).webCommandMetadata || 0).url;
  1129. if (typeof url === 'string' && typeof text === 'string') {
  1130.  
  1131. if (!isDisplayAsHandle(text)) return null;
  1132. const channelId = obtainChannelId(url);
  1133.  
  1134. return getDisplayName(channelId).then(fetchResult => {
  1135. let resolveResult = null;
  1136. if (fetchResult) {
  1137. // note: if that user shown is not found in `a[id]`, the hyperlink would not change
  1138.  
  1139. const textTrimmed = verifyAndConvertHandle(text, fetchResult);
  1140. if (textTrimmed) {
  1141. resolveResult = (md) => {
  1142. let commentReplyDialogRenderer = null;
  1143. try {
  1144. commentReplyDialogRenderer = md.actionButtons.commentActionButtonsRenderer.replyButton.buttonRenderer.navigationEndpoint.createCommentReplyDialogEndpoint.dialog.commentReplyDialogRenderer;
  1145. } catch (e) { }
  1146. if (!commentReplyDialogRenderer) return;
  1147. const editableText = commentReplyDialogRenderer.editableText || 0;
  1148.  
  1149. let runs = (editableText || 0).runs;
  1150. if (!runs || !runs[idx]) return;
  1151. if (runs[idx].text !== text) return;
  1152. runs[idx].text = text.replace(textTrimmed, `@${fetchResult.title}`); // HyperLink always @SomeOne
  1153. commentReplyDialogRenderer.editableText = Object.assign({}, commentReplyDialogRenderer.editableText);
  1154. };
  1155. }
  1156. }
  1157. return resolveResult; // function as a Promise
  1158. });
  1159. }
  1160.  
  1161. return null;
  1162. };
  1163.  
  1164. /**
  1165. *
  1166. * @param {ParentNode} parentNode
  1167. * @param {Function} callback
  1168. */
  1169. function findTextNodes(parentNode, callback) {
  1170. /**
  1171. *
  1172. * @param {Node} node
  1173. * @returns
  1174. */
  1175. function traverse(node) {
  1176. if (node.nodeType === Node.TEXT_NODE) {
  1177. let r = callback(node);
  1178. if (r === true) return true;
  1179. }
  1180.  
  1181. let childNode = nodeFirstChild(node);
  1182. while (childNode) {
  1183. let r = traverse(childNode);
  1184. if (r === true) return true;
  1185. childNode = nodeNextSibling(childNode);
  1186. }
  1187. }
  1188.  
  1189. traverse(parentNode);
  1190. }
  1191.  
  1192. function updatePinnedCommentBadge(md, title, langTitle) {
  1193.  
  1194.  
  1195. let setPinnedCommentBadge = false;
  1196. if (((((md.pinnedCommentBadge || 0).pinnedCommentBadgeRenderer || 0).label || 0).runs || 0).length === 2) {
  1197. setPinnedCommentBadge = true;
  1198. }
  1199. if (setPinnedCommentBadge) {
  1200. let idx = 0;
  1201. let jdx = -1;
  1202. for (const textRun of md.pinnedCommentBadge.pinnedCommentBadgeRenderer.label.runs) {
  1203. if (textRun.text === title) {
  1204. jdx = idx;
  1205. break;
  1206. }
  1207. idx++;
  1208. }
  1209. if (jdx >= 0) {
  1210. const runs = md.pinnedCommentBadge.pinnedCommentBadgeRenderer.label.runs.slice(0);
  1211. runs[jdx] = Object.assign({}, runs[jdx], { text: langTitle });
  1212. md.pinnedCommentBadge = Object.assign({}, md.pinnedCommentBadge);
  1213. md.pinnedCommentBadge.pinnedCommentBadgeRenderer = Object.assign({}, md.pinnedCommentBadge.pinnedCommentBadgeRenderer);
  1214. md.pinnedCommentBadge.pinnedCommentBadgeRenderer.label = Object.assign({}, md.pinnedCommentBadge.pinnedCommentBadgeRenderer.label, { runs });
  1215. }
  1216. }
  1217.  
  1218. }
  1219.  
  1220. /**
  1221. *
  1222. * @param {ParentNode} parentNode
  1223. * @param {Object[]} contentTexts
  1224. * @param {number} aidx
  1225. * @returns
  1226. */
  1227. const mobileContentHandleAsync = async (parentNode, contentTexts, aidx) => {
  1228.  
  1229.  
  1230. let commentText = elementQS(parentNode, '.comment-text');
  1231. if (!commentText) return;
  1232. commentText = elementQS(commentText, '.yt-core-attributed-string') || commentText;
  1233. let currentText = commentText.textContent;
  1234. let textMatch = commentText.textContent === contentTexts.map(e => e.text).join('');
  1235. if (!textMatch) return;
  1236.  
  1237. const contentText = contentTexts[aidx];
  1238.  
  1239. /** @type {string} */
  1240. const handleText = contentText.text.trim();
  1241. /** @type {string | null | undefined} */
  1242. let channelId;
  1243.  
  1244. let cache = handleToChannelId.get(handleText);
  1245.  
  1246. if (typeof cache === 'string') {
  1247. channelId = cache;
  1248. } else if (cache instanceof AsyncValue) {
  1249. channelId = await cache.getValue();
  1250. } else {
  1251. /** @type {AsyncValue<string>} */
  1252. const asyncValue = new AsyncValue();
  1253. handleToChannelId.set(handleText, asyncValue);
  1254. channelId = await asyncValue.getValue(); // note: it could be never resolved
  1255. }
  1256.  
  1257. if (!channelId) return; // just in case
  1258.  
  1259. if (commentText.isConnected !== true) return; // already removed
  1260. if (commentText.textContent !== currentText) return; // already changed
  1261.  
  1262. const fetchResult = await getDisplayName(channelId);
  1263.  
  1264. if (fetchResult === null) return;
  1265.  
  1266. const { title, langTitle, externalId } = fetchResult;
  1267. const titleForDisplay = langTitle || title;
  1268. if (externalId !== channelId) return; // channel id must be the same
  1269.  
  1270. if (commentText.isConnected !== true) return; // already removed
  1271. if (commentText.textContent !== currentText) return; // already changed
  1272.  
  1273. let search = contentText.text;
  1274. if (typeof search === 'string') {
  1275.  
  1276. const searchTrimmed = search.trim();
  1277. if (searchTrimmed === handleText) { // ensure integrity after getDisplayName
  1278. contentText.text = search.replace(searchTrimmed, "@" + titleForDisplay); // mention
  1279.  
  1280. findTextNodes(commentText, (textnode) => {
  1281. if (textnode.nodeValue.indexOf(search) >= 0) {
  1282. textnode.nodeValue = textnode.nodeValue.replace(search, contentText.text);
  1283. return true;
  1284. }
  1285. });
  1286. }
  1287.  
  1288. }
  1289.  
  1290. }
  1291.  
  1292. /**
  1293. * Process Checking when there is new (unprocessed) anchor DOM element with hyperlink of channel
  1294. * @type { (anchor: HTMLElement, channelHref: string?, mt: string?) => Promise<void> }
  1295. */
  1296. const domCheckAsync = isMobile ? async (anchor, channelHref, mt) => {
  1297.  
  1298. try {
  1299. if (!channelHref || !mt) return;
  1300. let parentNode = nodeParent(anchor);
  1301. while (parentNode instanceof Node) {
  1302. if (parentNode.nodeName === 'YTM-COMMENT-RENDERER' || ('_commentData' in parentNode)) break;
  1303. parentNode = nodeParent(parentNode);
  1304. }
  1305. if (parentNode instanceof Node) { } else return;
  1306.  
  1307. let displayTextDOM = elementQS(parentNode, '.comment-header .comment-title');
  1308. if (!displayTextDOM) return;
  1309. displayTextDOM = elementQS(displayTextDOM, '.yt-core-attributed-string') || displayTextDOM;
  1310. let airaLabel = anchor.getAttribute('aria-label')
  1311. const parentNodeData = (parentNode.inst || parentNode).data;
  1312. let runs = ((parentNodeData || 0).authorText || 0).runs;
  1313.  
  1314. if (displayTextDOM && airaLabel && displayTextDOM.textContent.trim() === airaLabel.trim() && isDisplayAsHandle(airaLabel) && runs && (runs[0] || 0).text === airaLabel) {
  1315.  
  1316. if (!channelIdToHandle.has(mt)) {
  1317. channelIdToHandle.set(mt, {
  1318. handleText: airaLabel.trim(),
  1319. justPossible: true
  1320. });
  1321. }
  1322.  
  1323. const fetchResult = await getDisplayName(mt);
  1324.  
  1325. if (fetchResult === null) return;
  1326.  
  1327. const { title, langTitle, externalId } = fetchResult;
  1328. const titleForDisplay = langTitle || title;
  1329. if (externalId !== mt) return; // channel id must be the same
  1330.  
  1331. // anchor href might be changed by external
  1332. if (!anchorIntegrityCheck(anchor, channelHref, externalId)) return;
  1333.  
  1334. if (displayTextDOM.isConnected !== true) return; // already removed
  1335.  
  1336. let found = false;
  1337.  
  1338. findTextNodes(displayTextDOM, (textnode) => {
  1339. if (textnode.nodeValue === airaLabel) {
  1340. textnode.nodeValue = titleForDisplay;
  1341. found = true;
  1342. return true;
  1343. }
  1344. });
  1345.  
  1346. if (!found) return;
  1347. anchor.setAttribute('aria-label', titleForDisplay);
  1348. runs[0].text = titleForDisplay;
  1349.  
  1350. if (UPDATE_PIN_NAME && title && langTitle && langTitle !== title) {
  1351. const renderer = HTMLElement.prototype.closest.call(anchor, 'ytm-comment-renderer');
  1352. const pinned = !renderer ? null : HTMLElement.prototype.querySelector.call(renderer, 'ytm-pinned-comment-badge-renderer');
  1353. const spanText = !pinned ? null : HTMLElement.prototype.querySelector.call(pinned, 'span.yt-core-attributed-string[role="text"]');
  1354. const tc = spanText ? spanText.textContent : '';
  1355. updatePinnedCommentBadge(parentNodeData, title, langTitle);
  1356. if (tc && tc === spanText.textContent) {
  1357. let idx = tc.indexOf(title);
  1358. if (idx >= 0) {
  1359. spanText.textContent = tc.substring(0, idx) + langTitle + tc.substring(idx + title.length);
  1360. }
  1361. }
  1362. }
  1363.  
  1364. const contentTexts = (parentNodeData.contentText || 0).runs;
  1365. if (contentTexts && contentTexts.length >= 2) {
  1366. for (let aidx = 0; aidx < contentTexts.length; aidx++) {
  1367. const contentText = contentTexts[aidx] || 0;
  1368. if (contentText.italics !== true) {
  1369. let isHandle = isDisplayAsHandle(contentText.text);
  1370. if (isHandle) {
  1371. mobileContentHandleAsync(parentNode, contentTexts, aidx);
  1372. }
  1373. }
  1374. }
  1375. }
  1376.  
  1377. }
  1378. } catch (e) {
  1379. console.warn(e);
  1380. }
  1381.  
  1382. } : async (anchor, channelHref, mt) => {
  1383.  
  1384. try {
  1385. if (!channelHref || !mt) return;
  1386. let parentNode = nodeParent(anchor);
  1387. while (parentNode instanceof Node) {
  1388. const cnt = parentNode.inst || parentNode;
  1389. if (typeof cnt.is === 'string' && typeof cnt.dataChanged === 'function') break;
  1390. parentNode = nodeParent(parentNode);
  1391. }
  1392. if (parentNode instanceof Node) { } else return;
  1393. const parentNodeController = parentNode.inst || parentNode;
  1394. const authorText = (parentNodeController.data || 0).authorText;
  1395. const currentDisplayed = (authorText || 0).simpleText;
  1396. if (typeof currentDisplayed !== 'string') return;
  1397. if (!isDisplayAsHandle(currentDisplayed)) return;
  1398.  
  1399. if (!channelIdToHandle.has(mt)) {
  1400. channelIdToHandle.set(mt, {
  1401. handleText: currentDisplayed.trim(),
  1402. justPossible: true
  1403. });
  1404. }
  1405.  
  1406. let oldDataChangedFn = parentNodeController.dataChanged;
  1407. if (typeof oldDataChangedFn === 'function' && !oldDataChangedFn.jkrgx) {
  1408. let newDataChangedFn = dataChangedFuncStore.get(oldDataChangedFn);
  1409. if (!newDataChangedFn) {
  1410. newDataChangedFn = dataChangeFuncProducer(oldDataChangedFn);
  1411. newDataChangedFn.jkrgx = 1;
  1412. dataChangedFuncStore.set(oldDataChangedFn, newDataChangedFn);
  1413. }
  1414. parentNodeController.dataChanged = newDataChangedFn;
  1415. }
  1416.  
  1417. oldDataChangedFn = parentNodeController.hostElement.dataChanged;
  1418. if (typeof oldDataChangedFn === 'function' && !oldDataChangedFn.jkrgx) {
  1419. let newDataChangedFn = dataChangedFuncStore.get(oldDataChangedFn);
  1420. if (!newDataChangedFn) {
  1421. newDataChangedFn = dataChangeFuncProducer(oldDataChangedFn);
  1422. newDataChangedFn.jkrgx = 1;
  1423. dataChangedFuncStore.set(oldDataChangedFn, newDataChangedFn);
  1424. }
  1425. parentNodeController.hostElement.dataChanged = newDataChangedFn;
  1426. }
  1427.  
  1428. const fetchResult = await getDisplayName(mt);
  1429.  
  1430. if (fetchResult === null) return;
  1431.  
  1432. const { title, langTitle, externalId } = fetchResult;
  1433. const titleForDisplay = langTitle || title;
  1434. if (externalId !== mt) return; // channel id must be the same
  1435.  
  1436. // anchor href might be changed by external
  1437. if (!anchorIntegrityCheck(anchor, channelHref, externalId)) return;
  1438.  
  1439. const parentNodeData = parentNodeController.data;
  1440. const funcPromises = [];
  1441.  
  1442. if (parentNodeController.isAttached === true && parentNode.isConnected === true && typeof parentNodeData === 'object' && parentNodeData && parentNodeData.authorText === authorText) {
  1443.  
  1444. if (authorText.simpleText !== currentDisplayed) return;
  1445. const currentDisplayTrimmed = verifyAndConvertHandle(currentDisplayed, fetchResult);
  1446. const cSimpleText = ((parentNodeData.authorText || 0).simpleText || '');
  1447. if (currentDisplayTrimmed && currentDisplayed !== titleForDisplay && cSimpleText === currentDisplayed) {
  1448.  
  1449. // the inside hyperlinks will be only converted if its parent author name is handle
  1450. const contentTexts = (parentNodeData.contentText || 0).runs;
  1451. if (contentTexts && contentTexts.length >= 1) {
  1452. for (let aidx = 0; aidx < contentTexts.length; aidx++) {
  1453. const r = contentTextProcess(contentTexts, aidx);
  1454. if (r instanceof Promise) funcPromises.push(r);
  1455. }
  1456. }
  1457.  
  1458. const md = Object.assign({}, parentNodeData);
  1459. let setBadge = false;
  1460. if (((((md.authorCommentBadge || 0).authorCommentBadgeRenderer || 0).authorText || 0).simpleText || '').trim() === cSimpleText.trim()) {
  1461. setBadge = true;
  1462. }
  1463. // parentNode.data = Object.assign({}, { jkrgx: 1 });
  1464. md.authorText = Object.assign({}, md.authorText, { simpleText: currentDisplayed.replace(currentDisplayTrimmed, titleForDisplay) });
  1465. if (setBadge) {
  1466. md.authorCommentBadge = Object.assign({}, md.authorCommentBadge);
  1467. md.authorCommentBadge.authorCommentBadgeRenderer = Object.assign({}, md.authorCommentBadge.authorCommentBadgeRenderer);
  1468. md.authorCommentBadge.authorCommentBadgeRenderer.authorText = Object.assign({}, md.authorCommentBadge.authorCommentBadgeRenderer.authorText, { simpleText: titleForDisplay });
  1469.  
  1470. }
  1471.  
  1472. let setReplyEditableText = false;
  1473. if (((((((((parentNodeData.actionButtons || 0).commentActionButtonsRenderer || 0).replyButton || 0).buttonRenderer || 0).navigationEndpoint || 0).createCommentReplyDialogEndpoint || 0).dialog || 0).commentReplyDialogRenderer || 0).editableText) {
  1474. setReplyEditableText = true;
  1475. }
  1476. if (setReplyEditableText) {
  1477. const commentReplyDialogRenderer = parentNodeData.actionButtons.commentActionButtonsRenderer.replyButton.buttonRenderer.navigationEndpoint.createCommentReplyDialogEndpoint.dialog.commentReplyDialogRenderer;
  1478. const editableText = commentReplyDialogRenderer.editableText;
  1479. const editableTexts = editableText.runs;
  1480. if (editableTexts && editableTexts.length >= 1) {
  1481. for (let aidx = 0; aidx < editableTexts.length; aidx++) {
  1482. const r = editableTextProcess(editableTexts, aidx);
  1483. if (r instanceof Promise) funcPromises.push(r);
  1484. }
  1485. }
  1486. }
  1487.  
  1488. if (UPDATE_PIN_NAME) {
  1489. updatePinnedCommentBadge(md, title, langTitle);
  1490. }
  1491.  
  1492. if (funcPromises.length >= 1) {
  1493. let funcs = await Promise.all(funcPromises);
  1494.  
  1495. for (const func of funcs) {
  1496. if (typeof func === 'function') {
  1497. func(md);
  1498. }
  1499. }
  1500. }
  1501.  
  1502. if (parentNodeController.isAttached === true && parentNode.isConnected === true) {
  1503. parentNodeController.data = Object.assign({}, md, { jkrgx: 1 });
  1504. addCSSRulesIfRequired();
  1505. }
  1506.  
  1507.  
  1508. }
  1509.  
  1510. }
  1511. } catch (e) {
  1512. console.warn(e);
  1513. }
  1514.  
  1515. }
  1516.  
  1517. /**
  1518. *
  1519. * @param {string} url
  1520. * @returns
  1521. */
  1522. const channelUrlUnify = (url) => {
  1523.  
  1524. if (typeof url !== 'string') return url; // type unchanged
  1525. let c0 = url.charAt(0);
  1526. if (c0 === '/') return url;
  1527. let i = url.indexOf('.youtube.com/');
  1528. if (i >= 1) url = url.substring(i + '.youtube.com/'.length - 1);
  1529. else if (url.charAt(0) !== '/') url = '/' + url;
  1530. return url
  1531.  
  1532. }
  1533.  
  1534. const parentYtElement = (node, nodeName, maxN) => {
  1535. for (let pDom = nodeParent(node); (pDom instanceof HTMLElement) && maxN-- > 0; pDom = nodeParent(pDom)) {
  1536. if (pDom.nodeName === nodeName) {
  1537. return pDom;
  1538. }
  1539. }
  1540. return null;
  1541. }
  1542.  
  1543. const checkShortsChannelName = (rchannelNameDOM) => {
  1544. let browseId = '';
  1545. const pDom = parentYtElement(rchannelNameDOM, 'YTD-REEL-PLAYER-HEADER-RENDERER', 18);
  1546.  
  1547. if (pDom instanceof HTMLElement) {
  1548. const pDomController = pDom.inst || pDom;
  1549. const runs = (((pDomController.data || 0).channelTitleText || 0).runs || 0);
  1550. if (runs.length === 1) {
  1551. const browserEndpoint = (((runs[0] || 0).navigationEndpoint || 0).browseEndpoint || 0);
  1552. browseId = (browserEndpoint.browseId || '');
  1553. const currentDisplayText = runs[0].text || ''
  1554.  
  1555. if (/^UC[-_a-zA-Z0-9+=.]+$/.test(browseId) && isDisplayAsHandle(currentDisplayText)) {
  1556.  
  1557. if (!channelIdToHandle.has(browseId)) {
  1558. channelIdToHandle.set(browseId, {
  1559. handleText: currentDisplayText.trim(),
  1560. justPossible: true
  1561. });
  1562. }
  1563.  
  1564.  
  1565. // let anchorNode = elementQS(rchannelNameDOM, `a[href="${browserEndpoint.canonicalBaseUrl}"]`);
  1566. // anchorNode && anchorNode.setAttribute('jkrgy', browseId); // for checking
  1567. // anchorNode &&
  1568. getDisplayName(browseId).then(fetchResult => {
  1569.  
  1570. if (fetchResult === null) return;
  1571.  
  1572. const { title, langTitle, externalId } = fetchResult;
  1573. const titleForDisplay = langTitle || title;
  1574. if (externalId !== browseId) return; // channel id must be the same
  1575.  
  1576. let hDom = pDom;
  1577. const hDomHostElement = hDom.hostElement || hDom;
  1578. const hDomController = hDom.inst || hDom;
  1579. let hData = (hDomController || 0).data || 0;
  1580. const runs = (((hData || 0).channelTitleText || 0).runs || 0);
  1581. if (runs && runs.length === 1 && (runs[0] || 0).text === currentDisplayText && hDomHostElement.isConnected === true) {
  1582.  
  1583. let runs2 = [Object.assign({}, runs[0], { text: titleForDisplay })];
  1584.  
  1585.  
  1586. /*
  1587. runs.push({text: title, bold: true, "fontColor": "black",
  1588.  
  1589. size: "SIZE_DEFAULT",
  1590. style: "STYLE_TEXT",
  1591. });
  1592. */
  1593.  
  1594. hDomController.data = Object.assign({}, hDomController.data, { channelTitleText: { runs: runs2 }, rSk0e: 1 });
  1595. addCSSRulesIfRequired();
  1596.  
  1597. byPassCheckStore.delete(rchannelNameDOM);
  1598. }
  1599.  
  1600.  
  1601.  
  1602. })
  1603. }
  1604.  
  1605.  
  1606. }
  1607. }
  1608. return browseId
  1609. }
  1610.  
  1611. let firstDOMCheck = false;
  1612. const firstDOMChecker = isMobile ? () => {
  1613.  
  1614. let playerMicroformatRenderer = null;
  1615. try {
  1616. playerMicroformatRenderer = window.ytInitialPlayerResponse.microformat.playerMicroformatRenderer
  1617. } catch (e) { }
  1618. if (playerMicroformatRenderer && playerMicroformatRenderer.ownerChannelName && playerMicroformatRenderer.ownerProfileUrl) {
  1619. cfg.ownerChannelName = playerMicroformatRenderer.ownerChannelName;
  1620. cfg.ownerProfileUrl = playerMicroformatRenderer.ownerProfileUrl.replace(/[\S]+youtube.com\//, 'youtube.com/');
  1621. }
  1622.  
  1623. const channelNameDOM = document.querySelector('ytm-slim-owner-renderer');
  1624. const channelNameCData = !channelNameDOM ? null : (channelNameDOM.inst || channelNameDOM || 0).data;
  1625.  
  1626. if (channelNameCData) {
  1627. let mainChannelUrl = null;
  1628. let mainFormattedNameUrl = null;
  1629. let mainChannelText = null;
  1630. let mainFormattedNameText = null;
  1631.  
  1632. try {
  1633. mainChannelUrl = channelUrlUnify(channelNameCData.channelUrl)
  1634. mainFormattedNameUrl = channelUrlUnify(channelNameCData.title.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl)
  1635. mainChannelText = channelNameCData.channelName
  1636. mainFormattedNameText = channelNameCData.title.runs[0].text
  1637. } catch (e) { }
  1638.  
  1639. if (mainChannelUrl === mainFormattedNameUrl && mainChannelText === mainFormattedNameText && typeof mainChannelUrl === 'string' && typeof mainChannelText === 'string') {
  1640.  
  1641.  
  1642. let channelId = obtainChannelId(mainChannelUrl);
  1643. if (channelId) {
  1644. if (mainChannelText.startsWith('@')) return;
  1645. if (mainChannelText.trim() !== mainChannelText) return;
  1646.  
  1647. displayNameResStore.set(channelId, {
  1648. channelUrl: `https://www.youtube.com/channel/${channelId}`,
  1649. externalId: `${channelId}`,
  1650. ownerUrls: [],
  1651. title: mainChannelText,
  1652. vanityChannelUrl: null,
  1653. verified123: true
  1654. });
  1655. }
  1656.  
  1657.  
  1658. }
  1659. }
  1660.  
  1661. } : () => {
  1662.  
  1663. const channelNameDOM = document.querySelector('ytd-channel-name.ytd-video-owner-renderer');
  1664. const channelNameCnt = !channelNameDOM ? null : channelNameDOM.inst || channelNameDOM;
  1665. const channelNameCData = (channelNameCnt || 0).__data || (channelNameDOM || 0).__data;
  1666. if (channelNameCData) {
  1667. let mainChannelUrl = null;
  1668. let mainFormattedNameUrl = null;
  1669. let mainChannelText = null;
  1670. let mainFormattedNameText = null;
  1671. try {
  1672. mainChannelUrl = channelNameCData.channelName.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl
  1673. mainFormattedNameUrl = channelNameCData.formattedName.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl
  1674. mainChannelText = channelNameCData.channelName.runs[0].text
  1675. mainFormattedNameText = channelNameCData.formattedName.runs[0].text
  1676. } catch (e) { }
  1677.  
  1678. if (mainChannelUrl === mainFormattedNameUrl && mainChannelText === mainFormattedNameText && typeof mainChannelUrl === 'string' && typeof mainChannelText === 'string') {
  1679.  
  1680. let m = /^\/(@[-_a-zA-Z0-9.]{3,30})$/.exec(mainChannelUrl);
  1681.  
  1682. if (m && m[1] && mainChannelText !== m[1]) {
  1683.  
  1684. const channelHandle = m[1];
  1685. if (/[^-_a-zA-Z0-9.]/.test(mainChannelText)) {
  1686. langPreferredDisplayNameMap.set(channelHandle, mainChannelText);
  1687. }
  1688.  
  1689. } else {
  1690.  
  1691. let channelId = obtainChannelId(mainChannelUrl);
  1692. if (channelId) {
  1693. if (mainChannelText.startsWith('@')) return;
  1694. if (mainChannelText.trim() !== mainChannelText) return;
  1695.  
  1696. displayNameResStore.set(channelId, {
  1697. channelUrl: `https://www.youtube.com/channel/${channelId}`,
  1698. externalId: `${channelId}`,
  1699. ownerUrls: [],
  1700. title: mainChannelText,
  1701. vanityChannelUrl: null,
  1702. verified123: true
  1703. });
  1704. }
  1705.  
  1706. }
  1707. }
  1708. }
  1709.  
  1710. };
  1711.  
  1712. // let newAnchorAdded = false;
  1713. /*
  1714.  
  1715. const intersectionobserver = new IntersectionObserver((entries) => {
  1716. let anchorAppear = false;
  1717. for (const entry of entries) {
  1718. if (entry.isIntersecting === true) {
  1719. anchorAppear = true;
  1720. break;
  1721. }
  1722. }
  1723. if (anchorAppear && newAnchorAdded) {
  1724. newAnchorAdded = false; // stacking will be only reset when one or more anchor added to DOM.
  1725. mutex.nextIndex = 0; // higher pirority for new elements being shown
  1726. }
  1727. }, {
  1728. rootMargin:"0px 0px 0px 0px",
  1729. threshold:1
  1730. })
  1731. */
  1732.  
  1733.  
  1734. /* globals WeakRef:false */
  1735.  
  1736. /** @type {(o: Object | null) => WeakRef | null} */
  1737. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  1738.  
  1739. /** @type {(wr: Object | null) => Object | null} */
  1740. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  1741.  
  1742. let doShortsChecking = false;
  1743. let doDescriptionChecking = true;
  1744.  
  1745. let lastNewAnchorLastWR = null;
  1746.  
  1747. const mutexForDOMMutations = new Mutex();
  1748.  
  1749. const byPassCheckStore = new WeakSet();
  1750.  
  1751. const mutObserverForShortsChannelName = new MutationObserver((mutations) => {
  1752. for (const mutation of mutations) {
  1753. const target = mutation.target;
  1754. if (!target) continue;
  1755. const rchannelNameElm = parentYtElement(target, 'YTD-CHANNEL-NAME', 18);
  1756. if (!rchannelNameElm) continue;
  1757. const rchannelNameCnt = rchannelNameElm.inst || rchannelNameElm;
  1758. if ((rchannelNameCnt.data || 0).rSk0e) continue;
  1759. const channelId = rchannelNameElm.getAttribute('jkrgy');
  1760. if (channelId) {
  1761. if (byPassCheckStore.has(rchannelNameElm)) continue;
  1762. const object = channelIdToHandle.get(channelId);
  1763. if (object && object.handleText && !object.justPossible) {
  1764. const textDom = ((rchannelNameCnt.$ || 0).text || 0);
  1765. if (textDom) {
  1766. let t = textDom.textContent.trim()
  1767. if (t === object.handleText || t === '') {
  1768. rchannelNameElm.removeAttribute('jkrgy');
  1769. Promise.resolve().then(domChecker);
  1770. }
  1771. }
  1772. }
  1773. }
  1774. }
  1775. })
  1776.  
  1777. let domCheckScheduledForShorts = false;
  1778. const domCheckSelectorForShorts = [
  1779. 'ytd-channel-name.ytd-reel-player-header-renderer:not([jkrgy]) a.yt-simple-endpoint[href]'
  1780. ].join(', ');
  1781. const domCheckerForShorts = () => {
  1782.  
  1783. if (domCheckScheduledForShorts) return;
  1784. if (document.querySelector(domCheckSelectorForShorts) === null) {
  1785. return;
  1786. }
  1787. domCheckScheduledForShorts = true;
  1788.  
  1789. mutexForDOMMutations.lockWith(lockResolve => {
  1790.  
  1791.  
  1792. // setTimeout(()=>{ // delay due to data update is after [is-active] switching
  1793.  
  1794.  
  1795. domCheckScheduledForShorts = false;
  1796.  
  1797. const nodes = document.querySelectorAll(domCheckSelectorForShorts);
  1798. if (nodes.length === 0) {
  1799. lockResolve();
  1800. return;
  1801. }
  1802.  
  1803. for (const node of nodes) {
  1804.  
  1805. const rchannelNameElm = parentYtElement(node, 'YTD-CHANNEL-NAME', 18);
  1806. if (rchannelNameElm) {
  1807. mutObserverForShortsChannelName.observe(rchannelNameElm, {
  1808. childList: true,
  1809. subtree: true
  1810. });
  1811. let channelId = '';
  1812. byPassCheckStore.add(rchannelNameElm);
  1813. channelId = checkShortsChannelName(rchannelNameElm);
  1814. rchannelNameElm.setAttribute('jkrgy', channelId || '');
  1815. }
  1816.  
  1817. }
  1818.  
  1819. lockResolve();
  1820.  
  1821. // }, 80);
  1822.  
  1823.  
  1824. })
  1825.  
  1826. }
  1827.  
  1828. const domCheckerForDescriptionAnchor = (s) => {
  1829.  
  1830. findTextNodes(s, (textNode) => {
  1831. const p = textNode.nodeValue;
  1832. const index = p.indexOf('@');
  1833. if (index < 0) return;
  1834. const q = p.substring(index);
  1835. const b = isDisplayAsHandle(q);
  1836. if (b) {
  1837. const href = s.getAttribute('href')
  1838.  
  1839. const channelId = obtainChannelId(href);
  1840. if (channelId) {
  1841. let h = q.trim();
  1842.  
  1843. if (!channelIdToHandle.has(channelId)) {
  1844. channelIdToHandle.set(channelId, {
  1845. handleText: h,
  1846. justPossible: true
  1847. });
  1848. }
  1849.  
  1850. s.setAttribute('jkrgy', channelId);
  1851.  
  1852. getDisplayName(channelId).then(fetchResult => {
  1853.  
  1854. if (fetchResult === null) return;
  1855.  
  1856. const { title, langTitle, externalId } = fetchResult;
  1857. const titleForDisplay = langTitle || title;
  1858. if (externalId !== channelId) return; // channel id must be the same
  1859.  
  1860. const anchorElement = s;
  1861. const anchorTextNode = textNode;
  1862.  
  1863. if (anchorElement.isConnected === true && anchorTextNode.isConnected === true && anchorElement.contains(anchorTextNode) && anchorTextNode.nodeValue === p && s.getAttribute('href') === href) {
  1864.  
  1865. anchorTextNode.nodeValue = `${p.substring(0, index)}${q.replace(h, titleForDisplay)}`;
  1866.  
  1867. }
  1868.  
  1869. });
  1870. }
  1871. }
  1872. return true;
  1873. });
  1874.  
  1875. }
  1876. const domCheckerForDescription = isMobile ? () => {
  1877. // example https://m.youtube.com/watch?v=jKt4Ah47L7Q
  1878. for (const s of document.querySelectorAll('span.yt-core-attributed-string a.yt-core-attributed-string__link.yt-core-attributed-string__link--display-type.yt-core-attributed-string__link--call-to-action-color[href*="channel/"]:not([dxcPj])')) {
  1879. s.setAttribute('dxcPj', '');
  1880. domCheckerForDescriptionAnchor(s);
  1881. }
  1882. } : () => {
  1883. // example https://www.youtube.com/watch?v=jKt4Ah47L7Q
  1884. for (const s of document.querySelectorAll('yt-attributed-string a.yt-core-attributed-string__link.yt-core-attributed-string__link--display-type.yt-core-attributed-string__link--call-to-action-color[href*="channel/"]:not([dxcPj])')) {
  1885. s.setAttribute('dxcPj', '');
  1886. domCheckerForDescriptionAnchor(s);
  1887. }
  1888. };
  1889.  
  1890. let domCheckScheduled = false;
  1891. const domCheckSelector = isMobile ? 'a[aria-label^="@"][href*="channel/"]:not([jkrgy])' : 'a[id][href*="channel/"]:not([jkrgy])';
  1892. const domChecker = () => {
  1893.  
  1894. doShortsChecking && domCheckerForShorts();
  1895. doDescriptionChecking && domCheckerForDescription();
  1896.  
  1897. if (domCheckScheduled) return;
  1898. if (document.querySelector(domCheckSelector) === null) {
  1899. return;
  1900. }
  1901. domCheckScheduled = true;
  1902.  
  1903. mutexForDOMMutations.lockWith(lockResolve => {
  1904. domCheckScheduled = false;
  1905.  
  1906. const newAnchors = document.querySelectorAll(domCheckSelector);
  1907. if (newAnchors.length === 0) {
  1908. lockResolve();
  1909. return;
  1910. }
  1911.  
  1912. const cNewAnchorFirst = newAnchors[0]; // non-null
  1913. const cNewAnchorLast = newAnchors[newAnchors.length - 1]; // non-null
  1914. /** @type {HTMLElement | null} */
  1915. const lastNewAnchorLast = kRef(lastNewAnchorLastWR); // HTMLElement | null
  1916. lastNewAnchorLastWR = mWeakRef(cNewAnchorLast);
  1917.  
  1918. if (!firstDOMCheck) {
  1919. firstDOMCheck = true;
  1920. USE_CHANNEL_META && firstDOMChecker();
  1921. }
  1922.  
  1923. Promise.resolve()
  1924. .then(() => {
  1925. if (lastNewAnchorLast && mutex.nextIndex >= 1) {
  1926. if ((lastNewAnchorLast.compareDocumentPosition(cNewAnchorFirst) & 2) === 2) {
  1927. mutex.nextIndex = 0;
  1928. }
  1929. }
  1930. }).then(() => {
  1931.  
  1932. // newAnchorAdded = true;
  1933. for (const anchor of newAnchors) {
  1934. // author-text or name
  1935. // normal url: /channel/xxxxxxx
  1936. // Improve YouTube! - https://www.youtube.com/channel/xxxxxxx/videos
  1937. const href = anchor.getAttribute('href');
  1938. const channelId = obtainChannelId(href); // string, can be empty
  1939. anchor.setAttribute('jkrgy', channelId);
  1940. // intersectionobserver.unobserve(anchor);
  1941. // intersectionobserver.observe(anchor); // force first occurance
  1942. domCheckAsync(anchor, href, channelId);
  1943. }
  1944.  
  1945. }).then(lockResolve).catch(e => {
  1946. lockResolve();
  1947. console.warn(e);
  1948. });
  1949.  
  1950. });
  1951.  
  1952.  
  1953. };
  1954.  
  1955.  
  1956. /** @type {MutationObserver | null} */
  1957. let domObserver = null;
  1958.  
  1959. /** @type {(callback: (event: Event)=>{})} */
  1960. const onPageFetched = isMobile ? (callback) => {
  1961.  
  1962. let state = 0;
  1963.  
  1964. window.addEventListener('state-change', function (evt) {
  1965. if (state === 1) return; // ignore intermediate events
  1966. state = 1; // before value: 0 or 2;
  1967. callback(evt);
  1968. }, false);
  1969.  
  1970. window.addEventListener('state-navigateend', function (evt) { // fallback
  1971. let inSeq = state === 1;
  1972. state = 2; // 2 when loaded
  1973. if (!inSeq) callback(evt);
  1974. }, false);
  1975.  
  1976. } : (callback) => {
  1977.  
  1978. let state = 0;
  1979.  
  1980. document.addEventListener('yt-page-data-fetched', function (evt) {
  1981. if (state === 1) return; // ignore intermediate events if any
  1982. state = 1; // before value: 0 or 2;
  1983. callback(evt);
  1984. }, false);
  1985.  
  1986. document.addEventListener('yt-navigate-finish', function (evt) { // fallback
  1987. let inSeq = state === 1;
  1988. state = 2; // 2 when loaded
  1989. if (!inSeq) callback(evt);
  1990. }, false);
  1991.  
  1992. };
  1993.  
  1994. /**
  1995. *
  1996. * @param {Node} app
  1997. * @returns
  1998. */
  1999. function setupOnPageFetched(app) {
  2000.  
  2001. firstDOMCheck = false;
  2002.  
  2003. if (!cfg['INNERTUBE_API_KEY']) {
  2004. console.warn("Userscript Error: INNERTUBE_API_KEY is not found.");
  2005. return;
  2006. }
  2007.  
  2008. if (CHANGE_FOR_SHORTS_CHANNEL_NAME && location.pathname.startsWith('/shorts/')) {
  2009. doShortsChecking = true;
  2010. } else {
  2011. doShortsChecking = false;
  2012. }
  2013.  
  2014. if (!domObserver) {
  2015. domObserver = new MutationObserver(domChecker);
  2016. } else {
  2017. domObserver.takeRecords();
  2018. domObserver.disconnect();
  2019. }
  2020.  
  2021. domObserver.observe(app, { childList: true, subtree: true });
  2022. domChecker();
  2023.  
  2024. }
  2025.  
  2026. /**
  2027. *
  2028. * @param {Object|null} dest
  2029. * @param {Object} ytcfg
  2030. * @param {string[]} keys
  2031. * @returns
  2032. */
  2033. function getYtConfig(dest, ytcfg, keys) {
  2034. dest = dest || {};
  2035. if (typeof ytcfg.get === 'function') { // mobile
  2036. for (const key of keys) {
  2037. dest[key] = ytcfg.get(key);
  2038. }
  2039. } else {
  2040. const cfgData = ytcfg.data_ || ytcfg.__data || ytcfg._data || ytcfg.data || ytcfg; // dekstop - ytcfg.data_
  2041. for (const key of keys) {
  2042. dest[key] = cfgData[key];
  2043. }
  2044. }
  2045. return dest;
  2046. }
  2047.  
  2048. /** @type { (evt: Object) => Element | null } */
  2049. const getPageApp = isMobile
  2050. ? ((evt) => (document.querySelector('ytm-app#app') || document.querySelector('ytm-app')))
  2051. : ((evt) => ((evt || 0).target || document.querySelector('ytd-app')));
  2052.  
  2053. onPageFetched(async (evt) => {
  2054. const app = getPageApp(evt);
  2055. console.assert(app);
  2056. const ytcfg = ((window || 0).ytcfg || 0);
  2057. getYtConfig(cfg, ytcfg, ['INNERTUBE_API_KEY', 'INNERTUBE_CLIENT_VERSION', 'HL', 'GL']);
  2058. setupOnPageFetched(app);
  2059. });
  2060.  
  2061. })({ fetch, JSON, Request, AbortController, setTimeout, clearTimeout });