Greasy Fork

来自缓存

Greasy Fork is available in English.

SoldBy pour amazon

révèles l'origine des vendeurs sur amazon

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         SoldBy pour amazon
// @name:fr      SoldBy - Révéler les vendeurs sur Amazon 
// @namespace    http://greasyfork.icu/users/108513
// @version      2.2.1
// @description  révèles l'origine des vendeurs sur amazon
// @author       seb-du17
// @license      MIT
// @match        https://www.amazon.co.jp/*
// @match        https://www.amazon.co.uk/*
// @match        https://www.amazon.com/*
// @match        https://www.amazon.com.be/*
// @match        https://www.amazon.com.mx/*
// @match        https://www.amazon.com.tr/*
// @match        https://www.amazon.de/*
// @match        https://www.amazon.es/*
// @match        https://www.amazon.fr/*
// @match        https://www.amazon.it/*
// @match        https://www.amazon.nl/*
// @match        https://www.amazon.se/*
// @grant        GM.getValue
// @grant        GM.setValue
// @run-at       document-idle
// @compatible   firefox Optimized for Firefox 146
// ==/UserScript==

/* jshint esversion: 11 */
/* global AbortController, IntersectionObserver, DOMParser, Intl */

(function() {
    'use strict';

    // ========== Configuration TURBO ==========
    const CONFIG = {
        HIGHLIGHTED_COUNTRIES: ['CN', 'HK'],
        HIGHLIGHT_UNKNOWN: true,
        HIDE_HIGHLIGHTED: false,
        MAX_ASIN_AGE_DAYS: 3, // Garde le cache plus longtemps pour moins recharger
        MAX_SELLER_AGE_DAYS: 14,
        FETCH_TIMEOUT: 5000, // Timeout plus court pour ne pas bloquer la file
        MAX_CONCURRENT_FETCHES: 25, // Augmenté de 10 à 25
        DEBOUNCE_DELAY: 100, // Plus réactif au scroll
        PRELOAD_MARGIN: '1500px', // Prépare plus loin en avance
        IMMEDIATE_PROCESS_COUNT: 40, // Traite les 40 premiers items direct
        LAZY_COUNTRY_FETCH: true,
        DEBUG: false
    };

    // ========== Regex précompilées ==========
    const REGEX = {
        ASIN_DP: /\/dp\/(.*?)(?:$|\?|\/)/,
        COUNTRY_CODE: /^[A-Z]{2}$/,
        RATING_DEFAULT: /(\d+%).*?\((\d+)/,
        RATING_TURKISH: /(%\d+).*?\((\d+)/,
        RATING_GERMAN: /(\d+ %).*?\((\d+)/
    };

    // ========== Sélecteurs précompilés ==========
    const SELECTORS = {
        PRODUCTS_WITHOUT_ASIN: [
            '.a-carousel-card > div:not([data-asin])',
            '.octopus-pc-item:not([data-asin])',
            'li[class*=ProductGridItem__]:not([data-asin])',
            'div[class*=_octopus-search-result-card_style_apbSearchResultItem]:not([data-asin])',
            '.sbv-product:not([data-asin])',
            '.a-cardui #gridItemRoot:not([data-asin])'
        ].join(','),

        PRODUCTS_WITH_ASIN: [
            'div[data-asin]:not([data-asin=""])',
            ':not([data-seller-processed])',
            ':not([data-uuid*=s-searchgrid-carousel])',
            ':not([role="img"])',
            ':not(.a-hidden)'
        ].join(''),

        SPECIAL_PAGES: [
            '#gc-detail-page',
            '.reload_gc_balance',
            '#dp.digitaltextfeeds',
            '#dp.magazine',
            '#dp.ebooks',
            '#dp.audible',
            '.av-page-desktop',
            '.avu-retail-page'
        ].join(','),

        THIRD_PARTY_SELLER: [
            '#desktop_qualifiedBuyBox :not(#usedAccordionRow) #sellerProfileTriggerId',
            '#desktop_qualifiedBuyBox :not(#usedAccordionRow) #merchant-info a:first-of-type',
            '#exports_desktop_qualifiedBuybox :not(#usedAccordionRow) #sellerProfileTriggerId',
            '#exports_desktop_qualifiedBuybox :not(#usedAccordionRow) #merchant-info a:first-of-type'
        ].join(',')
    };

    // ========== Cache mémoire ==========
    const memoryCache = {
        asins: new Map(),
        sellers: new Map()
    };

    // ========== Queue de fetch ==========
    const fetchQueue = [];
    let activeFetches = 0;
    let debounceTimer;
    let intersectionObserver;

    // PATCH: évite les observe() multiples sur les mêmes nodes
    const observedProducts = new WeakSet();

    // ========== Fetch avec timeout ==========
    function fetchWithTimeout(url, timeout) {
        return new Promise(function(resolve, reject) {
            const controller = new AbortController();
            const timeoutId = setTimeout(function() {
                controller.abort();
                reject(new Error('Timeout'));
            }, timeout);

            fetch(url, {
                signal: controller.signal,
                // priority: 'low' supprimé pour aller plus vite
            })
            .then(function(response) {
                clearTimeout(timeoutId);
                if (response.ok) {
                    return response.text();
                }
                throw new Error('HTTP ' + response.status);
            })
            .then(resolve)
            .catch(reject);
        });
    }

    // ========== Process single fetch item ==========
    function processFetchItem(item) {
        fetchWithTimeout(item.url, CONFIG.FETCH_TIMEOUT)
            .then(function(html) {
                item.resolve(html);
            })
            .catch(function(err) {
                if (CONFIG.DEBUG) {
                    console.warn('[SoldBy] Fetch failed:', item.url, err);
                }
                item.resolve(null);
            })
            .finally(function() {
                activeFetches--;
                if (fetchQueue.length) {
                    processQueue();
                }
            });
    }

    // ========== Queue processor ==========
    function processQueue() {
        while (fetchQueue.length && activeFetches < CONFIG.MAX_CONCURRENT_FETCHES) {
            const item = fetchQueue.shift();
            activeFetches++;
            processFetchItem(item);
        }
    }

    function queueFetch(url) {
        return new Promise(function(resolve) {
            fetchQueue.push({ url: url, resolve: resolve });
            processQueue();
        });
    }

    // ========== LocalStorage avec cache mémoire ==========
    function getFromStorage(key) {
        if (memoryCache.asins.has(key)) {
            return memoryCache.asins.get(key);
        }
        if (memoryCache.sellers.has(key)) {
            return memoryCache.sellers.get(key);
        }

        const value = localStorage.getItem(key);
        if (!value) { return null; }

        try {
            const parsed = JSON.parse(value);
            if (key.startsWith('asin-')) {
                memoryCache.asins.set(key, parsed);
            } else if (key.startsWith('seller-')) {
                memoryCache.sellers.set(key, parsed);
            }
            return parsed;
        } catch (e) {
            return null;
        }
    }

    function saveToStorage(key, value) {
        const json = JSON.stringify(value);
        localStorage.setItem(key, json);

        if (key.startsWith('asin-')) {
            memoryCache.asins.set(key, value);
            if (memoryCache.asins.size > 500) { // Cache mémoire augmenté
                const firstKey = memoryCache.asins.keys().next().value;
                memoryCache.asins.delete(firstKey);
            }
        } else if (key.startsWith('seller-')) {
            memoryCache.sellers.set(key, value);
            if (memoryCache.sellers.size > 300) { // Cache mémoire augmenté
                const firstKey = memoryCache.sellers.keys().next().value;
                memoryCache.sellers.delete(firstKey);
            }
        }
    }

    function isExpired(timestamp, maxAgeDays) {
        const age = Date.now() - timestamp;
        const maxAge = maxAgeDays * 24 * 60 * 60 * 1000;
        return age > maxAge;
    }

    // ========== Extract ASIN from product ==========
    function extractAsin(product) {
        const link = product.querySelector('a:not(.s-grid-status-badge-container > a):not(.a-popover-trigger)');
        if (!link) { return null; }

        const href = decodeURIComponent(link.href);
        const searchParams = new URLSearchParams(href);
        if (searchParams.get('pd_rd_i')) {
            return searchParams.get('pd_rd_i');
        }

        const match = href.match(REGEX.ASIN_DP);
        return match ? match[1] : null;
    }

    // ========== Parse HTML ==========
    function parseHTML(html) {
        return new DOMParser().parseFromString(html, 'text/html');
    }

    // ========== Get seller from product page ==========
    function getSellerFromProductPage(html) {
        const doc = parseHTML(html);

        if (doc.querySelector(SELECTORS.SPECIAL_PAGES)) {
            return { name: 'Amazon', id: null };
        }

        const sellerLink = doc.querySelector(SELECTORS.THIRD_PARTY_SELLER);
        if (sellerLink) {
            const params = new URLSearchParams(sellerLink.href);
            const sellerId = params.get('seller');
            const sellerName = sellerLink.textContent.trim().replace(/\"/g, '\"');
            return { name: sellerName, id: sellerId };
        }

        const merchantInfo = doc.querySelector('#merchant-info') ||
                             doc.querySelector('#tabular-buybox .tabular-buybox-text') ||
                             doc.querySelector('[offer-display-feature-name="desktop-merchant-info"]');

        if (merchantInfo && merchantInfo.textContent.trim().replace(/\s/g, '').length) {
            return { name: 'Amazon', id: null };
        }

        return { name: '? ? ?', id: null };
    }

    // ========== Get seller details from seller page ==========
    function getSellerDetailsFromSellerPage(html) {
        const doc = parseHTML(html);
        const container = doc.getElementById('seller-profile-container');
        if (!container) { return null; }

        const isRedesign = container.classList.contains('spp-redesigned');
        let country = '?';

        if (isRedesign) {
            const addresses = doc.querySelectorAll('#page-section-detail-seller-info .a-box-inner .a-row.a-spacing-none.indent-left');
            if (addresses.length) {
                const lastAddr = addresses[addresses.length - 1];
                const text = lastAddr.textContent.toUpperCase();
                if (REGEX.COUNTRY_CODE.test(text)) {
                    country = text;
                }
            }
        } else {
            const lists = doc.querySelectorAll('ul.a-unordered-list.a-nostyle.a-vertical');
            if (lists.length) {
                const lastList = lists[lists.length - 1];
                const items = lastList.querySelectorAll('li');
                if (items.length) {
                    const text = items[items.length - 1].textContent.toUpperCase();
                    if (REGEX.COUNTRY_CODE.test(text)) {
                        country = text;
                    }
                }
            }
        }

        const sellerName = doc.getElementById('seller-name');
        if (sellerName && sellerName.textContent.includes('Amazon')) {
            return { country: country, ratingScore: 'N/A', ratingCount: '0' };
        }

        const feedbackEl = doc.getElementById('seller-info-feedback-summary');
        if (!feedbackEl) {
            return { country: country, ratingScore: '0%', ratingCount: '0' };
        }

        const desc = feedbackEl.querySelector('.feedback-detail-description');
        const star = feedbackEl.querySelector('.a-icon-alt');

        if (!desc || !star) {
            return { country: country, ratingScore: '0%', ratingCount: '0' };
        }

        let text = desc.textContent.replace(star.textContent, '');
        const lang = document.documentElement.lang;
        let regex = REGEX.RATING_DEFAULT;
        let zeroPercent = '0%';

        if (lang === 'tr-tr') {
            regex = REGEX.RATING_TURKISH;
            zeroPercent = '%0';
        } else if (lang === 'de-de' || lang === 'fr-be') {
            regex = REGEX.RATING_GERMAN;
            zeroPercent = '0 %';
        }

        const match = text.match(regex);
        return {
            country: country,
            ratingScore: match ? match[1] : zeroPercent,
            ratingCount: match ? match[2] : '0'
        };
    }

    // ========== Flag emoji ==========
    function getFlagEmoji(countryCode) {
        if (countryCode === '?') { return '❓'; }
        const codePoints = [];
        for (let i = 0; i < countryCode.length; i++) {
            codePoints.push(127397 + countryCode.charCodeAt(i));
        }
        return String.fromCodePoint.apply(null, codePoints);
    }

    // ========== Find product title ==========
    function findTitle(product) {
        if (product.dataset.avar) {
            return product.querySelector('.a-color-base.a-spacing-none.a-link-normal');
        }
        if (product.parentElement && product.parentElement.classList.contains('a-carousel-card')) {
            return product.querySelector('h2') || product.querySelectorAll('.a-link-normal')[1];
        }
        if (product.id === 'gridItemRoot') {
            return product.querySelectorAll('.a-link-normal')[1];
        }
        const h2 = product.querySelector('h2');
        if (h2) { return h2; }

        return product.querySelector('.a-link-normal');
    }

    // ========== Create info box ==========
    function createInfoBox(product) {
        const container = document.createElement('div');
        container.className = 'sb-info-ct';

        const box = document.createElement('div');
        box.className = 'sb-info';

        const icon = document.createElement('div');
        icon.className = 'sb-icon sb-loading';

        const text = document.createElement('div');
        text.className = 'sb-text';
        text.textContent = 'Loading...';

        box.appendChild(icon);
        box.appendChild(text);
        container.appendChild(box);

        const title = findTitle(product);
        if (title && title.parentNode) {
            title.parentNode.insertBefore(container, title.nextSibling);
        } else {
            product.appendChild(container);
        }
    }

    // ========== Update info box ==========
    function updateInfoBox(product, sellerData) {
        const container = product.querySelector('.sb-info-ct');
        if (!container) { return; }

        const box = container.querySelector('.sb-info');
        const icon = container.querySelector('.sb-icon');
        const text = container.querySelector('.sb-text');

        icon.classList.remove('sb-loading');

        if (sellerData.name === 'Amazon' || sellerData.name.includes('Amazon')) {
            const img = document.createElement('img');
            img.src = '/favicon.ico';
            icon.innerHTML = '';
            icon.appendChild(img);
            text.textContent = sellerData.name;
            box.title = sellerData.name;
            return;
        }

        if (sellerData.name === '? ? ?') {
            icon.textContent = '❓';
            icon.style.fontSize = '1.5em';
            text.textContent = 'Unknown';
            return;
        }

        if (!sellerData.country) {
            icon.textContent = '⏳';
            icon.style.fontSize = '1.5em';
            text.textContent = sellerData.name;
            box.title = 'Loading country info...';
        } else {
            icon.textContent = getFlagEmoji(sellerData.country);
            text.textContent = sellerData.name;

            if (sellerData.ratingScore && sellerData.ratingCount) {
                text.textContent += ' (' + sellerData.ratingScore + ' | ' + sellerData.ratingCount + ')';
            }

            let title = '';
            if (sellerData.country && sellerData.country !== '?') {
                try {
                    const displayNames = new Intl.DisplayNames([document.documentElement.lang], { type: 'region' });
                    title = displayNames.of(sellerData.country) + ' | ';
                } catch (e) {
                    title = sellerData.country + ' | ';
                }
            }

            title += sellerData.name;
            if (sellerData.ratingScore) {
                title += ' (' + sellerData.ratingScore + ' | ' + sellerData.ratingCount + ')';
            }

            box.title = title;

            if (CONFIG.HIGHLIGHTED_COUNTRIES.indexOf(sellerData.country) !== -1 ||
                (CONFIG.HIGHLIGHT_UNKNOWN && sellerData.country === '?')) {

                product.classList.add('sb-highlight');

                if (CONFIG.HIDE_HIGHLIGHTED) {
                    let hideElement = product;
                    if (product.parentElement && product.parentElement.classList.contains('a-carousel-card')) {
                        hideElement = product.parentElement;
                    }
                    hideElement.classList.add('sb-hide');
                }
            }

            if (sellerData.id) {
                const existing = container.querySelector('.sb-link');
                if (!existing) {
                    const link = document.createElement('a');
                    link.className = 'sb-link';
                    link.href = window.location.origin + '/sp?seller=' + sellerData.id;
                    link.appendChild(box.cloneNode(true));
                    container.innerHTML = '';
                    container.appendChild(link);
                }
            }
        }
    }

    // ========== Process single product ==========
    function processProduct(product) {
        const asin = product.dataset.asin;
        if (!asin) { return; }

        product.dataset.sellerProcessed = '1';
        createInfoBox(product);

        const asinKey = 'asin-' + asin;
        const cached = getFromStorage(asinKey);

        if (cached && !isExpired(cached.ts, CONFIG.MAX_ASIN_AGE_DAYS)) {
            const sellerData = { name: cached.sn, id: cached.sid };

            if (cached.sid && cached.sid !== 'Amazon') {
                const sellerKey = 'seller-' + cached.sid;
                const sellerCached = getFromStorage(sellerKey);

                if (sellerCached && !isExpired(sellerCached.ts, CONFIG.MAX_SELLER_AGE_DAYS)) {
                    sellerData.country = sellerCached.c;
                    sellerData.ratingScore = sellerCached.rs;
                    sellerData.ratingCount = sellerCached.rc;
                    updateInfoBox(product, sellerData);
                    return;
                }
            }

            if (CONFIG.LAZY_COUNTRY_FETCH) {
                updateInfoBox(product, sellerData);
                fetchSellerDetails(product, sellerData);
            } else {
                fetchSellerDetails(product, sellerData);
            }
            return;
        }

        updateInfoBox(product, { name: 'Processing...', id: null });

        const url = window.location.origin + '/dp/' + asin + '?psc=1';
        queueFetch(url).then(function(html) {
            if (!html) { return; }

            const sellerData = getSellerFromProductPage(html);
            saveToStorage(asinKey, {
                sn: sellerData.name,
                sid: sellerData.id,
                ts: Date.now()
            });

            if (sellerData.name === 'Amazon' || !sellerData.id) {
                updateInfoBox(product, sellerData);
            } else {
                if (CONFIG.LAZY_COUNTRY_FETCH) {
                    updateInfoBox(product, sellerData);
                    fetchSellerDetails(product, sellerData);
                } else {
                    fetchSellerDetails(product, sellerData);
                }
            }
        });
    }

    // ========== Fetch seller details ==========
    function fetchSellerDetails(product, sellerData) {
        const sellerKey = 'seller-' + sellerData.id;
        const url = window.location.origin + '/sp?seller=' + sellerData.id;

        queueFetch(url).then(function(html) {
            if (!html) {
                updateInfoBox(product, sellerData);
                return;
            }

            const details = getSellerDetailsFromSellerPage(html);
            if (details) {
                sellerData.country = details.country;
                sellerData.ratingScore = details.ratingScore;
                sellerData.ratingCount = details.ratingCount;

                saveToStorage(sellerKey, {
                    c: details.country,
                    rs: details.ratingScore,
                    rc: details.ratingCount,
                    ts: Date.now()
                });

                updateInfoBox(product, sellerData);
            }
        });
    }

    // ========== Extract ASINs from products ==========
    function extractAsins() {
        const products = document.querySelectorAll(SELECTORS.PRODUCTS_WITHOUT_ASIN);
        for (let i = 0; i < products.length; i++) {
            const product = products[i];
            const asin = extractAsin(product);
            if (asin) {
                product.dataset.asin = asin;
            }
        }
    }

    // ========== Handle intersection ==========
    function handleIntersection(entry) {
        if (entry.isIntersecting) {
            processProduct(entry.target);
            intersectionObserver.unobserve(entry.target);
        }
    }

    // ========== Scan for new products ==========
    function scanProducts() {
        extractAsins();
        const products = document.querySelectorAll(SELECTORS.PRODUCTS_WITH_ASIN);
        for (let i = 0; i < products.length; i++) {
            const p = products[i];
            if (!observedProducts.has(p)) {
                observedProducts.add(p);
                intersectionObserver.observe(p);
            }
        }
    }

    // ========== Process immediate products ==========
    function processImmediateProducts() {
        extractAsins();
        const products = document.querySelectorAll(SELECTORS.PRODUCTS_WITH_ASIN);
        const toProcess = Math.min(CONFIG.IMMEDIATE_PROCESS_COUNT, products.length);

        for (let i = 0; i < toProcess; i++) {
            processProduct(products[i]);
        }

        for (let i = toProcess; i < products.length; i++) {
            const p = products[i];
            if (!observedProducts.has(p)) {
                observedProducts.add(p);
                intersectionObserver.observe(p);
            }
        }

        if (CONFIG.DEBUG) {
            console.log('[SoldBy] Processed ' + toProcess + ' products immediately');
        }
    }

    // ========== Debounced scan ==========
    function debouncedScan() {
        clearTimeout(debounceTimer);
        debounceTimer = setTimeout(scanProducts, CONFIG.DEBOUNCE_DELAY);
    }

    // ========== IntersectionObserver ==========
    intersectionObserver = new IntersectionObserver(
        function(entries) {
            for (let i = 0; i < entries.length; i++) {
                handleIntersection(entries[i]);
            }
        },
        { rootMargin: CONFIG.PRELOAD_MARGIN }
    );

    // ========== MutationObserver ==========
    const mutationObserver = new MutationObserver(function(mutations) {
        for (let i = 0; i < mutations.length; i++) {
            const m = mutations[i];
            if (m.type === 'childList' && m.addedNodes && m.addedNodes.length) {
                debouncedScan();
                return; // PATCH: un seul déclenchement suffit
            }
        }
    });

    // ========== CSS ==========
    function injectCSS() {
        const css = `
            .sb-hide { display: none !important; }
            .sb-info-ct { margin-top: 4px; cursor: default; }
            .sb-info {
                display: inline-flex; gap: 4px; background: #fff;
                font-size: 0.9em; padding: 2px 4px;
                border: 1px solid #d5d9d9; border-radius: 4px; max-width: 100%;
            }
            .sb-loading {
                display: inline-block; width: 0.8em; height: 0.8em;
                border: 3px solid rgba(255, 153, 0, 0.3); border-radius: 50%;
                border-top-color: #ff9900; animation: sb-spin 1s ease-in-out infinite;
                margin: 1px 3px 0;
            }
            @keyframes sb-spin { to { transform: rotate(360deg); } }
            .sb-icon { vertical-align: text-top; text-align: center; font-size: 1.8em; }
            .sb-icon img { width: 0.82em; height: 0.82em; }
            .sb-text {
                color: #1d1d1d !important; white-space: nowrap;
                text-overflow: ellipsis; overflow: hidden;
            }
            a.sb-link:hover .sb-info {
                box-shadow: 0 2px 5px 0 rgba(213, 217, 217, 0.5);
                background-color: #f7fafa;
            }
            .sb-highlight .s-card-container,
            .sb-highlight[data-avar],
            .sb-highlight .s-card-border {
                background-color: #f9e3e4 !important;
                border-color: #e3abae !important;
            }
            .sb-highlight .s-card-drop-shadow {
                box-shadow: none; border: 1px solid #e3abae;
            }
        `;
        const style = document.createElement('style');
        style.textContent = css;
        document.head.appendChild(style);
    }

    // ========== Init ==========
    function init() {
        injectCSS();
        processImmediateProducts();
        mutationObserver.observe(document.body, {
            childList: true,
            subtree: true
        });
        if (CONFIG.DEBUG) {
            console.log('[SoldBy] Initialized - Lazy country fetch enabled');
        }
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();