SoldBy - Reveal Sellers on Amazon

Shows name, country of origin and ratings for third party sellers on Amazon (and highlights Chinese sellers)

  1. // ==UserScript==
  2. // @name SoldBy - Reveal Sellers on Amazon
  3. // @name:de SoldBy - Verkäufer auf Amazon anzeigen
  4. // @name:fr SoldBy - Révéler les vendeurs sur Amazon
  5. // @name:es SoldBy - Revelar vendedores en Amazon
  6. // @name:it SoldBy - Rivela i venditori su Amazon
  7. // @description Shows name, country of origin and ratings for third party sellers on Amazon (and highlights Chinese sellers)
  8. // @description:de Zeigt Name, Herkunftsland und Bewertungen von Drittanbietern auf Amazon an (und hebt chinesische Anbieter hervor)
  9. // @description:fr Montre le nom, le pays d'origine et les évaluations des vendeurs tiers sur Amazon (et met en évidence les vendeurs chinois)
  10. // @description:es Muestra el nombre, el país de origen y las valoraciones de los vendedores de terceros en el Amazon (y destaca los vendedores chinos)
  11. // @description:it Mostra il nome, il paese di origine e le valutazioni per i venditori di terze parti su Amazon (e mette in evidenza i venditori cinesi)
  12. // @namespace https://github.com/tadwohlrapp
  13. // @author Tad Wohlrapp
  14. // @version 1.7.2
  15. // @license MIT
  16. // @homepageURL https://github.com/tadwohlrapp/soldby
  17. // @supportURL https://github.com/tadwohlrapp/soldby/issues
  18. // @icon https://github.com/tadwohlrapp/soldby/raw/main/userscript/img/icon.png
  19. // @match https://www.amazon.co.jp/*
  20. // @match https://www.amazon.co.uk/*
  21. // @match https://www.amazon.com/*
  22. // @match https://www.amazon.com.be/*
  23. // @match https://www.amazon.com.mx/*
  24. // @match https://www.amazon.com.tr/*
  25. // @match https://www.amazon.de/*
  26. // @match https://www.amazon.es/*
  27. // @match https://www.amazon.fr/*
  28. // @match https://www.amazon.it/*
  29. // @match https://www.amazon.nl/*
  30. // @match https://www.amazon.se/*
  31. // @require https://openuserjs.org/src/libs/sizzle/GM_config.min.js
  32. // @grant GM.getValue
  33. // @grant GM.setValue
  34. // @compatible firefox Tested on Firefox v119 with Violentmonkey v2.16.0, Tampermonkey v4.19.0 and Greasemonkey v4.11
  35. // @compatible chrome Tested on Chrome v119 with Violentmonkey v2.16.0 and Tampermonkey v4.19.0
  36. // ==/UserScript==
  37.  
  38. (function () {
  39. 'use strict';
  40.  
  41. function accessLocalStorageItems(type, deleteItems = false) {
  42. const entries = Object.keys(localStorage).filter(storageItem => storageItem.startsWith(type.toLowerCase() + '-'))
  43. if (!deleteItems) return entries.length
  44. const approval = confirm(`Do you really want to delete all ${entries.length} ${type}s from your browser\'s local storage? This action cannot be undone.`)
  45. if (!approval) return null
  46. entries.forEach(storageItem => {
  47. localStorage.removeItem(storageItem)
  48. })
  49. updateLocalStorageItemCount(type)
  50. alert(`Done! ${entries.length} ${type} entries were deleted`)
  51. }
  52.  
  53. function updateLocalStorageItemCount(type) {
  54. GM_config.fields[`local-storage-clear-${type.toLowerCase()}`].node.value = `Delete ${accessLocalStorageItems(type)} ${type}s from local storage`
  55. }
  56.  
  57. const frame = document.createElement('div');
  58. frame.classList.add('sb-options');
  59.  
  60. const backdrop = document.createElement('div');
  61. backdrop.classList.add('sb-options--backdrop');
  62.  
  63. document.body.appendChild(frame);
  64. document.body.appendChild(backdrop);
  65.  
  66. GM_config.init({
  67. 'id': 'sb-settings',
  68. 'title': 'SoldBy Settings',
  69. 'fields': {
  70. 'countries': {
  71. 'section': ['Countries to highlight',
  72. 'Country codes as per ISO 3166-1 alpha-2, separated by a comma or space.'],
  73. 'label': 'List of country codes',
  74. 'type': 'text',
  75. 'default': 'CN, HK'
  76. },
  77. 'unknown': {
  78. 'section': ['Highlight undetectable countries',
  79. 'Some sellers have incomplete/invalid profiles with their country of origin missing.'],
  80. 'label': 'Highlight products sold from unknown countries',
  81. 'type': 'checkbox',
  82. 'default': true
  83. },
  84. 'hide': {
  85. 'section': ['Hide products instead of highlighting them',
  86. 'If you get overwhelmed by the sheer mass of highlighted product listings, maybe you want to hide them completely.'],
  87. 'label': 'Yes, hide all highlighted products',
  88. 'type': 'checkbox',
  89. 'default': false
  90. },
  91. 'max-asin-age': {
  92. 'section': ['Check every x days if the seller for a product has changed',
  93. 'For better performance, SoldBy caches product-seller relationships in your browser\'s local storage for one day (24 hours). You can change that value here.'],
  94. 'label': 'Number of days after which a product ASIN should be re-fetched',
  95. 'type': 'int',
  96. 'default': 1
  97. },
  98. 'max-seller-age': {
  99. 'section': ['Check every x days if a seller has new ratings (or has moved countries)',
  100. 'For better performance, SoldBy caches a seller\'s ratings and country in your browser\'s local storage for one week (7 days). You can change that value here.'],
  101. 'label': 'Number of days after which a seller\'s info should be re-fetched',
  102. 'type': 'int',
  103. 'default': 7
  104. },
  105. 'local-storage-clear-asin': {
  106. 'section': ['Clear SoldBy data from my browser\'s local storage',
  107. 'If you feel your browser\'s local storage might be stuffed with too much of SoldBy\'s data, you can delete the items here.'],
  108. 'label': ' ',
  109. 'type': 'button',
  110. 'size': 25,
  111. 'click': function () { accessLocalStorageItems('ASIN', true) }
  112. },
  113. 'local-storage-clear-seller': {
  114. 'label': ' ',
  115. 'type': 'button',
  116. 'value': 'test',
  117. 'size': 25,
  118. 'click': function () { accessLocalStorageItems('Seller', true) }
  119. },
  120. },
  121. 'events': {
  122. 'init': onInit,
  123. 'open': () => {
  124. GM_config.frame.removeAttribute('style');
  125. backdrop.style.display = 'block';
  126.  
  127. const buttons = frame.querySelectorAll('button');
  128. wrapBtn(buttons[0], true);
  129. wrapBtn(buttons[1]);
  130.  
  131. backdrop.addEventListener('click', () => {
  132. GM_config.close();
  133. });
  134. document.onkeydown = function (evt) {
  135. evt = evt || window.event;
  136. var isEscape = false;
  137. if ("key" in evt) isEscape = (evt.key === "Escape" || evt.key === "Esc");
  138. if (isEscape) GM_config.close();
  139. };
  140. updateLocalStorageItemCount('ASIN')
  141. updateLocalStorageItemCount('Seller')
  142. },
  143. 'save': () => {
  144. GM_config.close();
  145. },
  146. 'close': () => {
  147. backdrop.removeAttribute('style');
  148. },
  149. },
  150. 'frame': frame
  151. });
  152.  
  153. function onInit() {
  154. GM_config.css.basic = '';
  155.  
  156. // Link to Settings in Footer:
  157. try {
  158. const settingsLink = document.createElement('button');
  159. const navFooterCopyright = document.querySelector('.navFooterCopyright');
  160. navFooterCopyright.appendChild(settingsLink);
  161. settingsLink.addEventListener('click', () => { GM_config.open(); });
  162. settingsLink.textContent = '⚙️ SoldBy';
  163. wrapBtn(settingsLink, false, true);
  164. } catch {
  165. console.error('Could not add settings link');
  166. }
  167.  
  168. const countriesArr = GM_config.get('countries').split(/(?:,| )+/);
  169. if (GM_config.get('unknown')) countriesArr.push('?');
  170.  
  171. const options = {
  172. maxAgeAsinFetch: GM_config.get('max-asin-age'),
  173. maxAgeSellerFetch: GM_config.get('max-seller-age'),
  174. highlightedCountries: countriesArr,
  175. hideHighlightedProducts: GM_config.get('hide')
  176. };
  177.  
  178. function showSellerCountry() {
  179.  
  180. // Gets the ASIN for every visible product and sets it as "data-asin" attribute
  181. getAsin();
  182.  
  183. // Identify products by looking for "data-asin" attribute
  184. const productsWithAsinSelectors = [
  185. 'div[data-asin]',
  186. 'not([data-asin=""])',
  187. 'not([data-seller-name])',
  188. 'not([data-uuid*=s-searchgrid-carousel])',
  189. 'not([role="img"])',
  190. 'not(#averageCustomerReviews)',
  191. 'not(#detailBullets_averageCustomerReviews)',
  192. 'not(.inline-twister-swatch)',
  193. 'not(.contributorNameID)',
  194. 'not(.a-hidden)',
  195. 'not(.rpi-learn-more-card-content)',
  196. 'not(#reviews-image-gallery-container)',
  197. 'not([class*=_cross-border-widget_style_preload-widget])',
  198. 'not([data-video-url])'
  199. ];
  200. const products = document.querySelectorAll(productsWithAsinSelectors.join(':'));
  201.  
  202. // If no new products are found, return.
  203. if (products.length == 0) return;
  204.  
  205. products.forEach((product) => {
  206.  
  207. // Give each product the data-seller-name attribute to prevent re-capturing.
  208. product.dataset.sellerName = 'loading...';
  209.  
  210. createInfoBox(product);
  211.  
  212. if (localStorage.getItem(asinKey(product))) {
  213. getSellerIdAndNameFromLocalStorage(product);
  214. } else {
  215. getSellerIdAndNameFromProductPage(product);
  216. }
  217. });
  218. }
  219.  
  220. // Run script once on document ready
  221. showSellerCountry();
  222.  
  223. // Initialize new MutationObserver
  224. const mutationObserver = new MutationObserver(showSellerCountry);
  225.  
  226. // Let MutationObserver target the grid containing all thumbnails
  227. const targetNode = document.body;
  228.  
  229. const mutationObserverOptions = {
  230. childList: true,
  231. subtree: true
  232. }
  233.  
  234. // Run MutationObserver
  235. mutationObserver.observe(targetNode, mutationObserverOptions);
  236.  
  237. function parse(html) {
  238. const parser = new DOMParser();
  239. return parser.parseFromString(html, 'text/html');
  240. }
  241.  
  242. function getAsin() {
  243.  
  244. // Check current page for products (without "data-asin" attribute)
  245. const productSelectors = [
  246. '.a-carousel-card > div:not([data-asin])',
  247. '.octopus-pc-item:not([data-asin])',
  248. 'li[class*=ProductGridItem__]:not([data-asin])',
  249. 'div[class*=_octopus-search-result-card_style_apbSearchResultItem]:not([data-asin])',
  250. '.sbv-product:not([data-asin])',
  251. '.a-cardui #gridItemRoot:not([data-asin])'
  252. ];
  253. const products = document.querySelectorAll(String(productSelectors));
  254.  
  255. // If no new products are found, return.
  256. if (products.length == 0) return;
  257.  
  258. products.forEach((product) => {
  259.  
  260. // Take the first link but not if it's inside the "Bestseller" container (links to bestsellers page instead of product page) and not if it has the popover-trigger class, as its href is just "javascript:void(0)" (hidden feedback form on sponsored products)
  261. const link = product.querySelector('a:not(.s-grid-status-badge-container > a):not(.a-popover-trigger)');
  262.  
  263. // If link cannot be found, return
  264. if (!link) return;
  265.  
  266. link.href = decodeURIComponent(link.href);
  267. let asin = '';
  268. const searchParams = new URLSearchParams(link.href);
  269. if (searchParams.get('pd_rd_i')) {
  270. asin = searchParams.get('pd_rd_i')
  271. } else if (/\/dp\/(.*?)($|\?|\/)/.test(link.href)) {
  272. asin = link.href.match(/\/dp\/(.*?)($|\?|\/)/)[1]
  273. }
  274. product.dataset.asin = asin;
  275. });
  276. }
  277.  
  278. function getSellerIdAndNameFromLocalStorage(product) {
  279. const { sid: sellerId, sn: sellerName, ts: timeStamp } = JSON.parse(localStorage.getItem(asinKey(product)));
  280.  
  281. validateItemAge(product, timeStamp, 'asin');
  282.  
  283. if (sellerId) product.dataset.sellerId = sellerId;
  284. product.dataset.sellerName = sellerName;
  285. setSellerDetails(product);
  286. }
  287.  
  288. function getSellerIdAndNameFromProductPage(product, refetch = false) {
  289. // fetch seller, get data, save in local storage, set attributes
  290.  
  291. if (!product.dataset.asin) return;
  292.  
  293. if (refetch) console.log('Re-fetching ' + asinKey(product) + ' from product page');
  294.  
  295. const link = window.location.origin + '/dp/' + product.dataset.asin + '?psc=1';
  296.  
  297. fetch(link).then(function (response) {
  298. if (response.ok) {
  299. return response.text();
  300. }
  301. }).then(function (html) {
  302. const productPage = parse(html);
  303.  
  304. let sellerId, sellerName;
  305.  
  306. // weed out various special product pages:
  307. const specialPageSelectors = [
  308. '#gc-detail-page', /* gift card sold by amazon */
  309. '.reload_gc_balance', /* reload amazon balance */
  310. '#dp.digitaltextfeeds, #dp.magazine, #dp.ebooks, #dp.audible', /* magazines, subscriptions, audible, etc */
  311. '.av-page-desktop, .avu-retail-page' /* prime video */
  312. ];
  313.  
  314. if (productPage.querySelector(String(specialPageSelectors))) {
  315. sellerName = 'Amazon';
  316. } else {
  317. // find third party seller mention on product page
  318. const thirdPartySellerSelectors = [
  319. '#desktop_qualifiedBuyBox :not(#usedAccordionRow) #sellerProfileTriggerId',
  320. '#desktop_qualifiedBuyBox :not(#usedAccordionRow) #merchant-info a:first-of-type',
  321. '#exports_desktop_qualifiedBuybox :not(#usedAccordionRow) #sellerProfileTriggerId',
  322. '#exports_desktop_qualifiedBuybox :not(#usedAccordionRow) #merchant-info a:first-of-type',
  323. '#newAccordionRow #sellerProfileTriggerId',
  324. '#newAccordionRow #merchant-info a:first-of-type'
  325. ]
  326.  
  327. const thirdPartySeller = productPage.querySelector(String(thirdPartySellerSelectors));
  328.  
  329. if (thirdPartySeller) {
  330.  
  331. // Get seller ID
  332. const searchParams = new URLSearchParams(thirdPartySeller.href);
  333. sellerId = searchParams.get('seller');
  334. const sellerUrl = window.location.origin + '/sp?seller=' + sellerId;
  335.  
  336. // Get seller Name
  337. sellerName = thirdPartySeller.textContent.trim().replaceAll('"', '“');
  338. } else {
  339.  
  340. let queryMerchantName = ' ';
  341. if (productPage.querySelector('#tabular-buybox .tabular-buybox-text')) {
  342. queryMerchantName = productPage.querySelector('#tabular-buybox .tabular-buybox-container > .tabular-buybox-text:last-of-type').textContent.trim();
  343. } else if (productPage.querySelector('#merchant-info')) {
  344. queryMerchantName = productPage.querySelector('#merchant-info').textContent.trim();
  345. } else if (productPage.querySelector('[offer-display-feature-name="desktop-merchant-info"]')) {
  346. queryMerchantName = productPage.querySelector('[offer-display-feature-name="desktop-merchant-info"]').textContent.trim();
  347. }
  348.  
  349. if (queryMerchantName.replace(/\s/g, '').length) {
  350. sellerName = 'Amazon';
  351. } else {
  352. sellerName = '? ? ?';
  353. }
  354. }
  355. }
  356.  
  357. // Set data-seller-name attribute
  358. product.dataset.sellerName = sellerName;
  359.  
  360. if (sellerId) {
  361. // If seller is known: set ASIN with corresponding seller in local storage
  362. localStorage.setItem(asinKey(product), `{"sid":"${sellerId}","sn":"${sellerName}","ts":"${Date.now()}"}`);
  363. // Set data-seller-id attribute
  364. product.dataset.sellerId = sellerId;
  365. }
  366.  
  367. if (sellerName == 'Amazon') {
  368. localStorage.setItem(asinKey(product), `{"sn":"${sellerName}","ts":"${Date.now()}"}`);
  369. }
  370.  
  371. setSellerDetails(product);
  372.  
  373. }).catch(function (err) {
  374. console.warn('Something went wrong fetching ' + link, err);
  375. });
  376. }
  377.  
  378. function setSellerDetails(product) {
  379. if (product.dataset.sellerName.includes('Amazon') || product.dataset.sellerName == '? ? ?') {
  380. populateInfoBox(product);
  381. return; // if seller is Amazon or unknown, no further steps are needed
  382. }
  383.  
  384. if (localStorage.getItem(sellerKey(product))) {
  385. getSellerCountryAndRatingfromLocalStorage(product);
  386. } else {
  387. getSellerCountryAndRatingfromSellerPage(product);
  388. }
  389. }
  390.  
  391. function getSellerCountryAndRatingfromLocalStorage(product) {
  392.  
  393. // seller key found in local storage
  394. const { c: country, rs: ratingScore, rc: ratingCount, ts: timeStamp } = JSON.parse(localStorage.getItem(sellerKey(product)));
  395.  
  396. validateItemAge(product, timeStamp, 'seller');
  397.  
  398. product.dataset.sellerCountry = country;
  399. product.dataset.sellerRatingScore = ratingScore;
  400. product.dataset.sellerRatingCount = ratingCount;
  401.  
  402. highlightProduct(product);
  403. populateInfoBox(product);
  404. }
  405.  
  406. function getSellerCountryAndRatingfromSellerPage(product, refetch = false) {
  407. // seller key not found in local storage. fetch seller details from seller-page
  408.  
  409. if (refetch) console.log('Re-fetching ' + sellerKey(product) + ' from product page');
  410.  
  411. // build seller link
  412. const link = window.location.origin + '/sp?seller=' + product.dataset.sellerId;
  413.  
  414. fetch(link).then(function (response) {
  415. if (response.ok) {
  416. return response.text();
  417. } else if (response.status === 503) {
  418. product.dataset.blocked = true;
  419. populateInfoBox(product);
  420. throw new Error('🙄 Too many requests. Amazon blocked seller page. Please try again in a few minutes.');
  421. } else {
  422. throw new Error(response.status);
  423. }
  424. }).then(function (html) {
  425.  
  426. let seller = getSellerDetailsFromSellerPage(parse(html));
  427. // --> seller.country (e.g. 'US')
  428. // --> seller.rating.score (e.g. '69%')
  429. // --> seller.rating.count (e.g. '420')
  430.  
  431. // Set attributes: data-seller-country, data-seller-rating-score and data-seller-rating-count
  432. product.dataset.sellerCountry = seller.country;
  433. product.dataset.sellerRatingScore = seller.rating.score;
  434. product.dataset.sellerRatingCount = seller.rating.count;
  435.  
  436. // Write to local storage
  437. localStorage.setItem(sellerKey(product), `{"c":"${seller.country}","rs":"${seller.rating.score}","rc":"${seller.rating.count}","ts":"${Date.now()}"}`);
  438.  
  439. highlightProduct(product);
  440. populateInfoBox(product);
  441.  
  442. }).catch(function (err) {
  443. console.warn('Could not fetch seller data for "' + product.dataset.sellerName + '" (' + link + '):', err);
  444. });
  445. }
  446.  
  447. function getSellerDetailsFromSellerPage(sellerPage) {
  448. // Detect Amazon's 2022-04-20 redesign
  449. const sellerProfileContainer = sellerPage.getElementById('seller-profile-container');
  450. const isRedesign = sellerProfileContainer.classList.contains('spp-redesigned');
  451.  
  452. const country = getSellerCountryFromSellerPage(sellerPage, isRedesign); // returns DE
  453. const rating = getSellerRatingFromSellerPage(sellerPage); // returns 91%
  454.  
  455. return { country, rating };
  456. }
  457.  
  458. function getSellerCountryFromSellerPage(sellerPage, isRedesign) {
  459. let country;
  460. if (isRedesign) {
  461. let addressArr = sellerPage.querySelectorAll('#page-section-detail-seller-info .a-box-inner .a-row.a-spacing-none.indent-left');
  462. country = addressArr[addressArr.length - 1]?.textContent.toUpperCase();
  463. } else {
  464. try {
  465. const sellerUl = sellerPage.querySelectorAll('ul.a-unordered-list.a-nostyle.a-vertical'); //get all ul
  466. const sellerUlLast = sellerUl[sellerUl.length - 1]; //get last list
  467. const sellerLi = sellerUlLast.querySelectorAll('li'); //get all li
  468. const sellerLiLast = sellerLi[sellerLi.length - 1]; //get last li
  469. country = sellerLiLast.textContent.toUpperCase();
  470. } catch {
  471. return '?';
  472. }
  473. }
  474. return (/^[A-Z]{2}$/.test(country)) ? country : '?';
  475. }
  476.  
  477. function getSellerRatingFromSellerPage(sellerPage) {
  478. if (sellerPage.getElementById('seller-name').textContent.includes('Amazon')) {
  479. return false; // seller is Amazon subsidiary and doesn't display ratings
  480. }
  481.  
  482. let feedbackEl = sellerPage.getElementById('seller-info-feedback-summary')
  483. let text = feedbackEl.querySelector('.feedback-detail-description').textContent
  484. let starText = feedbackEl.querySelector('.a-icon-alt').textContent
  485. text = text.replace(starText, '')
  486. let regex = /(\d+%).*?\((\d+)/;
  487. let zeroPercent = '0%';
  488.  
  489. const lang = document.documentElement.lang
  490. // Turkish places the percentage sign in front (e.g. %89)
  491. if (lang === 'tr-tr') {
  492. regex = /(%\d+).*?\((\d+)/;
  493. zeroPercent = '%0';
  494. }
  495.  
  496. // Special treatment for amazon.de in German and amazon.com.be in French
  497. if (lang === 'de-de' || lang === 'fr-be') {
  498. regex = /(\d+ %).*?\((\d+)/;
  499. zeroPercent = '0 %';
  500. }
  501.  
  502. let rating = text.match(regex);
  503. let score = rating ? rating[1] : zeroPercent;
  504. let count = rating ? rating[2] : '0';
  505.  
  506. return { score, count };
  507. }
  508.  
  509. function highlightProduct(product) {
  510. if (!options.highlightedCountries.includes(product.dataset.sellerCountry)) return;
  511.  
  512. // Highlight sellers from countries defined in 'options.highlightedCountries'
  513. product.classList.add('product--highlight');
  514.  
  515. if (!options.hideHighlightedProducts) return;
  516.  
  517. // When hideHighlightedProducts is true: Find correct element to hide
  518. let hiddenElement = product;
  519. if (product.parentElement.classList.contains('a-carousel-card')) {
  520. hiddenElement = product.parentElement;
  521. }
  522. if (product.closest('.sbx-desktop') !== null) {
  523. hiddenElement = product.closest('.sbx-desktop');
  524. }
  525. if (product.classList.contains('sbv-product')) {
  526. hiddenElement = product.closest('.s-result-item');
  527. }
  528. hiddenElement.classList.add('sb--hide');
  529. }
  530.  
  531. function createInfoBox(product) {
  532. const infoBoxCt = document.createElement('div');
  533. infoBoxCt.classList.add('seller-info-ct', 'a-size-small');
  534.  
  535. const infoBox = document.createElement('div');
  536. infoBox.classList.add('seller-info');
  537.  
  538. const icon = document.createElement('div');
  539. icon.classList.add('seller-icon', 'seller-loading');
  540. infoBox.appendChild(icon);
  541.  
  542. const text = document.createElement('div');
  543. text.classList.add('seller-text');
  544. text.textContent = product.dataset.sellerName;
  545. infoBox.appendChild(text);
  546.  
  547. infoBoxCt.appendChild(infoBox);
  548.  
  549. let productTitle = findTitle(product);
  550.  
  551. if (productTitle) {
  552. productTitle.parentNode.insertBefore(infoBoxCt, productTitle.nextSibling);
  553. } else {
  554. product.appendChild(infoBoxCt);
  555. }
  556.  
  557. fixHeights(product);
  558. }
  559.  
  560. function populateInfoBox(product) {
  561. const container = product.querySelector('.seller-info-ct');
  562. const infoBox = container.querySelector('.seller-info');
  563. const icon = container.querySelector('.seller-icon');
  564. const text = container.querySelector('.seller-text');
  565.  
  566. // remove loading spinner
  567. icon.classList.remove('seller-loading');
  568.  
  569. // replace "loading..." with real seller name
  570. text.textContent = product.dataset.sellerName;
  571.  
  572. if (product.dataset.sellerId && product.dataset.sellerId !== 'Amazon') {
  573. // Create link to seller profile if sellerId is valid
  574. const anchor = document.createElement('a');
  575. anchor.classList.add('seller-link');
  576. anchor.appendChild(infoBox);
  577. container.appendChild(anchor);
  578. anchor.href = window.location.origin + '/sp?seller=' + product.dataset.sellerId;
  579. }
  580.  
  581. if (product.dataset.blocked) {
  582. icon.textContent = '⚠️';
  583. icon.style.fontSize = "1.5em";
  584. infoBox.title = 'Error 503: Too many requests. Amazon blocked seller page. Please try again in a few minutes.';
  585. return;
  586. }
  587.  
  588. if (product.dataset.sellerName.includes('Amazon')) {
  589. // Seller is Amazon or one of its subsidiaries (Warehouse, UK, US, etc.)
  590. const amazonIcon = document.createElement('img');
  591. amazonIcon.src = '/favicon.ico';
  592. icon.appendChild(amazonIcon);
  593. infoBox.title = product.dataset.sellerName;
  594. return;
  595. }
  596.  
  597. // 1. Set icon, create infoBox title (if country known)
  598. if (product.dataset.sellerCountry && product.dataset.sellerCountry != '?') {
  599. icon.textContent = getFlagEmoji(product.dataset.sellerCountry);
  600. infoBox.title = (new Intl.DisplayNames([document.documentElement.lang], { type: 'region' })).of(product.dataset.sellerCountry) + ' | ';
  601. } else {
  602. icon.textContent = '❓';
  603. icon.style.fontSize = "1.5em";
  604. }
  605.  
  606. if (!product.dataset.sellerId) {
  607. console.error('No seller found', product);
  608. return;
  609. }
  610.  
  611. // 2. Append name to infoBox title
  612. infoBox.title += product.dataset.sellerName;
  613.  
  614. // 3. Append rating to text and infoBox title
  615. const ratingText = `(${product.dataset.sellerRatingScore} | ${product.dataset.sellerRatingCount})`;
  616. text.textContent += ` ${ratingText}`;
  617. infoBox.title += ` ${ratingText}`;
  618. }
  619.  
  620. function findTitle(product) {
  621. //TODO switch case
  622. try {
  623. let title;
  624. if (product.dataset.avar) {
  625. title = product.querySelector('.a-color-base.a-spacing-none.a-link-normal');
  626. } else if (product.parentElement.classList.contains('a-carousel-card')) {
  627. if (product.classList.contains('a-section') && product.classList.contains('a-spacing-none')) {
  628. title = product.querySelector('.a-link-normal');
  629. } else if (product.querySelector('.a-truncate:not([data-a-max-rows="1"])') !== null) {
  630. title = product.querySelector('.a-truncate');
  631. } else if (product.querySelector('h2') !== null) {
  632. title = product.getElementsByTagName("h2")[0];
  633. } else {
  634. title = product.querySelectorAll('.a-link-normal')[1];
  635. }
  636. } else if (product.id == 'gridItemRoot' || product.closest('#zg') !== null) {
  637. title = product.querySelectorAll('.a-link-normal')[1];
  638. } else if (product.classList.contains('octopus-pc-item-v3')) {
  639. title = product.querySelectorAll('.octopus-pc-asin-title, .octopus-pc-dotd-title')[0];
  640. } else if (product.classList.contains('octopus-pc-lightning-deal-item-v3')) {
  641. title = product.querySelector('.octopus-pc-deal-title');
  642. } else if (product.querySelector('.sponsored-products-truncator-truncated') !== null) {
  643. title = product.querySelector('.sponsored-products-truncator-truncated');
  644. } else {
  645. title = product.getElementsByTagName("h2")[0];
  646. }
  647. return title;
  648. } catch (error) {
  649. console.error(error, product);
  650. }
  651. }
  652.  
  653. function fixHeights(product) {
  654. // fixes for grid-item:
  655. if (product.id == 'gridItemRoot') {
  656. product.style.height = product.offsetHeight + 20 + 'px';
  657. }
  658.  
  659. if (product.classList.contains('octopus-pc-item')) {
  660.  
  661. const els = document.querySelectorAll('.octopus-pc-card-height-v3, .octopus-dotd-height, .octopus-lightning-deal-height');
  662. for (const el of els) {
  663. if (!el.getAttribute('style')) el.style.height = el.offsetHeight + 30 + 'px';
  664. }
  665.  
  666. const text = product.querySelectorAll('.octopus-pc-deal-block-section, .octopus-pc-dotd-info-section')[0];
  667. if (text) text.style.height = text.offsetHeight + 30 + 'px';
  668.  
  669. if (product.classList.contains('octopus-pc-lightning-deal-item-v3') && !product.dataset.height) {
  670. product.style.setProperty('height', product.offsetHeight + 30 + 'px', 'important');
  671. product.dataset.height = 'set';
  672. }
  673. }
  674.  
  675. if (product.closest('#rhf') !== null && product.closest('.a-carousel-viewport') !== null) {
  676. const els = document.querySelectorAll('.a-carousel-viewport, .a-carousel-left, .a-carousel-right');
  677. for (const el of els) {
  678. if (el.getAttribute('style') && !el.dataset.height) {
  679. el.style.height = el.offsetHeight + 30 + 'px';
  680. el.dataset.height = 'set';
  681. }
  682. }
  683. }
  684.  
  685. // hide stupid blocking links on sponsored products
  686. if (product.closest('.sbx-desktop') !== null) {
  687. const links = product.querySelectorAll('a:empty');
  688. links.forEach((link) => {
  689. link.style.height = 0;
  690. });
  691. }
  692. }
  693.  
  694. function asinKey(product) {
  695. return 'asin-' + product.dataset.asin;
  696. }
  697.  
  698. function sellerKey(product) {
  699. return 'seller-' + product.dataset.sellerId
  700. }
  701.  
  702. // validate storage item age and trigger re-fetch if needed
  703. function validateItemAge(product, itemTimeStamp, itemType) {
  704. const currentItemAge = Date.now() - parseInt(itemTimeStamp);
  705. let allowedItemAge, key, refetchFunction;
  706.  
  707. switch (itemType) {
  708. case 'asin':
  709. allowedItemAge = options.maxAgeAsinFetch * 1000 * 3600 * 24;
  710. key = asinKey;
  711. refetchFunction = getSellerIdAndNameFromProductPage;
  712. break;
  713. case 'seller':
  714. allowedItemAge = options.maxAgeSellerFetch * 1000 * 3600 * 24;
  715. key = sellerKey;
  716. refetchFunction = getSellerCountryAndRatingfromSellerPage;
  717. break;
  718. }
  719.  
  720. if (currentItemAge > allowedItemAge) {
  721. console.warn('Storage item ' + key(product) + ' is ' + readableItemAge(currentItemAge) + ' old. We must re-fetch it');
  722. return refetchFunction(product, true);
  723. }
  724. }
  725. }
  726.  
  727. // Country Code to Flag Emoji (Source: https://dev.to/jorik/country-code-to-flag-emoji-a21)
  728. function getFlagEmoji(countryCode) {
  729. const codePoints = countryCode
  730. .split('')
  731. .map(char => 127397 + char.charCodeAt());
  732. return String.fromCodePoint(...codePoints);
  733. }
  734.  
  735. // wrap function to create buttons with amazon's ui
  736. function wrapBtn(el, primary = false, small = false) {
  737. const wrapper = document.createElement('span');
  738. el.classList.add('a-button-inner', 'a-button-text');
  739. wrapper.classList.add('a-button');
  740. if (primary) wrapper.classList.add('a-button-primary');
  741. if (small) wrapper.classList.add('a-button-small');
  742. el.parentNode.insertBefore(wrapper, el);
  743. wrapper.appendChild(el);
  744. }
  745.  
  746. // convert storage item age from millisecs to days and hours
  747. function readableItemAge(ms) {
  748. const days = Math.floor(ms / (24 * 60 * 60 * 1000));
  749. const daysms = ms % (24 * 60 * 60 * 1000);
  750. const hours = Math.floor(daysms / (60 * 60 * 1000));
  751. return days + ' days and ' + hours + ' hours';
  752. }
  753.  
  754. function addGlobalStyle(css) {
  755. const head = document.getElementsByTagName('head')[0];
  756. if (!head) return;
  757. const style = document.createElement('style');
  758. style.innerHTML = css;
  759. head.appendChild(style);
  760. }
  761.  
  762. addGlobalStyle(`
  763. .sb--hide {
  764. display: none !important;
  765. }
  766.  
  767. .seller-info-ct {
  768. cursor: default;
  769. margin-top: 4px;
  770. }
  771.  
  772. .seller-info {
  773. display: inline-flex;
  774. gap: 4px;
  775. background: #fff;
  776. font-size: 0.9em;
  777. padding: 2px 4px;
  778. border: 1px solid #d5d9d9;
  779. border-radius: 4px;
  780. max-width: 100%;
  781. }
  782.  
  783. .seller-loading {
  784. display: inline-block;
  785. width: 0.8em;
  786. height: 0.8em;
  787. border: 3px solid rgb(255 153 0 / 30%);
  788. border-radius: 50%;
  789. border-top-color: #ff9900;
  790. animation: spin 1s ease-in-out infinite;
  791. margin: 1px 3px 0;
  792. }
  793.  
  794. @keyframes spin {
  795. to {
  796. transform: rotate(360deg);
  797. }
  798. }
  799.  
  800. .seller-icon {
  801. vertical-align: text-top;
  802. text-align: center;
  803. font-size: 1.8em;
  804. }
  805.  
  806. .seller-icon svg {
  807. width: 0.8em;
  808. height: 0.7em;
  809. }
  810.  
  811. .seller-icon img {
  812. width: 0.82em;
  813. height: 0.82em;
  814. }
  815.  
  816. .seller-text {
  817. color: #1d1d1d !important;
  818. white-space: nowrap;
  819. text-overflow: ellipsis;
  820. overflow: hidden;
  821. }
  822.  
  823. a.seller-link:hover .seller-info {
  824. box-shadow: 0 2px 5px 0 rgb(213 217 217 / 50%);
  825. background-color: #f7fafa;
  826. border-color: #d5d9d9;
  827. }
  828.  
  829. a.seller-link:hover .seller-name {
  830. text-decoration: underline;
  831. }
  832.  
  833. .product--highlight .s-card-container,
  834. .product--highlight[data-avar],
  835. .product--highlight.sbv-product,
  836. .a-carousel-has-buttons .product--highlight,
  837. #gridItemRoot.product--highlight,
  838. #gridItemRoot.product--highlight .a-cardui,
  839. .product--highlight .octopus-pc-item-image-section,
  840. .product--highlight .octopus-pc-asin-info-section,
  841. .product--highlight .octopus-pc-deal-block-section,
  842. .product--highlight .octopus-pc-dotd-info-section,
  843. .acswidget-carousel .product--highlight .acs-product-block {
  844. background-color: #f9e3e4;
  845. border-color: #f9e3e4;
  846. }
  847.  
  848. #gridItemRoot.product--highlight,
  849. .product--highlight .s-card-border {
  850. border-color: #e3abae;
  851. }
  852.  
  853. .product--highlight .s-card-drop-shadow {
  854. box-shadow: none;
  855. border: 1px solid #e3abae;
  856. }
  857.  
  858. .product--highlight .s-card-drop-shadow .s-card-border {
  859. border-color: #f9e3e4;
  860. }
  861.  
  862. .product--highlight[data-avar],
  863. .a-carousel-has-buttons .product--highlight {
  864. padding: 0 2px;
  865. box-sizing: content-box;
  866. }
  867.  
  868. .product--highlight.zg-carousel-general-faceout,
  869. #rhf .product--highlight {
  870. box-shadow: inset 0 0 0 1px #e3abae;
  871. padding: 0 6px;
  872. word-break: break-all;
  873. }
  874.  
  875. .product--highlight.zg-carousel-general-faceout img,
  876. #rhf .product--highlight img {
  877. max-width: 100% !important;
  878. }
  879.  
  880. #rhf .product--highlight img {
  881. margin: 1px auto -1px;
  882. }
  883.  
  884. .product--highlight a,
  885. .product--highlight .a-color-base,
  886. .product--highlight .a-price[data-a-color='base'] {
  887. color: #842029 !important;
  888. }
  889.  
  890. #gridItemRoot .seller-info {
  891. margin-bottom: 6px;
  892. }
  893.  
  894. .octopus-pc-item-v3 .seller-info-ct,
  895. .octopus-pc-lightning-deal-item-v3 .seller-info-ct {
  896. padding: 4px 20px 0;
  897. }
  898.  
  899. .sbx-desktop .seller-info-ct {
  900. margin: 0;
  901. }
  902.  
  903. .sp-shoveler .seller-info-ct {
  904. margin: -2px 0 3px;
  905. }
  906.  
  907. .p13n-sc-shoveler .seller-info-ct {
  908. margin: 0;
  909. }
  910.  
  911. .octopus-pc-item-image-section-v3 {
  912. text-align: center;
  913. }
  914.  
  915. #rhf .a-section.a-spacing-mini {
  916. text-align: center;
  917. }
  918.  
  919. a:hover.a-color-base,
  920. a:hover.seller-link {
  921. text-decoration: none;
  922. }
  923.  
  924. .sb-options {
  925. display: none;
  926. left: 50%;
  927. margin: 5vh auto;
  928. max-height: 90vh;
  929. max-width: 80vw;
  930. overflow-y: auto;
  931. position: fixed;
  932. top: 0;
  933. transform: translateX(-50%);
  934. width: 666px;
  935. z-index: 999;
  936. background-color: white;
  937. padding: 1rem;
  938. }
  939.  
  940. .sb-options--backdrop {
  941. display: none;
  942. position: fixed;
  943. top: 0;
  944. left: 0;
  945. bottom: 0;
  946. right: 0;
  947. background-color: rgba(0, 0, 0, 0.4);
  948. user-select: none;
  949. z-index: 199;
  950. }
  951.  
  952. #sb-settings * {
  953. font-family: inherit;
  954. }
  955.  
  956. #sb-settings .config_var {
  957. display: flex;
  958. flex-direction: row;
  959. justify-content: flex-start;
  960. }
  961.  
  962. #sb-settings .config_var input[type="checkbox"] {
  963. margin-right: 4px;
  964. min-width: 13px;
  965. }
  966.  
  967. #sb-settings #sb-settings_countries_var,
  968. #sb-settings #sb-settings_max-asin-age_var,
  969. #sb-settings #sb-settings_max-seller-age_var {
  970. flex-direction: column-reverse;
  971. }
  972.  
  973. #sb-settings .config_header {
  974. font-size: 165%;
  975. line-height: 32px;
  976. }
  977.  
  978. #sb-settings_header::before {
  979. content: '';
  980. display: inline-block;
  981. width: 32px;
  982. height: 32px;
  983. background: url(https://github.com/tadwohlrapp/soldby/raw/main/userscript/img/icon.png);
  984. background-size: contain;
  985. margin-right: 12px;
  986. vertical-align: bottom;
  987. }
  988.  
  989. #sb-settings .config_header,
  990. #sb-settings .config_var {
  991. padding-bottom: 16px;
  992. margin-bottom: 16px;
  993. border-bottom: 1px solid #ccc;
  994. }
  995.  
  996. #sb-settings .section_header {
  997. font-size: 110%;
  998. font-weight: 500;
  999. }
  1000.  
  1001. #sb-settings .section_desc {
  1002. color: #666;
  1003. background: #fff;
  1004. }
  1005.  
  1006. #sb-settings label {
  1007. font-weight: 500;
  1008. }
  1009.  
  1010. #sb-settings_buttons_holder {
  1011. display: flex;
  1012. flex-direction: row-reverse;
  1013. gap: 12px;
  1014. align-items: center;
  1015. }
  1016.  
  1017. #sb-settings #sb-settings_local-storage-clear-asin_var,
  1018. #sb-settings #sb-settings_local-storage-clear-seller_var{
  1019. width: 50%;
  1020. display: inline-flex;
  1021. }
  1022.  
  1023. #sb-settings #sb-settings_local-storage-clear-seller_var {
  1024. justify-content: flex-end;
  1025. }
  1026. `);
  1027. })();
  1028.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址