Greasy Fork

European Price Checker for Amazon (fr, de, es, it)

Compare product prices on Amazon.fr, Amazon.de, Amazon.es, and Amazon.it to find the best deal. Integrates CamelCamelCamel for price history charts.

当前为 2024-11-01 提交的版本,查看 最新版本

// ==UserScript==
// @name         European Price Checker for Amazon (fr, de, es, it)
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  Compare product prices on Amazon.fr, Amazon.de, Amazon.es, and Amazon.it to find the best deal. Integrates CamelCamelCamel for price history charts.
// @author       bNj
// @icon         https://i.ibb.co/qrjrcVy/amz-price-checker.png
// @match        https://www.amazon.fr/*
// @match        https://www.amazon.de/*
// @match        https://www.amazon.es/*
// @match        https://www.amazon.it/*
// @grant        GM_xmlhttpRequest
// @connect      amazon.fr
// @connect      amazon.es
// @connect      amazon.it
// @connect      amazon.de
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Entry point
    function main() {
        if (!extractASIN()) return;
        if (!getBasePrice()) return;

        injectStyles();
        createLoadingContainer();
        fetchPricesFromOtherSites();
    }

    const ASIN_REGEX = /\/([A-Z0-9]{10})(?:[/?]|$)/;
    let asin;
    let basePrice;
    let selectedTimePeriod = 'all';
    const priceResults = [];
    let requestCount = 0;

    const PARTNER_IDS = {
        fr: 'geeksince1983-21',
        es: 'geeksince1901-21',
        it: 'geeksince1903-21',
        de: 'geeksince190d-21'
    };

    const amazonSites = [
        { name: 'Amazon.fr', country: 'fr', flag: 'https://flagcdn.com/w20/fr.png' },
        { name: 'Amazon.es', country: 'es', flag: 'https://flagcdn.com/w20/es.png' },
        { name: 'Amazon.it', country: 'it', flag: 'https://flagcdn.com/w20/it.png' },
        { name: 'Amazon.de', country: 'de', flag: 'https://flagcdn.com/w20/de.png' }
    ];

    function extractASIN() {
        const asinMatch = window.location.href.match(ASIN_REGEX);
        if (!asinMatch) return false;
        asin = asinMatch[1];
        return true;
    }

    function getBasePrice() {
        basePrice = getPriceFromDocument(document);
        return basePrice !== null;
    }

    function injectStyles() {
        const styles = `
        #amazonPriceComparisonContainer {
            margin-top: 20px;
            padding: 10px;
            background-color: #f9f9f9;
            border: 1px solid #ccc;
            border-radius: 8px;
            position: relative;
            font-size: 11px;
        }
        .comparison-row {
            cursor: pointer;
            display: flex;
            justify-content: space-between;
            padding: 5px 0;
            border-bottom: 1px solid #ccc;
        }
        .comparison-row:hover {
            background-color: #f1f1f1;
        }
        .comparison-row.header-row {
            border-bottom: 2px solid #000;
            font-weight: bold;
            pointer-events: none;
        }
        #loadingMessage {
            text-align: center;
            font-weight: bold;
            font-size: 14px;
            display: flex;
            flex-direction: column;
            align-items: center;
            background-clip: text;
            -webkit-background-clip: text;
            color: transparent;
            background-image: linear-gradient(270deg, black 0%, black 20%, #FF9900 50%, black 80%, black 100%);
            background-size: 200% 100%;
            animation: loadingAnimation 2s linear infinite;
        }
        @keyframes loadingAnimation {
            0% { background-position: 100% 50%; }
            100% { background-position: 0% 50%; }
        }
        .active-button {
            background-color: #ff9900 !important;
        }
        .price-difference-positive {
            color: green;
        }
        .price-difference-negative {
            color: red;
        }
        .controls-container {
            text-align: center;
            margin: 10px;
            display: flex;
            justify-content: center;
            align-items: center;
            gap: 10px;
        }
        .aliexpress-container {
            margin-top: 20px;
            padding: 5px 10px;
            border: 1px solid #ccc;
            border-radius: 8px;
            text-align: center;
            max-width: 300px;
            margin-left: auto;
            margin-right: auto;
            cursor: pointer;
            transition: background-color 0.3s ease;
        }
        .aliexpress-container:hover {
            background-color: #ffe6cc;
        }
        .aliexpress-link {
            text-decoration: none;
            color: inherit;
            font-weight: bold;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        .aliexpress-icon {
            width: 24px;
            margin-right: 8px;
        }
        .footer {
            text-align: right;
            font-size: 0.7em;
            color: #666;
            margin-top: 10px;
        }
        .footer-logo {
            width: 20px;
            height: 20px;
            vertical-align: middle;
            margin-right: 5px;
        }
        .control-button {
            padding: 5px 10px;
            border: 1px solid #ccc;
            border-radius: 5px;
            background-color: #f9f9f9;
            cursor: pointer;
        }
        .control-button.active {
            background-color: #ff9900 !important;
        }
        .checkbox-container {
            display: flex;
            align-items: center;
            flex-direction: column;
        }
        .checkbox-label {
            font-weight: normal;
        }
        .chart-container {
            margin-top: 20px;
            text-align: center;
        }
        .chart-image {
            max-width: 100%;
            height: auto;
            border: 1px solid #ccc;
            border-radius: 8px;
        }
        `;
        const styleSheet = document.createElement('style');
        styleSheet.type = 'text/css';
        styleSheet.innerText = styles;
        document.head.appendChild(styleSheet);
    }

    function createLoadingContainer() {
        const priceElement = document.querySelector('.priceToPay, #priceblock_ourprice, #priceblock_dealprice, #priceblock_saleprice');
        if (priceElement && priceElement.parentNode) {
            const container = document.createElement('div');
            container.id = 'amazonPriceComparisonContainer';
            container.innerHTML = `
                <div id="loadingMessage">
                    <img src="https://i.ibb.co/qrjrcVy/amz-price-checker.png" alt="Loading logo" style="width: 50px; height: 50px; margin-bottom: 10px;">
                    Checking other Amazon sites...
                </div>`;
            priceElement.parentNode.appendChild(container);
        }
    }

    function fetchPricesFromOtherSites() {
        amazonSites.forEach(site => {
            const url = `https://www.amazon.${site.country}/dp/${asin}?tag=${PARTNER_IDS[site.country]}`;
            GM_xmlhttpRequest({
                method: 'GET',
                url: url,
                headers: { 'User-Agent': 'Mozilla/5.0', 'Accept-Language': 'en-US,en;q=0.5' },
                onload: response => handleResponse(site, response),
                onerror: () => handleResponse(site, null)
            });
        });
    }

    function handleResponse(site, response) {
        requestCount++;
        if (response && response.status === 200) {
            const parser = new DOMParser();
            const doc = parser.parseFromString(response.responseText, 'text/html');
            const price = getPriceFromDocument(doc);
            const deliveryPrice = getDeliveryPriceFromDocument(doc);
            if (price !== null) {
                priceResults.push({ ...site, price, deliveryPrice });
            }
        }
        if (requestCount === amazonSites.length) {
            displayAllResults();
        }
    }

    function displayAllResults() {
        const priceContainer = document.querySelector('#amazonPriceComparisonContainer');
        if (!priceContainer) return;
        priceContainer.innerHTML = '';

        createComparisonTable(priceContainer);
        addControls(priceContainer);
        addCamelCamelCamelChart(priceContainer);
        addAliExpressLink(priceContainer);
        addFooter(priceContainer);
    }

    function createComparisonTable(priceContainer) {
        const headerRow = document.createElement('div');
        headerRow.className = 'comparison-row header-row';
        ['Site', 'Price', 'Delivery', 'Total', 'Difference'].forEach(header => {
            const headerCell = createCell(header, true);
            headerRow.appendChild(headerCell);
        });
        priceContainer.appendChild(headerRow);

        priceResults.sort((a, b) => (a.price + a.deliveryPrice) - (b.price + b.deliveryPrice));

        priceResults.forEach(result => {
            const row = document.createElement('div');
            row.className = 'comparison-row';
            row.onclick = () => window.open(`https://www.amazon.${result.country}/dp/${asin}?tag=${PARTNER_IDS[result.country]}`, '_blank');

            const totalPrice = result.price + (result.deliveryPrice || 0);
            const difference = totalPrice - basePrice;
            const differencePercentage = ((difference / basePrice) * 100).toFixed(2);
            const differenceClass = difference < 0 ? 'price-difference-positive' : difference > 0 ? 'price-difference-negative' : '';

            row.append(
                createCell(`<img src="${result.flag}" alt="${result.name} flag" style="vertical-align: middle; margin-right: 5px;"> ${result.name}`),
                createCell(`€${result.price.toFixed(2)}`),
                createCell(result.deliveryPrice ? `<img src="https://img.icons8.com/?size=100&id=12248&format=png&color=000000" width="20"/> €${result.deliveryPrice.toFixed(2)}` : '-'),
                createCell(`€${totalPrice.toFixed(2)}`),
                createCell(difference !== 0 ? `<span class="${differenceClass}">${difference >= 0 ? '+' : ''}€${difference.toFixed(2)} (${differencePercentage}%)</span>` : '-')
            );

            priceContainer.appendChild(row);
        });
    }

    function createCell(content, isHeader = false) {
        const cell = document.createElement('div');
        cell.style.flex = '1';
        cell.style.textAlign = 'center';
        cell.innerHTML = content;
        if (isHeader) {
            cell.style.fontWeight = 'bold';
        }
        return cell;
    }

    function addControls(priceContainer) {
        const controlsContainer = document.createElement('div');
        controlsContainer.className = 'controls-container';

        const timePeriods = [
            { id: 'btn1Month', label: '1 Month', value: '1m' },
            { id: 'btn3Months', label: '3 Months', value: '3m' },
            { id: 'btn6Months', label: '6 Months', value: '6m' },
            { id: 'btn1Year', label: '1 Year', value: '1y' },
            { id: 'btnAll', label: 'All', value: 'all' }
        ];

        timePeriods.forEach(period => {
            const button = document.createElement('button');
            button.id = period.id;
            button.textContent = period.label;
            button.className = `control-button ${period.value === selectedTimePeriod ? 'active' : ''}`;

            button.addEventListener('click', () => {
                selectedTimePeriod = period.value;
                document.querySelectorAll('.control-button').forEach(btn => {
                    btn.classList.remove('active');
                });
                button.classList.add('active');
                updateChartUrl();
            });

            controlsContainer.appendChild(button);
        });

        const checkboxes = [
            { id: 'checkboxAmazon', label: 'Amazon', filename: 'amazon', disabled: true, checked: true },
            { id: 'checkboxNew', label: 'New', filename: 'new', checked: false },
            { id: 'checkboxUsed', label: 'Used', filename: 'used', checked: false }
        ];

        checkboxes.forEach(checkbox => {
            const container = document.createElement('div');
            container.className = 'checkbox-container';

            const checkboxElement = document.createElement('input');
            checkboxElement.type = 'checkbox';
            checkboxElement.id = checkbox.id;
            checkboxElement.checked = checkbox.checked;
            if (checkbox.disabled) checkboxElement.disabled = true;

            checkboxElement.addEventListener('change', updateChartUrl);

            const labelElement = document.createElement('label');
            labelElement.htmlFor = checkbox.id;
            labelElement.textContent = checkbox.label;
            labelElement.className = 'checkbox-label';

            container.append(checkboxElement, labelElement);
            controlsContainer.appendChild(container);
        });

        priceContainer.appendChild(controlsContainer);
    }

    function addCamelCamelCamelChart(priceContainer) {
        const chartContainer = document.createElement('div');
        chartContainer.className = 'chart-container';

        const countryCode = getCurrentCountryCode();
        const chartUrl = getCamelChartUrl(countryCode, asin, selectedTimePeriod);
        const camelUrl = `https://${countryCode}.camelcamelcamel.com/product/${asin}`;
        chartContainer.innerHTML = `<a href="${camelUrl}" target="_blank"><img src="${chartUrl}" alt="Price history for ${asin}" class="chart-image"></a>`;

        priceContainer.appendChild(chartContainer);
    }

    function addAliExpressLink(priceContainer) {
        const titleElement = document.querySelector('#productTitle');
        const productTitle = titleElement ? titleElement.textContent.trim() : null;

        if (!productTitle) return;

        // Simplify the product title by removing unnecessary words
        const simplifiedTitle = simplifyProductTitle(productTitle);

        const searchUrl = `https://www.aliexpress.com/wholesale?SearchText=${encodeURIComponent(simplifiedTitle)}`;
        const aliexpressIcon = 'https://cdn.icon-icons.com/icons2/2699/PNG/96/aliexpress_logo_icon_167892.png';

        const aliexpressContainer = document.createElement('div');
        aliexpressContainer.className = 'aliexpress-container';

        aliexpressContainer.innerHTML = `
            <a href="${searchUrl}" target="_blank" class="aliexpress-link">
                <img src="${aliexpressIcon}" alt="AliExpress Icon" class="aliexpress-icon">
                Check this product on AliExpress
            </a>
        `;

        priceContainer.appendChild(aliexpressContainer);
    }

    // Function to simplify the product title by removing stop words in multiple languages
    function simplifyProductTitle(title) {
        const stopWords = [
            // English
            'the', 'and', 'or', 'for', 'with', 'a', 'an', 'of', 'in', 'on', 'at', 'by', 'from', 'to',
            // French
            'le', 'la', 'les', 'un', 'une', 'des', 'du', 'de', 'et', 'ou', 'pour', 'avec', 'en', 'sur', 'au', 'aux', 'dans', 'par', 'à',
            // Spanish
            'el', 'la', 'los', 'las', 'un', 'una', 'unos', 'unas', 'de', 'y', 'o', 'para', 'con', 'en', 'sobre', 'al', 'a', 'del', 'por',
            // Italian
            'il', 'la', 'i', 'gli', 'le', 'un', 'una', 'uno', 'dei', 'delle', 'degli', 'di', 'e', 'o', 'per', 'con', 'in', 'su', 'al', 'allo', 'della', 'dell', 'da', 'dal', 'dalla',
            // German
            'der', 'die', 'das', 'und', 'oder', 'für', 'mit', 'ein', 'eine', 'einer', 'eines', 'einem', 'einen', 'den', 'dem', 'des', 'von', 'zu', 'im', 'an', 'auf', 'bei', 'als', 'aus', 'nach', 'bei', 'gegen', 'um', 'durch', 'über', 'unter', 'am', 'vom'
        ];

        const words = title.toLowerCase().split(/\s+/);
        const filteredWords = words.filter(word => !stopWords.includes(word) && word.length > 1);
        return filteredWords.join(' ');
    }

    function addFooter(container) {
        const footer = document.createElement('div');
        footer.className = 'footer';
        footer.innerHTML = `
            <img src="https://i.ibb.co/qrjrcVy/amz-price-checker.png" alt="Logo" class="footer-logo">
            European Price Checker for Amazon by bNj v${GM_info.script.version}
        `;
        container.appendChild(footer);
    }

    function getCurrentCountryCode() {
        const hostname = window.location.hostname;
        if (hostname.includes('amazon.de')) return 'de';
        if (hostname.includes('amazon.es')) return 'es';
        if (hostname.includes('amazon.it')) return 'it';
        return 'fr';
    }

    function getCamelChartUrl(countryCode, asin, timePeriod) {
        const selectedFilenames = getSelectedFilenames();
        return `https://charts.camelcamelcamel.com/${countryCode}/${asin}/${selectedFilenames}.png?force=1&zero=0&w=600&h=300&desired=false&legend=1&ilt=1&tp=${timePeriod}&fo=0&lang=en`;
    }

    function getSelectedFilenames() {
        const checkboxes = [
            { id: 'checkboxAmazon', filename: 'amazon' },
            { id: 'checkboxNew', filename: 'new' },
            { id: 'checkboxUsed', filename: 'used' }
        ];

        return Array.from(document.querySelectorAll('input[type="checkbox"]:checked'))
            .map(checkbox => checkboxes.find(cb => cb.id === checkbox.id)?.filename)
            .filter(Boolean)
            .join('-');
    }

    function updateChartUrl() {
        const countryCode = getCurrentCountryCode();
        const chartUrl = getCamelChartUrl(countryCode, asin, selectedTimePeriod);
        const camelUrl = `https://${countryCode}.camelcamelcamel.com/product/${asin}`;
        const chartImage = document.querySelector('#amazonPriceComparisonContainer img[alt^="Price history"]');
        if (chartImage) {
            chartImage.src = chartUrl;
            chartImage.parentElement.href = camelUrl;
        }
    }

    function getPriceFromDocument(doc) {
        const priceElement = doc.querySelector('.priceToPay, #priceblock_ourprice, #priceblock_dealprice, #priceblock_saleprice');
        if (!priceElement) return null;
        const priceText = priceElement.textContent;
        return parsePrice(priceText);
    }

    function parsePrice(priceText) {
        if (!priceText) return null;
        const cleanedText = priceText.replace(/[^0-9,\.]/g, '').replace(',', '.');
        const price = parseFloat(cleanedText);
        return isNaN(price) ? null : price;
    }

    function getDeliveryPriceFromDocument(doc) {
        const deliveryMatch = doc.body.innerHTML.match(/data-csa-c-delivery-price="[^"]*?(\d+[.,]\d{2})/);
        if (deliveryMatch) {
            const priceStr = deliveryMatch[1].replace(',', '.');
            const price = parseFloat(priceStr);
            return isNaN(price) ? 0 : price;
        }
        return 0;
    }

    // Start the script
    main();
})();