ニコニコ動画 引用コメントツール

dアニメストア ニコニコ支店の引用コメント関連のツール

当前为 2022-11-22 提交的版本,查看 最新版本

// ==UserScript==
// @name         ニコニコ動画 引用コメントツール
// @namespace    https://midra.me
// @version      1.7.4
// @description  dアニメストア ニコニコ支店の引用コメント関連のツール
// @author       Midra
// @license      MIT
// @match        https://www.nicovideo.jp/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=nicovideo.jp
// @run-at       document-start
// @noframes
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @connect      nicovideo.jp
// @require      https://gf.qytechs.cn/scripts/7212-gm-config-eight-s-version/code/GM_config%20(eight's%20version).js?version=156587
// ==/UserScript==

(() => {
  // src/util.js
  var generateDocumentByHTML = (html) => {
    const elem = document.createElement("html");
    elem.insertAdjacentHTML("beforeend", html);
    return elem;
  };
  var generateElementByHTML = (html) => {
    const elem = document.createElement("div");
    elem.insertAdjacentHTML("beforeend", html);
    return elem.firstElementChild;
  };
  var filterObject = (obj) => {
    if (obj != null && typeof obj === "object" && !Array.isArray(obj)) {
      Object.keys(obj).forEach((key) => {
        if (obj[key] == null) {
          delete obj[key];
        } else {
          filterObject(obj[key]);
        }
      });
    }
  };
  var numberToKansuji = (num) => {
    let result = "";
    if (typeof num === "number" && 0 < num && num < 1e5) {
      const kansujiA = ["", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u4E03", "\u516B", "\u4E5D"];
      const kansujiB = ["", "\u5341", "\u767E", "\u5343", "\u4E07"];
      const numAry = Array.from(num.toString()).map((v) => Number(v));
      numAry.reverse().forEach((n, idx, ary) => {
        if (n === 0) {
          return;
        } else if (idx === 0) {
          result = `${kansujiA[n]}${result}`;
        } else if (n === 1) {
          if (3 <= idx && 5 <= ary.length) {
            result = `${kansujiA[n]}${kansujiB[idx]}${result}`;
          } else {
            result = `${kansujiB[idx]}${result}`;
          }
        } else {
          result = `${kansujiA[n]}${kansujiB[idx]}${result}`;
        }
      });
    }
    return result !== "" ? result : null;
  };
  var kansujiToNumber = (kansuji) => {
    let result = 0;
    if (typeof kansuji === "string") {
      const kansujiA = ["", "\u4E00", "\u4E8C", "\u4E09", "\u56DB", "\u4E94", "\u516D", "\u4E03", "\u516B", "\u4E5D"];
      const kansujiB = ["", "\u5341", "\u767E", "\u5343", "\u4E07"];
      const numberB = [1, 10, 100, 1e3, 1e4];
      let isKansuji;
      const kansujiAry = Array.from(kansuji);
      kansujiAry.forEach((char) => {
        isKansuji || (isKansuji = Boolean([...kansujiA, ...kansujiB].find((v) => v === char)));
      });
      if (isKansuji) {
        kansujiAry.forEach((char, idx, ary) => {
          if (char === null) {
            return;
          }
          const idxA = kansujiA.findIndex((v) => v === char);
          const idxB = kansujiB.findIndex((v) => v === char);
          if (idxA !== -1) {
            if (idx + 1 < kansujiAry.length) {
              const nextIdxB = kansujiB.findIndex((v) => v === ary[idx + 1]);
              if (nextIdxB !== -1) {
                result += idxA * numberB[nextIdxB];
                ary[idx + 1] = null;
              }
            } else {
              result += idxA;
            }
          }
          if (idxB !== -1) {
            if (idx + 1 < kansujiAry.length) {
              const nextIdxB = kansujiB.findIndex((v) => v === ary[idx + 1]);
              if (nextIdxB !== -1) {
                result += numberB[idxB] * numberB[nextIdxB];
                ary[idx + 1] = null;
              } else {
                result += numberB[idxB];
              }
            } else {
              result += numberB[idxB];
            }
          }
        });
      }
    }
    return 0 < result ? result : null;
  };
  var convertToRoman = (num) => {
    const decimal = [1e3, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
    const romanNumeral = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
    let result = "";
    decimal.forEach((val, idx) => {
      while (val <= num) {
        result += romanNumeral[idx];
        num -= val;
      }
    });
    return result || num.toString();
  };
  var fixRomanNum = (str) => {
    if (typeof str === "string") {
      const ronamNum = [null, "\u2170", "\u2171", "\u2172", "\u2173", "\u2174", "\u2175", "\u2176", "\u2177", "\u2178", "\u2179", "\u217A", "\u217B"];
      return str.replace(/[ⅰⅱⅲⅳⅴⅵⅶⅷⅸⅹⅺⅻ]/, (char) => {
        const idx = ronamNum.indexOf(char);
        return idx !== -1 ? convertToRoman(Number(idx)).toLowerCase() : char;
      });
    }
  };
  var normalizeText = (text) => {
    if (typeof text === "string") {
      text = text.toLowerCase();
      text = fixRomanNum(text);
      text = text.replace(/[-−\(\)()「」「」『』【】[]〈〉《》〔〕{}{}\[\]]/g, " ");
      text = text.replace(/[a-z0-9]/g, (s) => String.fromCharCode(s.charCodeAt(0) - 65248));
      text = text.replace(/./g, (s) => ({
        "\u301C": "~",
        "\uFF1F": "?",
        "\uFF01": "!",
        "\u201D": '"',
        "\u2019": "'",
        "\xB4": "'",
        "\uFF40": "`",
        "\uFF1A": ":",
        "\uFF0C": ",",
        "\uFF0E": ".",
        "\u30FB": "\uFF65",
        "\uFF0F": "/",
        "\uFF03": "#",
        "\uFF04": "$",
        "\uFF05": "%",
        "\uFF06": "&",
        "\uFF1D": "=",
        "\uFF20": "@"
      })[s] || s);
      return text;
    } else {
      return "";
    }
  };
  var normalizeEpisodeNumber = (str) => {
    return str.replace(/第?(\d+|[一二三四五六七八九十百千万]+)話|episode(\d+)|#(\d+)|\s(\d+)\s/g, (_, p1, p2, p3, p4) => {
      const num = Number(p1 || p2 || p3 || p4);
      if (Number.isFinite(num)) {
        return num.toString();
      } else {
        return kansujiToNumber(p1).toString();
      }
    });
  };
  var optimizeEpisodeNumberForSearch = (title) => {
    title = title.replace(/\s+/g, ' ').trim();
    const splitedTitle = title.split(' ');
    if (splitedTitle.length === 3) {
      const matchedNumber = splitedTitle[1].match(/\d+|[一二三四五六七八九十百千万]+/);
      if (matchedNumber !== null) {
        let num = Number(matchedNumber[0]);
        if (Number.isNaN(num)) {
          num = kansujiToNumber(matchedNumber[0]);
        }
        splitedTitle[1] = `${num.toString()}話`;
        title = splitedTitle.join(' ');
      }
    }
    return title.replace(/第?(\d+|[一二三四五六七八九十百千万]+)話|episode(\d+)|#(\d+)/g, (_, p1, p2, p3) => {
      let num = Number(p1 || p2 || p3);
      if (Number.isNaN(num)) {
        num = kansujiToNumber(p1);
      }
      if (num < 10) {
        const zeroPadding = ('00' + num).slice(-2);
        return ` ${num} OR ${zeroPadding} OR ${numberToKansuji(num)} `;
      } else {
        return ` ${num} OR ${numberToKansuji(num)} `;
      }
    })
  };
  var optimizeTitleForSearch = (title = "") => {
    title = normalizeText(title);
    title = optimizeEpisodeNumberForSearch(title);
    title = title.replace(/\s+/g, " ").trim();
    return title;
  };
  var isEqualTitle = (titleA, titleB) => {
    let result = false;
    if (typeof titleA === 'string' && titleA !== '' && typeof titleB === 'string' && titleB !== '') {
      result = titleA === titleB;
      if (!result) {
        titleA = normalizeText(titleA);
        titleA = normalizeEpisodeNumber(titleA);
        titleA = titleA.replace(/\s+/g, '').trim();
        titleB = normalizeText(titleB);
        titleB = normalizeEpisodeNumber(titleB);
        titleB = titleB.replace(/\s+/g, '').trim();
        result = titleA === titleB;
      }
    }
    return result;
  };

  // src/api.js
  var WATCH_V3 = "https://www.nicovideo.jp/api/watch/v3";
  var THREADS = "https://nvcomment.nicovideo.jp/v1/threads";
  var CHANNEL_VIDEO_DANIME_LINKS = "https://public-api.ch.nicovideo.jp/v1/user/channelVideoDAnimeLinks";
  var SEARCH_V2 = "https://api.search.nicovideo.jp/api/v2/snapshot/video/contents/search";
  var NICORU_KEYS = " https://nvapi.nicovideo.jp/v1/comment/keys/nicoru";
  var getVideoData = async (videoId) => {
    if (videoId === void 0) {
      throw new Error("[ECT] ERROR: videoId is undefined");
    }
    try {
      const requestQuery = {
        actionTrackId: `${Array.from(Array(10)).map(() => "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[Math.random() * 62 | 0]).join("")}_${Date.now()}`
      };
      const res2 = await fetch(`${WATCH_V3}/${videoId}?${new URLSearchParams(requestQuery)}`, {
        method: "GET",
        headers: {
          "x-client-os-type": "others",
          "x-frontend-id": 6,
          "x-frontend-version": 0
        }
      });
      const json = await res2.json();
      if (json.data !== void 0) {
        return json.data;
      } else {
        throw new Error({
          message: "[ECT] ERROR: getVideoData",
          object: res2
        });
      }
    } catch (e) {
      throw new Error(e);
    }
  };
  var getThreads = async (videoData) => {
    if (videoData === void 0) {
      throw new Error("[ECT] ERROR: videoData is undefined");
    }
    try {
      const res2 = await fetch(THREADS, {
        method: "POST",
        headers: {
          "x-client-os-type": "others",
          "x-frontend-id": 6,
          "x-frontend-version": 0
        },
        body: JSON.stringify({
          additionals: {},
          params: videoData.comment?.nvComment?.params || {},
          threadKey: videoData.comment?.nvComment?.threadKey
        })
      });
      const json = await res2.json();
      if (json.data !== void 0) {
        return json.data.threads;
      } else {
        throw new Error({
          message: "[ECT] ERROR: getThreads",
          object: res2
        });
      }
    } catch (e) {
      throw new Error(e);
    }
  };
  var getLinkedVideoId = async (videoId) => {
    if (videoId === void 0) {
      throw new Error("[ECT] ERROR: videoId is undefined");
    }
    try {
      const requestQuery = {
        videoId,
        _frontendId: 6
      };
      const res2 = await fetch(`${CHANNEL_VIDEO_DANIME_LINKS}?${new URLSearchParams(requestQuery)}`);
      const json = await res2.json();
      if (json.data !== void 0) {
        return json.data.items[0].linkedVideoId;
      } else {
        throw new Error({
          message: "[ECT] ERROR: getLinkedVideoId",
          object: res2
        });
      }
    } catch (e) {
      throw new Error(e);
    }
  };
  var search = async (query) => {
    return new Promise((resolve, reject) => {
      if (query === void 0) {
        reject("query is undefined");
      }
      try {
        const requestQuery = {
          q: query.q,
          targets: query.targets.join(),
          fields: query.fields?.join() || ["contentId", "title", "channelId", "lengthSeconds", "tags"].join(),
          _sort: query._sort || "+startTime",
          _offset: query._offset,
          _limit: query._limit || 5,
          _context: "ect"
        };
        if (query.filters !== void 0) {
          Object.entries(query.filters).forEach((val) => {
            if (Array.isArray(val[1]) && val[1].length === 2) {
              requestQuery[`filters[${val[0]}][gte]`] = val[1][0];
              requestQuery[`filters[${val[0]}][lte]`] = val[1][1];
            } else {
              requestQuery[`filters[${val[0]}][0]`] = val[1];
            }
          });
        }
        filterObject(requestQuery);
        GM_xmlhttpRequest({
          method: "GET",
          url: `${SEARCH_V2}?${new URLSearchParams(requestQuery)}`,
          headers: {
            "User-Agent": "ECT/1.0"
          },
          responseType: "json",
          onload: (e) => {
            if (e.response !== void 0) {
              resolve(e.response.data);
            } else {
              reject({
                message: "[ECT] ERROR: search",
                object: res
              });
            }
          },
          onerror: (e) => reject(e)
        });
      } catch (e) {
        reject(e);
      }
    });
  };
  var getNicoruKey = async (threadId, fork) => {
    if (threadId === void 0 || fork === void 0) {
      return new Error("[ECT] ERROR: threadId or fork is undefined");
    }
    try {
      const requestQuery = {
        threadId,
        fork
      };
      const res2 = await fetch(`${NICORU_KEYS}?${new URLSearchParams(requestQuery)}`, {
        method: "GET",
        headers: {
          "x-frontend-id": 6,
          "x-frontend-version": 0
        },
        credentials: "include"
      });
      const json = await res2.json();
      if (json.data !== void 0) {
        return json.data.nicoruKey;
      } else {
        throw new Error({
          message: "[ECT] ERROR: getNicoruKey",
          object: res2
        });
      }
    } catch (e) {
      throw new Error(e);
    }
  };

  // src/index.js
  (() => {
    "use strict";
    const configInitData = {
      extraMain: {
        label: "\u300C\u5F15\u7528\u30B3\u30E1\u30F3\u30C8\u300D\u3092\u300C\u30C1\u30E3\u30F3\u30CD\u30EB\u30B3\u30E1\u30F3\u30C8\u300D\u306B\u7D71\u5408\u3059\u308B",
        type: "checkbox",
        default: true
      },
      extraEasy: {
        label: "\u300C\u5F15\u7528\u304B\u3093\u305F\u3093\u30B3\u30E1\u30F3\u30C8\u300D\u3092\u300C\u304B\u3093\u305F\u3093\u30B3\u30E1\u30F3\u30C8\u300D\u306B\u7D71\u5408\u3059\u308B",
        type: "checkbox",
        default: true
      },
      extraMainFromDanime: {
        label: "\u300Cd\u30A2\u30CB\u30E1\u30B9\u30C8\u30A2 \u30CB\u30B3\u30CB\u30B3\u652F\u5E97\u300D\u306E\u30B3\u30E1\u30F3\u30C8\u3092\u5F15\u7528\u30FB\u7D71\u5408\u3059\u308B",
        type: "checkbox",
        default: false
      },
      forcedExtra: {
        label: "\u30B3\u30E1\u30F3\u30C8\u3092\u5F37\u5236\u7684\u306B\u5F15\u7528\u30FB\u7D71\u5408\u3059\u308B",
        type: "checkbox",
        default: false
      },
      showAddedCommentCount: {
        label: "\u7D71\u5408\u3057\u305F\u30B3\u30E1\u30F3\u30C8\u306E\u6570\u3092\u30B3\u30E1\u30F3\u30C8\u6570\u6A2A\u306B\u8868\u793A\u3059\u308B",
        type: "checkbox",
        default: true
      },
      showExtraViewCount: {
        label: "\u5F15\u7528\u3057\u305F\u52D5\u753B\u306E\u518D\u751F\u6570\u3092\u518D\u751F\u6570\u6A2A\u306B\u8868\u793A\u3059\u308B",
        type: "checkbox",
        default: false
      },
      deleteExtra: {
        label: "\u300C\u5F15\u7528\u30B3\u30E1\u30F3\u30C8\u300D\u3068\u300C\u5F15\u7528\u304B\u3093\u305F\u3093\u30B3\u30E1\u30F3\u30C8\u300D\u3092\u975E\u8868\u793A\u306B\u3059\u308B",
        type: "checkbox",
        default: false
      },
      deleteEasy: {
        label: "\u300C\u304B\u3093\u305F\u3093\u30B3\u30E1\u30F3\u30C8\u300D\u3068\u300C\u5F15\u7528\u304B\u3093\u305F\u3093\u30B3\u30E1\u30F3\u30C8\u300D\u3092\u975E\u8868\u793A\u306B\u3059\u308B",
        type: "checkbox",
        default: false
      },
      kawaiiPct: {
        label: "\u30B3\u30E1\u30F3\u30C8\u306E\u300C\u304B\u308F\u3044\u3044\u300D\u7387\u3092\u8868\u793A\u3059\u308B",
        type: "checkbox",
        default: false
      }
    };
    GM_config.init("\u30CB\u30B3\u30CB\u30B3\u52D5\u753B \u5F15\u7528\u30B3\u30E1\u30F3\u30C8\u30C4\u30FC\u30EB \u8A2D\u5B9A", configInitData);
    GM_config.onload = () => {
      setTimeout(() => {
        alert("\u8A2D\u5B9A\u3092\u53CD\u6620\u3055\u305B\u308B\u306B\u306F\u30DA\u30FC\u30B8\u3092\u518D\u8AAD\u307F\u8FBC\u307F\u3057\u3066\u304F\u3060\u3055\u3044\u3002");
      }, 200);
    };
    GM_registerMenuCommand("\u8A2D\u5B9A", GM_config.open);
    const config = {};
    Object.keys(configInitData).forEach((v) => {
      config[v] = GM_config.get(v);
    });
    console.log("[ECT] config:", config);
    if (!location.pathname.startsWith("/watch/"))
      return;
    const ECT = {
      DANIME_CHANNEL_ID: "ch2632720",
      get videoId() {
        return location.pathname.split("/")[2];
      },
      getChannnelThreadsData(videoData) {
        return videoData?.comment?.threads?.filter((thread) => {
          return {
            "main": "community",
            "easy": "easy"
          }[thread.forkLabel] === thread.label;
        }) || [];
      },
      getExtraThreadsData(videoData) {
        return videoData?.comment?.threads?.filter((thread) => {
          return {
            "main": "extra-community",
            "easy": "extra-easy"
          }[thread.forkLabel] === thread.label;
        }) || [];
      },
      async getLinkedVideo(videoId) {
        try {
          const linkedVideoId = await getLinkedVideoId(videoId);
          const linkedVideoData = await getVideoData(linkedVideoId);
          const linkedVideoThreads = await getThreads(linkedVideoData);
          return {
            videoData: linkedVideoData,
            threads: linkedVideoThreads
          };
        } catch (e) {
          console.error(e);
        }
      },
      async getIdenticalVideo(videoData) {
        if (videoData === void 0 || videoData.video === void 0) {
          throw new Error("[ECT] ERROR: videoData is undefined");
        }
        try {
          const searchTitle = optimizeTitleForSearch(videoData.video.title);
          const duration = videoData.video.duration;
          console.log("[ECT] search title:", searchTitle);
          let searchResult;
          let filteredResult;
          try {
            const query = {
              targets: ["title"],
              filters: {
                "categoryTags": "\u30A2\u30CB\u30E1",
                "genre.keyword": "\u30A2\u30CB\u30E1",
                "lengthSeconds": Number.isFinite(duration) ? [duration - 2, duration + 2] : void 0
              }
            };
            searchResult = await search({
              q: searchTitle,
              ...query
            });
            console.log("[ECT] search result 1:", searchResult);
            if (Array.isArray(searchResult) && searchResult.length <= 1) {
              searchResult = await search({
                q: videoData.video.title,
                ...query
              });
              console.log("[ECT] search result 2:", searchResult);
            }
            if (Array.isArray(searchResult) && 0 < searchResult.length) {
              filteredResult = searchResult.filter((val) => isEqualTitle(val.title, videoData.video.title) && val.contentId !== videoData.video.id);
            }
          } catch (e) {
            console.error(e);
          }
          if (!Array.isArray(filteredResult) || filteredResult[0] === void 0) {
            try {
              const res2 = await fetch(`https://www.nicovideo.jp/search/${encodeURIComponent(searchTitle)}?genre=anime&sort=f&order=a`);
              const elem = generateDocumentByHTML(await res2.text());
              searchResult = Array.from(elem.querySelectorAll('.item[data-video-item][data-video-id^="so"]')).filter((item) => {
                const videoLength = item.getElementsByClassName("videoLength")[0].textContent.trim().split(":");
                const videoDuration = videoLength.length === 2 ? Number(videoLength[0]) * 60 + Number(videoLength[1]) : 0;
                return Math.abs(videoDuration - duration) <= 2;
              }).map((item) => ({
                contentId: item.dataset.videoId,
                title: item.querySelector(".itemTitle > a")?.title?.trim()
              }));
              console.log("[ECT] search result 3:", searchResult);
              if (Array.isArray(searchResult) && 0 < searchResult.length) {
                filteredResult = searchResult.filter((val) => isEqualTitle(val.title, videoData.video.title) && val.contentId !== videoData.video.id);
              }
            } catch (e) {
              console.error(e);
            }
          }
          console.log("[ECT] search result (filtered):", filteredResult);
          if (filteredResult[0] !== void 0) {
            const identicalVideoData = await getVideoData(filteredResult[0].contentId);
            if (identicalVideoData?.channel?.isOfficialAnime) {
              const identicalVideoThreads = await getThreads(identicalVideoData);
              return {
                videoData: identicalVideoData,
                threads: identicalVideoThreads
              };
            }
          }
        } catch (e) {
          console.error(e);
        }
      }
    };
    unsafeWindow.ECT = ECT;
    unsafeWindow.fetch = new Proxy(unsafeWindow.fetch, {
      apply: async function(target, thisArg, argumentsList) {
        if (argumentsList[0].startsWith(THREADS) && argumentsList[0].endsWith("/nicorus") && unsafeWindow["origExtraThreadsOldNums"] !== null && unsafeWindow["origExtraThreads"] !== null) {
          try {
            const splitedUrl = argumentsList[0].split("/");
            const body = JSON.parse(argumentsList[1].body);
            const oldNums = unsafeWindow["origExtraThreadsOldNums"][`${splitedUrl[5]}-${body.fork}`];
            const oldNo = oldNums[body.no];
            body.no = oldNo || body.no;
            let isExtraComment = false;
            for (const thread of unsafeWindow["origExtraThreads"]) {
              if (!isExtraComment && thread.fork === body.fork) {
                if (oldNo !== void 0) {
                  isExtraComment = Boolean(thread.comments?.find((cmt) => cmt.no === oldNo && cmt.body === body.content));
                  if (isExtraComment) {
                    splitedUrl[5] = thread.id;
                    body.videoId = thread.videoId;
                    body.nicoruKey = await getNicoruKey(splitedUrl[5], body.fork);
                  }
                }
              }
            }
            argumentsList[0] = splitedUrl.join("/");
            argumentsList[1].body = JSON.stringify(body);
          } catch (e) {
            console.error(e);
          }
          return Reflect.apply(target, thisArg, argumentsList);
        } else if (argumentsList[0] !== THREADS) {
          return Reflect.apply(target, thisArg, argumentsList);
        }
        const promise = Reflect.apply(target, thisArg, argumentsList);
        let videoData;
        let extraVideoData;
        let linkedVideo;
        let identicalVideo;
        let extraThreadsData = [];
        let channelThreadsData = [];
        unsafeWindow["origExtraThreads"] = null;
        unsafeWindow["origExtraThreadsOldNums"] = null;
        try {
          setTimeout(() => {
            document.querySelector(".FormattedNumber-addedComment")?.remove();
            document.querySelector(".FormattedNumber-extraView")?.remove();
          }, 0);
          console.log("[ECT] %cfetch start%c", "color:white;background-color:blue;", "");
          videoData = await getVideoData(ECT.videoId);
          if (!videoData?.channel?.isOfficialAnime) {
            return promise;
          }
          console.log("[ECT] videoData:", videoData);
          if (videoData.channel.id === ECT.DANIME_CHANNEL_ID) {
            extraThreadsData = ECT.getExtraThreadsData(videoData);
            if (extraThreadsData.length !== 0) {
              extraVideoData = await getVideoData(extraThreadsData[0].videoId);
            }
          } else if (!config["deleteExtra"] && config["extraMainFromDanime"]) {
            linkedVideo = await ECT.getLinkedVideo(ECT.videoId);
            if (linkedVideo !== void 0 && Math.abs(videoData.video.duration - linkedVideo.videoData.video.duration) <= 2) {
              extraThreadsData = ECT.getChannnelThreadsData(linkedVideo.videoData);
              if (extraThreadsData.length !== 0) {
                extraVideoData = linkedVideo.videoData;
              }
            }
          }
          console.log("[ECT] linkedVideo:", linkedVideo);
          if (!config["deleteExtra"] && config["forcedExtra"] && extraThreadsData.length === 0 && (videoData.channel.id === ECT.DANIME_CHANNEL_ID || config["extraMainFromDanime"])) {
            identicalVideo = await ECT.getIdenticalVideo(videoData);
            if (identicalVideo !== void 0) {
              extraThreadsData = ECT.getChannnelThreadsData(identicalVideo.videoData);
              if (extraThreadsData.length !== 0) {
                extraVideoData = identicalVideo.videoData;
              }
            }
          }
          console.log("[ECT] identicalVideo:", identicalVideo);
          console.log("[ECT] extraThreadsData:", extraThreadsData);
          channelThreadsData = ECT.getChannnelThreadsData(videoData);
          console.log("[ECT] channelThreadsData:", channelThreadsData);
          console.log("[ECT] extraVideoData:", extraVideoData);
        } catch (e) {
          console.error(e);
        }
        const response = await promise;
        const json = await response.json();
        console.log("[ECT] json:", json);
        if (Boolean(json.data?.threads?.length)) {
          let addedCommentCount = 0;
          for (const extraThreadData of extraThreadsData) {
            try {
              const extraThreadIdx = json.data.threads.findIndex((thread) => extraThreadData.id.toString() === thread.id && extraThreadData.forkLabel === thread.fork);
              if (config["deleteExtra"] && extraThreadIdx !== -1) {
                delete json.data.threads[extraThreadIdx];
              } else {
                const targetThreadData = channelThreadsData.find((channelThread) => extraThreadData.forkLabel === channelThread.forkLabel && extraThreadData.label.indexOf(channelThread.label) !== -1);
                if (targetThreadData === void 0) {
                  continue;
                }
                const targetThreadIdx = json.data.threads.findIndex((thread) => targetThreadData.id.toString() === thread.id && targetThreadData.forkLabel === thread.fork);
                if (targetThreadIdx === -1) {
                  continue;
                }
                if (config["extraMain"] && extraThreadData.forkLabel === "main" || config["extraEasy"] && extraThreadData.forkLabel === "easy" && !config["deleteEasy"] || config["forcedExtra"] && identicalVideo !== void 0) {
                  let extraThread;
                  if (extraThreadIdx !== -1) {
                    extraThread = json.data.threads[extraThreadIdx];
                    delete json.data.threads[extraThreadIdx];
                  } else {
                    extraThread = (linkedVideo || identicalVideo)?.threads?.find((thread) => extraThreadData.id.toString() === thread.id && extraThreadData.forkLabel === thread.fork);
                  }
                  if (extraThread == null) {
                    continue;
                  }
                  unsafeWindow["origExtraThreads"] || (unsafeWindow["origExtraThreads"] = []);
                  unsafeWindow["origExtraThreads"].push(JSON.parse(JSON.stringify({
                    videoId: extraThreadData.videoId,
                    ...extraThread
                  })));
                  addedCommentCount += extraThread.commentCount;
                  json.data.threads[targetThreadIdx].comments.push(...extraThread.comments);
                  json.data.threads[targetThreadIdx].comments.sort((a, b) => new Date(a.postedAt).getTime() - new Date(b.postedAt).getTime());
                  const oldNo = [];
                  json.data.threads[targetThreadIdx].comments.forEach((v, i) => {
                    oldNo[i + 1] = v.no;
                    v.no = i + 1;
                  });
                  unsafeWindow["origExtraThreadsOldNums"] || (unsafeWindow["origExtraThreadsOldNums"] = {});
                  unsafeWindow["origExtraThreadsOldNums"][`${targetThreadData.id}-${targetThreadData.forkLabel}`] = oldNo;
                  json.data.threads[targetThreadIdx].commentCount += extraThread.commentCount;
                }
              }
              json.data.threads = json.data.threads.filter(Boolean);
            } catch (e) {
              console.error(e);
            }
          }
          if (config["deleteEasy"]) {
            json.data.threads = json.data.threads.filter((v) => v.fork !== "easy");
          }
          if (config["showAddedCommentCount"] && addedCommentCount !== 0) {
            setTimeout((cnt) => {
              const counter = document.querySelector(".CommentCountMeta-counter > .FormattedNumber");
              counter?.insertAdjacentHTML(
                "afterend",
                `<span class="FormattedNumber-addedComment">&nbsp;(+${cnt.toLocaleString()})</span>`
              );
            }, 0, addedCommentCount);
          }
          console.log(`[ECT] \u7D71\u5408\u3057\u305F\u5F15\u7528\u30B3\u30E1\u30F3\u30C8\u6570: ${addedCommentCount}`);
        }
        if (config["showExtraViewCount"] && Boolean(extraVideoData?.video?.count?.view)) {
          setTimeout((cnt) => {
            const counter = document.querySelector(".VideoViewCountMeta-counter > .FormattedNumber");
            counter?.insertAdjacentHTML(
              "afterend",
              `<span class="FormattedNumber-extraView">&nbsp;(+${cnt.toLocaleString()})</span>`
            );
          }, 0, extraVideoData.video.count.view);
        }
        if (config["kawaiiPct"]) {
          let cmtCnt = 0;
          let kawaiiCnt = 0;
          for (const thread of json.data.threads) {
            for (const comment of thread.comments) {
              cmtCnt++;
              if (comment.body.indexOf("\u304B\u308F\u3044\u3044") !== -1) {
                kawaiiCnt++;
              }
            }
          }
          const kawaiiPct = Math.round(kawaiiCnt / cmtCnt * 10 * 100) / 10;
          if (0 < kawaiiPct) {
            setTimeout((pct) => {
              try {
                const kawaiiPctElem = document.querySelector(".KawaiiPctMeta") || document.querySelector(".CommentCountMeta")?.cloneNode(true);
                if (kawaiiPctElem instanceof HTMLElement) {
                  kawaiiPctElem.classList.add("KawaiiPctMeta");
                  kawaiiPctElem.querySelector(".CommentCountMeta-title").textContent = "\u304B\u308F\u3044\u3044\u7387";
                  kawaiiPctElem.querySelector(".CommentCountMeta-counter").textContent = `${pct}%`;
                  document.querySelector(".CommentCountMeta").insertAdjacentElement("afterend", kawaiiPctElem);
                }
              } catch (e) {
                console.error(e);
              }
            }, 0, kawaiiPct);
          }
        }
        return new Response(JSON.stringify(json), {
          status: response.status,
          statusText: response.statusText,
          headers: response.headers
        });
      }
    });
    const obs_opt = {
      childList: true,
      subtree: true
    };
    const obs = new MutationObserver((mutationRecord) => {
      for (const { addedNodes } of mutationRecord) {
        for (const added of addedNodes) {
          if (added instanceof HTMLElement && added.classList.contains("ContextMenu-wrapper")) {
            obs.disconnect();
            const menuContainer = added.getElementsByClassName("VideoContextMenuContainer")[0];
            if (menuContainer instanceof HTMLElement) {
              const ectOptionBtn = generateElementByHTML(
                `
              <div class="VideoContextMenu-group">
                <div class="ContextMenuItem">\u5F15\u7528\u30B3\u30E1\u30F3\u30C8\u30C4\u30FC\u30EB \u8A2D\u5B9A</div>
              </div>
              `
              );
              ectOptionBtn.firstElementChild.addEventListener("click", GM_config.open);
              menuContainer.appendChild(ectOptionBtn);
            }
            obs.observe(document.body, obs_opt);
          }
        }
      }
    });
    obs.observe(document.body, obs_opt);
  })();
})();

QingJ © 2025

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