buyNow!

ふたばちゃんねるのスレッド上で貼られた特定のECサイトのURLからタイトルとあれば価格と画像を取得する

目前為 2024-09-02 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         buyNow!
// @namespace    http://2chan.net/
// @version      0.8.8
// @description  ふたばちゃんねるのスレッド上で貼られた特定のECサイトのURLからタイトルとあれば価格と画像を取得する
// @author       ame-chan
// @match        http://*.2chan.net/b/res/*
// @match        https://*.2chan.net/b/res/*
// @match        https://kako.futakuro.com/futa/*
// @match        https://tsumanne.net/si/data/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=2chan.net
// @grant        GM_xmlhttpRequest
// @connect      amazon.co.jp
// @connect      www.amazon.co.jp
// @connect      amzn.to
// @connect      amzn.asia
// @connect      media-amazon.com
// @connect      m.media-amazon.com
// @connect      dlsite.com
// @connect      img.dlsite.jp
// @connect      bookwalker.jp
// @connect      c.bookwalker.jp
// @connect      store.steampowered.com
// @connect      cdn.akamai.steamstatic.com
// @connect      cdn.cloudflare.steamstatic.com
// @connect      store.cloudflare.steamstatic.com
// @connect      youtube.com
// @connect      youtu.be
// @connect      nintendo.com
// @connect      store-jp.nintendo.com
// @connect      dmm.co.jp
// @connect      www.dmm.co.jp
// @connect      dlsoft.dmm.co.jp
// @connect      pics.dmm.co.jp
// @connect      doujin-assets.dmm.co.jp
// @license      MIT
// ==/UserScript==
(function () {
  'use strict';
  const WHITE_LIST_DOMAINS = [
    'amazon.co.jp',
    'amzn.to',
    'amzn.asia',
    'dlsite.com',
    'bookwalker.jp',
    'store.steampowered.com',
    'youtube.com',
    'youtu.be',
    'store-jp.nintendo.com',
    'dlsoft.dmm.co.jp',
    'www.dmm.co.jp',
  ];
  const WHITE_LIST_SELECTORS = (() => WHITE_LIST_DOMAINS.map((domain) => `a[href*="${domain}"]`).join(','))();
  const convertHostname = (path) => new URL(path).hostname;
  const isAmazon = (path) => /^(www\.)?amazon.co.jp|amzn\.to|amzn\.asia$/.test(convertHostname(path));
  const isDLsite = (path) => /^(www\.)?dlsite\.com$/.test(convertHostname(path));
  const isBookwalker = (path) => /^(www\.)?bookwalker.jp$/.test(convertHostname(path));
  const isSteam = (path) => /^store\.steampowered\.com$/.test(convertHostname(path));
  const isYouTube = (path) => /^youtu\.be|(www\.)?youtube.com$/.test(convertHostname(path));
  const isNintendoStore = (path) => /^store-jp\.nintendo\.com$/.test(convertHostname(path));
  const isFanzaDoujin = (path) => /^(www\.)?dmm\.co\.jp$/.test(convertHostname(path));
  const isFanzaDlsoft = (path) => /^dlsoft\.dmm\.co\.jp$/.test(convertHostname(path));
  const isProductPage = (url) =>
    /^https?:\/\/(?:www\.)?amazon(.+?)\/(?:exec\/obidos\/ASIN|gp\/product|gp\/aw\/d|o\/ASIN|(?:.+?\/)?dp)\/[A-Z0-9]{10}/.test(
      url,
    ) ||
    /^https?:\/\/amzn.(asia|to)\//.test(url) ||
    /^https?:\/\/(www\.)?dlsite\.com\/.+?\/[A-Z0-9]{8,}(\.html)?/.test(url) ||
    /^https?:\/\/(www\.)?bookwalker\.jp\/[a-z0-9]{10}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{4}\-[a-z0-9]{12}/.test(url) ||
    /^https?:\/\/(www\.)?bookwalker\.jp\/series\/[0-9]+\/list/.test(url) ||
    /^https?:\/\/store.steampowered.com\/(agecheck\/)?app\/\d+/.test(url) ||
    /^https?:\/\/(youtu\.be\/|((www|m)\.)?youtube.com\/(watch\?v=|live\/))\w+/.test(url) ||
    /^https?:\/\/store-jp\.nintendo\.com\/list\/software\/(D)?[0-9]+.html/.test(url) ||
    /^https?:\/\/dlsoft\.dmm\.co\.jp\/(list|detail)\/.+?/.test(url) ||
    /^https?:\/\/(www\.)?dmm\.co\.jp\/dc\/doujin\/.+?/.test(url);
  const getBrandName = (url) => {
    if (isAmazon(url)) {
      return 'amazon';
    } else if (isDLsite(url)) {
      return 'dlsite';
    } else if (isBookwalker(url)) {
      return 'bookwalker';
    } else if (isSteam(url)) {
      return 'steam';
    } else if (isYouTube(url)) {
      return 'youtube';
    } else if (isNintendoStore(url)) {
      return 'nintendo';
    } else if (isFanzaDlsoft(url)) {
      return 'fanzaDlsoft';
    } else if (isFanzaDoujin(url)) {
      return 'fanzaDoujin';
    }
    return '';
  };
  const getOgpImage = (targetDocument) => targetDocument.querySelector('meta[property="og:image"]')?.content || '';
  const getSelectorConditions = {
    amazon: {
      price: (targetDocument) => {
        const priceRange = () => {
          const rangeElm = targetDocument.querySelector('.a-price-range');
          if (!rangeElm) return 0;
          rangeElm.querySelectorAll('.a-offscreen').forEach((el) => el.remove());
          return rangeElm.textContent?.replace(/[\s]+/g, '');
        };
        try {
          const price =
            targetDocument.querySelector('#twister-plus-price-data-price')?.value ||
            targetDocument.querySelector('#kindle-price')?.textContent?.replace(/[\s¥,]+/g, '') ||
            targetDocument.querySelector('[name="displayedPrice"]')?.value ||
            targetDocument.querySelector('.a-price-whole')?.textContent?.replace(/[\s¥,]+/g, '');
          return Math.round(Number(price)) || priceRange() || 0;
        } catch (e) {
          return 0;
        }
      },
      image: (targetDocument) =>
        targetDocument.querySelector('#landingImage')?.src ||
        targetDocument.querySelector('.unrolledScrollBox li:first-child img')?.src ||
        targetDocument.querySelector('[data-a-image-name]')?.src ||
        targetDocument.querySelector('#imgBlkFront')?.src,
      title: (targetDocument) =>
        targetDocument.querySelector('#productTitle')?.textContent ||
        targetDocument.querySelector('#title')?.textContent ||
        targetDocument.querySelector('#landingImage')?.alt ||
        targetDocument.querySelector('.unrolledScrollBox li:first-child img')?.alt ||
        targetDocument.querySelector('[data-a-image-name]')?.alt ||
        targetDocument.querySelector('#imgBlkFront')?.alt,
    },
    dlsite: {
      price: (targetDocument) => {
        try {
          const url = targetDocument.querySelector('meta[property="og:url"]')?.content;
          const productId = url.split('/').pop()?.replace('.html', '');
          const priceElm = targetDocument.querySelector(`[data-product_id="${productId}"][data-price]`);
          return parseInt(priceElm?.getAttribute('data-price') || '0', 10);
        } catch (e) {
          return 0;
        }
      },
      image: getOgpImage,
    },
    bookwalker: {
      price: (targetDocument) => {
        try {
          const price =
            Number(
              targetDocument
                .querySelector('.m-tile-list .m-tile .m-book-item__price-num')
                ?.textContent?.replace(/,/g, ''),
            ) || Number(targetDocument.querySelector('#jsprice')?.textContent?.replace(/[円,]/g, ''));
          return Number.isInteger(price) && price > 0 ? price : 0;
        } catch (e) {
          return 0;
        }
      },
      image: (targetDocument) =>
        targetDocument.querySelector('.m-tile-list .m-tile img')?.getAttribute('data-original') ||
        getOgpImage(targetDocument),
    },
    steam: {
      price: (targetDocument) => {
        try {
          const elm =
            targetDocument.querySelector('.game_area_purchase_game_wrapper .game_purchase_price.price') ||
            targetDocument.querySelector('.game_area_purchase_game .game_purchase_price.price') ||
            targetDocument.querySelector('.game_area_purchase_game_wrapper .discount_final_price');
          const price = elm?.firstChild?.textContent?.replace(/[¥,\s\t\r\n]+/g, '');
          const isComingSoon = targetDocument.querySelector('.game_area_comingsoon');
          const isAgeCheck = targetDocument.querySelector('#app_agegate');
          const num = Number(price);
          if (isAgeCheck) {
            return 'ログインか年齢確認が必要です';
          } else if (isComingSoon) {
            return '近日登場';
          } else if (Number.isInteger(num) && num > 0) {
            return num;
          } else if (typeof price === 'string') {
            return price;
          }
          return 0;
        } catch (e) {
          return 0;
        }
      },
      image: getOgpImage,
    },
    nintendo: {
      price: (targetDocument) => {
        try {
          const priceElm = targetDocument.querySelector('.js-productMainRenderedPrice > span:first-of-type');
          const priceText = priceElm?.textContent?.replace(/,/g, '');
          const price = Number(priceText);
          if (Number.isInteger(price) && price > 0) {
            return price;
          } else if (typeof priceText === 'string') {
            return priceText;
          }
          return 0;
        } catch (e) {
          return 0;
        }
      },
      image: getOgpImage,
    },
    fanzaDlsoft: {
      price: (targetDocument) => {
        try {
          const priceElm =
            targetDocument.querySelector('.tx-bskt-price') ||
            targetDocument.querySelector('.sellingPrice__discountedPrice');
          const priceText = priceElm?.textContent?.replace(/[,円]/g, '');
          const price = Number(priceText);
          if (Number.isInteger(price) && price > 0) {
            return price;
          } else if (typeof priceText === 'string') {
            return priceText;
          }
          return 0;
        } catch (e) {
          return 0;
        }
      },
      image: (targetDocument) => {
        return getOgpImage(targetDocument) || targetDocument.querySelector('.d-item #list .img img')?.src || '';
      },
    },
    fanzaDoujin: {
      price: (targetDocument) => {
        try {
          const priceText =
            targetDocument.querySelector('p.priceList__main')?.textContent?.replace(/[,円]/g, '') ||
            targetDocument.querySelector('.purchase__btn')?.getAttribute('data-price');
          const price = Number(priceText);
          if (Number.isInteger(price) && price > 0) {
            return price;
          } else if (typeof priceText === 'string') {
            return priceText;
          }
          return 0;
        } catch (e) {
          return 0;
        }
      },
      image: (targetDocument) => {
        return (
          getOgpImage(targetDocument) || targetDocument.querySelector('.productList .tileListImg__tmb img')?.src || ''
        );
      },
    },
    // 画像のみ取得
    youtube: {
      price: () => 0,
      image: getOgpImage,
    },
  };
  const addedStyle = `<style id="userjs-buyNow-style">
  .userjs-title {
    display: flex;
    flex-direction: row;
    margin: 8px 0 16px;
    gap: 16px;
    padding: 16px;
    line-height: 1.6 !important;
    color: #ff3860 !important;
    background-color: #fff;
    border-radius: 4px;
  }
  .userjs-title-inner {
    display: flex;
    flex-direction: column;
    gap: 8px;
    line-height: 1.6 !important;
    color: #ff3860 !important;
  }
  .userjs-link {
    padding-right: 24px;
    background-image: url('data:image/svg+xml;charset=utf8,%3Csvg%20width%3D%2216%22%20height%3D%2216%22%20viewBox%3D%220%200%2038%2038%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20stroke%3D%22%23000%22%3E%3Cg%20fill%3D%22none%22%20fill-rule%3D%22evenodd%22%3E%3Cg%20transform%3D%22translate(1%201)%22%20stroke-width%3D%222%22%3E%3Ccircle%20stroke-opacity%3D%22.5%22%20cx%3D%2218%22%20cy%3D%2218%22%20r%3D%2218%22%2F%3E%3Cpath%20d%3D%22M36%2018c0-9.94-8.06-18-18-18%22%3E%20%3CanimateTransform%20attributeName%3D%22transform%22%20type%3D%22rotate%22%20from%3D%220%2018%2018%22%20to%3D%22360%2018%2018%22%20dur%3D%221s%22%20repeatCount%3D%22indefinite%22%2F%3E%3C%2Fpath%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E');
    background-repeat: no-repeat;
    background-position: right center;
  }
  .userjs-imageWrap {
    width: 150px;
  }
  .userjs-imageWrap.-center {
    text-align: center;
  }
  .userjs-imageWrap.-large {
    width: 600px;
  }
  .userjs-image {
    max-width: none !important;
    max-height: none !important;
    transition: all 0.2s ease-in-out;
    border-radius: 4px;
  }
  .userjs-price {
    display: block;
    color: #228b22 !important;
    font-weight: 700;
  }
  </style>`;
  if (!document.querySelector('#userjs-buyNow-style')) {
    document.head.insertAdjacentHTML('beforeend', addedStyle);
  }
  class FileReaderEx extends FileReader {
    constructor() {
      super();
    }
    #readAs(blob, ctx) {
      return new Promise((res, rej) => {
        super.addEventListener('load', ({ target }) => target?.result && res(target.result));
        super.addEventListener('error', ({ target }) => target?.error && rej(target.error));
        super[ctx](blob);
      });
    }
    readAsArrayBuffer(blob) {
      return this.#readAs(blob, 'readAsArrayBuffer');
    }
    readAsDataURL(blob) {
      return this.#readAs(blob, 'readAsDataURL');
    }
  }
  const fetchData = (url, responseType) =>
    new Promise((resolve) => {
      let options = {
        method: 'GET',
        url,
        timeout: 10000,
        onload: (result) => {
          if (result.status === 200 || result.status === 404) {
            return resolve(result.response);
          }
          return resolve(false);
        },
        onerror: () => resolve(false),
        ontimeout: () => resolve(false),
      };
      if (typeof responseType === 'string') {
        options = {
          ...options,
          responseType,
        };
      }
      GM_xmlhttpRequest(options);
    });
  const setFailedText = (linkElm) => {
    if (linkElm && linkElm instanceof HTMLAnchorElement) {
      linkElm.insertAdjacentHTML('afterend', '<span class="userjs-title">データ取得失敗</span>');
    }
  };
  const getPriceText = (price) => {
    let priceText = price;
    if (!price) return '';
    if (typeof price === 'number' && Number.isInteger(price) && price > 0) {
      priceText = new Intl.NumberFormat('ja-JP', {
        style: 'currency',
        currency: 'JPY',
      }).format(price);
    }
    return `<span class="userjs-price">${priceText}</span>`;
  };
  const setTitleText = ({ targetDocument, selectorCondition, linkElm, brandName }) => {
    let titleElm = targetDocument.querySelector('title');
    let title = titleElm?.textContent ?? '';
    // Amazonはtitleタグが無い場合がある
    if (title === '' && brandName === 'amazon') {
      title = selectorCondition.title(targetDocument);
    }
    if (!title) {
      setFailedText(linkElm);
      return;
    }
    const price = selectorCondition.price(targetDocument);
    const priceText = getPriceText(price);
    const nextSibling = linkElm.nextElementSibling;
    if (nextSibling && nextSibling instanceof HTMLElement && nextSibling.tagName.toLowerCase() === 'br') {
      nextSibling.style.display = 'none';
    }
    if (title === 'サイトエラー') {
      const errorText = targetDocument.querySelector('#error_box')?.textContent;
      if (errorText) {
        title = errorText;
      }
    }
    if (linkElm && linkElm instanceof HTMLAnchorElement) {
      linkElm.insertAdjacentHTML(
        'afterend',
        `<div class="userjs-title">
        <span class="userjs-title-inner">${title}${priceText}</span>
      </div>`,
      );
    }
  };
  const setImageElm = async ({ imagePath, titleTextElm }) => {
    const imageMinSize = 150;
    const imageMaxSize = 600;
    const imageEventHandler = (e) => {
      const self = e.currentTarget;
      const div = self?.parentElement;
      if (!(self instanceof HTMLImageElement) || !div) return;
      if (self.width === imageMinSize) {
        div.classList.remove('-center');
        div.classList.add('-large');
        self.width = imageMaxSize;
      } else {
        div.classList.remove('-large');
        if (self.naturalWidth > imageMinSize) {
          self.width = imageMinSize;
        } else {
          div.classList.add('-center');
          self.width = self.naturalWidth;
        }
      }
    };
    const blob = await fetchData(imagePath, 'blob');
    const titleInnerElm = titleTextElm.querySelector('.userjs-title-inner');
    if (!blob || !titleInnerElm) return false;
    const dataUrl = await new FileReaderEx().readAsDataURL(blob);
    const div = document.createElement('div');
    div.classList.add('userjs-imageWrap');
    const img = document.createElement('img');
    img.addEventListener('load', () => {
      if (img.naturalWidth < imageMinSize) {
        img.width = img.naturalWidth;
      }
    });
    img.src = dataUrl;
    img.width = imageMinSize;
    img.classList.add('userjs-image');
    div.appendChild(img);
    img.addEventListener('click', imageEventHandler);
    titleInnerElm.insertAdjacentElement('beforebegin', div);
    return img;
  };
  const setLoading = (linkElm) => {
    const parentElm = linkElm.parentElement;
    if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
      return;
    }
    linkElm.classList.add('userjs-link');
  };
  const removeLoading = (targetElm) => targetElm.classList.remove('userjs-link');
  // AgeCheck
  const isAgeCheck = (targetDocument, selector) => targetDocument.querySelector(selector) !== null;
  const getAgeCheckConfirmAdultPageHref = ({ targetDocument, selector, domain = '' }) => {
    const yesBtnLinkElm = targetDocument.querySelector(selector);
    if (yesBtnLinkElm instanceof HTMLAnchorElement) {
      return `${domain}${yesBtnLinkElm.getAttribute('href')}`;
    }
    return false;
  };
  const getAgeCheckPassedAdultDocument = async ({ targetDocument, linkElm, parser, selector, domain = '' }) => {
    const newHref = getAgeCheckConfirmAdultPageHref({
      targetDocument,
      selector,
      domain,
    });
    const htmlData = newHref && (await fetchData(newHref));
    if (!htmlData) {
      setFailedText(linkElm);
      removeLoading(linkElm);
      return false;
    }
    return parser.parseFromString(htmlData, 'text/html');
  };
  const getNewDocument = async ({ targetDocument, linkElm, parser, brandName }) => {
    const domain = brandName === 'amazon' ? 'https://www.amazon.co.jp' : '';
    const selector = (() => {
      switch (brandName) {
        case 'amazon':
          return '#black-curtain-yes-button a';
        case 'fanzaDlsoft':
        case 'fanzaDoujin':
          return '.ageCheck__link.ageCheck__link--r18';
        default:
          return false;
      }
    })();
    if (selector) {
      const newDocument = await getAgeCheckPassedAdultDocument({
        targetDocument,
        linkElm,
        parser,
        selector,
        domain,
      });
      if (newDocument) {
        return newDocument;
      }
    }
    return false;
  };
  // ふたクロで「新着レスに自動スクロール」にチェックが入っている場合画像差し込み後に下までスクロールさせる
  const scrollIfAutoScrollIsEnabled = () => {
    const checkboxElm = document.querySelector('#autolive_scroll');
    const readmoreElm = document.querySelector('#res_menu');
    if (checkboxElm === null || readmoreElm === null || !checkboxElm?.checked) {
      return;
    }
    const elementHeight = readmoreElm.offsetHeight;
    const viewportHeight = window.innerHeight;
    const offsetTop = readmoreElm.offsetTop;
    window.scrollTo({
      top: offsetTop - viewportHeight + elementHeight,
      behavior: 'smooth',
    });
  };
  const insertURLData = async (linkElm) => {
    const parentElm = linkElm.parentElement;
    if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
      removeLoading(linkElm);
      return;
    }
    const brandName = getBrandName(linkElm.href);
    if (brandName === '') {
      setFailedText(linkElm);
      removeLoading(linkElm);
      return;
    }
    const htmlData = await fetchData(linkElm.href);
    if (!htmlData) {
      setFailedText(linkElm);
      removeLoading(linkElm);
      return;
    }
    const adultPageLists = ['amazon', 'fanzaDlsoft', 'fanzaDoujin'];
    const parser = new DOMParser();
    let targetDocument = parser.parseFromString(htmlData, 'text/html');
    // AmazonやFANZAのアダルトページ確認画面スキップ
    if (adultPageLists.includes(brandName)) {
      const is18xAmazon = isAgeCheck(targetDocument, '#black-curtain-warning');
      const is18xFanza = isAgeCheck(targetDocument, '.ageCheck');
      if (is18xAmazon || is18xFanza) {
        const newDocument = await getNewDocument({
          targetDocument,
          linkElm,
          parser,
          brandName,
        });
        if (newDocument) {
          targetDocument = newDocument;
        }
      }
    }
    const selectorCondition = getSelectorConditions[brandName];
    setTitleText({
      targetDocument,
      selectorCondition,
      linkElm,
      brandName,
    });
    const titleTextElm = linkElm.nextElementSibling;
    const imagePath = selectorCondition.image(targetDocument);
    if (imagePath && titleTextElm) {
      const imageElm = await setImageElm({
        imagePath,
        titleTextElm,
      });
      if (imageElm instanceof HTMLImageElement) {
        imageElm.onload = () => scrollIfAutoScrollIsEnabled();
      }
    } else {
      const hasFailedElm = linkElm.nextElementSibling?.classList.contains('userjs-title');
      if (!hasFailedElm) {
        setFailedText(linkElm);
      }
    }
    removeLoading(linkElm);
  };
  const replaceDefaultURL = (targetElm) => {
    const linkElms = targetElm.querySelectorAll('a[href]');
    const replaceUrl = (url) => {
      const regex = /http:\/\/www\.dlsite\.com\/(.+?)\/dlaf\/=\/link\/work\/aid\/[a-zA-Z]+\/id\/(RJ[0-9]+)\.html/;
      const newUrlFormat = 'https://www.dlsite.com/$1/work/=/product_id/$2.html';
      return url.replace(regex, newUrlFormat);
    };
    const decodeHtmlEntities = (text) => {
      return text.replace(/&#(\d+);/g, (_, dec) => {
        return String.fromCharCode(dec);
      });
    };
    for (const linkElm of linkElms) {
      const brandName = getBrandName(linkElm.href);
      const href = linkElm.getAttribute('href');
      if (brandName === 'dlsite') {
        linkElm.href = decodeHtmlEntities(decodeURIComponent(replaceUrl(href.replace('/bin/jump.php?', ''))));
      } else {
        linkElm.href = decodeHtmlEntities(decodeURIComponent(href.replace('/bin/jump.php?', '')));
      }
    }
  };
  const searchLinkElements = async (targetElm) => {
    const linkElms = targetElm.querySelectorAll(WHITE_LIST_SELECTORS);
    if (!linkElms.length) return;
    const processBatch = async (batch) => {
      const promises = batch.map(async (linkElm) => {
        if (!linkElm.classList.contains('userjs-link')) return;
        await insertURLData(linkElm);
      });
      await Promise.all(promises);
    };
    for (const linkElm of linkElms) {
      setLoading(linkElm);
    }
    for (let i = 0; i < linkElms.length; i += 5) {
      const batch = Array.from(linkElms).slice(i, i + 5);
      await processBatch(batch);
    }
  };
  const mutationLinkElements = async (mutations) => {
    for (const mutation of mutations) {
      for (const addedNode of mutation.addedNodes) {
        if (!(addedNode instanceof HTMLElement)) continue;
        replaceDefaultURL(addedNode);
        searchLinkElements(addedNode);
      }
    }
  };
  const threadElm = document.querySelector('.thre');
  if (threadElm instanceof HTMLElement) {
    replaceDefaultURL(threadElm);
    searchLinkElements(threadElm);
    const observer = new MutationObserver(mutationLinkElements);
    observer.observe(threadElm, {
      childList: true,
    });
  }
})();