Restore YouTube Username from Handle to Custom

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

当前为 2024-06-28 提交的版本,查看 最新版本

  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.12.0
  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://raw.githubusercontent.com/cyfung1031/userscript-supports/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} (before May 2024)
  139. * See isDisplayAsHandle
  140. */
  141.  
  142. /**
  143. @typedef {string} ChannelId
  144. * UC[-_a-zA-Z0-9+=.]{22}
  145. * https://support.google.com/youtube/answer/6070344?hl=en
  146. * The channel ID is the 24 character alphanumeric string that starts with 'UC' in the channel URL.
  147. */
  148.  
  149. /**
  150. @typedef {string} DisplayName
  151. * Display Name
  152. */
  153.  
  154. /**
  155. @typedef DisplayNameResultObject
  156. @type {Object}
  157. @property {DisplayName} title
  158. @property {DisplayName} langTitle
  159. @property {ChannelId?} externalId
  160. @property {string[]} ownerUrls
  161. @property {string} channelUrl
  162. @property {string} vanityChannelUrl
  163. @property {boolean|undefined} verified123
  164. */
  165.  
  166. /**
  167. @typedef ChannelIdToHandleResult
  168. @type {Object}
  169. @property {HandleText} handleText
  170. @property {boolean} justPossible
  171. */
  172.  
  173. (function (__CONTEXT__) {
  174. 'use strict';
  175.  
  176. // const JUST_REMOVE_AT_FOR_EXPANDED_LANG = true;
  177.  
  178. const USE_RSS_FETCHER = true; // might 404
  179. const USE_TIMEOUT_SIGNAL = false;
  180. const USE_CHANNEL_META = true;
  181. const CHANGE_FOR_SHORTS_CHANNEL_NAME = false;
  182. const USE_LANG_SPECIFIC_NAME = true;
  183. const UPDATE_PIN_NAME = true; // for USE_LANG_SPECIFIC_NAME
  184. const FIX_RTL_ISSUE = true;
  185.  
  186. const IGNORE_NO_NAME = false;
  187.  
  188. /** @type {globalThis.PromiseConstructor} */
  189. const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  190.  
  191. const { fetch, JSON, Request, AbortController, setTimeout, clearTimeout } = __CONTEXT__; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  192.  
  193. const timeoutSignal = AbortController && USE_TIMEOUT_SIGNAL ? (timeout) => {
  194. let controller = new AbortController();
  195. const r = controller.signal;
  196. let cid = setTimeout(() => {
  197. if (cid >= 1) {
  198. cid = 0;
  199. controller && controller.abort();
  200. controller = null;
  201. }
  202. }, timeout);
  203. r.clearTimeout = () => {
  204. if (cid >= 1) {
  205. clearTimeout(cid);
  206. cid = 0;
  207. }
  208. }
  209. return r;
  210. } : () => { };
  211.  
  212. const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);
  213.  
  214. const fxOperator = (proto, propertyName) => {
  215. let propertyDescriptorGetter = null;
  216. try {
  217. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  218. // https://github.com/cyfung1031/userscript-supports/issues/9
  219. // .parentNode becomes DocumentFragment
  220. /**
  221. *
  222. * Issue Description: YtCustomElements - Custom DOM Operations overrided in FireFox
  223. *
  224. * e.g. ytd-comment-renderer#comment > div#body
  225. *
  226. * ${div#body}.parentNode = DocumentFragment <Node.parentNode>
  227. * ${ytd-comment-renderer#comment}.firstElementChild <Element.firstElementChild>
  228. *
  229. * Cofirmed Affected: parentNode, firstChild, firstElementChild, lastChild, lastElementChild, querySelector, & querySelectorAll
  230. *
  231. * Alternative way: ytCustomElement.$.xxxxxxx
  232. *
  233. */
  234. } catch (e) { }
  235. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  236. };
  237.  
  238. const fxAPI = (proto, propertyName) => {
  239. const methodFunc = proto[propertyName];
  240. return typeof methodFunc === 'function' ? (e, ...args) => methodFunc.apply(e, args) : (e, ...args) => e[propertyName](...args);
  241. };
  242.  
  243. /** @type { (node: Node)=>Node | null } */
  244. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  245. /** @type { (node: Node)=>Node | null } */
  246. const nodeFirstChild = fxOperator(Node.prototype, 'firstChild');
  247. /** @type { (node: Node)=>Node | null } */
  248. const nodeNextSibling = fxOperator(Node.prototype, 'nextSibling');
  249.  
  250. /** @type { (node: ParentNode, selector: string)=>Element | null } */
  251. const elementQS = fxAPI(Element.prototype, 'querySelector');
  252. /** @type { (node: ParentNode, selector: string)=>Element[] } */
  253. const elementQSA = fxAPI(Element.prototype, 'querySelectorAll');
  254.  
  255. /*
  256.  
  257.  
  258. init
  259.  
  260. initialdata
  261. state-progress
  262. state-responsereceived
  263. player-autonav-pause
  264. state-change
  265. state-navigateend
  266. player-initialized
  267. renderer-module-load-start
  268. video-data-change
  269. player-state-change
  270. updateui
  271. renderer-module-load-end -> channel owner DOM available
  272. player-autonav-pause
  273. updateui
  274. renderer-module-load-start
  275. updateui
  276. renderer-module-load-end
  277.  
  278.  
  279. playing
  280.  
  281. h5player.video-progress
  282. h5player.video-progress
  283. h5player.video-progress
  284. h5player.video-progress
  285. ...
  286.  
  287.  
  288.  
  289.  
  290. navigate new video
  291.  
  292. state-navigatestart
  293. state-change => channel owener DOM vanished
  294. state-progress
  295. ...
  296. h5player.player-state-change
  297. h5player.video-data-change
  298. h5player.player-state-change
  299. h5player.muted-autoplay-change
  300. h5player.volume-change
  301. h5player.video-data-change
  302. h5player.volume-change
  303. ...
  304.  
  305. state-progress
  306. ...
  307.  
  308. state-progress => channel owner DOM appear [just before state-responsereceived]
  309. state-responsereceived
  310.  
  311. video-data-change
  312. state-change
  313. state-navigateend
  314.  
  315.  
  316.  
  317.  
  318. UX interaction
  319.  
  320. user-activity
  321. player-autonav-pause
  322.  
  323.  
  324.  
  325.  
  326.  
  327. EventTarget.prototype.uhfsE = EventTarget.prototype.dispatchEvent
  328.  
  329. EventTarget.prototype.dispatchEvent = function (ev) {
  330. if (ev instanceof Event) {
  331. if (ev.type === 'video-progress') { } else {
  332. console.log(ev.type, this, ev.target)
  333. if (ev.target === null) {
  334. console.log(ev.type, (document.querySelector('ytm-app') || 0).data)
  335. console.log(ev.type, (document.querySelector('ytm-slim-owner-renderer') || 0).textContent)
  336. }
  337. }
  338. }
  339. return this.uhfsE.apply(this, arguments);
  340. }
  341.  
  342.  
  343.  
  344. */
  345.  
  346. const isMobile = location.hostname === 'm.youtube.com';
  347.  
  348. if (!isMobile && typeof AbortSignal !== 'undefined') {
  349. document.addEventListener('yt-action', () => {
  350. try {
  351. yt.config_.EXPERIMENT_FLAGS.enable_profile_cards_on_comments = true;
  352. } catch (e) { }
  353. }, { capture: true, passive: true, once: true })
  354. }
  355.  
  356.  
  357. const delayPn = delay => new Promise((fn => setTimeout(fn, delay)));
  358.  
  359. const { retrieveCE } = isMobile ? (() => {
  360.  
  361. const retrieveCE = () => {
  362. console.warn('retrieveCE is not supported');
  363. }
  364. return { retrieveCE };
  365.  
  366. })() : (() => {
  367.  
  368. const isCustomElementsProvided = typeof customElements !== "undefined" && typeof (customElements || 0).whenDefined === "function";
  369.  
  370. const promiseForCustomYtElementsReady = isCustomElementsProvided ? Promise.resolve(0) : new Promise((callback) => {
  371. const EVENT_KEY_ON_REGISTRY_READY = "ytI-ce-registry-created";
  372. if (typeof customElements === 'undefined') {
  373. if (!('__CE_registry' in document)) {
  374. // https://github.com/webcomponents/polyfills/
  375. Object.defineProperty(document, '__CE_registry', {
  376. get() {
  377. // return undefined
  378. },
  379. set(nv) {
  380. if (typeof nv == 'object') {
  381. delete this.__CE_registry;
  382. this.__CE_registry = nv;
  383. this.dispatchEvent(new CustomEvent(EVENT_KEY_ON_REGISTRY_READY));
  384. }
  385. return true;
  386. },
  387. enumerable: false,
  388. configurable: true
  389. })
  390. }
  391. let eventHandler = (evt) => {
  392. document.removeEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
  393. const f = callback;
  394. callback = null;
  395. eventHandler = null;
  396. f();
  397. };
  398. document.addEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
  399. } else {
  400. callback();
  401. }
  402. });
  403.  
  404. const retrieveCE = async (nodeName) => {
  405. try {
  406. isCustomElementsProvided || (await promiseForCustomYtElementsReady);
  407. await customElements.whenDefined(nodeName);
  408. const dummy = document.querySelector(nodeName) || document.createElement(nodeName);
  409. const cProto = insp(dummy).constructor.prototype;
  410. return cProto;
  411. } catch (e) {
  412. console.warn(e);
  413. }
  414. }
  415.  
  416. return {retrieveCE};
  417.  
  418. })();
  419.  
  420. const cfg = {};
  421. class Mutex {
  422.  
  423. constructor() {
  424. this.p = Promise.resolve()
  425. }
  426.  
  427. /**
  428. * @param {(lockResolve: () => void)} f
  429. */
  430. lockWith(f) {
  431. this.p = this.p.then(() => new Promise(f).catch(console.warn))
  432. }
  433.  
  434. }
  435.  
  436. class OrderedMutex extends Mutex {
  437. constructor() {
  438. super();
  439. this.nextIndex = 0;
  440. this.arr = [];
  441. this.lockFunc = resolve => {
  442. if (this.arr.length >= 1) {
  443. let f = this.arr[0];
  444. if (typeof f === 'function') {
  445. this.arr.shift();
  446. if (this.nextIndex > 0) this.nextIndex--;
  447. f(resolve);
  448. return;
  449. } else {
  450. // reset if unknown error
  451. this.arr.length = 0;
  452. this.nextIndex = 0;
  453. }
  454. }
  455. resolve();
  456. };
  457. }
  458. /**
  459. * @param {(lockResolve: () => void)} f
  460. */
  461. add(f) {
  462. if (this.nextIndex === this.arr.length) {
  463. this.arr.push(f); this.nextIndex++;
  464. } else {
  465. this.arr.splice(this.nextIndex++, 0, f);
  466. }
  467. this.lockWith(this.lockFunc);
  468. }
  469. }
  470. // usage: run network request one by one
  471. const mutex = new OrderedMutex();
  472.  
  473. const createPipeline = () => {
  474. let pipelineMutex = Promise.resolve();
  475. const pipelineExecution = fn => {
  476. return new Promise((resolve, reject) => {
  477. pipelineMutex = pipelineMutex.then(async () => {
  478. let res;
  479. try {
  480. res = await fn();
  481. } catch (e) {
  482. console.log('error_F1', e);
  483. reject(e);
  484. }
  485. resolve(res);
  486. }).catch(console.warn);
  487. });
  488. };
  489. return pipelineExecution;
  490. };
  491.  
  492. /**
  493. * @typedef { AsyncValue<T> | T} AsyncOrResolvedValue<T>
  494. * @template T */
  495.  
  496. /**
  497. * usage: cache the network result per web application instance.
  498. * @type {Map<ChannelId, AsyncOrResolvedValue<DisplayNameResultObject>>} */
  499. const displayNameResStore = new Map();
  500.  
  501. /**
  502. * usage: mapping the .dataChanged() to the wrapped function - only few entries as most are {ytd-comment-renderer}.dataChanged().
  503. * @type {WeakMap<Function, Function>} */
  504. const dataChangedFuncStore = new WeakMap();
  505.  
  506. /**
  507. * usage: for \@Mention inside comments in YouTube Mobile that without hyperlinks for channelId.
  508. * @type {Map<HandleText, AsyncOrResolvedValue<ChannelId>>} */
  509. const handleToChannelId = new Map();
  510.  
  511. /**
  512. * usage: in RSS fetching, no handle text will be obtained from the response. inject the handle into the response.
  513. * @type {Map<ChannelId, ChannelIdToHandleResult>} */
  514. const channelIdToHandle = new Map();
  515.  
  516. /**
  517. * usage: replace handle to lang-specific display name
  518. * @type {Map<HandleText, DisplayName>} */
  519. const langPreferredDisplayNameMap = new Map();
  520.  
  521. /**
  522. * AsyncValue
  523. * @class
  524. * @template T
  525. */
  526. class AsyncValue {
  527. constructor() {
  528. /** @type {T | null} */
  529. this.__value__ = null;
  530. /** @type {((value: any)=>void) | null} */
  531. this.__resolve__ = null;
  532. this.__promise__ = new Promise(resolve => {
  533. this.__resolve__ = resolve;
  534. });
  535. }
  536. /**
  537. *
  538. * @param {T} value
  539. */
  540. setValue(value) {
  541. const promise = this.__promise__;
  542. if (promise === null) throw 'Value has already been set.';
  543. this.__value__ = value;
  544. this.__promise__ = null;
  545. this.__resolve__ ? this.__resolve__() : Promise.resolve().then(() => this.__resolve__());
  546. }
  547. /**
  548. *
  549. * @returns {Promise<T>}
  550. */
  551. async getValue() {
  552. const promise = this.__promise__;
  553. if (promise === null) return this.__value__;
  554. await promise; // promise become null
  555. this.__promise__ = null; // just in case
  556. this.__resolve__ = null;
  557. return this.__value__;
  558. }
  559. }
  560.  
  561. let isCSSAdded = false;
  562. const addCSSRulesIfRequired = () => {
  563. if (isCSSAdded) return;
  564. isCSSAdded = true;
  565. const style = document.createElement('style')
  566. let nonce = document.querySelector('head style[nonce]');
  567. nonce = nonce ? nonce.getAttribute('nonce') : null;
  568.  
  569. /* #contenteditable-root.yt-formatted-string:lang(he) */ /* Hebrew (LTR, when mixed with LTR languages) */
  570. /* #contenteditable-root.yt-formatted-string:lang(he) */ /* Hebrew (pure) */
  571.  
  572. const cssText01_FIX_RTL_ISSUE = FIX_RTL_ISSUE ? `
  573.  
  574. /* Left-to-Right (LTR) Languages */
  575. #contenteditable-root.yt-formatted-string:lang(en) /* English */,
  576. #contenteditable-root.yt-formatted-string:lang(es) /* Spanish */,
  577. #contenteditable-root.yt-formatted-string:lang(fr) /* French */,
  578. #contenteditable-root.yt-formatted-string:lang(de) /* German */,
  579. #contenteditable-root.yt-formatted-string:lang(it) /* Italian */,
  580. #contenteditable-root.yt-formatted-string:lang(pt) /* Portuguese */,
  581. #contenteditable-root.yt-formatted-string:lang(ru) /* Russian */,
  582. #contenteditable-root.yt-formatted-string:lang(zh) /* Chinese */,
  583. #contenteditable-root.yt-formatted-string:lang(ja) /* Japanese */,
  584. #contenteditable-root.yt-formatted-string:lang(ko) /* Korean */,
  585. #contenteditable-root.yt-formatted-string:lang(nl) /* Dutch */,
  586. #contenteditable-root.yt-formatted-string:lang(sv) /* Swedish */,
  587. #contenteditable-root.yt-formatted-string:lang(fi) /* Finnish */,
  588. #contenteditable-root.yt-formatted-string:lang(da) /* Danish */,
  589. #contenteditable-root.yt-formatted-string:lang(no) /* Norwegian */,
  590. #contenteditable-root.yt-formatted-string:lang(pl) /* Polish */,
  591. #contenteditable-root.yt-formatted-string:lang(cs) /* Czech */,
  592. #contenteditable-root.yt-formatted-string:lang(sk) /* Slovak */,
  593. #contenteditable-root.yt-formatted-string:lang(hu) /* Hungarian */,
  594. #contenteditable-root.yt-formatted-string:lang(tr) /* Turkish */,
  595. #contenteditable-root.yt-formatted-string:lang(el) /* Greek */,
  596. #contenteditable-root.yt-formatted-string:lang(id) /* Indonesian */,
  597. #contenteditable-root.yt-formatted-string:lang(ms) /* Malay */,
  598. #contenteditable-root.yt-formatted-string:lang(th) /* Thai */,
  599. #contenteditable-root.yt-formatted-string:lang(vi) /* Vietnamese */
  600. {
  601. direction: ltr;
  602. }
  603.  
  604. /* Right-to-Left (RTL) Languages */
  605. #contenteditable-root.yt-formatted-string:lang(ar) /* Arabic */,
  606. #contenteditable-root.yt-formatted-string:lang(fa) /* Persian */,
  607. #contenteditable-root.yt-formatted-string:lang(ur) /* Urdu */,
  608. #contenteditable-root.yt-formatted-string:lang(dv) /* Divehi */
  609. {
  610. direction: rtl;
  611. }
  612.  
  613. ` : '';
  614. style.textContent = `
  615. ${cssText01_FIX_RTL_ISSUE}
  616. `.trim();
  617. if (nonce) style.setAttribute('nonce', nonce);
  618. document.head.appendChild(style);
  619. }
  620.  
  621. /**
  622. *
  623. * @param {DisplayNameResultObject} resultInfo
  624. * @param {string} channelId
  625. */
  626. const cacheHandleToChannel = (resultInfo, channelId) => {
  627.  
  628. const { vanityChannelUrl, ownerUrls } = resultInfo;
  629.  
  630. let handleText = urlToHandle(vanityChannelUrl || '');
  631.  
  632. if (handleText) {
  633. // match = true;
  634. } else if ((ownerUrls || 0).length >= 1) {
  635. for (const ownerUrl of ownerUrls) {
  636. handleText = urlToHandle(ownerUrl || '');
  637.  
  638. if (handleText) {
  639. // match = true;
  640. break;
  641. }
  642. }
  643. }
  644. if (handleText) {
  645.  
  646. const asyncValue = handleToChannelId.get(handleText); // nothing if no pending promise
  647. if (asyncValue instanceof AsyncValue) {
  648. asyncValue.setValue(channelId);
  649. }
  650. handleToChannelId.set(handleText, channelId);
  651. }
  652.  
  653.  
  654. }
  655.  
  656. /**
  657. *
  658. * @param {string} channelId
  659. * @param {Function} onDownloaded
  660. * @param {Function} onResulted
  661. * @param {Function} onError
  662. */
  663. const fetcherBrowseAPI = (channelId, onDownloaded, onResulted, onError) => {
  664.  
  665. let signal = timeoutSignal(4000);
  666.  
  667. const requestBody = {
  668. "context": {
  669. "client": {
  670. "visitorData": "Cgs0aVg0VjFWM0U0USi0jvOkBg%3D%3D", // [optional] fake visitorData to avoid dynamic visitorData generated in response
  671. "clientName": "MWEB", // "WEB", "MWEB"
  672. "clientVersion": `${cfg.INNERTUBE_CLIENT_VERSION || '2.20230614.01.00'}`, // same as WEB version
  673. "originalUrl": `https://m.youtube.com/channel/${channelId}`,
  674. "playerType": "UNIPLAYER",
  675. "platform": "MOBILE", // "DESKTOP", "MOBILE", "TABLET"
  676. "clientFormFactor": "SMALL_FORM_FACTOR", // "LARGE_FORM_FACTOR", "SMALL_FORM_FACTOR"
  677. "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",
  678. "mainAppWebInfo": {
  679. "graftUrl": `/channel/${channelId}`,
  680. "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER",
  681. "isWebNativeShareAvailable": true
  682. }
  683. },
  684. "user": {
  685. "lockedSafetyMode": false
  686. },
  687. "request": {
  688. "useSsl": true,
  689. "internalExperimentFlags": [],
  690. "consistencyTokenJars": []
  691. }
  692. },
  693. "browseId": `${channelId}`
  694. };
  695.  
  696. if (USE_LANG_SPECIFIC_NAME && cfg.HL && cfg.GL) {
  697. Object.assign(requestBody.context.client, {
  698. "hl": cfg.HL,
  699. "gl": cfg.GL,
  700. });
  701. }
  702.  
  703. fetch(new Request(`/youtubei/v1/browse?key=${cfg.INNERTUBE_API_KEY}&prettyPrint=false`, {
  704. "method": "POST",
  705. "mode": "same-origin",
  706. "credentials": "omit",
  707.  
  708. referrerPolicy: "no-referrer",
  709. cache: "default", // no effect on POST request
  710. // cache: "force-cache",
  711. redirect: "error", // there shall be no redirection in this API request
  712. integrity: "",
  713. keepalive: false,
  714. signal,
  715.  
  716. "headers": {
  717. "Content-Type": "application/json", // content type of the body data in POST request
  718. "Accept-Encoding": "gzip, deflate, br", // YouTube Response - br
  719. // X-Youtube-Bootstrap-Logged-In: false,
  720. // X-Youtube-Client-Name: 1, // INNERTUBE_CONTEXT_CLIENT_NAME
  721. // X-Youtube-Client-Version: "2.20230622.06.00" // INNERTUBE_CONTEXT_CLIENT_VERSION
  722.  
  723. "Accept": "application/json",
  724. },
  725. "body": JSON.stringify(requestBody)
  726. })).then(res => {
  727. signal && signal.clearTimeout && signal.clearTimeout();
  728. signal = null;
  729. onDownloaded();
  730. onDownloaded = null;
  731. return res.json();
  732. }).then(resJson => {
  733. const resultInfo = ((resJson || 0).metadata || 0).channelMetadataRenderer;
  734. const title = resultInfo ? resultInfo.title : null;
  735. if (title || title === '') {
  736.  
  737. resultInfo.title = '';
  738. resultInfo.langTitle = title;
  739.  
  740. if (cfg.ownerProfileUrl && cfg.ownerChannelName && cfg.ownerChannelName !== title) {
  741. let matched = false;
  742.  
  743. const ownerProfileUrlLowerCase = cfg.ownerProfileUrl.toLowerCase();
  744. if (ownerProfileUrlLowerCase.endsWith(`/channel/${channelId.toLowerCase()}`)) matched = true;
  745. else {
  746.  
  747. for (const ownerUrl of resultInfo.ownerUrls) {
  748. if (ownerUrl.toLowerCase().endsWith(ownerProfileUrlLowerCase)) {
  749. matched = true;
  750. break;
  751. }
  752. }
  753.  
  754. }
  755.  
  756. if (matched) {
  757. resultInfo.title = cfg.ownerChannelName;
  758. }
  759. }
  760.  
  761. }
  762.  
  763. onResulted(resultInfo);
  764. onResulted = null;
  765. }).catch(onError);
  766.  
  767. };
  768.  
  769. /**
  770. *
  771. * @param {string} channelId
  772. * @param {Function} onDownloaded
  773. * @param {Function} onResulted
  774. * @param {Function} onError
  775. */
  776. let fetchRSSFailed = false;
  777. const fetcherRSS = location.origin !== 'https://www.youtube.com' ? null : (channelId, onDownloaded, onResulted, onError) => {
  778.  
  779. let signal = timeoutSignal(4000);
  780. fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`, {
  781. // YouTube RSS Response - public, max-age=900
  782.  
  783. "method": "GET",
  784. "mode": "same-origin",
  785. "credentials": "omit",
  786.  
  787. referrerPolicy: "no-referrer",
  788. cache: "default",
  789. // cache: "no-cache",
  790. redirect: "error", // there shall be no redirection in this API request
  791. integrity: "",
  792. keepalive: false,
  793. signal,
  794.  
  795. "headers": {
  796. "Cache-Control": "public, max-age=900, stale-while-revalidate=1800",
  797. // refer "Cache-Control Use Case Examples" in https://www.koyeb.com/blog/using-cache-control-and-cdns-to-improve-performance-and-reduce-latency
  798. // seems YouTube RSS Feeds server insists its own Cache-Control.
  799.  
  800. // "Content-Type": "text/xml; charset=UTF-8",
  801. "Accept-Encoding": "gzip, deflate, br", // YouTube Response - gzip
  802. // X-Youtube-Bootstrap-Logged-In: false,
  803. // X-Youtube-Client-Name: 1, // INNERTUBE_CONTEXT_CLIENT_NAME
  804. // X-Youtube-Client-Version: "2.20230622.06.00" // INNERTUBE_CONTEXT_CLIENT_VERSION
  805.  
  806. "Accept": "text/xml",
  807. }
  808. }).then(res => {
  809.  
  810. signal && signal.clearTimeout && signal.clearTimeout();
  811. signal = null;
  812.  
  813. if (res.ok === true && res.redirected === false && res.status === 200 && res.type === "basic") {
  814.  
  815. onDownloaded();
  816. onDownloaded = null;
  817. return res.text();
  818.  
  819. }
  820. return null;
  821.  
  822. }).then(resText => {
  823. if (resText && typeof resText === 'string' && resText.includes('<feed') && !resText.includes('<!DOCTYPE')) {
  824.  
  825. } else {
  826. if (onDownloaded) {
  827.  
  828. onDownloaded();
  829. onDownloaded = null;
  830. }
  831. onResulted(-1);
  832. return;
  833.  
  834. }
  835.  
  836.  
  837. // console.log(124, , resText)
  838. let eIdx = resText.indexOf('<entry>');
  839. let mText = (eIdx > 0) ? `${resText.substring(0, eIdx).trim()}</feed>` : resText;
  840.  
  841. // simple: https://www.youtube.com/feeds/videos.xml?channel_id=UC-MUu72gixoYlhV5_mWa36g
  842. // standard: https://www.youtube.com/feeds/videos.xml?channel_id=UCGSfK5HeiIhuFfXoKE47TzA
  843. // long: https://www.youtube.com/feeds/videos.xml?channel_id=UC8cSGjKxDuh2mWG1hDOTdBg
  844. // special: http://www.youtube.com/feeds/videos.xml?channel_id=UCmZ2-GUxmdWFKfXA5IN0x-w
  845.  
  846.  
  847. let name, uri, mt;
  848.  
  849. // ===============================================================
  850.  
  851. /* removed in 2023.06.30 */
  852.  
  853.  
  854. // ===============================================================
  855.  
  856.  
  857. const wIdx1 = mText.indexOf('<author>');
  858. const wIdx2 = wIdx1 > 0 ? mText.indexOf('</author>', wIdx1 + 8) : -1;
  859.  
  860. if (wIdx1 > 0 && wIdx2 > 0) {
  861. let qText = mText.substring(wIdx1, wIdx2 + '</author>'.length);
  862. const template = document.createElement('template');
  863. template.innerHTML = qText;
  864. /** @type {HTMLElement | null} */
  865. let authorChild = ((template.content || 0).firstElementChild || 0).firstElementChild;
  866. for (; authorChild !== null; authorChild = authorChild.nextElementSibling) {
  867. const nodeName = authorChild.nodeName;
  868. if (nodeName === 'NAME') name = authorChild.textContent;
  869. else if (nodeName === 'URI') {
  870. uri = authorChild.textContent;
  871. mt = obtainChannelId(uri);
  872. }
  873. }
  874. template.innerHTML = '';
  875. }
  876.  
  877. if (!name && !uri) {
  878.  
  879. // fallback
  880.  
  881. const parser = new DOMParser();
  882. const xmlDoc = parser.parseFromString(mText, "text/xml");
  883.  
  884. // const author = xmlDoc.querySelector("author");
  885.  
  886. // https://extendsclass.com/xpath-tester.html
  887.  
  888. const authorChilds = xmlDoc.evaluate("//*[name()='author']/*", xmlDoc, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
  889.  
  890. let authorChild;
  891. while ((authorChild = authorChilds.iterateNext()) !== null) {
  892. if (authorChild.nodeName === 'name') name = authorChild.textContent;
  893. else if (authorChild.nodeName === 'uri') {
  894. uri = authorChild.textContent;
  895. mt = obtainChannelId(uri);
  896. }
  897. }
  898.  
  899. try {
  900. xmlDoc.firstChild.remove();
  901. } catch (e) { }
  902.  
  903. }
  904.  
  905.  
  906.  
  907.  
  908. let res = null;
  909.  
  910. if ((name || name === '') && uri && mt && mt === channelId) {
  911.  
  912.  
  913. const handleObj = channelIdToHandle.get(mt);
  914.  
  915. if (handleObj) {
  916.  
  917. const handleText = handleObj.handleText;
  918.  
  919. if (handleText === name) channelIdToHandle.delete(mt);
  920. else {
  921. handleObj.justPossible = false; // ignore @handle checking
  922.  
  923. res = {
  924. "title": name,
  925. "externalId": mt,
  926. "ownerUrls": [
  927. `http://www.youtube.com/${handleText}`
  928. ],
  929. "channelUrl": uri,
  930. "vanityChannelUrl": `http://www.youtube.com/${handleText}`
  931. };
  932.  
  933. if (USE_LANG_SPECIFIC_NAME) {
  934. const langPreferredDisplayName = langPreferredDisplayNameMap.get(handleText);
  935. if (langPreferredDisplayName && name !== langPreferredDisplayName) res.langTitle = langPreferredDisplayName;
  936. }
  937.  
  938. }
  939. }
  940. }
  941.  
  942. onResulted(res);
  943. onResulted = null;
  944.  
  945. // let resultInfo = ((res || 0).metadata || 0).channelMetadataRenderer;
  946. // onResulted(resultInfo);
  947. }).catch(onError);
  948.  
  949.  
  950.  
  951. }
  952.  
  953. sessionStorage.setItem('js-yt-usernames', '');
  954. let isFirstStackNewRequestSuccess = true;
  955. /**
  956. *
  957. * @param {string} channelId
  958. */
  959. function stackNewRequest(channelId) {
  960.  
  961. mutex.add(lockResolve => {
  962.  
  963. let bResult = displayNameResStore.get(channelId);
  964. if (!(bResult instanceof AsyncValue)) {
  965. // resolved or removed
  966. lockResolve(); lockResolve = null;
  967. return;
  968. }
  969.  
  970. let setResult = (result) => {
  971. setResult = null;
  972. if (!result) {
  973. bResult.fetchingState = 0;
  974. bResult.setValue(null);
  975. displayNameResStore.delete(channelId); // create another network response in the next request
  976. } else {
  977. bResult.fetchingState = 4;
  978. bResult.setValue(result);
  979. displayNameResStore.set(channelId, result); // update store result to resolved value
  980. }
  981. bResult = null;
  982. }
  983.  
  984. if (bResult.fetchingState >= 2) { // fetchingState == 3 or 4
  985. // request is already done. no need to stack any request
  986. lockResolve(); lockResolve = null;
  987. return;
  988. }
  989.  
  990. if (!document.querySelector(`[jkrgy="${channelId}"]`)) {
  991. // element has already been removed
  992. lockResolve(); lockResolve = null;
  993. setResult(null);
  994. return;
  995. }
  996.  
  997. //INNERTUBE_API_KEY = ytcfg.data_.INNERTUBE_API_KEY
  998.  
  999. bResult.fetchingState = 2;
  1000.  
  1001.  
  1002. const retry = () => {
  1003.  
  1004. fetchRSSFailed = true;
  1005.  
  1006. bResult.fetchingState = 2;
  1007.  
  1008. // Promise.resolve(channelId).then(stackNewRequest);
  1009. fetcherBrowseAPI(channelId, mobj.fetchOnDownload, mobj.fetchOnSuccess, mobj.fetchOnFail);
  1010. }
  1011.  
  1012. const mobj = {
  1013. fetchOnDownload: () => {
  1014. bResult.fetchingState = 3;
  1015. },
  1016. fetchOnSuccess: resultInfo => {
  1017.  
  1018. if (resultInfo === -1) {
  1019. retry();
  1020. return;
  1021. }
  1022.  
  1023. lockResolve && lockResolve();
  1024. lockResolve = null;
  1025. // console.log(900)
  1026.  
  1027. if (!resultInfo) {
  1028. // invalid json format
  1029. setResult(null);
  1030. return;
  1031. }
  1032.  
  1033. const titleForDisplay = resultInfo.langTitle || resultInfo.title;
  1034. const handleObj = channelIdToHandle.get(channelId);
  1035. const handleId = handleObj ? handleObj.handleText : '';
  1036. if (IGNORE_NO_NAME && !titleForDisplay) {
  1037. if (handleId) {
  1038. resultInfo.langTitle = resultInfo.title = handleObj.handleText;
  1039. } else {
  1040. setResult(null);
  1041. return;
  1042. }
  1043. }
  1044.  
  1045. cacheHandleToChannel(resultInfo, channelId);
  1046.  
  1047. const { title, langTitle, externalId, ownerUrls, channelUrl, vanityChannelUrl } = resultInfo;
  1048.  
  1049. const displayNameRes = { title, langTitle, externalId, ownerUrls, channelUrl, vanityChannelUrl, verified123: false };
  1050.  
  1051. if (handleId) {
  1052. if (isFirstStackNewRequestSuccess) {
  1053. isFirstStackNewRequestSuccess = false;
  1054. sessionStorage.setItem('js-yt-usernames', `${handleId}\t${titleForDisplay}`);
  1055. } else {
  1056. let current = sessionStorage.getItem('js-yt-usernames') || '';
  1057. if (current.length > 900000) {
  1058. const na = current.split('\n');
  1059. const n3 = Math.floor(na.length / 3);
  1060. if (n3 > 2) {
  1061. const newText = na.slice(0, n3).concat(na.slice(na.length - n3)).join('\n');
  1062. if (newText.length < 720000) {
  1063. current = newText;
  1064. }
  1065. }
  1066. }
  1067. sessionStorage.setItem('js-yt-usernames', `${current}\n${handleId}\t${titleForDisplay}`);
  1068. }
  1069. }
  1070.  
  1071. setResult(displayNameRes);
  1072. },
  1073. fetchOnFail: e => {
  1074.  
  1075.  
  1076. console.warn(e);
  1077. if (mobj.fetcher === fetcherRSS) {
  1078. retry();
  1079.  
  1080. } else {
  1081.  
  1082. lockResolve && lockResolve();
  1083. lockResolve = null;
  1084. // console.warn(e);
  1085. setResult && setResult(null);
  1086. }
  1087. }
  1088. }
  1089.  
  1090.  
  1091. // note: when setResult(null), the fetch will be requested again if the same username appears. (multiple occurrences)
  1092. // consider the network problem might be fixed in the 2nd attempt, the name will be changed in the 2nd attempt but ignore 1st attempt.
  1093. mobj.fetcher = USE_RSS_FETCHER && fetcherRSS && !fetchRSSFailed && channelIdToHandle.has(channelId) ? fetcherRSS : fetcherBrowseAPI;
  1094. mobj.fetcher(channelId, mobj.fetchOnDownload, mobj.fetchOnSuccess, mobj.fetchOnFail);
  1095.  
  1096. });
  1097.  
  1098. }
  1099.  
  1100. /**
  1101. *
  1102. * @param {string} url Example: _www\.youtube\.com/\@yr__kd_
  1103. * @returns Example: _\@yr__kd_
  1104. */
  1105. function urlToHandle(url) {
  1106.  
  1107. if (typeof url !== 'string') return '';
  1108. let i = url.indexOf('.youtube.com/');
  1109. if (i >= 1) url = url.substring(i + '.youtube.com/'.length);
  1110. else if (url.charAt(0) === '/') url = url.substring(1);
  1111. return isDisplayAsHandle(url) ? url : '';
  1112.  
  1113. }
  1114.  
  1115. /**
  1116. *
  1117. * @param {string} channelId Example: UC0gmRdmpDWJ4dt7DAeRaawA
  1118. * @returns {Promise<DisplayNameResultObject | null>}
  1119. */
  1120. async function getDisplayName(channelId) {
  1121.  
  1122. let cache = displayNameResStore.get(channelId);
  1123. let isStackNewRequest = false;
  1124.  
  1125. /** @type {AsyncValue<DisplayNameResultObject> | null | undefined} */
  1126. let aResult;
  1127.  
  1128. if (cache instanceof AsyncValue) {
  1129. aResult = cache;
  1130. if (aResult.fetchingState >= 2) {
  1131. // aResult.fetchingState
  1132. // 2: network fetch started
  1133. // 3: network fetch ended
  1134. // isStackNewRequest = false;
  1135. } else {
  1136. // 0: invalid before
  1137. // 1: scheduled but not yet fetch
  1138. isStackNewRequest = true;
  1139. }
  1140. } else if (cache) {
  1141. // resolved result
  1142. return cache;
  1143. } else {
  1144. // else no cached value found
  1145. /** @type {AsyncValue<DisplayNameResultObject>} */
  1146. aResult = new AsyncValue();
  1147. displayNameResStore.set(channelId, aResult);
  1148. isStackNewRequest = true;
  1149. }
  1150.  
  1151. if (isStackNewRequest) {
  1152. aResult.fetchingState = 1;
  1153. stackNewRequest(channelId);
  1154. }
  1155. const result = await aResult.getValue();
  1156. return result;
  1157. }
  1158.  
  1159.  
  1160. /**
  1161. *
  1162. * @param {string} href Example: https\:\/\/www\.youtube\.com/channel/UC0gmRdmpDWJ4dt7DAeRaawA
  1163. * @returns
  1164. * * Outdated?
  1165. */
  1166. const obtainChannelId = (href) => {
  1167. const s = `/${href}`;
  1168. if (s.length >= 33) {
  1169. const m = /\/channel\/(UC[-_a-zA-Z0-9+=.]{22})(\/|$)/.exec(s);
  1170. return !m ? '' : (m[1] || '');
  1171. }
  1172. return '';
  1173. };
  1174.  
  1175. const isUCBrowserId = (browseId) => {
  1176. return typeof browseId === 'string' && browseId.length === 24 && browseId.startsWith('UC') && /^UC[-_a-zA-Z0-9+=.]{22}$/.test(browseId);
  1177. }
  1178.  
  1179. const browseEndpointBaseUrlType = (browseEndpoint, displayText) => {
  1180. if (isDisplayAsHandle(displayText) && isUCBrowserId(browseEndpoint.browseId) && typeof browseEndpoint.canonicalBaseUrl === 'string') {
  1181. if (`/${displayText}` === browseEndpoint.canonicalBaseUrl) return 1 | 2;
  1182. if (`/channel/${browseEndpoint.browseId}` === browseEndpoint.canonicalBaseUrl) return 1 | 4;
  1183. if (browseEndpoint.canonicalBaseUrl.includes(`/${browseEndpoint.browseId}`)) return 1 | 8;
  1184. return 1;
  1185. }
  1186. return 0;
  1187. }
  1188.  
  1189. /**
  1190. *
  1191. * @param {Element} ytElm
  1192. */
  1193. const resetWhenDataChanged = (ths) => {
  1194. const cnt = insp(ths);
  1195. const ytElm = cnt.hostElement instanceof Element ? cnt.hostElement : cnt instanceof Element ? cnt : ths;
  1196. if (ytElm instanceof Element) {
  1197. const anchors = elementQSA(ytElm, 'a[id][href][jkrgy]');
  1198. // const anchors = elementQSA(ytElm, 'a[id][href*="channel/"][jkrgy]');
  1199. if ((anchors || 0).length >= 1 && (insp(ytElm).data || 0).jkrgx !== 1) {
  1200. for (const anchor of anchors) {
  1201. anchor.removeAttribute('jkrgy');
  1202. }
  1203. }
  1204. }
  1205. }
  1206.  
  1207. /**
  1208. *
  1209. * @param {Element} ytElm
  1210. */
  1211. const resetWhenPropChanged = (ths) => {
  1212. const cnt = insp(ths);
  1213. const ytElm = cnt.hostElement instanceof Element ? cnt.hostElement : cnt instanceof Element ? cnt : ths;
  1214. if (ytElm instanceof Element) {
  1215. const anchors = elementQSA(ytElm, 'a[id][href][jkrgy]');
  1216. // const anchors = elementQSA(ytElm, 'a[id][href*="channel/"][jkrgy]');
  1217. if ((anchors || 0).length >= 1 && ((insp(ytElm).commentEntity || 0).author || 0).jkrgx !== 1) {
  1218. for (const anchor of anchors) {
  1219. anchor.removeAttribute('jkrgy');
  1220. }
  1221. }
  1222. }
  1223. }
  1224.  
  1225. /**
  1226. *
  1227. * @param {HTMLAnchorElement} anchor Example: https\:\/\/www\.youtube\.com/channel/UCRmLncxsQFcOOC8OhzUIfxQ/videos
  1228. * @param {string} hrefAttribute Example: /channel/UCRmLncxsQFcOOC8OhzUIfxQ (old) 2024 Feb -> /@user-abc1234
  1229. * @param {string} channelId Example: UCRmLncxsQFcOOC8OhzUIfxQ
  1230. * @returns
  1231. */
  1232. const anchorIntegrityCheck = (anchor, hrefAttribute) => {
  1233.  
  1234. let currentHref = anchor.getAttribute('href');
  1235. if (currentHref === hrefAttribute) return true; // /channel/UCRmLncxsQFcOOC8OhzUIfxQ // /channel/UCRmLncxsQFcOOC8OhzUIfxQ
  1236.  
  1237. return (currentHref + '/').indexOf(hrefAttribute + '/') >= 0;
  1238.  
  1239. };
  1240.  
  1241. /**
  1242. *
  1243. * @param {string} currentDisplayed
  1244. * @param {DisplayNameResultObject} fetchResult
  1245. * @returns
  1246. */
  1247. const verifyAndConvertHandle = (currentDisplayed, fetchResult) => {
  1248.  
  1249. const { title, langTitle, externalId, ownerUrls, channelUrl, vanityChannelUrl, verified123 } = fetchResult;
  1250.  
  1251. const currentDisplayTrimmed = currentDisplayed.trim(); // @IORIMATSUNAGA
  1252. const currentDisplayTrimmedLowerCase = currentDisplayTrimmed.toLowerCase(); // @iorimatsunaga
  1253. let match = false;
  1254. if (verified123) {
  1255. match = true;
  1256. } else if ((vanityChannelUrl || '').toLowerCase().endsWith(`/${currentDisplayTrimmedLowerCase}`)) {
  1257. match = true;
  1258. } else if ((ownerUrls || 0).length >= 1) {
  1259. for (const ownerUrl of ownerUrls) {
  1260. if ((ownerUrl || '').toLowerCase().endsWith(`/${currentDisplayTrimmed}`)) {
  1261. match = true;
  1262. break;
  1263. }
  1264. }
  1265. }
  1266. if (match) {
  1267. return currentDisplayTrimmed;
  1268. }
  1269. return '';
  1270.  
  1271. };
  1272.  
  1273. /**
  1274. *
  1275. *
  1276.  
  1277. ### [Handle naming guidelines](https://support.google.com/youtube/answer/11585688?hl=en&co=GENIE.Platform%3DAndroid)
  1278.  
  1279. - Is between 3-30 characters
  1280. - Is made up of alphanumeric characters (A–Z, a–z, 0–9)
  1281. - Your handle can also include: underscores (_), hyphens (-), dots (.)
  1282. - Is not URL-like or phone number-like
  1283. - Is not already being used
  1284. - Follows YouTube's Community Guidelines
  1285.  
  1286. ### Handle automatically generated by YouTube
  1287. - `@user-XXXX`
  1288. - without dot (.)
  1289.  
  1290.  
  1291. ## Intl. Language Support
  1292. #### Since May~June 2024
  1293.  
  1294. - Before: Is made up of alphanumeric characters (A–Z, a–z, 0–9)
  1295.  
  1296. - After: Uses alphabet letters or numbers from one of our 75 supported languages
  1297. - Your handle can also include: underscores (_), hyphens (-), periods (.), Latin middle dots (·).
  1298. - Mixed scripts are only allowed in certain circumstances.
  1299. - Left-to-right scripts cannot be mixed with right-to-left scripts in a single handle except when numbers are added to the end of the handle.
  1300.  
  1301.  
  1302.  
  1303. *
  1304. * @param {string} text
  1305. * @returns
  1306. */
  1307. const isDisplayAsHandle = (text) => {
  1308. if (typeof text !== 'string') return false;
  1309. if (text.length < 4) return false;
  1310. if (text.indexOf('@') < 0) return false;
  1311. return exactHandleText(text.trim(), true);
  1312. };
  1313.  
  1314. const handleTextAllowSymbols = new Set('_-.·'.split('').map(e=>e.codePointAt()));
  1315. const isExactHandleText = (text, i = 0) => {
  1316. let cpCount = 0, cp = 0;
  1317. let cpMin = Number.MAX_VALUE
  1318. let cpMax = Number.MIN_VALUE
  1319. const suffixNumber = /\d+$/.exec(text);
  1320. const suffixNumberLen = `${suffixNumber || ''}`.length;
  1321. const textLen = text.length;
  1322. if (textLen - i > 30) return false;
  1323. let l = textLen - suffixNumberLen;
  1324. let maxCpCount = 30 - suffixNumberLen;
  1325. if(maxCpCount <= 0) return maxCpCount === 0;
  1326. let uchars = [];
  1327. for (; i < l; i++) {
  1328. cp = text.codePointAt(i);
  1329. if (!cp) break;
  1330. if (!handleTextAllowSymbols.has(cp)) {
  1331. uchars.push(cp);
  1332. if (cpMin > cp) cpMin = cp;
  1333. if (cpMax < cp) cpMax = cp;
  1334. }
  1335. if (cp < 0x7FFFFFFF) {
  1336. while (cp > 0xFFFF) {
  1337. cp = cp >> 16;
  1338. i++;
  1339. }
  1340. } else {
  1341. console.warn('cp >= 0x7FFFFFFF')
  1342. return false;
  1343. }
  1344. cpCount++;
  1345. if (cpCount > 30) return false;
  1346. }
  1347. const ft = String.fromCodePoint(...uchars);
  1348. if (cpMin >= 32 && cpMax <= 122) {
  1349. return /^[-_a-zA-Z0-9.]{3,30}$/.test(ft);
  1350. }
  1351. if (cpMin >= 0x4E00 && cpMax <= 0xD7AF) {
  1352. if (/^[\u4E00-\u9FFF\uAC00-\uD7AF]{1,10}$/.test(ft)) return "Han|Hangul";
  1353. } else if (cpMin >= 0x1200 && cpMax <= 0x30FF) {
  1354. if (/^[\u1200-\u137F\u3040-\u30FF]{2,20}$/.test(ft)) return "Ethiopic|Hiragana|Katakana";
  1355. }
  1356. return false;
  1357.  
  1358. }
  1359.  
  1360. const exactHandleText = (text, b) => {
  1361. return text.startsWith('@') && isExactHandleText(text, 1) ? (b ? true : text.substring(1)) : (b ? false : '');
  1362. }
  1363.  
  1364. const exactHandleUrl_ = (text, b) => {
  1365. return text.startsWith('/@') && isExactHandleText(text, 2) ? (b ? true : text.substring(2)) : (b ? false : '');
  1366. }
  1367.  
  1368. const exactHandleUrl = (text, b) => {
  1369. if(!text.startsWith('/@') ) return false;
  1370. let text2 = text.substring(2);
  1371. if(/%[0-9A-Fa-f]/.test(text2) && /^[a-zA-Z%0-9-_.·]+$/.test(text2)){
  1372. try{
  1373. text2 = decodeURI(text2);
  1374. }catch(e){}
  1375. text = `/@${text2}`
  1376. }
  1377. return exactHandleUrl_(text, b);
  1378. }
  1379.  
  1380. try {
  1381. // https://www.youtube.com/watch?v=hfopp1vLFmk
  1382. let b = true
  1383.  
  1384.  
  1385. b = b && isDisplayAsHandle('@7r0n215 ');
  1386. b = b && isDisplayAsHandle('@user-ox5by7bw1m ');
  1387. b = b && isDisplayAsHandle('@Samsungfan2016 ');
  1388. b = b && isDisplayAsHandle('@김태래 ');
  1389. b = b && !isDisplayAsHandle('@%EA%B9%80%ED%83%9C%EB%9E%98 ');
  1390.  
  1391. b = b && !exactHandleText('@7r0n215 ', true);
  1392. b = b && !exactHandleText('@user-ox5by7bw1m ', true);
  1393. b = b && !exactHandleText('@Samsungfan2016 ', true);
  1394. b = b && !exactHandleText('@김태래 ', true);
  1395. b = b && !exactHandleText('@%EA%B9%80%ED%83%9C%EB%9E%98 ', true);
  1396.  
  1397.  
  1398. b = b && !exactHandleUrl('/@7r0n215 ', true);
  1399. b = b && !exactHandleUrl('/@user-ox5by7bw1m ', true);
  1400. b = b && !exactHandleUrl('/@Samsungfan2016 ', true);
  1401. b = b && !exactHandleUrl('/@김태래 ', true);
  1402. b = b && !exactHandleUrl('/@%EA%B9%80%ED%83%9C%EB%9E%98 ', true);
  1403.  
  1404.  
  1405. b = b && isDisplayAsHandle(' @7r0n215');
  1406. b = b && isDisplayAsHandle(' @user-ox5by7bw1m');
  1407. b = b && isDisplayAsHandle(' @Samsungfan2016');
  1408. b = b && isDisplayAsHandle(' @김태래');
  1409. b = b && !isDisplayAsHandle(' @%EA%B9%80%ED%83%9C%EB%9E%98');
  1410.  
  1411. b = b && !exactHandleText(' @7r0n215', true);
  1412. b = b && !exactHandleText(' @user-ox5by7bw1m', true);
  1413. b = b && !exactHandleText(' @Samsungfan2016', true);
  1414. b = b && !exactHandleText(' @김태래', true);
  1415. b = b && !exactHandleText(' @%EA%B9%80%ED%83%9C%EB%9E%98', true);
  1416.  
  1417. b = b && !exactHandleUrl(' /@7r0n215', true);
  1418. b = b && !exactHandleUrl(' /@user-ox5by7bw1m', true);
  1419. b = b && !exactHandleUrl(' /@Samsungfan2016', true);
  1420. b = b && !exactHandleUrl(' /@김태래', true);
  1421. b = b && !exactHandleUrl(' /@%EA%B9%80%ED%83%9C%EB%9E%98', true);
  1422.  
  1423.  
  1424. b = b && isDisplayAsHandle('@7r0n215');
  1425. b = b && isDisplayAsHandle('@user-ox5by7bw1m');
  1426. b = b && isDisplayAsHandle('@Samsungfan2016');
  1427. b = b && isDisplayAsHandle('@김태래');
  1428. b = b && !isDisplayAsHandle('@%EA%B9%80%ED%83%9C%EB%9E%98');
  1429.  
  1430. b = b && exactHandleText('@7r0n215', true);
  1431. b = b && exactHandleText('@user-ox5by7bw1m', true);
  1432. b = b && exactHandleText('@Samsungfan2016', true);
  1433. b = b && exactHandleText('@김태래', true);
  1434. b = b && !exactHandleText('@%EA%B9%80%ED%83%9C%EB%9E%98', true);
  1435.  
  1436.  
  1437. b = b && exactHandleUrl('/@7r0n215', true);
  1438. b = b && exactHandleUrl('/@user-ox5by7bw1m', true);
  1439. b = b && exactHandleUrl('/@Samsungfan2016', true);
  1440. b = b && exactHandleUrl('/@김태래', true);
  1441. b = b && exactHandleUrl('/@%EA%B9%80%ED%83%9C%EB%9E%98', true);
  1442.  
  1443. b = b && isDisplayAsHandle('@Lions_joyfulpenda');
  1444. b = b && isDisplayAsHandle('@kowlsl12');
  1445. b = b && isDisplayAsHandle('@hyezzozzo_baseball_curtaincall');
  1446. b = b && isDisplayAsHandle('@blue-pink-love');
  1447.  
  1448.  
  1449.  
  1450. if (!b) console.error('!!!! wrong coding !!!!');
  1451. } catch (e) {
  1452. console.error('!!!! wrong coding !!!!', e);
  1453. }
  1454. /**
  1455. *
  1456. * @param {Object[]} contentTexts
  1457. * @param {number} idx
  1458. * @returns
  1459. */
  1460. const contentTextProcess = (_md, contentTexts, idx) => {
  1461. const md = _md;
  1462. const contentText = contentTexts[idx];
  1463. const text = (contentText || 0).text;
  1464. const browseId = ((contentText.navigationEndpoint || 0).browseEndpoint || 0).browseId || '';
  1465. const canonicalBaseUrl = ((contentText.navigationEndpoint || 0).browseEndpoint || 0).canonicalBaseUrl || ''; // TBC
  1466. const url = (((contentText.navigationEndpoint || 0).commandMetadata || 0).webCommandMetadata || 0).url; // TBC
  1467. if (typeof url === 'string' && typeof text === 'string' && typeof browseId === 'string' && browseId && isUCBrowserId(browseId)) {
  1468.  
  1469. if (!isDisplayAsHandle(text)) return null;
  1470. const channelId = browseId;
  1471.  
  1472. return getDisplayName(channelId).then(fetchResult => {
  1473. let resolveResult = null;
  1474. if (fetchResult) {
  1475. // note: if that user shown is not found in `a[id]`, the hyperlink would not change
  1476.  
  1477. const textTrimmed = verifyAndConvertHandle(text, fetchResult);
  1478. if (textTrimmed) {
  1479. resolveResult = (() => {
  1480. let runs = ((md || 0).contentText || 0).runs;
  1481. if (!runs || !runs[idx]) return;
  1482. if (runs[idx].text !== text) return;
  1483. runs[idx].text = text.replace(textTrimmed, `@${fetchResult.title}`); // HyperLink always @SomeOne
  1484. md.contentText = Object.assign({}, md.contentText);
  1485. })();
  1486. }
  1487. }
  1488. return resolveResult; // function as a Promise
  1489. });
  1490. }
  1491.  
  1492. return null;
  1493. };
  1494.  
  1495. const dynamicEditableTextByControllerResolve = isMobile ? 0 : (() => {
  1496. /**
  1497. *
  1498. *
  1499. f.showReplyDialog = function(a) {
  1500. if (a) {
  1501. var b = this.replyBox;
  1502. b || (b = document.createElement("ytd-comment-reply-dialog-renderer"),
  1503. b.id = "replybox",
  1504. qF(this.replyDialogDiv).appendChild(b));
  1505. b.data = a;
  1506. this.replyDialogDiv.hidden = !1;
  1507. b.openDialog()
  1508. }
  1509. }
  1510.  
  1511. // a = <ytd-comment-engagement-bar>...</ytd-comment-engagement-bar>
  1512. // b = "showReplyDialog"
  1513. // c = undefined
  1514. function Lpb(a, b, c) {
  1515. var d = a.polymerController;
  1516. a[b] = function() {
  1517. var e = va.apply(0, arguments);
  1518. a.loggingStatus.currentExternalCall = b;
  1519. a.loggingStatus.bypassProxyController = !0;
  1520. var g, k = ((g = a.is) != null ? g : a.tagName).toLowerCase();
  1521. ZE(k, b, "PROPERTY_ACCESS_CALL_EXTERNAL");
  1522. var m;
  1523. g = (m = c != null ? c : d[b]) == null ? void 0 : m.call.apply(m, [d].concat(ha(e)));
  1524. a.loggingStatus.currentExternalCall = void 0;
  1525. a.loggingStatus.bypassProxyController = !1;
  1526. return g
  1527. }
  1528. }
  1529.  
  1530. // a = {..., commandMetadata, createCommentReplyDialogEndpoint}
  1531. // b = {commandController: {onSuccess: ƒ, onServerError: ƒ} , forceClickLogging : true, form: {element, event} }
  1532. kQb.prototype.resolveCommand = function(a, b) {
  1533. var c, d, e, g, k, m, p, q;
  1534. return t(function(r) {
  1535. e = (c = G(a, n6a)) == null ? void 0 : (d = c.dialog) == null ? void 0 : d.commentReplyDialogRenderer;
  1536. if (!e)
  1537. throw new Em("No dialog in createCommentReplyDialogEndpoint");
  1538. k = (g = b.form) == null ? void 0 : g.event;
  1539. if (!k)
  1540. throw new Em("Event not passed in when resolving command");
  1541. m = h(k.composedPath());
  1542. for (p = m.next(); !p.done; p = m.next())
  1543. if (q = p.value,
  1544. q.tagName === "YTD-COMMENT-ACTION-BUTTONS-RENDERER" || q.tagName === "YTD-COMMENT-ENGAGEMENT-BAR")
  1545. return q.showReplyDialog(e),
  1546. r.return();
  1547. ma(r)
  1548. })
  1549. }
  1550.  
  1551.  
  1552. *
  1553. */
  1554.  
  1555.  
  1556. const setupForCommentReplyDialogRenderer = (cProto) => {
  1557. if (cProto && typeof cProto.showReplyDialog === 'function' && cProto.showReplyDialog.length === 1 && !cProto.showReplyDialog392) {
  1558. cProto.showReplyDialog392 = cProto.showReplyDialog;
  1559. cProto.showReplyDialog = function (a) {
  1560. let goDefault = true;
  1561. try {
  1562. if (a && a.editableText) {
  1563. const arr = [];
  1564. fixCommentReplyDialogRenderer(a, arr);
  1565. const pAll = Promise.all(arr).catch(e => console.warn);
  1566. Promise.race([pAll, delayPn(300)]).then(() => { // network request should be already done before
  1567. this.showReplyDialog392(a);
  1568. });
  1569. goDefault = false;
  1570. }
  1571. } catch (e) { }
  1572. if (goDefault) {
  1573. return this.showReplyDialog392(a);
  1574. }
  1575. };
  1576. }
  1577. };
  1578.  
  1579. retrieveCE('ytd-comment-action-buttons-renderer').then(cProto => {
  1580. setupForCommentReplyDialogRenderer(cProto);
  1581. });
  1582. retrieveCE('ytd-comment-engagement-bar').then(cProto => {
  1583. setupForCommentReplyDialogRenderer(cProto);
  1584. });
  1585.  
  1586. return 1;
  1587.  
  1588. })();
  1589.  
  1590. const editableTextProcess = (_commentReplyDialogRenderer, editableTexts, idx) => {
  1591. const commentReplyDialogRenderer = _commentReplyDialogRenderer;
  1592. const editableText = editableTexts[idx];
  1593. const text = (editableText || 0).text;
  1594.  
  1595. const browseId = ((editableText.navigationEndpoint || 0).browseEndpoint || 0).browseId || '';
  1596. const canonicalBaseUrl = ((editableText.navigationEndpoint || 0).browseEndpoint || 0).canonicalBaseUrl || ''; // TBC
  1597. const url = (((editableText.navigationEndpoint || 0).commandMetadata || 0).webCommandMetadata || 0).url; // TBC
  1598. if (typeof url === 'string' && typeof text === 'string' && typeof browseId === 'string' && browseId && isUCBrowserId(browseId)) {
  1599.  
  1600. if (!isDisplayAsHandle(text)) return null;
  1601. const channelId = browseId;
  1602.  
  1603. return getDisplayName(channelId).then(fetchResult => {
  1604. let resolveResult = null;
  1605. if (fetchResult) {
  1606. // note: if that user shown is not found in `a[id]`, the hyperlink would not change
  1607.  
  1608. const textTrimmed = verifyAndConvertHandle(text, fetchResult);
  1609. if (textTrimmed) {
  1610. resolveResult = (() => {
  1611. if (!commentReplyDialogRenderer) return;
  1612. const editableText = commentReplyDialogRenderer.editableText || 0;
  1613. let runs = (editableText || 0).runs;
  1614. if (!runs || !runs[idx]) return;
  1615. if (runs[idx].text !== text) return;
  1616. runs[idx].text = text.replace(textTrimmed, `@${fetchResult.title}`); // HyperLink always @SomeOne
  1617. commentReplyDialogRenderer.editableText = Object.assign({}, commentReplyDialogRenderer.editableText);
  1618. })();
  1619. }
  1620. }
  1621. return resolveResult; // function as a Promise
  1622. });
  1623. }
  1624.  
  1625. return null;
  1626. };
  1627. const contentTextProcessViewModel = async (content, idx) => {
  1628. const { commandRuns, content: text, styleRuns } = content;
  1629. if (typeof text !== 'string') return;
  1630. const names = [];
  1631.  
  1632. function replaceNamePN(o) {
  1633. return getDisplayName(o.channelId).then(fetchResult => {
  1634. let resolveResult = null;
  1635. if (fetchResult) {
  1636. const textTrimmed = verifyAndConvertHandle(o.handle, fetchResult);
  1637. if (textTrimmed) {
  1638. o.display = `@${fetchResult.title}`;
  1639. o.newText = o.oldText.replace(o.handle, o.display);
  1640. }
  1641. }
  1642. return resolveResult; // function as a Promise
  1643. });
  1644. }
  1645. const promises = [];
  1646. for (const commandRun of commandRuns) {
  1647. let browseEndpoint = null;
  1648. if (commandRun.length >= 1 && commandRun.startIndex >= 0 && (browseEndpoint = ((commandRun.onTap || 0).innertubeCommand || 0).browseEndpoint)) {
  1649. let substr = text.substring(commandRun.startIndex, commandRun.startIndex + commandRun.length);
  1650. const substrTrimmed = substr.trim();
  1651. const valBrowseEndpointBaseUrlType = browseEndpointBaseUrlType(browseEndpoint, substrTrimmed);
  1652. if (valBrowseEndpointBaseUrlType > 1) {
  1653. const o = {
  1654. startIndex: commandRun.startIndex,
  1655. endIndex: commandRun.startIndex + commandRun.length,
  1656. handle: substrTrimmed,
  1657. oldText: substr,
  1658. newText: null,
  1659. channelId: browseEndpoint.browseId,
  1660. nStartIndex: null,
  1661. nEndIndex: null,
  1662. nText: null
  1663. }
  1664. names.push(o);
  1665. promises.push(replaceNamePN(o));
  1666. }
  1667. }
  1668. }
  1669. await Promise.all(promises);
  1670.  
  1671. let offset = 0;
  1672. for (const o of names) {
  1673. o.nStartIndex = o.startIndex + offset;
  1674. o.nText = o.newText === null ? o.oldText : o.newText;
  1675. o.nEndIndex = o.nStartIndex + offset + o.nText.length;
  1676. offset += o.nText.length - o.oldText.length;
  1677. }
  1678.  
  1679. let w1 = 0, w2 = 0;
  1680. let s = [];
  1681. let j = 0;
  1682. for (let i = 0; i < text.length;) {
  1683. while (w1 < commandRuns.length && commandRuns[w1].startIndex < i) w1++;
  1684. while (w2 < styleRuns.length && styleRuns[w2].startIndex < i) w2++;
  1685.  
  1686. if (j < names.length && i == names[j].startIndex) {
  1687.  
  1688. if (w1 < commandRuns.length && commandRuns[w1].startIndex === names[j].startIndex) {
  1689. const run = commandRuns[w1];
  1690. const o = names[j];
  1691. if (run.length === o.endIndex - o.startIndex) {
  1692. run.startIndex = o.nStartIndex;
  1693. run.length = o.nText.length;
  1694. }
  1695. w1++;
  1696. }
  1697.  
  1698. if (w2 < styleRuns.length && styleRuns[w2].startIndex === names[j].startIndex) {
  1699. const run = styleRuns[w2];
  1700. const o = names[j];
  1701. if (run.length === o.endIndex - o.startIndex) {
  1702. run.startIndex = o.nStartIndex;
  1703. run.length = o.nText.length;
  1704. }
  1705. w2++;
  1706. }
  1707.  
  1708.  
  1709. // s.push(text.substring(names[j].startIndex, names[j].endIndex));
  1710. s.push(names[j].nText);
  1711. i = names[j].endIndex;
  1712. j++;
  1713. } else {
  1714. if (j >= names.length) {
  1715. s.push(text.substring(i));
  1716. i = text.length;
  1717. } else {
  1718. if (j < names.length && names[j].startIndex > i + 1) {
  1719. s.push(text.substring(i, names[j].startIndex));
  1720. i = names[j].startIndex;
  1721. } else {
  1722. s.push(text.charAt(i));
  1723. i++;
  1724. }
  1725. }
  1726. }
  1727. }
  1728.  
  1729. content.content = s.join('');
  1730. s.length = 0;
  1731.  
  1732.  
  1733. };
  1734.  
  1735. /**
  1736. *
  1737. * @param {ParentNode & Element} parentNode
  1738. * @param {Function} callback
  1739. */
  1740. function findTextNodes(parentNode, callback) {
  1741. /**
  1742. *
  1743. * @param {Node} node
  1744. * @returns
  1745. */
  1746. function traverse(node) {
  1747. if (node.nodeType === Node.TEXT_NODE) {
  1748. let r = callback(node);
  1749. if (r === true) return true;
  1750. } else if (node.nodeType === Node.ELEMENT_NODE) {
  1751. for (let childNode = nodeFirstChild(node); childNode; childNode = nodeNextSibling(childNode)) {
  1752. const r = traverse(childNode);
  1753. if (r === true) return true;
  1754. }
  1755. }
  1756. }
  1757.  
  1758. traverse(parentNode);
  1759. }
  1760.  
  1761. function updatePinnedCommentBadge(md, title, langTitle) {
  1762. const titleForDisplay = langTitle || title;
  1763. if (typeof titleForDisplay !== 'string') return;
  1764. if (title === titleForDisplay) return;
  1765. const runs = ((((md.pinnedCommentBadge || 0).pinnedCommentBadgeRenderer || 0).label || 0).runs || 0); // can be 0
  1766. if (runs.length === 2) {
  1767. let idx = 0;
  1768. let jdx = -1;
  1769. for (const textRun of runs) {
  1770. if (textRun.text === title) {
  1771. jdx = idx;
  1772. break;
  1773. }
  1774. idx++;
  1775. }
  1776. if (jdx >= 0) {
  1777. const runs = md.pinnedCommentBadge.pinnedCommentBadgeRenderer.label.runs.slice(0);
  1778. runs[jdx] = Object.assign({}, runs[jdx], { text: titleForDisplay });
  1779. md.pinnedCommentBadge = Object.assign({}, md.pinnedCommentBadge);
  1780. md.pinnedCommentBadge.pinnedCommentBadgeRenderer = Object.assign({}, md.pinnedCommentBadge.pinnedCommentBadgeRenderer);
  1781. md.pinnedCommentBadge.pinnedCommentBadgeRenderer.label = Object.assign({}, md.pinnedCommentBadge.pinnedCommentBadgeRenderer.label, { runs });
  1782. }
  1783. }
  1784. }
  1785.  
  1786. /**
  1787. *
  1788. * @param {ParentNode} parentNode
  1789. * @param {Object[]} contentTexts
  1790. * @param {number} aidx
  1791. * @returns
  1792. */
  1793. const mobileContentHandleAsync = async (parentNode, contentTexts, aidx) => {
  1794.  
  1795.  
  1796. let commentText = elementQS(parentNode, '.comment-text');
  1797. if (!commentText) return;
  1798. commentText = elementQS(commentText, '.yt-core-attributed-string') || commentText;
  1799. let currentText = commentText.textContent;
  1800. let textMatch = commentText.textContent === contentTexts.map(e => e.text).join('');
  1801. if (!textMatch) return;
  1802.  
  1803. const contentText = contentTexts[aidx];
  1804.  
  1805. /** @type {string} */
  1806. const handleText = contentText.text.trim();
  1807. /** @type {string | null | undefined} */
  1808. let channelId;
  1809.  
  1810. let cache = handleToChannelId.get(handleText);
  1811.  
  1812. if (typeof cache === 'string') {
  1813. channelId = cache;
  1814. } else if (cache instanceof AsyncValue) {
  1815. channelId = await cache.getValue();
  1816. } else {
  1817. /** @type {AsyncValue<string>} */
  1818. const asyncValue = new AsyncValue();
  1819. handleToChannelId.set(handleText, asyncValue);
  1820. channelId = await asyncValue.getValue(); // note: it could be never resolved
  1821. }
  1822.  
  1823. if (!channelId) return; // just in case
  1824.  
  1825. if (commentText.isConnected !== true) return; // already removed
  1826. if (commentText.textContent !== currentText) return; // already changed
  1827.  
  1828. const fetchResult = await getDisplayName(channelId);
  1829.  
  1830. if (fetchResult === null) return;
  1831.  
  1832. const { title, langTitle, externalId } = fetchResult;
  1833. const titleForDisplay = langTitle || title;
  1834. if (externalId !== channelId) return; // channel id must be the same
  1835.  
  1836. if (commentText.isConnected !== true) return; // already removed
  1837. if (commentText.textContent !== currentText) return; // already changed
  1838.  
  1839. let search = contentText.text;
  1840. if (typeof search === 'string') {
  1841.  
  1842. const searchTrimmed = search.trim();
  1843. if (searchTrimmed === handleText) { // ensure integrity after getDisplayName
  1844. contentText.text = search.replace(searchTrimmed, "@" + titleForDisplay); // mention
  1845.  
  1846. findTextNodes(commentText, (textnode) => {
  1847. if (textnode.nodeValue.indexOf(search) >= 0) {
  1848. textnode.nodeValue = textnode.nodeValue.replace(search, contentText.text);
  1849. return true;
  1850. }
  1851. });
  1852. }
  1853.  
  1854. }
  1855.  
  1856. }
  1857.  
  1858. /**
  1859. * Process Checking when there is new (unprocessed) anchor DOM element with hyperlink of channel
  1860. * @type { (anchor: HTMLElement, anchorHref: string?, mt: string?) => Promise<void> }
  1861. */
  1862.  
  1863. const getDisplayTextDOMForCommentRenderer = (root) => { // mobile
  1864. let displayTextDOM = elementQS(root, '.comment-header .comment-title');
  1865. if (displayTextDOM) {
  1866. displayTextDOM = elementQS(displayTextDOM, '.yt-core-attributed-string') || displayTextDOM;
  1867. }
  1868. return displayTextDOM;
  1869. }
  1870.  
  1871. const ytPathnameExtract = (href) => {
  1872.  
  1873. if (!href || typeof href !== 'string') return '';
  1874. let i = 0;
  1875. if (href.startsWith('/')) {
  1876.  
  1877. } else if ((i = href.indexOf('youtube.com/')) >= 0) {
  1878. href = href.substring(i + 11);
  1879. } else {
  1880. return '';
  1881. }
  1882. return href;
  1883.  
  1884. }
  1885.  
  1886. const getHrefType = (href) => {
  1887. if (href.length === 33 && href.startsWith('/channel/') && /^\/channel\/UC[-_a-zA-Z0-9+=.]{22}$/.test(href)) {
  1888. return 1;
  1889. } else if (href.length >= 5 && href.length <= 32 && exactHandleUrl(href, true)) {
  1890. return 2;
  1891. }
  1892. return 0;
  1893. }
  1894.  
  1895. const getYTextType = (text) => {
  1896. // if (typeof text === 'string') return 1;
  1897. if (typeof text === 'object' && text) {
  1898. if (typeof text.simpleText === 'string') return 2;
  1899. const runs = text.runs;
  1900. if (runs && runs.length === 1) {
  1901. let r = runs[0];
  1902. return typeof r === 'string' ? 3 : typeof (r || 0).text === 'string' ? 4 : null;
  1903. }
  1904. }
  1905. return null;
  1906. }
  1907.  
  1908. const getYTextContent = (text) => {
  1909. // if (typeof text === 'string') return text;
  1910. if (typeof text === 'object' && text) {
  1911. const simpleText = text.simpleText;
  1912. if (typeof simpleText === 'string') return simpleText;
  1913. const runs = text.runs;
  1914. if (runs && runs.length === 1) {
  1915. let r = runs[0];
  1916. return typeof r === 'string' ? r : typeof (r || 0).text === 'string' ? r.text : null;
  1917. }
  1918. }
  1919. return null;
  1920. }
  1921.  
  1922. const setYTextContent = (text, fn) => {
  1923. // if (typeof text === 'string') {
  1924. // res=fn(text);
  1925. // }
  1926. if (typeof text === 'object' && text) {
  1927. const simpleText = text.simpleText;
  1928. if (typeof simpleText === 'string') {
  1929. return Object.assign({}, text, { simpleText: fn(simpleText) });
  1930. }
  1931. const runs = text.runs;
  1932. if (runs && runs.length === 1) {
  1933. let r = runs[0];
  1934. return typeof r === 'string' ? Object.assign({}, text, { runs: [fn(r)] }) : typeof (r || 0).text === 'string' ? Object.assign({}, text, { runs: [Object.assign({}, r, { text: fn(r.text) })] }) : null;
  1935. }
  1936. }
  1937. return null;
  1938. }
  1939.  
  1940. const fixCommentReplyDialogRenderer = (commentReplyDialogRenderer, funcPromises) => {
  1941. if (!commentReplyDialogRenderer) return;
  1942. const editableText = commentReplyDialogRenderer.editableText;
  1943. if (!editableText) return;
  1944. const editableTexts = editableText.runs;
  1945. if (editableTexts && editableTexts.length >= 1) {
  1946. for (let aidx = 0; aidx < editableTexts.length; aidx++) {
  1947. const r = editableTextProcess(commentReplyDialogRenderer, editableTexts, aidx);
  1948. if (r instanceof Promise) funcPromises.push(r);
  1949. }
  1950. }
  1951. // $0.polymerController.$['action-buttons'].polymerController.__data.replyButtonRenderer.navigationEndpoint.createCommentReplyDialogEndpoint.dialog.commentReplyDialogRenderer.editableText
  1952. }
  1953.  
  1954. const domCheckAsync = isMobile ? async (anchor, hrefAttribute, mt) => {
  1955.  
  1956. let sHandleName = null;
  1957. let sNodeValue = null;
  1958. try {
  1959. if (!hrefAttribute || !mt) return;
  1960. let parentNode = nodeParent(anchor);
  1961. while (parentNode instanceof Node) {
  1962. if (parentNode.nodeName === 'YTM-COMMENT-RENDERER' || ('_commentData' in parentNode)) break;
  1963. parentNode = nodeParent(parentNode);
  1964. }
  1965. if (parentNode instanceof Node) { } else return;
  1966.  
  1967. const commentRendererAuthorTextDOM = getDisplayTextDOMForCommentRenderer(parentNode);
  1968. const anchorAiraLabel = anchor.getAttribute('aria-label');
  1969. const anchorHrefValue = ytPathnameExtract(anchor.getAttribute('href')); // guess (Feb 2024)
  1970.  
  1971. const cnt = insp(parentNode);
  1972. const parentNodeData = cnt._commentData || cnt.data || 0;
  1973. const authorTextData = (parentNodeData.authorText || 0);
  1974.  
  1975. let tHandleName = null;
  1976.  
  1977. if (commentRendererAuthorTextDOM && authorTextData) {
  1978. const hType = getHrefType(anchorHrefValue);
  1979. let ytext = null;
  1980. if (hType === 2) {
  1981. tHandleName = anchorHrefValue.substring(1); // guess
  1982. } else if (hType === 1) {
  1983. ytext = getYTextContent(authorTextData)
  1984. tHandleName = ytext; // authorTextData.runs[0].text
  1985. }
  1986. if (typeof tHandleName === 'string' && commentRendererAuthorTextDOM.textContent.trim() === tHandleName && isDisplayAsHandle(tHandleName)) {
  1987. ytext = ytext || getYTextContent(authorTextData)
  1988. if (ytext === tHandleName) {
  1989. sHandleName = tHandleName;
  1990. sNodeValue = tHandleName;
  1991. }
  1992. }
  1993. }
  1994.  
  1995. if (typeof sHandleName === 'string' && typeof sNodeValue === 'string') {
  1996.  
  1997. if (!channelIdToHandle.has(mt)) {
  1998. channelIdToHandle.set(mt, {
  1999. handleText: sHandleName,
  2000. justPossible: true
  2001. });
  2002. }
  2003.  
  2004. const fetchResult = await getDisplayName(mt);
  2005.  
  2006. if (fetchResult === null) return;
  2007.  
  2008. const { title, langTitle, externalId } = fetchResult;
  2009. const titleForDisplay = langTitle || title;
  2010. if (externalId !== mt) return; // channel id must be the same
  2011.  
  2012. // anchor href might be changed by external
  2013. if (!anchorIntegrityCheck(anchor, hrefAttribute)) return;
  2014.  
  2015. if (commentRendererAuthorTextDOM.isConnected !== true) return; // already removed
  2016.  
  2017. let found = false;
  2018.  
  2019. findTextNodes(commentRendererAuthorTextDOM, (textnode) => {
  2020. if (textnode.nodeValue === sNodeValue) {
  2021. textnode.nodeValue = titleForDisplay;
  2022. found = true;
  2023. return true;
  2024. }
  2025. });
  2026.  
  2027. if (!found) return;
  2028. if (anchorAiraLabel && anchorAiraLabel.trim() === sHandleName) anchor.setAttribute('aria-label', anchorAiraLabel.replace(sHandleName, titleForDisplay));
  2029. const authorTextDataNew = setYTextContent(authorTextData, (t) => t.replace(t.trim(), titleForDisplay));
  2030. if (authorTextDataNew && parentNodeData.authorText === authorTextData) {
  2031. parentNodeData.authorText = authorTextDataNew;
  2032. }
  2033.  
  2034. if (UPDATE_PIN_NAME && title && langTitle && langTitle !== title) {
  2035. const renderer = HTMLElement.prototype.closest.call(anchor, 'ytm-comment-renderer');
  2036. const pinned = !renderer ? null : HTMLElement.prototype.querySelector.call(renderer, 'ytm-pinned-comment-badge-renderer');
  2037. const spanText = !pinned ? null : HTMLElement.prototype.querySelector.call(pinned, 'span.yt-core-attributed-string[role="text"]');
  2038. const tc = spanText ? spanText.textContent : '';
  2039. updatePinnedCommentBadge(parentNodeData, title, langTitle);
  2040. let idx;
  2041. if (tc && tc === spanText.textContent && (idx = tc.indexOf(title)) >= 0) {
  2042. spanText.textContent = tc.substring(0, idx) + langTitle + tc.substring(idx + title.length);
  2043. }
  2044. }
  2045.  
  2046. const contentTexts = (parentNodeData.contentText || 0).runs;
  2047. if (contentTexts && contentTexts.length >= 2) {
  2048. for (let aidx = 0; aidx < contentTexts.length; aidx++) {
  2049. const contentText = contentTexts[aidx] || 0;
  2050. if (contentText.italics !== true && isDisplayAsHandle(contentText.text)) {
  2051. mobileContentHandleAsync(parentNode, contentTexts, aidx);
  2052. }
  2053. }
  2054. }
  2055.  
  2056. }
  2057.  
  2058. } catch (e) {
  2059. console.warn(e);
  2060. }
  2061.  
  2062. } : async (anchor, hrefAttribute, mt) => {
  2063.  
  2064. try {
  2065. if (!hrefAttribute || !mt) return;
  2066. let parentNode = nodeParent(anchor);
  2067. let isCommentViewModel = 0;
  2068. while (parentNode instanceof Node) {
  2069. const cnt = insp(parentNode);
  2070. if (typeof cnt.is === 'string') {
  2071. if (cnt.commentEntity && !cnt._propertiesChanged) { isCommentViewModel = 2; break; } // 2024.04.30: ytd-comment-view-model (v3)
  2072. if (cnt.commentEntity && typeof cnt._propertiesChanged === 'function') { isCommentViewModel = 1; break; } // Feb 2024: ytd-comment-view-model (v1 & v2)
  2073. else if (typeof cnt.dataChanged === 'function') break;
  2074. }
  2075. parentNode = nodeParent(parentNode);
  2076. }
  2077. if (parentNode instanceof Node) { } else return;
  2078. const parentNodeController = insp(parentNode);
  2079.  
  2080. if (isCommentViewModel > 0) {
  2081.  
  2082. const commentEntity = parentNodeController.commentEntity || 0;
  2083. const commentEntityAuthor = commentEntity.author || 0;
  2084. const currentDisplayed = commentEntityAuthor.displayName || 0;
  2085. if (typeof currentDisplayed !== 'string') return;
  2086. if (!isDisplayAsHandle(currentDisplayed)) return;
  2087. if (!channelIdToHandle.has(mt)) {
  2088. channelIdToHandle.set(mt, {
  2089. handleText: currentDisplayed.trim(),
  2090. justPossible: true
  2091. });
  2092. }
  2093.  
  2094. // not working for CommentViewModel v3
  2095. if (typeof parentNodeController._propertiesChanged === 'function' && !parentNodeController._propertiesChanged159) {
  2096. const cntp = parentNodeController.constructor.prototype;
  2097. const c = cntp._propertiesChanged === parentNodeController._propertiesChanged ? cntp : parentNodeController;
  2098. c._propertiesChanged159 = c._propertiesChanged;
  2099. c._propertiesChanged = function () {
  2100. resetWhenPropChanged(this);
  2101. return this._propertiesChanged159.apply(this, arguments);
  2102. };
  2103. }
  2104.  
  2105. const fetchResult = await getDisplayName(mt);
  2106.  
  2107. if (fetchResult === null) return;
  2108.  
  2109. const { title, langTitle, externalId } = fetchResult;
  2110. const titleForDisplay = langTitle || title;
  2111. if (externalId !== mt) return; // channel id must be the same
  2112.  
  2113. // anchor href might be changed by external
  2114. if (!anchorIntegrityCheck(anchor, hrefAttribute)) return;
  2115.  
  2116. if (commentEntityAuthor.displayName === currentDisplayed && commentEntityAuthor.channelId === mt) {
  2117. commentEntityAuthor.displayName = titleForDisplay;
  2118.  
  2119. if (commentEntity.properties) {
  2120. if (commentEntity.properties.authorButtonA11y === currentDisplayed) {
  2121. commentEntity.properties.authorButtonA11y = titleForDisplay;
  2122. }
  2123.  
  2124. // the inside hyperlinks will be only converted if its parent author name is handle
  2125. const content = commentEntity.properties.content;
  2126. if (content && (content.commandRuns || 0).length >= 1) {
  2127. try {
  2128. await contentTextProcessViewModel(content);
  2129.  
  2130. // $0.polymerController.commentEntity = Object.assign({}, $0.polymerController.commentEntity , { properties: Object.assign({}, $0.polymerController.commentEntity.properties, { content: Object.assign({},$0.polymerController.commentEntity.properties.content) } )})
  2131.  
  2132. // parentNodeController.commentEntity.properties = Object.assign({}, parentNodeController.commentEntity.properties, {
  2133. // content: Object.assign({}, parentNodeController.commentEntity.properties.content)
  2134. // });
  2135.  
  2136. commentEntity.properties.content = Object.assign({}, content);
  2137. } catch (e) {
  2138. console.log(e);
  2139. }
  2140.  
  2141. }
  2142.  
  2143. }
  2144.  
  2145. if (commentEntityAuthor && parentNodeController.isAttached === true && parentNode.isConnected === true) {
  2146. commentEntityAuthor.jkrgx = 1;
  2147. addCSSRulesIfRequired();
  2148. }
  2149.  
  2150. let commentReplyDialogRenderer;
  2151.  
  2152. const actionButtonsCnt = insp((parentNodeController.$ || 0)['action-buttons'] || 0);
  2153. if (actionButtonsCnt) {
  2154.  
  2155. try {
  2156. commentReplyDialogRenderer = actionButtonsCnt.__data.replyButtonRenderer.navigationEndpoint.createCommentReplyDialogEndpoint.dialog.commentReplyDialogRenderer;
  2157. } catch (e) { }
  2158. }
  2159. const funcPromises = [];
  2160. commentReplyDialogRenderer && fixCommentReplyDialogRenderer(commentReplyDialogRenderer, funcPromises);
  2161.  
  2162.  
  2163. if (funcPromises.length >= 1) {
  2164. await Promise.all(funcPromises);
  2165. }
  2166.  
  2167. parentNodeController.commentEntity = Object.assign({}, parentNodeController.commentEntity);
  2168.  
  2169. }
  2170. } else {
  2171.  
  2172. const authorText = (parentNodeController.data || 0).authorText;
  2173. const currentDisplayed = getYTextContent(authorText || 0); // authorText.simpleText
  2174. if (typeof currentDisplayed !== 'string') return;
  2175. if (!isDisplayAsHandle(currentDisplayed)) return;
  2176.  
  2177. if (!channelIdToHandle.has(mt)) {
  2178. channelIdToHandle.set(mt, {
  2179. handleText: currentDisplayed.trim(),
  2180. justPossible: true
  2181. });
  2182. }
  2183.  
  2184. if (typeof parentNodeController.dataChanged === 'function' && !parentNodeController.dataChanged159) {
  2185. const cntp = parentNodeController.constructor.prototype;
  2186. const c = cntp.dataChanged === parentNodeController.dataChanged ? cntp : parentNodeController;
  2187. c.dataChanged159 = c.dataChanged;
  2188. c.dataChanged = function () {
  2189. resetWhenDataChanged(this);
  2190. return this.dataChanged159.apply(this, arguments);
  2191. };
  2192. }
  2193.  
  2194. const fetchResult = await getDisplayName(mt);
  2195.  
  2196. if (fetchResult === null) return;
  2197.  
  2198. const { title, langTitle, externalId } = fetchResult;
  2199. const titleForDisplay = langTitle || title;
  2200. if (externalId !== mt) return; // channel id must be the same
  2201.  
  2202. // anchor href might be changed by external
  2203. if (!anchorIntegrityCheck(anchor, hrefAttribute)) return;
  2204.  
  2205. const parentNodeData = parentNodeController.data;
  2206. const funcPromises = [];
  2207.  
  2208. if (parentNodeController.isAttached === true && parentNode.isConnected === true && typeof parentNodeData === 'object' && parentNodeData && parentNodeData.authorText === authorText) {
  2209.  
  2210. if (getYTextContent(authorText) !== currentDisplayed) return;
  2211. const currentDisplayTrimmed = verifyAndConvertHandle(currentDisplayed, fetchResult);
  2212. const cSimpleText = (getYTextContent(parentNodeData.authorText || 0) || '');
  2213. if (currentDisplayTrimmed && currentDisplayed !== titleForDisplay && cSimpleText === currentDisplayed) {
  2214.  
  2215. const md = Object.assign({}, parentNodeData);
  2216.  
  2217. // the inside hyperlinks will be only converted if its parent author name is handle
  2218. const contentTexts = (md.contentText || 0).runs;
  2219. if (contentTexts && contentTexts.length >= 1) {
  2220. for (let aidx = 0; aidx < contentTexts.length; aidx++) {
  2221. const r = contentTextProcess(md, contentTexts, aidx);
  2222. if (r instanceof Promise) funcPromises.push(r);
  2223. }
  2224. }
  2225.  
  2226. const authorCommentBadgeAuthorText = (((md.authorCommentBadge || 0).authorCommentBadgeRenderer || 0).authorText || 0);
  2227.  
  2228. const authorCommentBadgeAuthorTextNew = authorCommentBadgeAuthorText ? setYTextContent(authorCommentBadgeAuthorText, (currentText) => {
  2229. if ((currentText || '').trim() === cSimpleText.trim()) {
  2230. return (currentText || '').replace((currentText || '').trim(), titleForDisplay);
  2231. } else {
  2232. return currentText;
  2233. }
  2234. }) : null;
  2235.  
  2236. // parentNode.data = Object.assign({}, { jkrgx: 1 });
  2237. const authorTextNew = setYTextContent(md.authorText, (t) => t.replace(currentDisplayTrimmed, titleForDisplay))
  2238. md.authorText = authorTextNew;
  2239.  
  2240. if (authorCommentBadgeAuthorTextNew) {
  2241. const authorCommentBadgeRendererNew = Object.assign({}, md.authorCommentBadge.authorCommentBadgeRenderer, { authorText: authorCommentBadgeAuthorTextNew });
  2242. md.authorCommentBadge = Object.assign({}, md.authorCommentBadge, { authorCommentBadgeRenderer: authorCommentBadgeRendererNew });
  2243. }
  2244.  
  2245. let setReplyEditableText = false;
  2246. if (((((((((parentNodeData.actionButtons || 0).commentActionButtonsRenderer || 0).replyButton || 0).buttonRenderer || 0).navigationEndpoint || 0).createCommentReplyDialogEndpoint || 0).dialog || 0).commentReplyDialogRenderer || 0).editableText) {
  2247. setReplyEditableText = true;
  2248. }
  2249. if (setReplyEditableText) {
  2250. const commentReplyDialogRenderer = parentNodeData.actionButtons.commentActionButtonsRenderer.replyButton.buttonRenderer.navigationEndpoint.createCommentReplyDialogEndpoint.dialog.commentReplyDialogRenderer;
  2251. fixCommentReplyDialogRenderer(commentReplyDialogRenderer, funcPromises);
  2252.  
  2253. }
  2254.  
  2255. if (UPDATE_PIN_NAME) {
  2256. updatePinnedCommentBadge(md, title, langTitle);
  2257. }
  2258.  
  2259. if (funcPromises.length >= 1) {
  2260. await Promise.all(funcPromises);
  2261. }
  2262.  
  2263. if (parentNodeController.isAttached === true && parentNode.isConnected === true) {
  2264. parentNodeController.data = Object.assign({}, md, { jkrgx: 1 });
  2265. addCSSRulesIfRequired();
  2266. }
  2267.  
  2268.  
  2269. }
  2270.  
  2271. }
  2272. }
  2273. } catch (e) {
  2274. console.warn(e);
  2275. }
  2276.  
  2277. }
  2278.  
  2279. /**
  2280. *
  2281. * @param {string} url
  2282. * @returns
  2283. */
  2284. const channelUrlUnify = (url) => {
  2285.  
  2286. if (typeof url !== 'string') return url; // type unchanged
  2287. let c0 = url.charAt(0);
  2288. if (c0 === '/') return url;
  2289. let i = url.indexOf('.youtube.com/');
  2290. if (i >= 1) url = url.substring(i + '.youtube.com/'.length - 1);
  2291. else if (url.charAt(0) !== '/') url = '/' + url;
  2292. return url
  2293.  
  2294. }
  2295.  
  2296. const parentYtElement = (node, nodeName, maxN) => {
  2297. for (let pDom = nodeParent(node); (pDom instanceof HTMLElement) && maxN-- > 0; pDom = nodeParent(pDom)) {
  2298. if (pDom.nodeName === nodeName) {
  2299. return pDom;
  2300. }
  2301. }
  2302. return null;
  2303. }
  2304.  
  2305. const checkShortsChannelName = (rchannelNameDOM) => {
  2306. let browseId = '';
  2307. const pDom = parentYtElement(rchannelNameDOM, 'YTD-REEL-PLAYER-HEADER-RENDERER', 18);
  2308.  
  2309. if (pDom instanceof HTMLElement) {
  2310. const pDomController = insp(pDom);
  2311. const channelTitleText = (pDomController.data || 0).channelTitleText || 0;
  2312. const runs = ((channelTitleText || 0).runs || 0);
  2313. if (getYTextType(channelTitleText) === 4) {
  2314. const browserEndpoint = (((runs[0] || 0).navigationEndpoint || 0).browseEndpoint || 0);
  2315. browseId = (browserEndpoint.browseId || '');
  2316. const currentDisplayText = runs[0].text || ''
  2317.  
  2318. if (isUCBrowserId(browseId) && isDisplayAsHandle(currentDisplayText)) {
  2319.  
  2320. if (!channelIdToHandle.has(browseId)) {
  2321. channelIdToHandle.set(browseId, {
  2322. handleText: currentDisplayText.trim(),
  2323. justPossible: true
  2324. });
  2325. }
  2326.  
  2327. // let anchorNode = elementQS(rchannelNameDOM, `a[href="${browserEndpoint.canonicalBaseUrl}"]`);
  2328. // anchorNode && anchorNode.setAttribute('jkrgy', browseId); // for checking
  2329. // anchorNode &&
  2330. getDisplayName(browseId).then(fetchResult => {
  2331.  
  2332. if (fetchResult === null) return;
  2333.  
  2334. const { title, langTitle, externalId } = fetchResult;
  2335. const titleForDisplay = langTitle || title;
  2336. if (externalId !== browseId) return; // channel id must be the same
  2337.  
  2338. const hDom = pDom;
  2339. const hDomHostElement = hDom.hostElement || hDom;
  2340. const hDomController = insp(hDom);
  2341. const hData = (hDomController || 0).data || 0;
  2342. const hChannelTitleText = (hData || 0).channelTitleText || 0;
  2343. const runs = ((hChannelTitleText).runs || 0);
  2344. if (getYTextType(hChannelTitleText) === 4 && (runs[0] || 0).text === currentDisplayText && hDomHostElement.isConnected === true) {
  2345.  
  2346. const channelTitleTextNew = { runs: [Object.assign({}, runs[0], { text: titleForDisplay })] }
  2347.  
  2348. hDomController.data = Object.assign({}, hDomController.data, { channelTitleText: channelTitleTextNew, rSk0e: 1 });
  2349. addCSSRulesIfRequired();
  2350.  
  2351. byPassCheckStore.delete(rchannelNameDOM);
  2352. }
  2353.  
  2354. })
  2355. }
  2356.  
  2357. }
  2358. }
  2359. return browseId
  2360. }
  2361.  
  2362. let firstDOMCheck = false;
  2363. const updateDisplayNameFn = (channelId, newName) => {
  2364. const cached = displayNameResStore.get(channelId);
  2365. if (typeof (cached || 0).title === 'string') {
  2366. cached.title = newName;
  2367. } else if (!cached) {
  2368. displayNameResStore.set(channelId, {
  2369. channelUrl: `https://www.youtube.com/channel/${channelId}`,
  2370. externalId: `${channelId}`,
  2371. ownerUrls: [],
  2372. title: newName,
  2373. vanityChannelUrl: null,
  2374. verified123: true
  2375. });
  2376. } else {
  2377. console.warn('unexpected display name cache', cached);
  2378. }
  2379. };
  2380. const firstDOMChecker = isMobile ? () => {
  2381.  
  2382. let playerMicroformatRenderer = null;
  2383. try {
  2384. playerMicroformatRenderer = window.ytInitialPlayerResponse.microformat.playerMicroformatRenderer
  2385. } catch (e) { }
  2386. if (playerMicroformatRenderer && playerMicroformatRenderer.ownerChannelName && playerMicroformatRenderer.ownerProfileUrl) {
  2387. cfg.ownerChannelName = playerMicroformatRenderer.ownerChannelName;
  2388. cfg.ownerProfileUrl = playerMicroformatRenderer.ownerProfileUrl.replace(/[\S]+youtube.com\//, 'youtube.com/');
  2389. }
  2390.  
  2391. const channelNameDOM = document.querySelector('ytm-slim-owner-renderer');
  2392. const channelNameCData = !channelNameDOM ? null : insp(channelNameDOM).data;
  2393.  
  2394. if (channelNameCData) {
  2395. let mainChannelUrl = null;
  2396. let mainFormattedNameUrl = null;
  2397. let mainChannelText = null;
  2398. let mainFormattedNameText = null;
  2399. let mainChannelBrowseId = null;
  2400.  
  2401. try {
  2402. mainChannelUrl = channelUrlUnify(channelNameCData.channelUrl) // channelUrlUnify("http://www.youtube.com/channel/UC5CwaMl1eIgY8h02uZw7u8A")
  2403. mainFormattedNameUrl = channelUrlUnify(channelNameCData.title.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl) // channelUrlUnify("/channel/UC5CwaMl1eIgY8h02uZw7u8A")
  2404. mainChannelText = channelNameCData.channelName
  2405. mainFormattedNameText = channelNameCData.title.runs[0].text
  2406. mainChannelBrowseId = channelNameCData.navigationEndpoint.browseEndpoint.browseId;
  2407. } catch (e) { }
  2408.  
  2409. if (mainChannelUrl === mainFormattedNameUrl && mainChannelText === mainFormattedNameText && typeof mainChannelUrl === 'string' && typeof mainChannelText === 'string' && mainChannelBrowseId && isUCBrowserId(mainChannelBrowseId)) {
  2410.  
  2411. const channelId = mainChannelBrowseId;
  2412. if (channelId) {
  2413. if (mainChannelText.startsWith('@')) return;
  2414. if (mainChannelText.trim() !== mainChannelText) return;
  2415. updateDisplayNameFn(channelId, mainChannelText);
  2416. }
  2417.  
  2418.  
  2419. }
  2420. }
  2421.  
  2422. } : () => {
  2423.  
  2424. const channelNameDOM = document.querySelector('ytd-channel-name.ytd-video-owner-renderer');
  2425. const channelNameCnt = insp(channelNameDOM);
  2426. const channelNameCData = (channelNameCnt || 0).__data || (channelNameDOM || 0).__data;
  2427. if (channelNameCData) {
  2428. let mainChannelUrl = null;
  2429. let mainFormattedNameUrl = null;
  2430. let mainChannelText = null;
  2431. let mainFormattedNameText = null;
  2432. let mainChannelBrowseId = null;
  2433. try {
  2434. mainChannelUrl = channelNameCData.channelName.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl
  2435. mainFormattedNameUrl = channelNameCData.formattedName.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl
  2436. mainChannelText = channelNameCData.channelName.runs[0].text
  2437. mainFormattedNameText = channelNameCData.formattedName.runs[0].text
  2438. mainChannelBrowseId = channelNameCData.channelName.runs[0].navigationEndpoint.browseEndpoint.browseId;
  2439. } catch (e) { }
  2440.  
  2441. if (mainChannelUrl === mainFormattedNameUrl && mainChannelText === mainFormattedNameText && typeof mainChannelUrl === 'string' && typeof mainChannelText === 'string' && mainChannelBrowseId && isUCBrowserId(mainChannelBrowseId)) {
  2442.  
  2443. const m = exactHandleUrl(mainChannelUrl, false);
  2444.  
  2445. if (m && mainChannelText !== m) {
  2446.  
  2447. const channelHandle = m[1];
  2448. if (/[^-_a-zA-Z0-9.]/.test(mainChannelText)) {
  2449. langPreferredDisplayNameMap.set(channelHandle, mainChannelText);
  2450. }
  2451.  
  2452. } else {
  2453.  
  2454. const channelId = mainChannelBrowseId;
  2455. if (channelId) {
  2456. if (mainChannelText.startsWith('@')) return;
  2457. if (mainChannelText.trim() !== mainChannelText) return;
  2458. updateDisplayNameFn(channelId, mainChannelText);
  2459. }
  2460.  
  2461. }
  2462. }
  2463. }
  2464.  
  2465. };
  2466.  
  2467. // let newAnchorAdded = false;
  2468. /*
  2469.  
  2470. const intersectionobserver = new IntersectionObserver((entries) => {
  2471. let anchorAppear = false;
  2472. for (const entry of entries) {
  2473. if (entry.isIntersecting === true) {
  2474. anchorAppear = true;
  2475. break;
  2476. }
  2477. }
  2478. if (anchorAppear && newAnchorAdded) {
  2479. newAnchorAdded = false; // stacking will be only reset when one or more anchor added to DOM.
  2480. mutex.nextIndex = 0; // higher pirority for new elements being shown
  2481. }
  2482. }, {
  2483. rootMargin:"0px 0px 0px 0px",
  2484. threshold:1
  2485. })
  2486. */
  2487.  
  2488.  
  2489. /* globals WeakRef:false */
  2490.  
  2491. /** @type {(o: Object | null) => WeakRef | null} */
  2492. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  2493.  
  2494. /** @type {(wr: Object | null) => Object | null} */
  2495. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  2496.  
  2497. let doShortsChecking = false;
  2498. let doDescriptionChecking = true;
  2499.  
  2500. let lastNewAnchorLastWR = null;
  2501.  
  2502. const pipelineForDOMMutations = createPipeline();
  2503.  
  2504. const byPassCheckStore = new WeakSet();
  2505.  
  2506. const mutObserverForShortsChannelName = new MutationObserver((mutations) => {
  2507. for (const mutation of mutations) {
  2508. const target = mutation.target;
  2509. if (!target) continue;
  2510. const rchannelNameElm = parentYtElement(target, 'YTD-CHANNEL-NAME', 18);
  2511. if (!rchannelNameElm) continue;
  2512. const rchannelNameCnt = insp(rchannelNameElm);
  2513. if ((rchannelNameCnt.data || 0).rSk0e) continue;
  2514. const channelId = rchannelNameElm.getAttribute('jkrgy');
  2515. if (channelId) {
  2516. if (byPassCheckStore.has(rchannelNameElm)) continue;
  2517. const handleObj = channelIdToHandle.get(channelId);
  2518. if (handleObj && handleObj.handleText && !handleObj.justPossible) {
  2519. const textDom = ((rchannelNameCnt.$ || 0).text || 0);
  2520. if (textDom) {
  2521. let t = textDom.textContent.trim()
  2522. if (t === handleObj.handleText || t === '') {
  2523. rchannelNameElm.removeAttribute('jkrgy');
  2524. // Promise.resolve().then(domChecker);
  2525. }
  2526. }
  2527. }
  2528. }
  2529. }
  2530. });
  2531.  
  2532. let domCheckScheduledForShorts = false;
  2533. const domCheckSelectorForShorts = [
  2534. 'ytd-channel-name.ytd-reel-player-header-renderer:not([jkrgy]) a.yt-simple-endpoint[href]'
  2535. ].join(', ');
  2536.  
  2537.  
  2538. const domCheckerForShorts = async () => {
  2539.  
  2540. if (domCheckScheduledForShorts) return;
  2541. if (document.querySelector(domCheckSelectorForShorts) === null) {
  2542. return;
  2543. }
  2544. domCheckScheduledForShorts = true;
  2545.  
  2546. await pipelineForDOMMutations(async () => {
  2547.  
  2548. domCheckScheduledForShorts = false;
  2549. // setTimeout(()=>{ // delay due to data update is after [is-active] switching
  2550.  
  2551. try {
  2552.  
  2553. const nodes = document.querySelectorAll(domCheckSelectorForShorts);
  2554. if (nodes.length === 0) {
  2555. return;
  2556. }
  2557.  
  2558. for (const node of nodes) {
  2559.  
  2560. const rchannelNameElm = parentYtElement(node, 'YTD-CHANNEL-NAME', 18);
  2561. if (rchannelNameElm) {
  2562. mutObserverForShortsChannelName.observe(rchannelNameElm, {
  2563. childList: true,
  2564. subtree: true
  2565. });
  2566. byPassCheckStore.add(rchannelNameElm);
  2567. const channelId = checkShortsChannelName(rchannelNameElm);
  2568. rchannelNameElm.setAttribute('jkrgy', channelId || '');
  2569. }
  2570.  
  2571. }
  2572.  
  2573. } catch (e) {
  2574. console.log('error occur', e);
  2575. }
  2576.  
  2577. });
  2578. domCheckScheduledForShorts = false;
  2579.  
  2580. };
  2581.  
  2582. /** @param {Element} s */
  2583. const domCheckerForDescriptionAnchor = (s) => {
  2584.  
  2585. findTextNodes(s, (textNode) => {
  2586. const p = textNode.nodeValue;
  2587. const index = p.indexOf('@');
  2588. if (index < 0) return;
  2589. const q = p.substring(index);
  2590. const b = isDisplayAsHandle(q);
  2591. if (b) {
  2592. const href = s.getAttribute('href')
  2593.  
  2594. const channelId = obtainChannelId(href);
  2595. if (channelId) {
  2596. const h = q.trim();
  2597.  
  2598. if (!firstDOMCheck) {
  2599. firstDOMCheck = true;
  2600. USE_CHANNEL_META && firstDOMChecker();
  2601. }
  2602.  
  2603. if (!channelIdToHandle.has(channelId)) {
  2604. channelIdToHandle.set(channelId, {
  2605. handleText: h,
  2606. justPossible: true
  2607. });
  2608. }
  2609.  
  2610. s.setAttribute('jkrgy', channelId);
  2611. getDisplayName(channelId).then(fetchResult => {
  2612.  
  2613. if (fetchResult === null) return;
  2614.  
  2615. const { title, langTitle, externalId } = fetchResult;
  2616. const titleForDisplay = langTitle || title;
  2617. if (externalId !== channelId) return; // channel id must be the same
  2618.  
  2619. const anchorElement = s;
  2620. const anchorTextNode = textNode;
  2621.  
  2622. if (anchorElement.isConnected === true && anchorTextNode.isConnected === true && anchorElement.contains(anchorTextNode) && anchorTextNode.nodeValue === p && s.getAttribute('href') === href) {
  2623.  
  2624. anchorTextNode.nodeValue = `${p.substring(0, index)}${q.replace(h, titleForDisplay)}`;
  2625.  
  2626. }
  2627.  
  2628. });
  2629. }
  2630. }
  2631. return true;
  2632. });
  2633.  
  2634. }
  2635.  
  2636. // domCheckerForDescription: To be reviewed
  2637. // Example: https://www.youtube.com/watch?v=1NL-sIP9p_U
  2638. const domCheckerForDescription = isMobile ? () => {
  2639. // example https://m.youtube.com/watch?v=jKt4Ah47L7Q
  2640. 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])')) {
  2641. s.setAttribute('dxcpj', '');
  2642. domCheckerForDescriptionAnchor(s);
  2643. }
  2644. } : () => {
  2645. // example https://www.youtube.com/watch?v=jKt4Ah47L7Q
  2646. 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])')) {
  2647. s.setAttribute('dxcpj', '');
  2648. domCheckerForDescriptionAnchor(s);
  2649. }
  2650. };
  2651.  
  2652. const domCheckScheduledBools = [];
  2653. const domCheckScheduledSelectors = isMobile ? [
  2654. 'a[aria-label^="@"][href*="channel/"]:not([jkrgy])', // old; Before Feb 2024
  2655. 'a.comment-icon-container[href*="/@"]:not([jkrgy])', // Feb 2024
  2656. 'a.comment-icon-container[href*="channel/"]:not([jkrgy])' // Mar 2024
  2657. ] : [
  2658. 'a[id][href*="channel/"]:not([jkrgy])', // old; Before Feb 2024
  2659. 'a.yt-simple-endpoint.style-scope[id][href^="/@"]:not([jkrgy])' // Feb 2024
  2660. ];
  2661.  
  2662. const newAuthorAnchorsProceed = async (newAnchors) => {
  2663.  
  2664. const cNewAnchorFirst = newAnchors[0]; // non-null
  2665. const cNewAnchorLast = newAnchors[newAnchors.length - 1]; // non-null
  2666. /** @type {HTMLElement | null} */
  2667. const lastNewAnchorLast = kRef(lastNewAnchorLastWR); // HTMLElement | null
  2668. lastNewAnchorLastWR = mWeakRef(cNewAnchorLast);
  2669.  
  2670. if (!firstDOMCheck) {
  2671. firstDOMCheck = true;
  2672. USE_CHANNEL_META && firstDOMChecker();
  2673. }
  2674.  
  2675. await Promise.resolve();
  2676.  
  2677. if (lastNewAnchorLast && mutex.nextIndex >= 1) {
  2678. if ((lastNewAnchorLast.compareDocumentPosition(cNewAnchorFirst) & 2) === 2) {
  2679. mutex.nextIndex = 0;
  2680. }
  2681. await Promise.resolve();
  2682. }
  2683.  
  2684. // newAnchorAdded = true;
  2685. for (const anchor of newAnchors) {
  2686. const hrefAttribute = anchor.getAttribute('href');
  2687. const hrefV = ytPathnameExtract(hrefAttribute);
  2688. let channelId = '';
  2689. const ytElm = isMobile ? closestYtmData(anchor) : closestYtParent(anchor);
  2690. if (ytElm) {
  2691. const cnt = insp(ytElm);
  2692. const { browseId, canonicalBaseUrl } = getAuthorBrowseEndpoint(cnt) || 0;
  2693.  
  2694. if (browseId && canonicalBaseUrl === hrefV && browseId.startsWith('UC') && /^UC[-_a-zA-Z0-9+=.]{22}$/.test(browseId)) {
  2695.  
  2696. const hrefType = getHrefType(hrefV);
  2697. if (hrefType === 1) {
  2698. if (hrefV === `/channel/${browseId}`) {
  2699. let authorText = null;
  2700. if (isMobile) { // mobile only
  2701. authorText = (cnt._commentData || cnt.data || ytElm._commentData || ytElm.data || 0).authorText || 0;
  2702. } else if (!isMobile) {
  2703. authorText = (cnt.data || 0).authorText || 0;
  2704. }
  2705. if (authorText) {
  2706. const text = getYTextContent(authorText);
  2707. if (typeof text === 'string' && text.startsWith('@') && exactHandleText(text, true)) {
  2708. channelIdToHandle.set(browseId, {
  2709. handleText: `${text}`,
  2710. justPossible: true
  2711. });
  2712. }
  2713. }
  2714. channelId = browseId;
  2715. }
  2716. } else if (hrefType === 2) {
  2717. const handle = hrefV.substring(2);
  2718. channelIdToHandle.set(browseId, {
  2719. handleText: `@${handle}`
  2720. });
  2721. channelId = browseId;
  2722. }
  2723.  
  2724. }
  2725.  
  2726. if (!channelId && hrefV) { // fallback
  2727.  
  2728. // author-text or name
  2729. // normal url: /channel/xxxxxxx
  2730. // Improve YouTube! - https://www.youtube.com/channel/xxxxxxx/videos
  2731.  
  2732. const temp = obtainChannelId(hrefV); // string, can be empty
  2733. if (temp) {
  2734. channelId = temp;
  2735. }
  2736.  
  2737. }
  2738. }
  2739. anchor.setAttribute('jkrgy', channelId);
  2740. if (channelId) {
  2741. domCheckAsync(anchor, hrefAttribute, channelId);
  2742. }
  2743. }
  2744.  
  2745. }
  2746.  
  2747. const closestYtParent = (dom) => {
  2748. while (dom instanceof Element) {
  2749. if (typeof dom.is === 'string' && dom.is) return dom;
  2750. dom = dom.parentNode;
  2751. }
  2752. return null;
  2753. };
  2754. const closestYtmData = (dom) => {
  2755. while (dom instanceof Element) {
  2756. if (typeof (dom._commentData || dom.data || 0) === 'object') return dom;
  2757. dom = dom.parentNode;
  2758. }
  2759. return null;
  2760. };
  2761. const noAuthorDataSet = new Set();
  2762. const getAuthorBrowseEndpoint = (cnt) => {
  2763. let d;
  2764. let haveData = false;
  2765. if (isMobile) {
  2766. const data = ((cnt || 0).data || 0);
  2767. if (data) {
  2768. haveData = true;
  2769. if (d = (data.authorNameEndpoint || data.authorEndpoint)) return d.browseEndpoint || null;
  2770. }
  2771. } else {
  2772. const __data = ((cnt || 0).__data || 0);
  2773. if (__data) {
  2774. haveData = true;
  2775. if (d = __data.authorTextCommand) return d.browseEndpoint || null; // ytd-comment-renderer
  2776. if (d = (__data.authorNameEndpoint || __data.authorEndpoint)) return d.browseEndpoint || null; // ytd-comment-view-model (v1 & v2)
  2777. if ((d = (__data.data || 0)) && (d = (d.authorNameEndpoint || d.authorEndpoint))) return d.browseEndpoint || null; // ytd-author-comment-badge-renderer
  2778. // note: authorNameEndpoint instead of authorEndpoint for ytd-comment-view-model since 2024.04
  2779.  
  2780. } else {
  2781.  
  2782. if ((d = ((cnt || 0).data || 0)) && (d = (d.authorTextCommand || d.authorNameEndpoint || d.authorEndpoint))) return d.browseEndpoint || null; // ytd-comment-view-model (2024.05.25 ?)
  2783.  
  2784. if (d = (cnt.authorTextCommand || cnt.authorNameEndpoint || cnt.authorEndpoint)) return d.browseEndpoint || null; // ytd-comment-view-model (2024.04.30) (v3) (?)
  2785.  
  2786. }
  2787.  
  2788. }
  2789. if (haveData) {
  2790. const tag = cnt.is;
  2791. if (!noAuthorDataSet.has(tag)) {
  2792. noAuthorDataSet.add(tag);
  2793. if (tag === 'ytd-video-description-infocards-section-renderer') {
  2794. return null; // skip console logging
  2795. } else if (tag === 'ytd-thumbnail') {
  2796. return null;
  2797. } else if (tag === 'ytd-structured-description-channel-lockup-renderer') {
  2798. return null;
  2799. } else if (tag === 'ytd-rich-grid-media') {
  2800. return null;
  2801. } else if (tag === 'ytd-rich-metadata-renderer') {
  2802. return null;
  2803. } else if (tag === 'ytd-guide-entry-renderer') {
  2804. return null;
  2805. } else if (tag === 'ytd-grid-channel-renderer') {
  2806. return null;
  2807. } else if (tag === 'ytd-channel-renderer') {
  2808. return null;
  2809. }
  2810. console.log('no browseEndpoint can be found', tag);
  2811. }
  2812. } else {
  2813. console.log('no browseEndpoint can be found', (cnt || 0).is, 'NO DATA');
  2814. }
  2815. return null;
  2816. };
  2817.  
  2818. const domAuthorNameCheckN = async (kq) => {
  2819. if (domCheckScheduledBools[kq]) return;
  2820. const selector = domCheckScheduledSelectors[kq];
  2821. if (!selector || document.querySelector(selector) === null) return;
  2822. domCheckScheduledBools[kq] = true;
  2823. await pipelineForDOMMutations(async () => {
  2824. domCheckScheduledBools[kq] = false;
  2825. try {
  2826. const newAnchors = document.querySelectorAll(selector);
  2827. if (newAnchors.length === 0) return;
  2828. await newAuthorAnchorsProceed(newAnchors);
  2829. } catch (e) {
  2830. console.log('error occur', e);
  2831. }
  2832. });
  2833. domCheckScheduledBools[kq] = false;
  2834. };
  2835.  
  2836. let _lastAnchorCount = -1;
  2837. const anchorCollection = document.getElementsByTagName('A');
  2838.  
  2839. /*
  2840. const collectionChangeObserverFn = (collection)=>{
  2841. let _len = -1;
  2842. let ws = null;
  2843. ws = new WeakSet(collection);
  2844. return {check: ()=>{
  2845. const lastLen = _len;
  2846. const len = collection.length;
  2847. if(len !== lastLen){
  2848. ws = new WeakSet(collection);
  2849. return true;
  2850. }
  2851. for (const elm of collection) {
  2852. if (!ws.has(elm)){
  2853. ws = new WeakSet(collection);
  2854. return true;
  2855. }
  2856. }
  2857. return false;
  2858. }}
  2859.  
  2860. }
  2861. */
  2862.  
  2863. const domChecker = (mutations) => {
  2864.  
  2865. const currAnchorCount = anchorCollection.length;
  2866. const lastAnchorCount = _lastAnchorCount;
  2867. _lastAnchorCount = currAnchorCount;
  2868. if (currAnchorCount === 0) return;
  2869.  
  2870. if (mutations && lastAnchorCount === currAnchorCount) {
  2871. // test to skip
  2872. let state = 0;
  2873. let addedNonElement = false;
  2874. for (const mutation of mutations) {
  2875. const addedNodes = mutation.addedNodes || 0;
  2876. const removedNodes = mutation.removedNodes || 0;
  2877. const target = mutation.target || 0;
  2878. if (target.nodeName === 'YT-FORMATTED-STRING') continue; // ignore changes in <yt-formatted-string>
  2879. if (addedNodes.length >= 1) {
  2880. state |= 1;
  2881. if (!addedNonElement) {
  2882. for (const addedNode of addedNodes) {
  2883. if (!(addedNode instanceof Element)) {
  2884. addedNonElement = true;
  2885. break;
  2886. }
  2887. }
  2888. }
  2889. if (state & 2) break;
  2890. } else if (removedNodes.length >= 1) {
  2891. state |= 2;
  2892. if (state & 1) break;
  2893. } else {
  2894. state |= 4;
  2895. break;
  2896. }
  2897. }
  2898. if (state === 1) {
  2899. if (!addedNonElement) return; // no change in anchor
  2900. } else if (state === 2) {
  2901. return; // only removal
  2902. } else if (state === 0) {
  2903. return; // no recognized mutation
  2904. }
  2905. }
  2906.  
  2907. doShortsChecking && domCheckerForShorts();
  2908. doDescriptionChecking && domCheckerForDescription();
  2909. for (let i = 0; i < domCheckScheduledSelectors.length; i++) domAuthorNameCheckN(i);
  2910.  
  2911. };
  2912.  
  2913.  
  2914. /** @type {MutationObserver | null} */
  2915. let domObserver = null;
  2916.  
  2917. /** @type {(callback: (event: Event)=>{})} */
  2918. const onPageFetched = isMobile ? (callback) => {
  2919.  
  2920. let state = 0;
  2921.  
  2922. window.addEventListener('state-change', function (evt) {
  2923. if (state === 1) return; // ignore intermediate events
  2924. state = 1; // before value: 0 or 2;
  2925. callback(evt);
  2926. }, false);
  2927.  
  2928. window.addEventListener('state-navigateend', function (evt) { // fallback
  2929. let inSeq = state === 1;
  2930. state = 2; // 2 when loaded
  2931. if (!inSeq) callback(evt);
  2932. }, false);
  2933.  
  2934. } : (callback) => {
  2935.  
  2936. let state = 0;
  2937.  
  2938. document.addEventListener('yt-page-data-fetched', function (evt) {
  2939. if (state === 1) return; // ignore intermediate events if any
  2940. state = 1; // before value: 0 or 2;
  2941. callback(evt);
  2942. }, false);
  2943.  
  2944. document.addEventListener('yt-navigate-finish', function (evt) { // fallback
  2945. let inSeq = state === 1;
  2946. state = 2; // 2 when loaded
  2947. if (!inSeq) callback(evt);
  2948. }, false);
  2949.  
  2950. };
  2951.  
  2952.  
  2953. // let mActive = false;
  2954. // let lastContent = '';
  2955. const removeAttrs = () => {
  2956. for (const s of document.querySelectorAll('a[href][dxcpj]')) {
  2957. s.removeAttribute('dxcpj');
  2958. }
  2959. for (const s of document.querySelectorAll('a[href][jkrgy]')) {
  2960. s.removeAttribute('jkrgy');
  2961. }
  2962. }
  2963.  
  2964. // setInterval(() => {
  2965. // if (!mActive) return;
  2966. // let content = (document.querySelector('ytd-comments-header-renderer #title') || 0).textContent || '';
  2967. // if (content !== lastContent) {
  2968. // lastContent = content;
  2969. // removeAttrs();
  2970. // }
  2971. // }, 400);
  2972.  
  2973. /**
  2974. *
  2975. * @param {Node} app
  2976. * @returns
  2977. */
  2978. function setupOnPageFetched(app) {
  2979.  
  2980. firstDOMCheck = false;
  2981.  
  2982. if (!cfg['INNERTUBE_API_KEY']) {
  2983. console.warn("Userscript Error: INNERTUBE_API_KEY is not found.");
  2984. return;
  2985. }
  2986.  
  2987. if (CHANGE_FOR_SHORTS_CHANNEL_NAME && location.pathname.startsWith('/shorts/')) {
  2988. doShortsChecking = true;
  2989. } else {
  2990. doShortsChecking = false;
  2991. }
  2992.  
  2993. _lastAnchorCount = -1;
  2994.  
  2995. if (!domObserver) {
  2996. domObserver = new MutationObserver(domChecker);
  2997. } else {
  2998. domObserver.takeRecords();
  2999. domObserver.disconnect();
  3000. }
  3001.  
  3002. // lastContent = (document.querySelector('ytd-comments-header-renderer #title') || 0).textContent || '';
  3003. // mActive = true;
  3004. removeAttrs();
  3005.  
  3006. domObserver.observe(app, { childList: true, subtree: true, attributes: true, attributeFilter: ['jkrgy', 'href', 'dxcpj'] });
  3007. domChecker();
  3008.  
  3009. }
  3010.  
  3011. /**
  3012. *
  3013. * @param {Object|null} dest
  3014. * @param {Object} ytcfg
  3015. * @param {string[]} keys
  3016. * @returns
  3017. */
  3018. function getYtConfig(dest, ytcfg, keys) {
  3019. dest = dest || {};
  3020. if (typeof ytcfg.get === 'function') { // mobile
  3021. for (const key of keys) {
  3022. dest[key] = ytcfg.get(key);
  3023. }
  3024. } else {
  3025. const cfgData = ytcfg.data_ || ytcfg.__data || ytcfg._data || ytcfg.data || ytcfg; // dekstop - ytcfg.data_
  3026. for (const key of keys) {
  3027. dest[key] = cfgData[key];
  3028. }
  3029. }
  3030. return dest;
  3031. }
  3032.  
  3033. /** @type { (evt: Object) => Element | null } */
  3034. const getPageApp = isMobile
  3035. ? ((evt) => (document.querySelector('ytm-app#app') || document.querySelector('ytm-app')))
  3036. : ((evt) => ((evt || 0).target || document.querySelector('ytd-app')));
  3037.  
  3038. onPageFetched(async (evt) => {
  3039. const app = getPageApp(evt);
  3040. console.assert(app);
  3041. const ytcfg = ((window || 0).ytcfg || 0);
  3042. getYtConfig(cfg, ytcfg, ['INNERTUBE_API_KEY', 'INNERTUBE_CLIENT_VERSION', 'HL', 'GL']);
  3043. setupOnPageFetched(app);
  3044. });
  3045.  
  3046. })({ fetch, JSON, Request, AbortController, setTimeout, clearTimeout });