buyNow!

ふたばちゃんねるのスレッド上で貼られたAmazonのURLからtitleとあれば画像を取得する

目前為 2023-04-22 提交的版本,檢視 最新版本

// ==UserScript==
// @name         buyNow!
// @namespace    http://2chan.net/
// @version      0.1.2
// @description  ふたばちゃんねるのスレッド上で貼られたAmazonのURLからtitleとあれば画像を取得する
// @author       ame-chan
// @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      https://amazon.co.jp/*
// @connect      https://www.amazon.co.jp/*
// @connect      https://amzn.to/*
// @connect      https://amzn.asia/*
// @license      MIT
// ==/UserScript==
(function () {
  'use strict';
  const WHITE_LIST_URLS = [
    'https://amazon.co.jp/',
    'https://www.amazon.co.jp/',
    'https://amzn.to/',
    'https://amzn.asia/',
  ];
  const isAmazon = (path) => ['amazon.co.jp', 'amzn.to', 'amzn.asia'].some((url) => path.includes(url));
  const WHITE_LIST_SELECTORS = (() => WHITE_LIST_URLS.map((url) => `a[href^="${url}"]`).join(','))();
  const isProductPage = (url) =>
    /https:\/\/(www\.)?amazon\.co\.jp\/.*(?:gp\/product|dp)\/[A-Z0-9]{10}/.test(url) ||
    /https:\/\/amzn.(asia|to)\//.test(url);
  const addedStyle = `<style id="userjs-get-title-link">
  .userjs-title {
    display: block;
    margin: 8px 0 16px;
    padding: 8px 16px;
    line-height: 1.6 !important;
    color: #dc143c !important;
    background-color: #fff;
    border-radius: 4px;
  }
  </style>`;
  if (!document.querySelector('#userjs-get-title-link')) {
    document.head.insertAdjacentHTML('beforeend', addedStyle);
  }
  class FileReaderEx extends FileReader {
    /**
     * FileReaderのPromiseラッパー
     * @example
     * ```js
     * const blob = await (await fetch(url)).blob();
     * const result = await new utils.FileReaderEx().readAsArrayBuffer(blob);
     * const buffer = new Uint8Array(result as ArrayBuffer);
     * buffer;
     * ```
     */ 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 getHTMLData = (url) =>
    new Promise((resolve) => {
      GM_xmlhttpRequest({
        method: 'GET',
        url,
        timeout: 10000,
        onload: (result) => {
          if (result.status === 200) {
            return resolve(result.responseText);
          }
          return resolve(false);
        },
        onerror: (err) => err && resolve(false),
        ontimeout: () => resolve(false),
      });
    });
  const setFailedText = (linkElm) => {
    linkElm.insertAdjacentHTML('afterend', `<span class="userjs-title">データ取得失敗</span>`);
  };
  const setTitleText = (targetDocument, linkElm) => {
    const titleElm = targetDocument.querySelector('title');
    if (!titleElm || !titleElm?.textContent) return;
    linkElm.insertAdjacentHTML('afterend', `<span class="userjs-title">${titleElm.textContent}</span>`);
  };
  const setImageElm = async (targetDocument, titleTextElm) => {
    const imageEventHandler = (e) => {
      const self = e.currentTarget;
      if (!(self instanceof HTMLImageElement)) return;
      if (self.width === 100) {
        self.width = 600;
      } else {
        self.width = 100;
      }
    };
    const imageElm =
      targetDocument.querySelector('#landingImage') ||
      targetDocument.querySelector('.unrolledScrollBox li:first-child img');
    if (!(imageElm instanceof HTMLImageElement)) return;
    const blob = await (await fetch(imageElm.src)).blob();
    const dataUrl = await new FileReaderEx().readAsDataURL(blob);
    const img = document.createElement('img');
    img.src = dataUrl;
    img.width = 100;
    img.classList.add('userjs-image');
    titleTextElm.insertAdjacentElement('afterend', img);
    img.addEventListener('click', imageEventHandler);
  };
  const setLoading = (linkElm) => {
    const loadingSVG =
      '<svg data-id="userjs-loading" width="16" height="16" viewBox="0 0 38 38" xmlns="http://www.w3.org/2000/svg" stroke="#000"><g fill="none" fill-rule="evenodd"><g transform="translate(1 1)" stroke-width="2"><circle stroke-opacity=".5" cx="18" cy="18" r="18"/><path d="M36 18c0-9.94-8.06-18-18-18"> <animateTransform attributeName="transform" type="rotate" from="0 18 18" to="360 18 18" dur="1s" repeatCount="indefinite"/></path></g></g></svg>';
    const parentElm = linkElm.parentElement;
    if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
      return;
    }
    linkElm.insertAdjacentHTML('afterend', loadingSVG);
  };
  const removeLoading = (targetElm) => targetElm.parentElement?.querySelector('[data-id="userjs-loading"]')?.remove();
  const insertURLData = async (linkElm) => {
    const parentElm = linkElm.parentElement;
    if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
      return;
    }
    const htmlData = await getHTMLData(linkElm.href);
    if (!htmlData) {
      setFailedText(linkElm);
      return;
    }
    const parser = new DOMParser();
    const targetDocument = parser.parseFromString(htmlData, 'text/html');
    setTitleText(targetDocument, linkElm);
    const titleTextElm = parentElm?.querySelector('.userjs-title');
    if (isAmazon(linkElm.href) && titleTextElm) {
      await setImageElm(targetDocument, titleTextElm);
    }
    removeLoading(linkElm);
  };
  const searchLinkElements = (targetElm) => {
    const linkElms = targetElm.querySelectorAll(WHITE_LIST_SELECTORS);
    if (!linkElms.length) return;
    for (const linkElm of linkElms) {
      if (!(linkElm instanceof HTMLElement)) continue;
      setLoading(linkElm);
      void insertURLData(linkElm);
    }
  };
  const mutationLinkElements = async (mutations) => {
    for (const mutation of mutations) {
      for (const addedNode of mutation.addedNodes) {
        if (!(addedNode instanceof HTMLElement)) continue;
        searchLinkElements(addedNode);
      }
    }
  };
  const threadElm = document.querySelector('.thre');
  if (threadElm) {
    searchLinkElements(threadElm);
    const observer = new MutationObserver(mutationLinkElements);
    observer.observe(threadElm, {
      childList: true,
    });
  }
})();