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

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

    let requestCount = 0;

    const updateRequestCount = () => {
        requestCount++;
        if (requestCount === amazonSites.length) {
            removeLoadingIndicator();
        }
    };

    const createLoadingContainer = () => {
        console.log('Creating loading container');
        const priceElement = document.querySelector('.priceToPay, #priceblock_ourprice, #priceblock_dealprice, #priceblock_saleprice');
        if (priceElement) {
            console.log('Price element found');
            if (priceElement.parentNode) {
                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);
                console.log('Loading container added to the DOM');
                animateLoadingText();
                basePrice = getPriceFromPage(priceElement.textContent);
                if (basePrice === null || isNaN(basePrice)) {
                    console.error('Base price is invalid:', basePrice);
                    return;
                }
                console.log('Base price:', basePrice);
            } else {
                console.log('Price element has no parent node');
            }
        } else {
            console.log('Price element not found');
        }
    };

    const animateLoadingText = () => {
        console.log('Starting loading text animation');
        const text = document.getElementById('animatedText');
        if (text) {
            console.log('Animated text element found');
            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);
        } else {
            console.log('Animated text element not found');
        }
    };

    const removeLoadingIndicator = () => {
        console.log('Removing loading indicator');
        const loadingMessage = document.getElementById('loadingMessage');
        if (loadingMessage) {
            loadingMessage.style.transition = 'opacity 1s';
            loadingMessage.style.opacity = '0';
            setTimeout(() => {
                loadingMessage.remove();
                console.log('Loading indicator removed');
                displayAllResults();
            }, 1000);
        } else {
            console.error('Loading message not found when trying to remove it');
        }
    };

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

    const getPriceFromResponse = (responseText) => {
        console.log('Extracting price from response');
        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>/);
        if (priceMatch) {
            console.log('Price extracted from response:', priceMatch);
        } else {
            console.error('No price found in response');
        }
        return priceMatch ? parseFloat(`${priceMatch[1]}.${priceMatch[2]}`) : null;
    };

    const getDeliveryPriceFromResponse = (responseText) => {
        console.log('Extracting delivery price from response');
        const deliveryMatch = responseText.match(/data-csa-c-delivery-price="[^"]*(\d+[\.,]\d{2})/);
        if (deliveryMatch) {
            console.log('Delivery price extracted:', deliveryMatch[1]);
        } else {
            console.error('No delivery price found in response');
        }
        return deliveryMatch ? parseFloat(deliveryMatch[1].replace(',', '.')) : null;
    };

    const addPriceToPage = (siteName, price, deliveryPrice, url, flag, percentageDifference, totalPercentageDifference) => {
        console.log('Adding price to page for site:', siteName);
        const priceContainer = document.querySelector('#amazonPriceComparisonContainer');
        if (!priceContainer) {
            console.error('Price container not found');
            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 = () => {
        console.log('Adding CamelCamelCamel chart');
        const priceContainer = document.querySelector('#amazonPriceComparisonContainer');
        if (!priceContainer) {
            console.error('Price container not found for CamelCamelCamel chart');
            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) => {
        console.log('Storing price result for site:', siteName, 'Price:', price, 'Delivery Price:', deliveryPrice);
        if (price !== null) {
            priceResults.push({ siteName, price, deliveryPrice, url, flag });
        }
        console.log('Current stored results:', priceResults);
    };

    const displayAllResults = () => {
        console.log('Displaying all results');
        const priceContainer = document.querySelector('#amazonPriceComparisonContainer');
        if (!priceContainer) {
            console.error('Price container not found');
            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) {
            console.log('Sorting and displaying price results:', priceResults);
            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 {
            console.log('No price results available');
            priceContainer.innerHTML = '<div style="text-align: center;">No prices available</div>';
        }

        addCamelCamelCamelChart();
    };

    createLoadingContainer();

   amazonSites.forEach(site => {
        console.log('Requesting price for', site.name);
        GM_xmlhttpRequest({
            method: 'GET',
            url: site.url,
            headers: {
                'User-Agent': 'Mozilla/5.0',
                'Accept-Language': 'en-US,en;q=0.5'
            },
            onload: function(response) {
                console.log('Response received for', site.name, 'Status:', response.status);
                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();
                }
                updateRequestCount();
            },
            onerror: function() {
                console.error('Error requesting price for', site.name);
                storePriceResult(site.name, null, null, site.url, site.flag);
                if (priceResults.length === amazonSites.length) {
                    removeLoadingIndicator();
                }
                updateRequestCount();
            }
        });
    });
})();