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    https://greasyfork.org/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();
    }

})();