commit 8043c10df211cc1454377ce4e601a3b95208be8c Author: hak8or Date: Tue May 27 23:22:31 2025 -0400 initial commit diff --git a/ebay_hdd.js b/ebay_hdd.js new file mode 100644 index 0000000..1e8982d --- /dev/null +++ b/ebay_hdd.js @@ -0,0 +1,540 @@ +// Ebay Cost/TB Calculator & Sorter V2.6 +// For use with Tampermonkey @require directive. +// Original author: Your Name / AI Assistant +// Description: Calculates and displays cost per TB for eBay listings, allows sorting, +// handles ambiguous titles, and includes unit tests for parsing logic. + +(function() { + 'use strict'; + + const DEBUG_MODE = true; // Set to true to run unit tests on load + + // --- Global Variables --- + let originalOrderMap = new Map(); // Stores { itemId: originalIndex } + let isSorted = false; + let mainListParentElement = null; // Cached parent element + let observer = null; // MutationObserver instance + let observerTargetNode = null; // Node the observer is attached to + const observerConfig = { childList: true, subtree: true }; // Observer configuration + let processTimer = null; // Timer for debouncing observer callback + + // --- Core Parsing Functions --- + function parseSizeAndQuantity(title) { + title = title.toUpperCase(); + let totalTB = 0; + let quantity = 1; // Default to 1 + let needed_description_check = false; + let individualSizeTB = 0; + + // 1. Parse Quantity - More conservative approach + // Prioritize explicit "LOT OF N", "PACK OF N", "N PACK", "N LOT", "BULK N" + const explicitQtyPatterns = [ + /\b(?:LOT\s+OF|LOT)\s*\(?\s*(\d+)\s*\)?/i, // e.g., "LOT OF (9)", "LOT (9)", "LOT 9" + /\b(?:LOT\s+OF|LOT)\s*\*\s*(\d+)/i, // e.g., "Lot of*10" + /\b(?:PACK\s+OF|PACK|BULK)\s*\(?\s*(\d+)\s*\)?/i, // e.g., "PACK OF 5", "5 PACK", "BULK 5" + /\b(\d+)\s*-\s*PACK\b/i, // e.g., "10-PACK" + /\b(\d+)\s*COUNT\b/i // e.g., "10 COUNT" + ]; + + for (const pattern of explicitQtyPatterns) { + const qtyMatch = title.match(pattern); + if (qtyMatch && qtyMatch[1]) { + const parsedQty = parseInt(qtyMatch[1], 10); + // Added a sanity check for quantity (e.g., not excessively large from a model number) + if (parsedQty > 0 && parsedQty < 500) { + quantity = parsedQty; + break; // Found a valid quantity + } + } + } + + // 2. Parse Size + const sizeMatches = []; + // Refined regex: TB/GB must be followed by a word boundary or specific terminators (space, hyphen, comma, parens, end of string) + // This prevents matching "GB" in "6GB/S" or "6GBPS" + 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]; // Pick the smallest detected size + if (uniqueSizesTB.length > 1) { + needed_description_check = true; // Multiple different sizes mentioned + } + } + } + + // 3. Further checks for ambiguity for needed_description_check + if (title.match(/\d+(?:\.\d+)?\s*(?:GB|TB)\s*(?:-|&|OR|TO)\s*\d+(?:\.\d+)?\s*(?:GB|TB)/i)) { + needed_description_check = true; // e.g. "120GB-250GB" or "120GB & 250GB" + } + if (quantity > 1 && title.includes("MIXED")) { + needed_description_check = true; + } + // If "CHECK DESCRIPTION" or similar is present, and there's ambiguity in quantity or size + 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 it's a lot (quantity > 1) and no size could be determined (totalTB is 0), flag for check. + if (quantity > 1 && totalTB === 0) { + needed_description_check = true; + } + // If only one size was found and quantity is 1, ensure check is false unless other flags hit + if (quantity === 1 && sizeMatches.length === 1 && !needed_description_check) { + needed_description_check = false; + } + + + return { totalTB: parseFloat(totalTB.toFixed(4)), quantity, needed_description_check }; // Return totalTB with more precision + } + + function parsePrice(priceText) { + if (priceText.toLowerCase().includes(' to ')) { + return null; // Price range, cannot calculate + } + const priceMatch = priceText.match(/\$?([\d,]+\.?\d*)/); + if (priceMatch) { + return parseFloat(priceMatch[1].replace(/,/g, '')); + } + return null; + } + + // --- Unit Testing --- + function runUnitTests() { + console.log("Ebay Cost/TB: --- Running Unit Tests for parseSizeAndQuantity ---"); + const testCases = [ + { title: "LOT OF (9) MAJOR BRAND 2.5\" 7MM SSD * Kingston, Samsung, SanDisk& PNY*120-250GB", expected: { totalTB: 9 * 0.120, quantity: 9, needed_description_check: true } }, + { title: "Lot of 10 Intel 256 GB 2.5\" SATA SSD different Model check the Description", expected: { totalTB: 10 * 0.256, quantity: 10, needed_description_check: true } }, + { title: "Lot of*10 Mixed brands 240GB-256GB 2.5\" SATA SSD Drives Working & tested", expected: { totalTB: 10 * 0.240, quantity: 10, needed_description_check: true } }, + { title: "Lot of 9 SSD 120&128 GB 2.5\" SATA different brands check the description", expected: { totalTB: 9 * 0.120, quantity: 9, needed_description_check: true } }, + { title: "Bulk 5 Lot Samsung 870 EVO 500GB SSD SATA - Used - Tested Passed Smart Test", expected: { totalTB: 5 * 0.500, quantity: 5, 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, needed_description_check: false } }, + { title: "Brand New Crucial X6 2TB Portable External SSD (CT2000X6SSD9)", expected: { totalTB: 2.0, quantity: 1, needed_description_check: false } }, + { title: "Western Digital WD_BLACK SN850X 2TB NVMe Internal SSD", expected: { totalTB: 2.0, quantity: 1, needed_description_check: false } }, + { title: "Corsair Force Series MP600 1TB Gen4 PCIe X4 NVMe M.2 SSD Up to 4950 MB/s CSSD...", expected: { totalTB: 1.0, quantity: 1, needed_description_check: false } }, + { title: "Micron 5100 MAX 1.84TB SATA 6Gb/s 2.5\" SSD MTFDDAK1T9TCC-1AR1ZABYY", expected: { totalTB: 1.84, quantity: 1, needed_description_check: false } }, + { title: "Dell 0HGX92 1.6TB 2.5” PCIe NVMe Gen4 SSD Intel D7-P5600 SSDPF2KE016T9T HGX92 ES", expected: { totalTB: 1.6, quantity: 1, needed_description_check: false } }, + { title: "2 X 4TB SSDs Lot", expected: { totalTB: 4.0, quantity: 1, needed_description_check: false } }, // Current simplified Q parsing will result in Q=1, totalTB=4TB. To get Q=2 needs more complex logic. + { title: "LOT OF 2X 1TB SSDs", expected: { totalTB: 2 * 1.0, quantity: 2, needed_description_check: false } }, // Should be caught by "LOT OF (N)" if "2X" is seen as "2" + { title: "10-PACK 1TB SSD", expected: { totalTB: 10 * 1.0, quantity: 10, needed_description_check: false } }, + { title: "SSD 5 COUNT 256GB", expected: { totalTB: 5 * 0.256, quantity: 5, needed_description_check: false } }, + { title: "Single 2TB Drive", expected: { totalTB: 2.0, quantity: 1, needed_description_check: false } }, + { title: "Lot of 2 (512GB SSDs)", expected: { totalTB: 2 * 0.512, quantity: 2, needed_description_check: false } }, + { title: "Mixed Lot SSDs 120GB, 240GB, 500GB - Total 3 drives", expected: { totalTB: 0.120, quantity: 1, needed_description_check: true } } // Q parsing for "Total N drives" not implemented, smallest size taken. Q will be 1. + ]; + + let testsPassed = 0; + let testsFailed = 0; + + testCases.forEach((test, index) => { + const result = parseSizeAndQuantity(test.title); + // Using a small tolerance for floating point comparison of totalTB + const totalTBCheck = Math.abs(result.totalTB - test.expected.totalTB) < 0.0001; + const quantityCheck = result.quantity === test.expected.quantity; + const neededCheck = result.needed_description_check === test.expected.needed_description_check; + + if (totalTBCheck && quantityCheck && neededCheck) { + // console.log(`Test ${index + 1}: PASSED - "${test.title}"`); + testsPassed++; + } else { + console.error(`Test ${index + 1}: FAILED - "${test.title}"`); + console.error(` Expected: totalTB=${test.expected.totalTB.toFixed(4)}, Q=${test.expected.quantity}, Check=${test.expected.needed_description_check}`); + console.error(` Actual: totalTB=${result.totalTB.toFixed(4)}, Q=${result.quantity}, Check=${result.needed_description_check}`); + testsFailed++; + } + }); + + console.log(`--- Unit Test Summary: ${testsPassed} Passed, ${testsFailed} Failed ---`); + } + + + // --- DOM Manipulation & Display --- + function addStyles() { + const css = ` + li.s-item, li.srp-results__item { + position: relative !important; overflow: visible !important; padding-bottom: 5px !important; + } + .cost-per-tb-info { + position: absolute; right: 10px; top: 40px; + background-color: rgba(0, 100, 0, 0.85); color: white; + padding: 4px 8px; border-radius: 5px; z-index: 1000; + font-size: 0.9em; font-weight: bold; text-align: center; + box-shadow: 1px 1px 3px rgba(0,0,0,0.5); transition: transform 0.2s ease; + } + .cost-per-tb-info:hover { transform: scale(1.05); } + .cost-per-tb-info small.check-desc-note { + color: #FFD700; /* Gold */ + font-weight: bold; + } + #costPerTbSortControl { + display: inline-block; margin-left: 20px; padding: 8px 12px; + border: 1px solid #ccc; border-radius: 4px; background-color: #f0f0f0; + vertical-align: middle; color: #333; cursor: pointer; + font-size: 14px; + } + #costPerTbSortControl:hover { background-color: #e9e9e9; } + #costPerTbSortControl input[type="checkbox"] { margin-right: 8px; vertical-align: middle; } + #costPerTbSortControl label { cursor: pointer; vertical-align: middle; color: #333; } + `; + try { + const styleSheet = document.createElement("style"); + styleSheet.type = "text/css"; + styleSheet.innerText = css; + document.head.appendChild(styleSheet); + } catch (e) { + console.error("Ebay Cost/TB: Failed to add styles:", e); + } + } + + function getItemSelector() { + if (document.querySelector('li.s-item')) return 'li.s-item'; + if (document.querySelector('li.srp-results__item')) return 'li.srp-results__item'; + if (document.querySelector('div.s-item')) return 'div.s-item'; + return 'li[class*="s-item"], div[class*="s-item"]'; + } + + function getMainListParent() { + if (mainListParentElement && document.body.contains(mainListParentElement)) { + return mainListParentElement; + } + mainListParentElement = document.querySelector('ul.srp-results, div#srp-river-results ul.srp-results'); + if (!mainListParentElement) { + mainListParentElement = document.querySelector('div#srp-river-results ul'); + } + if (!mainListParentElement) { + const itemSelector = getItemSelector(); + const containers = document.querySelectorAll('ul, div'); + for (const container of containers) { + if (container.querySelector(itemSelector)) { + const directChildren = Array.from(container.children).filter(child => child.matches(itemSelector)); + if (directChildren.length > 1) { + mainListParentElement = container; + break; + } + } + } + } + if (!mainListParentElement) { + console.error("Ebay Cost/TB: CRITICAL - Could not find main list parent element."); + } + return mainListParentElement; + } + + function storeOriginalOrder(itemsNodeList) { + if (originalOrderMap.size > 0 && itemsNodeList.length === originalOrderMap.size) { + return; + } + originalOrderMap.clear(); + itemsNodeList.forEach((itemElement, index) => { + let itemId = itemElement.id; + if (!itemId) { + const linkWithItemId = itemElement.querySelector('a[href*="/itm/"]'); + if (linkWithItemId && linkWithItemId.href) { + const match = linkWithItemId.href.match(/\/itm\/(\d+)/); + if (match && match[1]) { + itemId = "ebayitem_" + match[1]; + itemElement.id = itemId; + } + } + } + if (itemId && !originalOrderMap.has(itemId)) { + originalOrderMap.set(itemId, index); + } else if (!itemId) { + // console.warn("Ebay Cost/TB: Item found without a usable ID during storeOriginalOrder.", itemElement); + } + }); + // console.log("Ebay Cost/TB: Stored original order for", originalOrderMap.size, "items."); + } + + function processResults() { + const parent = getMainListParent(); + if (!parent) return; + + const itemSelector = getItemSelector(); + const itemsOnPage = parent.querySelectorAll(itemSelector + ':not(.cost-per-tb-processed-flag)'); + + itemsOnPage.forEach(item => { + const titleElement = item.querySelector('.s-item__title, .srp-results__title'); + const priceElement = item.querySelector('.s-item__price, .srp-results__price'); + + if (titleElement && priceElement) { + const title = titleElement.innerText; + const priceText = priceElement.innerText; + const price = parsePrice(priceText); + const parsedInfo = parseSizeAndQuantity(title); // Returns totalTB with more precision + const totalTB = parsedInfo.totalTB; + const needed_description_check = parsedInfo.needed_description_check; + + item.dataset.neededDescriptionCheck = String(needed_description_check); + + let displayElement = item.querySelector('.cost-per-tb-info'); + if (!displayElement) { + displayElement = document.createElement('div'); + displayElement.className = 'cost-per-tb-info'; + const imageContainer = item.querySelector('.s-item__image-section, .s-item__image'); + if (imageContainer && imageContainer.parentNode) { + imageContainer.parentNode.style.position = 'relative'; + imageContainer.parentNode.appendChild(displayElement); + } else { + item.appendChild(displayElement); + } + } + + if (price !== null && totalTB > 0) { + const costPerTB = price / totalTB; + if (needed_description_check) { + item.dataset.costPerTb = '9999999'; // Sentinel for sorting + // Display totalTB.toFixed(2) for consistency in UI + displayElement.innerHTML = `$${costPerTB.toFixed(2)}* / TB
(${totalTB.toFixed(2)} TB, Check Desc.)`; + } else { + item.dataset.costPerTb = costPerTB; + displayElement.innerHTML = `$${costPerTB.toFixed(2)} / TB
(${totalTB.toFixed(2)} TB)`; + } + } else { + item.dataset.costPerTb = '9999999'; + let ambiguousNote = needed_description_check || (parsedInfo.quantity > 1 && totalTB === 0) ? "(Check Desc.)" : "(Details N/A)"; + // Display totalTB.toFixed(2) if totalTB > 0 but price is null, else ambiguousNote + let tbDisplay = totalTB > 0 ? `(${totalTB.toFixed(2)} TB, Price N/A)` : ambiguousNote; + if (totalTB === 0 && parsedInfo.quantity > 1) tbDisplay = ambiguousNote; // Explicitly show check desc if lot with no size + else if (totalTB === 0) tbDisplay = "(Size N/A)"; + + + displayElement.innerHTML = `Details unclear
${tbDisplay}`; + } + + if(displayElement.querySelector('span')) displayElement.querySelector('span').style.fontSize = '0.8em'; + if(displayElement.querySelector('small')) displayElement.querySelector('small').style.fontSize = '0.7em'; + + } else { + item.dataset.costPerTb = '9999999'; + } + item.classList.add('cost-per-tb-processed-flag'); + item.dataset.costPerTbProcessed = 'true'; + }); + + const allItems = parent.querySelectorAll(itemSelector); + if (originalOrderMap.size === 0 || Math.abs(originalOrderMap.size - allItems.length) > 5) { + if (allItems.length > 0) storeOriginalOrder(allItems); + } + + if (isSorted) { + sortResults(); + } + } + + function sortResults() { + const parent = getMainListParent(); + if (!parent) return; + + const itemSelector = getItemSelector(); + let itemsArray = Array.from(parent.querySelectorAll(itemSelector)); + + if (originalOrderMap.size === 0 && itemsArray.length > 0) { + storeOriginalOrder(itemsArray); + } + + itemsArray.sort((a, b) => { + const costA = parseFloat(a.dataset.costPerTb || '9999999'); + const costB = parseFloat(b.dataset.costPerTb || '9999999'); + return costA - costB; + }); + + if (observer) observer.disconnect(); + itemsArray.forEach(item => parent.appendChild(item)); + if (observer && observerTargetNode) { + observer.observe(observerTargetNode, observerConfig); + } else if (observer && !observerTargetNode) { + setupObserver(); + } + // console.log("Ebay Cost/TB: Sorted by cost per TB."); + } + + function restoreOriginalOrder() { + const parent = getMainListParent(); + if (!parent) return; + if (originalOrderMap.size === 0) { + const itemSelector = getItemSelector(); + const currentItems = parent.querySelectorAll(itemSelector); + if (currentItems.length > 0) storeOriginalOrder(currentItems); + if (originalOrderMap.size === 0) return; + } + + let itemsToReorder = []; + originalOrderMap.forEach((originalIndex, itemId) => { + const itemElement = document.getElementById(itemId) || parent.querySelector(`[id="${itemId}"]`); + if (itemElement && parent.contains(itemElement)) { + itemsToReorder.push({ element: itemElement, originalIndex: originalIndex, id: itemId }); + } + }); + + const itemSelector = getItemSelector(); + const currentItemsInDOM = parent.querySelectorAll(itemSelector); + let newItemsCount = 0; + currentItemsInDOM.forEach(domItem => { + let itemId = domItem.id; + if (!itemId) { + const linkWithItemId = domItem.querySelector('a[href*="/itm/"]'); + if (linkWithItemId && linkWithItemId.href) { + const match = linkWithItemId.href.match(/\/itm\/(\d+)/); + if (match && match[1]) itemId = "ebayitem_" + match[1]; + } + } + if (itemId && !originalOrderMap.has(itemId)) { + itemsToReorder.push({ element: domItem, originalIndex: 900000 + (Array.from(domItem.parentNode.children).indexOf(domItem)), id: itemId || 'new_item_' + newItemsCount++ }); + } else if (!itemId && !itemsToReorder.some(entry => entry.element === domItem)) { + itemsToReorder.push({ element: domItem, originalIndex: 950000 + (Array.from(domItem.parentNode.children).indexOf(domItem)), id: 'no_id_item_' + newItemsCount++ }); + } + }); + + itemsToReorder.sort((a, b) => a.originalIndex - b.originalIndex); + + if (observer) observer.disconnect(); + itemsToReorder.forEach(entry => parent.appendChild(entry.element)); + if (observer && observerTargetNode) { + observer.observe(observerTargetNode, observerConfig); + } else if (observer && !observerTargetNode) { + setupObserver(); + } + // console.log("Ebay Cost/TB: Restored original order."); + } + + function handleSortToggle(event) { + const checkbox = event.currentTarget; + if (checkbox.checked) { + isSorted = true; + sortResults(); + } else { + isSorted = false; + restoreOriginalOrder(); + } + } + + function addSortControl() { + let controlsContainer = document.querySelector('.srp-controls__sort- σήμερα') || document.querySelector('.srp-controls__sort') || document.querySelector('div[class*="srp-controls__sort"]'); + if (!controlsContainer) { + controlsContainer = document.querySelector('.srp-sort, .srp-controls'); + if (controlsContainer && controlsContainer.firstChild && controlsContainer.firstChild.nodeName === "UL") { + controlsContainer = controlsContainer.firstChild; + } + } + + if (!controlsContainer) { + const fallbackContainer = document.querySelector('.srp-river-main') || document.querySelector('#srp-river-results') || document.body; + if (fallbackContainer) { + controlsContainer = document.createElement('div'); + controlsContainer.style.textAlign = 'center'; controlsContainer.style.margin = '10px 0'; + fallbackContainer.insertBefore(controlsContainer, fallbackContainer.firstChild); + } else { return; } + } + if (document.getElementById('costPerTbSortControlWrapper')) return; + + const controlWrapper = document.createElement('div'); + controlWrapper.id = 'costPerTbSortControlWrapper'; + controlWrapper.style.display = 'inline-block'; controlWrapper.style.marginLeft = '20px'; controlWrapper.style.verticalAlign = 'middle'; + + const controlDiv = document.createElement('div'); + controlDiv.id = 'costPerTbSortControl'; + + const checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; checkbox.id = 'costPerTbSortCheckbox'; checkbox.checked = isSorted; checkbox.style.verticalAlign = 'middle'; + + const label = document.createElement('label'); + label.htmlFor = 'costPerTbSortCheckbox'; label.innerText = 'Sort by $/TB'; label.style.verticalAlign = 'middle'; label.style.marginLeft = '5px'; + + controlDiv.appendChild(checkbox); controlDiv.appendChild(label); + controlWrapper.appendChild(controlDiv); + + controlDiv.addEventListener('click', (e) => { + if (e.target !== checkbox) { + checkbox.checked = !checkbox.checked; + checkbox.dispatchEvent(new Event('change', { bubbles: true })); + } + }); + checkbox.addEventListener('change', handleSortToggle); + + if (controlsContainer.nextSibling) { + controlsContainer.parentNode.insertBefore(controlWrapper, controlsContainer.nextSibling); + } else { + controlsContainer.parentNode.appendChild(controlWrapper); + } + } + + function setupObserver() { + observerTargetNode = getMainListParent(); + if (!observerTargetNode) { + observerTargetNode = document.querySelector('#srp-river-results') || document.body; + } + if (!observerTargetNode) return; + + const itemSelector = getItemSelector(); + const callback = function(mutationsList, obs) { + let needsProcessing = false; + for(const mutation of mutationsList) { + if (mutation.type === 'childList') { + if (mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach(node => { + if (node.nodeType === 1 && !node.classList.contains('cost-per-tb-info') && (node.matches && node.matches(itemSelector) || (node.querySelector && node.querySelector(itemSelector)))) { + needsProcessing = true; + } + }); + } + if (mutation.removedNodes.length > 0 && !isSorted) { + mutation.removedNodes.forEach(node => { + if (node.nodeType === 1 && node.matches && node.matches(itemSelector)) { + needsProcessing = true; originalOrderMap.clear(); + } + }); + } + } + if (!document.getElementById('costPerTbSortControlWrapper')) { + const sortControlsArea = document.querySelector('.srp-controls__sort, div[class*="srp-controls__sort"], .srp-sort, .srp-controls'); + if (sortControlsArea) needsProcessing = true; + } + } + if (needsProcessing) { + clearTimeout(processTimer); + processTimer = setTimeout(() => { + processResults(); + addSortControl(); + }, 750); + } + }; + if (observer) observer.disconnect(); + observer = new MutationObserver(callback); + observer.observe(observerTargetNode, observerConfig); + // console.log("Ebay Cost/TB: MutationObserver set up on:", observerTargetNode); + } + + function init() { + console.log("Ebay Cost/TB V2.6 (for Tampermonkey @require) starting..."); + if (DEBUG_MODE) { + runUnitTests(); + } + addStyles(); + setTimeout(() => { + processResults(); + addSortControl(); + setupObserver(); + }, 2000); + } + + if (document.readyState === "complete" || document.readyState === "interactive") { + init(); + } else { + window.addEventListener('load', init); + } +})();