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-10-28 提交的版本,查看 最新版本

// ==UserScript==
// @name         European Price Checker for Amazon (fr, de, es, it)
// @namespace    http://tampermonkey.net/
// @version      2.0
// @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';

    const ASIN_REGEX = /\/([A-Z0-9]{10})(?:[/?]|$)/;
    const asinMatch = window.location.href.match(ASIN_REGEX);

    if (!asinMatch) {
        return;
    }

    const asin = asinMatch[1];
    const amazonSites = [
        { name: 'Amazon.fr', url: `https://www.amazon.fr/dp/${asin}`, domain: 'amazon.fr', flag: 'https://flagcdn.com/w20/fr.png' },
        { name: 'Amazon.es', url: `https://www.amazon.es/dp/${asin}`, domain: 'amazon.es', flag: 'https://flagcdn.com/w20/es.png' },
        { name: 'Amazon.it', url: `https://www.amazon.it/dp/${asin}`, domain: 'amazon.it', flag: 'https://flagcdn.com/w20/it.png' },
        { name: 'Amazon.de', url: `https://www.amazon.de/dp/${asin}`, domain: 'amazon.de', flag: 'https://flagcdn.com/w20/de.png' }
    ];

    let basePrice = null;

    const createLoadingContainer = () => {
        const priceElement = document.querySelector('.priceToPay, #priceblock_ourprice, #priceblock_dealprice');
        if (priceElement) {
            const container = document.createElement('div');
            container.id = 'amazonPriceComparisonContainer';
            container.style.cssText = 'margin-top: 20px; padding: 10px; background-color: #f9f9f9; border: 1px solid #ccc; border-radius: 8px; position: relative; font-size: 12px;';
            container.innerHTML = `
                <div id="loadingMessage" style="text-align: center; font-weight: bold; position: relative;">
                    <img src="https://i.ibb.co/qrjrcVy/amz-price-checker.png" alt="European Price Checker Logo" style="width: 50px; height: 50px; margin-bottom: 10px;"><br>
                    <span id="animatedText">Checking other Amazon sites...</span>
                </div>`;
            priceElement.parentNode.appendChild(container);
            animateLoadingText();
            basePrice = getPriceFromPage(priceElement.textContent);
        }
    };

    const animateLoadingText = () => {
        const text = document.getElementById('animatedText');
        if (text) {
            let position = 0;
            setInterval(() => {
                position = (position + 2) % 100;
                text.style.cssText = `
                    background-image: linear-gradient(90deg, black 0%, black ${position - 20}%, #FF9900 ${position}%, black ${position + 20}%, black 100%);
                    background-clip: text;
                    -webkit-background-clip: text;
                    color: transparent;
                    font-weight: bold;
                    font-size: 14px;
                `;
            }, 80);
        }
    };

    const removeLoadingIndicator = () => {
        const loadingMessage = document.getElementById('loadingMessage');
        if (loadingMessage) {
            loadingMessage.style.transition = 'opacity 1s';
            loadingMessage.style.opacity = '0';
            setTimeout(() => {
                loadingMessage.remove();
                displayAllResults();
            }, 1000);
        }
    };

    const getPriceFromPage = (priceText) => {
        priceText = priceText.replace(/\s+/g, '');
        const priceMatch = priceText.match(/\d+[\.,]\d{2}/);
        return priceMatch ? parseFloat(priceMatch[0].replace(',', '.')) : null;
    };

    const getPriceFromResponse = (responseText) => {
        const priceMatch = responseText.match(/<span class="a-price-whole">(\d+)<span class="a-price-decimal">[.,]<\/span><\/span><span class="a-price-fraction">(\d{2})<\/span>/) ||
                          responseText.match(/<span class="a-price aok-align-center reinventPricePriceToPayMargin priceToPay"[^>]*>.*?<span class="a-price-whole">(\d+)<span class="a-price-decimal">(.)<\/span><\/span><span class="a-price-fraction">(\d{2})<\/span>/);
        return priceMatch ? parseFloat(`${priceMatch[1]}.${priceMatch[2]}`) : null;
    };

    const getDeliveryPriceFromResponse = (responseText) => {
        const deliveryMatch = responseText.match(/data-csa-c-delivery-price="[^"]*(\d+[\.,]\d{2})/);
        return deliveryMatch ? parseFloat(deliveryMatch[1].replace(',', '.')) : null;
    };

    const addPriceToPage = (siteName, price, deliveryPrice, url, flag, percentageDifference, totalPercentageDifference) => {
        const priceContainer = document.querySelector('#amazonPriceComparisonContainer');
        if (!priceContainer) return;

        const row = document.createElement('div');
        row.className = 'comparison-row';
        row.dataset.totalPrice = price + (deliveryPrice || 0); // Store total price for sorting
        row.style.cssText = 'cursor: pointer; display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 1px solid #ccc;';
        row.onmouseover = () => row.style.backgroundColor = '#f1f1f1';
        row.onmouseout = () => row.style.backgroundColor = 'inherit';
        row.onclick = () => window.open(url, '_blank');

        const createCell = (content, style = '') => {
            const cell = document.createElement('div');
            cell.style.flex = '1';
            cell.style.textAlign = 'center';
            cell.style.cssText += style;
            cell.innerHTML = content;
            return cell;
        };

        const totalPrice = price + (deliveryPrice || 0);
        row.append(
            createCell(`<img src="${flag}" alt="${siteName} flag" style="vertical-align: middle; margin-right: 5px;"> ${siteName}`),
            createCell(`€${price.toFixed(2)}`),
            createCell(percentageDifference !== null ? `<span style="color: ${percentageDifference > 0 ? 'red' : 'green'};">${percentageDifference > 0 ? '+' : ''}${percentageDifference.toFixed(2)}% (€${(price - basePrice).toFixed(2)})</span>` : '-'),
            createCell(deliveryPrice !== null ? `<img src="https://img.icons8.com/?size=100&id=12248&format=png&color=000000" width="20"/> €${deliveryPrice.toFixed(2)}` : '-'),
            createCell(`<strong>€${totalPrice.toFixed(2)}</strong>`),
            createCell(totalPercentageDifference !== null ? `<span style="color: ${totalPercentageDifference > 0 ? 'red' : 'green'};">${totalPercentageDifference > 0 ? '+' : ''}${totalPercentageDifference.toFixed(2)}% (€${(totalPrice - basePrice).toFixed(2)})</span>` : '-')
        );

        priceContainer.appendChild(row);
    };

    const addCamelCamelCamelChart = () => {
        const priceContainer = document.querySelector('#amazonPriceComparisonContainer');
        if (!priceContainer) return;

        const chartContainer = document.createElement('div');
        chartContainer.style.marginTop = '20px';
        chartContainer.style.textAlign = 'center';

        let countryCode = 'fr';
        if (window.location.hostname.includes('amazon.de')) countryCode = 'de';
        else if (window.location.hostname.includes('amazon.es')) countryCode = 'es';
        else if (window.location.hostname.includes('amazon.it')) countryCode = 'it';

        const controlsContainer = document.createElement('div');
        controlsContainer.style.cssText = 'text-align: center; margin: 10px; display: flex; justify-content: center; align-items: center; gap: 10px;';

        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' }
        ];

        let selectedTimePeriod = 'all';

        timePeriods.forEach(period => {
            const button = document.createElement('button');
            button.id = period.id;
            button.textContent = period.label;
            button.style.cssText = `
                padding: 5px 10px;
                border: 1px solid #ccc;
                border-radius: 5px;
                background-color: ${period.value === selectedTimePeriod ? '#ff9900' : '#f9f9f9'};
                cursor: pointer;
            `;

            button.addEventListener('click', () => {
                selectedTimePeriod = period.value;
                timePeriods.forEach(p => {
                    document.getElementById(p.id).style.backgroundColor = p.value === selectedTimePeriod ? '#ff9900' : '#f9f9f9';
                });
                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.style.display = 'flex';
            container.style.alignItems = 'center';

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

            const labelElement = document.createElement('label');
            labelElement.htmlFor = checkbox.id;
            labelElement.textContent = checkbox.label;
            labelElement.style.fontWeight = 'normal';

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

        priceContainer.appendChild(controlsContainer);

        const updateChartUrl = () => {
            const selectedFilenames = checkboxes
                .filter(checkbox => document.getElementById(checkbox.id).checked)
                .map(checkbox => checkbox.filename)
                .join('-');

            const chartUrl = `https://charts.camelcamelcamel.com/${countryCode}/${asin}/${selectedFilenames}.png?force=1&zero=0&w=600&h=300&desired=false&legend=1&ilt=1&tp=${selectedTimePeriod}&fo=0&lang=en`;
            const camelUrl = `https://${countryCode}.camelcamelcamel.com/product/${asin}`;
            chartContainer.innerHTML = `<a href="${camelUrl}" target="_blank"><img src="${chartUrl}" alt="Price history chart for ${asin}" style="max-width: 100%; height: auto; border: 1px solid #ccc; border-radius: 8px;"></a>`;
        };

        checkboxes.forEach(checkbox => {
            document.getElementById(checkbox.id).addEventListener('change', updateChartUrl);
        });

        updateChartUrl();
        priceContainer.appendChild(chartContainer);

        // Add footer with script info after the chart
        const footer = document.createElement('div');
        footer.style.cssText = 'text-align: right; font-size: 0.7em; color: #666; margin-top: 10px;';
        footer.innerHTML = `<img src="https://i.ibb.co/qrjrcVy/amz-price-checker.png" alt="Logo" style="width: 20px; height: 20px; vertical-align: middle; margin-right: 5px;"> European Price Checker for Amazon by bNj v${GM_info.script.version}`;
        priceContainer.appendChild(footer);
    };

    let priceResults = [];

    const storePriceResult = (siteName, price, deliveryPrice, url, flag) => {
        if (price !== null) {
            priceResults.push({ siteName, price, deliveryPrice, url, flag });
        }
    };

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

        priceContainer.innerHTML = ''; // Clear loading message and icon

        const headerRow = document.createElement('div');
        headerRow.className = 'comparison-row header-row';
        headerRow.style.cssText = 'display: flex; justify-content: space-between; padding: 5px 0; border-bottom: 2px solid #000; font-weight: bold;';

        ['Site', 'Price', 'Difference (Price %)', 'Delivery', 'Total', 'Difference (Total %)'].forEach(header => {
            const headerCell = document.createElement('div');
            headerCell.style.flex = '1';
            headerCell.style.textAlign = 'center';
            headerCell.textContent = header;
            headerRow.appendChild(headerCell);
        });

        priceContainer.appendChild(headerRow);

        if (priceResults.length > 0 && basePrice !== null) {
            priceResults.sort((a, b) => (a.price + (a.deliveryPrice || 0)) - (b.price + (b.deliveryPrice || 0)));

            priceResults.forEach(result => {
                const percentageDifference = ((result.price - basePrice) / basePrice) * 100;
                const totalPrice = result.price + (result.deliveryPrice || 0);
                const totalBasePrice = basePrice;
                const totalPercentageDifference = ((totalPrice - totalBasePrice) / totalBasePrice) * 100;
                addPriceToPage(result.siteName, result.price, result.deliveryPrice, result.url, result.flag, percentageDifference, totalPercentageDifference);
            });
        } else {
            priceContainer.innerHTML = '<div style="text-align: center;">No prices available</div>';
        }

        addCamelCamelCamelChart();
    };

    createLoadingContainer();

    amazonSites.forEach(site => {
        GM_xmlhttpRequest({
            method: 'GET',
            url: site.url,
            headers: {
                'User-Agent': 'Mozilla/5.0',
                'Accept-Language': 'en-US,en;q=0.5'
            },
            onload: function(response) {
                if (response.status === 200) {
                    const price = getPriceFromResponse(response.responseText);
                    const deliveryPrice = getDeliveryPriceFromResponse(response.responseText);
                    storePriceResult(site.name, price, deliveryPrice, site.url, site.flag);
                } else {
                    storePriceResult(site.name, null, null, site.url, site.flag);
                }
                if (priceResults.length === amazonSites.length) {
                    removeLoadingIndicator();
                }
            },
            onerror: function() {
                storePriceResult(site.name, null, null, site.url, site.flag);
                if (priceResults.length === amazonSites.length) {
                    removeLoadingIndicator();
                }
            }
        });
    });
})();