// ==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.');
}
}
})();