Restore YouTube Username from Handle to Custom

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

  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.13.12
  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. // @require https://cdn.jsdelivr.net/gh/cyfung1031/userscript-supports@5d83d154956057bdde19e24f95b332cb9a78fcda/library/default-trusted-type-policy.js
  45.  
  46. // @compatible firefox Violentmonkey
  47. // @compatible firefox Tampermonkey
  48. // @compatible firefox Firemonkey
  49. // @compatible chrome Violentmonkey
  50. // @compatible chrome Tampermonkey
  51. // @compatible opera Violentmonkey
  52. // @compatible opera Tampermonkey
  53. // @compatible safari Stay
  54. // @compatible edge Violentmonkey
  55. // @compatible edge Tampermonkey
  56. // @compatible brave Violentmonkey
  57. // @compatible brave Tampermonkey
  58.  
  59. // @description To restore YouTube Username to the traditional custom name
  60. // @description:ja YouTubeのユーザー名を伝統的なカスタム名に復元するために。
  61. // @description:zh-TW 將 YouTube 使用者名稱從 Handle 恢復為自訂名稱
  62. // @description:zh-CN 将 YouTube 用户名从 Handle 恢复为自定义名称
  63.  
  64. // @description:ko YouTube 사용자 이름을 전통적인 사용자 지정 이름으로 복원합니다.
  65. // @description:ru Восстановление имени пользователя YouTube с помощью обычного настраиваемого имени
  66. // @description:af Herstel YouTube-gebruikersnaam vanaf Handvat na Aangepaste Naam
  67. // @description:az YouTube İstifadəçi Adını Özəl Adından İstifadə Etmək
  68. // @description:id Mengembalikan Nama Pengguna YouTube dari Handle ke Kustom
  69. // @description:ms Pulihkan Nama Pengguna YouTube dari Handle ke Tersuai
  70. // @description:bs Vrati YouTube korisničko ime sa ručke na prilagođeno ime
  71. // @description:ca Restaurar el nom d'usuari de YouTube de la màniga al personalitzat
  72. // @description:cs Obnovte uživatelské jméno YouTube z rukojeti na vlastní
  73. // @description:da Gendan YouTube-brugernavn fra Håndtag til Brugerdefineret
  74. // @description:de Stellt den YouTube-Benutzernamen von Handle auf Benutzerdefiniert wieder her
  75. // @description:et Taasta YouTube'i kasutajanimi käepidemelt kohandatud nimeks
  76. // @description:es Restaurar el nombre de usuario de YouTube de la manija al personalizado
  77. // @description:eu Berrezarri YouTube Erabiltzaile-izena Manipulatik Pertsonalizatuera
  78. // @description:fr Restaurer le nom d'utilisateur YouTube de la poignée au nom personnalisé
  79. // @description:gl Restaura o nome de usuario de YouTube da manexa ao personalizado
  80. // @description:hr Vrati YouTube korisničko ime s ručke na prilagođeno ime
  81. // @description:zu Hlanganisa Igama Lokusebenza lwe-YouTube kusuka kwi-Handle kuze kube kusebenza kakhulu
  82. // @description:is Endurheimtu YouTube Notandanafn frá Handfangi til Sérsniðins
  83. // @description:it Ripristina il nome utente di YouTube da Handle a Personalizzato
  84. // @description:sw Rejesha Jina la Mtumiaji wa YouTube kutoka kwa Kishughulikia hadi Desturi
  85. // @description:lv Atjaunot YouTube lietotāja vārdu no roktura uz pielāgotu
  86. // @description:lt Atkurti „YouTube“ naudotojo vardą iš rankenos į tinkamą
  87. // @description:hu Visszaállítja a YouTube felhasználónevet a fogantyútól a testreszabottra
  88. // @description:nl Herstel YouTube-gebruikersnaam van Handle naar Aangepaste naam
  89. // @description:uz YouTube foydalanuvchining nomini Hurda dan Shaxsiylashtirilgan nomga qaytarish
  90. // @description:pl Przywróć nazwę użytkownika YouTube z uchwytu na niestandardową
  91. // @description:pt Restaurar o nome de usuário do YouTube de Handle para Personalizado
  92. // @description:pt-BR Restaurar o nome de usuário do YouTube de Handle para Personalizado
  93. // @description:ro Restaurați Numele de Utilizator YouTube de la Mâner la Personalizat
  94. // @description:sq Rikthe emrin e përdoruesit të YouTube nga Maja në Personalizuar
  95. // @description:sk Obnovte používateľské meno YouTube z rukoväte na vlastné
  96. // @description:sl Obnovite uporabniško ime YouTube iz ročaja v prilagojeno ime
  97. // @description:sr Вратите YouTube корисничко име са ручке на прилагођено име
  98. // @description:fi Palauta YouTube-käyttäjänimi kahvasta räätälöityyn nimeen
  99. // @description:sv Återställ YouTube-användarnamnet från handtaget till anpassat namn
  100. // @description:vi Khôi phục Tên người dùng YouTube từ Tay cầm thành Tùy chỉnh
  101. // @description:tr Özel İsme Kullanıcı Adını İade Etme
  102. // @description:be Вярнуць імя карыстальніка YouTube з ручкі на наладжваецца
  103. // @description:bg Възстановяване на потребителско име на YouTube от дръжка до персонализирано име
  104. // @description:ky YouTube колдонуучунун атты боюнча атын тамашалау
  105. // @description:kk YouTube пайдаланушының атын Handle танымасынан Жеке атауға қайта түсіру
  106. // @description:mk Врати го името на YouTube корисникот од држачот во прилагодено име
  107. // @description:mn YouTube хэрэглэгчийн нэрийг хүчирхийлэгчидээс Байгууллагад нь солих
  108. // @description:uk Відновлення імені користувача YouTube з ручки на налаштоване ім'я
  109. // @description:el Επαναφορά Ονόματος Χρήστη YouTube από τη Λαβή σε Προσαρμοσμένο
  110. // @description:hy Վերականգնել YouTube-ի Օգտագործողի Անունը Ձեռքից Հատուցվածությանով
  111. // @description:ur ہینڈل سے یوٹیوب یوزر نیم کو کسٹم میں بحال کریں
  112. // @description:ar استعادة اسم مستخدم YouTube من المقبض إلى مخصص
  113. // @description:fa بازیابی نام کاربری یوتیوب از دستگیره به سفارشی
  114. // @description:ne ह्यान्डलबाट यूट्युब प्रयोगकर्तानाम अनुकूलमा उपयोग गर्नुहोस्
  115. // @description:mr हॅंडलमधून यूट्यूब वापरकर्तानाव अनुकूलित करा
  116. // @description:hi हैंडल से यूट्यूब यूजरनेम को कस्टम में बदलें
  117. // @description:as কাস্টম হতে হ্যান্ডেল পৰা ইউটিউব ব্যৱহাৰকাৰী নাম পুনৰ্স্থাপন কৰক
  118. // @description:bn হ্যান্ডেল থেকে কাস্টম করে ইউটিউব ব্যবহারকারীর নাম পুনরুদ্ধার করুন
  119. // @description:pa ਯੂਟਿਊਬ ਯੂਜਰਨਾਂ ਦਾ ਨਾਂ ਹੈਂਡਲ ਤੋਂ ਕਸਟਮ ਵਿੱਚ ਮੁੜ ਲਾਓ
  120. // @description:gu હેન્ડલથી કસ્ટમમાંથી YouTube વપરાશકર્તાનું નામ પુનર્સ્થાપિત કરો
  121. // @description:or କଷ୍ଟମରେ ହେନ୍ଡେଲରୁ YouTube ବ୍ୟବହାରକାରୀଙ୍କ ନାମ ପୁନର୍ପ୍ରାପ୍ତ କରନ୍ତୁ
  122. // @description:ta ஹேண்டில் இருந்து கஸ்டம் ஆக்கும் யூடியூப் பயனர்பெயரை மீட்டமைக்கவும்
  123. // @description:te హాండును నుంచి YouTube వాడుకరి పేరును కస్టమ్‌కు పునరుద్ధరించండి
  124. // @description:kn ಹ್ಯಾಂಡಲ್‌ನಿಂದ YouTube ಬಳಕೆದಾರ ಹೆಸರನ್ನು ಅನುಕೂಲಿತ ಮಾಡಿ
  125. // @description:ml ഹാന്റിൽ നിന്ന് കസ്റ്റം ആക്കാംമെന്ന് യുട്യൂബ് ഉപയോക്തൃനാമം മറ്റുള്ളവരെ മാറ്റുക
  126. // @description:si හැන්ඩල් සහිතව YouTube භාවිතයේ පරිශීලක නාමය ස්වයං කරයි
  127. // @description:th กู้คืนชื่อผู้ใช้ YouTube จากแฮนดิลไปเป็นชื่อที่กำหนดเอง
  128. // @description:lo ເຮັດຊະນິດຊື່ຜູ້ໃຊ້ YouTube ຈາກ Handle ຈົດໄວ້ເປັນຊື່ປັດຈຸບັນ
  129. // @description:my Handle မှ YouTube အသုံးပြုသူအမည်ကို စတင်ပြန်စစ်ဆေးပါ
  130. // @description:ka YouTube მომხმარებლის სახელის აღდგენა ხანდლიდან მორგებულ სახელში
  131. // @description:am ስለ YouTube የተጠቃሚ ስም ማስቀመጫዎቹን ከ Handle ወደ Custom ውስጥ እንደሚመጣ ይረዳሉ
  132. // @description:km កំណត់ YouTube ពាក្យឈ្មោះពី Handle ទៅជាឈ្មោះផ្ទាល់ខ្លួន
  133.  
  134. // ==/UserScript==
  135.  
  136. const NUM_CHANNEL_ID_MIN_LEN_old1 = 4; // 4 = @abc
  137. const NUM_CHANNEL_ID_MIN_LEN_1 = 2; // 4 = @abc -> 2 = @a
  138. const NUM_CHANNEL_ID_MIN_LEN_2 = 3; // 5 = /@abc -> 3 = /@a
  139.  
  140.  
  141. const defaultPolicy = (typeof trustedTypes !== 'undefined' && trustedTypes.defaultPolicy) || { createHTML: s => s };
  142. function createHTML(s) {
  143. return defaultPolicy.createHTML(s);
  144. }
  145.  
  146. let trustHTMLErr = null;
  147. try {
  148. document.createElement('div').innerHTML = createHTML('1');
  149. } catch (e) {
  150. trustHTMLErr = e;
  151. }
  152.  
  153. if (trustHTMLErr) {
  154. console.log(`trustHTMLErr`, trustHTMLErr);
  155. trustHTMLErr(); // exit userscript
  156. }
  157.  
  158.  
  159. // -----------------------------------------------------------------------------------------------------------------------------
  160.  
  161. /* jshint esversion:8 */
  162.  
  163. const Object_ = Object;
  164.  
  165. /**
  166. @typedef {string} HandleText
  167. * \@[-_a-zA-Z0-9.]{3,30} (before May 2024)
  168. * See isDisplayAsHandle
  169. */
  170.  
  171. /**
  172. @typedef {string} ChannelId
  173. * UC[-_a-zA-Z0-9+=.]{22}
  174. * https://support.google.com/youtube/answer/6070344?hl=en
  175. * The channel ID is the 24 character alphanumeric string that starts with 'UC' in the channel URL.
  176. */
  177.  
  178. /**
  179. @typedef {string} DisplayName
  180. * Display Name
  181. */
  182.  
  183. /**
  184. @typedef DisplayNameResultObject
  185. @type {Object}
  186. @property {DisplayName} title
  187. @property {DisplayName} langTitle
  188. @property {ChannelId?} externalId
  189. @property {string[]} ownerUrls
  190. @property {string} channelUrl
  191. @property {string} vanityChannelUrl
  192. @property {boolean|undefined} verified123
  193. */
  194.  
  195. /**
  196. @typedef ChannelIdToHandleResult
  197. @type {Object}
  198. @property {HandleText} handleText
  199. @property {boolean} justPossible
  200. */
  201.  
  202. (function (__CONTEXT__) {
  203. 'use strict';
  204.  
  205. // const JUST_REMOVE_AT_FOR_EXPANDED_LANG = true;
  206.  
  207. const USE_RSS_FETCHER = true; // might 404
  208. const USE_TIMEOUT_SIGNAL = false;
  209. const USE_CHANNEL_META = true;
  210. const CHANGE_FOR_SHORTS_CHANNEL_NAME = false;
  211. const USE_LANG_SPECIFIC_NAME = true;
  212. const UPDATE_PIN_NAME = true; // for USE_LANG_SPECIFIC_NAME
  213. const FIX_RTL_ISSUE = true;
  214.  
  215. const IGNORE_NO_NAME = false;
  216.  
  217. /** @type {globalThis.PromiseConstructor} */
  218. const Promise = (async () => { })().constructor; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  219.  
  220. const { fetch, JSON, Request, AbortController, setTimeout, clearTimeout } = __CONTEXT__; // YouTube hacks Promise in WaterFox Classic and "Promise.resolve(0)" nevers resolve.
  221.  
  222. const timeoutSignal = AbortController && USE_TIMEOUT_SIGNAL ? (timeout) => {
  223. let controller = new AbortController();
  224. const r = controller.signal;
  225. let cid = setTimeout(() => {
  226. if (cid >= 1) {
  227. cid = 0;
  228. controller && controller.abort();
  229. controller = null;
  230. }
  231. }, timeout);
  232. r.clearTimeout = () => {
  233. if (cid >= 1) {
  234. clearTimeout(cid);
  235. cid = 0;
  236. }
  237. }
  238. return r;
  239. } : () => { };
  240.  
  241. const insp = o => o ? (o.polymerController || o.inst || o || 0) : (o || 0);
  242.  
  243. const fxOperator = (proto, propertyName) => {
  244. let propertyDescriptorGetter = null;
  245. try {
  246. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  247. // https://github.com/cyfung1031/userscript-supports/issues/9
  248. // .parentNode becomes DocumentFragment
  249. /**
  250. *
  251. * Issue Description: YtCustomElements - Custom DOM Operations overrided in FireFox
  252. *
  253. * e.g. ytd-comment-renderer#comment > div#body
  254. *
  255. * ${div#body}.parentNode = DocumentFragment <Node.parentNode>
  256. * ${ytd-comment-renderer#comment}.firstElementChild <Element.firstElementChild>
  257. *
  258. * Cofirmed Affected: parentNode, firstChild, firstElementChild, lastChild, lastElementChild, querySelector, & querySelectorAll
  259. *
  260. * Alternative way: ytCustomElement.$.xxxxxxx
  261. *
  262. */
  263. } catch (e) { }
  264. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  265. };
  266.  
  267. const fxAPI = (proto, propertyName) => {
  268. const methodFunc = proto[propertyName];
  269. return typeof methodFunc === 'function' ? (e, ...args) => methodFunc.apply(e, args) : (e, ...args) => e[propertyName](...args);
  270. };
  271.  
  272. /** @type { (node: Node)=>Node | null } */
  273. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  274. /** @type { (node: Node)=>Node | null } */
  275. const nodeFirstChild = fxOperator(Node.prototype, 'firstChild');
  276. /** @type { (node: Node)=>Node | null } */
  277. const nodeNextSibling = fxOperator(Node.prototype, 'nextSibling');
  278.  
  279. /** @type { (node: ParentNode, selector: string)=>Element | null } */
  280. const elementQS = fxAPI(Element.prototype, 'querySelector');
  281. /** @type { (node: ParentNode, selector: string)=>Element[] } */
  282. const elementQSA = fxAPI(Element.prototype, 'querySelectorAll');
  283.  
  284. /*
  285.  
  286.  
  287. init
  288.  
  289. initialdata
  290. state-progress
  291. state-responsereceived
  292. player-autonav-pause
  293. state-change
  294. state-navigateend
  295. player-initialized
  296. renderer-module-load-start
  297. video-data-change
  298. player-state-change
  299. updateui
  300. renderer-module-load-end -> channel owner DOM available
  301. player-autonav-pause
  302. updateui
  303. renderer-module-load-start
  304. updateui
  305. renderer-module-load-end
  306.  
  307.  
  308. playing
  309.  
  310. h5player.video-progress
  311. h5player.video-progress
  312. h5player.video-progress
  313. h5player.video-progress
  314. ...
  315.  
  316.  
  317.  
  318.  
  319. navigate new video
  320.  
  321. state-navigatestart
  322. state-change => channel owener DOM vanished
  323. state-progress
  324. ...
  325. h5player.player-state-change
  326. h5player.video-data-change
  327. h5player.player-state-change
  328. h5player.muted-autoplay-change
  329. h5player.volume-change
  330. h5player.video-data-change
  331. h5player.volume-change
  332. ...
  333.  
  334. state-progress
  335. ...
  336.  
  337. state-progress => channel owner DOM appear [just before state-responsereceived]
  338. state-responsereceived
  339.  
  340. video-data-change
  341. state-change
  342. state-navigateend
  343.  
  344.  
  345.  
  346.  
  347. UX interaction
  348.  
  349. user-activity
  350. player-autonav-pause
  351.  
  352.  
  353.  
  354.  
  355.  
  356. EventTarget.prototype.uhfsE = EventTarget.prototype.dispatchEvent
  357.  
  358. EventTarget.prototype.dispatchEvent = function (ev) {
  359. if (ev instanceof Event) {
  360. if (ev.type === 'video-progress') { } else {
  361. console.log(ev.type, this, ev.target)
  362. if (ev.target === null) {
  363. console.log(ev.type, (document.querySelector('ytm-app') || 0).data)
  364. console.log(ev.type, (document.querySelector('ytm-slim-owner-renderer') || 0).textContent)
  365. }
  366. }
  367. }
  368. return this.uhfsE.apply(this, arguments);
  369. }
  370.  
  371.  
  372.  
  373. */
  374.  
  375. const isMobile = location.hostname === 'm.youtube.com';
  376.  
  377. if (!isMobile && typeof AbortSignal !== 'undefined') {
  378. document.addEventListener('yt-action', () => {
  379. try {
  380. yt.config_.EXPERIMENT_FLAGS.enable_profile_cards_on_comments = true;
  381. } catch (e) { }
  382. }, { capture: true, passive: true, once: true })
  383. }
  384.  
  385.  
  386. const delayPn = delay => new Promise((fn => setTimeout(fn, delay)));
  387.  
  388. const { retrieveCE } = isMobile ? (() => {
  389.  
  390. const retrieveCE = () => {
  391. console.warn('retrieveCE is not supported');
  392. }
  393. return { retrieveCE };
  394.  
  395. })() : (() => {
  396.  
  397. const isCustomElementsProvided = typeof customElements !== "undefined" && typeof (customElements || 0).whenDefined === "function";
  398.  
  399. const promiseForCustomYtElementsReady = isCustomElementsProvided ? Promise.resolve(0) : new Promise((callback) => {
  400. const EVENT_KEY_ON_REGISTRY_READY = "ytI-ce-registry-created";
  401. if (typeof customElements === 'undefined') {
  402. if (!('__CE_registry' in document)) {
  403. // https://github.com/webcomponents/polyfills/
  404. Object.defineProperty(document, '__CE_registry', {
  405. get() {
  406. // return undefined
  407. },
  408. set(nv) {
  409. if (typeof nv == 'object') {
  410. delete this.__CE_registry;
  411. this.__CE_registry = nv;
  412. this.dispatchEvent(new CustomEvent(EVENT_KEY_ON_REGISTRY_READY));
  413. }
  414. return true;
  415. },
  416. enumerable: false,
  417. configurable: true
  418. })
  419. }
  420. let eventHandler = (evt) => {
  421. document.removeEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
  422. const f = callback;
  423. callback = null;
  424. eventHandler = null;
  425. f();
  426. };
  427. document.addEventListener(EVENT_KEY_ON_REGISTRY_READY, eventHandler, false);
  428. } else {
  429. callback();
  430. }
  431. });
  432.  
  433. const retrieveCE = async (nodeName) => {
  434. try {
  435. isCustomElementsProvided || (await promiseForCustomYtElementsReady);
  436. await customElements.whenDefined(nodeName);
  437. const dummy = document.querySelector(nodeName) || document.createElement(nodeName);
  438. const cProto = insp(dummy).constructor.prototype;
  439. return cProto;
  440. } catch (e) {
  441. console.warn(e);
  442. }
  443. }
  444.  
  445. return { retrieveCE };
  446.  
  447. })();
  448.  
  449. const cfg = {};
  450. class Mutex {
  451.  
  452. constructor() {
  453. this.p = Promise.resolve()
  454. }
  455.  
  456. /**
  457. * @param {(lockResolve: () => void)} f
  458. */
  459. lockWith(f) {
  460. this.p = this.p.then(() => new Promise(f).catch(console.warn))
  461. }
  462.  
  463. }
  464.  
  465. class OrderedMutex extends Mutex {
  466. constructor() {
  467. super();
  468. this.nextIndex = 0;
  469. this.arr = [];
  470. this.lockFunc = resolve => {
  471. if (this.arr.length >= 1) {
  472. let f = this.arr[0];
  473. if (typeof f === 'function') {
  474. this.arr.shift();
  475. if (this.nextIndex > 0) this.nextIndex--;
  476. f(resolve);
  477. return;
  478. } else {
  479. // reset if unknown error
  480. this.arr.length = 0;
  481. this.nextIndex = 0;
  482. }
  483. }
  484. resolve();
  485. };
  486. }
  487. /**
  488. * @param {(lockResolve: () => void)} f
  489. */
  490. add(f) {
  491. if (this.nextIndex === this.arr.length) {
  492. this.arr.push(f); this.nextIndex++;
  493. } else {
  494. this.arr.splice(this.nextIndex++, 0, f);
  495. }
  496. this.lockWith(this.lockFunc);
  497. }
  498. }
  499. // usage: run network request one by one
  500. const mutex = new OrderedMutex();
  501.  
  502. const createPipeline = () => {
  503. let pipelineMutex = Promise.resolve();
  504. const pipelineExecution = fn => {
  505. return new Promise((resolve, reject) => {
  506. pipelineMutex = pipelineMutex.then(async () => {
  507. let res;
  508. try {
  509. res = await fn();
  510. } catch (e) {
  511. console.log('error_F1', e);
  512. reject(e);
  513. }
  514. resolve(res);
  515. }).catch(console.warn);
  516. });
  517. };
  518. return pipelineExecution;
  519. };
  520.  
  521. /**
  522. * @typedef { AsyncValue<T> | T} AsyncOrResolvedValue<T>
  523. * @template T */
  524.  
  525. /**
  526. * usage: cache the network result per web application instance.
  527. * @type {Map<ChannelId, AsyncOrResolvedValue<DisplayNameResultObject>>} */
  528. const displayNameResStore = new Map();
  529.  
  530. /**
  531. * usage: mapping the .dataChanged() to the wrapped function - only few entries as most are {ytd-comment-renderer}.dataChanged().
  532. * @type {WeakMap<Function, Function>} */
  533. const dataChangedFuncStore = new WeakMap();
  534.  
  535. /**
  536. * usage: for \@Mention inside comments in YouTube Mobile that without hyperlinks for channelId.
  537. * @type {Map<HandleText, AsyncOrResolvedValue<ChannelId>>} */
  538. const handleToChannelId = new Map();
  539.  
  540. /**
  541. * usage: in RSS fetching, no handle text will be obtained from the response. inject the handle into the response.
  542. * @type {Map<ChannelId, ChannelIdToHandleResult>} */
  543. const channelIdToHandle = new Map();
  544.  
  545. /**
  546. * usage: replace handle to lang-specific display name
  547. * @type {Map<HandleText, DisplayName>} */
  548. const langPreferredDisplayNameMap = new Map();
  549.  
  550. /**
  551. * AsyncValue
  552. * @class
  553. * @template T
  554. */
  555. class AsyncValue {
  556. constructor() {
  557. /** @type {T | null} */
  558. this.__value__ = null;
  559. /** @type {((value: any)=>void) | null} */
  560. this.__resolve__ = null;
  561. this.__promise__ = new Promise(resolve => {
  562. this.__resolve__ = resolve;
  563. });
  564. }
  565. /**
  566. *
  567. * @param {T} value
  568. */
  569. setValue(value) {
  570. const promise = this.__promise__;
  571. if (promise === null) throw 'Value has already been set.';
  572. this.__value__ = value;
  573. this.__promise__ = null;
  574. this.__resolve__ ? this.__resolve__() : Promise.resolve().then(() => this.__resolve__());
  575. }
  576. /**
  577. *
  578. * @returns {Promise<T>}
  579. */
  580. async getValue() {
  581. const promise = this.__promise__;
  582. if (promise === null) return this.__value__;
  583. await promise; // promise become null
  584. this.__promise__ = null; // just in case
  585. this.__resolve__ = null;
  586. return this.__value__;
  587. }
  588. }
  589.  
  590. let isCSSAdded = false;
  591. const addCSSRulesIfRequired = () => {
  592. if (isCSSAdded) return;
  593. isCSSAdded = true;
  594. const style = document.createElement('style')
  595. let nonce = document.querySelector('head style[nonce]');
  596. nonce = nonce ? nonce.getAttribute('nonce') : null;
  597.  
  598. /* #contenteditable-root.yt-formatted-string:lang(he) */ /* Hebrew (LTR, when mixed with LTR languages) */
  599. /* #contenteditable-root.yt-formatted-string:lang(he) */ /* Hebrew (pure) */
  600.  
  601. const cssText01_FIX_RTL_ISSUE = FIX_RTL_ISSUE ? `
  602.  
  603. /* Left-to-Right (LTR) Languages */
  604. #contenteditable-root.yt-formatted-string:lang(en) /* English */,
  605. #contenteditable-root.yt-formatted-string:lang(es) /* Spanish */,
  606. #contenteditable-root.yt-formatted-string:lang(fr) /* French */,
  607. #contenteditable-root.yt-formatted-string:lang(de) /* German */,
  608. #contenteditable-root.yt-formatted-string:lang(it) /* Italian */,
  609. #contenteditable-root.yt-formatted-string:lang(pt) /* Portuguese */,
  610. #contenteditable-root.yt-formatted-string:lang(ru) /* Russian */,
  611. #contenteditable-root.yt-formatted-string:lang(zh) /* Chinese */,
  612. #contenteditable-root.yt-formatted-string:lang(ja) /* Japanese */,
  613. #contenteditable-root.yt-formatted-string:lang(ko) /* Korean */,
  614. #contenteditable-root.yt-formatted-string:lang(nl) /* Dutch */,
  615. #contenteditable-root.yt-formatted-string:lang(sv) /* Swedish */,
  616. #contenteditable-root.yt-formatted-string:lang(fi) /* Finnish */,
  617. #contenteditable-root.yt-formatted-string:lang(da) /* Danish */,
  618. #contenteditable-root.yt-formatted-string:lang(no) /* Norwegian */,
  619. #contenteditable-root.yt-formatted-string:lang(pl) /* Polish */,
  620. #contenteditable-root.yt-formatted-string:lang(cs) /* Czech */,
  621. #contenteditable-root.yt-formatted-string:lang(sk) /* Slovak */,
  622. #contenteditable-root.yt-formatted-string:lang(hu) /* Hungarian */,
  623. #contenteditable-root.yt-formatted-string:lang(tr) /* Turkish */,
  624. #contenteditable-root.yt-formatted-string:lang(el) /* Greek */,
  625. #contenteditable-root.yt-formatted-string:lang(id) /* Indonesian */,
  626. #contenteditable-root.yt-formatted-string:lang(ms) /* Malay */,
  627. #contenteditable-root.yt-formatted-string:lang(th) /* Thai */,
  628. #contenteditable-root.yt-formatted-string:lang(vi) /* Vietnamese */
  629. {
  630. direction: ltr;
  631. }
  632.  
  633. /* Right-to-Left (RTL) Languages */
  634. #contenteditable-root.yt-formatted-string:lang(ar) /* Arabic */,
  635. #contenteditable-root.yt-formatted-string:lang(fa) /* Persian */,
  636. #contenteditable-root.yt-formatted-string:lang(ur) /* Urdu */,
  637. #contenteditable-root.yt-formatted-string:lang(dv) /* Divehi */
  638. {
  639. direction: rtl;
  640. }
  641.  
  642. ` : '';
  643. style.textContent = `
  644. ${cssText01_FIX_RTL_ISSUE}
  645. `.trim();
  646. if (nonce) style.setAttribute('nonce', nonce);
  647. document.head.appendChild(style);
  648. }
  649.  
  650. /**
  651. *
  652. * @param {DisplayNameResultObject} resultInfo
  653. * @param {string} channelId
  654. */
  655. const cacheHandleToChannel = (resultInfo, channelId) => {
  656.  
  657. const { vanityChannelUrl, ownerUrls } = resultInfo;
  658.  
  659. let handleText = urlToHandle(vanityChannelUrl || '');
  660.  
  661. if (handleText) {
  662. // match = true;
  663. } else if ((ownerUrls || 0).length >= 1) {
  664. for (const ownerUrl of ownerUrls) {
  665. handleText = urlToHandle(ownerUrl || '');
  666.  
  667. if (handleText) {
  668. // match = true;
  669. break;
  670. }
  671. }
  672. }
  673. if (handleText) {
  674.  
  675. const asyncValue = handleToChannelId.get(handleText); // nothing if no pending promise
  676. if (asyncValue instanceof AsyncValue) {
  677. asyncValue.setValue(channelId);
  678. }
  679. handleToChannelId.set(handleText, channelId);
  680. }
  681.  
  682.  
  683. }
  684.  
  685. /**
  686. *
  687. * @param {string} channelId
  688. * @param {Function} onDownloaded
  689. * @param {Function} onResulted
  690. * @param {Function} onError
  691. */
  692. const fetcherBrowseAPI = (channelId, onDownloaded, onResulted, onError) => {
  693.  
  694. let signal = timeoutSignal(4000);
  695.  
  696. const requestBody = {
  697. "context": {
  698. "client": {
  699. "visitorData": "Cgs0aVg0VjFWM0U0USi0jvOkBg%3D%3D", // [optional] fake visitorData to avoid dynamic visitorData generated in response
  700. "clientName": "MWEB", // "WEB", "MWEB"
  701. "clientVersion": `${cfg.INNERTUBE_CLIENT_VERSION || '2.20230614.01.00'}`, // same as WEB version
  702. "originalUrl": `https://m.youtube.com/channel/${channelId}`,
  703. "playerType": "UNIPLAYER",
  704. "platform": "MOBILE", // "DESKTOP", "MOBILE", "TABLET"
  705. "clientFormFactor": "SMALL_FORM_FACTOR", // "LARGE_FORM_FACTOR", "SMALL_FORM_FACTOR"
  706. "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",
  707. "mainAppWebInfo": {
  708. "graftUrl": `/channel/${channelId}`,
  709. "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER",
  710. "isWebNativeShareAvailable": true
  711. }
  712. },
  713. "user": {
  714. "lockedSafetyMode": false
  715. },
  716. "request": {
  717. "useSsl": true,
  718. "internalExperimentFlags": [],
  719. "consistencyTokenJars": []
  720. }
  721. },
  722. "browseId": `${channelId}`
  723. };
  724.  
  725. if (USE_LANG_SPECIFIC_NAME && cfg.HL && cfg.GL) {
  726. Object.assign(requestBody.context.client, {
  727. "hl": cfg.HL,
  728. "gl": cfg.GL,
  729. });
  730. }
  731.  
  732. fetch(new Request(`/youtubei/v1/browse?key=${cfg.INNERTUBE_API_KEY}&prettyPrint=false`, {
  733. "method": "POST",
  734. "mode": "same-origin",
  735. "credentials": "omit",
  736.  
  737. referrerPolicy: "no-referrer",
  738. cache: "default", // no effect on POST request
  739. // cache: "force-cache",
  740. redirect: "error", // there shall be no redirection in this API request
  741. integrity: "",
  742. keepalive: false,
  743. signal,
  744.  
  745. "headers": {
  746. "Content-Type": "application/json", // content type of the body data in POST request
  747. "Accept-Encoding": "gzip, deflate, br", // YouTube Response - br
  748. // X-Youtube-Bootstrap-Logged-In: false,
  749. // X-Youtube-Client-Name: 1, // INNERTUBE_CONTEXT_CLIENT_NAME
  750. // X-Youtube-Client-Version: "2.20230622.06.00" // INNERTUBE_CONTEXT_CLIENT_VERSION
  751.  
  752. "Accept": "application/json",
  753. },
  754. "body": JSON.stringify(requestBody)
  755. })).then(res => {
  756. signal && signal.clearTimeout && signal.clearTimeout();
  757. signal = null;
  758. onDownloaded();
  759. onDownloaded = null;
  760. return res.json();
  761. }).then(resJson => {
  762. const resultInfo = ((resJson || 0).metadata || 0).channelMetadataRenderer;
  763. const title = resultInfo ? resultInfo.title : null;
  764. if (title || title === '') {
  765.  
  766. resultInfo.title = '';
  767. resultInfo.langTitle = title;
  768.  
  769. if (cfg.ownerProfileUrl && cfg.ownerChannelName && cfg.ownerChannelName !== title) {
  770. let matched = false;
  771.  
  772. const ownerProfileUrlLowerCase = cfg.ownerProfileUrl.toLowerCase();
  773. if (ownerProfileUrlLowerCase.endsWith(`/channel/${channelId.toLowerCase()}`)) matched = true;
  774. else {
  775.  
  776. for (const ownerUrl of resultInfo.ownerUrls) {
  777. if (ownerUrl.toLowerCase().endsWith(ownerProfileUrlLowerCase)) {
  778. matched = true;
  779. break;
  780. }
  781. }
  782.  
  783. }
  784.  
  785. if (matched) {
  786. resultInfo.title = cfg.ownerChannelName;
  787. }
  788. }
  789.  
  790. }
  791.  
  792. onResulted(resultInfo);
  793. onResulted = null;
  794. }).catch(onError);
  795.  
  796. };
  797.  
  798. /**
  799. *
  800. * @param {string} channelId
  801. * @param {Function} onDownloaded
  802. * @param {Function} onResulted
  803. * @param {Function} onError
  804. */
  805. let fetchRSSFailed = false;
  806. const fetcherRSS = location.origin !== 'https://www.youtube.com' ? null : (channelId, onDownloaded, onResulted, onError) => {
  807.  
  808. let signal = timeoutSignal(4000);
  809. fetch(`https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`, {
  810. // YouTube RSS Response - public, max-age=900
  811.  
  812. "method": "GET",
  813. "mode": "same-origin",
  814. "credentials": "omit",
  815.  
  816. referrerPolicy: "no-referrer",
  817. cache: "default",
  818. // cache: "no-cache",
  819. redirect: "error", // there shall be no redirection in this API request
  820. integrity: "",
  821. keepalive: false,
  822. signal,
  823.  
  824. "headers": {
  825. "Cache-Control": "public, max-age=900, stale-while-revalidate=1800",
  826. // refer "Cache-Control Use Case Examples" in https://www.koyeb.com/blog/using-cache-control-and-cdns-to-improve-performance-and-reduce-latency
  827. // seems YouTube RSS Feeds server insists its own Cache-Control.
  828.  
  829. // "Content-Type": "text/xml; charset=UTF-8",
  830. "Accept-Encoding": "gzip, deflate, br", // YouTube Response - gzip
  831. // X-Youtube-Bootstrap-Logged-In: false,
  832. // X-Youtube-Client-Name: 1, // INNERTUBE_CONTEXT_CLIENT_NAME
  833. // X-Youtube-Client-Version: "2.20230622.06.00" // INNERTUBE_CONTEXT_CLIENT_VERSION
  834.  
  835. "Accept": "text/xml",
  836. }
  837. }).then(res => {
  838.  
  839. signal && signal.clearTimeout && signal.clearTimeout();
  840. signal = null;
  841.  
  842. if (res.ok === true && res.redirected === false && res.status === 200 && res.type === "basic") {
  843.  
  844. onDownloaded();
  845. onDownloaded = null;
  846. return res.text();
  847.  
  848. }
  849. return null;
  850.  
  851. }).then(resText => {
  852. if (resText && typeof resText === 'string' && resText.includes('<feed') && !resText.includes('<!DOCTYPE')) {
  853.  
  854. } else {
  855. if (onDownloaded) {
  856.  
  857. onDownloaded();
  858. onDownloaded = null;
  859. }
  860. onResulted(-1);
  861. return;
  862.  
  863. }
  864.  
  865.  
  866. // console.log(124, , resText)
  867. let eIdx = resText.indexOf('<entry>');
  868. let mText = (eIdx > 0) ? `${resText.substring(0, eIdx).trim()}</feed>` : resText;
  869.  
  870. // simple: https://www.youtube.com/feeds/videos.xml?channel_id=UC-MUu72gixoYlhV5_mWa36g
  871. // standard: https://www.youtube.com/feeds/videos.xml?channel_id=UCGSfK5HeiIhuFfXoKE47TzA
  872. // long: https://www.youtube.com/feeds/videos.xml?channel_id=UC8cSGjKxDuh2mWG1hDOTdBg
  873. // special: http://www.youtube.com/feeds/videos.xml?channel_id=UCmZ2-GUxmdWFKfXA5IN0x-w
  874.  
  875.  
  876. let name, uri, mt;
  877.  
  878. // ===============================================================
  879.  
  880. /* removed in 2023.06.30 */
  881.  
  882.  
  883. // ===============================================================
  884.  
  885.  
  886. const wIdx1 = mText.indexOf('<author>');
  887. const wIdx2 = wIdx1 > 0 ? mText.indexOf('</author>', wIdx1 + 8) : -1;
  888.  
  889. if (wIdx1 > 0 && wIdx2 > 0) {
  890. let qText = mText.substring(wIdx1, wIdx2 + '</author>'.length);
  891. const template = document.createElement('template');
  892. template.innerHTML = createHTML(qText);
  893. /** @type {HTMLElement | null} */
  894. let authorChild = ((template.content || 0).firstElementChild || 0).firstElementChild;
  895. for (; authorChild !== null; authorChild = authorChild.nextElementSibling) {
  896. const nodeName = authorChild.nodeName;
  897. if (nodeName === 'NAME') name = authorChild.textContent;
  898. else if (nodeName === 'URI') {
  899. uri = authorChild.textContent;
  900. mt = obtainChannelId(uri);
  901. }
  902. }
  903. template.innerHTML = createHTML('');
  904. }
  905.  
  906. if (!name && !uri) {
  907.  
  908. // fallback
  909.  
  910. const parser = new DOMParser();
  911. const xmlDoc = parser.parseFromString(mText, "text/xml");
  912.  
  913. // const author = xmlDoc.querySelector("author");
  914.  
  915. // https://extendsclass.com/xpath-tester.html
  916.  
  917. const authorChilds = xmlDoc.evaluate("//*[name()='author']/*", xmlDoc, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
  918.  
  919. let authorChild;
  920. while ((authorChild = authorChilds.iterateNext()) !== null) {
  921. if (authorChild.nodeName === 'name') name = authorChild.textContent;
  922. else if (authorChild.nodeName === 'uri') {
  923. uri = authorChild.textContent;
  924. mt = obtainChannelId(uri);
  925. }
  926. }
  927.  
  928. try {
  929. xmlDoc.firstChild.remove();
  930. } catch (e) { }
  931.  
  932. }
  933.  
  934.  
  935.  
  936.  
  937. let res = null;
  938.  
  939. if ((name || name === '') && uri && mt && mt === channelId) {
  940.  
  941.  
  942. const handleObj = channelIdToHandle.get(mt);
  943.  
  944. if (handleObj) {
  945.  
  946. const handleText = handleObj.handleText;
  947.  
  948. if (handleText === name) channelIdToHandle.delete(mt);
  949. else {
  950. handleObj.justPossible = false; // ignore @handle checking
  951.  
  952. res = {
  953. "title": name,
  954. "externalId": mt,
  955. "ownerUrls": [
  956. `http://www.youtube.com/${handleText}`
  957. ],
  958. "channelUrl": uri,
  959. "vanityChannelUrl": `http://www.youtube.com/${handleText}`
  960. };
  961.  
  962. if (USE_LANG_SPECIFIC_NAME) {
  963. const langPreferredDisplayName = langPreferredDisplayNameMap.get(handleText);
  964. if (langPreferredDisplayName && name !== langPreferredDisplayName) res.langTitle = langPreferredDisplayName;
  965. }
  966.  
  967. }
  968. }
  969. }
  970.  
  971. onResulted(res);
  972. onResulted = null;
  973.  
  974. // let resultInfo = ((res || 0).metadata || 0).channelMetadataRenderer;
  975. // onResulted(resultInfo);
  976. }).catch(onError);
  977.  
  978.  
  979.  
  980. }
  981.  
  982. sessionStorage.setItem('js-yt-usernames', '');
  983. let isFirstStackNewRequestSuccess = true;
  984. /**
  985. *
  986. * @param {string} channelId
  987. */
  988. function stackNewRequest(channelId) {
  989.  
  990. mutex.add(lockResolve => {
  991.  
  992. let bResult = displayNameResStore.get(channelId);
  993. if (!(bResult instanceof AsyncValue)) {
  994. // resolved or removed
  995. lockResolve(); lockResolve = null;
  996. return;
  997. }
  998.  
  999. let setResult = (result) => {
  1000. setResult = null;
  1001. if (!result) {
  1002. bResult.fetchingState = 0;
  1003. bResult.setValue(null);
  1004. displayNameResStore.delete(channelId); // create another network response in the next request
  1005. } else {
  1006. bResult.fetchingState = 4;
  1007. bResult.setValue(result);
  1008. displayNameResStore.set(channelId, result); // update store result to resolved value
  1009. }
  1010. bResult = null;
  1011. }
  1012.  
  1013. if (bResult.fetchingState >= 2) { // fetchingState == 3 or 4
  1014. // request is already done. no need to stack any request
  1015. lockResolve(); lockResolve = null;
  1016. return;
  1017. }
  1018.  
  1019. if (!document.querySelector(`[jkrgy="${channelId}"]`) && !skipElementCheck.has(channelId)) {
  1020. // element has already been removed
  1021. lockResolve(); lockResolve = null;
  1022. setResult(null);
  1023. return;
  1024. }
  1025.  
  1026. //INNERTUBE_API_KEY = ytcfg.data_.INNERTUBE_API_KEY
  1027.  
  1028. bResult.fetchingState = 2;
  1029.  
  1030.  
  1031. const retry = () => {
  1032.  
  1033. fetchRSSFailed = true;
  1034.  
  1035. bResult.fetchingState = 2;
  1036.  
  1037. // Promise.resolve(channelId).then(stackNewRequest);
  1038. fetcherBrowseAPI(channelId, mobj.fetchOnDownload, mobj.fetchOnSuccess, mobj.fetchOnFail);
  1039. }
  1040.  
  1041. const mobj = {
  1042. fetchOnDownload: () => {
  1043. bResult.fetchingState = 3;
  1044. },
  1045. fetchOnSuccess: resultInfo => {
  1046.  
  1047. if (resultInfo === -1) {
  1048. retry();
  1049. return;
  1050. }
  1051.  
  1052. lockResolve && lockResolve();
  1053. lockResolve = null;
  1054. // console.log(900)
  1055.  
  1056. if (!resultInfo) {
  1057. // invalid json format
  1058. setResult(null);
  1059. return;
  1060. }
  1061.  
  1062. const titleForDisplay = resultInfo.langTitle || resultInfo.title;
  1063. const handleObj = channelIdToHandle.get(channelId);
  1064. const handleId = handleObj ? handleObj.handleText : '';
  1065. if (IGNORE_NO_NAME && !titleForDisplay) {
  1066. if (handleId) {
  1067. resultInfo.langTitle = resultInfo.title = handleObj.handleText;
  1068. } else {
  1069. setResult(null);
  1070. return;
  1071. }
  1072. }
  1073.  
  1074. cacheHandleToChannel(resultInfo, channelId);
  1075.  
  1076. const { title, langTitle, externalId, ownerUrls, channelUrl, vanityChannelUrl } = resultInfo;
  1077.  
  1078. const displayNameRes = { title, langTitle, externalId, ownerUrls, channelUrl, vanityChannelUrl, verified123: false };
  1079.  
  1080. if (handleId) {
  1081. if (isFirstStackNewRequestSuccess) {
  1082. isFirstStackNewRequestSuccess = false;
  1083. sessionStorage.setItem('js-yt-usernames', `${handleId}\t${titleForDisplay}`);
  1084. } else {
  1085. let current = sessionStorage.getItem('js-yt-usernames') || '';
  1086. if (current.length > 900000) {
  1087. const na = current.split('\n');
  1088. const n3 = Math.floor(na.length / 3);
  1089. if (n3 > 2) {
  1090. const newText = na.slice(0, n3).concat(na.slice(na.length - n3)).join('\n');
  1091. if (newText.length < 720000) {
  1092. current = newText;
  1093. }
  1094. }
  1095. }
  1096. sessionStorage.setItem('js-yt-usernames', `${current}\n${handleId}\t${titleForDisplay}`);
  1097. }
  1098. }
  1099.  
  1100. setResult(displayNameRes);
  1101. },
  1102. fetchOnFail: e => {
  1103.  
  1104.  
  1105. console.warn(e);
  1106. if (mobj.fetcher === fetcherRSS) {
  1107. retry();
  1108.  
  1109. } else {
  1110.  
  1111. lockResolve && lockResolve();
  1112. lockResolve = null;
  1113. // console.warn(e);
  1114. setResult && setResult(null);
  1115. }
  1116. }
  1117. }
  1118.  
  1119.  
  1120. // note: when setResult(null), the fetch will be requested again if the same username appears. (multiple occurrences)
  1121. // consider the network problem might be fixed in the 2nd attempt, the name will be changed in the 2nd attempt but ignore 1st attempt.
  1122. mobj.fetcher = USE_RSS_FETCHER && fetcherRSS && !fetchRSSFailed && channelIdToHandle.has(channelId) ? fetcherRSS : fetcherBrowseAPI;
  1123. mobj.fetcher(channelId, mobj.fetchOnDownload, mobj.fetchOnSuccess, mobj.fetchOnFail);
  1124.  
  1125. });
  1126.  
  1127. }
  1128.  
  1129. /**
  1130. *
  1131. * @param {string} url Example: _www\.youtube\.com/\@yr__kd_
  1132. * @returns Example: _\@yr__kd_
  1133. */
  1134. function urlToHandle(url) {
  1135.  
  1136. if (typeof url !== 'string') return '';
  1137. let i = url.indexOf('.youtube.com/');
  1138. if (i >= 1) url = url.substring(i + '.youtube.com/'.length);
  1139. else if (url.charAt(0) === '/') url = url.substring(1);
  1140. return isDisplayAsHandle(url) ? url : '';
  1141.  
  1142. }
  1143.  
  1144. const skipElementCheck = new Set(); // username in reply quote (editable)
  1145.  
  1146. /**
  1147. *
  1148. * @param {string} channelId Example: UC0gmRdmpDWJ4dt7DAeRaawA
  1149. * @returns {Promise<DisplayNameResultObject | null>}
  1150. */
  1151. async function getDisplayName(channelId) {
  1152.  
  1153. let cache = displayNameResStore.get(channelId);
  1154. let isStackNewRequest = false;
  1155.  
  1156. /** @type {AsyncValue<DisplayNameResultObject> | null | undefined} */
  1157. let aResult;
  1158.  
  1159. if (cache instanceof AsyncValue) {
  1160. aResult = cache;
  1161. if (aResult.fetchingState >= 2) {
  1162. // aResult.fetchingState
  1163. // 2: network fetch started
  1164. // 3: network fetch ended
  1165. // isStackNewRequest = false;
  1166. } else {
  1167. // 0: invalid before
  1168. // 1: scheduled but not yet fetch
  1169. isStackNewRequest = true;
  1170. }
  1171. } else if (cache) {
  1172. // resolved result
  1173. return cache;
  1174. } else {
  1175. // else no cached value found
  1176. /** @type {AsyncValue<DisplayNameResultObject>} */
  1177. aResult = new AsyncValue();
  1178. displayNameResStore.set(channelId, aResult);
  1179. isStackNewRequest = true;
  1180. }
  1181.  
  1182. if (isStackNewRequest) {
  1183. aResult.fetchingState = 1;
  1184. stackNewRequest(channelId);
  1185. }
  1186. const result = await aResult.getValue();
  1187. return result;
  1188. }
  1189.  
  1190.  
  1191. /**
  1192. *
  1193. * @param {string} href Example: https\:\/\/www\.youtube\.com/channel/UC0gmRdmpDWJ4dt7DAeRaawA
  1194. * @returns
  1195. * * Outdated?
  1196. */
  1197. const obtainChannelId = (href) => {
  1198. const s = `/${href}`;
  1199. if (s.length >= 33) {
  1200. const m = /\/channel\/(UC[-_a-zA-Z0-9+=.]{22})(\/|$)/.exec(s);
  1201. return !m ? '' : (m[1] || '');
  1202. }
  1203. return '';
  1204. };
  1205.  
  1206. const isUCBrowserId = (browseId) => {
  1207. return typeof browseId === 'string' && browseId.length === 24 && browseId.startsWith('UC') && /^UC[-_a-zA-Z0-9+=.]{22}$/.test(browseId);
  1208. }
  1209.  
  1210. const browseEndpointBaseUrlType = (browseEndpoint, displayText) => {
  1211. if (isDisplayAsHandle(displayText) && isUCBrowserId(browseEndpoint.browseId) && typeof browseEndpoint.canonicalBaseUrl === 'string') {
  1212. if (`/${displayText}` === browseEndpoint.canonicalBaseUrl) return 1 | 2;
  1213. if (`/channel/${browseEndpoint.browseId}` === browseEndpoint.canonicalBaseUrl) return 1 | 4;
  1214. if (browseEndpoint.canonicalBaseUrl.includes(`/${browseEndpoint.browseId}`)) return 1 | 8;
  1215. return 1;
  1216. }
  1217. return 0;
  1218. }
  1219.  
  1220. /**
  1221. *
  1222. * @param {Element} ytElm
  1223. */
  1224. const resetWhenDataChanged = (ths) => {
  1225. const cnt = insp(ths);
  1226. const ytElm = cnt.hostElement instanceof Element ? cnt.hostElement : cnt instanceof Element ? cnt : ths;
  1227. if (ytElm instanceof Element) {
  1228. const anchors = elementQSA(ytElm, 'a[id][href][jkrgy]');
  1229. // const anchors = elementQSA(ytElm, 'a[id][href*="channel/"][jkrgy]');
  1230. if ((anchors || 0).length >= 1 && (insp(ytElm).data || 0).jkrgx !== 1) {
  1231. for (const anchor of anchors) {
  1232. anchor.removeAttribute('jkrgy');
  1233. }
  1234. }
  1235. }
  1236. }
  1237.  
  1238. /**
  1239. *
  1240. * @param {Element} ytElm
  1241. */
  1242. const resetWhenPropChanged = (ths) => {
  1243. const cnt = insp(ths);
  1244. const ytElm = cnt.hostElement instanceof Element ? cnt.hostElement : cnt instanceof Element ? cnt : ths;
  1245. if (ytElm instanceof Element) {
  1246. const anchors = elementQSA(ytElm, 'a[id][href][jkrgy]');
  1247. // const anchors = elementQSA(ytElm, 'a[id][href*="channel/"][jkrgy]');
  1248. if ((anchors || 0).length >= 1 && ((insp(ytElm).commentEntity || 0).author || 0).jkrgx !== 1) {
  1249. for (const anchor of anchors) {
  1250. anchor.removeAttribute('jkrgy');
  1251. }
  1252. }
  1253. }
  1254. }
  1255.  
  1256. /**
  1257. *
  1258. * @param {HTMLAnchorElement} anchor Example: https\:\/\/www\.youtube\.com/channel/UCRmLncxsQFcOOC8OhzUIfxQ/videos
  1259. * @param {string} hrefAttribute Example: /channel/UCRmLncxsQFcOOC8OhzUIfxQ (old) 2024 Feb -> /@user-abc1234
  1260. * @param {string} channelId Example: UCRmLncxsQFcOOC8OhzUIfxQ
  1261. * @returns
  1262. */
  1263. const anchorIntegrityCheck = (anchor, hrefAttribute) => {
  1264.  
  1265. let currentHref = anchor.getAttribute('href');
  1266. if (currentHref === hrefAttribute) return true; // /channel/UCRmLncxsQFcOOC8OhzUIfxQ // /channel/UCRmLncxsQFcOOC8OhzUIfxQ
  1267.  
  1268. return (currentHref + '/').indexOf(hrefAttribute + '/') >= 0;
  1269.  
  1270. };
  1271.  
  1272. /**
  1273. *
  1274. * @param {string} currentDisplayed
  1275. * @param {DisplayNameResultObject} fetchResult
  1276. * @returns
  1277. */
  1278.  
  1279. const urlTestTwice = (m, a, b) => {
  1280. return m.endsWith(a) || (a === b ? false : m.endsWith(b));
  1281. };
  1282. const verifyAndConvertHandle = (currentDisplayed, fetchResult) => {
  1283.  
  1284. const { title, langTitle, externalId, ownerUrls, channelUrl, vanityChannelUrl, verified123 } = fetchResult;
  1285.  
  1286. const currentDisplayTrimmed = currentDisplayed.trim(); // @IORIMATSUNAGA
  1287. const currentDisplayTrimmedLowerCase = currentDisplayTrimmed.toLowerCase(); // @iorimatsunaga
  1288. const currentDisplayTrimmedLowerCaseForURL = encodeURI(currentDisplayTrimmedLowerCase);
  1289. let match = false;
  1290. if (verified123) {
  1291. match = true;
  1292. } else if (urlTestTwice((vanityChannelUrl || '').toLowerCase(), `/${currentDisplayTrimmedLowerCaseForURL}`, `/${currentDisplayTrimmedLowerCase}`)) {
  1293. match = true;
  1294. } else if ((ownerUrls || 0).length >= 1) {
  1295. for (const ownerUrl of ownerUrls) {
  1296. if (urlTestTwice((ownerUrl || '').toLowerCase(), `/${currentDisplayTrimmedLowerCaseForURL}`, `/${currentDisplayTrimmedLowerCase}`)) {
  1297. match = true;
  1298. break;
  1299. }
  1300. }
  1301. }
  1302. if (match) {
  1303. return currentDisplayTrimmed;
  1304. }
  1305. return '';
  1306.  
  1307. };
  1308.  
  1309. /**
  1310. *
  1311. *
  1312.  
  1313. ### [Handle naming guidelines](https://support.google.com/youtube/answer/11585688?hl=en&co=GENIE.Platform%3DAndroid)
  1314.  
  1315. - Is between 3-30 characters
  1316. - Is made up of alphanumeric characters (A–Z, a–z, 0–9)
  1317. - Your handle can also include: underscores (_), hyphens (-), dots (.)
  1318. - Is not URL-like or phone number-like
  1319. - Is not already being used
  1320. - Follows YouTube's Community Guidelines
  1321.  
  1322. ### Handle automatically generated by YouTube
  1323. - `@user-XXXX`
  1324. - without dot (.)
  1325.  
  1326.  
  1327. ## Intl. Language Support
  1328. #### Since May~June 2024
  1329.  
  1330. - Before: Is made up of alphanumeric characters (A–Z, a–z, 0–9)
  1331.  
  1332. - After: Uses alphabet letters or numbers from one of our 75 supported languages
  1333. - Your handle can also include: underscores (_), hyphens (-), periods (.), Latin middle dots (·).
  1334. - Mixed scripts are only allowed in certain circumstances.
  1335. - 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.
  1336.  
  1337.  
  1338.  
  1339.  
  1340. *
  1341. * @param {string} text
  1342. * @returns
  1343. */
  1344. const isDisplayAsHandle = (text) => {
  1345. if (typeof text !== 'string') return false;
  1346. const n = text.length;
  1347. if (n <= NUM_CHANNEL_ID_MIN_LEN_old1) {
  1348. if (n < NUM_CHANNEL_ID_MIN_LEN_1) return false; // @
  1349. // @巡空 -> <1, 2>
  1350. // @abc -> <4, 0>
  1351. const textFullL = text.replace(/[\u20-\uFF]+/g, '').length;
  1352. const textHalfL = n - textFullL;
  1353. if (textHalfL < 1) return false;
  1354. if (textFullL === 0 && textHalfL < 4) return false;
  1355. }
  1356. if (text.indexOf('@') < 0) return false;
  1357. return exactHandleText(text.trim(), true);
  1358. };
  1359.  
  1360. const isValidMinSlashHandle = (text) => {
  1361. return text && typeof text === 'string' && text.length >= NUM_CHANNEL_ID_MIN_LEN_2 && text.startsWith('/@');
  1362. }
  1363.  
  1364.  
  1365. const languageMapR0 = [];
  1366.  
  1367. const languageMap = [
  1368. { range: [0x0041, 0x005A], langs: ['English', 'Afrikaans', 'Albanian', 'Bosnian', 'Catalan', 'Croatian', 'Czech', 'Danish', 'Dutch', 'Estonian', 'Filipino', 'Finnish', 'French', 'Galician', 'German', 'Hungarian', 'Icelandic', 'Indonesian', 'Italian', 'Latvian', 'Lithuanian', 'Macedonian', 'Malay', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Slovak', 'Slovenian', 'Spanish', 'Swedish', 'Swahili', 'Turkish', 'Zulu'] }, // Basic Latin (A-Z)
  1369. { range: [0x0061, 0x007A], langs: ['English', 'Afrikaans', 'Albanian', 'Bosnian', 'Catalan', 'Croatian', 'Czech', 'Danish', 'Dutch', 'Estonian', 'Filipino', 'Finnish', 'French', 'Galician', 'German', 'Hungarian', 'Icelandic', 'Indonesian', 'Italian', 'Latvian', 'Lithuanian', 'Macedonian', 'Malay', 'Norwegian', 'Polish', 'Portuguese', 'Romanian', 'Slovak', 'Slovenian', 'Spanish', 'Swedish', 'Swahili', 'Turkish', 'Zulu'] }, // Basic Latin (a-z)
  1370. { range: [0x00C0, 0x00FF], langs: ['French', 'Portuguese', 'Spanish', 'Afrikaans'] }, // Latin-1 Supplement
  1371. { range: [0x0100, 0x017F], langs: ['Latvian', 'Lithuanian', 'Polish', 'Czech', 'Slovak', 'Slovenian', 'Croatian', 'Bosnian', 'Serbian', 'Montenegrin'] }, // Latin Extended-A
  1372. { range: [0x0180, 0x024F], langs: ['African languages', 'Esperanto'] }, // Latin Extended-B
  1373. { range: [0x0370, 0x03FF], langs: ['Greek'] }, // Greek and Coptic
  1374. { range: [0x0400, 0x04FF], langs: ['Russian', 'Ukrainian', 'Bulgarian', 'Serbian', 'Macedonian', 'Belarusian', 'Kazakh', 'Kyrgyz', 'Uzbek'] }, // Cyrillic
  1375. { range: [0x0500, 0x052F], langs: ['Kazakh', 'Kyrgyz', 'Uzbek'] }, // Cyrillic Supplement
  1376. { range: [0x0530, 0x058F], langs: ['Armenian'] }, // Armenian
  1377. { range: [0x0590, 0x05FF], langs: ['Hebrew'] }, // Hebrew
  1378. { range: [0x0600, 0x06FF], langs: ['Arabic', 'Persian', 'Urdu'] }, // Arabic
  1379. { range: [0x0700, 0x074F], langs: ['Syriac'] }, // Syriac
  1380. { range: [0x0750, 0x077F], langs: ['Arabic Supplement'] }, // Arabic Supplement
  1381. { range: [0x0780, 0x07BF], langs: ['Thaana'] }, // Thaana
  1382. { range: [0x0900, 0x097F], langs: ['Hindi', 'Marathi', 'Sanskrit', 'Nepali'] }, // Devanagari
  1383. { range: [0x0980, 0x09FF], langs: ['Bengali', 'Assamese'] }, // Bengali
  1384. { range: [0x0A00, 0x0A7F], langs: ['Punjabi'] }, // Gurmukhi
  1385. { range: [0x0A80, 0x0AFF], langs: ['Gujarati'] }, // Gujarati
  1386. { range: [0x0B00, 0x0B7F], langs: ['Odia'] }, // Oriya
  1387. { range: [0x0B80, 0x0BFF], langs: ['Tamil'] }, // Tamil
  1388. { range: [0x0C00, 0x0C7F], langs: ['Telugu'] }, // Telugu
  1389. { range: [0x0C80, 0x0CFF], langs: ['Kannada'] }, // Kannada
  1390. { range: [0x0D00, 0x0D7F], langs: ['Malayalam'] }, // Malayalam
  1391. { range: [0x0D80, 0x0DFF], langs: ['Sinhala'] }, // Sinhala
  1392. { range: [0x0E00, 0x0E7F], langs: ['Thai'] }, // Thai
  1393. { range: [0x0E80, 0x0EFF], langs: ['Lao'] }, // Lao
  1394. { range: [0x0F00, 0x0FFF], langs: ['Tibetan'] }, // Tibetan
  1395. { range: [0x1000, 0x109F], langs: ['Burmese'] }, // Myanmar
  1396. { range: [0x10A0, 0x10FF], langs: ['Georgian'] }, // Georgian
  1397. { range: [0x1100, 0x11FF], langs: ['Korean'] }, // Hangul Jamo
  1398. { range: [0x1200, 0x137F], langs: ['Amharic', 'Tigrinya', 'Geez'] }, // Ethiopic
  1399. { range: [0x13A0, 0x13FF], langs: ['Cherokee'] }, // Cherokee
  1400. { range: [0x1400, 0x167F], langs: ['Canadian Aboriginal'] }, // Unified Canadian Aboriginal Syllabics
  1401. { range: [0x1680, 0x169F], langs: ['Ogham'] }, // Ogham
  1402. { range: [0x16A0, 0x16FF], langs: ['Runic'] }, // Runic
  1403. { range: [0x1700, 0x171F], langs: ['Tagalog'] }, // Tagalog
  1404. { range: [0x1720, 0x173F], langs: ['Hanunoo'] }, // Hanunoo
  1405. { range: [0x1740, 0x175F], langs: ['Buhid'] }, // Buhid
  1406. { range: [0x1760, 0x177F], langs: ['Tagbanwa'] }, // Tagbanwa
  1407. { range: [0x1780, 0x17FF], langs: ['Khmer'] }, // Khmer
  1408. { range: [0x1800, 0x18AF], langs: ['Mongolian'] }, // Mongolian
  1409. { range: [0x1E00, 0x1EFF], langs: ['Latin Extended Additional'] }, // Latin Extended Additional
  1410. { range: [0x1F00, 0x1FFF], langs: ['Greek Extended'] }, // Greek Extended
  1411. { range: [0x2000, 0x206F], langs: ['General Punctuation'] }, // General Punctuation
  1412. { range: [0x2070, 0x209F], langs: ['Superscripts and Subscripts'] }, // Superscripts and Subscripts
  1413. { range: [0x20A0, 0x20CF], langs: ['Currency Symbols'] }, // Currency Symbols
  1414. { range: [0x2100, 0x214F], langs: ['Letterlike Symbols'] }, // Letterlike Symbols
  1415. { range: [0x2150, 0x218F], langs: ['Number Forms'] }, // Number Forms
  1416. { range: [0x2190, 0x21FF], langs: ['Arrows'] }, // Arrows
  1417. { range: [0x2200, 0x22FF], langs: ['Mathematical Operators'] }, // Mathematical Operators
  1418. { range: [0x2300, 0x23FF], langs: ['Miscellaneous Technical'] }, // Miscellaneous Technical
  1419. { range: [0x2400, 0x243F], langs: ['Control Pictures'] }, // Control Pictures
  1420. { range: [0x2440, 0x245F], langs: ['Optical Character Recognition'] }, // Optical Character Recognition
  1421. { range: [0x2460, 0x24FF], langs: ['Enclosed Alphanumerics'] }, // Enclosed Alphanumerics
  1422. { range: [0x2500, 0x257F], langs: ['Box Drawing'] }, // Box Drawing
  1423. { range: [0x2580, 0x259F], langs: ['Block Elements'] }, // Block Elements
  1424. { range: [0x25A0, 0x25FF], langs: ['Geometric Shapes'] }, // Geometric Shapes
  1425. { range: [0x2600, 0x26FF], langs: ['Miscellaneous Symbols'] }, // Miscellaneous Symbols
  1426. { range: [0x2700, 0x27BF], langs: ['Dingbats'] }, // Dingbats
  1427.  
  1428. { range: [0x3000, 0x303F], langs: ['Chinese (Simplified)', 'Chinese (Traditional)', 'Japanese', 'Korean'] },
  1429.  
  1430. // { range: [0x3000, 0x303F], langs: ['CJK Symbols and Punctuation'] }, // CJK Symbols and Punctuation
  1431. { range: [0x3040, 0x309F], langs: ['Japanese'] }, // Hiragana
  1432. { range: [0x30A0, 0x30FF], langs: ['Japanese'] }, // Katakana
  1433. { range: [0x3100, 0x312F], langs: ['Bopomofo'] }, // Bopomofo
  1434. { range: [0x3130, 0x318F], langs: ['Korean'] }, // Hangul Compatibility Jamo
  1435. { range: [0x3190, 0x319F], langs: ['Kanbun'] }, // Kanbun
  1436. { range: [0x31A0, 0x31BF], langs: ['Bopomofo Extended'] }, // Bopomofo Extended
  1437. { range: [0x31F0, 0x31FF], langs: ['Katakana Phonetic Extensions'] }, // Katakana Phonetic Extensions
  1438. { range: [0x3200, 0x32FF], langs: ['Enclosed CJK Letters and Months'] }, // Enclosed CJK Letters and Months
  1439. { range: [0x3300, 0x33FF], langs: ['CJK Compatibility'] }, // CJK Compatibility
  1440. { range: [0x4E00, 0x9FFF], langs: ['Chinese (Simplified)', 'Chinese (Traditional)', 'Japanese', 'Korean'] }, // CJK Unified Ideographs
  1441. { range: [0xAC00, 0xD7AF], langs: ['Korean'] }, // Hangul Syllables
  1442. { range: [0xD800, 0xDBFF], langs: ['High Surrogates'] }, // High Surrogates
  1443. { range: [0xDC00, 0xDFFF], langs: ['Low Surrogates'] }, // Low Surrogates
  1444. { range: [0xF900, 0xFAFF], langs: ['CJK Compatibility Ideographs'] }, // CJK Compatibility Ideographs
  1445. { range: [0xFB00, 0xFB4F], langs: ['Alphabetic Presentation Forms'] }, // Alphabetic Presentation Forms
  1446. { range: [0xFB50, 0xFDFF], langs: ['Arabic Presentation Forms-A'] }, // Arabic Presentation Forms-A
  1447. { range: [0xFE20, 0xFE2F], langs: ['Combining Half Marks'] }, // Combining Half Marks
  1448. { range: [0xFE70, 0xFEFF], langs: ['Arabic Presentation Forms-B'] }, // Arabic Presentation Forms-B
  1449. { range: [0xFF00, 0xFFEF], langs: ['Halfwidth and Fullwidth Forms'] }, // Halfwidth and Fullwidth Forms
  1450. { range: [0xFFF0, 0xFFFF], langs: ['Specials'] }, // Specials
  1451. { range: [0x10000, 0x1007F], langs: ['Linear B Syllabary'] }, // Linear B Syllabary
  1452. { range: [0x10080, 0x100FF], langs: ['Linear B Ideograms'] }, // Linear B Ideograms
  1453. { range: [0x10100, 0x1013F], langs: ['Aegean Numbers'] }, // Aegean Numbers
  1454. { range: [0x10140, 0x1018F], langs: ['Ancient Greek Numbers'] }, // Ancient Greek Numbers
  1455. { range: [0x10190, 0x101CF], langs: ['Ancient Symbols'] }, // Ancient Symbols
  1456. { range: [0x101D0, 0x101FF], langs: ['Phaistos Disc'] }, // Phaistos Disc
  1457. { range: [0x10280, 0x1029F], langs: ['Lycian'] }, // Lycian
  1458. { range: [0x102A0, 0x102DF], langs: ['Carian'] }, // Carian
  1459. { range: [0x10300, 0x1032F], langs: ['Old Italic'] }, // Old Italic
  1460. { range: [0x10330, 0x1034F], langs: ['Gothic'] }, // Gothic
  1461. { range: [0x10380, 0x1039F], langs: ['Ugaritic'] }, // Ugaritic
  1462. { range: [0x103A0, 0x103DF], langs: ['Old Persian'] }, // Old Persian
  1463. { range: [0x10400, 0x1044F], langs: ['Deseret'] }, // Deseret
  1464. { range: [0x10450, 0x1047F], langs: ['Shavian'] }, // Shavian
  1465. { range: [0x10480, 0x104AF], langs: ['Osmanya'] }, // Osmanya
  1466. { range: [0x10800, 0x1083F], langs: ['Cypriot Syllabary'] }, // Cypriot Syllabary
  1467. { range: [0x10840, 0x1085F], langs: ['Imperial Aramaic'] }, // Imperial Aramaic
  1468. { range: [0x10900, 0x1091F], langs: ['Phoenician'] }, // Phoenician
  1469. { range: [0x10920, 0x1093F], langs: ['Lydian'] }, // Lydian
  1470. { range: [0x10A00, 0x10A5F], langs: ['Kharoshthi'] }, // Kharoshthi
  1471. { range: [0x10A60, 0x10A7F], langs: ['Old South Arabian'] }, // Old South Arabian
  1472. { range: [0x10B00, 0x10B3F], langs: ['Avestan'] }, // Avestan
  1473. { range: [0x10B40, 0x10B5F], langs: ['Inscriptional Parthian'] }, // Inscriptional Parthian
  1474. { range: [0x10B60, 0x10B7F], langs: ['Inscriptional Pahlavi'] }, // Inscriptional Pahlavi
  1475. { range: [0x10C00, 0x10C4F], langs: ['Old Turkic'] }, // Old Turkic
  1476. { range: [0x10E60, 0x10E7F], langs: ['Rumi Numeral Symbols'] }, // Rumi Numeral Symbols
  1477. { range: [0x11000, 0x1107F], langs: ['Brahmi'] }, // Brahmi
  1478. { range: [0x11080, 0x110CF], langs: ['Kaithi'] }, // Kaithi
  1479. { range: [0x12000, 0x123FF], langs: ['Cuneiform'] }, // Cuneiform
  1480. { range: [0x13000, 0x1342F], langs: ['Egyptian Hieroglyphs'] }, // Egyptian Hieroglyphs
  1481. { range: [0x16800, 0x16A3F], langs: ['Bamum'] }, // Bamum
  1482. { range: [0x1B000, 0x1B0FF], langs: ['Kana Supplement'] }, // Kana Supplement
  1483. { range: [0x1D000, 0x1D0FF], langs: ['Byzantine Musical Symbols'] }, // Byzantine Musical Symbols
  1484. { range: [0x1D100, 0x1D1FF], langs: ['Musical Symbols'] }, // Musical Symbols
  1485. { range: [0x1D200, 0x1D24F], langs: ['Ancient Greek Musical Notation'] }, // Ancient Greek Musical Notation
  1486. { range: [0x1D300, 0x1D35F], langs: ['Tai Xuan Jing Symbols'] }, // Tai Xuan Jing Symbols
  1487. { range: [0x1D360, 0x1D37F], langs: ['Counting Rod Numerals'] }, // Counting Rod Numerals
  1488. { range: [0x1D400, 0x1D7FF], langs: ['Mathematical Alphanumeric Symbols'] }, // Mathematical Alphanumeric Symbols
  1489. { range: [0x1F000, 0x1F02F], langs: ['Mahjong Tiles'] }, // Mahjong Tiles
  1490. { range: [0x1F030, 0x1F09F], langs: ['Domino Tiles'] }, // Domino Tiles
  1491. { range: [0x1F0A0, 0x1F0FF], langs: ['Playing Cards'] }, // Playing Cards
  1492. { range: [0x1F100, 0x1F1FF], langs: ['Enclosed Alphanumeric Supplement'] }, // Enclosed Alphanumeric Supplement
  1493. { range: [0x1F200, 0x1F2FF], langs: ['Enclosed Ideographic Supplement'] }, // Enclosed Ideographic Supplement
  1494. { range: [0x1F300, 0x1F5FF], langs: ['Miscellaneous Symbols and Pictographs'] }, // Miscellaneous Symbols and Pictographs
  1495. { range: [0x1F600, 0x1F64F], langs: ['Emoticons'] }, // Emoticons
  1496. { range: [0x1F680, 0x1F6FF], langs: ['Transport and Map Symbols'] }, // Transport and Map Symbols
  1497. { range: [0x1F700, 0x1F77F], langs: ['Alchemical Symbols'] }, // Alchemical Symbols
  1498. { range: [0x20000, 0x2A6DF], langs: ['CJK Unified Ideographs Extension B'] }, // CJK Unified Ideographs Extension B
  1499. { range: [0x2A700, 0x2B73F], langs: ['CJK Unified Ideographs Extension C'] }, // CJK Unified Ideographs Extension C
  1500. { range: [0x2B740, 0x2B81F], langs: ['CJK Unified Ideographs Extension D'] }, // CJK Unified Ideographs Extension D
  1501. { range: [0x2F800, 0x2FA1F], langs: ['CJK Compatibility Ideographs Supplement'] }, // CJK Compatibility Ideographs Supplement
  1502. ];
  1503.  
  1504.  
  1505.  
  1506. let lastLangEntry = null;
  1507.  
  1508. function getPossibleLanguages(unicodeChar) {
  1509. const unicode = typeof unicodeChar === 'number' ? unicodeChar : typeof unicodeChar === 'string' ? unicodeChar.codePointAt(0) : 0;
  1510.  
  1511. if (unicode >= 0x20) {
  1512. if (unicode >= 0x0370 && lastLangEntry && unicode >= lastLangEntry.range[0] && unicode <= lastLangEntry.range[1]) {
  1513. return lastLangEntry.langs;
  1514. }
  1515. for (const entry of languageMap) {
  1516. if (unicode >= entry.range[0] && unicode <= entry.range[1]) {
  1517. if (entry.range[0] >= 0x0370) lastLangEntry = entry;
  1518. return entry.langs;
  1519. }
  1520. if (entry.range[0] >= unicode && entry.range[1] >= unicode) break;
  1521. }
  1522. }
  1523.  
  1524. return languageMapR0;
  1525. }
  1526.  
  1527. const handleTextAllowSymbolsNChars = new Set('_-.·0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map(e => e.codePointAt()));
  1528. const blacklistChars = new Set('@;:"\'!$#%^&*()[]<>,?/\\+{}|~`!'.split('').map(e => e.codePointAt()));
  1529.  
  1530. const _cache_isExactHandleText = new Map();
  1531. const isExactHandleText = (text, i = 0) => {
  1532. let cpCount = 0, cp = 0;
  1533. let cpMin = Number.MAX_VALUE
  1534. let cpMax = Number.MIN_VALUE
  1535. const suffixNumber = /\d+$/.exec(text);
  1536. const suffixNumberLen = `${suffixNumber || ''}`.length;
  1537. const textLen = text.length;
  1538. if (textLen - i > 30) return false;
  1539. let l = textLen - suffixNumberLen;
  1540. let maxCpCount = 30 - suffixNumberLen;
  1541. if (maxCpCount <= 0) return maxCpCount === 0;
  1542. const mtext = text.substring(i);
  1543. let res = _cache_isExactHandleText.get(mtext);
  1544. if (typeof res !== 'undefined') return res;
  1545.  
  1546. const retFn = (res) => {
  1547. _cache_isExactHandleText.set(mtext, res);
  1548. return res;
  1549. }
  1550.  
  1551. let uchars = [];
  1552. const uRanges = new Set();
  1553. for (; i < l; i++) {
  1554. cp = text.codePointAt(i);
  1555. if (!cp) break;
  1556. if (!handleTextAllowSymbolsNChars.has(cp)) {
  1557. // @[LANG]-c2w (found in Dec 2024)
  1558. if (blacklistChars.has(cp)) return retFn(false);
  1559. const uRange = getPossibleLanguages(cp);
  1560. uRanges.add(uRange);
  1561. uchars.push(cp);
  1562. if (cpMin > cp) cpMin = cp;
  1563. if (cpMax < cp) cpMax = cp;
  1564. }
  1565. if (cp < 0x7FFFFFFF) {
  1566. while (cp > 0xFFFF) {
  1567. cp = cp >> 16;
  1568. i++;
  1569. }
  1570. } else {
  1571. console.warn('cp >= 0x7FFFFFFF')
  1572. return retFn(false);
  1573. }
  1574. cpCount++;
  1575. if (cpCount > 30) {
  1576. return retFn(false);
  1577. }
  1578. }
  1579.  
  1580. // console.log(mtext, cpMin,cpMax, uRanges)
  1581. if (!uRanges.size || (cpMin >= 32 && cpMax <= 122)) {
  1582. return retFn(/^[-_a-zA-Z0-9.·]{3,30}$/.test(mtext));
  1583. }
  1584.  
  1585. let mm = {}, mi = 0;
  1586. for (const uRange of uRanges) {
  1587. if (uRange) {
  1588. for (const lang of uRange) {
  1589. if (lang) {
  1590. mm[lang] = (mm[lang] || 0) + 1;
  1591. }
  1592. }
  1593. }
  1594. mi++;
  1595. }
  1596. let unilang = "";
  1597. let dd = Object.entries(mm).filter(a => a[1] === mi);
  1598. if (dd.length === 1) {
  1599. unilang = dd[0];
  1600. } else if (dd.length === 3 && dd[0][0] === 'Arabic' && dd[1][0] === 'Persian' && dd[2][0] === 'Urdu') {
  1601. return retFn("Arabic");
  1602. }else if (dd.length === 4 && dd[0][0] === 'Chinese (Simplified)' && dd[1][0] === 'Chinese (Traditional)' && dd[2][0] === 'Japanese' && dd[3][0] === 'Korean') {
  1603. return retFn("CJK");
  1604. }
  1605.  
  1606.  
  1607.  
  1608. if (unilang[0] === "Japanese") {
  1609. return retFn("Japanese"); // japanese handle can mix with kanji
  1610. }
  1611.  
  1612.  
  1613. if (cpMin >= 0x1000) {
  1614. const ft = String.fromCodePoint(...uchars);
  1615. if (cpMin >= 0x4E00 && cpMax <= 0xD7AF) {
  1616. if (/^[\u4E00-\u9FFF\uAC00-\uD7AF]{1,10}$/.test(ft)) return retFn("Han|Hangul");
  1617. } else if (cpMin >= 0x1200 && cpMax <= 0x30FF) {
  1618. if (/^[\u1200-\u137F\u3040-\u30FF]{2,20}$/.test(ft)) return retFn("Ethiopic|Hiragana|Katakana");
  1619. }
  1620. }
  1621.  
  1622. return retFn(false);
  1623.  
  1624. }
  1625.  
  1626. const exactHandleText = (text, b) => {
  1627. return text.startsWith('@') && isExactHandleText(text, 1) ? (b ? true : text.substring(1)) : (b ? false : '');
  1628. }
  1629.  
  1630. const exactHandleUrl_ = (text, b, c) => {
  1631. if (c === 2) {
  1632. if (isValidMinSlashHandle(text)) {
  1633. text = urlHandleConversion(text) || text;
  1634. } else {
  1635. return b ? false : '';
  1636. }
  1637. } else if (c === 1 && (text.length < NUM_CHANNEL_ID_MIN_LEN_2 || text.length > 32)) { // 2+30
  1638. return false;
  1639. }
  1640. return text.startsWith('/@') && isExactHandleText(text, 2) ? (b ? true : text.substring(2)) : (b ? false : '');
  1641. }
  1642.  
  1643. const _cache_urlHandleConversion = new Map();
  1644. const urlHandleConversion = (text) => {
  1645.  
  1646. let converted = _cache_urlHandleConversion.get(text);
  1647.  
  1648. if (typeof converted === 'string') {
  1649.  
  1650. text = converted;
  1651.  
  1652. } else if (text.includes('%')) {
  1653. let text0 = text;
  1654.  
  1655. let text2 = text.substring(2);
  1656. if (/%[0-9A-Fa-f]{2}/.test(text2) && /^[-_a-zA-Z%0-9.·]+$/.test(text2)) {
  1657. try {
  1658. text2 = decodeURI(text2);
  1659. } catch (e) { }
  1660. text = `/@${text2}`
  1661. }
  1662. _cache_urlHandleConversion.set(text0, text);
  1663.  
  1664. }
  1665.  
  1666.  
  1667. if (exactHandleUrl_(text, true, 1)) {
  1668. return text;
  1669. }
  1670.  
  1671. }
  1672. const exactHandleUrl = (text, b) => {
  1673. return exactHandleUrl_(text, b, 2);
  1674. }
  1675.  
  1676. try {
  1677. // https://www.youtube.com/watch?v=hfopp1vLFmk
  1678. // https://www.youtube.com/watch?v=V-4PiHDX2Mg
  1679. let b = true
  1680.  
  1681.  
  1682. b = b && isDisplayAsHandle('@7r0n215 ');
  1683. b = b && isDisplayAsHandle('@user-ox5by7bw1m ');
  1684. b = b && isDisplayAsHandle('@Samsungfan2016 ');
  1685. b = b && isDisplayAsHandle('@김태래 ');
  1686. b = b && !isDisplayAsHandle('@%EA%B9%80%ED%83%9C%EB%9E%98 ');
  1687.  
  1688. b = b && !exactHandleText('@7r0n215 ', true);
  1689. b = b && !exactHandleText('@user-ox5by7bw1m ', true);
  1690. b = b && !exactHandleText('@Samsungfan2016 ', true);
  1691. b = b && !exactHandleText('@김태래 ', true);
  1692. b = b && !exactHandleText('@%EA%B9%80%ED%83%9C%EB%9E%98 ', true);
  1693.  
  1694.  
  1695. b = b && !exactHandleUrl('/@7r0n215 ', true);
  1696. b = b && !exactHandleUrl('/@user-ox5by7bw1m ', true);
  1697. b = b && !exactHandleUrl('/@Samsungfan2016 ', true);
  1698. b = b && !exactHandleUrl('/@김태래 ', true);
  1699. b = b && !exactHandleUrl('/@%EA%B9%80%ED%83%9C%EB%9E%98 ', true);
  1700.  
  1701.  
  1702. b = b && isDisplayAsHandle(' @7r0n215');
  1703. b = b && isDisplayAsHandle(' @user-ox5by7bw1m');
  1704. b = b && isDisplayAsHandle(' @Samsungfan2016');
  1705. b = b && isDisplayAsHandle(' @김태래');
  1706. b = b && !isDisplayAsHandle(' @%EA%B9%80%ED%83%9C%EB%9E%98');
  1707.  
  1708. b = b && !exactHandleText(' @7r0n215', true);
  1709. b = b && !exactHandleText(' @user-ox5by7bw1m', true);
  1710. b = b && !exactHandleText(' @Samsungfan2016', true);
  1711. b = b && !exactHandleText(' @김태래', true);
  1712. b = b && !exactHandleText(' @%EA%B9%80%ED%83%9C%EB%9E%98', true);
  1713.  
  1714. b = b && !exactHandleUrl(' /@7r0n215', true);
  1715. b = b && !exactHandleUrl(' /@user-ox5by7bw1m', true);
  1716. b = b && !exactHandleUrl(' /@Samsungfan2016', true);
  1717. b = b && !exactHandleUrl(' /@김태래', true);
  1718. b = b && !exactHandleUrl(' /@%EA%B9%80%ED%83%9C%EB%9E%98', true);
  1719.  
  1720.  
  1721. b = b && isDisplayAsHandle('@7r0n215');
  1722. b = b && isDisplayAsHandle('@user-ox5by7bw1m');
  1723. b = b && isDisplayAsHandle('@Samsungfan2016');
  1724. b = b && isDisplayAsHandle('@김태래');
  1725. b = b && !isDisplayAsHandle('@%EA%B9%80%ED%83%9C%EB%9E%98');
  1726.  
  1727. b = b && exactHandleText('@7r0n215', true);
  1728. b = b && exactHandleText('@user-ox5by7bw1m', true);
  1729. b = b && exactHandleText('@Samsungfan2016', true);
  1730. b = b && exactHandleText('@김태래', true);
  1731. b = b && !exactHandleText('@%EA%B9%80%ED%83%9C%EB%9E%98', true);
  1732.  
  1733.  
  1734. b = b && exactHandleUrl('/@7r0n215', true);
  1735. b = b && exactHandleUrl('/@user-ox5by7bw1m', true);
  1736. b = b && exactHandleUrl('/@Samsungfan2016', true);
  1737. b = b && exactHandleUrl('/@김태래', true);
  1738. b = b && exactHandleUrl('/@%EA%B9%80%ED%83%9C%EB%9E%98', true);
  1739.  
  1740. b = b && isDisplayAsHandle('@Lions_joyfulpenda');
  1741. b = b && isDisplayAsHandle('@kowlsl12');
  1742. b = b && isDisplayAsHandle('@hyezzozzo_baseball_curtaincall');
  1743. b = b && isDisplayAsHandle('@blue-pink-love');
  1744.  
  1745.  
  1746. b = b && isDisplayAsHandle('@mr.memebear901');
  1747. b = b && isDisplayAsHandle('@すいちゃん可愛い');
  1748. b = b && exactHandleUrl('/@%E3%81%99%E3%81%84%E3%81%A1%E3%82%83%E3%82%93%E5%8F%AF%E6%84%9B%E3%81%84');
  1749. b = b && isDisplayAsHandle('@ハシビロ公');
  1750. b = b && exactHandleUrl('/@%E3%83%8F%E3%82%B7%E3%83%93%E3%83%AD%E5%85%AC');
  1751. b = b && isDisplayAsHandle('@ex1524')
  1752. b = b && isDisplayAsHandle('@K88571')
  1753. b = b && isDisplayAsHandle('@مجلةالرابالعربي')
  1754. b = b && !isDisplayAsHandle('@@مجلةالرابالعربي')
  1755.  
  1756. b = b && isDisplayAsHandle('@ともとも-e7d8b')
  1757. b = b && isDisplayAsHandle('@佐藤漣-x7i')
  1758. b = b && isDisplayAsHandle('@巡空')
  1759. b = b && isDisplayAsHandle('@abc')
  1760. b = b && isDisplayAsHandle('@巡')
  1761. b = b && !isDisplayAsHandle('@xy')
  1762. b = b && !isDisplayAsHandle('@z')
  1763. b = b && isDisplayAsHandle('@茶々-l9y')
  1764.  
  1765.  
  1766. if (!b) console.error('!!!! wrong coding !!!!');
  1767. } catch (e) {
  1768. console.error('!!!! wrong coding !!!!', e);
  1769. }
  1770. /**
  1771. *
  1772. * @param {Object[]} contentTexts
  1773. * @param {number} idx
  1774. * @returns
  1775. */
  1776. const contentTextProcess = (_md, contentTexts, idx) => {
  1777. const md = _md;
  1778. const contentText = contentTexts[idx];
  1779. const text = (contentText || 0).text;
  1780. const browseId = ((contentText.navigationEndpoint || 0).browseEndpoint || 0).browseId || '';
  1781. const canonicalBaseUrl = ((contentText.navigationEndpoint || 0).browseEndpoint || 0).canonicalBaseUrl || ''; // TBC
  1782. const url = (((contentText.navigationEndpoint || 0).commandMetadata || 0).webCommandMetadata || 0).url; // TBC
  1783. if (typeof url === 'string' && typeof text === 'string' && typeof browseId === 'string' && browseId && isUCBrowserId(browseId)) {
  1784.  
  1785. if (!isDisplayAsHandle(text)) return null;
  1786. const channelId = browseId;
  1787.  
  1788. return getDisplayName(channelId).then(fetchResult => {
  1789. let resolveResult = null;
  1790. if (fetchResult) {
  1791. // note: if that user shown is not found in `a[id]`, the hyperlink would not change
  1792.  
  1793. const textTrimmed = verifyAndConvertHandle(text, fetchResult);
  1794. if (textTrimmed) {
  1795. resolveResult = (() => {
  1796. let runs = ((md || 0).contentText || 0).runs;
  1797. if (!runs || !runs[idx]) return;
  1798. if (runs[idx].text !== text) return;
  1799. runs[idx].text = text.replace(textTrimmed, `@${fetchResult.title}`); // HyperLink always @SomeOne
  1800. md.contentText = Object.assign({}, md.contentText);
  1801. })();
  1802. }
  1803. }
  1804. return resolveResult; // function as a Promise
  1805. });
  1806. }
  1807.  
  1808. return null;
  1809. };
  1810.  
  1811. const dynamicEditableTextByControllerResolve = isMobile ? 0 : (() => {
  1812. /**
  1813. *
  1814. *
  1815. f.showReplyDialog = function(a) {
  1816. if (a) {
  1817. var b = this.replyBox;
  1818. b || (b = document.createElement("ytd-comment-reply-dialog-renderer"),
  1819. b.id = "replybox",
  1820. qF(this.replyDialogDiv).appendChild(b));
  1821. b.data = a;
  1822. this.replyDialogDiv.hidden = !1;
  1823. b.openDialog()
  1824. }
  1825. }
  1826.  
  1827. // a = <ytd-comment-engagement-bar>...</ytd-comment-engagement-bar>
  1828. // b = "showReplyDialog"
  1829. // c = undefined
  1830. function Lpb(a, b, c) {
  1831. var d = a.polymerController;
  1832. a[b] = function() {
  1833. var e = va.apply(0, arguments);
  1834. a.loggingStatus.currentExternalCall = b;
  1835. a.loggingStatus.bypassProxyController = !0;
  1836. var g, k = ((g = a.is) != null ? g : a.tagName).toLowerCase();
  1837. ZE(k, b, "PROPERTY_ACCESS_CALL_EXTERNAL");
  1838. var m;
  1839. g = (m = c != null ? c : d[b]) == null ? void 0 : m.call.apply(m, [d].concat(ha(e)));
  1840. a.loggingStatus.currentExternalCall = void 0;
  1841. a.loggingStatus.bypassProxyController = !1;
  1842. return g
  1843. }
  1844. }
  1845.  
  1846. // a = {..., commandMetadata, createCommentReplyDialogEndpoint}
  1847. // b = {commandController: {onSuccess: ƒ, onServerError: ƒ} , forceClickLogging : true, form: {element, event} }
  1848. kQb.prototype.resolveCommand = function(a, b) {
  1849. var c, d, e, g, k, m, p, q;
  1850. return t(function(r) {
  1851. e = (c = G(a, n6a)) == null ? void 0 : (d = c.dialog) == null ? void 0 : d.commentReplyDialogRenderer;
  1852. if (!e)
  1853. throw new Em("No dialog in createCommentReplyDialogEndpoint");
  1854. k = (g = b.form) == null ? void 0 : g.event;
  1855. if (!k)
  1856. throw new Em("Event not passed in when resolving command");
  1857. m = h(k.composedPath());
  1858. for (p = m.next(); !p.done; p = m.next())
  1859. if (q = p.value,
  1860. q.tagName === "YTD-COMMENT-ACTION-BUTTONS-RENDERER" || q.tagName === "YTD-COMMENT-ENGAGEMENT-BAR")
  1861. return q.showReplyDialog(e),
  1862. r.return();
  1863. ma(r)
  1864. })
  1865. }
  1866.  
  1867.  
  1868. *
  1869. */
  1870.  
  1871.  
  1872. const setupForCommentReplyDialogRenderer = (cProto) => {
  1873. if (cProto && typeof cProto.showReplyDialog === 'function' && cProto.showReplyDialog.length === 1 && !cProto.showReplyDialog392) {
  1874. cProto.showReplyDialog392 = cProto.showReplyDialog;
  1875. cProto.showReplyDialog = function (a) {
  1876. let goDefault = true;
  1877. try {
  1878. if (a && a.editableText) {
  1879. const arr = [];
  1880. fixCommentReplyDialogRenderer(a, arr);
  1881. const pAll = Promise.all(arr).catch(e => console.warn);
  1882. Promise.race([pAll, delayPn(300)]).then(() => { // network request should be already done before
  1883. this.showReplyDialog392(a);
  1884. });
  1885. goDefault = false;
  1886. }
  1887. } catch (e) { }
  1888. if (goDefault) {
  1889. return this.showReplyDialog392(a);
  1890. }
  1891. };
  1892. }
  1893. };
  1894.  
  1895. retrieveCE('ytd-comment-action-buttons-renderer').then(cProto => {
  1896. setupForCommentReplyDialogRenderer(cProto);
  1897. });
  1898. retrieveCE('ytd-comment-engagement-bar').then(cProto => {
  1899. setupForCommentReplyDialogRenderer(cProto);
  1900. });
  1901.  
  1902. return 1;
  1903.  
  1904. })();
  1905.  
  1906. const editableTextProcess = (_commentReplyDialogRenderer, editableTexts, idx) => {
  1907. const commentReplyDialogRenderer = _commentReplyDialogRenderer;
  1908. const editableText = editableTexts[idx];
  1909. const text = (editableText || 0).text;
  1910.  
  1911. const browseId = ((editableText.navigationEndpoint || 0).browseEndpoint || 0).browseId || '';
  1912. const canonicalBaseUrl = ((editableText.navigationEndpoint || 0).browseEndpoint || 0).canonicalBaseUrl || ''; // TBC
  1913. const url = (((editableText.navigationEndpoint || 0).commandMetadata || 0).webCommandMetadata || 0).url; // TBC
  1914. if (typeof url === 'string' && typeof text === 'string' && typeof browseId === 'string' && browseId && isUCBrowserId(browseId)) {
  1915.  
  1916. if (!isDisplayAsHandle(text)) return null;
  1917. const channelId = browseId;
  1918.  
  1919. return getDisplayName(channelId).then(fetchResult => {
  1920. let resolveResult = null;
  1921. if (fetchResult) {
  1922. // note: if that user shown is not found in `a[id]`, the hyperlink would not change
  1923.  
  1924. const textTrimmed = verifyAndConvertHandle(text, fetchResult);
  1925. if (textTrimmed) {
  1926. resolveResult = (() => {
  1927. if (!commentReplyDialogRenderer) return;
  1928. const editableText = commentReplyDialogRenderer.editableText || 0;
  1929. let runs = (editableText || 0).runs;
  1930. if (!runs || !runs[idx]) return;
  1931. if (runs[idx].text !== text) return;
  1932. runs[idx].text = text.replace(textTrimmed, `@${fetchResult.title}`); // HyperLink always @SomeOne
  1933. commentReplyDialogRenderer.editableText = Object.assign({}, commentReplyDialogRenderer.editableText);
  1934. })();
  1935. }
  1936. }
  1937. return resolveResult; // function as a Promise
  1938. });
  1939. }
  1940.  
  1941. return null;
  1942. };
  1943.  
  1944. const { contentTextProcessViewModel } = (() => {
  1945.  
  1946. const isIterable = (obj) => (Symbol.iterator in Object_(obj));
  1947.  
  1948. /**
  1949. * Optimized version of expandIntervals.
  1950. * @param {Array} arr - Array of mapping objects with oIdx1, oIdx2, nIdx1, nIdx2.
  1951. * @returns {Array} - Expanded and sorted interval mappings.
  1952. */
  1953. function expandIntervals(arr) {
  1954. if (!arr || arr.length === 0) return [];
  1955.  
  1956. const n = arr.length; // n>=1
  1957.  
  1958. // Sort the mappings by oIdx1 if not already sorted
  1959. if (n > 1) arr.sort((a, b) => a.oIdx1 - b.oIdx1);
  1960.  
  1961. const result = new Array(n * 2 + 1); // max length
  1962. let j = 0;
  1963.  
  1964. // Handle the first gap if it exists
  1965. if (arr[0].oIdx1 > 0) {
  1966. result[j++] = {
  1967. oStart: 0,
  1968. oEnd: arr[0].oIdx1,
  1969. nStart: 0,
  1970. nEnd: arr[0].nIdx1
  1971. };
  1972. }
  1973.  
  1974. for (let i = 0, current; current = arr[i]; i++) {
  1975. result[j++] = {
  1976. oStart: current.oIdx1,
  1977. oEnd: current.oIdx2,
  1978. nStart: current.nIdx1,
  1979. nEnd: current.nIdx2
  1980. };
  1981.  
  1982. if (i >= n - 1) {
  1983. // Handle the final gap
  1984. result[j++] = {
  1985. oStart: current.oIdx2,
  1986. oEnd: null,
  1987. nStart: current.nIdx2,
  1988. nEnd: null
  1989. };
  1990. break;
  1991. }
  1992.  
  1993. // If not the last interval, handle the gap
  1994. const next = arr[i + 1];
  1995. const gapStart = current.oIdx2;
  1996. const gapEnd = next.oIdx1;
  1997.  
  1998. if (gapStart < gapEnd) {
  1999. const shift = next.nIdx1 - next.oIdx1;
  2000. result[j++] = {
  2001. oStart: gapStart,
  2002. oEnd: gapEnd,
  2003. nStart: gapStart + shift,
  2004. nEnd: gapEnd + shift
  2005. };
  2006. }
  2007. }
  2008.  
  2009.  
  2010. result.length = j;
  2011.  
  2012. return result;
  2013. }
  2014.  
  2015.  
  2016. // abcdef. [0,3][3,6]
  2017.  
  2018. /**
  2019. * Updates a target array in-place based on sorted interval mappings.
  2020. * Assumes both targetArray and intervalMappings are sorted by startIndex.
  2021. * @param {Array} uArr - Array of objects with startIndex and length.
  2022. * @param {Array} itMap - Expanded and sorted interval mappings.
  2023. */
  2024. function updateArrayInPlaceOptimized(uArr, itMap) {
  2025. let mapIdx = 0;
  2026. const itMapN = itMap.length;
  2027.  
  2028. const mapIndex = (qIndex) => {
  2029. while (
  2030. mapIdx < itMapN &&
  2031. itMap[mapIdx].oEnd !== null &&
  2032. qIndex > itMap[mapIdx].oEnd
  2033. ) {
  2034. mapIdx++;
  2035. }
  2036. // expect: qIndex>=[mapIdx].oStart && ([mapIdx].oEnd === null || qIndex<=[mapIdx].oEnd)
  2037.  
  2038. if (mapIdx >= itMapN) {
  2039. throw new Error(`No mapping found for index ${qIndex}`);
  2040. }
  2041.  
  2042. const { oStart, nStart, oEnd, nEnd } = itMap[mapIdx];
  2043.  
  2044. if (qIndex == oStart) {
  2045. return nStart;
  2046. } else if (qIndex === oEnd) {
  2047. return nEnd;
  2048. } else {
  2049. const size1 = oEnd - oStart;
  2050. const size2 = nEnd === null ? size1 : nEnd - nStart;
  2051. if (size1 !== size2) {
  2052. return Math.round(nStart + (qIndex - oStart) / size1 * size2);
  2053. } else {
  2054. return nStart + (qIndex - oStart);
  2055. }
  2056. }
  2057.  
  2058. };
  2059. for (const item of uArr) {
  2060.  
  2061. const {startIndex, length} = item;
  2062.  
  2063. if (typeof startIndex === 'number' && typeof length === 'number' && startIndex >= 0 && length >= 0) {
  2064.  
  2065. const newStart = mapIndex(startIndex);
  2066. const newEnd = mapIndex(startIndex + length);
  2067.  
  2068. if (newStart === undefined || newEnd === undefined) {
  2069. throw new Error(`Mapping failed for item: ${JSON.stringify(item)}`);
  2070. }
  2071.  
  2072. item.startIndex = newStart;
  2073. item.length = newEnd - newStart;
  2074. }
  2075. };
  2076. }
  2077.  
  2078. function replaceNamePN(o) {
  2079. const channelId = o.channelId;
  2080. channelIdToHandle.set(channelId, {
  2081. handleText: o.handle,
  2082. justPossible: true
  2083. });
  2084. skipElementCheck.add(channelId);
  2085. return getDisplayName(channelId).then(fetchResult => {
  2086. let resolveResult = null;
  2087. skipElementCheck.delete(channelId);
  2088. if (fetchResult && o.channelId === channelId) {
  2089. const textTrimmed = verifyAndConvertHandle(o.handle, fetchResult);
  2090. if (textTrimmed) {
  2091. o.display = `@${fetchResult.title}`;
  2092. o.newText = o.oldText.replace(o.handle, o.display);
  2093. o.nLen = o.newText.length;
  2094. o.nOffset = o.nLen - o.oLen;
  2095. }
  2096. }
  2097. return resolveResult; // function as a Promise
  2098. });
  2099. }
  2100.  
  2101. const contentTextProcessViewModel = async (content, idx) => {
  2102. const { commandRuns, content: text } = content;
  2103. if (typeof text !== 'string' || !commandRuns) return;
  2104. let uBool = false;
  2105.  
  2106. const objList = [];
  2107. for (const [key, value] of Object.entries(content)) {
  2108. if ((value || 0).length >= 1 && typeof value === 'object' && isIterable(value)) {
  2109. objList.push(value);
  2110. if (key === 'commandRuns') uBool = true;
  2111. }
  2112. }
  2113.  
  2114. if (!uBool) return;
  2115.  
  2116. const nameInfos = [];
  2117.  
  2118. const promises = [];
  2119. for (const commandRun of commandRuns) {
  2120. let browseEndpoint = null;
  2121. if (commandRun.length >= 1 && commandRun.startIndex >= 0 && (browseEndpoint = ((commandRun.onTap || 0).innertubeCommand || 0).browseEndpoint)) {
  2122. const substr = text.substring(commandRun.startIndex, commandRun.startIndex + commandRun.length);
  2123. const substrTrimmed = substr.trim();
  2124. const valBrowseEndpointBaseUrlType = browseEndpointBaseUrlType(browseEndpoint, substrTrimmed);
  2125. if (valBrowseEndpointBaseUrlType > 1) {
  2126. const o = {
  2127. startIndex: commandRun.startIndex,
  2128. endIndex: commandRun.startIndex + commandRun.length,
  2129. handle: substrTrimmed,
  2130. oldText: substr,
  2131. newText: null,
  2132. channelId: browseEndpoint.browseId,
  2133. oLen: commandRun.length,
  2134. nLen: null,
  2135. nOffset: 0,
  2136. }
  2137. nameInfos.push(o);
  2138. promises.push(replaceNamePN(o));
  2139. }
  2140. }
  2141. }
  2142. if (nameInfos.length === 0) return; // other command runs
  2143. await Promise.all(promises);
  2144.  
  2145. /*
  2146. if replaceNamePN fails, then following will not set.
  2147. o.display = `@${fetchResult.title}`;
  2148. o.newText = o.oldText.replace(o.handle, o.display);
  2149. o.nLen = o.newText.length;
  2150. o.nOffset = o.nLen - o.oLen;
  2151. */
  2152.  
  2153. let text1 = text;
  2154.  
  2155. try {
  2156. const mapInput = new Array(nameInfos.length);
  2157. let ac = 0;
  2158. let j = 0;
  2159.  
  2160. for (let k = 0; k < nameInfos.length; k++) {
  2161. const o = nameInfos[k];
  2162. if (typeof o.newText === 'string' && o.nLen === o.newText.length && Number.isFinite(o.nOffset) && typeof o.display === 'string') {
  2163. mapInput[j++] = {
  2164. oIdx1: o.startIndex,
  2165. oIdx2: o.endIndex,
  2166.  
  2167. nIdx1: o.startIndex + ac,
  2168. nIdx2: o.endIndex + ac + o.nOffset,
  2169. newText: o.newText
  2170.  
  2171. }
  2172. ac += o.nOffset;
  2173. }
  2174. }
  2175. mapInput.length = j;
  2176.  
  2177. const mapOutput = expandIntervals(mapInput);
  2178. const n = mapOutput.length;
  2179. const u = new Array(n);
  2180.  
  2181. j = 0;
  2182. for (let i = 0; i < n; i++) {
  2183. const { oStart, oEnd } = mapOutput[i];
  2184. if (j < mapInput.length && oStart === mapInput[j].oIdx1 && oEnd === mapInput[j].oIdx2) {
  2185. u[i] = mapInput[j++].newText;
  2186. } else {
  2187. u[i] = text.substring(oStart, oEnd !== null ? oEnd : text.length);
  2188. }
  2189. }
  2190. text1 = u.join('');
  2191. u.length = 0;
  2192.  
  2193. for (const runs of objList) {
  2194. updateArrayInPlaceOptimized(runs, mapOutput)
  2195. }
  2196.  
  2197. content.content = text1;
  2198.  
  2199. } catch (e) {
  2200. console.warn(e);
  2201. }
  2202.  
  2203.  
  2204.  
  2205.  
  2206.  
  2207. };
  2208.  
  2209.  
  2210. // const contentTextProcessViewModel = async (content, idx) => {
  2211. // const { commandRuns, content: text, styleRuns } = content;
  2212. // if (typeof text !== 'string') return;
  2213. // const names = [];
  2214.  
  2215. // function replaceNamePN(o) {
  2216. // return getDisplayName(o.channelId).then(fetchResult => {
  2217. // let resolveResult = null;
  2218. // if (fetchResult) {
  2219. // const textTrimmed = verifyAndConvertHandle(o.handle, fetchResult);
  2220. // if (textTrimmed) {
  2221. // o.display = `@${fetchResult.title}`;
  2222. // o.newText = o.oldText.replace(o.handle, o.display);
  2223. // }
  2224. // }
  2225. // return resolveResult; // function as a Promise
  2226. // });
  2227. // }
  2228. // const promises = [];
  2229. // for (const commandRun of commandRuns) {
  2230. // let browseEndpoint = null;
  2231. // if (commandRun.length >= 1 && commandRun.startIndex >= 0 && (browseEndpoint = ((commandRun.onTap || 0).innertubeCommand || 0).browseEndpoint)) {
  2232. // let substr = text.substring(commandRun.startIndex, commandRun.startIndex + commandRun.length);
  2233. // const substrTrimmed = substr.trim();
  2234. // const valBrowseEndpointBaseUrlType = browseEndpointBaseUrlType(browseEndpoint, substrTrimmed);
  2235. // if (valBrowseEndpointBaseUrlType > 1) {
  2236. // const o = {
  2237. // startIndex: commandRun.startIndex,
  2238. // endIndex: commandRun.startIndex + commandRun.length,
  2239. // handle: substrTrimmed,
  2240. // oldText: substr,
  2241. // newText: null,
  2242. // channelId: browseEndpoint.browseId,
  2243. // nStartIndex: null,
  2244. // nEndIndex: null,
  2245. // nText: null
  2246. // }
  2247. // names.push(o);
  2248. // promises.push(replaceNamePN(o));
  2249. // }
  2250. // }
  2251. // }
  2252. // await Promise.all(promises);
  2253.  
  2254. // let offset = 0;
  2255. // for (const o of names) {
  2256. // o.nStartIndex = o.startIndex + offset;
  2257. // o.nText = o.newText === null ? o.oldText : o.newText;
  2258. // o.nEndIndex = o.nStartIndex + offset + o.nText.length;
  2259. // offset += o.nText.length - o.oldText.length;
  2260. // }
  2261.  
  2262. // let w1 = 0, w2 = 0;
  2263. // let s = [];
  2264. // let j = 0;
  2265. // for (let i = 0; i < text.length;) {
  2266. // while (w1 < commandRuns.length && commandRuns[w1].startIndex < i) w1++;
  2267. // while (w2 < styleRuns.length && styleRuns[w2].startIndex < i) w2++;
  2268.  
  2269. // if (j < names.length && i == names[j].startIndex) {
  2270.  
  2271. // if (w1 < commandRuns.length && commandRuns[w1].startIndex === names[j].startIndex) {
  2272. // const run = commandRuns[w1];
  2273. // const o = names[j];
  2274. // if (run.length === o.endIndex - o.startIndex) {
  2275. // run.startIndex = o.nStartIndex;
  2276. // run.length = o.nText.length;
  2277. // }
  2278. // w1++;
  2279. // }
  2280.  
  2281. // if (w2 < styleRuns.length && styleRuns[w2].startIndex === names[j].startIndex) {
  2282. // const run = styleRuns[w2];
  2283. // const o = names[j];
  2284. // if (run.length === o.endIndex - o.startIndex) {
  2285. // run.startIndex = o.nStartIndex;
  2286. // run.length = o.nText.length;
  2287. // }
  2288. // w2++;
  2289. // }
  2290.  
  2291.  
  2292. // // s.push(text.substring(names[j].startIndex, names[j].endIndex));
  2293. // s.push(names[j].nText);
  2294. // i = names[j].endIndex;
  2295. // j++;
  2296. // } else {
  2297. // if (j >= names.length) {
  2298. // s.push(text.substring(i));
  2299. // i = text.length;
  2300. // } else {
  2301. // if (j < names.length && names[j].startIndex > i + 1) {
  2302. // s.push(text.substring(i, names[j].startIndex));
  2303. // i = names[j].startIndex;
  2304. // } else {
  2305. // s.push(text.charAt(i));
  2306. // i++;
  2307. // }
  2308. // }
  2309. // }
  2310. // }
  2311.  
  2312. // content.content = s.join('');
  2313. // s.length = 0;
  2314.  
  2315.  
  2316. // };
  2317.  
  2318. return { contentTextProcessViewModel };
  2319. })();
  2320.  
  2321.  
  2322. /**
  2323. *
  2324. * @param {ParentNode & Element} parentNode
  2325. * @param {Function} callback
  2326. */
  2327. function findTextNodes(parentNode, callback) {
  2328. /**
  2329. *
  2330. * @param {Node} node
  2331. * @returns
  2332. */
  2333. function traverse(node) {
  2334. if (node.nodeType === Node.TEXT_NODE) {
  2335. let r = callback(node);
  2336. if (r === true) return true;
  2337. } else if (node.nodeType === Node.ELEMENT_NODE) {
  2338. for (let childNode = nodeFirstChild(node); childNode; childNode = nodeNextSibling(childNode)) {
  2339. const r = traverse(childNode);
  2340. if (r === true) return true;
  2341. }
  2342. }
  2343. }
  2344.  
  2345. traverse(parentNode);
  2346. }
  2347.  
  2348. function updatePinnedCommentBadge(md, title, langTitle) {
  2349. const titleForDisplay = langTitle || title;
  2350. if (typeof titleForDisplay !== 'string') return;
  2351. if (title === titleForDisplay) return;
  2352. const runs = ((((md.pinnedCommentBadge || 0).pinnedCommentBadgeRenderer || 0).label || 0).runs || 0); // can be 0
  2353. if (runs.length === 2) {
  2354. let idx = 0;
  2355. let jdx = -1;
  2356. for (const textRun of runs) {
  2357. if (textRun.text === title) {
  2358. jdx = idx;
  2359. break;
  2360. }
  2361. idx++;
  2362. }
  2363. if (jdx >= 0) {
  2364. const runs = md.pinnedCommentBadge.pinnedCommentBadgeRenderer.label.runs.slice(0);
  2365. runs[jdx] = Object.assign({}, runs[jdx], { text: titleForDisplay });
  2366. md.pinnedCommentBadge = Object.assign({}, md.pinnedCommentBadge);
  2367. md.pinnedCommentBadge.pinnedCommentBadgeRenderer = Object.assign({}, md.pinnedCommentBadge.pinnedCommentBadgeRenderer);
  2368. md.pinnedCommentBadge.pinnedCommentBadgeRenderer.label = Object.assign({}, md.pinnedCommentBadge.pinnedCommentBadgeRenderer.label, { runs });
  2369. }
  2370. }
  2371. }
  2372.  
  2373. /**
  2374. *
  2375. * @param {ParentNode} parentNode
  2376. * @param {Object[]} contentTexts
  2377. * @param {number} aidx
  2378. * @returns
  2379. */
  2380. const mobileContentHandleAsync = async (parentNode, contentTexts, aidx) => {
  2381.  
  2382.  
  2383. let commentText = elementQS(parentNode, '.comment-text');
  2384. if (!commentText) return;
  2385. commentText = elementQS(commentText, '.yt-core-attributed-string') || commentText;
  2386. let currentText = commentText.textContent;
  2387. let textMatch = commentText.textContent === contentTexts.map(e => e.text).join('');
  2388. if (!textMatch) return;
  2389.  
  2390. const contentText = contentTexts[aidx];
  2391.  
  2392. /** @type {string} */
  2393. const handleText = contentText.text.trim();
  2394. /** @type {string | null | undefined} */
  2395. let channelId;
  2396.  
  2397. let cache = handleToChannelId.get(handleText);
  2398.  
  2399. if (typeof cache === 'string') {
  2400. channelId = cache;
  2401. } else if (cache instanceof AsyncValue) {
  2402. channelId = await cache.getValue();
  2403. } else {
  2404. /** @type {AsyncValue<string>} */
  2405. const asyncValue = new AsyncValue();
  2406. handleToChannelId.set(handleText, asyncValue);
  2407. channelId = await asyncValue.getValue(); // note: it could be never resolved
  2408. }
  2409.  
  2410. if (!channelId) return; // just in case
  2411.  
  2412. if (commentText.isConnected !== true) return; // already removed
  2413. if (commentText.textContent !== currentText) return; // already changed
  2414.  
  2415. const fetchResult = await getDisplayName(channelId);
  2416.  
  2417. if (fetchResult === null) return;
  2418.  
  2419. const { title, langTitle, externalId } = fetchResult;
  2420. const titleForDisplay = langTitle || title;
  2421. if (externalId !== channelId) return; // channel id must be the same
  2422.  
  2423. if (commentText.isConnected !== true) return; // already removed
  2424. if (commentText.textContent !== currentText) return; // already changed
  2425.  
  2426. let search = contentText.text;
  2427. if (typeof search === 'string') {
  2428.  
  2429. const searchTrimmed = search.trim();
  2430. if (searchTrimmed === handleText) { // ensure integrity after getDisplayName
  2431. contentText.text = search.replace(searchTrimmed, "@" + titleForDisplay); // mention
  2432.  
  2433. findTextNodes(commentText, (textnode) => {
  2434. if (textnode.nodeValue.indexOf(search) >= 0) {
  2435. textnode.nodeValue = textnode.nodeValue.replace(search, contentText.text);
  2436. return true;
  2437. }
  2438. });
  2439. }
  2440.  
  2441. }
  2442.  
  2443. }
  2444.  
  2445. /**
  2446. * Process Checking when there is new (unprocessed) anchor DOM element with hyperlink of channel
  2447. * @type { (anchor: HTMLElement, anchorHref: string?, mt: string?) => Promise<void> }
  2448. */
  2449.  
  2450. const getDisplayTextDOMForCommentRenderer = (root) => { // mobile
  2451. let displayTextDOM = elementQS(root, '.comment-header .comment-title');
  2452. if (displayTextDOM) {
  2453. displayTextDOM = elementQS(displayTextDOM, '.yt-core-attributed-string') || displayTextDOM;
  2454. }
  2455. return displayTextDOM;
  2456. }
  2457.  
  2458. const ytPathnameExtract = (href) => {
  2459.  
  2460. if (!href || typeof href !== 'string') return '';
  2461. let i = 0;
  2462. if (href.startsWith('/')) {
  2463.  
  2464. } else if ((i = href.indexOf('youtube.com/')) >= 0) {
  2465. href = href.substring(i + 11);
  2466. } else {
  2467. return '';
  2468. }
  2469. return href;
  2470.  
  2471. }
  2472.  
  2473. const getHrefType = (href) => {
  2474. if (href && typeof href === 'string') {
  2475. if (href.length === 33 && href.startsWith('/channel/') && /^\/channel\/UC[-_a-zA-Z0-9+=.]{22}$/.test(href)) {
  2476. return 1;
  2477. } else {
  2478. if (isValidMinSlashHandle(href)) {
  2479. href = urlHandleConversion(href) || href;
  2480. }
  2481. if (href && exactHandleUrl_(href, true, 1)) {
  2482. return 2;
  2483. }
  2484. }
  2485. }
  2486. return 0;
  2487. }
  2488.  
  2489. const getYTextType = (text) => {
  2490. // if (typeof text === 'string') return 1;
  2491. if (typeof text === 'object' && text) {
  2492. if (typeof text.simpleText === 'string') return 2;
  2493. const runs = text.runs;
  2494. if (runs && runs.length === 1) {
  2495. let r = runs[0];
  2496. return typeof r === 'string' ? 3 : typeof (r || 0).text === 'string' ? 4 : null;
  2497. }
  2498. }
  2499. return null;
  2500. }
  2501.  
  2502. const getYTextContent = (text) => {
  2503. // if (typeof text === 'string') return text;
  2504. if (typeof text === 'object' && text) {
  2505. const simpleText = text.simpleText;
  2506. if (typeof simpleText === 'string') return simpleText;
  2507. const runs = text.runs;
  2508. if (runs && runs.length === 1) {
  2509. let r = runs[0];
  2510. return typeof r === 'string' ? r : typeof (r || 0).text === 'string' ? r.text : null;
  2511. }
  2512. }
  2513. return null;
  2514. }
  2515.  
  2516. const setYTextContent = (text, fn) => {
  2517. // if (typeof text === 'string') {
  2518. // res=fn(text);
  2519. // }
  2520. if (typeof text === 'object' && text) {
  2521. const simpleText = text.simpleText;
  2522. if (typeof simpleText === 'string') {
  2523. return Object.assign({}, text, { simpleText: fn(simpleText) });
  2524. }
  2525. const runs = text.runs;
  2526. if (runs && runs.length === 1) {
  2527. let r = runs[0];
  2528. 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;
  2529. }
  2530. }
  2531. return null;
  2532. }
  2533.  
  2534. const fixCommentReplyDialogRenderer = (commentReplyDialogRenderer, funcPromises) => {
  2535. if (!commentReplyDialogRenderer) return;
  2536. const editableText = commentReplyDialogRenderer.editableText;
  2537. if (!editableText) return;
  2538. const editableTexts = editableText.runs;
  2539. if (editableTexts && editableTexts.length >= 1) {
  2540. for (let aidx = 0; aidx < editableTexts.length; aidx++) {
  2541. const r = editableTextProcess(commentReplyDialogRenderer, editableTexts, aidx);
  2542. if (r instanceof Promise) funcPromises.push(r);
  2543. }
  2544. }
  2545. // $0.polymerController.$['action-buttons'].polymerController.__data.replyButtonRenderer.navigationEndpoint.createCommentReplyDialogEndpoint.dialog.commentReplyDialogRenderer.editableText
  2546. }
  2547.  
  2548. const domCheckAsync = isMobile ? async (anchor, hrefAttribute, mt) => {
  2549.  
  2550. let sHandleName = null;
  2551. let sNodeValue = null;
  2552. try {
  2553. if (!hrefAttribute || !mt) return;
  2554. let parentNode = nodeParent(anchor);
  2555. while (parentNode instanceof Node) {
  2556. if (parentNode.nodeName === 'YTM-COMMENT-RENDERER' || ('_commentData' in parentNode)) break;
  2557. parentNode = nodeParent(parentNode);
  2558. }
  2559. if (parentNode instanceof Node) { } else return;
  2560.  
  2561. const commentRendererAuthorTextDOM = getDisplayTextDOMForCommentRenderer(parentNode);
  2562. const anchorAiraLabel = anchor.getAttribute('aria-label');
  2563. const anchorHrefValue = ytPathnameExtract(anchor.getAttribute('href')); // guess (Feb 2024)
  2564.  
  2565. const cnt = insp(parentNode);
  2566. const parentNodeData = cnt._commentData || cnt.data || 0;
  2567. const authorTextData = (parentNodeData.authorText || 0);
  2568.  
  2569. let tHandleName = null;
  2570.  
  2571. if (commentRendererAuthorTextDOM && authorTextData) {
  2572. const hType = getHrefType(anchorHrefValue);
  2573. let ytext = null;
  2574. if (hType === 2) {
  2575. tHandleName = anchorHrefValue.substring(1); // guess
  2576. } else if (hType === 1) {
  2577. ytext = getYTextContent(authorTextData)
  2578. tHandleName = ytext; // authorTextData.runs[0].text
  2579. }
  2580. if (typeof tHandleName === 'string' && commentRendererAuthorTextDOM.textContent.trim() === tHandleName && isDisplayAsHandle(tHandleName)) {
  2581. ytext = ytext || getYTextContent(authorTextData)
  2582. if (ytext === tHandleName) {
  2583. sHandleName = tHandleName;
  2584. sNodeValue = tHandleName;
  2585. }
  2586. }
  2587. }
  2588.  
  2589. if (typeof sHandleName === 'string' && typeof sNodeValue === 'string') {
  2590.  
  2591. if (!channelIdToHandle.has(mt)) {
  2592. channelIdToHandle.set(mt, {
  2593. handleText: sHandleName,
  2594. justPossible: true
  2595. });
  2596. }
  2597.  
  2598. const fetchResult = await getDisplayName(mt);
  2599.  
  2600. if (fetchResult === null) return;
  2601.  
  2602. const { title, langTitle, externalId } = fetchResult;
  2603. const titleForDisplay = langTitle || title;
  2604. if (externalId !== mt) return; // channel id must be the same
  2605.  
  2606. // anchor href might be changed by external
  2607. if (!anchorIntegrityCheck(anchor, hrefAttribute)) return;
  2608.  
  2609. if (commentRendererAuthorTextDOM.isConnected !== true) return; // already removed
  2610.  
  2611. let found = false;
  2612.  
  2613. findTextNodes(commentRendererAuthorTextDOM, (textnode) => {
  2614. if (textnode.nodeValue === sNodeValue) {
  2615. textnode.nodeValue = titleForDisplay;
  2616. found = true;
  2617. return true;
  2618. }
  2619. });
  2620.  
  2621. if (!found) return;
  2622. if (anchorAiraLabel && anchorAiraLabel.trim() === sHandleName) anchor.setAttribute('aria-label', anchorAiraLabel.replace(sHandleName, titleForDisplay));
  2623. const authorTextDataNew = setYTextContent(authorTextData, (t) => t.replace(t.trim(), titleForDisplay));
  2624. if (authorTextDataNew && parentNodeData.authorText === authorTextData) {
  2625. parentNodeData.authorText = authorTextDataNew;
  2626. }
  2627.  
  2628. if (UPDATE_PIN_NAME && title && langTitle && langTitle !== title) {
  2629. const renderer = HTMLElement.prototype.closest.call(anchor, 'ytm-comment-renderer');
  2630. const pinned = !renderer ? null : HTMLElement.prototype.querySelector.call(renderer, 'ytm-pinned-comment-badge-renderer');
  2631. const spanText = !pinned ? null : HTMLElement.prototype.querySelector.call(pinned, 'span.yt-core-attributed-string[role="text"]');
  2632. const tc = spanText ? spanText.textContent : '';
  2633. updatePinnedCommentBadge(parentNodeData, title, langTitle);
  2634. let idx;
  2635. if (tc && tc === spanText.textContent && (idx = tc.indexOf(title)) >= 0) {
  2636. spanText.textContent = tc.substring(0, idx) + langTitle + tc.substring(idx + title.length);
  2637. }
  2638. }
  2639.  
  2640. const contentTexts = (parentNodeData.contentText || 0).runs;
  2641. if (contentTexts && contentTexts.length >= 2) {
  2642. for (let aidx = 0; aidx < contentTexts.length; aidx++) {
  2643. const contentText = contentTexts[aidx] || 0;
  2644. if (contentText.italics !== true && isDisplayAsHandle(contentText.text)) {
  2645. mobileContentHandleAsync(parentNode, contentTexts, aidx);
  2646. }
  2647. }
  2648. }
  2649.  
  2650. }
  2651.  
  2652. } catch (e) {
  2653. console.warn(e);
  2654. }
  2655.  
  2656. } : async (anchor, hrefAttribute, mt) => {
  2657.  
  2658. try {
  2659. if (!hrefAttribute || !mt) return;
  2660. let parentNode = nodeParent(anchor);
  2661. let isCommentViewModel = 0;
  2662. while (parentNode instanceof Node) {
  2663. const cnt = insp(parentNode);
  2664. if (typeof cnt.is === 'string') {
  2665. if (cnt.commentEntity && !cnt._propertiesChanged) { isCommentViewModel = 2; break; } // 2024.04.30: ytd-comment-view-model (v3)
  2666. if (cnt.commentEntity && typeof cnt._propertiesChanged === 'function') { isCommentViewModel = 1; break; } // Feb 2024: ytd-comment-view-model (v1 & v2)
  2667. else if (typeof cnt.dataChanged === 'function') break;
  2668. }
  2669. parentNode = nodeParent(parentNode);
  2670. }
  2671. if (parentNode instanceof Node) { } else return;
  2672. const parentNodeController = insp(parentNode);
  2673.  
  2674. if (isCommentViewModel > 0) {
  2675.  
  2676. const commentEntity = parentNodeController.commentEntity || 0;
  2677. const commentEntityAuthor = commentEntity.author || 0;
  2678. const currentDisplayed = commentEntityAuthor.displayName || 0;
  2679. if (typeof currentDisplayed !== 'string') return;
  2680. if (!isDisplayAsHandle(currentDisplayed)) return;
  2681. if (!channelIdToHandle.has(mt)) {
  2682. channelIdToHandle.set(mt, {
  2683. handleText: currentDisplayed.trim(),
  2684. justPossible: true
  2685. });
  2686. }
  2687.  
  2688. // not working for CommentViewModel v3
  2689. if (typeof parentNodeController._propertiesChanged === 'function' && !parentNodeController._propertiesChanged159) {
  2690. const cntp = parentNodeController.constructor.prototype;
  2691. const c = cntp._propertiesChanged === parentNodeController._propertiesChanged ? cntp : parentNodeController;
  2692. c._propertiesChanged159 = c._propertiesChanged;
  2693. c._propertiesChanged = function () {
  2694. resetWhenPropChanged(this);
  2695. return this._propertiesChanged159.apply(this, arguments);
  2696. };
  2697. }
  2698.  
  2699. const fetchResult = await getDisplayName(mt);
  2700.  
  2701. if (fetchResult === null) return;
  2702.  
  2703. const { title, langTitle, externalId } = fetchResult;
  2704. const titleForDisplay = langTitle || title;
  2705. if (externalId !== mt) return; // channel id must be the same
  2706.  
  2707. // anchor href might be changed by external
  2708. if (!anchorIntegrityCheck(anchor, hrefAttribute)) return;
  2709.  
  2710. if (commentEntityAuthor.displayName === currentDisplayed && commentEntityAuthor.channelId === mt) {
  2711. commentEntityAuthor.displayName = titleForDisplay;
  2712.  
  2713. if (commentEntity.properties) {
  2714. if (commentEntity.properties.authorButtonA11y === currentDisplayed) {
  2715. commentEntity.properties.authorButtonA11y = titleForDisplay;
  2716. }
  2717.  
  2718. // the inside hyperlinks will be only converted if its parent author name is handle
  2719. const content = commentEntity.properties.content;
  2720. if (content && (content.commandRuns || 0).length >= 1) {
  2721. try {
  2722. await contentTextProcessViewModel(content);
  2723.  
  2724. // $0.polymerController.commentEntity = Object.assign({}, $0.polymerController.commentEntity , { properties: Object.assign({}, $0.polymerController.commentEntity.properties, { content: Object.assign({},$0.polymerController.commentEntity.properties.content) } )})
  2725.  
  2726. // parentNodeController.commentEntity.properties = Object.assign({}, parentNodeController.commentEntity.properties, {
  2727. // content: Object.assign({}, parentNodeController.commentEntity.properties.content)
  2728. // });
  2729.  
  2730. commentEntity.properties.content = Object.assign({}, content);
  2731. } catch (e) {
  2732. console.log(e);
  2733. }
  2734.  
  2735. }
  2736.  
  2737. }
  2738.  
  2739. if (commentEntityAuthor && parentNodeController.isAttached === true && parentNode.isConnected === true) {
  2740. commentEntityAuthor.jkrgx = 1;
  2741. addCSSRulesIfRequired();
  2742. }
  2743.  
  2744. let commentReplyDialogRenderer;
  2745.  
  2746. const actionButtonsCnt = insp((parentNodeController.$ || 0)['action-buttons'] || 0);
  2747. if (actionButtonsCnt) {
  2748.  
  2749. try {
  2750. commentReplyDialogRenderer = actionButtonsCnt.__data.replyButtonRenderer.navigationEndpoint.createCommentReplyDialogEndpoint.dialog.commentReplyDialogRenderer;
  2751. } catch (e) { }
  2752. }
  2753. const funcPromises = [];
  2754. commentReplyDialogRenderer && fixCommentReplyDialogRenderer(commentReplyDialogRenderer, funcPromises);
  2755.  
  2756.  
  2757. if (funcPromises.length >= 1) {
  2758. await Promise.all(funcPromises);
  2759. }
  2760.  
  2761. parentNodeController.commentEntity = Object.assign({}, parentNodeController.commentEntity);
  2762.  
  2763. }
  2764. } else {
  2765.  
  2766. const authorText = (parentNodeController.data || 0).authorText;
  2767. const currentDisplayed = getYTextContent(authorText || 0); // authorText.simpleText
  2768. if (typeof currentDisplayed !== 'string') return;
  2769. if (!isDisplayAsHandle(currentDisplayed)) return;
  2770.  
  2771. if (!channelIdToHandle.has(mt)) {
  2772. channelIdToHandle.set(mt, {
  2773. handleText: currentDisplayed.trim(),
  2774. justPossible: true
  2775. });
  2776. }
  2777.  
  2778. if (typeof parentNodeController.dataChanged === 'function' && !parentNodeController.dataChanged159) {
  2779. const cntp = parentNodeController.constructor.prototype;
  2780. const c = cntp.dataChanged === parentNodeController.dataChanged ? cntp : parentNodeController;
  2781. c.dataChanged159 = c.dataChanged;
  2782. c.dataChanged = function () {
  2783. resetWhenDataChanged(this);
  2784. return this.dataChanged159.apply(this, arguments);
  2785. };
  2786. }
  2787.  
  2788. const fetchResult = await getDisplayName(mt);
  2789.  
  2790. if (fetchResult === null) return;
  2791.  
  2792. const { title, langTitle, externalId } = fetchResult;
  2793. const titleForDisplay = langTitle || title;
  2794. if (externalId !== mt) return; // channel id must be the same
  2795.  
  2796. // anchor href might be changed by external
  2797. if (!anchorIntegrityCheck(anchor, hrefAttribute)) return;
  2798.  
  2799. const parentNodeData = parentNodeController.data;
  2800. const funcPromises = [];
  2801.  
  2802. if (parentNodeController.isAttached === true && parentNode.isConnected === true && typeof parentNodeData === 'object' && parentNodeData && parentNodeData.authorText === authorText) {
  2803.  
  2804. if (getYTextContent(authorText) !== currentDisplayed) return;
  2805. const currentDisplayTrimmed = verifyAndConvertHandle(currentDisplayed, fetchResult);
  2806. const cSimpleText = (getYTextContent(parentNodeData.authorText || 0) || '');
  2807. if (currentDisplayTrimmed && currentDisplayed !== titleForDisplay && cSimpleText === currentDisplayed) {
  2808.  
  2809. const md = Object.assign({}, parentNodeData);
  2810.  
  2811. // the inside hyperlinks will be only converted if its parent author name is handle
  2812. const contentTexts = (md.contentText || 0).runs;
  2813. if (contentTexts && contentTexts.length >= 1) {
  2814. for (let aidx = 0; aidx < contentTexts.length; aidx++) {
  2815. const r = contentTextProcess(md, contentTexts, aidx);
  2816. if (r instanceof Promise) funcPromises.push(r);
  2817. }
  2818. }
  2819.  
  2820. const authorCommentBadgeAuthorText = (((md.authorCommentBadge || 0).authorCommentBadgeRenderer || 0).authorText || 0);
  2821.  
  2822. const authorCommentBadgeAuthorTextNew = authorCommentBadgeAuthorText ? setYTextContent(authorCommentBadgeAuthorText, (currentText) => {
  2823. if ((currentText || '').trim() === cSimpleText.trim()) {
  2824. return (currentText || '').replace((currentText || '').trim(), titleForDisplay);
  2825. } else {
  2826. return currentText;
  2827. }
  2828. }) : null;
  2829.  
  2830. // parentNode.data = Object.assign({}, { jkrgx: 1 });
  2831. const authorTextNew = setYTextContent(md.authorText, (t) => t.replace(currentDisplayTrimmed, titleForDisplay))
  2832. md.authorText = authorTextNew;
  2833.  
  2834. if (authorCommentBadgeAuthorTextNew) {
  2835. const authorCommentBadgeRendererNew = Object.assign({}, md.authorCommentBadge.authorCommentBadgeRenderer, { authorText: authorCommentBadgeAuthorTextNew });
  2836. md.authorCommentBadge = Object.assign({}, md.authorCommentBadge, { authorCommentBadgeRenderer: authorCommentBadgeRendererNew });
  2837. }
  2838.  
  2839. let setReplyEditableText = false;
  2840. if (((((((((parentNodeData.actionButtons || 0).commentActionButtonsRenderer || 0).replyButton || 0).buttonRenderer || 0).navigationEndpoint || 0).createCommentReplyDialogEndpoint || 0).dialog || 0).commentReplyDialogRenderer || 0).editableText) {
  2841. setReplyEditableText = true;
  2842. }
  2843. if (setReplyEditableText) {
  2844. const commentReplyDialogRenderer = parentNodeData.actionButtons.commentActionButtonsRenderer.replyButton.buttonRenderer.navigationEndpoint.createCommentReplyDialogEndpoint.dialog.commentReplyDialogRenderer;
  2845. fixCommentReplyDialogRenderer(commentReplyDialogRenderer, funcPromises);
  2846.  
  2847. }
  2848.  
  2849. if (UPDATE_PIN_NAME) {
  2850. updatePinnedCommentBadge(md, title, langTitle);
  2851. }
  2852.  
  2853. if (funcPromises.length >= 1) {
  2854. await Promise.all(funcPromises);
  2855. }
  2856.  
  2857. if (parentNodeController.isAttached === true && parentNode.isConnected === true) {
  2858. parentNodeController.data = Object.assign({}, md, { jkrgx: 1 });
  2859. addCSSRulesIfRequired();
  2860. }
  2861.  
  2862.  
  2863. }
  2864.  
  2865. }
  2866. }
  2867. } catch (e) {
  2868. console.warn(e);
  2869. }
  2870.  
  2871. }
  2872.  
  2873. /**
  2874. *
  2875. * @param {string} url
  2876. * @returns
  2877. */
  2878. const channelUrlUnify = (url) => {
  2879.  
  2880. if (typeof url !== 'string') return url; // type unchanged
  2881. let c0 = url.charAt(0);
  2882. if (c0 === '/') return url;
  2883. let i = url.indexOf('.youtube.com/');
  2884. if (i >= 1) url = url.substring(i + '.youtube.com/'.length - 1);
  2885. else if (url.charAt(0) !== '/') url = '/' + url;
  2886. return url
  2887.  
  2888. }
  2889.  
  2890. const parentYtElement = (node, nodeName, maxN) => {
  2891. for (let pDom = nodeParent(node); (pDom instanceof HTMLElement) && maxN-- > 0; pDom = nodeParent(pDom)) {
  2892. if (pDom.nodeName === nodeName) {
  2893. return pDom;
  2894. }
  2895. }
  2896. return null;
  2897. }
  2898.  
  2899. const checkShortsChannelName = (rchannelNameDOM) => {
  2900. let browseId = '';
  2901. const pDom = parentYtElement(rchannelNameDOM, 'YTD-REEL-PLAYER-HEADER-RENDERER', 18);
  2902.  
  2903. if (pDom instanceof HTMLElement) {
  2904. const pDomController = insp(pDom);
  2905. const channelTitleText = (pDomController.data || 0).channelTitleText || 0;
  2906. const runs = ((channelTitleText || 0).runs || 0);
  2907. if (getYTextType(channelTitleText) === 4) {
  2908. const browserEndpoint = (((runs[0] || 0).navigationEndpoint || 0).browseEndpoint || 0);
  2909. browseId = (browserEndpoint.browseId || '');
  2910. const currentDisplayText = runs[0].text || ''
  2911.  
  2912. if (isUCBrowserId(browseId) && isDisplayAsHandle(currentDisplayText)) {
  2913.  
  2914. if (!channelIdToHandle.has(browseId)) {
  2915. channelIdToHandle.set(browseId, {
  2916. handleText: currentDisplayText.trim(),
  2917. justPossible: true
  2918. });
  2919. }
  2920.  
  2921. // let anchorNode = elementQS(rchannelNameDOM, `a[href="${browserEndpoint.canonicalBaseUrl}"]`);
  2922. // anchorNode && anchorNode.setAttribute('jkrgy', browseId); // for checking
  2923. // anchorNode &&
  2924. getDisplayName(browseId).then(fetchResult => {
  2925.  
  2926. if (fetchResult === null) return;
  2927.  
  2928. const { title, langTitle, externalId } = fetchResult;
  2929. const titleForDisplay = langTitle || title;
  2930. if (externalId !== browseId) return; // channel id must be the same
  2931.  
  2932. const hDom = pDom;
  2933. const hDomHostElement = hDom.hostElement || hDom;
  2934. const hDomController = insp(hDom);
  2935. const hData = (hDomController || 0).data || 0;
  2936. const hChannelTitleText = (hData || 0).channelTitleText || 0;
  2937. const runs = ((hChannelTitleText).runs || 0);
  2938. if (getYTextType(hChannelTitleText) === 4 && (runs[0] || 0).text === currentDisplayText && hDomHostElement.isConnected === true) {
  2939.  
  2940. const channelTitleTextNew = { runs: [Object.assign({}, runs[0], { text: titleForDisplay })] }
  2941.  
  2942. hDomController.data = Object.assign({}, hDomController.data, { channelTitleText: channelTitleTextNew, rSk0e: 1 });
  2943. addCSSRulesIfRequired();
  2944.  
  2945. byPassCheckStore.delete(rchannelNameDOM);
  2946. }
  2947.  
  2948. })
  2949. }
  2950.  
  2951. }
  2952. }
  2953. return browseId
  2954. }
  2955.  
  2956. let firstDOMCheck = false;
  2957. const updateDisplayNameFn = (channelId, newName) => {
  2958. const cached = displayNameResStore.get(channelId);
  2959. if (typeof (cached || 0).title === 'string') {
  2960. cached.title = newName;
  2961. } else if (!cached) {
  2962. displayNameResStore.set(channelId, {
  2963. channelUrl: `https://www.youtube.com/channel/${channelId}`,
  2964. externalId: `${channelId}`,
  2965. ownerUrls: [],
  2966. title: newName,
  2967. vanityChannelUrl: null,
  2968. verified123: true
  2969. });
  2970. } else {
  2971. console.warn('unexpected display name cache', cached);
  2972. }
  2973. };
  2974. const firstDOMChecker = isMobile ? () => {
  2975.  
  2976. let playerMicroformatRenderer = null;
  2977. try {
  2978. playerMicroformatRenderer = window.ytInitialPlayerResponse.microformat.playerMicroformatRenderer
  2979. } catch (e) { }
  2980. if (playerMicroformatRenderer && playerMicroformatRenderer.ownerChannelName && playerMicroformatRenderer.ownerProfileUrl) {
  2981. cfg.ownerChannelName = playerMicroformatRenderer.ownerChannelName;
  2982. cfg.ownerProfileUrl = playerMicroformatRenderer.ownerProfileUrl.replace(/[\S]+youtube.com\//, 'youtube.com/');
  2983. }
  2984.  
  2985. const channelNameDOM = document.querySelector('ytm-slim-owner-renderer');
  2986. const channelNameCData = !channelNameDOM ? null : insp(channelNameDOM).data;
  2987.  
  2988. if (channelNameCData) {
  2989. let mainChannelUrl = null;
  2990. let mainFormattedNameUrl = null;
  2991. let mainChannelText = null;
  2992. let mainFormattedNameText = null;
  2993. let mainChannelBrowseId = null;
  2994.  
  2995. try {
  2996. mainChannelUrl = channelUrlUnify(channelNameCData.channelUrl) // channelUrlUnify("http://www.youtube.com/channel/UC5CwaMl1eIgY8h02uZw7u8A")
  2997. mainFormattedNameUrl = channelUrlUnify(channelNameCData.title.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl) // channelUrlUnify("/channel/UC5CwaMl1eIgY8h02uZw7u8A")
  2998. mainChannelText = channelNameCData.channelName
  2999. mainFormattedNameText = channelNameCData.title.runs[0].text
  3000. mainChannelBrowseId = channelNameCData.navigationEndpoint.browseEndpoint.browseId;
  3001. } catch (e) { }
  3002.  
  3003. if (mainChannelUrl === mainFormattedNameUrl && mainChannelText === mainFormattedNameText && typeof mainChannelUrl === 'string' && typeof mainChannelText === 'string' && mainChannelBrowseId && isUCBrowserId(mainChannelBrowseId)) {
  3004.  
  3005. const channelId = mainChannelBrowseId;
  3006. if (channelId) {
  3007. if (mainChannelText.startsWith('@')) return;
  3008. if (mainChannelText.trim() !== mainChannelText) return;
  3009. updateDisplayNameFn(channelId, mainChannelText);
  3010. }
  3011.  
  3012.  
  3013. }
  3014. }
  3015.  
  3016. } : () => {
  3017.  
  3018. const channelNameDOM = document.querySelector('ytd-channel-name.ytd-video-owner-renderer');
  3019. const channelNameCnt = insp(channelNameDOM);
  3020. const channelNameCData = (channelNameCnt || 0).__data || (channelNameDOM || 0).__data;
  3021. if (channelNameCData) {
  3022. let mainChannelUrl = null;
  3023. let mainFormattedNameUrl = null;
  3024. let mainChannelText = null;
  3025. let mainFormattedNameText = null;
  3026. let mainChannelBrowseId = null;
  3027. try {
  3028. mainChannelUrl = channelNameCData.channelName.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl
  3029. mainFormattedNameUrl = channelNameCData.formattedName.runs[0].navigationEndpoint.browseEndpoint.canonicalBaseUrl
  3030. mainChannelText = channelNameCData.channelName.runs[0].text
  3031. mainFormattedNameText = channelNameCData.formattedName.runs[0].text
  3032. mainChannelBrowseId = channelNameCData.channelName.runs[0].navigationEndpoint.browseEndpoint.browseId;
  3033. } catch (e) { }
  3034.  
  3035. if (mainChannelUrl === mainFormattedNameUrl && mainChannelText === mainFormattedNameText && typeof mainChannelUrl === 'string' && typeof mainChannelText === 'string' && mainChannelBrowseId && isUCBrowserId(mainChannelBrowseId)) {
  3036.  
  3037. const m = exactHandleUrl(mainChannelUrl, false);
  3038.  
  3039. if (m && mainChannelText !== m) {
  3040.  
  3041. const channelHandle = m[1];
  3042. if (/[^-_a-zA-Z0-9.]/.test(mainChannelText)) {
  3043. langPreferredDisplayNameMap.set(channelHandle, mainChannelText);
  3044. }
  3045.  
  3046. } else {
  3047.  
  3048. const channelId = mainChannelBrowseId;
  3049. if (channelId) {
  3050. if (mainChannelText.startsWith('@')) return;
  3051. if (mainChannelText.trim() !== mainChannelText) return;
  3052. updateDisplayNameFn(channelId, mainChannelText);
  3053. }
  3054.  
  3055. }
  3056. }
  3057. }
  3058.  
  3059. };
  3060.  
  3061. // let newAnchorAdded = false;
  3062. /*
  3063.  
  3064. const intersectionobserver = new IntersectionObserver((entries) => {
  3065. let anchorAppear = false;
  3066. for (const entry of entries) {
  3067. if (entry.isIntersecting === true) {
  3068. anchorAppear = true;
  3069. break;
  3070. }
  3071. }
  3072. if (anchorAppear && newAnchorAdded) {
  3073. newAnchorAdded = false; // stacking will be only reset when one or more anchor added to DOM.
  3074. mutex.nextIndex = 0; // higher pirority for new elements being shown
  3075. }
  3076. }, {
  3077. rootMargin:"0px 0px 0px 0px",
  3078. threshold:1
  3079. })
  3080. */
  3081.  
  3082.  
  3083. /* globals WeakRef:false */
  3084.  
  3085. /** @type {(o: Object | null) => WeakRef | null} */
  3086. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  3087.  
  3088. /** @type {(wr: Object | null) => Object | null} */
  3089. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  3090.  
  3091. let doShortsChecking = false;
  3092. let doDescriptionChecking = true;
  3093.  
  3094. let lastNewAnchorLastWR = null;
  3095.  
  3096. const pipelineForDOMMutations = createPipeline();
  3097.  
  3098. const byPassCheckStore = new WeakSet();
  3099.  
  3100. const mutObserverForShortsChannelName = new MutationObserver((mutations) => {
  3101. for (const mutation of mutations) {
  3102. const target = mutation.target;
  3103. if (!target) continue;
  3104. const rchannelNameElm = parentYtElement(target, 'YTD-CHANNEL-NAME', 18);
  3105. if (!rchannelNameElm) continue;
  3106. const rchannelNameCnt = insp(rchannelNameElm);
  3107. if ((rchannelNameCnt.data || 0).rSk0e) continue;
  3108. const channelId = rchannelNameElm.getAttribute('jkrgy');
  3109. if (channelId) {
  3110. if (byPassCheckStore.has(rchannelNameElm)) continue;
  3111. const handleObj = channelIdToHandle.get(channelId);
  3112. if (handleObj && handleObj.handleText && !handleObj.justPossible) {
  3113. const textDom = ((rchannelNameCnt.$ || 0).text || 0);
  3114. if (textDom) {
  3115. let t = textDom.textContent.trim()
  3116. if (t === handleObj.handleText || t === '') {
  3117. rchannelNameElm.removeAttribute('jkrgy');
  3118. // Promise.resolve().then(domChecker);
  3119. }
  3120. }
  3121. }
  3122. }
  3123. }
  3124. });
  3125.  
  3126. let domCheckScheduledForShorts = false;
  3127. const domCheckSelectorForShorts = [
  3128. 'ytd-channel-name.ytd-reel-player-header-renderer:not([jkrgy]) a.yt-simple-endpoint[href]'
  3129. ].join(', ');
  3130.  
  3131.  
  3132. const domCheckerForShorts = async () => {
  3133.  
  3134. if (domCheckScheduledForShorts) return;
  3135. if (document.querySelector(domCheckSelectorForShorts) === null) {
  3136. return;
  3137. }
  3138. domCheckScheduledForShorts = true;
  3139.  
  3140. await pipelineForDOMMutations(async () => {
  3141.  
  3142. domCheckScheduledForShorts = false;
  3143. // setTimeout(()=>{ // delay due to data update is after [is-active] switching
  3144.  
  3145. try {
  3146.  
  3147. const nodes = document.querySelectorAll(domCheckSelectorForShorts);
  3148. if (nodes.length === 0) {
  3149. return;
  3150. }
  3151.  
  3152. for (const node of nodes) {
  3153.  
  3154. const rchannelNameElm = parentYtElement(node, 'YTD-CHANNEL-NAME', 18);
  3155. if (rchannelNameElm) {
  3156. mutObserverForShortsChannelName.observe(rchannelNameElm, {
  3157. childList: true,
  3158. subtree: true
  3159. });
  3160. byPassCheckStore.add(rchannelNameElm);
  3161. const channelId = checkShortsChannelName(rchannelNameElm);
  3162. rchannelNameElm.setAttribute('jkrgy', channelId || '');
  3163. }
  3164.  
  3165. }
  3166.  
  3167. } catch (e) {
  3168. console.log('error occur', e);
  3169. }
  3170.  
  3171. });
  3172. domCheckScheduledForShorts = false;
  3173.  
  3174. };
  3175.  
  3176. /** @param {Element} s */
  3177. const domCheckerForDescriptionAnchor = (s) => {
  3178.  
  3179. findTextNodes(s, (textNode) => {
  3180. const p = textNode.nodeValue;
  3181. const index = p.indexOf('@');
  3182. if (index < 0) return;
  3183. const q = p.substring(index);
  3184. const b = isDisplayAsHandle(q);
  3185. if (b) {
  3186. const href = s.getAttribute('href')
  3187.  
  3188. const channelId = obtainChannelId(href);
  3189. if (channelId) {
  3190. const h = q.trim();
  3191.  
  3192. if (!firstDOMCheck) {
  3193. firstDOMCheck = true;
  3194. USE_CHANNEL_META && firstDOMChecker();
  3195. }
  3196.  
  3197. if (!channelIdToHandle.has(channelId)) {
  3198. channelIdToHandle.set(channelId, {
  3199. handleText: h,
  3200. justPossible: true
  3201. });
  3202. }
  3203.  
  3204. s.setAttribute('jkrgy', channelId);
  3205. getDisplayName(channelId).then(fetchResult => {
  3206.  
  3207. if (fetchResult === null) return;
  3208.  
  3209. const { title, langTitle, externalId } = fetchResult;
  3210. const titleForDisplay = langTitle || title;
  3211. if (externalId !== channelId) return; // channel id must be the same
  3212.  
  3213. const anchorElement = s;
  3214. const anchorTextNode = textNode;
  3215.  
  3216. if (anchorElement.isConnected === true && anchorTextNode.isConnected === true && anchorElement.contains(anchorTextNode) && anchorTextNode.nodeValue === p && s.getAttribute('href') === href) {
  3217.  
  3218. anchorTextNode.nodeValue = `${p.substring(0, index)}${q.replace(h, titleForDisplay)}`;
  3219.  
  3220. }
  3221.  
  3222. });
  3223. }
  3224. }
  3225. return true;
  3226. });
  3227.  
  3228. }
  3229.  
  3230. // domCheckerForDescription: To be reviewed
  3231. // Example: https://www.youtube.com/watch?v=1NL-sIP9p_U
  3232. const domCheckerForDescription = isMobile ? () => {
  3233. // example https://m.youtube.com/watch?v=jKt4Ah47L7Q
  3234. 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])')) {
  3235. s.setAttribute('dxcpj', '');
  3236. domCheckerForDescriptionAnchor(s);
  3237. }
  3238. } : () => {
  3239. // example https://www.youtube.com/watch?v=jKt4Ah47L7Q
  3240. 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])')) {
  3241. s.setAttribute('dxcpj', '');
  3242. domCheckerForDescriptionAnchor(s);
  3243. }
  3244. };
  3245.  
  3246. const domCheckScheduledBools = [];
  3247. const domCheckScheduledSelectors = isMobile ? [
  3248. 'a[aria-label^="@"][href*="channel/"]:not([jkrgy])', // old; Before Feb 2024
  3249. 'a.comment-icon-container[href*="/@"]:not([jkrgy])', // Feb 2024
  3250. 'a.comment-icon-container[href*="channel/"]:not([jkrgy])' // Mar 2024
  3251. ] : [
  3252. 'a[id][href*="channel/"]:not([jkrgy])', // old; Before Feb 2024
  3253. 'a.yt-simple-endpoint.style-scope[id][href^="/@"]:not([jkrgy])', // Feb 2024
  3254. 'a.yt-simple-endpoint.style-scope[id][href^="http://www.youtube.com/@"]:not([jkrgy])' // Dec 2024
  3255. ];
  3256.  
  3257. const newAuthorAnchorsProceed = async (newAnchors) => {
  3258.  
  3259. const cNewAnchorFirst = newAnchors[0]; // non-null
  3260. const cNewAnchorLast = newAnchors[newAnchors.length - 1]; // non-null
  3261. /** @type {HTMLElement | null} */
  3262. const lastNewAnchorLast = kRef(lastNewAnchorLastWR); // HTMLElement | null
  3263. lastNewAnchorLastWR = mWeakRef(cNewAnchorLast);
  3264.  
  3265. if (!firstDOMCheck) {
  3266. firstDOMCheck = true;
  3267. USE_CHANNEL_META && firstDOMChecker();
  3268. }
  3269.  
  3270. await Promise.resolve();
  3271.  
  3272. if (lastNewAnchorLast && mutex.nextIndex >= 1) {
  3273. if ((lastNewAnchorLast.compareDocumentPosition(cNewAnchorFirst) & 2) === 2) {
  3274. mutex.nextIndex = 0;
  3275. }
  3276. await Promise.resolve();
  3277. }
  3278.  
  3279. // newAnchorAdded = true;
  3280. for (const anchor of newAnchors) {
  3281. const hrefAttribute = anchor.getAttribute('href');
  3282. const hrefV = ytPathnameExtract(hrefAttribute);
  3283. let mChannelId = '';
  3284. const ytElm = isMobile ? closestYtmData(anchor) : closestYtParent(anchor);
  3285. if (ytElm) {
  3286. const cnt = insp(ytElm);
  3287. const { browseId, canonicalBaseUrl } = getAuthorBrowseEndpoint(cnt) || 0;
  3288. let wChannelId = '';
  3289. if (!browseId && !canonicalBaseUrl && cnt) {
  3290. // Dec 2024; no browserEndPoint -> urlEndpoint -> channelId not available
  3291. // comment view model -> commentEntity.author.channelId
  3292. const author = ((cnt.__data || cnt.data || 0).commentEntity || 0).author || 0;
  3293. if (author) {
  3294. const { channelId } = author;
  3295. const innertubeCommand = ((author || 0).channelPageEndpoint || 0).innertubeCommand || 0;
  3296. const endpoint = innertubeCommand.browseEndpoint || innertubeCommand.urlEndpoint || author.browseEndpoint || browseEndpoint.urlEndpoint || 0;
  3297. const url = endpoint.canonicalBaseUrl || endpoint.url || 0;
  3298. if (channelId && ytPathnameExtract(url) === hrefV && channelId.startsWith('UC') && /^UC[-_a-zA-Z0-9+=.]{22}$/.test(channelId)) {
  3299. wChannelId = channelId;
  3300. }
  3301. }
  3302. } else if (browseId && ytPathnameExtract(canonicalBaseUrl) === hrefV && browseId.startsWith('UC') && /^UC[-_a-zA-Z0-9+=.]{22}$/.test(browseId)) {
  3303. wChannelId = browseId;
  3304. }
  3305.  
  3306. if (wChannelId) {
  3307.  
  3308. const hrefType = getHrefType(hrefV);
  3309. // console.log(599, hrefV, hrefType)
  3310. if (hrefType === 1) {
  3311. if (hrefV === `/channel/${wChannelId}`) {
  3312. let authorText = null;
  3313. if (isMobile) { // mobile only
  3314. authorText = (cnt._commentData || cnt.data || ytElm._commentData || ytElm.data || 0).authorText || 0;
  3315. } else if (!isMobile) {
  3316. authorText = (cnt.data || 0).authorText || 0;
  3317. }
  3318. if (authorText) {
  3319. const text = getYTextContent(authorText);
  3320. if (typeof text === 'string' && text.startsWith('@') && exactHandleText(text, true)) {
  3321. channelIdToHandle.set(wChannelId, {
  3322. handleText: `${text}`,
  3323. justPossible: true
  3324. });
  3325. }
  3326. }
  3327. mChannelId = wChannelId;
  3328. }
  3329. } else if (hrefType === 2) {
  3330. const handle = hrefV.substring(2);
  3331. channelIdToHandle.set(wChannelId, {
  3332. handleText: `@${handle}`
  3333. });
  3334. mChannelId = wChannelId;
  3335. }
  3336.  
  3337. }
  3338.  
  3339. if (!mChannelId && hrefV) { // fallback
  3340.  
  3341. // author-text or name
  3342. // normal url: /channel/xxxxxxx
  3343. // Improve YouTube! - https://www.youtube.com/channel/xxxxxxx/videos
  3344.  
  3345. const temp = obtainChannelId(hrefV); // string, can be empty
  3346. if (temp) {
  3347. mChannelId = temp;
  3348. }
  3349.  
  3350. }
  3351. }
  3352. anchor.setAttribute('jkrgy', mChannelId);
  3353. if (mChannelId) {
  3354. domCheckAsync(anchor, hrefAttribute, mChannelId);
  3355. }
  3356. }
  3357.  
  3358. }
  3359.  
  3360. const closestYtParent = (dom) => {
  3361. while (dom instanceof Element) {
  3362. if (typeof dom.is === 'string' && dom.is) return dom;
  3363. dom = dom.parentNode;
  3364. }
  3365. return null;
  3366. };
  3367. const closestYtmData = (dom) => {
  3368. while (dom instanceof Element) {
  3369. if (typeof (dom._commentData || dom.data || 0) === 'object') return dom;
  3370. dom = dom.parentNode;
  3371. }
  3372. return null;
  3373. };
  3374. /*
  3375. const cntWithParent = (cnt, parentSelector) => {
  3376. const hostElement = cnt.hostElement || cnt;
  3377. if (hostElement instanceof HTMLElement) {
  3378. return hostElement.closest(parentSelector);
  3379. }
  3380. return null;
  3381. }
  3382. */
  3383. const noAuthorDataSet = new Set();
  3384. const getAuthorBrowseEndpoint = (cnt) => {
  3385. let d;
  3386. let haveData = false;
  3387. if (isMobile) {
  3388. const data = ((cnt || 0).data || 0);
  3389. if (data) {
  3390. haveData = true;
  3391. if (d = (data.authorNameEndpoint || data.authorEndpoint)) return d.browseEndpoint || null;
  3392. }
  3393. } else {
  3394. const __data = ((cnt || 0).__data || 0);
  3395. if (__data) {
  3396. haveData = true;
  3397. if (d = __data.authorTextCommand) return d.browseEndpoint || null; // ytd-comment-renderer
  3398. if (d = (__data.authorNameEndpoint || __data.authorEndpoint)) return d.browseEndpoint || null; // ytd-comment-view-model (v1 & v2)
  3399. if ((d = (__data.data || 0)) && (d = (d.authorNameEndpoint || d.authorEndpoint))) return d.browseEndpoint || null; // ytd-author-comment-badge-renderer
  3400. // note: authorNameEndpoint instead of authorEndpoint for ytd-comment-view-model since 2024.04
  3401.  
  3402. } else {
  3403.  
  3404. if ((d = ((cnt || 0).data || 0)) && (d = (d.authorTextCommand || d.authorNameEndpoint || d.authorEndpoint))) return d.browseEndpoint || null; // ytd-comment-view-model (2024.05.25 ?)
  3405.  
  3406. if (d = (cnt.authorTextCommand || cnt.authorNameEndpoint || cnt.authorEndpoint)) return d.browseEndpoint || null; // ytd-comment-view-model (2024.04.30) (v3) (?)
  3407.  
  3408. }
  3409.  
  3410. }
  3411. if (haveData) {
  3412. const tag = cnt.is;
  3413. if (!noAuthorDataSet.has(tag)) {
  3414. noAuthorDataSet.add(tag);
  3415. if (tag === 'ytd-video-description-infocards-section-renderer') {
  3416. return null; // skip console logging
  3417. } else if (tag === 'ytd-thumbnail') {
  3418. return null;
  3419. } else if (tag === 'ytd-structured-description-channel-lockup-renderer') {
  3420. return null;
  3421. } else if (tag === 'ytd-rich-grid-media') {
  3422. return null;
  3423. } else if (tag === 'ytd-rich-metadata-renderer') {
  3424. return null;
  3425. } else if (tag === 'ytd-guide-entry-renderer') {
  3426. return null;
  3427. } else if (tag === 'ytd-grid-channel-renderer') {
  3428. return null;
  3429. } else if (tag === 'ytd-channel-renderer') {
  3430. return null;
  3431. }
  3432. console.log('no browseEndpoint can be found', tag);
  3433. }
  3434. } else {
  3435. const is = (cnt || 0).is
  3436. if (is === 'ytd-guide-entry-renderer') {
  3437.  
  3438. } else {
  3439. console.log('no browseEndpoint can be found', is, 'NO DATA');
  3440. }
  3441. }
  3442. return null;
  3443. };
  3444.  
  3445. const domAuthorNameCheckN = async (kq) => {
  3446. if (domCheckScheduledBools[kq]) return;
  3447. const selector = domCheckScheduledSelectors[kq];
  3448. if (!selector || document.querySelector(selector) === null) return;
  3449. domCheckScheduledBools[kq] = true;
  3450. await pipelineForDOMMutations(async () => {
  3451. domCheckScheduledBools[kq] = false;
  3452. try {
  3453. const newAnchors = document.querySelectorAll(selector);
  3454. if (newAnchors.length === 0) return;
  3455. await newAuthorAnchorsProceed(newAnchors);
  3456. } catch (e) {
  3457. console.log('error occur', e);
  3458. }
  3459. });
  3460. domCheckScheduledBools[kq] = false;
  3461. };
  3462.  
  3463. let _lastAnchorCount = -1;
  3464. const anchorCollection = document.getElementsByTagName('A');
  3465.  
  3466. /*
  3467. const collectionChangeObserverFn = (collection)=>{
  3468. let _len = -1;
  3469. let ws = null;
  3470. ws = new WeakSet(collection);
  3471. return {check: ()=>{
  3472. const lastLen = _len;
  3473. const len = collection.length;
  3474. if(len !== lastLen){
  3475. ws = new WeakSet(collection);
  3476. return true;
  3477. }
  3478. for (const elm of collection) {
  3479. if (!ws.has(elm)){
  3480. ws = new WeakSet(collection);
  3481. return true;
  3482. }
  3483. }
  3484. return false;
  3485. }}
  3486.  
  3487. }
  3488. */
  3489.  
  3490. const domChecker = (mutations) => {
  3491.  
  3492. const currAnchorCount = anchorCollection.length;
  3493. const lastAnchorCount = _lastAnchorCount;
  3494. _lastAnchorCount = currAnchorCount;
  3495. if (currAnchorCount === 0) return;
  3496.  
  3497. if (mutations && lastAnchorCount === currAnchorCount) {
  3498. // test to skip
  3499. let state = 0;
  3500. let addedNonElement = false;
  3501. for (const mutation of mutations) {
  3502. const addedNodes = mutation.addedNodes || 0;
  3503. const removedNodes = mutation.removedNodes || 0;
  3504. const target = mutation.target || 0;
  3505. if (target.nodeName === 'YT-FORMATTED-STRING') continue; // ignore changes in <yt-formatted-string>
  3506. if (addedNodes.length >= 1) {
  3507. state |= 1;
  3508. if (!addedNonElement) {
  3509. for (const addedNode of addedNodes) {
  3510. if (!(addedNode instanceof Element)) {
  3511. addedNonElement = true;
  3512. break;
  3513. }
  3514. }
  3515. }
  3516. if (state & 2) break;
  3517. } else if (removedNodes.length >= 1) {
  3518. state |= 2;
  3519. if (state & 1) break;
  3520. } else {
  3521. state |= 4;
  3522. break;
  3523. }
  3524. }
  3525. if (state === 1) {
  3526. if (!addedNonElement) return; // no change in anchor
  3527. } else if (state === 2) {
  3528. return; // only removal
  3529. } else if (state === 0) {
  3530. return; // no recognized mutation
  3531. }
  3532. }
  3533.  
  3534. doShortsChecking && domCheckerForShorts();
  3535. doDescriptionChecking && domCheckerForDescription();
  3536. for (let i = 0; i < domCheckScheduledSelectors.length; i++) domAuthorNameCheckN(i);
  3537.  
  3538. };
  3539.  
  3540.  
  3541. /** @type {MutationObserver | null} */
  3542. let domObserver = null;
  3543.  
  3544. /** @type {(callback: (event: Event)=>{})} */
  3545. const onPageFetched = isMobile ? (callback) => {
  3546.  
  3547. let state = 0;
  3548.  
  3549. window.addEventListener('state-change', function (evt) {
  3550. if (state === 1) return; // ignore intermediate events
  3551. state = 1; // before value: 0 or 2;
  3552. callback(evt);
  3553. }, false);
  3554.  
  3555. window.addEventListener('state-navigateend', function (evt) { // fallback
  3556. let inSeq = state === 1;
  3557. state = 2; // 2 when loaded
  3558. if (!inSeq) callback(evt);
  3559. }, false);
  3560.  
  3561. } : (callback) => {
  3562.  
  3563. let state = 0;
  3564.  
  3565. document.addEventListener('yt-page-data-fetched', function (evt) {
  3566. if (state === 1) return; // ignore intermediate events if any
  3567. state = 1; // before value: 0 or 2;
  3568. callback(evt);
  3569. }, false);
  3570.  
  3571. document.addEventListener('yt-navigate-finish', function (evt) { // fallback
  3572. let inSeq = state === 1;
  3573. state = 2; // 2 when loaded
  3574. if (!inSeq) callback(evt);
  3575. }, false);
  3576.  
  3577. };
  3578.  
  3579.  
  3580. // let mActive = false;
  3581. // let lastContent = '';
  3582. const removeAttrs = () => {
  3583. for (const s of document.querySelectorAll('a[href][dxcpj]')) {
  3584. s.removeAttribute('dxcpj');
  3585. }
  3586. for (const s of document.querySelectorAll('a[href][jkrgy]')) {
  3587. s.removeAttribute('jkrgy');
  3588. }
  3589. }
  3590.  
  3591. // setInterval(() => {
  3592. // if (!mActive) return;
  3593. // let content = (document.querySelector('ytd-comments-header-renderer #title') || 0).textContent || '';
  3594. // if (content !== lastContent) {
  3595. // lastContent = content;
  3596. // removeAttrs();
  3597. // }
  3598. // }, 400);
  3599.  
  3600. /**
  3601. *
  3602. * @param {Node} app
  3603. * @returns
  3604. */
  3605. function setupOnPageFetched(app) {
  3606.  
  3607. firstDOMCheck = false;
  3608.  
  3609. if (!cfg['INNERTUBE_API_KEY']) {
  3610. console.warn("Userscript Error: INNERTUBE_API_KEY is not found.");
  3611. return;
  3612. }
  3613.  
  3614. if (CHANGE_FOR_SHORTS_CHANNEL_NAME && location.pathname.startsWith('/shorts/')) {
  3615. doShortsChecking = true;
  3616. } else {
  3617. doShortsChecking = false;
  3618. }
  3619.  
  3620. _lastAnchorCount = -1;
  3621.  
  3622. if (!domObserver) {
  3623. domObserver = new MutationObserver(domChecker);
  3624. } else {
  3625. domObserver.takeRecords();
  3626. domObserver.disconnect();
  3627. }
  3628.  
  3629. // lastContent = (document.querySelector('ytd-comments-header-renderer #title') || 0).textContent || '';
  3630. // mActive = true;
  3631. removeAttrs();
  3632.  
  3633. domObserver.observe(app, { childList: true, subtree: true, attributes: true, attributeFilter: ['jkrgy', 'href', 'dxcpj'] });
  3634. domChecker();
  3635.  
  3636. }
  3637.  
  3638. /**
  3639. *
  3640. * @param {Object|null} dest
  3641. * @param {Object} ytcfg
  3642. * @param {string[]} keys
  3643. * @returns
  3644. */
  3645. function getYtConfig(dest, ytcfg, keys) {
  3646. dest = dest || {};
  3647. if (typeof ytcfg.get === 'function') { // mobile
  3648. for (const key of keys) {
  3649. dest[key] = ytcfg.get(key);
  3650. }
  3651. } else {
  3652. const cfgData = ytcfg.data_ || ytcfg.__data || ytcfg._data || ytcfg.data || ytcfg; // dekstop - ytcfg.data_
  3653. for (const key of keys) {
  3654. dest[key] = cfgData[key];
  3655. }
  3656. }
  3657. return dest;
  3658. }
  3659.  
  3660. /** @type { (evt: Object) => Element | null } */
  3661. const getPageApp = isMobile
  3662. ? ((evt) => (document.querySelector('ytm-app#app') || document.querySelector('ytm-app')))
  3663. : ((evt) => ((evt || 0).target || document.querySelector('ytd-app')));
  3664.  
  3665. onPageFetched(async (evt) => {
  3666. const app = getPageApp(evt);
  3667. console.assert(app);
  3668. const ytcfg = ((window || 0).ytcfg || 0);
  3669. getYtConfig(cfg, ytcfg, ['INNERTUBE_API_KEY', 'INNERTUBE_CLIENT_VERSION', 'HL', 'GL']);
  3670. setupOnPageFetched(app);
  3671. });
  3672.  
  3673. })({ fetch, JSON, Request, AbortController, setTimeout, clearTimeout });

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址