Return YouTube Comment Username

This is to change the handle in the YouTube comments section to a username.

当前为 2023-02-20 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Return YouTube Comment Username
  3. // @name:ja YouTubeコメント欄の名前を元に戻す
  4. // @version 0.1.2
  5. // @author yakisova41
  6. // @license MIT
  7. // @namespace https://yt-returnname-api.pages.dev/extension/
  8. // @description This is to change the handle in the YouTube comments section to a username.
  9. // @description:ja YouTubeのコメント欄の名前がハンドル(@...)表記になってしまった場合に、元のユーザーネームに上書きします。
  10. // @match https://www.youtube.com/*
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. (() => {
  15. // src/lib/eventRoot.ts
  16. function pageChangeListener(eventElement) {
  17. let beforeHref = "";
  18. const observer = new MutationObserver(() => {
  19. const href = location.href;
  20. if (href !== beforeHref) {
  21. eventElement.dispatchEvent(
  22. new CustomEvent("pageChange", {
  23. detail: {
  24. beforeHref,
  25. newHref: href
  26. }
  27. })
  28. );
  29. }
  30. beforeHref = href;
  31. });
  32. observer.observe(document.querySelector("body"), {
  33. childList: true,
  34. subtree: true
  35. });
  36. }
  37. function createEventRoot() {
  38. const eventElement = document.createElement("div");
  39. pageChangeListener(eventElement);
  40. return {
  41. addEventListener: (eName, listener, options) => {
  42. eventElement.addEventListener(eName, listener, options);
  43. const pageChangeListener2 = () => {
  44. eventElement.removeEventListener(
  45. "pageChange",
  46. pageChangeListener2
  47. );
  48. eventElement.removeEventListener(eName, listener, options);
  49. };
  50. eventElement.addEventListener("pageChange", pageChangeListener2);
  51. },
  52. dispatchEvent: (e) => {
  53. eventElement.dispatchEvent(e);
  54. },
  55. removeEventListener: (eName, listener, options) => {
  56. eventElement.removeEventListener(eName, listener, options);
  57. },
  58. native: eventElement
  59. };
  60. }
  61.  
  62. // src/lib/findElement.ts
  63. var findElement = (selector, limit = 1e3) => {
  64. return new Promise((resolve, reject) => {
  65. let i = 0;
  66. const interval = setInterval(() => {
  67. const elem = document.querySelector(selector);
  68. if (elem !== null) {
  69. clearInterval(interval);
  70. resolve(elem);
  71. }
  72. if (limit < i) {
  73. clearInterval(interval);
  74. reject(null);
  75. }
  76. i = i + 1;
  77. });
  78. });
  79. };
  80. var findElementAll = (selector, limit = 1e3) => {
  81. return new Promise((resolve, reject) => {
  82. let i = 0;
  83. const interval = setInterval(() => {
  84. const elems = document.querySelectorAll(selector);
  85. if (elems.length !== 0) {
  86. clearInterval(interval);
  87. resolve(elems);
  88. }
  89. if (limit < i) {
  90. clearInterval(interval);
  91. reject([]);
  92. }
  93. i = i + 1;
  94. });
  95. });
  96. };
  97.  
  98. // src/watch/getUserName.ts
  99. async function getUserName(href) {
  100. const id = href.split("/")[4];
  101. const data = await fetch(
  102. `https://yt-returnname-api.pages.dev/api/idToName?id=${id}`,
  103. {
  104. method: "POST"
  105. }
  106. ).then((res) => res.text());
  107. return data;
  108. }
  109.  
  110. // src/watch/replaceComments.ts
  111. function replaceComments(comments, page) {
  112. const nameStore = [];
  113. comments.forEach(async (c, index) => {
  114. let nthChild;
  115. if (page === 0) {
  116. nthChild = index + 1;
  117. } else {
  118. nthChild = page * 20 + (index + 1);
  119. }
  120. const commentElem = await findElement(
  121. `#comments > #sections > #contents > ytd-comment-thread-renderer:nth-child(${nthChild})`
  122. );
  123. const channelHrefElem = commentElem.querySelector(
  124. "#comment > #body > #main > #header > #header-author > h3 > a "
  125. );
  126. let nameElem;
  127. nameElem = commentElem.querySelector(
  128. "#comment > #body > #main > #header > #header-author > #author-comment-badge > ytd-author-comment-badge-renderer > #name > ytd-channel-name > #container > #text-container > #text "
  129. );
  130. if (nameElem === null) {
  131. nameElem = channelHrefElem.querySelector("span");
  132. }
  133. if (channelHrefElem.href in nameStore) {
  134. nameElem.innerHTML = nameStore[channelHrefElem.href];
  135. } else {
  136. getUserName(channelHrefElem.href).then((name) => {
  137. nameElem.innerHTML = name;
  138. nameStore[channelHrefElem.href] = name;
  139. });
  140. }
  141. });
  142. }
  143.  
  144. // src/listeners/commentRenderingListener.ts
  145. async function renderingListener(eventRoot) {
  146. let renderingTrigger = false;
  147. const contents = await findElement("#comments > #sections");
  148. const observer = new MutationObserver(() => {
  149. if ("can-show-more" in contents.attributes) {
  150. renderingTrigger = true;
  151. } else if ("continuation-is-reloading" in contents.attributes) {
  152. renderingTrigger = true;
  153. eventRoot.dispatchEvent(
  154. new CustomEvent("commentsContinuationReloading")
  155. );
  156. } else {
  157. if (renderingTrigger) {
  158. renderingTrigger = false;
  159. eventRoot.dispatchEvent(
  160. new CustomEvent("commentsRenderingSuccess")
  161. );
  162. }
  163. }
  164. });
  165. observer.observe(contents, {
  166. attributes: true
  167. });
  168. return observer;
  169. }
  170.  
  171. // src/watch/replaceReplies.ts
  172. async function replaceReplies(page, targetIndex) {
  173. let nthChild;
  174. if (page === 0) {
  175. nthChild = targetIndex + 1;
  176. } else {
  177. nthChild = page * 20 + (targetIndex + 1);
  178. }
  179. const repliesElem = await findElementAll(
  180. `#comments > #sections > #contents > ytd-comment-thread-renderer:nth-child(${nthChild}) > #replies > ytd-comment-replies-renderer > #expander > #expander-contents > #contents > ytd-comment-renderer`
  181. );
  182. repliesElem.forEach((elem) => {
  183. const channelHrefElem = elem.querySelector(
  184. "#body > #main > #header > #header-author > h3 > a "
  185. );
  186. let nameElem;
  187. nameElem = elem.querySelector(
  188. "#body > #main > #header > #header-author > #author-comment-badge > ytd-author-comment-badge-renderer > #name > ytd-channel-name > #container > #text-container > #text "
  189. );
  190. if (nameElem === null) {
  191. nameElem = channelHrefElem.querySelector("span");
  192. }
  193. getUserName(channelHrefElem.href).then((name) => {
  194. nameElem.innerHTML = name;
  195. });
  196. const textElems = elem.querySelectorAll(
  197. "#body > #main > #comment-content > #expander > #content > #content-text > a.yt-formatted-string"
  198. );
  199. textElems.forEach((textElem) => {
  200. const text = textElem.innerHTML;
  201. if (text.match("@.*")) {
  202. getUserName(textElem.href).then((name) => {
  203. textElem.innerHTML = "@" + name;
  204. });
  205. }
  206. });
  207. });
  208. }
  209.  
  210. // src/watch/watch.ts
  211. async function watch(eventRoot) {
  212. const renderListener = await renderingListener(eventRoot);
  213. eventRoot.addEventListener("pageChange", () => {
  214. renderListener.disconnect();
  215. });
  216. const commentsTargetIdStore = [];
  217. let commentsPage = 0;
  218. eventRoot.addEventListener("commentsContinuationReloading", () => {
  219. commentsPage = 0;
  220. });
  221. eventRoot.addEventListener(
  222. "commentFetch",
  223. ({ detail: { comments, mode } }) => {
  224. comments.forEach((comment, index) => {
  225. if (comment["commentThreadRenderer"]["replies"] !== void 0) {
  226. commentsTargetIdStore.push({
  227. index,
  228. commentsPage,
  229. targetId: comment["commentThreadRenderer"]["replies"]["commentRepliesRenderer"]["targetId"]
  230. });
  231. }
  232. });
  233. if (mode === 1) {
  234. const handleRenderingSucess = () => {
  235. replaceComments(comments, commentsPage);
  236. commentsPage = commentsPage + 1;
  237. eventRoot.native.removeEventListener(
  238. "commentsRenderingSuccess",
  239. handleRenderingSucess
  240. );
  241. };
  242. eventRoot.native.addEventListener(
  243. "commentsRenderingSuccess",
  244. handleRenderingSucess
  245. );
  246. } else {
  247. replaceComments(comments, commentsPage);
  248. commentsPage = commentsPage + 1;
  249. }
  250. }
  251. );
  252. eventRoot.addEventListener(
  253. "repliesFetch",
  254. ({ detail: { replies, targetId } }) => {
  255. commentsTargetIdStore.forEach((targetData, index) => {
  256. if (targetData.targetId === targetId) {
  257. replaceReplies(targetData.commentsPage, targetData.index);
  258. }
  259. });
  260. }
  261. );
  262. }
  263.  
  264. // src/lib/FetchIntercepter.ts
  265. function FetchIntercepter(originalFetch) {
  266. const actions = [];
  267. return {
  268. start: () => {
  269. window.fetch = (...arg) => {
  270. const [request, init] = arg;
  271. const response = originalFetch(request, init);
  272. response.then((res) => {
  273. actions.forEach((action) => {
  274. action({
  275. request,
  276. init,
  277. response: res
  278. });
  279. });
  280. });
  281. return response;
  282. };
  283. },
  284. addAction: (action) => {
  285. actions.push(action);
  286. },
  287. stop: () => {
  288. window.fetch = originalFetch;
  289. }
  290. };
  291. }
  292.  
  293. // src/listeners/commentFetchListener.ts
  294. function commentFetchListener(eventRoot) {
  295. const intercepter = FetchIntercepter(window.fetch);
  296. intercepter.start();
  297. intercepter.addAction(async (data) => {
  298. if (typeof data.request["url"] === "string" && data.request["url"].match(
  299. "https://www.youtube.com/youtubei/v1/next.*"
  300. )) {
  301. const responseClone = data.response.clone();
  302. const text = await responseClone.text();
  303. const body = JSON.parse(text);
  304. const commentFetchMode = is_comments(body);
  305. if (commentFetchMode === 1) {
  306. const comments = body["onResponseReceivedEndpoints"][1]["reloadContinuationItemsCommand"]["continuationItems"];
  307. const data2 = removeContinuationItem(comments);
  308. eventRoot.dispatchEvent(
  309. new CustomEvent("commentFetch", {
  310. detail: {
  311. comments: data2,
  312. mode: commentFetchMode
  313. }
  314. })
  315. );
  316. }
  317. if (commentFetchMode === 2) {
  318. const comments = body["onResponseReceivedEndpoints"][0]["appendContinuationItemsAction"]["continuationItems"];
  319. const data2 = removeContinuationItem(comments);
  320. eventRoot.dispatchEvent(
  321. new CustomEvent("commentFetch", {
  322. detail: {
  323. comments: data2,
  324. mode: commentFetchMode
  325. }
  326. })
  327. );
  328. }
  329. if (commentFetchMode === 0) {
  330. if (body["onResponseReceivedEndpoints"][0]["appendContinuationItemsAction"]["targetId"].match("comment-replies.*")) {
  331. const replies = body["onResponseReceivedEndpoints"][0]["appendContinuationItemsAction"]["continuationItems"];
  332. const targetId = body["onResponseReceivedEndpoints"][0]["appendContinuationItemsAction"]["targetId"];
  333. eventRoot.dispatchEvent(
  334. new CustomEvent("repliesFetch", {
  335. detail: {
  336. replies,
  337. targetId
  338. }
  339. })
  340. );
  341. }
  342. }
  343. }
  344. });
  345. return {
  346. stop: intercepter.stop
  347. };
  348. }
  349. function is_comments(body) {
  350. const onResponseReceivedEndpoints = body["onResponseReceivedEndpoints"];
  351. if (onResponseReceivedEndpoints.length > 1 && "reloadContinuationItemsCommand" in onResponseReceivedEndpoints[1] && onResponseReceivedEndpoints[1]["reloadContinuationItemsCommand"]["targetId"] === "comments-section") {
  352. return 1;
  353. }
  354. if ("appendContinuationItemsAction" in onResponseReceivedEndpoints[0] && onResponseReceivedEndpoints[0]["appendContinuationItemsAction"]["targetId"] === "comments-section") {
  355. return 2;
  356. }
  357. return 0;
  358. }
  359. function removeContinuationItem(comments) {
  360. comments.forEach((comment, index) => {
  361. if ("continuationItemRenderer" in comment) {
  362. comments.splice(index, 1);
  363. }
  364. });
  365. return comments;
  366. }
  367.  
  368. // src/index.ts
  369. function main() {
  370. const eventRoot = createEventRoot();
  371. commentFetchListener(eventRoot);
  372. eventRoot.native.addEventListener(
  373. "pageChange",
  374. async (e) => {
  375. const { newHref } = e.detail;
  376. const pageName = newHref.split("/")[3].split("?")[0];
  377. if (pageName === "watch") {
  378. watch(eventRoot);
  379. }
  380. }
  381. );
  382. }
  383. main();
  384. })();