YouTube Comment Username Reveals

add user name for comment

  1. // ==UserScript==
  2. // @name YouTube Comment Username Reveals
  3. // @description add user name for comment
  4. // @namespace https://htsign.hateblo.jp
  5. // @version 0.3.7
  6. // @author htsign
  7. // @match https://www.youtube.com/*
  8. // @grant none
  9. // ==/UserScript==
  10.  
  11. {
  12. 'use strict';
  13.  
  14. /** @type {Map<string, string | null>} */
  15. const nameMap = new Map();
  16.  
  17. const pageManager = document.getElementById('page-manager');
  18. if (pageManager != null) {
  19. /**
  20. * @param {Node} node
  21. * @returns {node is HTMLElement}
  22. */
  23. const isHTMLElement = node => node instanceof HTMLElement;
  24.  
  25. /**
  26. *
  27. * @param {HTMLElement} element
  28. * @param {Name} name
  29. * @returns {element is HTMLElement & { is: Name }}
  30. * @template {string} Name
  31. */
  32. const is = (element, name) => 'is' in element && element.is === name;
  33.  
  34. const decode = (() => {
  35. /**
  36. * @type {[string, string][]}
  37. */
  38. const ENTITIES = [
  39. ['amp', '&'],
  40. ['apos', '\''],
  41. ['quot', '"'],
  42. ['nbsp', ' '],
  43. ['lt', '<'],
  44. ['gt', '>'],
  45. ['#39', '\''],
  46. ];
  47. /**
  48. * @param {string} s
  49. * @returns {string}
  50. */
  51. return s => ENTITIES.reduce((acc, [entity, sym]) => acc.replaceAll(`&${entity};`, sym), s);
  52. })();
  53.  
  54. /**
  55. * @param {HTMLAnchorElement} anchor
  56. * @param {string} name
  57. */
  58. const appendName = (anchor, name) => {
  59. // <span style="margin-left: 4px;" data-name="$name">( $name )</span>
  60. const span = anchor.querySelector(`span[data-name="${name}"]`) ?? Object.assign(
  61. document.createElement('span'),
  62. { textContent: `( ${name} )`, style: 'margin-left: 4px' },
  63. );
  64. Object.assign(span.dataset, { name });
  65.  
  66. // remove other names if exists
  67. for (const el of anchor.querySelectorAll(`span[data-name]:not([data-name="${name}"])`)) {
  68. el.remove();
  69. }
  70.  
  71. // append them name
  72. (anchor.querySelector('ytd-channel-name') ?? anchor).append(span);
  73. };
  74.  
  75. const pageManagerObserver = new MutationObserver(records => {
  76. const addedElements = records.flatMap(r => [...r.addedNodes]).filter(isHTMLElement);
  77.  
  78. for (const el of addedElements) {
  79. const commentsWrapper = el.querySelector('#columns #primary-inner #below ytd-comments');
  80.  
  81. if (commentsWrapper != null) {
  82. const contentsObserver = new MutationObserver(records => {
  83. const addedElements = records.flatMap(r => [...r.addedNodes]).filter(isHTMLElement);
  84.  
  85. for (const el of addedElements.filter(el => is(el, 'ytd-comment-view-model'))) {
  86. for (const author of el.querySelectorAll('#author-text, #name')) {
  87. const channelName = author.textContent.trim();
  88.  
  89. if (channelName == null) {
  90. console.warn('Username Reveals [name not found]:', author);
  91. continue;
  92. }
  93.  
  94. // append user name from map if nameMap has
  95. if (nameMap.has(channelName)) {
  96. const f = () => {
  97. // break if record is removed
  98. if (!nameMap.has(channelName)) return;
  99.  
  100. const name = nameMap.get(channelName);
  101. if (name == null) {
  102. return requestIdleCallback(f);
  103. }
  104. appendName(author, name);
  105. };
  106. f();
  107. continue;
  108. }
  109.  
  110. // reserve a record key for supress unnecessary request
  111. nameMap.set(channelName, null);
  112.  
  113. fetch(author.href).then(async response => {
  114. const text = await response.text();
  115. const [name] = text.match(/(?<=\<title\>).+?(?= - YouTube)/) ?? [];
  116.  
  117. if (name != null) {
  118. const _name = decode(name);
  119. appendName(author, _name);
  120. nameMap.set(channelName, _name);
  121. }
  122. }, error => {
  123. console.warn('Username Reveals [error]:', error);
  124. nameMap.delete(channelName);
  125. });
  126. }
  127. }
  128. });
  129. contentsObserver.observe(commentsWrapper, { childList: true, subtree: true });
  130. }
  131. }
  132. });
  133. pageManagerObserver.observe(pageManager, { childList: true });
  134. }
  135. }