TCG Player Sales Display Data

Remove obfuscation around TCG Player Sales Data

当前为 2022-05-27 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name TCG Player Sales Display Data
  3. // @namespace https://www.tcgplayer.com/
  4. // @version 0.27
  5. // @description Remove obfuscation around TCG Player Sales Data
  6. // @author Peter Creutzberger
  7. // @match https://www.tcgplayer.com/product/*
  8. // @icon https://www.tcgplayer.com/favicon.ico
  9. // @grant none
  10. // ==/UserScript==
  11.  
  12. (function() {
  13. 'use strict';
  14. writeDataRequestButton();
  15.  
  16. const addCondition = () => ({
  17. totalSpend: 0,
  18. totalQtySold: 0,
  19. totalOrders:0,
  20. historicSalesData: {daysAgo:{}},
  21. avgQtyPerOrder: function(totalQtySold,totalOrders ) {
  22. return (totalQtySold / totalOrders) > 0 ? (totalQtySold / totalOrders).toFixed(2) : 0;
  23. },
  24. marketPriceByOrder: function(totalSpend, totalOrders) {
  25. return (totalSpend / totalOrders).toFixed(2);
  26. }
  27. });
  28.  
  29. const cleanPriceValue = (price) => +price.replace(/[^0-9.]/g,'');
  30.  
  31. const strToInt = (str) => +str;
  32.  
  33. const shapeSalesData = (salesData) => salesData.length === 4 ? {date: salesData[0].innerText, condition: salesData[1].innerText, quantity: salesData[2].innerText, price: salesData[3].innerText} :
  34. {date: salesData[0].innerText, condition: `${salesData[2].innerText} with Photo`, quantity: salesData[3].innerText, price: salesData[4].innerText};
  35.  
  36. const checkSaleDate = (salesArray, saleDate, price) => {
  37. if (!salesArray.earliestSaleDateData || !salesArray.latestSaleDateData) { return {earliestSaleDateData: {date: saleDate, price: price}, latestSaleDateData: {date: saleDate, price: price}}; }
  38. const dateFromSaleDate = new Date(saleDate).getTime();
  39. const dateFromEarliestSaleDate = new Date(salesArray.earliestSaleDateData.date).getTime();
  40. if (dateFromSaleDate < dateFromEarliestSaleDate || dateFromSaleDate === dateFromEarliestSaleDate) { return Object.assign(salesArray, {earliestSaleDateData: {date: saleDate, price: price}}); }
  41. const dateFromLatestSaleDate = new Date(salesArray.latestSaleDateData.date).getTime();
  42. if (dateFromSaleDate > dateFromLatestSaleDate || dateFromSaleDate === dateFromLatestSaleDate) { return Object.assign(salesArray, {latestSaleDateData: {date: saleDate, price: price}}); }
  43. }
  44.  
  45. const checkOrderQty = (salesArray, date, qty, price) => { if ( !salesArray.largestQtySold || salesArray.largestQtySold.qty < qty) { return {largestQtySold: {date: date, qty: qty, price: price}}; } }
  46.  
  47. const historicDataSetting = (salesArray, saleDate, dateDiff, price, qty) => {
  48. if (!salesArray.historicSalesData.daysAgo || !salesArray.historicSalesData.daysAgo[dateDiff]) { return {totalSpend: price, totalQtySold: qty, totalOrders: 1, saleDate: saleDate}; }
  49. const historicSalesDataStatus = salesArray.historicSalesData.daysAgo[dateDiff];
  50. return {totalSpend: historicSalesDataStatus.totalSpend += price, totalQtySold: historicSalesDataStatus.totalQtySold += qty, totalOrders: historicSalesDataStatus.totalOrders += 1, saleDate: saleDate};
  51. }
  52.  
  53. const updateSalesTotals = (salesArray, price, qty) => {
  54. salesArray.totalSpend += (price * qty);
  55. salesArray.totalQtySold += qty;
  56. salesArray.totalOrders += 1;
  57. }
  58.  
  59. const gatherSalesData = () => {
  60. const salesByCondition = {};
  61. const modalDisplayLength = Array.from(document.getElementsByClassName("is-modal")).length -= 1;
  62. const historicDateArr = setHistoricDateArr(daysToLookBack());
  63. Array.from(document.getElementsByClassName("is-modal")[modalDisplayLength].children).forEach( (children, index) => {
  64. const listOfSales = Array.from(document.getElementsByClassName("is-modal")[modalDisplayLength].children);
  65. if (listOfSales[index]?.children[1]) {
  66. const reshapedSalesData = shapeSalesData( Array.from(listOfSales[index].children) );
  67. const currentCondition = reshapedSalesData.condition;
  68. if ( !Object.keys(salesByCondition).includes(currentCondition) ) { salesByCondition[currentCondition] = addCondition(); }
  69. const cleanPrice = cleanPriceValue(reshapedSalesData.price);
  70. Object.assign(salesByCondition[currentCondition],
  71. checkOrderQty(salesByCondition[currentCondition], reshapedSalesData.date, strToInt(reshapedSalesData.quantity), cleanPrice),
  72. checkSaleDate(salesByCondition[currentCondition], reshapedSalesData.date, cleanPrice)
  73. );
  74. updateSalesTotals(salesByCondition[currentCondition], cleanPrice, strToInt(reshapedSalesData.quantity));
  75. const saleDateDiff = historicDateArr.includes(reshapedSalesData.date) ? getSaleDateDiff(todaysDate, reshapedSalesData.date) : -1;
  76. if ( saleDateDiff > -1 ) {
  77. //updateSalesTotals(salesByCondition[currentCondition], cleanPrice, strToInt(reshapedSalesData.quantity));
  78. salesByCondition[currentCondition].historicSalesData.daysAgo[saleDateDiff] = historicDataSetting(salesByCondition[currentCondition], reshapedSalesData.date, saleDateDiff, cleanPrice, strToInt(reshapedSalesData.quantity));
  79. }
  80. }
  81. });
  82. return salesByCondition;
  83. }
  84.  
  85. const adjustSalesDataDivHeight = (div, timesToAdjustHeight) => (parseInt(div.style.height.replace(/[^0-9]/g,'')) + 115 * timesToAdjustHeight) + "px";
  86.  
  87. const writeSalesDataContainer = () => {
  88. const div = document.createElement('div');
  89. const setBottom = document.getElementsByClassName("_hj_feedback_container")[0] ? 'bottom:100px' : 'bottom:0';
  90. div.innerHTML = (`<div class="salesDataDisplay" style="position:fixed;${setBottom};left:0;z-index:8888;width:auto;height:0;max-height:600px;overflow-y:scroll;padding:0 5px 0 0;border:1px solid #d00;background:#999;color:#fff;line-height:normal"></div>`);
  91. document.body.prepend(div);
  92. }
  93.  
  94. const displaySalesData = (salesByCondition) => {
  95. const div = document.getElementsByClassName('salesDataDisplay')[0];
  96. salesByCondition.forEach(cardConditionData => {
  97. const cardDisplayData = buildSalesDataDisplay(cardConditionData[0], cardConditionData[1]);
  98. div.innerHTML += cardDisplayData.cardDisplayData;
  99. div.style.height = adjustSalesDataDivHeight(div, cardDisplayData.timesToAdjustHeight);
  100. });
  101. }
  102.  
  103. const buildSalesDataDisplay = (cardCondition, cardConditionData) => {
  104. let heightAdjustmentCount = 1;
  105. let cardDisplayString = `<div class="displayContainer"><strong>${cardCondition}</strong><br />
  106. <span id="salesHeader" style="margin-left: 20px;"><strong>Overall Sales Data</strong></span><br />
  107. <span id="totalSold" style="margin-left: 40px;">Total Sold: ${cardConditionData.totalQtySold} - Total Orders: ${cardConditionData.totalOrders} - Total Spend: ${cardConditionData.totalSpend.toFixed(2)}</span><br />
  108. <span id="avgQtyPerOrder" style="margin-left: 40px;">Avg Qty Per Order: ${cardConditionData.avgQtyPerOrder(cardConditionData.totalQtySold, cardConditionData.totalOrders)}</span><br />
  109. <span id="earliestSaleDate" style="margin-left: 40px;">Earliest Sale Date: ${cardConditionData.earliestSaleDateData.date} - Sale Price ${cardConditionData.earliestSaleDateData?.price}</span><br />
  110. <span id="latestSaleData" style="margin-left: 40px;">Latest Sale Date: ${cardConditionData.latestSaleDateData.date} - Sale Price: ${cardConditionData.latestSaleDateData?.price}</span><br />
  111. <span id="largestOrderInfo" style="margin-left: 40px;">Largest Order... Date: ${cardConditionData.largestQtySold.date} - Qty: ${cardConditionData.largestQtySold.qty} - Price Per: ${cardConditionData.largestQtySold.price}</span>`;
  112. if ( Object.keys(cardConditionData.historicSalesData.daysAgo).length ) {
  113. heightAdjustmentCount++;
  114. cardDisplayString += `<br /><span id="historicSalesHeader" style="margin-left: 20px;"><strong>Historic Sales Data</strong></span><br />`;
  115. const historicSalesData = cardConditionData.historicSalesData;
  116. Object.keys(historicSalesData.daysAgo).forEach( daysAgo =>
  117. cardDisplayString += `<span id="${daysAgo}-daysAgoMarker" style="margin-left: 30px;"><strong>Days Ago: ${daysAgo} - ${historicSalesData.daysAgo[daysAgo].saleDate}</strong></span><br />
  118. <span id="${daysAgo}-dayAgo-TotalSold" style="margin-left: 40px;">Total Orders: ${historicSalesData.daysAgo[daysAgo].totalOrders} - Total Spend: ${historicSalesData.daysAgo[daysAgo].totalSpend.toFixed(2)} - Total Qty Sold: ${historicSalesData.daysAgo[daysAgo].totalQtySold}</span><br />
  119. <span id="${daysAgo}-dayAgo-AvgQtyPerOrder" style="margin-left: 40px;">Avg Qty Per Order: ${cardConditionData.avgQtyPerOrder(historicSalesData.daysAgo[daysAgo].totalQtySold, historicSalesData.daysAgo[daysAgo].totalOrders)}</span><br />
  120. <span id="${daysAgo}-dayAgo-MarketPrice" style="margin-left: 40px;">Market Price: ${cardConditionData.marketPriceByOrder( historicSalesData.daysAgo[daysAgo].totalSpend, historicSalesData.daysAgo[daysAgo].totalOrders ) }</span><br />`
  121. );
  122. }
  123. cardDisplayString += `</div><br />`;
  124. return {cardDisplayData: cardDisplayString, timesToAdjustHeight: heightAdjustmentCount};
  125. }
  126.  
  127. const decorateSalesHistoryHeader = (clickCount = 0) => {
  128. const fontColor = '#fa7ad0';
  129. const backgroundColor = '#7afaa4';
  130. document.getElementsByClassName('modal__title')[0].children[0].innerHTML = `<div style="color:${fontColor};background:${backgroundColor}">GATHERING SALES DATA - Click Count: ${clickCount}</span>`;
  131. }
  132.  
  133. const toggleGatherDataButton = () => { document.getElementsByClassName('dataRequestButton')[0].disabled = !document.getElementsByClassName('dataRequestButton')[0].disabled; }
  134.  
  135. async function loadSalesDataSplash() {
  136. document.getElementsByClassName("price-guide__latest-sales__more")[0].children[0].click();
  137. await loadMoreSalesData();
  138. return gatherSalesData();
  139. }
  140.  
  141. const beginSalesDataDisplay = (salesByCondition) => {
  142. document.getElementsByClassName("modal__overlay")[0].click();
  143. writeSalesDataContainer();
  144. displaySalesData(Object.entries(salesByCondition).sort((conditionOne, conditionTwo) => conditionOne[1].totalQtySold - conditionTwo[1].totalQtySold ).reverse());
  145. }
  146.  
  147. async function loadMoreSalesData() {
  148. const maxClicks = 50;
  149. await sleep(500);
  150. for (let clickCount = 0; clickCount < maxClicks; clickCount++) {
  151. await sleep(500);
  152. decorateSalesHistoryHeader(clickCount);
  153. if (document.getElementsByClassName('price-guide-modal__load-more')[0]) { document.getElementsByClassName('price-guide-modal__load-more')[0].click();}
  154. else {clickCount = maxClicks; }
  155. }
  156. }
  157.  
  158. const sleep = (milliseconds) => { return new Promise(resolve => setTimeout(resolve, milliseconds)); }
  159.  
  160. window.startDataRequest = function() {
  161. clearHtmlElements();
  162. if (!document.getElementsByClassName("price-guide__latest-sales__more")[0]?.children[0]) { alert('Please wait for the "View Sales History" link to load then click the button again.'); }
  163. else {
  164. toggleGatherDataButton();
  165. loadSalesDataSplash()
  166. .then(result => beginSalesDataDisplay(result))
  167. .then(() => writeSalesToggle())
  168. .finally(() => toggleGatherDataButton())
  169. }
  170. }
  171.  
  172. /********************
  173. HTML element interaction
  174. ********************/
  175.  
  176. const clearHtmlElements = () => {
  177. ['salesDataDisplay', 'salesDataToggle'].forEach( selector => {
  178. if ( document.getElementsByClassName(selector)[0] ) { document.getElementsByClassName(selector)[0].remove(); }
  179. });
  180. }
  181.  
  182. window.toggleSalesData = function() {
  183. const display = document.getElementsByClassName('salesDataDisplay')[0].style.display;
  184. document.getElementsByClassName('salesDataDisplay')[0].style.display = display === 'none' ? 'inline' : 'none';
  185. }
  186.  
  187. /********************
  188. Write interactive HTML elements
  189. ********************/
  190.  
  191. const writeSalesToggle = () => {
  192. const div = document.createElement('div');
  193. div.innerHTML = ('<button class="salesDataToggle" style="position:fixed;top:40px;left:0;z-index:9999;width:auto;height:20px;padding:0 5px 0 0;background:#0b0;color:#fff;font-weight:bold;" onclick="toggleSalesData()">Toggle Sales Data Display</button>');
  194. document.body.prepend(div);
  195. }
  196.  
  197. function writeDaysToLookBackSpinner() {
  198. const div = document.createElement('div');
  199. div.style = 'position:fixed;top:0;left:0;z-index:9999;width:auto;height:20px;padding:0 5px 0 0;background:#a00;color:#fff;font-size:10pt;font-weight:bold;appearance:inherit;';
  200. div.innerHTML = ('Days to Look Back <input type="number" class="daysToLookBack" id="daysToLookBack" min="0" max="7" step="1" value="2" style="height:20px;font-size:10pt!important"/>');
  201. document.body.prepend(div);
  202. }
  203.  
  204. function writeDataRequestButton() {
  205. writeDaysToLookBackSpinner();
  206. const div = document.createElement('div');
  207. div.innerHTML = ('<button class="dataRequestButton" style="position:fixed;top:20px;left:0;z-index:9999;width:auto;height:20px;padding:0 5px 0 0;background:#00b;color:#fff;font-weight:bold;" onclick="startDataRequest()">Gather Sales Data</button>');
  208. document.body.prepend(div);
  209. }
  210.  
  211. /********************
  212. Re-inventing the wheel of time because we are not importing the moment library.
  213. ********************/
  214.  
  215. const setHistoricDateArr = (daysToLookBack) => {
  216. let historicDatesArr = [];
  217. for (let dayCount = 0; dayCount <= daysToLookBack; dayCount++) {
  218. historicDatesArr.push( formatDateToTCG( new Date(Date.now() - (daysInMilliseconds(dayCount) ))) );
  219. }
  220. return historicDatesArr;
  221. }
  222.  
  223. const formatDateToTCG = (date) => (date.getMonth() + 1) + '/' + date.getDate() + '/' + date.getFullYear().toString().slice(2);
  224.  
  225. const parseStrDate = (stringDate) => {
  226. const dateArr = stringDate.split('/');
  227. return new Date(dateArr[2], dateArr[0] - 1, dateArr[1]);
  228. }
  229.  
  230. const getSaleDateDiff = (firstDate, secondDate) => Math.ceil((parseStrDate(firstDate) - parseStrDate(secondDate)) / daysInMilliseconds(1) );
  231.  
  232. const daysInMilliseconds = (days = 1) => 1000 * 60 * 60 * (24 * days);
  233.  
  234. const daysToLookBack = () => +document.getElementsByClassName('daysToLookBack')[0].value >= 0 ? +document.getElementsByClassName('daysToLookBack')[0].value : 2;
  235.  
  236. const todaysDate = formatDateToTCG(new Date());
  237. })();
  238.