// ebay_core.js V1.4 - Shared Parsing & Extraction Logic // - Restructured JSON output with a "parsed" sub-object. // - Added parser_engine version. // - Removed itemUrl, added image_url. (function (root, factory) { if (typeof module === 'object' && module.exports) { module.exports = factory(); } else { root.EbayParser = factory(); } }(typeof self !== 'undefined' ? self : this, function () { 'use strict'; const EbayParser = {}; const PARSER_ENGINE_VERSION = 1; EbayParser.parseSizeAndQuantity = function(title) { title = title ? title.toUpperCase() : ""; let totalTB = 0; let quantity = 1; let needed_description_check = false; let individualSizeTB = 0; const explicitQtyPatterns = [ /\b(?:LOT\s+OF|LOT)\s*\(?\s*(\d+)\s*\)?/i, /\b(?:LOT\s+OF|LOT)\s*\*\s*(\d+)/i, /\b(?:PACK\s+OF|PACK|BULK)\s*\(?\s*(\d+)\s*\)?/i, /\b(\d+)\s*-\s*PACK\b/i, /\b(\d+)\s*COUNT\b/i ]; for (const pattern of explicitQtyPatterns) { const qtyMatch = title.match(pattern); if (qtyMatch && qtyMatch[1]) { const parsedQty = parseInt(qtyMatch[1], 10); if (parsedQty > 0 && parsedQty < 500) { quantity = parsedQty; break; } } } const sizeMatches = []; const sizeRegex = /(\d+(?:\.\d+)?)\s*(TB|GB)(?:\b|(?=\s|-|,|\(|\)|$))/g; let match; while ((match = sizeRegex.exec(title)) !== null) { sizeMatches.push({ value: parseFloat(match[1]), unit: match[2].toUpperCase() }); } if (sizeMatches.length > 0) { const uniqueSizesTB = [...new Set( sizeMatches.map(sm => sm.unit === 'GB' ? sm.value / 1000 : sm.value) )].sort((a, b) => a - b); if (uniqueSizesTB.length > 0) { individualSizeTB = uniqueSizesTB[0]; if (uniqueSizesTB.length > 1) needed_description_check = true; } } if (title.match(/\d+(?:\.\d+)?\s*(?:GB|TB)\s*(?:-|&|OR|TO)\s*\d+(?:\.\d+)?\s*(?:GB|TB)/i)) { needed_description_check = true; } if (quantity > 1 && title.includes("MIXED")) { needed_description_check = true; } if (title.includes("CHECK THE DESCRIPTION") || title.includes("CHECK DESCRIPTION") || title.includes("SEE DESCRIPTION")) { if (quantity > 1 || sizeMatches.length === 0 || sizeMatches.length > 1) { needed_description_check = true; } } if (individualSizeTB > 0) { totalTB = individualSizeTB * quantity; } if (quantity > 1 && totalTB === 0) { needed_description_check = true; } if (quantity === 1 && sizeMatches.length === 1 && !needed_description_check) { needed_description_check = false; } return { totalTB: parseFloat(totalTB.toFixed(4)), quantity: quantity, needed_description_check: needed_description_check, individualSizeTB: parseFloat(individualSizeTB.toFixed(4)) }; }; EbayParser.parsePrice = function(priceText) { priceText = priceText || ""; if (priceText.toLowerCase().includes(' to ')) { const rangeParts = priceText.split(/to/i); const firstPriceMatch = rangeParts[0] ? rangeParts[0].match(/\$?([\d,]+\.?\d*)/) : null; if (firstPriceMatch) { return parseFloat(firstPriceMatch[1].replace(/,/g, '')); } return null; } const priceMatch = priceText.match(/\$?([\d,]+\.?\d*)/); if (priceMatch) { return parseFloat(priceMatch[1].replace(/,/g, '')); } return null; }; EbayParser.runUnitTests = function() { const log = typeof console !== 'undefined' ? console.log : function() {}; const error = typeof console !== 'undefined' ? console.error : function() {}; log("Ebay Cost/TB: --- Running Unit Tests ---"); const testCases = [ { title: "LOT OF (9) MAJOR BRAND 2.5\" 7MM SSD * Kingston, Samsung, SanDisk& PNY*120-250GB", expected: { totalTB: 1.080, quantity: 9, individualSizeTB: 0.120, needed_description_check: true } }, { title: "Lot of 10 Intel 256 GB 2.5\" SATA SSD different Model check the Description", expected: { totalTB: 2.560, quantity: 10, individualSizeTB: 0.256, needed_description_check: true } }, { title: "Bulk 5 Lot Samsung 870 EVO 500GB SSD SATA - Used - Tested Passed Smart Test", expected: { totalTB: 2.500, quantity: 5, individualSizeTB: 0.500, needed_description_check: false } }, { title: "Samsung 1.6TB NVME PCIe 3.0 x8 2.75\" SSD MZPLK1T6HCHP PM1725 Series TLC", expected: { totalTB: 1.6, quantity: 1, individualSizeTB: 1.6, needed_description_check: false } }, { title: "Micron 5100 MAX 1.84TB SATA 6Gb/s 2.5\" SSD MTFDDAK1T9TCC-1AR1ZABYY", expected: { totalTB: 1.84, quantity: 1, individualSizeTB: 1.84, needed_description_check: false } }, { title: "10-PACK 1TB SSD", expected: { totalTB: 10.0, quantity: 10, individualSizeTB: 1.0, needed_description_check: false } }, ]; let testsPassed = 0; let testsFailed = 0; testCases.forEach((test, index) => { const result = EbayParser.parseSizeAndQuantity(test.title); const tbCheck = Math.abs(result.totalTB - test.expected.totalTB) < 0.0001; const qCheck = result.quantity === test.expected.quantity; const sizeCheck = Math.abs(result.individualSizeTB - test.expected.individualSizeTB) < 0.0001; const needCheck = result.needed_description_check === test.expected.needed_description_check; if (tbCheck && qCheck && sizeCheck && needCheck) testsPassed++; else { error(`Test ${index + 1}: FAILED - "${test.title}"`); error(` Expected: TTB=${test.expected.totalTB.toFixed(4)}, Q=${test.expected.quantity}, STB=${test.expected.individualSizeTB.toFixed(4)}, Check=${test.expected.needed_description_check}`); error(` Actual: TTB=${result.totalTB.toFixed(4)}, Q=${result.quantity}, STB=${result.individualSizeTB.toFixed(4)}, Check=${result.needed_description_check}`); testsFailed++; } }); log(`--- Unit Test Summary: ${testsPassed} Passed, ${testsFailed} Failed ---`); return testsFailed === 0; }; EbayParser.extractDataFromPage = function() { const itemSelector = 'li.s-item, li.srp-results__item, div.s-item[role="listitem"]'; const itemElements = document.querySelectorAll(itemSelector); const items = []; const today = new Date().toISOString(); itemElements.forEach(item => { const titleElement = item.querySelector('.s-item__title, .srp-results__title'); const priceElement = item.querySelector('.s-item__price'); // const linkElement = item.querySelector('.s-item__link, a[href*="/itm/"]'); // Not used for itemUrl anymore const imageElement = item.querySelector('.s-item__image-wrapper img.s-item__image-img, .s-item__image img'); // Common image selectors let rawTitle = titleElement ? titleElement.innerText.trim() : null; const priceText = priceElement ? priceElement.innerText.trim() : null; // const itemUrl = linkElement ? linkElement.href : null; // Removed // Try to get image URL, prefer data-src for lazy-loaded images, fallback to src let imageUrl = null; if (imageElement) { imageUrl = imageElement.dataset.src || imageElement.getAttribute('src'); } if (!rawTitle || !priceText) return; // Item ID is now critical, URL was for item ID let cleanedTitle = rawTitle; const newListingRegex = /^\s*NEW LISTING\s*[:\-\s]*/i; if (newListingRegex.test(cleanedTitle)) { cleanedTitle = rawTitle.replace(newListingRegex, "").trim(); } else if (newListingRegex.test(rawTitle)) { cleanedTitle = rawTitle.replace(newListingRegex, "").trim(); } const primaryDisplayPrice = EbayParser.parsePrice(priceText); let currentBidPrice = null; let finalBuyItNowPrice = null; let hasBestOffer = false; let itemIsAuction = false; const bidCountElement = item.querySelector('.s-item__bid-count'); if (bidCountElement && bidCountElement.innerText.toLowerCase().includes('bid')) { itemIsAuction = true; } const bestOfferElement = item.querySelector('.s-item__purchase-options--bo, .s-item__best-offer'); if (bestOfferElement) { hasBestOffer = true; } else { const secondaryInfoElements = item.querySelectorAll('.s-item__subtitle, .s-item__secondary-text, .s-item__detail--secondary'); secondaryInfoElements.forEach(el => { if (el.innerText.toLowerCase().includes('or best offer')) { hasBestOffer = true; } }); } if (itemIsAuction) { currentBidPrice = primaryDisplayPrice; const auctionBinPriceElement = item.querySelector('.s-item__buy-it-now-price'); if (auctionBinPriceElement) { finalBuyItNowPrice = EbayParser.parsePrice(auctionBinPriceElement.innerText); } } else { finalBuyItNowPrice = primaryDisplayPrice; } const parsedInfo = EbayParser.parseSizeAndQuantity(cleanedTitle); const totalTB = parsedInfo.totalTB; const quantity = parsedInfo.quantity; const individualSizeTB = parsedInfo.individualSizeTB; const needed_description_check = parsedInfo.needed_description_check; let costPerTB = null; if (primaryDisplayPrice !== null && totalTB > 0) { costPerTB = primaryDisplayPrice / totalTB; } // Extract Item ID from the item's link (still need a link element for this) let itemId = null; const linkForIdElement = item.querySelector('a.s-item__link[href*="/itm/"], .s-item__info > a[href*="/itm/"]'); if (linkForIdElement && linkForIdElement.href) { const itemMatch = linkForIdElement.href.match(/\/itm\/(\d+)/); if (itemMatch && itemMatch[1]) { itemId = itemMatch[1]; } } if(!itemId) return; // Skip if no item ID can be found, as it's crucial items.push({ title: cleanedTitle, itemId: itemId, // Crucial dateFound: today, currentBidPrice: currentBidPrice, buyItNowPrice: finalBuyItNowPrice, hasBestOffer: hasBestOffer, image_url: imageUrl, // <-- Added parsed: { // <-- Nested object itemCount: quantity, sizePerItemTB: individualSizeTB > 0 ? parseFloat(individualSizeTB.toFixed(3)) : null, totalTB: totalTB > 0 ? parseFloat(totalTB.toFixed(3)) : null, costPerTB: costPerTB !== null ? parseFloat(costPerTB.toFixed(2)) : null, needed_description_check: needed_description_check, parser_engine: PARSER_ENGINE_VERSION // <-- Added } // itemUrl: itemUrl, // <-- Removed }); }); return items; }; return EbayParser; }));