buyNow!

ふたばちゃんねるのスレッド上で貼られたAmazonのURLからtitleとあれば画像を取得する

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

  1. // ==UserScript==
  2. // @name buyNow!
  3. // @namespace http://2chan.net/
  4. // @version 0.1.2
  5. // @description ふたばちゃんねるのスレッド上で貼られたAmazonのURLからtitleとあれば画像を取得する
  6. // @author ame-chan
  7. // @match https://*.2chan.net/b/res/*
  8. // @match https://kako.futakuro.com/futa/*
  9. // @match https://tsumanne.net/si/data/*
  10. // @icon https://www.google.com/s2/favicons?sz=64&domain=2chan.net
  11. // @grant GM_xmlhttpRequest
  12. // @connect https://amazon.co.jp/*
  13. // @connect https://www.amazon.co.jp/*
  14. // @connect https://amzn.to/*
  15. // @connect https://amzn.asia/*
  16. // @license MIT
  17. // ==/UserScript==
  18. (function () {
  19. 'use strict';
  20. const WHITE_LIST_URLS = [
  21. 'https://amazon.co.jp/',
  22. 'https://www.amazon.co.jp/',
  23. 'https://amzn.to/',
  24. 'https://amzn.asia/',
  25. ];
  26. const isAmazon = (path) => ['amazon.co.jp', 'amzn.to', 'amzn.asia'].some((url) => path.includes(url));
  27. const WHITE_LIST_SELECTORS = (() => WHITE_LIST_URLS.map((url) => `a[href^="${url}"]`).join(','))();
  28. const isProductPage = (url) =>
  29. /https:\/\/(www\.)?amazon\.co\.jp\/.*(?:gp\/product|dp)\/[A-Z0-9]{10}/.test(url) ||
  30. /https:\/\/amzn.(asia|to)\//.test(url);
  31. const addedStyle = `<style id="userjs-get-title-link">
  32. .userjs-title {
  33. display: block;
  34. margin: 8px 0 16px;
  35. padding: 8px 16px;
  36. line-height: 1.6 !important;
  37. color: #dc143c !important;
  38. background-color: #fff;
  39. border-radius: 4px;
  40. }
  41. </style>`;
  42. if (!document.querySelector('#userjs-get-title-link')) {
  43. document.head.insertAdjacentHTML('beforeend', addedStyle);
  44. }
  45. class FileReaderEx extends FileReader {
  46. /**
  47. * FileReaderのPromiseラッパー
  48. * @example
  49. * ```js
  50. * const blob = await (await fetch(url)).blob();
  51. * const result = await new utils.FileReaderEx().readAsArrayBuffer(blob);
  52. * const buffer = new Uint8Array(result as ArrayBuffer);
  53. * buffer;
  54. * ```
  55. */ constructor() {
  56. super();
  57. }
  58. #readAs(blob, ctx) {
  59. return new Promise((res, rej) => {
  60. super.addEventListener('load', ({ target }) => target?.result && res(target.result));
  61. super.addEventListener('error', ({ target }) => target?.error && rej(target.error));
  62. super[ctx](blob);
  63. });
  64. }
  65. readAsArrayBuffer(blob) {
  66. return this.#readAs(blob, 'readAsArrayBuffer');
  67. }
  68. readAsDataURL(blob) {
  69. return this.#readAs(blob, 'readAsDataURL');
  70. }
  71. }
  72. const getHTMLData = (url) =>
  73. new Promise((resolve) => {
  74. GM_xmlhttpRequest({
  75. method: 'GET',
  76. url,
  77. timeout: 10000,
  78. onload: (result) => {
  79. if (result.status === 200) {
  80. return resolve(result.responseText);
  81. }
  82. return resolve(false);
  83. },
  84. onerror: (err) => err && resolve(false),
  85. ontimeout: () => resolve(false),
  86. });
  87. });
  88. const setFailedText = (linkElm) => {
  89. linkElm.insertAdjacentHTML('afterend', `<span class="userjs-title">データ取得失敗</span>`);
  90. };
  91. const setTitleText = (targetDocument, linkElm) => {
  92. const titleElm = targetDocument.querySelector('title');
  93. if (!titleElm || !titleElm?.textContent) return;
  94. linkElm.insertAdjacentHTML('afterend', `<span class="userjs-title">${titleElm.textContent}</span>`);
  95. };
  96. const setImageElm = async (targetDocument, titleTextElm) => {
  97. const imageEventHandler = (e) => {
  98. const self = e.currentTarget;
  99. if (!(self instanceof HTMLImageElement)) return;
  100. if (self.width === 100) {
  101. self.width = 600;
  102. } else {
  103. self.width = 100;
  104. }
  105. };
  106. const imageElm =
  107. targetDocument.querySelector('#landingImage') ||
  108. targetDocument.querySelector('.unrolledScrollBox li:first-child img');
  109. if (!(imageElm instanceof HTMLImageElement)) return;
  110. const blob = await (await fetch(imageElm.src)).blob();
  111. const dataUrl = await new FileReaderEx().readAsDataURL(blob);
  112. const img = document.createElement('img');
  113. img.src = dataUrl;
  114. img.width = 100;
  115. img.classList.add('userjs-image');
  116. titleTextElm.insertAdjacentElement('afterend', img);
  117. img.addEventListener('click', imageEventHandler);
  118. };
  119. const setLoading = (linkElm) => {
  120. const loadingSVG =
  121. '<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>';
  122. const parentElm = linkElm.parentElement;
  123. if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
  124. return;
  125. }
  126. linkElm.insertAdjacentHTML('afterend', loadingSVG);
  127. };
  128. const removeLoading = (targetElm) => targetElm.parentElement?.querySelector('[data-id="userjs-loading"]')?.remove();
  129. const insertURLData = async (linkElm) => {
  130. const parentElm = linkElm.parentElement;
  131. if (parentElm instanceof HTMLFontElement || !isProductPage(linkElm.href)) {
  132. return;
  133. }
  134. const htmlData = await getHTMLData(linkElm.href);
  135. if (!htmlData) {
  136. setFailedText(linkElm);
  137. return;
  138. }
  139. const parser = new DOMParser();
  140. const targetDocument = parser.parseFromString(htmlData, 'text/html');
  141. setTitleText(targetDocument, linkElm);
  142. const titleTextElm = parentElm?.querySelector('.userjs-title');
  143. if (isAmazon(linkElm.href) && titleTextElm) {
  144. await setImageElm(targetDocument, titleTextElm);
  145. }
  146. removeLoading(linkElm);
  147. };
  148. const searchLinkElements = (targetElm) => {
  149. const linkElms = targetElm.querySelectorAll(WHITE_LIST_SELECTORS);
  150. if (!linkElms.length) return;
  151. for (const linkElm of linkElms) {
  152. if (!(linkElm instanceof HTMLElement)) continue;
  153. setLoading(linkElm);
  154. void insertURLData(linkElm);
  155. }
  156. };
  157. const mutationLinkElements = async (mutations) => {
  158. for (const mutation of mutations) {
  159. for (const addedNode of mutation.addedNodes) {
  160. if (!(addedNode instanceof HTMLElement)) continue;
  161. searchLinkElements(addedNode);
  162. }
  163. }
  164. };
  165. const threadElm = document.querySelector('.thre');
  166. if (threadElm) {
  167. searchLinkElements(threadElm);
  168. const observer = new MutationObserver(mutationLinkElements);
  169. observer.observe(threadElm, {
  170. childList: true,
  171. });
  172. }
  173. })();