buyNow!

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

当前为 2024-10-17 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name buyNow!
  3. // @namespace http://2chan.net/
  4. // @version 0.8.10
  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)\/[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/.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(
  148. targetDocument
  149. .querySelector('.m-tile-list .m-tile .m-book-item__price-num')
  150. ?.textContent?.replace(/,/g, ''),
  151. ) || Number(targetDocument.querySelector('#jsprice')?.textContent?.replace(/[円,]/g, ''));
  152. return Number.isInteger(price) && price > 0 ? price : 0;
  153. } catch (e) {
  154. return 0;
  155. }
  156. },
  157. image: (targetDocument) =>
  158. targetDocument.querySelector('.m-tile-list .m-tile img')?.getAttribute('data-original') ||
  159. getOgpImage(targetDocument),
  160. },
  161. steam: {
  162. price: (targetDocument) => {
  163. try {
  164. const elm =
  165. targetDocument.querySelector('.game_area_purchase_game_wrapper .game_purchase_price.price') ||
  166. targetDocument.querySelector('.game_area_purchase_game .game_purchase_price.price') ||
  167. targetDocument.querySelector('.game_area_purchase_game_wrapper .discount_final_price');
  168. const price = elm?.firstChild?.textContent?.replace(/[¥,\s\t\r\n]+/g, '');
  169. const isComingSoon = targetDocument.querySelector('.game_area_comingsoon');
  170. const isAgeCheck = targetDocument.querySelector('#app_agegate');
  171. const num = Number(price);
  172. if (isAgeCheck) {
  173. return 'ログインか年齢確認が必要です';
  174. } else if (isComingSoon) {
  175. return '近日登場';
  176. } else if (Number.isInteger(num) && num > 0) {
  177. return num;
  178. } else if (typeof price === 'string') {
  179. return price;
  180. }
  181. return 0;
  182. } catch (e) {
  183. return 0;
  184. }
  185. },
  186. image: getOgpImage,
  187. },
  188. nintendo: {
  189. price: (targetDocument) => {
  190. try {
  191. const priceElm = targetDocument.querySelector('.js-productMainRenderedPrice > span:first-of-type');
  192. const priceText = priceElm?.textContent?.replace(/,/g, '');
  193. const price = Number(priceText);
  194. if (Number.isInteger(price) && price > 0) {
  195. return price;
  196. } else if (typeof priceText === 'string') {
  197. return priceText;
  198. }
  199. return 0;
  200. } catch (e) {
  201. return 0;
  202. }
  203. },
  204. image: getOgpImage,
  205. },
  206. fanzaDlsoft: {
  207. price: (targetDocument) => {
  208. try {
  209. const priceElm =
  210. targetDocument.querySelector('.tx-bskt-price') ||
  211. targetDocument.querySelector('.sellingPrice__discountedPrice');
  212. const priceText = priceElm?.textContent?.replace(/[,円]/g, '');
  213. const price = Number(priceText);
  214. if (Number.isInteger(price) && price > 0) {
  215. return price;
  216. } else if (typeof priceText === 'string') {
  217. return priceText;
  218. }
  219. return 0;
  220. } catch (e) {
  221. return 0;
  222. }
  223. },
  224. image: (targetDocument) => {
  225. return getOgpImage(targetDocument) || targetDocument.querySelector('.d-item #list .img img')?.src || '';
  226. },
  227. },
  228. fanzaDoujin: {
  229. price: (targetDocument) => {
  230. try {
  231. const priceText =
  232. targetDocument.querySelector('p.priceList__main')?.textContent?.replace(/[,円]/g, '') ||
  233. targetDocument.querySelector('.purchase__btn')?.getAttribute('data-price');
  234. const price = Number(priceText);
  235. if (Number.isInteger(price) && price > 0) {
  236. return price;
  237. } else if (typeof priceText === 'string') {
  238. return priceText;
  239. }
  240. return 0;
  241. } catch (e) {
  242. return 0;
  243. }
  244. },
  245. image: (targetDocument) => {
  246. return (
  247. getOgpImage(targetDocument) || targetDocument.querySelector('.productList .tileListImg__tmb img')?.src || ''
  248. );
  249. },
  250. },
  251. // 画像のみ取得
  252. youtube: {
  253. price: () => 0,
  254. image: getOgpImage,
  255. },
  256. };
  257. const addedStyle = `<style id="userjs-buyNow-style">
  258. .userjs-title {
  259. display: flex;
  260. flex-direction: row;
  261. margin: 8px 0 16px;
  262. gap: 16px;
  263. padding: 16px;
  264. line-height: 1.6 !important;
  265. color: #ff3860 !important;
  266. background-color: #fff;
  267. border-radius: 4px;
  268. }
  269. .userjs-title-inner {
  270. display: flex;
  271. flex-direction: column;
  272. gap: 8px;
  273. line-height: 1.6 !important;
  274. color: #ff3860 !important;
  275. }
  276. .userjs-link {
  277. padding-right: 24px;
  278. 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');
  279. background-repeat: no-repeat;
  280. background-position: right center;
  281. }
  282. .userjs-imageWrap {
  283. width: 150px;
  284. }
  285. .userjs-imageWrap.-center {
  286. text-align: center;
  287. }
  288. .userjs-imageWrap.-large {
  289. width: 600px;
  290. }
  291. .userjs-image {
  292. max-width: none !important;
  293. max-height: none !important;
  294. transition: all 0.2s ease-in-out;
  295. border-radius: 4px;
  296. }
  297. .userjs-price {
  298. display: block;
  299. color: #228b22 !important;
  300. font-weight: 700;
  301. }
  302. </style>`;
  303. if (!document.querySelector('#userjs-buyNow-style')) {
  304. document.head.insertAdjacentHTML('beforeend', addedStyle);
  305. }
  306. class FileReaderEx extends FileReader {
  307. constructor() {
  308. super();
  309. }
  310. #readAs(blob, ctx) {
  311. return new Promise((res, rej) => {
  312. super.addEventListener('load', ({ target }) => target?.result && res(target.result));
  313. super.addEventListener('error', ({ target }) => target?.error && rej(target.error));
  314. super[ctx](blob);
  315. });
  316. }
  317. readAsArrayBuffer(blob) {
  318. return this.#readAs(blob, 'readAsArrayBuffer');
  319. }
  320. readAsDataURL(blob) {
  321. return this.#readAs(blob, 'readAsDataURL');
  322. }
  323. }
  324. const fetchData = (url, responseType) =>
  325. new Promise((resolve) => {
  326. let options = {
  327. method: 'GET',
  328. url,
  329. timeout: 10000,
  330. onload: (result) => {
  331. if (result.status === 200 || result.status === 404) {
  332. return resolve(result.response);
  333. }
  334. return resolve(false);
  335. },
  336. onerror: () => resolve(false),
  337. ontimeout: () => resolve(false),
  338. };
  339. if (typeof responseType === 'string') {
  340. options = {
  341. ...options,
  342. responseType,
  343. };
  344. }
  345. GM_xmlhttpRequest(options);
  346. });
  347. const setFailedText = (linkElm) => {
  348. if (linkElm && linkElm instanceof HTMLAnchorElement) {
  349. linkElm.insertAdjacentHTML('afterend', '<span class="userjs-title">データ取得失敗</span>');
  350. }
  351. };
  352. const getPriceText = (price) => {
  353. let priceText = price;
  354. if (!price) return '';
  355. if (typeof price === 'number' && Number.isInteger(price) && price > 0) {
  356. priceText = new Intl.NumberFormat('ja-JP', {
  357. style: 'currency',
  358. currency: 'JPY',
  359. }).format(price);
  360. }
  361. return `<span class="userjs-price">${priceText}</span>`;
  362. };
  363. const setTitleText = ({ targetDocument, selectorCondition, linkElm, brandName }) => {
  364. let titleElm = targetDocument.querySelector('title');
  365. let title = titleElm?.textContent ?? '';
  366. // Amazonはtitleタグが無い場合がある
  367. if (title === '' && brandName === 'amazon') {
  368. title = selectorCondition.title(targetDocument);
  369. }
  370. if (!title) {
  371. setFailedText(linkElm);
  372. return;
  373. }
  374. const price = selectorCondition.price(targetDocument);
  375. const priceText = getPriceText(price);
  376. const nextSibling = linkElm.nextElementSibling;
  377. if (nextSibling && nextSibling instanceof HTMLElement && nextSibling.tagName.toLowerCase() === 'br') {
  378. nextSibling.style.display = 'none';
  379. }
  380. if (title === 'サイトエラー') {
  381. const errorText = targetDocument.querySelector('#error_box')?.textContent;
  382. if (errorText) {
  383. title = errorText;
  384. }
  385. }
  386. if (linkElm && linkElm instanceof HTMLAnchorElement) {
  387. linkElm.insertAdjacentHTML(
  388. 'afterend',
  389. `<div class="userjs-title">
  390. <span class="userjs-title-inner">${title}${priceText}</span>
  391. </div>`,
  392. );
  393. }
  394. };
  395. const setImageElm = async ({ imagePath, titleTextElm }) => {
  396. const imageMinSize = 150;
  397. const imageMaxSize = 600;
  398. const imageEventHandler = (e) => {
  399. const self = e.currentTarget;
  400. const div = self?.parentElement;
  401. if (!(self instanceof HTMLImageElement) || !div) return;
  402. if (self.width === imageMinSize) {
  403. div.classList.remove('-center');
  404. div.classList.add('-large');
  405. self.width = imageMaxSize;
  406. } else {
  407. div.classList.remove('-large');
  408. if (self.naturalWidth > imageMinSize) {
  409. self.width = imageMinSize;
  410. } else {
  411. div.classList.add('-center');
  412. self.width = self.naturalWidth;
  413. }
  414. }
  415. };
  416. const blob = await fetchData(imagePath, 'blob');
  417. const titleInnerElm = titleTextElm.querySelector('.userjs-title-inner');
  418. if (!blob || !titleInnerElm) return false;
  419. const dataUrl = await new FileReaderEx().readAsDataURL(blob);
  420. const div = document.createElement('div');
  421. div.classList.add('userjs-imageWrap');
  422. const img = document.createElement('img');
  423. img.addEventListener('load', () => {
  424. if (img.naturalWidth < imageMinSize) {
  425. img.width = img.naturalWidth;
  426. }
  427. });
  428. img.src = dataUrl;
  429. img.width = imageMinSize;
  430. img.classList.add('userjs-image');
  431. div.appendChild(img);
  432. img.addEventListener('click', imageEventHandler);
  433. titleInnerElm.insertAdjacentElement('beforebegin', div);
  434. return img;
  435. };
  436. const setLoading = (linkElm) => {
  437. const parentElm = linkElm.parentElement;
  438. if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
  439. return;
  440. }
  441. linkElm.classList.add('userjs-link');
  442. };
  443. const removeLoading = (targetElm) => targetElm.classList.remove('userjs-link');
  444. // AgeCheck
  445. const isAgeCheck = (targetDocument, selector) => targetDocument.querySelector(selector) !== null;
  446. const getAgeCheckConfirmAdultPageHref = ({ targetDocument, selector, domain = '' }) => {
  447. const yesBtnLinkElm = targetDocument.querySelector(selector);
  448. if (yesBtnLinkElm instanceof HTMLAnchorElement) {
  449. return `${domain}${yesBtnLinkElm.getAttribute('href')}`;
  450. }
  451. return false;
  452. };
  453. const getAgeCheckPassedAdultDocument = async ({ targetDocument, linkElm, parser, selector, domain = '' }) => {
  454. const newHref = getAgeCheckConfirmAdultPageHref({
  455. targetDocument,
  456. selector,
  457. domain,
  458. });
  459. const htmlData = newHref && (await fetchData(newHref));
  460. if (!htmlData) {
  461. setFailedText(linkElm);
  462. removeLoading(linkElm);
  463. return false;
  464. }
  465. return parser.parseFromString(htmlData, 'text/html');
  466. };
  467. const getNewDocument = async ({ targetDocument, linkElm, parser, brandName }) => {
  468. const domain = brandName === 'amazon' ? 'https://www.amazon.co.jp' : '';
  469. const selector = (() => {
  470. switch (brandName) {
  471. case 'amazon':
  472. return '#black-curtain-yes-button a';
  473. case 'fanzaDlsoft':
  474. case 'fanzaDoujin':
  475. return '.ageCheck__link.ageCheck__link--r18';
  476. default:
  477. return false;
  478. }
  479. })();
  480. if (selector) {
  481. const newDocument = await getAgeCheckPassedAdultDocument({
  482. targetDocument,
  483. linkElm,
  484. parser,
  485. selector,
  486. domain,
  487. });
  488. if (newDocument) {
  489. return newDocument;
  490. }
  491. }
  492. return false;
  493. };
  494. // ふたクロで「新着レスに自動スクロール」にチェックが入っている場合画像差し込み後に下までスクロールさせる
  495. const scrollIfAutoScrollIsEnabled = () => {
  496. const checkboxElm = document.querySelector('#autolive_scroll');
  497. const readmoreElm = document.querySelector('#res_menu');
  498. if (checkboxElm === null || readmoreElm === null || !checkboxElm?.checked) {
  499. return;
  500. }
  501. const elementHeight = readmoreElm.offsetHeight;
  502. const viewportHeight = window.innerHeight;
  503. const offsetTop = readmoreElm.offsetTop;
  504. window.scrollTo({
  505. top: offsetTop - viewportHeight + elementHeight,
  506. behavior: 'smooth',
  507. });
  508. };
  509. // AmazonURLの正規化(amzn.toやamzn.asiaなど)
  510. const canonicalizeAmazonURL = (targetDocument, linkElm) => {
  511. const dataAttrAsinElm =
  512. targetDocument.querySelector('[data-csa-c-asin]:not([data-csa-c-asin=""])') ||
  513. targetDocument.querySelector('[data-asin]:not([data-asin=""])') ||
  514. targetDocument.querySelector('[data-defaultAsin]:not([data-defaultAsin=""])');
  515. const hiddenAsinElm = targetDocument.querySelector('#deliveryBlockSelectAsin');
  516. let asin = '';
  517. if (dataAttrAsinElm !== null) {
  518. asin =
  519. dataAttrAsinElm.getAttribute('data-csa-c-asin') ||
  520. dataAttrAsinElm.getAttribute('data-asin') ||
  521. dataAttrAsinElm.getAttribute('data-defaultAsin');
  522. } else if (hiddenAsinElm !== null) {
  523. asin = hiddenAsinElm.getAttribute('value');
  524. }
  525. if (asin !== null && asin.length) {
  526. linkElm.href = `https://www.amazon.co.jp/dp/${asin}`;
  527. linkElm.textContent = `https://www.amazon.co.jp/dp/${asin}`;
  528. }
  529. };
  530. const insertURLData = async (linkElm) => {
  531. const parentElm = linkElm.parentElement;
  532. if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
  533. removeLoading(linkElm);
  534. return;
  535. }
  536. const brandName = getBrandName(linkElm.href);
  537. if (brandName === '') {
  538. setFailedText(linkElm);
  539. removeLoading(linkElm);
  540. return;
  541. }
  542. const htmlData = await fetchData(linkElm.href);
  543. if (!htmlData) {
  544. setFailedText(linkElm);
  545. removeLoading(linkElm);
  546. return;
  547. }
  548. const adultPageLists = ['amazon', 'fanzaDlsoft', 'fanzaDoujin'];
  549. const parser = new DOMParser();
  550. let targetDocument = parser.parseFromString(htmlData, 'text/html');
  551. // AmazonやFANZAのアダルトページ確認画面スキップ
  552. if (adultPageLists.includes(brandName)) {
  553. const is18xAmazon = isAgeCheck(targetDocument, '#black-curtain-warning');
  554. const is18xFanza = isAgeCheck(targetDocument, '.ageCheck');
  555. if (is18xAmazon || is18xFanza) {
  556. const newDocument = await getNewDocument({
  557. targetDocument,
  558. linkElm,
  559. parser,
  560. brandName,
  561. });
  562. if (newDocument) {
  563. targetDocument = newDocument;
  564. }
  565. }
  566. }
  567. const selectorCondition = getSelectorConditions[brandName];
  568. setTitleText({
  569. targetDocument,
  570. selectorCondition,
  571. linkElm,
  572. brandName,
  573. });
  574. const titleTextElm = linkElm.nextElementSibling;
  575. const imagePath = selectorCondition.image(targetDocument);
  576. if (imagePath && titleTextElm) {
  577. const imageElm = await setImageElm({
  578. imagePath,
  579. titleTextElm,
  580. });
  581. if (imageElm instanceof HTMLImageElement) {
  582. imageElm.onload = () => scrollIfAutoScrollIsEnabled();
  583. }
  584. } else {
  585. const hasFailedElm = linkElm.nextElementSibling?.classList.contains('userjs-title');
  586. if (!hasFailedElm) {
  587. setFailedText(linkElm);
  588. }
  589. }
  590. if (brandName === 'amazon') {
  591. canonicalizeAmazonURL(targetDocument, linkElm);
  592. }
  593. removeLoading(linkElm);
  594. };
  595. const replaceDefaultURL = (targetElm) => {
  596. const linkElms = targetElm.querySelectorAll('a[href]');
  597. const replaceUrl = (url) => {
  598. const regex = /http:\/\/www\.dlsite\.com\/(.+?)\/dlaf\/=\/link\/work\/aid\/[a-zA-Z]+\/id\/(RJ[0-9]+)\.html/;
  599. const newUrlFormat = 'https://www.dlsite.com/$1/work/=/product_id/$2.html';
  600. return url.replace(regex, newUrlFormat);
  601. };
  602. const decodeHtmlEntities = (text) => {
  603. return text.replace(/&#(\d+);/g, (_, dec) => {
  604. return String.fromCharCode(dec);
  605. });
  606. };
  607. for (const linkElm of linkElms) {
  608. const brandName = getBrandName(linkElm.href);
  609. const href = linkElm.getAttribute('href');
  610. if (brandName === 'dlsite') {
  611. linkElm.href = decodeHtmlEntities(decodeURIComponent(replaceUrl(href.replace('/bin/jump.php?', ''))));
  612. } else {
  613. linkElm.href = decodeHtmlEntities(decodeURIComponent(href.replace('/bin/jump.php?', '')));
  614. }
  615. }
  616. };
  617. const searchLinkElements = async (targetElm) => {
  618. const linkElms = targetElm.querySelectorAll(WHITE_LIST_SELECTORS);
  619. if (!linkElms.length) return;
  620. const processBatch = async (batch) => {
  621. const promises = batch.map(async (linkElm) => {
  622. if (!linkElm.classList.contains('userjs-link')) return;
  623. await insertURLData(linkElm);
  624. });
  625. await Promise.all(promises);
  626. };
  627. for (const linkElm of linkElms) {
  628. setLoading(linkElm);
  629. }
  630. for (let i = 0; i < linkElms.length; i += 5) {
  631. const batch = Array.from(linkElms).slice(i, i + 5);
  632. await processBatch(batch);
  633. }
  634. };
  635. const mutationLinkElements = async (mutations) => {
  636. for (const mutation of mutations) {
  637. for (const addedNode of mutation.addedNodes) {
  638. if (!(addedNode instanceof HTMLElement)) continue;
  639. replaceDefaultURL(addedNode);
  640. searchLinkElements(addedNode);
  641. }
  642. }
  643. };
  644. const threadElm = document.querySelector('.thre');
  645. if (threadElm instanceof HTMLElement) {
  646. replaceDefaultURL(threadElm);
  647. searchLinkElements(threadElm);
  648. const observer = new MutationObserver(mutationLinkElements);
  649. observer.observe(threadElm, {
  650. childList: true,
  651. });
  652. }
  653. })();