buyNow!

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

当前为 2023-04-22 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name buyNow!
  3. // @namespace http://2chan.net/
  4. // @version 0.2.2
  5. // @description ふたばちゃんねるのスレッド上で貼られたAmazonとDLsiteのURLからタイトルとあれば価格と画像を取得する
  6. // @author ame-chan
  7. // @match http://*.2chan.net/b/res/*
  8. // @match https://*.2chan.net/b/res/*
  9. // @match https://kako.futakuro.com/futa/*
  10. // @match https://tsumanne.net/si/data/*
  11. // @icon https://www.google.com/s2/favicons?sz=64&domain=2chan.net
  12. // @grant GM_xmlhttpRequest
  13. // @connect amazon.co.jp
  14. // @connect www.amazon.co.jp
  15. // @connect amzn.to
  16. // @connect amzn.asia
  17. // @connect dlsite.com
  18. // @license MIT
  19. // ==/UserScript==
  20. (function () {
  21. 'use strict';
  22. const WHITE_LIST_URLS = [
  23. 'https://amazon.co.jp/',
  24. 'https://www.amazon.co.jp/',
  25. 'https://amzn.to/',
  26. 'https://amzn.asia/',
  27. 'http://www.dlsite.com/',
  28. 'https://www.dlsite.com/',
  29. 'http://dlsite.com/',
  30. 'https://dlsite.com/',
  31. ];
  32. const isAmazon = (path) => ['amazon.co.jp', 'amzn.to', 'amzn.asia'].some((url) => path.includes(url));
  33. const isDLsite = (path) => path.includes('dlsite.com');
  34. const WHITE_LIST_SELECTORS = (() => WHITE_LIST_URLS.map((url) => `a[href^="${url}"]`).join(','))();
  35. const isProductPage = (url) =>
  36. /https:\/\/(www\.)?amazon\.co\.jp\/.*\/[A-Z0-9]{10}/.test(url) ||
  37. /https:\/\/amzn.(asia|to)\//.test(url) ||
  38. /https?:\/\/(www\.)?dlsite\.com\/.+?\/[A-Z0-9]{8,}\.html/.test(url);
  39. const getBrandName = (url) => {
  40. if (isAmazon(url)) {
  41. return 'amazon';
  42. } else if (isDLsite(url)) {
  43. return 'dlsite';
  44. }
  45. return '';
  46. };
  47. const addedStyle = `<style id="userjs-get-title-link">
  48. .userjs-title {
  49. display: block;
  50. margin: 8px 0 16px;
  51. padding: 8px 16px;
  52. line-height: 1.6 !important;
  53. color: #ff3860 !important;
  54. background-color: #fff;
  55. border-radius: 4px;
  56. }
  57. img {
  58. max-width: none;
  59. max-height: none;
  60. }
  61. .userjs-price {
  62. display: block;
  63. margin-top: 4px;
  64. color: #228b22 !important;
  65. font-weight: 700;
  66. }
  67. </style>`;
  68. if (!document.querySelector('#userjs-get-title-link')) {
  69. document.head.insertAdjacentHTML('beforeend', addedStyle);
  70. }
  71. class FileReaderEx extends FileReader {
  72. constructor() {
  73. super();
  74. }
  75. #readAs(blob, ctx) {
  76. return new Promise((res, rej) => {
  77. super.addEventListener('load', ({ target }) => target?.result && res(target.result));
  78. super.addEventListener('error', ({ target }) => target?.error && rej(target.error));
  79. super[ctx](blob);
  80. });
  81. }
  82. readAsArrayBuffer(blob) {
  83. return this.#readAs(blob, 'readAsArrayBuffer');
  84. }
  85. readAsDataURL(blob) {
  86. return this.#readAs(blob, 'readAsDataURL');
  87. }
  88. }
  89. const getHTMLData = (url) =>
  90. new Promise((resolve) => {
  91. GM_xmlhttpRequest({
  92. method: 'GET',
  93. url,
  94. timeout: 10000,
  95. onload: (result) => {
  96. if (result.status === 200) {
  97. return resolve(result.responseText);
  98. }
  99. return resolve(false);
  100. },
  101. onerror: (err) => err && resolve(false),
  102. ontimeout: () => resolve(false),
  103. });
  104. });
  105. const setFailedText = (linkElm) => {
  106. linkElm.insertAdjacentHTML('afterend', `<span class="userjs-title">データ取得失敗</span>`);
  107. };
  108. const setPriceText = ({ targetDocument, brandName }) => {
  109. if (brandName === '') return '';
  110. const targetElement = {
  111. amazon: () => {
  112. const priceRange = () => {
  113. const rangeElm = targetDocument.querySelector('.a-price-range');
  114. if (!rangeElm) return 0;
  115. rangeElm.querySelectorAll('.a-offscreen').forEach((el) => el.remove());
  116. return rangeElm.textContent?.replace(/[\s]+/g, '');
  117. };
  118. const price = targetDocument.querySelector('#twister-plus-price-data-price')?.value;
  119. return Number(price) || priceRange() || 0;
  120. },
  121. dlsite: () => {
  122. const url = targetDocument.querySelector('meta[property="og:url"]')?.content;
  123. const productId = url.split('/').pop()?.replace('.html', '');
  124. const priceElm = targetDocument.querySelector(`[data-product_id="${productId}"][data-price]`);
  125. return parseInt(priceElm?.getAttribute('data-price') || '', 10);
  126. },
  127. };
  128. const price = targetElement[brandName]();
  129. let priceText = price;
  130. if (!price) return;
  131. if (typeof price === 'number' && Number.isInteger(price) && price > 0) {
  132. priceText = new Intl.NumberFormat('ja-JP', {
  133. style: 'currency',
  134. currency: 'JPY',
  135. }).format(price);
  136. }
  137. return `<span class="userjs-price">${priceText}</span>`;
  138. };
  139. const setTitleText = ({ targetDocument, linkElm, brandName }) => {
  140. const titleElm = targetDocument.querySelector('title');
  141. if (!titleElm || !titleElm?.textContent) return;
  142. const priceText = setPriceText({
  143. targetDocument,
  144. brandName,
  145. });
  146. linkElm.insertAdjacentHTML('afterend', `<span class="userjs-title">${titleElm.textContent}${priceText}</span>`);
  147. };
  148. const setImageElm = async ({ targetDocument, titleTextElm, brandName }) => {
  149. if (brandName === '') return;
  150. const imageEventHandler = (e) => {
  151. const self = e.currentTarget;
  152. if (!(self instanceof HTMLImageElement)) return;
  153. if (self.width === 100) {
  154. self.width = 600;
  155. } else {
  156. self.width = 100;
  157. }
  158. };
  159. const targetElement = {
  160. amazon:
  161. targetDocument.querySelector('#landingImage')?.src ||
  162. targetDocument.querySelector('.unrolledScrollBox li:first-child img')?.src,
  163. dlsite: targetDocument.querySelector('meta[property="og:image"]')?.content,
  164. };
  165. const imagePath = targetElement[brandName];
  166. if (typeof imagePath !== 'string') return;
  167. const blob = await (await fetch(imagePath)).blob();
  168. const dataUrl = await new FileReaderEx().readAsDataURL(blob);
  169. const img = document.createElement('img');
  170. img.src = dataUrl;
  171. img.width = 100;
  172. img.classList.add('userjs-image');
  173. titleTextElm.insertAdjacentElement('afterend', img);
  174. img.addEventListener('click', imageEventHandler);
  175. };
  176. const setLoading = (linkElm) => {
  177. const loadingSVG =
  178. '<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>';
  179. const parentElm = linkElm.parentElement;
  180. if (
  181. parentElm instanceof HTMLFontElement ||
  182. !isProductPage(linkElm.href) ||
  183. parentElm?.querySelector('[data-id="userjs-loading"]')
  184. ) {
  185. return;
  186. }
  187. linkElm.insertAdjacentHTML('afterend', loadingSVG);
  188. };
  189. const removeLoading = (targetElm) => targetElm.parentElement?.querySelector('[data-id="userjs-loading"]')?.remove();
  190. const insertURLData = async (linkElm) => {
  191. const parentElm = linkElm.parentElement;
  192. if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
  193. return;
  194. }
  195. const htmlData = await getHTMLData(linkElm.href);
  196. if (!htmlData) {
  197. setFailedText(linkElm);
  198. removeLoading(linkElm);
  199. return;
  200. }
  201. const parser = new DOMParser();
  202. const targetDocument = parser.parseFromString(htmlData, 'text/html');
  203. const brandName = getBrandName(linkElm.href);
  204. setTitleText({
  205. targetDocument,
  206. linkElm,
  207. brandName,
  208. });
  209. const titleTextElm = parentElm?.querySelector('.userjs-title:last-of-type');
  210. if (titleTextElm) {
  211. await setImageElm({
  212. targetDocument,
  213. titleTextElm,
  214. brandName,
  215. });
  216. }
  217. removeLoading(linkElm);
  218. };
  219. const replaceDefaultURL = (targetElm) => {
  220. const linkElms = targetElm.querySelectorAll('a[href]');
  221. const replaceUrl = (url) => {
  222. const regex = /http:\/\/www\.dlsite\.com\/(.+?)\/dlaf\/=\/link\/work\/aid\/[a-zA-Z]+\/id\/(RJ[0-9]+)\.html/;
  223. const newUrlFormat = 'https://www.dlsite.com/$1/work/=/product_id/$2.html';
  224. return url.replace(regex, newUrlFormat);
  225. };
  226. for (const linkElm of linkElms) {
  227. const brandName = getBrandName(linkElm.href);
  228. const href = linkElm.getAttribute('href');
  229. if (brandName === 'dlsite') {
  230. linkElm.href = replaceUrl(href.replace('/bin/jump.php?', ''));
  231. } else {
  232. linkElm.href = href.replace('/bin/jump.php?', '');
  233. }
  234. }
  235. };
  236. const searchLinkElements = (targetElm) => {
  237. const linkElms = targetElm.querySelectorAll(WHITE_LIST_SELECTORS);
  238. if (!linkElms.length) return;
  239. for (const linkElm of linkElms) {
  240. if (!(linkElm instanceof HTMLElement)) continue;
  241. setLoading(linkElm);
  242. void insertURLData(linkElm);
  243. }
  244. };
  245. const mutationLinkElements = async (mutations) => {
  246. for (const mutation of mutations) {
  247. for (const addedNode of mutation.addedNodes) {
  248. if (!(addedNode instanceof HTMLElement)) continue;
  249. replaceDefaultURL(addedNode);
  250. searchLinkElements(addedNode);
  251. }
  252. }
  253. };
  254. const threadElm = document.querySelector('.thre');
  255. if (threadElm instanceof HTMLElement) {
  256. replaceDefaultURL(threadElm);
  257. searchLinkElements(threadElm);
  258. const observer = new MutationObserver(mutationLinkElements);
  259. observer.observe(threadElm, {
  260. childList: true,
  261. });
  262. }
  263. })();