您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Shows true prices including shipping and variants on AliExpress, since sellers often misleadingly put accessory variants as the primary price, not the advertised item.
当前为
// ==UserScript== // @name AliExpress Real Price // @namespace https://github.com/joshwand/aliexpress-real-price-userscript // @version 1.0.0 // @description Shows true prices including shipping and variants on AliExpress, since sellers often misleadingly put accessory variants as the primary price, not the advertised item. // @author Josh Wand // @license GPL-3.0-or-later // @copyright 2025 Josh Wand // @match *://*.aliexpress.com/* // @match *://*.aliexpress.us/* // @grant GM.xmlHttpRequest // @grant GM_addStyle // @grant GM.getValue // @grant GM.setValue // @grant GM.cookie // @connect aliexpress.us // @connect aliexpress.com // @grant GM_listValues // @grant GM_addStyle // @grant GM_xmlhttpRequest // @grant unsafeWindow // ==/UserScript== (function() { 'use strict'; // --- Global Cache Disable Flag --- let isCacheDisabled = false; // Debug logging utility const DEBUG = true; const log = (...args) => { if (DEBUG) { const productId = args.find(arg => typeof arg === 'object' && arg?.productId)?.productId || ''; console.log(`[AliExpress Real Price${productId ? ` - ID:${productId}` : ''}]`, ...args); } }; // Rate limiter for API calls class RateLimiter { constructor(maxRequests = 2, timeWindow = 1000) { this.maxRequests = maxRequests; this.timeWindow = timeWindow; this.requests = []; this.backoffTime = 1000; // Start with 1 second backoff this.maxBackoffTime = 32000; // Max backoff of 32 seconds } async waitForSlot() { // Remove old requests outside the time window const now = Date.now(); this.requests = this.requests.filter(time => now - time < this.timeWindow); // If we have capacity, add the request if (this.requests.length < this.maxRequests) { this.requests.push(now); return; } // Wait for the oldest request to expire const oldestRequest = this.requests[0]; const waitTime = this.timeWindow - (now - oldestRequest); await new Promise(resolve => setTimeout(resolve, waitTime)); return this.waitForSlot(); } async executeWithBackoff(fn) { while (true) { try { await this.waitForSlot(); const result = await fn(); this.backoffTime = 1000; // Reset backoff on success return result; } catch (error) { if (error.message?.includes('FAIL_SYS_ILLEGAL_ACCESS')) { log(`Rate limit exceeded, backing off for ${this.backoffTime}ms`); await new Promise(resolve => setTimeout(resolve, this.backoffTime)); this.backoffTime = Math.min(this.backoffTime * 2, this.maxBackoffTime); continue; } throw error; } } } } // Loading Manager for global progress class LoadingManager { constructor() { this.totalItems = 0; this.completedItems = 0; this.createElements(); this.addStyles(); // Add styles for the elements } createElements() { // Create main container for status text and clear button this.container = document.createElement('div'); this.container.className = 'ali-real-price-status-container'; // this.container.title = 'AliExpress Real Price UserScript'; // Create icon container for collapsed state this.iconContainer = document.createElement('div'); this.iconContainer.className = 'ali-real-price-icon'; this.iconContainer.innerHTML = '🐟'; // Fish icon - because the prices are fishy this.iconContainer.title = 'Hmm, something is fishy here...'; this.container.appendChild(this.iconContainer); // Create expandable content container this.expandableContent = document.createElement('div'); this.expandableContent.className = 'ali-real-price-expandable-content'; // Create status text this.statusText = document.createElement('div'); this.statusText.className = 'ali-real-price-status'; this.expandableContent.appendChild(this.statusText); // Create settings container with disclosure arrow this.settingsContainer = document.createElement('div'); this.settingsContainer.className = 'ali-real-price-settings-container'; // Add disclosure arrow this.disclosureArrow = document.createElement('span'); this.disclosureArrow.className = 'ali-real-price-disclosure-arrow collapsed'; // Create a separate text node for the arrow symbol this.arrowSymbol = document.createTextNode('▶'); this.disclosureArrow.appendChild(this.arrowSymbol); this.disclosureArrow.title = 'Advanced Options'; // Create custom tooltip this.tooltip = document.createElement('div'); this.tooltip.className = 'ali-real-price-tooltip'; this.tooltip.textContent = 'Advanced Options'; this.disclosureArrow.appendChild(this.tooltip); // Restore click handler this.disclosureArrow.onclick = () => { const container = this.settingsContainer; const isExpanding = !container.classList.contains('expanded'); container.classList.toggle('expanded'); this.disclosureArrow.classList.toggle('collapsed'); this.arrowSymbol.nodeValue = isExpanding ? '▼' : '▶'; }; // Remove default title to prevent both tooltips this.disclosureArrow.removeAttribute('title'); this.expandableContent.appendChild(this.disclosureArrow); // Settings content (initially hidden) this.settingsContent = document.createElement('div'); this.settingsContent.className = 'ali-real-price-settings-content'; // --- Create Clear Cache button --- this.clearCacheButton = document.createElement('span'); this.clearCacheButton.className = 'ali-real-price-clear-cache'; this.clearCacheButton.textContent = 'Clear Cache'; this.clearCacheButton.onclick = async () => { await clearCacheAndReload(); }; this.settingsContent.appendChild(this.clearCacheButton); // --- Create Disable Cache Checkbox --- this.disableCacheContainer = document.createElement('div'); this.disableCacheContainer.className = 'ali-real-price-disable-cache-container'; this.disableCacheCheckbox = document.createElement('input'); this.disableCacheCheckbox.type = 'checkbox'; this.disableCacheCheckbox.id = 'ali-real-price-disable-cache-checkbox'; this.disableCacheCheckbox.className = 'ali-real-price-disable-cache-checkbox'; log(`[LoadingManager.createElements] Setting checkbox state based on isCacheDisabled: ${isCacheDisabled}`); this.disableCacheCheckbox.checked = isCacheDisabled; this.disableCacheCheckbox.addEventListener('change', handleDisableCacheChange); this.disableCacheLabel = document.createElement('label'); this.disableCacheLabel.htmlFor = 'ali-real-price-disable-cache-checkbox'; this.disableCacheLabel.textContent = 'Disable Cache'; this.disableCacheLabel.className = 'ali-real-price-disable-cache-label'; this.disableCacheContainer.appendChild(this.disableCacheCheckbox); this.disableCacheContainer.appendChild(this.disableCacheLabel); this.settingsContent.appendChild(this.disableCacheContainer); // Add settings content to settings container this.settingsContainer.appendChild(this.settingsContent); this.expandableContent.appendChild(this.settingsContainer); // Add expandable content to main container this.container.appendChild(this.expandableContent); document.body.appendChild(this.container); // Add hover behavior this.container.addEventListener('mouseenter', () => { this.container.classList.add('expanded'); }); this.container.addEventListener('mouseleave', () => { // Only collapse if we're done loading AND the mouse isn't in the container if (this.completedItems >= this.totalItems && !this.container.matches(':hover')) { this.container.classList.remove('expanded'); // Also collapse settings if expanded this.settingsContainer.classList.remove('expanded'); this.disclosureArrow.classList.add('collapsed'); this.arrowSymbol.nodeValue = '▶'; } }); } // Add CSS styles addStyles() { const existingStyle = document.getElementById('ali-real-price-styles'); if (existingStyle) { existingStyle.remove(); } const styleElement = document.createElement('style'); styleElement.id = 'ali-real-price-styles'; styleElement.textContent = ` .ali-real-price-status-container { position: fixed; top: 10px; right: 10px; z-index: 99999; background-color: rgba(0, 0, 0, 0.8); color: white; padding: 8px; border-radius: 4px; box-shadow: 0 2px 5px rgba(0,0,0,0.3); transition: all 0.3s ease; cursor: pointer; display: flex; /* Use flexbox by default */ align-items: flex-start; /* Align items to top */ gap: 8px; /* Space between icon and content */ visibility: hidden; /* Use visibility instead of display: none */ opacity: 0; } .ali-real-price-status-container.visible { visibility: visible; opacity: 1; } .ali-real-price-icon { font-size: 16px; flex-shrink: 0; /* Prevent icon from shrinking */ } .ali-real-price-expandable-content { display: none; flex-grow: 1; /* Allow content to grow */ min-width: 0; /* Allow content to shrink if needed */ } .ali-real-price-status-container.expanded .ali-real-price-expandable-content { display: block; } .ali-real-price-status { font-size: 12px; margin-right: 10px; display: inline-block; } .ali-real-price-disclosure-arrow { font-size: 10px; margin-left: 5px; cursor: pointer; color: #666; position: relative; } .ali-real-price-tooltip { position: absolute; background: rgba(0, 0, 0, 0.8); color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap; pointer-events: none; opacity: 0; transition: opacity 0.15s; top: 100%; margin-top: 4px; right: 0; } /* Show tooltip on hover when collapsed (default state) */ .ali-real-price-disclosure-arrow.collapsed:hover .ali-real-price-tooltip { opacity: 1; } /* Hide tooltip when expanded */ .ali-real-price-disclosure-arrow:hover .ali-real-price-tooltip { opacity: 0; } /* Add a small arrow at the bottom of the tooltip */ .ali-real-price-tooltip:after { content: ''; position: absolute; top: -4px; right: 2px; border-width: 0 4px 4px 4px; border-style: solid; border-color: transparent transparent rgba(0, 0, 0, 0.8) transparent; } .ali-real-price-settings-container { margin-top: 5px; } .ali-real-price-settings-content { display: none; margin-top: 5px; padding-top: 5px; border-top: 1px solid rgba(255, 255, 255, 0.1); } .ali-real-price-settings-container.expanded .ali-real-price-settings-content { display: block; } .ali-real-price-clear-cache { color: #ffc107; font-size: 11px; cursor: pointer; text-decoration: underline; display: block; margin-bottom: 5px; } .ali-real-price-clear-cache:hover { color: #ffa000; } .ali-real-price-disable-cache-container { display: flex; align-items: center; margin-top: 5px; } .ali-real-price-disable-cache-checkbox { margin: 0 5px 0 0; cursor: pointer; } .ali-real-price-disable-cache-label { font-size: 11px; color: #ccc; cursor: pointer; user-select: none; } `; document.head.appendChild(styleElement); } startLoading(totalItems) { this.totalItems = totalItems; this.updateProgress(); this.container.classList.add('visible'); this.container.classList.remove('expanded'); } itemComplete() { log(`[itemComplete] Before increment: completed=${this.completedItems}, total=${this.totalItems}`); this.completedItems++; // If completedItems exceeds totalItems, update totalItems if (this.completedItems > this.totalItems) { this.totalItems = this.completedItems; } log(`[itemComplete] After increment: completed=${this.completedItems}, total=${this.totalItems}`); this.updateProgress(); if (this.completedItems >= this.totalItems) { // When complete, only collapse if mouse isn't in the container if (!this.container.matches(':hover')) { this.container.classList.remove('expanded'); } } } updateProgress() { log(`[updateProgress] Updating text: completed=${this.completedItems}, total=${this.totalItems}`); this.statusText.textContent = `Loading prices: ${this.completedItems}/${this.totalItems}`; // Show expanded state while loading if (this.completedItems < this.totalItems) { this.container.classList.add('expanded'); } } } // Global loading manager instance const loadingManager = new LoadingManager(); // Global rate limiter instance const rateLimiter = new RateLimiter(); // Global cache instance let globalCache; // Function to clear cache and reload async function clearCacheAndReload() { try { if (globalCache) { log('Clearing cache...'); await globalCache.clear(); alert('AliExpress Real Price cache cleared. Reloading page.'); window.location.reload(); } else { log('Cache instance not found'); alert('Cache instance not found.'); } } catch (error) { log('Error clearing cache:', error); alert('Error clearing cache. See console for details.'); } } log('Script starting...'); // MD5 implementation for sign generation function md5(string) { function cmn(q, a, b, x, s, t) { a = add32(add32(a, q), add32(x, t)); return add32((a << s) | (a >>> (32 - s)), b); } function ff(a, b, c, d, x, s, t) { return cmn((b & c) | ((~b) & d), a, b, x, s, t); } function gg(a, b, c, d, x, s, t) { return cmn((b & d) | (c & (~d)), a, b, x, s, t); } function hh(a, b, c, d, x, s, t) { return cmn(b ^ c ^ d, a, b, x, s, t); } function ii(a, b, c, d, x, s, t) { return cmn(c ^ (b | (~d)), a, b, x, s, t); } function md5cycle(x, k) { let a = x[0], b = x[1], c = x[2], d = x[3]; a = ff(a, b, c, d, k[0], 7, -680876936); d = ff(d, a, b, c, k[1], 12, -389564586); c = ff(c, d, a, b, k[2], 17, 606105819); b = ff(b, c, d, a, k[3], 22, -1044525330); a = ff(a, b, c, d, k[4], 7, -176418897); d = ff(d, a, b, c, k[5], 12, 1200080426); c = ff(c, d, a, b, k[6], 17, -1473231341); b = ff(b, c, d, a, k[7], 22, -45705983); a = ff(a, b, c, d, k[8], 7, 1770035416); d = ff(d, a, b, c, k[9], 12, -1958414417); c = ff(c, d, a, b, k[10], 17, -42063); b = ff(b, c, d, a, k[11], 22, -1990404162); a = ff(a, b, c, d, k[12], 7, 1804603682); d = ff(d, a, b, c, k[13], 12, -40341101); c = ff(c, d, a, b, k[14], 17, -1502002290); b = ff(b, c, d, a, k[15], 22, 1236535329); a = gg(a, b, c, d, k[1], 5, -165796510); d = gg(d, a, b, c, k[6], 9, -1069501632); c = gg(c, d, a, b, k[11], 14, 643717713); b = gg(b, c, d, a, k[0], 20, -373897302); a = gg(a, b, c, d, k[5], 5, -701558691); d = gg(d, a, b, c, k[10], 9, 38016083); c = gg(c, d, a, b, k[15], 14, -660478335); b = gg(b, c, d, a, k[4], 20, -405537848); a = gg(a, b, c, d, k[9], 5, 568446438); d = gg(d, a, b, c, k[14], 9, -1019803690); c = gg(c, d, a, b, k[3], 14, -187363961); b = gg(b, c, d, a, k[8], 20, 1163531501); a = gg(a, b, c, d, k[13], 5, -1444681467); d = gg(d, a, b, c, k[2], 9, -51403784); c = gg(c, d, a, b, k[7], 14, 1735328473); b = gg(b, c, d, a, k[12], 20, -1926607734); a = hh(a, b, c, d, k[5], 4, -378558); d = hh(d, a, b, c, k[8], 11, -2022574463); c = hh(c, d, a, b, k[11], 16, 1839030562); b = hh(b, c, d, a, k[14], 23, -35309556); a = hh(a, b, c, d, k[1], 4, -1530992060); d = hh(d, a, b, c, k[4], 11, 1272893353); c = hh(c, d, a, b, k[7], 16, -155497632); b = hh(b, c, d, a, k[10], 23, -1094730640); a = hh(a, b, c, d, k[13], 4, 681279174); d = hh(d, a, b, c, k[0], 11, -358537222); c = hh(c, d, a, b, k[3], 16, -722521979); b = hh(b, c, d, a, k[6], 23, 76029189); a = hh(a, b, c, d, k[9], 4, -640364487); d = hh(d, a, b, c, k[12], 11, -421815835); c = hh(c, d, a, b, k[15], 16, 530742520); b = hh(b, c, d, a, k[2], 23, -995338651); a = ii(a, b, c, d, k[0], 6, -198630844); d = ii(d, a, b, c, k[7], 10, 1126891415); c = ii(c, d, a, b, k[14], 15, -1416354905); b = ii(b, c, d, a, k[5], 21, -57434055); a = ii(a, b, c, d, k[12], 6, 1700485571); d = ii(d, a, b, c, k[3], 10, -1894986606); c = ii(c, d, a, b, k[10], 15, -1051523); b = ii(b, c, d, a, k[1], 21, -2054922799); a = ii(a, b, c, d, k[8], 6, 1873313359); d = ii(d, a, b, c, k[15], 10, -30611744); c = ii(c, d, a, b, k[6], 15, -1560198380); b = ii(b, c, d, a, k[13], 21, 1309151649); a = ii(a, b, c, d, k[4], 6, -145523070); d = ii(d, a, b, c, k[11], 10, -1120210379); c = ii(c, d, a, b, k[2], 15, 718787259); b = ii(b, c, d, a, k[9], 21, -343485551); x[0] = add32(a, x[0]); x[1] = add32(b, x[1]); x[2] = add32(c, x[2]); x[3] = add32(d, x[3]); } function md5blk(s) { let i, md5blks = []; for (i = 0; i < 64; i += 4) { md5blks[i >> 2] = s.charCodeAt(i) + (s.charCodeAt(i + 1) << 8) + (s.charCodeAt(i + 2) << 16) + (s.charCodeAt(i + 3) << 24); } return md5blks; } function md5blk_array(a) { let i, md5blks = []; for (i = 0; i < 64; i += 4) { md5blks[i >> 2] = a[i] + (a[i + 1] << 8) + (a[i + 2] << 16) + (a[i + 3] << 24); } return md5blks; } function md51(s) { let n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; for (i = 64; i <= s.length; i += 64) { md5cycle(state, md5blk(s.substring(i - 64, i))); } s = s.substring(i - 64); let tail = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; for (i = 0; i < s.length; i++) { tail[i >> 2] |= s.charCodeAt(i) << ((i % 4) << 3); } tail[i >> 2] |= 0x80 << ((i % 4) << 3); if (i > 55) { md5cycle(state, tail); for (i = 0; i < 16; i++) tail[i] = 0; } tail[14] = n * 8; md5cycle(state, tail); return state; } function md51_array(a) { let n = a.length, state = [1732584193, -271733879, -1732584194, 271733878], i; for (i = 64; i <= a.length; i += 64) { md5cycle(state, md5blk_array(a.subarray(i - 64, i))); } a = (i - 64) < a.length ? a.subarray(i - 64) : new Uint8Array(0); let tail = new Uint8Array(64), len = a.length; for (i = 0; i < len; i++) { tail[i] = a[i]; } tail[len] = 0x80; if (len > 55) { md5cycle(state, tail.subarray(0, 64)); for (i = 0; i < 64; i++) tail[i] = 0; } for (i = 0; i < 8; i++) tail[56 + i] = (n * 8) >>> (i * 8) & 0xff; md5cycle(state, tail); return state; } function hex_chr(n) { return '0123456789abcdef'.charAt(n); } function rhex(n) { let s = '', j = 0; for (; j < 4; j++) { s += hex_chr((n >> (j * 8 + 4)) & 0x0F) + hex_chr((n >> (j * 8)) & 0x0F); } return s; } function hex(x) { for (let i = 0; i < x.length; i++) { x[i] = rhex(x[i]); } return x.join(''); } function add32(a, b) { return (a + b) & 0xFFFFFFFF; } if (typeof string !== 'string') string = ''; let result; if (/[\x80-\xFF]/.test(string)) { result = hex(md51(unescape(encodeURIComponent(string)))); } else { result = hex(md51(string)); } return result; } // CSS Styles const STYLES = ` .ali-real-price-range { font-weight: bold; color: #333; } .ali-real-price-global-loading { position: fixed; top: 0; left: 0; right: 0; background: #2196F3; height: 3px; z-index: 10000; transition: width 0.3s ease-out; } .ali-real-price-global-status { position: fixed; top: 3px; right: 10px; background: rgba(33, 150, 243, 0.9); color: white; padding: 8px 12px; border-radius: 0 0 4px 4px; font-size: 12px; z-index: 10000; transition: opacity 0.3s ease-out; } .ali-real-price-median-indicator { color: #2196F3; margin-left: 4px; } .ali-real-price-distribution { height: 4px; background: #eee; margin: 2px 0; position: relative; } .ali-real-price-distribution-marker { position: absolute; width: 2px; height: 8px; background: #2196F3; top: -2px; } .ali-real-price-popup { position: absolute; z-index: 1000; background: white; border-radius: 4px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 12px; width: 280px; font-size: 12px; line-height: 1.5; } .ali-real-price-popup ul { list-style: none; padding: 0; margin: 0 0 10px 0; } .ali-real-price-popup li { padding: 4px 0; border-bottom: 1px solid #f5f5f5; } .ali-real-price-popup li.median-match { font-weight: bold; color: #2196F3; } .ali-real-price-popup .free-shipping-threshold { font-style: italic; color: #4CAF50; margin-top: 8px; } `; // Cache configuration const CACHE_CONFIG = { variants: { duration: 86400000, maxEntries: 100 }, // 24 hours shipping: { duration: 86400000, maxEntries: 100 }, // 24 hours context: { duration: 86400000, maxEntries: 10 } // 24 hours }; // DOM Selectors const SELECTORS = { productCard: [ '.search-card-item', // Main search results '.lq_b.io_it', // Alternative class combination '.comet-v2-list-item', // Keep some old selectors as fallback '.comet-v2-product-card', 'div[class*="ProductItem"]', 'div[class*="product-card"]', 'div[class*="card-out-wrapper"]' ].join(','), price: [ '.lq_j3', // Main price container '.lq_et', // Price wrapper '.l5_k6', '.U-S0j', 'div[class*="price-current"]', 'div[class*="PriceText"]', 'div[class*="productPrice"]', 'div[class*="price"]', // More generic fallbacks 'span[class*="price"]', '[data-price]', // Data attribute '[data-product-price]' ].join(','), title: [ '.lq_jl', // Product title '.lq_ae h3' // Title wrapper ].join(','), shipping: [ '.lq_lv', // Shipping info '.mi_l6[title*="shipping"]' // Shipping text ].join(','), discount: [ '.lq_eu', // Discount percentage '.lq_j4' // Original price ].join(','), relatedItems: [ '.pdp-recommend-item', '.recommend-item', '.bundle-item', 'div[class*="RecommendItem"]' ].join(','), variants: [ '.sku-property-item', '.sku-property-text', '.sku-property-image', 'div[class*="SkuItem"]' ].join(',') }; // Utility functions const utils = { extractProductId(element) { // Try multiple methods to find the product ID const methods = [ // Method 1: New URL pattern (from your example) () => { const link = element.getAttribute('href'); if (!link) return null; const match = link.match(/item\/(\d+)\.html/); return match ? match[1] : null; }, // Method 2: Legacy pattern () => { const link = element.querySelector('a[href*="/item/"]'); if (!link) return null; const match = link.href.match(/\/(\d+)\.html/); return match ? match[1] : null; }, // Method 3: Data attribute () => { return element.getAttribute('data-product-id') || element.getAttribute('data-item-id') || element.getAttribute('data-id'); } ]; // Try each method until we find a product ID for (const method of methods) { const id = method(); if (id) { log('Found product ID:', id, { productId: id }); return id; } } log('Could not find product ID for element:', element); return null; }, formatPrice(value, currency = 'USD') { return new Intl.NumberFormat('en-US', { style: 'currency', currency: currency }).format(value); }, delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, // Get cookie by name getCookie(name) { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i].trim(); if (cookie.startsWith(name + '=')) { return cookie.substring(name.length + 1); } } return ''; }, // Generate sign for API requests generateSign(token, timestamp, appKey, data) { const signStr = `${token}&${timestamp}&${appKey}&${data}`; return md5(signStr); } }; // Cache Manager class CacheManager { constructor() { this.cache = new Map(); // Load immediately, respecting the flag set during init this.loadFromStorage(); } async loadFromStorage() { if (isCacheDisabled) { log('Cache is disabled, skipping load from storage.'); this.cache.clear(); return; } try { const storedCache = await GM.getValue('aliexpress_cache', null); if (storedCache) { const parsed = JSON.parse(storedCache); // Only load non-expired entries Object.entries(parsed).forEach(([key, entry]) => { if (Date.now() <= entry.expiresAt) { this.cache.set(key, entry); } }); log('Loaded cache from storage:', this.cache.size, 'entries'); } } catch (error) { log('Error loading cache from storage:', error); } } async saveToStorage() { if (isCacheDisabled) { // log('Cache is disabled, skipping save to storage.'); // Maybe too noisy return; // Don't save if cache is disabled } try { // Convert Map to object for storage const cacheObj = {}; this.cache.forEach((value, key) => { cacheObj[key] = value; }); await GM.setValue('aliexpress_cache', JSON.stringify(cacheObj)); log('Saved cache to storage:', Object.keys(cacheObj).length, 'entries'); } catch (error) { log('Error saving cache to storage:', error); } } async get(key) { if (isCacheDisabled) return null; // Bypass cache if disabled const entry = this.cache.get(key); if (!entry) return null; if (Date.now() > entry.expiresAt) { this.cache.delete(key); await this.saveToStorage(); return null; } return entry.data; } async set(key, data, config) { if (isCacheDisabled) return; // Bypass cache if disabled // Check if config is provided, otherwise use a default or skip if (!config || !config.maxEntries || !config.duration) { log('Cache config missing for key:', key, ' Using default or skipping.'); // Define a default config or return if you don't want to cache without specific config config = CACHE_CONFIG.variants; // Example: Default to variants config // Or simply return if caching requires explicit config // return; } if (this.cache.size >= config.maxEntries) { const oldestKey = this.cache.keys().next().value; this.cache.delete(oldestKey); } this.cache.set(key, { data, timestamp: Date.now(), expiresAt: Date.now() + config.duration }); await this.saveToStorage(); } async clear() { this.cache.clear(); // Always allow clearing storage, even if cache is currently disabled await GM.setValue('aliexpress_cache', '{}'); log('Cache cleared'); } } // Data Manager class DataManager { constructor() { this.cache = new CacheManager(); this.tokenInitialized = false; } // Initialize token by making a simple request to AliExpress async initializeToken() { if (this.tokenInitialized) { return; } log('Initializing token...'); // Check if token already exists const token = utils.getCookie('_m_h5_tk'); if (token) { log('Token already exists:', token.split('_')[0]); this.tokenInitialized = true; return; } // Make a request to the AliExpress homepage to get the token try { log('Making request to initialize token...'); return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: 'https://www.aliexpress.us/', headers: { 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'accept-language': 'en-US,en;q=0.9', 'cache-control': 'no-cache', 'pragma': 'no-cache', 'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"macOS"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' }, onload: (response) => { // Check if token was set in cookies const newToken = utils.getCookie('_m_h5_tk'); if (newToken) { log('Token initialized successfully:', newToken.split('_')[0]); this.tokenInitialized = true; resolve(); } else { log('Failed to initialize token'); // Continue anyway resolve(); } }, onerror: (error) => { log('Error initializing token:', error); // Continue anyway resolve(); } }); }); } catch (error) { log('Error in token initialization:', error); // Continue anyway } } async fetchProductData(productId) { log('Fetching product data for ID:', productId, { productId }); const cacheKey = `product_${productId}`; const cachedData = await this.cache.get(cacheKey); if (cachedData) { log(`[ARP_EnhanceFlow] [fetchProductData] Found cached data for product: ${productId}. Returning it.`, { productId }); return cachedData; } try { // First get quick data from card const card = document.querySelector(`a[href*="${productId}"]`); let productData = null; if (card) { log('Found product card, extracting basic info'); const title = card.querySelector(SELECTORS.title)?.textContent?.trim() || ''; const priceContainer = card.querySelector(SELECTORS.price); const priceInfo = this.extractPriceFromElement(priceContainer); const shippingElement = card.querySelector(SELECTORS.shipping); const shippingInfo = this.extractShippingFromElement(shippingElement); const discountElement = card.querySelector(SELECTORS.discount); const discountInfo = this.extractDiscountFromElement(discountElement); productData = { productId, title, variants: [{ id: 'default', name: 'Default', price: { value: priceInfo.original || priceInfo.current, formattedPrice: utils.formatPrice(priceInfo.original || priceInfo.current), discountedValue: priceInfo.current, discountedFormattedPrice: utils.formatPrice(priceInfo.current), discount: discountInfo.percentage || '' }, shipping: { cost: shippingInfo.cost || 0, formattedPrice: utils.formatPrice(shippingInfo.cost || 0), freeThreshold: shippingInfo.freeThreshold }, stock: 999, isMainProduct: true }] }; } // // Try to fetch full product data using the direct Taobao API // try { // log('Trying direct Aliexpress API call'); // const apiData = await this.fetchDirectAliExpressAPI(productId); // if (apiData) { // log('Successfully fetched data from direct Aliexpress API', { apiData}); // await this.cache.set(cacheKey, apiData, CACHE_CONFIG.variants); // return apiData; // } // } catch (directApiError) { // log(`Direct Taobao API call failed for productId ${productId}:`, directApiError); // } // Try to fetch full product data using the API try { // Get token from cookies const token = utils.getCookie('_m_h5_tk')?.split('_')[0]; if (!token) { log(`No token found in cookies, trying to fetch product page data for productId ${productId}`); const pageData = await this.fetchDataFromProductPage(productId); if (pageData) { await this.cache.set(cacheKey, pageData, CACHE_CONFIG.variants); return pageData; } if (productData) { await this.cache.set(cacheKey, productData, CACHE_CONFIG.variants); return productData; } throw new Error('No token found and no fallback data available'); } log('Found token:', token); // Prepare API request const timestamp = Date.now(); const appKey = '12574478'; const apiVersion = '1.0'; // Construct the request data object const requestData = { productId, _lang: 'en_US', _currency: 'USD', country: 'US', province: '922867650000000000', city: '922867656497000000', channel: '', pdp_ext_f: '{"order":"10","eval":"1"}', sourceType: '', clientType: 'pc', ext: JSON.stringify({ site: 'usa', crawler: false, 'x-m-biz-bx-region': '', signedIn: true, host: 'www.aliexpress.us' }) }; // Convert request data to JSON string const dataStr = JSON.stringify(requestData); // Generate sign const sign = utils.generateSign(token, timestamp, appKey, dataStr); log('Generated sign:', sign); // Construct the API URL with all parameters const baseUrl = 'https://acs.aliexpress.us/h5/mtop.aliexpress.pdp.pc.query/1.0/'; const params = new URLSearchParams({ jsv: '2.5.1', appKey, t: timestamp, sign, api: 'mtop.aliexpress.pdp.pc.query', type: 'originaljsonp', v: apiVersion, timeout: '15000', dataType: 'originaljsonp', callback: 'mtopjsonp1', data: dataStr }); const apiUrl = `${baseUrl}?${params.toString()}`; log('Fetching from API URL:', apiUrl); return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: apiUrl, headers: { 'accept': '*/*', 'accept-language': 'en-US,en;q=0.9', 'cache-control': 'no-cache', 'pragma': 'no-cache', 'referer': 'https://www.aliexpress.us/', 'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"macOS"', 'sec-fetch-dest': 'script', 'sec-fetch-mode': 'no-cors', 'sec-fetch-site': 'same-site', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' }, withCredentials: true, // Important: send cookies with the request onload: (response) => { try { // log(`Received raw API response: ${response.responseText}`); // Extract JSON from JSONP response const jsonMatch = response.responseText.match(/mtopjsonp1\((.*)\)/); if (!jsonMatch) { throw new Error('Invalid JSONP response format'); } const apiResponseData = JSON.parse(jsonMatch[1]); log('Parsed API response:', {productId, apiResponseData}); if (apiResponseData.ret && apiResponseData.ret[0]?.startsWith('FAIL_')) { log('API returned error:', apiResponseData.ret[0]); if (apiResponseData.ret[0].includes('FAIL_SYS_ILLEGAL_ACCESS')) { throw new Error(apiResponseData.ret[0]); // This will trigger backoff } // Try to fetch product page data as fallback this.fetchDataFromProductPage(productId).then(pageData => { if (pageData) { this.cache.set(cacheKey, pageData, CACHE_CONFIG.variants); resolve(pageData); return; } if (productData) { this.cache.set(cacheKey, productData, CACHE_CONFIG.variants); resolve(productData); } else { reject(new Error(`API Error: ${apiResponseData.ret[0]}`)); } }).catch(err => { log('Error fetching product page data:', err); if (productData) { this.cache.set(cacheKey, productData, CACHE_CONFIG.variants); resolve(productData); } else { reject(new Error(`API Error: ${apiResponseData.ret[0]}`)); } }); return; } const fullProductData = this.parseProductData(apiResponseData); if (productData) { fullProductData.variants = fullProductData.variants.length > 0 ? fullProductData.variants : productData.variants; fullProductData.title = fullProductData.title || productData.title; } this.cache.set(cacheKey, fullProductData, CACHE_CONFIG.variants); resolve(fullProductData); } catch (error) { log('Error processing API response:', error); // Try to fetch product page data as fallback this.fetchDataFromProductPage(productId).then(pageData => { if (pageData) { this.cache.set(cacheKey, pageData, CACHE_CONFIG.variants); resolve(pageData); return; } if (productData) { this.cache.set(cacheKey, productData, CACHE_CONFIG.variants); resolve(productData); } else { reject(error); } }).catch(err => { log('Error fetching product page data:', err); if (productData) { this.cache.set(cacheKey, productData, CACHE_CONFIG.variants); resolve(productData); } else { reject(error); } }); } }, onerror: (error) => { log('Error fetching API data:', error); // Try to fetch product page data as fallback this.fetchDataFromProductPage(productId).then(pageData => { if (pageData) { this.cache.set(cacheKey, pageData, CACHE_CONFIG.variants); resolve(pageData); return; } if (productData) { this.cache.set(cacheKey, productData, CACHE_CONFIG.variants); resolve(productData); } else { reject(error); } }).catch(err => { log('Error fetching product page data:', err); if (productData) { this.cache.set(cacheKey, productData, CACHE_CONFIG.variants); resolve(productData); } else { reject(error); } }); } }); }); } catch (apiError) { log('API request failed, trying to fetch product page data:', apiError); try { const pageData = await this.fetchDataFromProductPage(productId); if (pageData) { this.cache.set(cacheKey, pageData, CACHE_CONFIG.variants); return pageData; } } catch (pageError) { log('Error fetching product page data:', pageError); } if (productData) { this.cache.set(cacheKey, productData, CACHE_CONFIG.variants); return productData; } throw apiError; } } catch (error) { log('Error in fetchProductData:', error); throw error; } } // Direct Taobao API call based on the shared resources async fetchDirectAliExpressAPI(productId) { log('Making direct Taobao API call for product ID:', productId); try { // Based on the shared resources, we'll use a different approach // This is based on the GitHub repo and blog post you shared // Prepare API request const timestamp = Date.now(); const appKey = '12574478'; // Construct the request data object const requestData = { itemId: productId, language: 'en', currency: 'USD', region: 'US', locale: 'en_US', site: 'usa' }; // Convert request data to JSON string const dataStr = JSON.stringify(requestData); // Construct the API URL const apiUrl = `https://www.aliexpress.us/aer-api/v1/product/detail?productId=${productId}`; log('Fetching from direct API URL:', apiUrl); return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: apiUrl, headers: { 'accept': 'application/json, text/plain, */*', 'accept-language': 'en-US,en;q=0.9', 'cache-control': 'no-cache', 'pragma': 'no-cache', 'referer': `https://www.aliexpress.us/item/${productId}.html`, 'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"macOS"', 'sec-fetch-dest': 'empty', 'sec-fetch-mode': 'cors', 'sec-fetch-site': 'same-origin', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' }, withCredentials: true, onload: (response) => { try { log('Received direct API response'); // if url is 404.html, we didn't find the product if (response.finalUrl.includes('404.html')) { log(`product ${productId} not found`); resolve(null); return; } // Parse JSON response const data = JSON.parse(response.responseText); log('Parsed direct API response:', data); if (!data.data || data.code !== 200) { log('Direct API returned error:', data.message || 'Unknown error'); resolve(null); return; } // Parse the product data const productData = this.parseDirectAPIResponse(data, productId); if (productData) { log('Successfully parsed direct API response'); resolve(productData); } else { log('Failed to parse direct API response'); resolve(null); } } catch (error) { log('Error processing direct API response:', error); resolve(null); } }, onerror: (error) => { log('Error fetching direct API data:', error); resolve(null); } }); }); } catch (error) { log('Error in fetchDirectTaobaoAPI:', error); return null; } } // Parse the direct API response parseDirectAPIResponse(data, productId) { try { log('Parsing direct API response', { productId }); const productDetail = data.data || {}; // Extract title const title = productDetail.productTitle || productDetail.title || ''; // Extract variants let variants = []; // Try to extract variants from skuModule const skuModule = productDetail.skuModule || {}; const skuPriceModule = productDetail.priceModule || {}; const shippingModule = productDetail.shippingModule || {}; if (skuModule.skuPriceList || skuModule.skuList) { const skuList = skuModule.skuPriceList || skuModule.skuList || []; variants = skuList.map(sku => { const skuId = sku.skuId || sku.id; const skuName = this.extractSkuName(sku, skuModule) || 'Default'; const priceInfo = sku.skuVal || sku; // Extract shipping info const shippingInfo = this.extractShippingInfoFromModule(shippingModule, productId); return { id: skuId, name: skuName, price: { value: priceInfo.skuAmount?.value || priceInfo.skuPrice || 0, formattedPrice: utils.formatPrice(priceInfo.skuAmount?.value || priceInfo.skuPrice || 0), discountedValue: priceInfo.skuActivityAmount?.value || priceInfo.actSkuPrice || priceInfo.skuPrice || 0, discountedFormattedPrice: utils.formatPrice(priceInfo.skuActivityAmount?.value || priceInfo.actSkuPrice || priceInfo.skuPrice || 0), discount: priceInfo.discount || '' }, shipping: shippingInfo, stock: sku.skuVal?.availQuantity || sku.inventory || 999, isMainProduct: this.isMainProductBySku(sku) }; }); } // If no variants found, create a default one if (variants.length === 0) { const priceInfo = skuPriceModule.formatedActivityPrice || skuPriceModule.formatedPrice || ''; const priceValue = parseFloat(priceInfo.replace(/[^\d.]/g, '')) || 0; // Extract shipping info const shippingInfo = this.extractShippingInfoFromModule(shippingModule, productId); variants = [{ id: 'default', name: 'Default', price: { value: priceValue, formattedPrice: utils.formatPrice(priceValue), discountedValue: priceValue, discountedFormattedPrice: utils.formatPrice(priceValue), discount: skuPriceModule.discount || '' }, shipping: shippingInfo, stock: 999, isMainProduct: true }]; } return { productId, title, variants }; } catch (error) { log('Error parsing direct API response:', error, { productId }); return null; } } // Extract SKU name from SKU object extractSkuName(sku, skuModule) { try { // Try to extract name from skuAttr (format: "14:350685#1m") if (sku.skuAttr) { const parts = sku.skuAttr.split('#'); if (parts.length > 1) { return parts[1]; } } // Try to extract name from propPath if (sku.propPath) { const propIds = sku.propPath.split(';').map(p => p.split(':')[1]); // Find property values const propNames = []; const props = skuModule.props || []; for (const prop of props) { const values = prop.values || []; for (const value of values) { if (propIds.includes(value.id)) { propNames.push(value.name); } } } if (propNames.length > 0) { return propNames.join(' '); } } return 'Default'; } catch (error) { log('Error extracting SKU name:', error); return 'Default'; } } // Extract shipping info from shipping module extractShippingInfoFromModule(shippingModule, productId) { try { log('Raw shipping module data:', shippingModule, { productId }); const defaultShipping = { cost: 0, formattedPrice: '$0.00', freeThreshold: null }; if (!shippingModule) { return defaultShipping; } // Find shipping cost const shippingOptions = shippingModule.freightCalculateInfo?.freight || []; if (shippingOptions.length === 0) { return defaultShipping; } // Get the cheapest shipping option const cheapestOption = shippingOptions.reduce((min, option) => { const cost = option.freightAmount?.value || 0; return cost < min.cost ? { cost, option } : min; }, { cost: Infinity, option: null }); if (cheapestOption.option) { const cost = cheapestOption.cost; // Check for free shipping threshold let freeThreshold = null; if (shippingModule.freightCalculateInfo?.freeShippingText) { const thresholdMatch = shippingModule.freightCalculateInfo.freeShippingText.match(/\$(\d+(\.\d{2})?)/); if (thresholdMatch) { freeThreshold = parseFloat(thresholdMatch[1]); } } return { cost, formattedPrice: utils.formatPrice(cost), freeThreshold }; } return defaultShipping; } catch (error) { log('Error extracting shipping info:', error); return { cost: 0, formattedPrice: '$0.00', freeThreshold: null }; } } extractPriceFromElement(element) { if (!element) return { current: 0, original: 0 }; try { // Extract current price const currentPriceText = element.textContent.match(/\$[\d,.]+/)?.[0] || '0'; const currentPrice = parseFloat(currentPriceText.replace(/[$,]/g, '')); // Extract original price if available (crossed out price) const originalPriceElement = element.querySelector('.lq_j4'); const originalPriceText = originalPriceElement?.textContent.match(/\$[\d,.]+/)?.[0] || currentPriceText; const originalPrice = parseFloat(originalPriceText.replace(/[$,]/g, '')); return { current: currentPrice, original: originalPrice }; } catch (error) { log('Error extracting price:', error); return { current: 0, original: 0 }; } } extractShippingFromElement(element) { if (!element) return { cost: 0, freeThreshold: null }; try { const text = element.textContent; const freeThresholdMatch = text.match(/Free shipping over \$(\d+(\.\d{2})?)/i); const shippingCostMatch = text.match(/Shipping: \$(\d+(\.\d{2})?)/i); return { cost: shippingCostMatch ? parseFloat(shippingCostMatch[1]) : 0, freeThreshold: freeThresholdMatch ? parseFloat(freeThresholdMatch[1]) : null }; } catch (error) { log('Error extracting shipping:', error); return { cost: 0, freeThreshold: null }; } } extractDiscountFromElement(element) { if (!element) return { percentage: '' }; try { const text = element.textContent; const percentageMatch = text.match(/-(\d+)%/); return { percentage: percentageMatch ? `-${percentageMatch[1]}%` : '' }; } catch (error) { log('Error extracting discount:', error); return { percentage: '' }; } } createSingleVariant(result, productId) { // Extract price from the page data const priceInfo = result.priceComponent || result.price || {}; // Extract shipping from the new path const shippingData = result.SHIPPING || {}; const deliveryLayout = shippingData.deliveryLayoutInfo?.[0] || {}; const shippingBizData = deliveryLayout.bizData || {}; // Get the price values const originalPrice = this.extractDefaultPrice(result); const discountedPrice = priceInfo.activityPrice || priceInfo.discountPrice || originalPrice; // Extract base shipping info const baseShippingInfo = this.extractShippingInfo(shippingBizData, productId); return [{ // Return as an array containing the single variant object id: 'default', name: 'Default', price: { value: originalPrice, formattedPrice: utils.formatPrice(originalPrice), discountedValue: discountedPrice, discountedFormattedPrice: utils.formatPrice(discountedPrice), discount: priceInfo.discount || '' }, shipping: baseShippingInfo, // Use the extracted base info stock: 999, isMainProduct: true }]; } parseProductData(data) { log('Parsing data:', data); const productId = data.data?.result?.productId || ''; // Extract productId for logging // Handle different API response structures const result = data.data?.result || data.data || {}; log('Result object:', result, { productId }); // Handle error responses if (data.ret && data.ret[0]?.startsWith('FAIL_')) { log('API returned error:', data.ret[0], { productId }); return { productId: productId, title: result.title || '', variants: [this.createDefaultVariant(result)] }; } // Extract basic product info const productInfo = { productId: productId, title: result.title || '', }; // Extract variants let variants = []; try { // Get SKU and price data from the correct paths const skuPaths = result.SKU?.skuPaths || []; const priceMap = result.PRICE?.skuIdStrPriceInfoMap || {}; // Extract shipping info from the new path const shippingData = result.SHIPPING || {}; const deliveryLayout = shippingData.deliveryLayoutInfo?.[0] || {}; const shippingBizData = deliveryLayout.bizData || {}; const deliveryGuarantee = shippingData.DELIVERY_GUARANTEE_SERVICE || {}; // Note: Free shipping text info might be nested differently, adjust if needed const freeShippingTextInfo = {}; // Placeholder if (skuPaths.length > 0) { variants = skuPaths.map(sku => { const skuId = sku.skuIdStr || sku.skuId; const priceInfo = priceMap[skuId] || {}; // Base variant data without shipping return { id: skuId, name: this.getSkuName(sku), price: { value: priceInfo.originalPrice?.value || 0, formattedPrice: priceInfo.originalPrice?.formatedAmount || '$0.00', discountedValue: this.extractPriceValue(priceInfo.salePriceString) || priceInfo.originalPrice?.value || 0, discountedFormattedPrice: priceInfo.salePriceString || priceInfo.originalPrice?.formatedAmount || '$0.00', discount: priceInfo.discount || '' }, stock: sku.skuStock || sku.availQuantity || 999, isMainProduct: this.isMainProductBySku(sku) }; }); } else { // Single variant case // Pass productId to createSingleVariant variants = [this.createSingleVariant(result, productId)]; } // Extract base shipping info ONCE const baseShippingInfo = this.extractShippingInfo(shippingBizData, productId); // Add shipping info (cost, guarantee, etc.) to all variants variants = this.addShippingInfo(variants, baseShippingInfo, deliveryGuarantee, freeShippingTextInfo, productId); } catch (error) { log('Error parsing variants:', error, { productId }); variants = [this.createDefaultVariant(result)]; } return { ...productInfo, variants: variants.length > 0 ? variants : [this.createDefaultVariant(result, productId)] }; } getSkuName(sku) { // Extract name from skuAttr (format: "14:350685#1m") const skuAttr = sku.skuAttr || ''; const parts = skuAttr.split('#'); return parts[1] || 'Default'; } isMainProductBySku(sku) { const name = (sku.skuAttr || '').toLowerCase(); return !this.isAccessory(name); } extractPriceValue(priceString) { if (!priceString) return 0; const match = priceString.match(/[\d,.]+/); return match ? parseFloat(match[0].replace(/,/g, '')) : 0; } createDefaultVariant(result, productId) { // Create a default variant when no variant info is available const defaultPrice = this.extractDefaultPrice(result); return { id: 'default', name: 'Default', price: { value: defaultPrice, formattedPrice: utils.formatPrice(defaultPrice), discountedValue: defaultPrice, discountedFormattedPrice: utils.formatPrice(defaultPrice), discount: '' }, shipping: { cost: 0, formattedPrice: '$0.00', freeThreshold: null }, stock: 999, isMainProduct: true }; } extractDefaultPrice(result) { // Try various paths to find the price const productId = result?.productId || ''; // Get productId if available const paths = [ result.priceComponent?.originalPrice, result.price?.originalPrice?.value, result.price?.minPrice, result.PRICE?.originalPrice?.value, result.PRICE?.minPrice ]; for (const price of paths) { if (typeof price === 'number' && !isNaN(price)) { return price; } } log('Could not extract default price from result object', { productId, result }); return 0; } extractShippingInfo(shippingBizData, productId) { log('Raw shippingBizData object:', shippingBizData, { productId }); // Log the full object const hasChoiceFreeShipping = shippingBizData?.choiceFreeShipping === 'yes'; log(`[extractShippingInfo] choiceFreeShipping status for ${productId}:`, hasChoiceFreeShipping); return { cost: shippingBizData?.displayAmount || 0, // Use formattedAmount if available, otherwise format the cost formattedPrice: shippingBizData?.formattedAmount || utils.formatPrice(shippingBizData?.displayAmount || 0), // Free threshold logic might need revisiting based on bizData structure - removed for now freeThreshold: null, // Keep null for now, threshold *value* extraction needs review hasChoiceFreeShipping: hasChoiceFreeShipping // Add the boolean status }; } extractFreeShippingThreshold(shipping) { // This function needs to be re-evaluated based on the new API structure. // It's currently not used because extractShippingInfo sets freeThreshold to null. log('extractFreeShippingThreshold called, but logic needs review based on SHIPPING.deliveryLayoutInfo structure', { shipping }); return null; } addShippingInfo(variants, baseShippingInfo, deliveryGuarantee, freeShippingTextInfo, productId) { // baseShippingInfo is the object returned by extractShippingInfo // deliveryGuarantee is the result.DELIVERY_GUARANTEE_SERVICE object // freeShippingTextInfo is the (potentially empty) free shipping text component return variants.map(variant => ({ ...variant, shipping: { ...baseShippingInfo, // Contains cost, formattedPrice, freeThreshold (currently null) guaranteedDays: deliveryGuarantee?.subContents?.[3]?.content?.match(/\d+/)?.[0] || null, freeShippingText: freeShippingTextInfo?.mainText || null // TODO: Re-evaluate freeThreshold extraction if needed } })); } isAccessory(name) { const accessoryKeywords = [ 'case', 'cover', 'protector', 'cable', 'adapter', 'charger', 'holder', 'stand', 'accessory', 'kit', 'pedal', 'spare', 'replacement', 'tool', 'bag', 'box' ]; return accessoryKeywords.some(keyword => name.includes(keyword)); } // Fetch product data directly from the product page HTML async fetchDataFromProductPage(productId) { log('Fetching product page data for ID:', productId); // Add log to indicate fallback log(`Falling back to fetching data directly from product page HTML for productId: ${productId}`, { productId }); try { const productUrl = `https://www.aliexpress.us/item/${productId}.html`; log('Fetching product page:', productUrl); return new Promise((resolve, reject) => { GM.xmlHttpRequest({ method: 'GET', url: productUrl, headers: { 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 'accept-language': 'en-US,en;q=0.9', 'cache-control': 'no-cache', 'pragma': 'no-cache', 'sec-ch-ua': '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"', 'sec-ch-ua-mobile': '?0', 'sec-ch-ua-platform': '"macOS"', 'sec-fetch-dest': 'document', 'sec-fetch-mode': 'navigate', 'sec-fetch-site': 'none', 'sec-fetch-user': '?1', 'upgrade-insecure-requests': '1', 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36' }, onload: (response) => { try { log('Received product page response'); // Extract product data from HTML const productData = this.extractProductDataFromHTML(response.responseText, productId); if (productData) { log('Successfully extracted product data from HTML'); resolve(productData); } else { log('Failed to extract product data from HTML'); resolve(null); } } catch (error) { log('Error processing product page response:', error); resolve(null); } }, onerror: (error) => { log('Error fetching product page:', error); resolve(null); } }); }); } catch (error) { log('Error in fetchProductPageData:', error); return null; } } // Extract product data from HTML extractProductDataFromHTML(html, productId) { try { log('Extracting product data from HTML'); // Create a temporary DOM element to parse the HTML const parser = new DOMParser(); const doc = parser.parseFromString(html, 'text/html'); // Look for the product data in the page const scriptElements = Array.from(doc.querySelectorAll('script')); // Find the script that contains the product data let productData = null; // Method 1: Look for runParams.data for (const script of scriptElements) { const content = script.textContent; if (content.includes('runParams.data')) { const match = content.match(/runParams\.data\s*=\s*({.*?});/s); if (match && match[1]) { try { productData = JSON.parse(match[1]); log('Found product data in runParams.data'); break; } catch (e) { log('Error parsing runParams.data:', e); } } } } // Method 2: Look for window.__INITIAL_STATE__ if (!productData) { for (const script of scriptElements) { const content = script.textContent; if (content.includes('window.__INITIAL_STATE__')) { const match = content.match(/window\.__INITIAL_STATE__\s*=\s*({.*?});/s); if (match && match[1]) { try { const state = JSON.parse(match[1]); productData = state.productDetail?.data; log('Found product data in window.__INITIAL_STATE__'); break; } catch (e) { log('Error parsing window.__INITIAL_STATE__:', e); } } } } } // Method 3: Look for data-pdp-json if (!productData) { const jsonElement = doc.querySelector('[data-pdp-json]'); if (jsonElement) { try { productData = JSON.parse(jsonElement.getAttribute('data-pdp-json')); log('Found product data in data-pdp-json attribute'); } catch (e) { log('Error parsing data-pdp-json:', e); } } } // Method 4: Look for window.runParams if (!productData) { for (const script of scriptElements) { const content = script.textContent; if (content.includes('window.runParams')) { const match = content.match(/window\.runParams\s*=\s*({.*?});/s); if (match && match[1]) { try { const runParams = JSON.parse(match[1]); productData = runParams.data; log('Found product data in window.runParams'); break; } catch (e) { log('Error parsing window.runParams:', e); } } } } } if (!productData) { log('Could not find product data in HTML'); return null; } // Extract title const title = productData.title || productData.subject || doc.querySelector('h1')?.textContent?.trim() || ''; // Extract variants let variants = []; // Try to extract variants from skuModule const skuModule = productData.skuModule || productData.skuInfo || {}; const skuPriceModule = productData.priceModule || productData.priceInfo || {}; if (skuModule.skuPriceList || skuModule.skuList) { const skuList = skuModule.skuPriceList || skuModule.skuList || []; variants = skuList.map(sku => { const skuId = sku.skuId || sku.id; const skuName = sku.skuAttr?.split('#')[1] || 'Default'; const priceInfo = sku.skuVal || sku; return { id: skuId, name: skuName, price: { value: priceInfo.skuAmount?.value || priceInfo.skuPrice || 0, formattedPrice: utils.formatPrice(priceInfo.skuAmount?.value || priceInfo.skuPrice || 0), discountedValue: priceInfo.skuActivityAmount?.value || priceInfo.actSkuPrice || priceInfo.skuPrice || 0, discountedFormattedPrice: utils.formatPrice(priceInfo.skuActivityAmount?.value || priceInfo.actSkuPrice || priceInfo.skuPrice || 0), discount: priceInfo.discount || '' }, shipping: { cost: 0, // We don't have shipping info from HTML formattedPrice: '$0.00', freeThreshold: null }, stock: sku.skuVal?.availQuantity || sku.inventory || 999, isMainProduct: true }; }); } // If no variants found, create a default one if (variants.length === 0) { const priceInfo = skuPriceModule.formatedActivityPrice || skuPriceModule.formatedPrice || ''; const priceValue = parseFloat(priceInfo.replace(/[^\d.]/g, '')) || 0; variants = [{ id: 'default', name: 'Default', price: { value: priceValue, formattedPrice: utils.formatPrice(priceValue), discountedValue: priceValue, discountedFormattedPrice: utils.formatPrice(priceValue), discount: skuPriceModule.discount || '' }, shipping: { cost: 0, formattedPrice: '$0.00', freeThreshold: null }, stock: 999, isMainProduct: true }]; } return { productId, title, variants }; } catch (error) { log('Error extracting product data from HTML:', error); return null; } } } // Price Context Calculator class PriceContextCalculator { calculatePageContext(productCards) { const prices = []; for (const card of productCards) { const priceElement = card.querySelector(SELECTORS.price); if (priceElement) { const price = this.extractPriceValue(priceElement.textContent); if (price) prices.push(price); } } if (prices.length === 0) return null; prices.sort((a, b) => a - b); const median = this.calculateMedian(prices); const threshold = median * 0.3; return { median, lowerBound: median - threshold, upperBound: median + threshold, distribution: this.calculateDistribution(prices) }; } calculateMedian(prices) { const mid = Math.floor(prices.length / 2); return prices.length % 2 === 0 ? (prices[mid - 1] + prices[mid]) / 2 : prices[mid]; } calculateDistribution(prices) { return { min: Math.min(...prices), max: Math.max(...prices), clusters: this.findPriceClusters(prices) }; } findPriceClusters(prices) { // Simple clustering based on price ranges const range = prices[prices.length - 1] - prices[0]; const step = range / 5; const clusters = []; for (let i = 0; i < 5; i++) { const min = prices[0] + (step * i); const max = prices[0] + (step * (i + 1)); const clusterPrices = prices.filter(p => p >= min && p < max); if (clusterPrices.length > 0) { clusters.push({ centerPrice: (min + max) / 2, count: clusterPrices.length, variance: this.calculateVariance(clusterPrices) }); } } return clusters; } calculateVariance(prices) { const mean = prices.reduce((a, b) => a + b) / prices.length; return Math.sqrt( prices.reduce((sq, n) => sq + Math.pow(n - mean, 2), 0) / prices.length ); } findBestMatchingVariant(variants, context) { if (!context || variants.length === 0) return variants[0]; return variants .map(variant => ({ ...variant, score: this.calculateVariantScore(variant, context) })) .sort((a, b) => b.score - a.score)[0]; } calculateVariantScore(variant, context) { const { median, lowerBound, upperBound } = context; const price = variant.price.discountedValue; const distanceScore = 1 / (Math.abs(price - median) + 1); const inRange = price >= lowerBound && price <= upperBound ? 1.5 : 0.5; const productTypeMultiplier = variant.isMainProduct ? 1.3 : 0.7; return distanceScore * inRange * productTypeMultiplier; } extractPriceValue(text) { const match = text.match(/[\d,.]+/); return match ? parseFloat(match[0].replace(/,/g, '')) : 0; } } // DOM Enhancement Manager class DOMEnhancer { constructor(dataManager, priceContextCalculator) { this.dataManager = dataManager; this.priceContextCalculator = priceContextCalculator; this.setupIntersectionObserver(); this.pendingEnhancements = new Set(); this.processedCards = new WeakSet(); // Track processed cards } setupIntersectionObserver() { this.observer = new IntersectionObserver( (entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const productCard = entry.target; const productId = utils.extractProductId(productCard); if (productId) { this.pendingEnhancements.add(productId); this.enhanceProductCard(productCard, productId); this.observer.unobserve(productCard); } } }); }, { rootMargin: '200px' } ); } async enhanceProductCard(card, productId) { // Check if we've already processed this card log(`[ARP_EnhanceFlow] [enhanceProductCard START] Processing card ${productId}`, { productId }); if (this.processedCards.has(card)) { log('Card already processed:', productId, { productId }); this.pendingEnhancements.delete(productId); loadingManager.itemComplete(); return; } log('Enhancing product card:', productId, { productId }); let priceElement = null; try { // Try multiple strategies to find the price element const priceSelectors = SELECTORS.price.split(','); // Log all potential price elements for debugging log('Searching for price element with selectors:', priceSelectors, { productId }); for (const selector of priceSelectors) { const elements = card.querySelectorAll(selector.trim()); if (elements.length > 0) { // Take the most specific (deepest) price element priceElement = Array.from(elements).reduce((best, current) => { const bestDepth = this.getElementDepth(best); const currentDepth = this.getElementDepth(current); return currentDepth > bestDepth ? current : best; }); log('Found price element using selector:', selector, { productId, elementHtml: priceElement.outerHTML, elementClass: priceElement.className }); break; } } if (!priceElement) { // If still not found, try searching deeper in the card log('No price element found with selectors, trying text pattern search', { productId }); const allElements = card.getElementsByTagName('*'); for (const element of allElements) { const text = element.textContent; // Look for price-like patterns (e.g., $XX.XX) if (/\$\d+(\.\d{2})?/.test(text) && !element.querySelector('*')) { priceElement = element; log('Found price element using text pattern:', { productId, text, elementHtml: element.outerHTML, elementClass: element.className }); break; } } } if (!priceElement) { log('No price element found for product:', productId, { productId, cardHtml: card.outerHTML }); this.pendingEnhancements.delete(productId); loadingManager.itemComplete(); return; } // Verify the element is still in the DOM if (!document.contains(priceElement)) { log('Price element is no longer in the DOM:', { productId, elementHtml: priceElement.outerHTML }); this.pendingEnhancements.delete(productId); loadingManager.itemComplete(); return; } // Mark the card as being processed this.processedCards.add(card); // Fetch data with rate limiting const productData = await rateLimiter.executeWithBackoff(async () => { return await this.dataManager.fetchProductData(productId); }); log(`[ARP_EnhanceFlow] [enhanceProductCard] Got productData (cached or fetched) for ${productId}`, { productId }); // Verify element is still valid after async operation if (!document.contains(priceElement)) { log('Price element was removed during async operation', { productId }); this.pendingEnhancements.delete(productId); loadingManager.itemComplete(); return; } log('Received product data for enhancement:', productData, { productId }); const context = this.priceContextCalculator.calculatePageContext( Array.from(document.querySelectorAll(SELECTORS.productCard)) ); const bestVariant = this.priceContextCalculator.findBestMatchingVariant( productData.variants, context ); this.updatePriceDisplay(priceElement, bestVariant, productData, context, productId); } catch (error) { log('Error enhancing product card:', productId, error, { productId }); if (priceElement && document.contains(priceElement)) { try { // Add error class instead of replacing completely priceElement.classList.add('ali-real-price-error'); priceElement.textContent = 'Price data unavailable'; } catch (displayError) { log('Error showing error state:', displayError, { productId }); } } } finally { // *** This SHOULD always run *** log(`[ARP_EnhanceFlow] [enhanceProductCard finally] Reached finally block for ${productId}`, { productId }); this.pendingEnhancements.delete(productId); loadingManager.itemComplete(); // This is the call that updates the counter } } updatePriceDisplay(element, bestVariant, productData, context, productId) { if (!element) { log('Cannot update price display - element is null'); return; } if (!element.parentNode) { log('Cannot update price display - element has no parent', { elementHtml: element.outerHTML, elementClass: element.className, elementId: element.id }); return; } // Get total price range (includes shipping) const priceRange = this.getPriceRange(productData.variants, productId); const displayOptions = { showShipping: true, // Keep flag for potential future use, but won't add text now showMedianIndicator: true, showPriceRange: true, showDistributionGraph: true }; // Start with the total price of the best matching variant const bestVariantTotal = (bestVariant.price?.discountedValue || 0) + (bestVariant.shipping?.cost || 0); let displayText = utils.formatPrice(bestVariantTotal); if (displayOptions.showPriceRange && priceRange.min !== priceRange.max) { // Display the total price range displayText = `${utils.formatPrice(priceRange.min)} - ${utils.formatPrice(priceRange.max)}`; } // Add note about shipping being included only if: // 1. The base shipping cost is > 0 // 2. There is NO "Choice Free Shipping" option available if (bestVariant.shipping?.cost > 0 && !bestVariant.shipping?.hasChoiceFreeShipping) { log(`Adding '(including shipping)' for ${productId} because cost is ${bestVariant.shipping?.cost} and hasChoiceFreeShipping is ${bestVariant.shipping?.hasChoiceFreeShipping}`); displayText += ' (including shipping)'; } else { log(`NOT adding '(including shipping)' for ${productId} because cost is ${bestVariant.shipping?.cost} and hasChoiceFreeShipping is ${bestVariant.shipping?.hasChoiceFreeShipping}`); } if (displayOptions.showMedianIndicator) { displayText += ' ⊙'; } // Instead of replacing the element, try to modify it in place first try { element.className = 'ali-real-price-range ' + element.className; element.innerHTML = displayText; // Add hover events for variant popup const card = element.closest(SELECTORS.productCard); if (card) { let popupTimeout; element.addEventListener('mouseenter', () => { log('mouseenter', {productId}); popupTimeout = setTimeout(() => { this.showVariantPopup(card, productData.variants, bestVariant, context, productId); }, 200); // Small delay to prevent flicker }); log('mouseenter event listener added', {productId}); element.addEventListener('mouseleave', () => { clearTimeout(popupTimeout); setTimeout(() => { this.hideVariantPopup(card, productId); }, 200); // Small delay to allow moving mouse to popup }); } else { log('no card found for when establishing hover events', {productId}); } if (displayOptions.showDistributionGraph) { this.addPriceDistributionGraph(element, bestVariant, priceRange, productId); } return; } catch (modifyError) { log('Failed to modify element in place:', modifyError); } // If modifying in place fails, try replacement try { const container = document.createElement('div'); container.className = 'ali-real-price-range'; container.innerHTML = displayText; // Add hover events for variant popup const card = element.closest(SELECTORS.productCard); if (card) { let popupTimeout; container.addEventListener('mouseenter', () => { log('mouseenter (container)', {productId}); popupTimeout = setTimeout(() => { this.showVariantPopup(card, productData.variants, bestVariant, context, productId); }, 200); // Small delay to prevent flicker }); container.addEventListener('mouseleave', () => { clearTimeout(popupTimeout); setTimeout(() => { this.hideVariantPopup(card, productId); }, 200); // Small delay to allow moving mouse to popup }); } if (displayOptions.showDistributionGraph) { this.addPriceDistributionGraph(container, bestVariant, priceRange, productId); } element.parentNode.replaceChild(container, element); } catch (error) { log('Error replacing price element:', error, { elementHtml: element.outerHTML, parentHtml: element.parentNode?.outerHTML }); } } showVariantPopup(card, variants, bestVariant, context, productId) { log('Showing variant popup', { productId }); // Remove any existing popup first this.hideVariantPopup(card, productId, false); // Don't log removal here const popup = document.createElement('div'); popup.className = 'ali-real-price-popup'; const variantList = document.createElement('ul'); const sortedVariants = [...variants].sort((a, b) => { const totalA = a.price.discountedValue + a.shipping.cost; const totalB = b.price.discountedValue + b.shipping.cost; return totalA - totalB; }); for (const variant of sortedVariants) { const variantItem = document.createElement('li'); const isMedianMatch = variant.id === bestVariant.id; variantItem.innerHTML = ` ${isMedianMatch ? '⊙ ' : '• '} ${variant.name} ${variant.price.discountedFormattedPrice} ${variant.shipping.cost > 0 ? `+ ${variant.shipping.formattedPrice} shipping` : ''} = ${utils.formatPrice(variant.price.discountedValue + variant.shipping.cost)} total `; if (isMedianMatch) { variantItem.classList.add('median-match'); } variantList.appendChild(variantItem); } popup.appendChild(variantList); const freeShippingThreshold = this.getFreeShippingThreshold(variants, productId); if (freeShippingThreshold) { const thresholdInfo = document.createElement('div'); thresholdInfo.className = 'free-shipping-threshold'; thresholdInfo.textContent = `Free shipping over ${utils.formatPrice(freeShippingThreshold)}`; popup.appendChild(thresholdInfo); } this.positionPopup(popup, card); card.appendChild(popup); } hideVariantPopup(card, productId, shouldLog = true) { const popup = card.querySelector('.ali-real-price-popup'); if (popup) { if (shouldLog) log('Hiding variant popup', { productId }); popup.remove(); } } positionPopup(popup, card) { const cardRect = card.getBoundingClientRect(); popup.style.left = '100%'; popup.style.top = '0'; // Reposition if popup would go off screen requestAnimationFrame(() => { const popupRect = popup.getBoundingClientRect(); if (popupRect.right > window.innerWidth) { popup.style.left = 'auto'; popup.style.right = '100%'; } }); } getPriceRange(variants, productId) { if (!variants || variants.length === 0) { log('No variants provided to getPriceRange', { productId }); return { min: 0, max: 0 }; } // Calculate total price (item + shipping) for each variant const totalPrices = variants.map(v => { const itemPrice = v.price?.discountedValue || 0; const shippingCost = v.shipping?.cost || 0; return itemPrice + shippingCost; }); if (totalPrices.length === 0) { log('Calculated totalPrices array is empty', { productId, variants }); return { min: 0, max: 0 }; } return { min: Math.min(...totalPrices), max: Math.max(...totalPrices) }; } getFreeShippingThreshold(variants, productId) { return variants.reduce((threshold, variant) => { return variant.shipping.freeThreshold !== null ? Math.min(threshold || Infinity, variant.shipping.freeThreshold) : threshold; }, null); } addPriceDistributionGraph(container, bestVariant, priceRange, productId) { const graph = document.createElement('div'); graph.className = 'ali-real-price-distribution'; const marker = document.createElement('div'); marker.className = 'ali-real-price-distribution-marker'; const position = ((bestVariant.price.discountedValue - priceRange.min) / (priceRange.max - priceRange.min)) * 100; marker.style.left = `${position}%`; graph.appendChild(marker); container.appendChild(graph); } // Helper method to get element depth in DOM getElementDepth(element) { let depth = 0; let current = element; while (current.parentNode) { depth++; current = current.parentNode; } return depth; } } // Initialize the userscript async function init() { log('Initializing script...'); // --- Load Cache Disable Preference FIRST --- const storedValue = await GM.getValue('aliexpress_disable_cache', false); log(`[init] Loaded 'aliexpress_disable_cache' from GM.getValue: ${storedValue} (Type: ${typeof storedValue})`); isCacheDisabled = storedValue; log(`[init] Set global isCacheDisabled to: ${isCacheDisabled}`); // --- Instantiate LoadingManager AFTER loading preference --- // MOVED LATER // const loadingManager = new LoadingManager(); // Instance is global now // Add styles try { GM_addStyle(STYLES); log('Styles added successfully'); } catch (error) { log('Error adding styles:', error); } // Create instances of main classes (except DOMEnhancer) const dataManager = new DataManager(); globalCache = dataManager.cache; // Store cache instance globally const priceContextCalculator = new PriceContextCalculator(); // const domEnhancer = new DOMEnhancer(dataManager, priceContextCalculator); // MOVED LATER // Initialize token await dataManager.initializeToken(); log('Token initialized (or checked)'); // Start observing product cards - COUNT FIRST const productCards = document.querySelectorAll(SELECTORS.productCard); log('Found initial product cards:', productCards.length); // Initialize loading manager with total number of products // Uses the GLOBAL loadingManager instance implicitly now if (productCards.length > 0) { loadingManager.completedItems = 0; loadingManager.startLoading(productCards.length); log(`Loading manager initialized: total=${loadingManager.totalItems}, completed=${loadingManager.completedItems}`); } else { // Ensure totalItems is 0 if no cards found initially loadingManager.totalItems = 0; loadingManager.completedItems = 0; loadingManager.updateProgress(); // Show 0/0 log(`Loading manager initialized: total=0 (no initial cards)`); } // --- Create DOMEnhancer AFTER initializing loadingManager --- const domEnhancer = new DOMEnhancer(dataManager, priceContextCalculator); log('DOMEnhancer created'); // --- Observe Initial Cards --- productCards.forEach(card => { const productId = utils.extractProductId(card); if (productId) { log('Observing and immediately enhancing initial card:', productId, { productId }); domEnhancer.observer.observe(card); // Still observe in case manual call fails or for other reasons domEnhancer.enhanceProductCard(card, productId); // Start processing immediately, do not await } else { log('Skipping initial card - no product ID found', card); // If no ID, we can't process, and don't need to increment total/complete counts for it. // Adjust loading manager counts if necessary (though startLoading already set the total based on querySelectorAll count) // Maybe decrement totalItems if an initial card lacks an ID? Or handle it gracefully in itemComplete? // For now, just log and skip. } }); // --- Handle dynamic content loading (MutationObserver) --- const observer = new MutationObserver((mutations) => { log('DOM mutation detected'); let newCards = []; mutations.forEach((mutation) => { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // Check if the added node itself is a product card if (node.matches(SELECTORS.productCard)) { newCards.push(node); } else { // Check if the added node contains product cards const cards = node.querySelectorAll(SELECTORS.productCard); if (cards.length > 0) { newCards.push(...Array.from(cards)); } } } }); }); // Filter out cards that might have already been processed // (e.g., if mutation observer fires multiple times rapidly) newCards = newCards.filter(card => !domEnhancer.processedCards.has(card)); if (newCards.length > 0) { log('Found new product cards via MutationObserver:', newCards.length); // Update loading manager with new total const newTotal = loadingManager.totalItems + newCards.length; // Reset completed count only if starting from zero if (loadingManager.totalItems === 0) { log('First batch of dynamic items detected, resetting completed count.'); loadingManager.completedItems = 0; } loadingManager.startLoading(newTotal); // Sets new total, updates display newCards.forEach(card => { const productId = utils.extractProductId(card); if (productId) { // Ensure we have an ID before observing log('Observing new card found by MutationObserver:', productId, { productId }); domEnhancer.observer.observe(card); } else { log('Skipping observation for new card - no product ID found', card); } }); } }); observer.observe(document.body, { childList: true, subtree: true }); log('Mutation observer started'); } // Start the script if (document.readyState === 'loading') { log('Document still loading, waiting for DOMContentLoaded'); document.addEventListener('DOMContentLoaded', init); } else { log('Document already loaded, initializing immediately'); init(); } // --- Function to handle cache disable checkbox change --- async function handleDisableCacheChange(event) { isCacheDisabled = event.target.checked; log('Cache disabled preference changed:', isCacheDisabled); await GM.setValue('aliexpress_disable_cache', isCacheDisabled); // Optional: Clear cache when disabling? if (isCacheDisabled && globalCache) { await globalCache.clear(); log('Cache cleared because it was disabled.'); // Optionally alert the user or reload // alert('Cache disabled and cleared.'); } } })();