// ==UserScript==
// @name buyNow!
// @namespace http://2chan.net/
// @version 0.2.3
// @description ふたばちゃんねるのスレッド上で貼られたAmazonとDLsiteの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 dlsite.com
// @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/',
'http://www.dlsite.com/',
'https://www.dlsite.com/',
'http://dlsite.com/',
'https://dlsite.com/',
];
const isAmazon = (path) => ['amazon.co.jp', 'amzn.to', 'amzn.asia'].some((url) => path.includes(url));
const isDLsite = (path) => path.includes('dlsite.com');
const WHITE_LIST_SELECTORS = (() => WHITE_LIST_URLS.map((url) => `a[href^="${url}"]`).join(','))();
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);
const getBrandName = (url) => {
if (isAmazon(url)) {
return 'amazon';
} else if (isDLsite(url)) {
return 'dlsite';
}
return '';
};
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: #ff3860 !important;
background-color: #fff;
border-radius: 4px;
}
img {
max-width: none;
max-height: none;
}
.userjs-price {
display: block;
margin-top: 4px;
color: #228b22 !important;
font-weight: 700;
}
</style>`;
if (!document.querySelector('#userjs-get-title-link')) {
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 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 setPriceText = ({ targetDocument, brandName }) => {
if (brandName === '') return '';
const targetElement = {
amazon: () => {
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('[name="displayedPrice"]')?.value;
return Number(price) || priceRange() || 0;
},
dlsite: () => {
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') || '', 10);
},
};
const price = targetElement[brandName]();
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, linkElm, brandName }) => {
const titleElm = targetDocument.querySelector('title');
if (!titleElm || !titleElm?.textContent) return;
const priceText = setPriceText({
targetDocument,
brandName,
});
linkElm.insertAdjacentHTML('afterend', `<span class="userjs-title">${titleElm.textContent}${priceText}</span>`);
};
const setImageElm = async ({ targetDocument, titleTextElm, brandName }) => {
if (brandName === '') return;
const imageEventHandler = (e) => {
const self = e.currentTarget;
if (!(self instanceof HTMLImageElement)) return;
if (self.width === 100) {
self.width = 600;
} else {
self.width = 100;
}
};
const targetElement = {
amazon:
targetDocument.querySelector('#landingImage')?.src ||
targetDocument.querySelector('.unrolledScrollBox li:first-child img')?.src ||
targetDocument.querySelector('[data-a-image-name]')?.src,
dlsite: targetDocument.querySelector('meta[property="og:image"]')?.content,
};
const imagePath = targetElement[brandName];
if (typeof imagePath !== 'string') return;
const blob = await (await fetch(imagePath)).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) ||
parentElm?.querySelector('[data-id="userjs-loading"]')
) {
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);
removeLoading(linkElm);
return;
}
const parser = new DOMParser();
const targetDocument = parser.parseFromString(htmlData, 'text/html');
const brandName = getBrandName(linkElm.href);
setTitleText({
targetDocument,
linkElm,
brandName,
});
const titleTextElm = parentElm?.querySelector('.userjs-title:last-of-type');
if (titleTextElm) {
await setImageElm({
targetDocument,
titleTextElm,
brandName,
});
}
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,
});
}
})();