// ==UserScript== // @name buyNow! // @namespace http://2chan.net/ // @version 0.6.1 // @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 // @connect youtube.com // @connect youtu.be // @license MIT // @downloadURL none // ==/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', ]; 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 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) || /^https?:\/\/(youtu\.be\/|(www\.)?youtube.com\/watch\?v=)\w+/.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'; } 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 Math.round(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, }, // 画像のみ取得 youtube: { price: () => 0, image: (targetDocument) => targetDocument.querySelector('meta[property="og:image"]')?.content, }, }; const addedStyle = ``; 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', 'データ取得失敗'); }; 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 `${priceText}`; }; 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', `