buyNow!

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

  1. // ==UserScript==
  2. // @name buyNow!
  3. // @namespace http://2chan.net/
  4. // @version 0.8.14
  5. // @description ふたばちゃんねるのスレッド上で貼られた特定のECサイトの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 media-amazon.com
  18. // @connect m.media-amazon.com
  19. // @connect dlsite.com
  20. // @connect img.dlsite.jp
  21. // @connect bookwalker.jp
  22. // @connect c.bookwalker.jp
  23. // @connect store.steampowered.com
  24. // @connect cdn.akamai.steamstatic.com
  25. // @connect cdn.cloudflare.steamstatic.com
  26. // @connect store.cloudflare.steamstatic.com
  27. // @connect youtube.com
  28. // @connect youtu.be
  29. // @connect nintendo.com
  30. // @connect store-jp.nintendo.com
  31. // @connect dmm.co.jp
  32. // @connect www.dmm.co.jp
  33. // @connect dlsoft.dmm.co.jp
  34. // @connect pics.dmm.co.jp
  35. // @connect doujin-assets.dmm.co.jp
  36. // @license MIT
  37. // ==/UserScript==
  38. (function () {
  39. 'use strict';
  40. const WHITE_LIST_DOMAINS = [
  41. 'amazon.co.jp',
  42. 'amzn.to',
  43. 'amzn.asia',
  44. 'dlsite.com',
  45. 'bookwalker.jp',
  46. 'store.steampowered.com',
  47. 'youtube.com',
  48. 'youtu.be',
  49. 'store-jp.nintendo.com',
  50. 'dlsoft.dmm.co.jp',
  51. 'www.dmm.co.jp',
  52. ];
  53. const WHITE_LIST_SELECTORS = (() => WHITE_LIST_DOMAINS.map((domain) => `a[href*="${domain}"]`).join(','))();
  54. const convertHostname = (path) => new URL(path).hostname;
  55. const isAmazon = (path) => /^(www\.)?amazon.co.jp|amzn\.to|amzn\.asia$/.test(convertHostname(path));
  56. const isDLsite = (path) => /^(www\.)?dlsite\.com$/.test(convertHostname(path));
  57. const isBookwalker = (path) => /^(www\.)?bookwalker.jp$/.test(convertHostname(path));
  58. const isSteam = (path) => /^store\.steampowered\.com$/.test(convertHostname(path));
  59. const isYouTube = (path) => /^youtu\.be|(www\.)?youtube.com$/.test(convertHostname(path));
  60. const isNintendoStore = (path) => /^store-jp\.nintendo\.com$/.test(convertHostname(path));
  61. const isFanzaDoujin = (path) => /^(www\.)?dmm\.co\.jp$/.test(convertHostname(path));
  62. const isFanzaDlsoft = (path) => /^dlsoft\.dmm\.co\.jp$/.test(convertHostname(path));
  63. const isProductPage = (url) =>
  64. /^https?:\/\/(?:www\.)?amazon(.+?)\/(?:exec\/obidos\/ASIN|gp\/product|gp\/aw\/d|o\/ASIN|(?:.+?\/)?dp|d)\/[A-Z0-9]{10}/.test(
  65. url,
  66. ) ||
  67. /^https?:\/\/amzn.(asia|to)\//.test(url) ||
  68. /^https?:\/\/(www\.)?dlsite\.com\/.+?\/[A-Z0-9]{8,}(\.html)?/.test(url) ||
  69. /^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) ||
  70. /^https?:\/\/(www\.)?bookwalker\.jp\/(series|tag)\/[0-9]+\//.test(url) ||
  71. /^https?:\/\/store.steampowered.com\/(agecheck\/)?app\/\d+/.test(url) ||
  72. /^https?:\/\/(youtu\.be\/|((www|m)\.)?youtube.com\/(watch\?v=|live\/))\w+/.test(url) ||
  73. /^https?:\/\/store-jp\.nintendo\.com\/(list\/software\/(D)?[0-9]+.html|item\/software\/(D)?[0-9]+)/.test(url) ||
  74. /^https?:\/\/dlsoft\.dmm\.co\.jp\/(list|detail)\/.+?/.test(url) ||
  75. /^https?:\/\/(www\.)?dmm\.co\.jp\/dc\/doujin\/.+?/.test(url);
  76. const getBrandName = (url) => {
  77. if (isAmazon(url)) {
  78. return 'amazon';
  79. } else if (isDLsite(url)) {
  80. return 'dlsite';
  81. } else if (isBookwalker(url)) {
  82. return 'bookwalker';
  83. } else if (isSteam(url)) {
  84. return 'steam';
  85. } else if (isYouTube(url)) {
  86. return 'youtube';
  87. } else if (isNintendoStore(url)) {
  88. return 'nintendo';
  89. } else if (isFanzaDlsoft(url)) {
  90. return 'fanzaDlsoft';
  91. } else if (isFanzaDoujin(url)) {
  92. return 'fanzaDoujin';
  93. }
  94. return '';
  95. };
  96. const getOgpImage = (targetDocument) => targetDocument.querySelector('meta[property="og:image"]')?.content || '';
  97. const getSelectorConditions = {
  98. amazon: {
  99. price: (targetDocument) => {
  100. const priceRange = () => {
  101. const rangeElm = targetDocument.querySelector('.a-price-range');
  102. if (!rangeElm) return 0;
  103. rangeElm.querySelectorAll('.a-offscreen').forEach((el) => el.remove());
  104. return rangeElm.textContent?.replace(/[\s]+/g, '');
  105. };
  106. try {
  107. const price =
  108. targetDocument.querySelector('#twister-plus-price-data-price')?.value ||
  109. targetDocument.querySelector('#kindle-price')?.textContent?.replace(/[\s¥,]+/g, '') ||
  110. targetDocument.querySelector('[name="displayedPrice"]')?.value ||
  111. targetDocument.querySelector('.a-price-whole')?.textContent?.replace(/[\s¥,]+/g, '');
  112. return Math.round(Number(price)) || priceRange() || 0;
  113. } catch (e) {
  114. return 0;
  115. }
  116. },
  117. image: (targetDocument) =>
  118. targetDocument.querySelector('#landingImage')?.src ||
  119. targetDocument.querySelector('.unrolledScrollBox li:first-child img')?.src ||
  120. targetDocument.querySelector('[data-a-image-name]')?.src ||
  121. targetDocument.querySelector('#imgBlkFront')?.src,
  122. title: (targetDocument) =>
  123. targetDocument.querySelector('#productTitle')?.textContent ||
  124. targetDocument.querySelector('#title')?.textContent ||
  125. targetDocument.querySelector('#landingImage')?.alt ||
  126. targetDocument.querySelector('.unrolledScrollBox li:first-child img')?.alt ||
  127. targetDocument.querySelector('[data-a-image-name]')?.alt ||
  128. targetDocument.querySelector('#imgBlkFront')?.alt,
  129. },
  130. dlsite: {
  131. price: (targetDocument) => {
  132. try {
  133. const url = targetDocument.querySelector('meta[property="og:url"]')?.content;
  134. const productId = url.split('/').pop()?.replace('.html', '');
  135. const priceElm = targetDocument.querySelector(`[data-product_id="${productId}"][data-price]`);
  136. return parseInt(priceElm?.getAttribute('data-price') || '0', 10);
  137. } catch (e) {
  138. return 0;
  139. }
  140. },
  141. image: getOgpImage,
  142. },
  143. bookwalker: {
  144. price: (targetDocument) => {
  145. try {
  146. const price =
  147. Number(targetDocument.querySelector('.t-c-sales-price__value')?.textContent?.replace(/[^0-9]/g, '')) ||
  148. Number(
  149. targetDocument
  150. .querySelector('.m-tile-list .m-tile .m-book-item__price-num')
  151. ?.textContent?.replace(/,/g, ''),
  152. ) ||
  153. Number(targetDocument.querySelector('#jsprice')?.textContent?.replace(/[円,]/g, ''));
  154. return Number.isInteger(price) && price > 0 ? price : 0;
  155. } catch (e) {
  156. return 0;
  157. }
  158. },
  159. image: (targetDocument) =>
  160. targetDocument.querySelector('.m-tile-list .m-tile img')?.getAttribute('data-original') ||
  161. getOgpImage(targetDocument),
  162. },
  163. steam: {
  164. price: (targetDocument) => {
  165. try {
  166. const elm =
  167. targetDocument.querySelector('.game_area_purchase_game_wrapper .game_purchase_price.price') ||
  168. targetDocument.querySelector('.game_area_purchase_game .game_purchase_price.price') ||
  169. targetDocument.querySelector('.game_area_purchase_game_wrapper .discount_final_price');
  170. const price = elm?.firstChild?.textContent?.replace(/[¥,\s\t\r\n]+/g, '');
  171. const isComingSoon = targetDocument.querySelector('.game_area_comingsoon');
  172. const isAgeCheck = targetDocument.querySelector('#app_agegate');
  173. const num = Number(price);
  174. if (isAgeCheck) {
  175. return 'ログインか年齢確認が必要です';
  176. } else if (isComingSoon) {
  177. return '近日登場';
  178. } else if (Number.isInteger(num) && num > 0) {
  179. return num;
  180. } else if (typeof price === 'string') {
  181. return price;
  182. }
  183. return 0;
  184. } catch (e) {
  185. return 0;
  186. }
  187. },
  188. image: getOgpImage,
  189. },
  190. nintendo: {
  191. price: (targetDocument) => {
  192. try {
  193. const mobifyData = targetDocument.querySelector('#mobify-data');
  194. if (mobifyData instanceof HTMLScriptElement && mobifyData.textContent) {
  195. const data = JSON.parse(mobifyData.textContent.trim());
  196. const queries = data?.__PRELOADED_STATE__?.__reactQuery?.queries ?? [];
  197. for (const query of queries) {
  198. const price = query?.state?.data?.price ?? 0;
  199. if (price > 0) {
  200. return price;
  201. }
  202. }
  203. }
  204. const priceElm = targetDocument.querySelector('.js-productMainRenderedPrice > span:first-of-type');
  205. const priceText = priceElm?.textContent?.replace(/,/g, '');
  206. const price = Number(priceText);
  207. if (Number.isInteger(price) && price > 0) {
  208. return price;
  209. } else if (typeof priceText === 'string') {
  210. return priceText;
  211. }
  212. return 0;
  213. } catch (e) {
  214. return 0;
  215. }
  216. },
  217. image: getOgpImage,
  218. },
  219. fanzaDlsoft: {
  220. price: (targetDocument) => {
  221. try {
  222. const priceElm =
  223. targetDocument.querySelector('.tx-bskt-price') ||
  224. targetDocument.querySelector('.sellingPrice__discountedPrice');
  225. const priceText = priceElm?.textContent?.replace(/[,円]/g, '');
  226. const price = Number(priceText);
  227. if (Number.isInteger(price) && price > 0) {
  228. return price;
  229. } else if (typeof priceText === 'string') {
  230. return priceText;
  231. }
  232. return 0;
  233. } catch (e) {
  234. return 0;
  235. }
  236. },
  237. image: (targetDocument) => {
  238. return getOgpImage(targetDocument) || targetDocument.querySelector('.d-item #list .img img')?.src || '';
  239. },
  240. },
  241. fanzaDoujin: {
  242. price: (targetDocument) => {
  243. try {
  244. const priceText =
  245. targetDocument.querySelector('p.priceList__main')?.textContent?.replace(/[,円]/g, '') ||
  246. targetDocument.querySelector('.purchase__btn')?.getAttribute('data-price');
  247. const price = Number(priceText);
  248. if (Number.isInteger(price) && price > 0) {
  249. return price;
  250. } else if (typeof priceText === 'string') {
  251. return priceText;
  252. }
  253. return 0;
  254. } catch (e) {
  255. return 0;
  256. }
  257. },
  258. image: (targetDocument) => {
  259. return (
  260. getOgpImage(targetDocument) || targetDocument.querySelector('.productList .tileListImg__tmb img')?.src || ''
  261. );
  262. },
  263. },
  264. // 画像のみ取得
  265. youtube: {
  266. price: () => 0,
  267. image: getOgpImage,
  268. },
  269. };
  270. const addedStyle = `<style id="userjs-buyNow-style">
  271. .userjs-title {
  272. display: flex;
  273. flex-direction: row;
  274. margin: 8px 0 16px;
  275. gap: 16px;
  276. padding: 16px;
  277. line-height: 1.6 !important;
  278. color: #ff3860 !important;
  279. background-color: #fff;
  280. border-radius: 4px;
  281. }
  282. .userjs-title-inner {
  283. display: flex;
  284. flex-direction: column;
  285. gap: 8px;
  286. line-height: 1.6 !important;
  287. color: #ff3860 !important;
  288. }
  289. .userjs-link {
  290. padding-right: 24px;
  291. 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');
  292. background-repeat: no-repeat;
  293. background-position: right center;
  294. }
  295. .userjs-imageWrap {
  296. width: 150px;
  297. }
  298. .userjs-imageWrap.-center {
  299. text-align: center;
  300. }
  301. .userjs-imageWrap.-large {
  302. width: 600px;
  303. }
  304. .userjs-image {
  305. max-width: none !important;
  306. max-height: none !important;
  307. transition: all 0.2s ease-in-out;
  308. border-radius: 4px;
  309. }
  310. .userjs-price {
  311. display: block;
  312. color: #228b22 !important;
  313. font-weight: 700;
  314. }
  315. </style>`;
  316. if (!document.querySelector('#userjs-buyNow-style')) {
  317. document.head.insertAdjacentHTML('beforeend', addedStyle);
  318. }
  319. class FileReaderEx extends FileReader {
  320. constructor() {
  321. super();
  322. }
  323. #readAs(blob, ctx) {
  324. return new Promise((res, rej) => {
  325. super.addEventListener('load', ({ target }) => target?.result && res(target.result));
  326. super.addEventListener('error', ({ target }) => target?.error && rej(target.error));
  327. super[ctx](blob);
  328. });
  329. }
  330. readAsArrayBuffer(blob) {
  331. return this.#readAs(blob, 'readAsArrayBuffer');
  332. }
  333. readAsDataURL(blob) {
  334. return this.#readAs(blob, 'readAsDataURL');
  335. }
  336. }
  337. const fetchData = (url, responseType) =>
  338. new Promise((resolve) => {
  339. let options = {
  340. method: 'GET',
  341. url,
  342. timeout: 10000,
  343. onload: (result) => {
  344. if (result.status === 200 || result.status === 404) {
  345. return resolve(result.response);
  346. }
  347. return resolve(false);
  348. },
  349. onerror: () => resolve(false),
  350. ontimeout: () => resolve(false),
  351. };
  352. if (typeof responseType === 'string') {
  353. options = {
  354. ...options,
  355. responseType,
  356. };
  357. }
  358. GM_xmlhttpRequest(options);
  359. });
  360. const setFailedText = (linkElm) => {
  361. if (linkElm && linkElm instanceof HTMLAnchorElement) {
  362. linkElm.insertAdjacentHTML('afterend', '<span class="userjs-title">データ取得失敗</span>');
  363. }
  364. };
  365. const getPriceText = (price) => {
  366. let priceText = price;
  367. if (!price) return '';
  368. if (typeof price === 'number' && Number.isInteger(price) && price > 0) {
  369. priceText = new Intl.NumberFormat('ja-JP', {
  370. style: 'currency',
  371. currency: 'JPY',
  372. }).format(price);
  373. }
  374. return `<span class="userjs-price">${priceText}</span>`;
  375. };
  376. const setTitleText = ({ targetDocument, selectorCondition, linkElm, brandName }) => {
  377. let titleElm = targetDocument.querySelector('title');
  378. let title = titleElm?.textContent ?? '';
  379. // Amazonはtitleタグが無い場合がある
  380. if (title === '' && brandName === 'amazon') {
  381. title = selectorCondition.title(targetDocument);
  382. }
  383. if (!title) {
  384. setFailedText(linkElm);
  385. return false;
  386. }
  387. const price = selectorCondition.price(targetDocument);
  388. const priceText = getPriceText(price);
  389. const nextSibling = linkElm.nextElementSibling;
  390. if (nextSibling && nextSibling instanceof HTMLElement && nextSibling.tagName.toLowerCase() === 'br') {
  391. nextSibling.style.display = 'none';
  392. }
  393. if (title === 'サイトエラー') {
  394. const errorText = targetDocument.querySelector('#error_box')?.textContent;
  395. if (errorText) {
  396. title = errorText;
  397. }
  398. }
  399. if (linkElm && linkElm instanceof HTMLAnchorElement) {
  400. linkElm.insertAdjacentHTML(
  401. 'afterend',
  402. `<div class="userjs-title">
  403. <span class="userjs-title-inner">${title}${priceText}</span>
  404. </div>`,
  405. );
  406. return true;
  407. }
  408. return false;
  409. };
  410. const setImageElm = async ({ imagePath, titleTextElm }) => {
  411. const imageMinSize = 150;
  412. const imageMaxSize = 600;
  413. const imageEventHandler = (e) => {
  414. const self = e.currentTarget;
  415. const div = self?.parentElement;
  416. if (!(self instanceof HTMLImageElement) || !div) return;
  417. if (self.width === imageMinSize) {
  418. div.classList.remove('-center');
  419. div.classList.add('-large');
  420. self.width = imageMaxSize;
  421. } else {
  422. div.classList.remove('-large');
  423. if (self.naturalWidth > imageMinSize) {
  424. self.width = imageMinSize;
  425. } else {
  426. div.classList.add('-center');
  427. self.width = self.naturalWidth;
  428. }
  429. }
  430. };
  431. const blob = await fetchData(imagePath, 'blob');
  432. const titleInnerElm = titleTextElm.querySelector('.userjs-title-inner');
  433. if (!blob || !titleInnerElm) return false;
  434. const dataUrl = await new FileReaderEx().readAsDataURL(blob);
  435. const div = document.createElement('div');
  436. div.classList.add('userjs-imageWrap');
  437. const img = document.createElement('img');
  438. img.addEventListener('load', () => {
  439. if (img.naturalWidth < imageMinSize) {
  440. img.width = img.naturalWidth;
  441. }
  442. });
  443. img.src = dataUrl;
  444. img.width = imageMinSize;
  445. img.classList.add('userjs-image');
  446. div.appendChild(img);
  447. img.addEventListener('click', imageEventHandler);
  448. titleInnerElm.insertAdjacentElement('beforebegin', div);
  449. return img;
  450. };
  451. const setLoading = (linkElm) => {
  452. const parentElm = linkElm.parentElement;
  453. if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
  454. return;
  455. }
  456. linkElm.classList.add('userjs-link');
  457. };
  458. const removeLoading = (targetElm) => targetElm.classList.remove('userjs-link');
  459. // AgeCheck
  460. const isAgeCheck = (targetDocument, selector) => targetDocument.querySelector(selector) !== null;
  461. const getAgeCheckConfirmAdultPageHref = ({ targetDocument, selector, domain = '' }) => {
  462. const yesBtnLinkElm = targetDocument.querySelector(selector);
  463. if (yesBtnLinkElm instanceof HTMLAnchorElement) {
  464. return `${domain}${yesBtnLinkElm.getAttribute('href')}`;
  465. }
  466. return false;
  467. };
  468. const getAgeCheckPassedAdultDocument = async ({ targetDocument, linkElm, parser, selector, domain = '' }) => {
  469. const newHref = getAgeCheckConfirmAdultPageHref({
  470. targetDocument,
  471. selector,
  472. domain,
  473. });
  474. const htmlData = newHref && (await fetchData(newHref));
  475. if (!htmlData) {
  476. setFailedText(linkElm);
  477. removeLoading(linkElm);
  478. return false;
  479. }
  480. return parser.parseFromString(htmlData, 'text/html');
  481. };
  482. const getNewDocument = async ({ targetDocument, linkElm, parser, brandName }) => {
  483. const domain = brandName === 'amazon' ? 'https://www.amazon.co.jp' : '';
  484. const selector = (() => {
  485. switch (brandName) {
  486. case 'amazon':
  487. return '#black-curtain-yes-button a';
  488. case 'fanzaDlsoft':
  489. case 'fanzaDoujin':
  490. return '.ageCheck__link.ageCheck__link--r18';
  491. default:
  492. return false;
  493. }
  494. })();
  495. if (selector) {
  496. const newDocument = await getAgeCheckPassedAdultDocument({
  497. targetDocument,
  498. linkElm,
  499. parser,
  500. selector,
  501. domain,
  502. });
  503. if (newDocument) {
  504. return newDocument;
  505. }
  506. }
  507. return false;
  508. };
  509. // ふたクロで「新着レスに自動スクロール」にチェックが入っている場合画像差し込み後に下までスクロールさせる
  510. const scrollIfAutoScrollIsEnabled = () => {
  511. const checkboxElm = document.querySelector('#autolive_scroll');
  512. const readmoreElm = document.querySelector('#res_menu');
  513. if (checkboxElm === null || readmoreElm === null || !checkboxElm?.checked) {
  514. return;
  515. }
  516. const elementHeight = readmoreElm.offsetHeight;
  517. const viewportHeight = window.innerHeight;
  518. const offsetTop = readmoreElm.offsetTop;
  519. window.scrollTo({
  520. top: offsetTop - viewportHeight + elementHeight,
  521. behavior: 'smooth',
  522. });
  523. };
  524. // AmazonURLの正規化(amzn.toやamzn.asiaなど)
  525. const canonicalizeAmazonURL = (targetDocument, linkElm) => {
  526. const scriptElms = targetDocument.querySelectorAll('script');
  527. let asin = '';
  528. for (const scriptElm of scriptElms) {
  529. const text = scriptElm.textContent;
  530. if (text && text.includes('var opts')) {
  531. [, asin] = text.match(/asin:\s?\"(.+?)\"/) || [];
  532. break;
  533. }
  534. }
  535. if (asin && asin.length) {
  536. linkElm.href = `https://www.amazon.co.jp/dp/${asin}`;
  537. linkElm.textContent = `https://www.amazon.co.jp/dp/${asin}`;
  538. }
  539. };
  540. const insertURLData = async (linkElm) => {
  541. const parentElm = linkElm.parentElement;
  542. if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
  543. removeLoading(linkElm);
  544. return;
  545. }
  546. const brandName = getBrandName(linkElm.href);
  547. if (brandName === '') {
  548. setFailedText(linkElm);
  549. removeLoading(linkElm);
  550. return;
  551. }
  552. const htmlData = await fetchData(linkElm.href);
  553. if (!htmlData) {
  554. setFailedText(linkElm);
  555. removeLoading(linkElm);
  556. return;
  557. }
  558. const adultPageLists = ['amazon', 'fanzaDlsoft', 'fanzaDoujin'];
  559. const parser = new DOMParser();
  560. let targetDocument = parser.parseFromString(htmlData, 'text/html');
  561. // AmazonやFANZAのアダルトページ確認画面スキップ
  562. if (adultPageLists.includes(brandName)) {
  563. const is18xAmazon = isAgeCheck(targetDocument, '#black-curtain-warning');
  564. const is18xFanza = isAgeCheck(targetDocument, '.ageCheck');
  565. if (is18xAmazon || is18xFanza) {
  566. const newDocument = await getNewDocument({
  567. targetDocument,
  568. linkElm,
  569. parser,
  570. brandName,
  571. });
  572. if (newDocument) {
  573. targetDocument = newDocument;
  574. }
  575. }
  576. }
  577. const selectorCondition = getSelectorConditions[brandName];
  578. const isSuccessSetTitleText = setTitleText({
  579. targetDocument,
  580. selectorCondition,
  581. linkElm,
  582. brandName,
  583. });
  584. const titleTextElm = linkElm.nextElementSibling;
  585. const imagePath = selectorCondition.image(targetDocument);
  586. if (imagePath && titleTextElm) {
  587. const imageElm = await setImageElm({
  588. imagePath,
  589. titleTextElm,
  590. });
  591. if (imageElm instanceof HTMLImageElement) {
  592. imageElm.onload = () => scrollIfAutoScrollIsEnabled();
  593. }
  594. } else if (!imagePath && !isSuccessSetTitleText) {
  595. const failedElm = linkElm.nextElementSibling;
  596. const hasFailedElm = failedElm instanceof HTMLElement && failedElm.classList.contains('userjs-title');
  597. if (!hasFailedElm) {
  598. setFailedText(linkElm);
  599. }
  600. }
  601. if (brandName === 'amazon') {
  602. canonicalizeAmazonURL(targetDocument, linkElm);
  603. }
  604. removeLoading(linkElm);
  605. };
  606. const replaceDefaultURL = (targetElm) => {
  607. const linkElms = targetElm.querySelectorAll('a[href]');
  608. const replaceUrl = (url) => {
  609. const regex = /http:\/\/www\.dlsite\.com\/(.+?)\/dlaf\/=\/link\/work\/aid\/[a-zA-Z]+\/id\/(RJ[0-9]+)\.html/;
  610. const newUrlFormat = 'https://www.dlsite.com/$1/work/=/product_id/$2.html';
  611. return url.replace(regex, newUrlFormat);
  612. };
  613. const decodeHtmlEntities = (text) => {
  614. return text.replace(/&#(\d+);/g, (_, dec) => {
  615. return String.fromCharCode(dec);
  616. });
  617. };
  618. for (const linkElm of linkElms) {
  619. const brandName = getBrandName(linkElm.href);
  620. const href = linkElm.getAttribute('href');
  621. if (brandName === 'dlsite') {
  622. linkElm.href = decodeHtmlEntities(decodeURIComponent(replaceUrl(href.replace('/bin/jump.php?', ''))));
  623. } else {
  624. linkElm.href = decodeHtmlEntities(decodeURIComponent(href.replace('/bin/jump.php?', '')));
  625. }
  626. }
  627. };
  628. const processingQueue = [];
  629. let activeRequests = 0;
  630. const MAX_CONCURRENT_REQUESTS = 3;
  631. const processQueue = async () => {
  632. while (activeRequests < MAX_CONCURRENT_REQUESTS && processingQueue.length > 0) {
  633. const linkElm = processingQueue.shift();
  634. if (linkElm) {
  635. activeRequests++;
  636. insertURLData(linkElm).finally(() => {
  637. activeRequests--;
  638. processQueue();
  639. });
  640. }
  641. }
  642. };
  643. const observeLinkElements = (linkElms) => {
  644. const winH = window.innerHeight;
  645. const observerOptions = {
  646. root: null,
  647. // ビューポートの上下にビューポートの高さ分のマージンを持たせる
  648. rootMargin: `${winH}px 0px`,
  649. threshold: 0,
  650. };
  651. const observer = new IntersectionObserver(async (entries, observer) => {
  652. for (const entry of entries) {
  653. if (entry.isIntersecting) {
  654. const linkElm = entry.target;
  655. observer.unobserve(linkElm);
  656. // 見えるようになったリンクを処理キューに追加
  657. processingQueue.push(linkElm);
  658. processQueue();
  659. }
  660. }
  661. }, observerOptions);
  662. linkElms.forEach((linkElm) => observer.observe(linkElm));
  663. };
  664. const searchLinkElements = async (targetElm) => {
  665. const linkElms = targetElm.querySelectorAll(WHITE_LIST_SELECTORS);
  666. if (!linkElms.length) return;
  667. for (const linkElm of linkElms) {
  668. setLoading(linkElm);
  669. }
  670. observeLinkElements(Array.from(linkElms));
  671. };
  672. const mutationLinkElements = async (mutations) => {
  673. for (const mutation of mutations) {
  674. for (const addedNode of mutation.addedNodes) {
  675. if (!(addedNode instanceof HTMLElement)) continue;
  676. replaceDefaultURL(addedNode);
  677. searchLinkElements(addedNode);
  678. }
  679. }
  680. };
  681. const threadElm = document.querySelector('.thre');
  682. if (threadElm instanceof HTMLElement) {
  683. replaceDefaultURL(threadElm);
  684. searchLinkElements(threadElm);
  685. const observer = new MutationObserver(mutationLinkElements);
  686. observer.observe(threadElm, {
  687. childList: true,
  688. });
  689. }
  690. })();