buyNow!

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

Mint 2023.04.24.. Lásd a legutóbbi verzió

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         buyNow!
// @namespace    http://2chan.net/
// @version      0.4.5
// @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.cloudflare.steamstatic.com
// @connect      store.cloudflare.steamstatic.com
// @license      MIT
// ==/UserScript==
(function () {
  'use strict';
  const WHITE_LIST_DOMAINS = [
    'amazon.co.jp',
    'amzn.to',
    'amzn.asia',
    'dlsite.com',
    'bookwalker.jp',
    'store.steampowered.com',
  ];
  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 isProductPage = (url) =>
    /^https?:\/\/(www\.)?amazon\.co\.jp\/.*\/[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);
  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';
    }
    return '';
  };
  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, '');
        };
        const price =
          targetDocument.querySelector('#twister-plus-price-data-price')?.value ||
          targetDocument.querySelector('#kindle-price')?.textContent?.replace(/[\s¥,]+/g, '') ||
          targetDocument.querySelector('[name="displayedPrice"]')?.value;
        return Number(price) || priceRange() || 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,
    },
    dlsite: {
      price: (targetDocument) => {
        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);
      },
      image: (targetDocument) => targetDocument.querySelector('meta[property="og:image"]')?.content,
    },
    bookwalker: {
      price: (targetDocument) => {
        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;
      },
      image: (targetDocument) =>
        targetDocument.querySelector('.m-tile-list .m-tile img')?.getAttribute('data-original') ||
        targetDocument.querySelector('meta[property="og:image"]')?.content,
    },
    steam: {
      price: (targetDocument) => {
        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;
      },
      image: (targetDocument) => targetDocument.querySelector('meta[property="og:image"]')?.content,
    },
  };
  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 {
    width: 400px;
  }
  .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-image {
    max-width: none !important;
    max-height: none !important;
    transition: all 0.3s ease-in-out;
    border-radius: 4px;
  }
  .userjs-price {
    display: block;
    margin-top: 4px;
    color: #228b22 !important;
    font-weight: 700;
  }
  [data-id="userjs-loading"] {
    margin-left: 4px;
  }
  </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) {
            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) => {
    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 }) => {
    const titleElm = targetDocument.querySelector('title');
    if (!titleElm || !titleElm?.textContent) return;
    const price = selectorCondition.price(targetDocument);
    const priceText = getPriceText(price);
    const nextSibling = linkElm.nextElementSibling;
    let title = titleElm.textContent;
    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;
      }
    }
    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;
      if (!(self instanceof HTMLImageElement)) return;
      if (self.width === imageMinSize) {
        self.width = imageMaxSize;
      } else {
        self.width = imageMinSize;
      }
    };
    const blob = await fetchData(imagePath, 'blob');
    if (!blob) return;
    const dataUrl = await new FileReaderEx().readAsDataURL(blob);
    const div = document.createElement('div');
    div.classList.add('userjs-imageWrap');
    const img = document.createElement('img');
    img.src = dataUrl;
    img.width = imageMinSize;
    img.classList.add('userjs-image');
    div.appendChild(img);
    img.addEventListener('click', imageEventHandler);
    titleTextElm.querySelector('.userjs-title-inner')?.insertAdjacentElement('beforebegin', div);
  };
  const setLoading = (linkElm) => {
    const parentElm = linkElm.parentElement;
    if (
      parentElm instanceof HTMLFontElement ||
      !isProductPage(linkElm.href) ||
      parentElm?.querySelector('[data-id="userjs-loading"]')
    ) {
      return;
    }
    linkElm.classList.add('userjs-link');
  };
  const removeLoading = (targetElm) => targetElm.classList.remove('userjs-link');
  const isAmazonConfirmAdultPage = (targetDocument) => targetDocument.querySelector('#black-curtain-warning') !== null;
  const getAmazonConfirmAdultPageHref = (targetDocument) => {
    const yesBtnLinkElm = targetDocument.querySelector('#black-curtain-yes-button a');
    if (yesBtnLinkElm instanceof HTMLAnchorElement) {
      return `https://www.amazon.co.jp${yesBtnLinkElm.getAttribute('href')}`;
    }
    return false;
  };
  const getAmazonAdultDocument = async (targetDocument, linkElm, parser) => {
    const newHref = getAmazonConfirmAdultPageHref(targetDocument);
    const htmlData = newHref && (await fetchData(newHref));
    if (!htmlData) {
      setFailedText(linkElm);
      removeLoading(linkElm);
      return false;
    }
    return parser.parseFromString(htmlData, 'text/html');
  };
  const insertURLData = async (linkElm) => {
    const parentElm = linkElm.parentElement;
    if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
      removeLoading(linkElm);
      return;
    }
    const htmlData = await fetchData(linkElm.href);
    if (!htmlData) {
      setFailedText(linkElm);
      removeLoading(linkElm);
      return;
    }
    const parser = new DOMParser();
    let targetDocument = parser.parseFromString(htmlData, 'text/html');
    // アダルトページ確認画面スキップ
    if (isAmazonConfirmAdultPage(targetDocument)) {
      const amazonAdultDocument = await getAmazonAdultDocument(targetDocument, linkElm, parser);
      if (amazonAdultDocument) {
        targetDocument = amazonAdultDocument;
      }
    }
    const brandName = getBrandName(linkElm.href);
    if (brandName === '') {
      setFailedText(linkElm);
      removeLoading(linkElm);
      return;
    }
    const selectorCondition = getSelectorConditions[brandName];
    setTitleText({
      targetDocument,
      selectorCondition,
      linkElm,
    });
    const titleTextElm = linkElm.nextElementSibling;
    const imagePath = selectorCondition.image(targetDocument);
    if (imagePath && titleTextElm) {
      await setImageElm({
        imagePath,
        titleTextElm,
      });
    }
    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);
    };
    for (const linkElm of linkElms) {
      const brandName = getBrandName(linkElm.href);
      const href = linkElm.getAttribute('href');
      if (brandName === 'dlsite') {
        linkElm.href = replaceUrl(href.replace('/bin/jump.php?', ''));
      } else {
        linkElm.href = href.replace('/bin/jump.php?', '');
      }
    }
  };
  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;
        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,
    });
  }
})();