251 lines
12 KiB
JavaScript
251 lines
12 KiB
JavaScript
// 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;
|
|
}));
|