Return YouTube Comment Username

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

目前為 2023-03-17 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name Return YouTube Comment Username
// @name:ja YouTubeコメント欄の名前を元に戻す
// @version 0.1.6
// @author yakisova41
// @license MIT
// @namespace https://yt-returnname-api.pages.dev/extension/
// @description This is to change the handle in the YouTube comments section to a username.
// @description:ja YouTubeのコメント欄の名前がハンドル(@...)表記になってしまった場合に、元のユーザーネームに上書きします。
// @match https://www.youtube.com/*
// @grant none
// ==/UserScript==

(() => {
  // src/lib/eventRoot.ts
  function pageChangeListener(eventElement) {
    let beforeHref = "";
    const observer = new MutationObserver(() => {
      const href = location.href;
      if (href !== beforeHref) {
        eventElement.dispatchEvent(
          new CustomEvent("pageChange", {
            detail: {
              beforeHref,
              newHref: href
            }
          })
        );
      }
      beforeHref = href;
    });
    observer.observe(document.querySelector("body"), {
      childList: true,
      subtree: true
    });
  }
  function createEventRoot() {
    const eventElement = document.createElement("div");
    pageChangeListener(eventElement);
    return {
      addEventListener: (eName, listener, options) => {
        eventElement.addEventListener(eName, listener, options);
        const pageChangeListener2 = () => {
          eventElement.removeEventListener(
            "pageChange",
            pageChangeListener2
          );
          eventElement.removeEventListener(eName, listener, options);
        };
        eventElement.addEventListener("pageChange", pageChangeListener2);
      },
      dispatchEvent: (e) => {
        eventElement.dispatchEvent(e);
      },
      removeEventListener: (eName, listener, options) => {
        eventElement.removeEventListener(eName, listener, options);
      },
      native: eventElement
    };
  }

  // src/lib/findElement.ts
  var findElement = (selector) => {
    return new Promise((resolve, reject) => {
      if (isNativeInterval()) {
        const interval = setInterval(() => {
          const elem = document.querySelector(selector);
          if (elem !== null) {
            clearInterval(interval);
            resolve(elem);
          }
        });
      } else {
        let search = function() {
          setTimeout(() => {
            const elem = document.querySelector(selector);
            if (elem !== null) {
              resolve(elem);
            } else {
              search();
            }
          });
        };
        search();
      }
    });
  };
  var findElementAll = (selector) => {
    return new Promise((resolve, reject) => {
      if (isNativeInterval()) {
        const interval = setInterval(() => {
          const elems = document.querySelectorAll(selector);
          if (elems.length !== 0) {
            clearInterval(interval);
            resolve(elems);
          }
        });
      } else {
        let search = function() {
          setTimeout(() => {
            const elems = document.querySelectorAll(selector);
            if (elems.length !== 0) {
              resolve(elems);
            } else {
              search();
            }
          });
        };
        search();
      }
    });
  };
  function isNativeInterval() {
    return window.setInterval.toString() === "function setInterval() { [native code] }";
  }

  // src/lib/getUserName.ts
  async function getUserName(href) {
    const id = href.split("/")[4];
    const data = await fetch(
      `https://www.youtube.com/youtubei/v1/browse?key=AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8&prettyPrint=false`,
      {
        method: "POST",
        headers: {
          accept: "*/*",
          "accept-encoding": "gzip, deflate, br",
          "accept-language": "ja",
          "content-type": "application/json",
          cookie: `GPS=1; YSC=sHEZ9k4QSS0; DEVICE_INFO=DEVICE_INFO; VISITOR_INFO1_LIVE=LLIIVVEE; PREF=f6=40000000&tz=Asia.Tokyo; ST-o2eza2=itct=itct&endpoint=%7B%22clickTrackingParams%22%3A%22CBQQ8JMBGAciEwjNqtCN86H9AhXnm1YBHABY%3D%22%2C%22commandMetadata%22%3A%7B%22webCommandMetadata%22%3A%7B%22url%22%3A%22%2F%40FUCKYOUTUBE%2Fchannels%22%2C%22webPageType%22%3A%22WEB_PAGE_TYPE_CHANNEL%22%2C%22rootVe%22%3A3611%2C%22apiUrl%22%3A%22%2Fyoutubei%2Fv1%2Fbrowse%22%7D%7D%2C%22browseEndpoint%22%3A%7B%22browseId%22%3A%22${id}%22%2C%22params%22%3A%22EghjaGFubmVsc_IGBAoCUgA%253D%22%2C%22canonicalBaseUrl%22%3A%22%2F%40FUCK_YOUTUBE%22%7D%7D`,
          dnt: "1",
          referer: `https://www.youtube.com/channel/${id}`,
          "sec-ch-ua": `"Chromium";v="110", "Not A(Brand";v="24", "Google Chrome";v="110"`,
          "sec-ch-ua-arch": "x86",
          "sec-ch-ua-bitness": "64",
          "sec-ch-ua-full-version": "110.0.5481.104",
          "sec-ch-ua-full-version-list": `"Chromium";v="110.0.5481.104", "Not A(Brand";v="24.0.0.0", "Google Chrome";v="110.0.5481.104"`,
          "sec-ch-ua-mobile": "?0",
          "sec-ch-ua-platform": "Windows",
          "sec-ch-ua-platform-version": "15.0.0",
          "sec-ch-ua-wow64": "?0",
          "sec-fetch-dest": "empty",
          "sec-fetch-mode": "same-origin",
          "sec-fetch-site": "same-origin",
          "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36",
          "x-client-data": "x-client-data",
          "x-goog-authuser": "0",
          "x-goog-visitor-id": "visitorData",
          "x-origin": "https://www.youtube.com",
          "x-youtube-bootstrap-logged-in": "true",
          "x-youtube-client-name": "1",
          "x-youtube-client-version": "2.20230217.01.00"
        },
        body: JSON.stringify({
          context: {
            client: {
              hl: "ja",
              gl: "JP",
              remoteHost: "1919:8a10:1145:1419:e1c9:b81a:09db:ff3a",
              deviceMake: "",
              deviceModel: "",
              visitorData: "visitorData",
              userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36,gzip(gfe)",
              clientName: "WEB",
              clientVersion: "2.20230217.01.00",
              osName: "Windows",
              osVersion: "10.0",
              originalUrl: "https://www.youtube.com/@FUCK_YOUTUBE/channels",
              platform: "DESKTOP",
              clientFormFactor: "UNKNOWN_FORM_FACTOR",
              configInfo: {
                appInstallData: "appInstallData"
              },
              userInterfaceTheme: "USER_INTERFACE_THEME_DARK",
              timeZone: "Asia/Tokyo",
              browserName: "Chrome",
              browserVersion: "110.0.0.0",
              acceptHeader: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
              deviceExperimentId: "deviceExperimentId",
              screenWidthPoints: 599,
              screenHeightPoints: 937,
              screenPixelDensity: 1,
              screenDensityFloat: 1,
              utcOffsetMinutes: 540,
              memoryTotalKbytes: "8000000",
              mainAppWebInfo: {
                graftUrl: "/@FUCK_YOUTUBE/channels",
                pwaInstallabilityStatus: "PWA_INSTALLABILITY_STATUS_CAN_BE_INSTALLED",
                webDisplayMode: "WEB_DISPLAY_MODE_BROWSER",
                isWebNativeShareAvailable: true
              }
            },
            user: { lockedSafetyMode: false },
            request: {
              useSsl: true,
              internalExperimentFlags: [],
              consistencyTokenJars: []
            },
            clickTracking: {
              clickTrackingParams: "CCgQ8JMBGAgiEwiW5Pey8qH9AhVVSA8CHUHLAGc="
            },
            adSignalsInfo: {
              params: [
                { key: "dt", value: "1676820301790" },
                { key: "flash", value: "0" },
                { key: "frm", value: "0" },
                { key: "u_tz", value: "540" },
                { key: "u_his", value: "1" },
                { key: "u_h", value: "1080" },
                { key: "u_w", value: "1920" },
                { key: "u_ah", value: "1040" },
                { key: "u_aw", value: "1920" },
                { key: "u_cd", value: "24" },
                { key: "bc", value: "31" },
                { key: "bih", value: "937" },
                { key: "biw", value: "582" },
                {
                  key: "brdim",
                  value: "-1920,0,-1920,0,1920,0,1920,1040,599,937"
                },
                { key: "vis", value: "1" },
                { key: "wgl", value: "true" },
                { key: "ca_type", value: "image" }
              ]
            }
          },
          browseId: id,
          params: "EghjaGFubmVsc_IGBAoCUgA%3D"
        })
      }
    ).then((res) => res.text()).then((text) => {
      const data2 = JSON.parse(text);
      const name = data2["header"]["c4TabbedHeaderRenderer"]["title"];
      return name;
    });
    return data;
  }

  // src/watch/replaceComments.ts
  function replaceComments(comments, page, eventRoot) {
    const nameStore = [];
    comments.forEach(async (c, index) => {
      const nthChild = page * 20 + (index + 1);
      const commentElem = await findElement(
        `#comments > #sections > #contents > ytd-comment-thread-renderer:nth-child(${nthChild})`
      );
      eventRoot.addEventListener("pageChange", () => {
        commentElem.remove();
      });
      const channelHrefElem = commentElem.querySelector(
        "#comment > #body > #main > #header > #header-author > h3 > a "
      );
      let nameElem;
      nameElem = commentElem.querySelector(
        "#comment > #body > #main > #header  > #header-author > #author-comment-badge > ytd-author-comment-badge-renderer > #name > ytd-channel-name > #container > #text-container > #text "
      );
      if (nameElem === null) {
        nameElem = channelHrefElem.querySelector("span");
      }
      if (channelHrefElem.href in nameStore) {
        nameElem.innerHTML = nameStore[channelHrefElem.href];
      } else {
        getUserName(channelHrefElem.href).then((name) => {
          nameElem.innerHTML = name;
          nameStore[channelHrefElem.href] = name;
        });
      }
    });
  }

  // src/listeners/commentRenderingListener.ts
  async function renderingListener(eventRoot) {
    let renderingTrigger = false;
    const contents = await findElement("#comments > #sections");
    const observer = new MutationObserver(() => {
      if ("can-show-more" in contents.attributes) {
        renderingTrigger = true;
      } else if ("continuation-is-reloading" in contents.attributes) {
        renderingTrigger = true;
        eventRoot.dispatchEvent(
          new CustomEvent("commentsContinuationReloading")
        );
      } else {
        if (renderingTrigger) {
          renderingTrigger = false;
          eventRoot.dispatchEvent(
            new CustomEvent("commentsRenderingSuccess")
          );
        }
      }
    });
    observer.observe(contents, {
      attributes: true
    });
    return observer;
  }

  // src/watch/replaceReplies.ts
  async function replaceReplies(page, targetIndex) {
    const nthChild = page * 20 + (targetIndex + 1);
    const repliesElem = await findElementAll(
      `#comments > #sections > #contents > ytd-comment-thread-renderer:nth-child(${nthChild}) > #replies > ytd-comment-replies-renderer > #expander > #expander-contents > #contents > ytd-comment-renderer`
    );
    repliesElem.forEach((elem) => {
      const channelHrefElem = elem.querySelector(
        "#body > #main > #header > #header-author > h3 > a "
      );
      let nameElem;
      nameElem = elem.querySelector(
        "#body > #main > #header > #header-author >  #author-comment-badge > ytd-author-comment-badge-renderer > #name > ytd-channel-name > #container > #text-container > #text "
      );
      if (nameElem === null) {
        nameElem = channelHrefElem.querySelector("span");
      }
      getUserName(channelHrefElem.href).then((name) => {
        nameElem.innerHTML = name;
      });
      const textElems = elem.querySelectorAll(
        "#body > #main > #comment-content > #expander > #content > #content-text > a.yt-formatted-string"
      );
      textElems.forEach((textElem) => {
        const text = textElem.innerHTML;
        if (text.match("@.*")) {
          getUserName(textElem.href).then((name) => {
            textElem.innerHTML = "@" + name;
          });
        }
      });
    });
  }

  // src/watch/watch.ts
  async function watch(eventRoot) {
    const renderListener = await renderingListener(eventRoot);
    eventRoot.addEventListener("pageChange", () => {
      renderListener.disconnect();
    });
    const commentsTargetIdStore = [];
    let commentsPage = 0;
    eventRoot.addEventListener("commentsContinuationReloading", () => {
      commentsPage = 0;
    });
    eventRoot.addEventListener(
      "commentFetch",
      ({ detail: { comments, mode } }) => {
        comments.forEach((comment, index) => {
          if (comment["commentThreadRenderer"]["replies"] !== void 0) {
            commentsTargetIdStore.push({
              index,
              commentsPage,
              targetId: comment["commentThreadRenderer"]["replies"]["commentRepliesRenderer"]["targetId"]
            });
          }
        });
        if (mode === 1) {
          if (comments.length !== 20) {
            replaceComments(comments, commentsPage, eventRoot);
            commentsPage = commentsPage + 1;
          } else {
            const handleRenderingSucess = () => {
              replaceComments(comments, commentsPage, eventRoot);
              commentsPage = commentsPage + 1;
              eventRoot.native.removeEventListener(
                "commentsRenderingSuccess",
                handleRenderingSucess
              );
            };
            eventRoot.native.addEventListener(
              "commentsRenderingSuccess",
              handleRenderingSucess
            );
          }
        } else {
          replaceComments(comments, commentsPage, eventRoot);
          commentsPage = commentsPage + 1;
        }
      }
    );
    eventRoot.addEventListener(
      "repliesFetch",
      ({ detail: { replies, targetId } }) => {
        commentsTargetIdStore.forEach((targetData, index) => {
          if (targetData.targetId === targetId) {
            replaceReplies(targetData.commentsPage, targetData.index);
          }
        });
      }
    );
  }

  // src/lib/FetchIntercepter.ts
  function FetchIntercepter(originalFetch) {
    const actions = [];
    return {
      start: () => {
        window.fetch = (...arg) => {
          const [request, init] = arg;
          const response = originalFetch(request, init);
          response.then((res) => {
            actions.forEach((action) => {
              action({
                request,
                init,
                response: res
              });
            });
          });
          return response;
        };
      },
      addAction: (action) => {
        actions.push(action);
      },
      stop: () => {
        window.fetch = originalFetch;
      }
    };
  }

  // src/listeners/commentFetchListener.ts
  function commentFetchListener(eventRoot) {
    const intercepter = FetchIntercepter(window.fetch);
    intercepter.start();
    intercepter.addAction(async (data) => {
      if (typeof data.request["url"] === "string" && data.request["url"].match(
        "https://www.youtube.com/youtubei/v1/next.*"
      )) {
        const responseClone = data.response.clone();
        const text = await responseClone.text();
        const body = JSON.parse(text);
        const commentFetchMode = is_comments(body);
        if (commentFetchMode === 1) {
          const comments = body["onResponseReceivedEndpoints"][1]["reloadContinuationItemsCommand"]["continuationItems"];
          const data2 = removeContinuationItem(comments);
          eventRoot.dispatchEvent(
            new CustomEvent("commentFetch", {
              detail: {
                comments: data2,
                mode: commentFetchMode
              }
            })
          );
        }
        if (commentFetchMode === 2) {
          const comments = body["onResponseReceivedEndpoints"][0]["appendContinuationItemsAction"]["continuationItems"];
          const data2 = removeContinuationItem(comments);
          eventRoot.dispatchEvent(
            new CustomEvent("commentFetch", {
              detail: {
                comments: data2,
                mode: commentFetchMode
              }
            })
          );
        }
        if (commentFetchMode === 0) {
          if (body["onResponseReceivedEndpoints"][0]["appendContinuationItemsAction"]["targetId"].match("comment-replies.*")) {
            const replies = body["onResponseReceivedEndpoints"][0]["appendContinuationItemsAction"]["continuationItems"];
            const targetId = body["onResponseReceivedEndpoints"][0]["appendContinuationItemsAction"]["targetId"];
            eventRoot.dispatchEvent(
              new CustomEvent("repliesFetch", {
                detail: {
                  replies,
                  targetId
                }
              })
            );
          }
        }
      }
    });
    return {
      stop: intercepter.stop
    };
  }
  function is_comments(body) {
    const onResponseReceivedEndpoints = body["onResponseReceivedEndpoints"];
    if (onResponseReceivedEndpoints.length > 1 && "reloadContinuationItemsCommand" in onResponseReceivedEndpoints[1] && onResponseReceivedEndpoints[1]["reloadContinuationItemsCommand"]["targetId"] === "comments-section") {
      return 1;
    }
    if ("appendContinuationItemsAction" in onResponseReceivedEndpoints[0] && onResponseReceivedEndpoints[0]["appendContinuationItemsAction"]["targetId"] === "comments-section") {
      return 2;
    }
    return 0;
  }
  function removeContinuationItem(comments) {
    comments.forEach((comment, index) => {
      if ("continuationItemRenderer" in comment) {
        comments.splice(index, 1);
      }
    });
    return comments;
  }

  // src/index.ts
  function main() {
    const eventRoot = createEventRoot();
    commentFetchListener(eventRoot);
    eventRoot.native.addEventListener(
      "pageChange",
      async (e) => {
        const { newHref } = e.detail;
        const pageName = newHref.split("/")[3].split("?")[0];
        if (pageName === "watch") {
          watch(eventRoot);
        }
      }
    );
  }
  main();
})();