BrickLink Price Averages

Adds the currently-for-sale price average to BL store listings

  1. // ==UserScript==
  2. // @name BrickLink Price Averages
  3. // @name:en BrickLink Price Averages
  4. // @namespace Violentmonkey Scripts
  5. // @match https://store.bricklink.com/*
  6. // @grant none
  7. // @version 1.0
  8. // @author The0x539
  9. // @description Adds the currently-for-sale price average to BL store listings
  10. // @run-at document-body
  11. // @license AGPL-3.0
  12. // ==/UserScript==
  13.  
  14. const RealXMLHttpRequest = window.XMLHttpRequest;
  15. class PatchedXMLHttpRequest extends RealXMLHttpRequest {
  16. constructor(...args) {
  17. super(...args);
  18. this.isSearch = false;
  19. this.jobDone = false;
  20. }
  21.  
  22. open(...args) {
  23. if (args[1].startsWith('/ajax/clone/store/searchitems.ajax')) {
  24. this.isSearch = true;
  25. }
  26. return super.open(...args);
  27. }
  28.  
  29. get responseText() {
  30. if (this.isSearch && !this.jobDone) {
  31. const response = JSON.parse(super.responseText);
  32. processSearchResponse(response);
  33. this.jobDone = true;
  34. }
  35.  
  36. return super.responseText;
  37. }
  38. }
  39. window.XMLHttpRequest = PatchedXMLHttpRequest;
  40.  
  41. const promises = new Map();
  42.  
  43. function processSearchResponse(response) {
  44. for (const item of response.result.groups.flatMap(g => g.items)) {
  45. const { itemID, colorID, itemName, colorName } = item;
  46. const key = (colorName + '\xA0' + itemName).trimStart();
  47. if (!promises.has(key)) {
  48. promises.set(key, getPrices(itemID, colorID));
  49. }
  50. }
  51. }
  52.  
  53. async function getPrices(itemID, colorID) {
  54. const response = await fetch(`/v2/catalog/catalogitem_pgtab.page?idItem=${itemID}&idColor=${colorID}`);
  55. const html = await response.text();
  56. const doc = new DOMParser().parseFromString(html, 'text/html');
  57.  
  58. const rows = doc.querySelectorAll('table.pcipgSummaryTable tr');
  59. const averages = [...rows]
  60. .filter(row => row.firstElementChild.innerText === 'Avg Price:')
  61. .map(row => row.lastElementChild.innerText);
  62.  
  63. const [new6Months, used6Months, newForSale, usedForSale] = averages;
  64. return { new6Months, used6Months, newForSale, usedForSale };
  65. }
  66.  
  67. function onUpdatePage(records, observer) {
  68. const selector = '.store-items article.table-row:not(:has(.buy div.average))';
  69.  
  70. const rows = records
  71. .flatMap(r => [...r.addedNodes])
  72. .filter(n => n instanceof HTMLElement)
  73. .flatMap(n => [...n.querySelectorAll(selector)]);
  74.  
  75. for (const row of rows) {
  76. addAverage(row);
  77. }
  78. }
  79.  
  80. async function addAverage(listing) {
  81. const key = listing.querySelector('.description p').innerText;
  82. const prices = await promises.get(key);
  83.  
  84. const condition = listing.querySelector('.condition strong').innerText;
  85. const price = (condition === 'Used') ? prices.usedForSale : prices.newForSale;
  86.  
  87. const newHtml = `
  88. <div class="average">
  89. <span>Average: </span>
  90. <strong>${price}</strong>
  91. </div>
  92. `;
  93. listing.querySelector('.buy').children[1].insertAdjacentHTML('afterend', newHtml);
  94. }
  95.  
  96. const observeOptions = { childList: true, subtree: true };
  97. new MutationObserver(onUpdatePage).observe(document.body, observeOptions);