Amazon Price Checker (FR, DE, ES, IT, BE, NL, UK, COM, PL) by bNj

Compare Amazon prices across different country sites with a leaner, faster script.

// ==UserScript==
// @name         Amazon Price Checker (FR, DE, ES, IT, BE, NL, UK, COM, PL) by bNj
// @namespace    http://tampermonkey.net/
// @version      4.01
// @description  Compare Amazon prices across different country sites with a leaner, faster script.
// @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/*
// @match        https://www.amazon.com.be/*
// @match        https://www.amazon.nl/*
// @match        https://www.amazon.co.uk/*
// @match        https://www.amazon.com/*
// @match        https://www.amazon.pl/*
// @grant        GM_xmlhttpRequest
// @connect      amazon.fr
// @connect      amazon.de
// @connect      amazon.es
// @connect      amazon.it
// @connect      amazon.com.be
// @connect      amazon.nl
// @connect      amazon.pl
// @connect      amazon.co.uk
// @connect      amazon.com
// @connect      summarizer.mon-bnj.workers.dev
// @connect      api.frankfurter.app
// @connect      alisearch.bnjnas.synology.me
// @license      All Rights Reserved
// @antifeature  referral-link
// @antifeature  tracking
// ==/UserScript==
(function(){
  'use strict';
  const ASIN_RE = /\/([A-Z0-9]{10})(?:[/?]|$)/,
        PARTNER_IDS = {
          fr:'bnjmazon-21',
          es:'bnjmazon08-21',
          it:'bnjmazon0d-21',
          de:'geeksince190d-21',
          'com.be':'geeksince1900',
          nl:'bnjmazon-21',
          pl:'bnjmazon-20',
          'co.uk':'bnjmazon-UK-21',
          com:'bnjmazon-20'
        },
        sites = [
          {name:'Amazon.fr',    c:'fr',     f:'https://flagcdn.com/w20/fr.png', cur:'EUR'},
          {name:'Amazon.es',    c:'es',     f:'https://flagcdn.com/w20/es.png', cur:'EUR'},
          {name:'Amazon.it',    c:'it',     f:'https://flagcdn.com/w20/it.png', cur:'EUR'},
          {name:'Amazon.de',    c:'de',     f:'https://flagcdn.com/w20/de.png', cur:'EUR'},
          {name:'Amazon.be',    c:'com.be', f:'https://flagcdn.com/w20/be.png', cur:'EUR'},
          {name:'Amazon.nl',    c:'nl',     f:'https://flagcdn.com/w20/nl.png', cur:'EUR'},
          {name:'Amazon.pl',    c:'pl',     f:'https://flagcdn.com/w20/pl.png', cur:'PLN'},
          {name:'Amazon.co.uk', c:'co.uk',  f:'https://flagcdn.com/w20/gb.png', cur:'GBP'},
          {name:'Amazon.com',   c:'com',    f:'https://flagcdn.com/w20/us.png', cur:'USD'}
        ];
  let asin, basePrice, selPeriod = 'all', firstLoaded = false, exRates,
      tableCont, chartCont, selEl, checks = [];

  // Modification : Charger les taux avant d'obtenir le prix de référence.
  function main(){
    if(!extractASIN()) return;
    fetchExRates().then(() => {
      if(!getBasePrice()) return;
      injectStyles();
      createBaseUI();
      fetchPrices();
    });
  }

  function extractASIN(){
    const m = location.href.match(ASIN_RE);
    if(!m) return false;
    asin = m[1];
    return true;
  }
  function getBasePrice(){
    basePrice = getPrice(document, getCurrentCountry());
    return basePrice !== null;
  }
  function injectStyles(){
    const css = `:root{--a:#FF9900;--bg:#fff;--font:Arial,sans-serif;--tc:#333;--bc:#ddd}
body{font-family:var(--font)!important}
#amz-checker-container{background:var(--bg);border:1px solid var(--bc);border-radius:10px;box-shadow:0 2px 6px rgba(0,0,0,0.1);font-size:12px;color:var(--tc);margin:0 auto;display:flex;flex-direction:column}
#amz-checker-header{background:var(--a);color:#fff;padding:5px 10px;border-radius:10px 10px 0 0;display:flex;align-items:center;gap:10px}
#amz-checker-header img{width:36px;height:36px}
#amz-checker-title{font-size:14px;font-weight:bold}
.loading-text-gradient{background-clip:text;color:transparent;background-image:linear-gradient(270deg,black 0%,black 20%,var(--a) 50%,black 80%,black 100%);background-size:200% 100%;animation:loadAnim 2s linear infinite}
@keyframes loadAnim{0%{background-position:100% 50%}100%{background-position:0 50%}}
#loadingMessage{text-align:center;font-weight:bold;font-size:13px;display:flex;flex-direction:column;align-items:center;margin:10px 0}
.amz-checker-content{padding:10px;flex:1}
#comparison-table{border:1px solid var(--bc);border-radius:8px;overflow:hidden;margin-bottom:15px}
.comparison-row{display:flex;justify-content:space-between;padding:5px 10px;border-bottom:1px solid var(--bc);cursor:pointer;transition:background 0.2s}
.comparison-row:hover{background:#f5f5f5}
.comparison-row.header-row{background:#eee;font-weight:bold;cursor:default}
.comparison-row.header-row:hover{background:#eee}
.comparison-row:last-child{border-bottom:none}
.comparison-row>div{text-align:center;flex:1}
.first-col{flex:0 0 120px;text-align:left !important;overflow:hidden}
.price-difference-positive{color:#008000}
.price-difference-negative{color:#f00}
.chart-container{margin-bottom:15px;border:1px solid var(--bc);border-radius:8px;padding:10px;position:relative;min-height:300px;text-align:center}
.chart-container .loader{position:absolute;top:50%;left:50%;margin:-24px 0 0 -24px}
.chart-controls{display:flex;align-items:center;gap:15px;margin-bottom:10px;flex-wrap:wrap;justify-content:center}
.chart-controls .checkbox-container{display:flex;align-items:center;font-size:12px}
.chart-controls .checkbox-label{margin-left:4px}
.chart-controls select{padding:3px 6px;font-size:12px}
.loader{position:relative;width:48px;height:48px;border-radius:50%;display:inline-block;border-top:4px solid #FFF;border-right:4px solid transparent;box-sizing:border-box;animation:rot 1s linear infinite}
.loader::after{content:'';box-sizing:border-box;position:absolute;left:0;top:0;width:48px;height:48px;border-radius:50%;border-left:4px solid #FF3D00;border-bottom:4px solid transparent;animation:rot .5s linear infinite reverse}
@keyframes rot{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
.chart-image{max-width:100%;margin-top:10px}
.aliexpress-wrapper{margin-bottom:15px}
.aliexpress-container{display:flex;align-items:center;justify-content:center;gap:8px;color:#ff5722;font-weight:bold;border:1px solid var(--bc);border-radius:6px;padding:8px 12px;cursor:pointer;transition:background 0.2s}
.aliexpress-container:hover{background:#fff8f0}
.aliexpress-icon{width:24px}
.aliexpress-results{margin-top:10px;display:flex;flex-wrap:wrap;gap:10px;justify-content:space-evenly}
.aliexpress-card{border:1px solid var(--bc);border-radius:4px;padding:5px;width:140px;text-align:center;box-shadow:0 2px 4px rgba(0,0,0,0.1);background:#fff}
.aliexpress-card img{width:100%;border-radius:4px 4px 0 0}
.product-summary-encart{border:1px solid var(--bc);border-radius:8px;padding:10px;background:#f9f9f9;margin-bottom:15px}
._Y3Itc_selected_2-xMA{font-weight:bold!important}
#amz-checker-footer{text-align:right;font-size:0.8em;color:#666;background:#fafafa;border-top:1px solid var(--bc);border-radius:0 0 10px 10px;padding:5px 10px}
#amz-checker-footer .footer-logo{width:18px;height:18px;vertical-align:middle;margin-right:5px}`;
    let s = document.createElement('style');
    s.type = 'text/css';
    s.textContent = css;
    document.head.appendChild(s);
  }
  function createBaseUI(){
    let c = document.createElement('div');
    c.id = 'amz-checker-container';
    c.innerHTML = `<div id="amz-checker-header"><img src="https://i.ibb.co/qrjrcVy/amz-price-checker.png" alt="Logo"/><span id="amz-checker-title">Amazon Price Checker</span></div>
      <div class="amz-checker-content"><div id="loadingMessage" class="loading-text-gradient">Checking other Amazon sites...</div></div>`;
    let p = document.querySelector('.priceToPay,#priceblock_ourprice,#priceblock_dealprice,#priceblock_saleprice');
    (p ? p.parentNode : document.body).appendChild(c);
  }
  function buildFinalUI(){
    let cnt = document.querySelector('#amz-checker-container .amz-checker-content');
    if(!cnt)return; cnt.innerHTML = '';
    addTable(cnt); addChart(cnt); /*addAliExpress(cnt);*/ addProductSummary(cnt); addFooter();
    updateChart();
  }
  function addTable(cnt){
    let tw = document.createElement('div'); tw.id = 'comparison-table';
    tableCont = document.createElement('div');
    let hr = document.createElement('div'); hr.className = 'comparison-row header-row';
    ['Site','Price (EUR)','Coupon','Delivery','Total','Difference'].forEach((h,i) => hr.appendChild(cell(h,true,i===0 ? 'first-col' : '')));
    tableCont.appendChild(hr); tw.appendChild(tableCont); cnt.appendChild(tw);
  }
  const cell = (txt, isH, ex) => {
    let d = document.createElement('div');
    d.innerHTML = txt;
    if(isH) d.style.fontWeight = 'bold';
    if(ex) d.classList.add(ex);
    return d;
  };
  function insertRow({ s, price, del, coupon, cur }){
    let tot = price - coupon + del, row = document.createElement('div'); row.className = 'comparison-row';
    row.onclick = () => window.open(`https://www.amazon.${s.c}/dp/${asin}?tag=${PARTNER_IDS[s.c]}`, '_blank');
    let diff = tot - basePrice, perc = ((diff/basePrice)*100).toFixed(0),
        dc = diff < 0 ? 'price-difference-positive' : diff > 0 ? 'price-difference-negative' : '';
    row.append(
      cell(`<img src="${s.f}" style="vertical-align:middle;margin-right:5px;width:20px;height:13px;"> ${s.name}`, false, 'first-col'),
      cell(showPrice(price, cur)),
      cell(coupon > 0 ? `- €${coupon.toFixed(2)}` : '-'),
      cell(del > 0 ? `+ €${del.toFixed(2)}` : '-'),
      cell(showPrice(tot, cur)),
      cell(diff !== 0 ? `<span class="${dc}">${diff >= 0 ? '+' : ''}€${diff.toFixed(2)} (${perc}%)</span>` : '-')
    );
    let rows = [...tableCont.querySelectorAll('.comparison-row:not(.header-row)')];
    let inserted = false;
    for(let r of rows){
      let t = parseFloat(r.children[4].textContent.replace(/[^0-9.,-]/g, '').replace(',', '.')) || 999999;
      if(tot < t){ tableCont.insertBefore(row, r); inserted = true; break; }
    }
    if(!inserted) tableCont.appendChild(row);
  }
  function addChart(cnt){
    chartCont = document.createElement('div'); chartCont.className = 'chart-container';
    let ctrl = document.createElement('div'); ctrl.className = 'chart-controls';
    selEl = document.createElement('select');
    [['1m','1 Month'], ['3m','3 Months'], ['6m','6 Months'], ['1y','1 Year'], ['all','All']].forEach(([v, l]) => {
      let o = document.createElement('option'); o.value = v; o.textContent = l; if(v === selPeriod) o.selected = true; selEl.appendChild(o);
    });
    selEl.onchange = () => { selPeriod = selEl.value; updateChart(); }; ctrl.appendChild(selEl);
    // Three checkboxes: Amazon (disabled), New, Used
    [['checkboxAmazon','Amazon','amazon', true, true], ['checkboxNew','New','new', false, true], ['checkboxUsed','Used','used', false, false]]
      .forEach(([id, label, fn, dis, chk]) => {
        let wrap = document.createElement('div'); wrap.className = 'checkbox-container';
        let inp = document.createElement('input'); inp.type = 'checkbox'; inp.id = id; inp.disabled = dis; inp.checked = chk; inp.onchange = updateChart;
        let lbl = document.createElement('label'); lbl.htmlFor = id; lbl.textContent = label; lbl.className = 'checkbox-label';
        wrap.append(inp, lbl); ctrl.appendChild(wrap); checks.push({ inp, fn });
      });
    chartCont.appendChild(ctrl);
    let spin = document.createElement('div'); spin.className = 'loader';
    let img = document.createElement('img'); img.alt = `Price history for ${asin}`; img.className = 'chart-image'; img.style.display = 'none';
    chartCont.append(spin, img); cnt.appendChild(chartCont);
  }
  function updateChart(){
    if(!chartCont)return;
    let cc = getCurrentCountry(), url = getChartUrl(cc, asin, selPeriod),
        spin = chartCont.querySelector('.loader'),
        img = chartCont.querySelector('.chart-image');
    spin.style.display = 'inline-block'; img.style.display = 'none';
    img.src = url;
    img.onload = () => { spin.style.display = 'none'; img.style.display = 'block'; };
    img.onerror = () => { spin.style.display = 'none'; img.style.display = 'block'; img.src = 'https://dummyimage.com/600x200/ccc/000&text=Image+Unavailable'; };
  }
  function getChartUrl(cc, a, tp){
    let f = checks.filter(c => c.inp.checked).map(c => c.fn).join('-'),
        base = `https://charts.camelcamelcamel.com/${cc}/${a}/${f}.png?force=1&zero=0&w=600&h=300&desired=false&legend=1&ilt=1&tp=${tp}&fo=0&lang=en`;
    return `https://camelcamelcamel.mon-bnj.workers.dev/?target=${encodeURIComponent(base)}`;
  }
  function addAliExpress(cnt){
    let wrap = document.createElement('div'); wrap.className = 'aliexpress-wrapper';
    let btn = document.createElement('div'); btn.className = 'aliexpress-container';
    btn.innerHTML = `<img src="https://img.icons8.com/color/48/aliexpress.png" class="aliexpress-icon"><span class="aliexpress-text">Check on AliExpress</span>`;
    btn.onclick = () => {
      let txt = btn.querySelector('.aliexpress-text');
      txt.textContent = 'Loading...'; txt.classList.add('loading-text-gradient');
      let imgEl = document.querySelector('#landingImage') || document.querySelector('#imgTagWrapperId img'),
          imgUrl = imgEl ? imgEl.src : "https://m.media-amazon.com/images/I/71sAMz1x82L.__AC_SX300_SY300_QL70_ML2_.jpg",
          url = "https://alisearch.bnjnas.synology.me/search?image_url=" + encodeURIComponent(imgUrl);
      GM_xmlhttpRequest({
        method:'GET', url,
        onload: r => {
          txt.classList.remove('loading-text-gradient'); txt.textContent = 'Check on AliExpress';
          try { displayAliRes(wrap, JSON.parse(r.responseText)); }
          catch(e){ txt.textContent = 'Error parsing result'; }
        },
        onerror: () => { txt.classList.remove('loading-text-gradient'); txt.textContent = 'Error fetching data'; }
      });
    };
    wrap.appendChild(btn); cnt.appendChild(wrap);
  }
  function displayAliRes(container, results){
    results.sort((a, b) => parsePrice(a.prix) - parsePrice(b.prix));
    let resCont = container.querySelector('.aliexpress-results') || document.createElement('div');
    resCont.className = 'aliexpress-results'; resCont.innerHTML = '';
    results.forEach(item => {
      let card = document.createElement('div'); card.className = 'aliexpress-card';
      let a = document.createElement('a'); a.href = item.lien; a.target = '_blank'; a.style.textDecoration = 'none'; a.style.color = 'inherit';
      let img = document.createElement('img'); img.src = item.image; img.alt = item.titre;
      let title = document.createElement('div'); title.textContent = item.titre;
      title.style.cssText = "font-size:12px;margin-top:5px;font-weight:bold;height:40px;overflow:hidden";
      let price = document.createElement('div'); price.textContent = item.prix;
      price.style.cssText = "font-size:12px;color:#ff5722;margin-top:5px";
      a.append(img, title, price); card.appendChild(a); resCont.appendChild(card);
    });
    if(!container.contains(resCont)) container.appendChild(resCont);
  }
  const parsePrice = s => { let n = parseFloat(s.replace(/[^\d.,-]/g, '').replace(',', '.')); return isNaN(n) ? 999999 : n; };
  function addProductSummary(cnt){
    let sum = document.querySelector('#cr-product-insights-cards');
    if(sum){
      let clone = sum.cloneNode(true);
      clone.classList.add('product-summary-encart');
      clone.querySelectorAll('i[id^="close-button-"]').forEach(i => i.remove());
      cnt.appendChild(clone);
      addAspectListeners(clone);
    }
  }
  function addAspectListeners(clone){
    clone.querySelectorAll('[id^="aspect-button-0-"]').forEach(btn => {
      btn.onclick = () => {
        let X = btn.id.split('-')[3], sheet = document.getElementById(`aspect-bottom-sheet-0-${X}`);
        if(!sheet)return;
        clone.querySelectorAll('[id^="aspect-bottom-sheet-0-"]').forEach(s => s.style.display = 'none');
        sheet.style.display = 'block';
        clone.querySelectorAll('[id^="aspect-button-0-"]').forEach(b => b.classList.remove('_Y3Itc_selected_2-xMA'));
        btn.classList.add('_Y3Itc_selected_2-xMA');
      };
    });
  }
  let footerDone = false;
  function addFooter(){
    if(footerDone)return; footerDone = true;
    let cont = document.getElementById('amz-checker-container');
    if(!cont)return;
    let f = document.createElement('div'); f.id = 'amz-checker-footer';
    let ver = (typeof GM_info !== 'undefined' && GM_info.script && GM_info.script.version) ? GM_info.script.version : '4.x';
    f.innerHTML = `<img src="https://i.ibb.co/qrjrcVy/amz-price-checker.png" class="footer-logo"> Amazon Price Checker v${ver}`;
    cont.appendChild(f);
  }
  function getCurrentCountry(){
    let h = location.hostname;
    if(h.includes('amazon.com') && !h.includes('amazon.com.be') && !h.includes('amazon.co.uk')) return 'com';
    if(h.includes('amazon.de')) return 'de';
    if(h.includes('amazon.es')) return 'es';
    if(h.includes('amazon.it')) return 'it';
    if(h.includes('amazon.com.be')) return 'com.be';
    if(h.includes('amazon.nl')) return 'nl';
    if(h.includes('amazon.pl')) return 'pl';
    if(h.includes('amazon.co.uk')) return 'co.uk';
    return 'fr';
  }
  function getPrice(doc, ctry){
    let el = doc.querySelector('.priceToPay,#priceblock_ourprice,#priceblock_dealprice,#priceblock_saleprice');
    if(!el)return null;
    let raw = parseFloat(el.textContent.replace(/[^0-9,\.]/g, '').replace(',', '.'));
    return toEUR(raw, getCurrency(ctry));
  }
  function getCurrency(ctry){
    let s = sites.find(x => x.c === ctry);
    return s ? s.cur : 'EUR';
  }
  function toEUR(amt, cur){
    if(!exRates || typeof amt !== 'number') return amt;
    if(cur === 'EUR') return amt;
    let r = exRates[cur];
    return r ? amt / r : amt;
  }
  function fetchExRates(){
    return new Promise(resolve => {
      let cached = localStorage.getItem('exchangeRates'),
          ts = localStorage.getItem('exchangeRatesTimestamp'),
          now = Date.now();
      if(cached && ts && (now - ts < 3600000)){
        exRates = JSON.parse(cached); return resolve();
      }
      GM_xmlhttpRequest({
        method:'GET',
        url:'https://api.frankfurter.app/latest?from=EUR&to=USD,GBP,PLN,EUR',
        onload: r => {
          if(r.status === 200){
            let data = JSON.parse(r.responseText);
            exRates = data.rates;
            localStorage.setItem('exchangeRates', JSON.stringify(exRates));
            localStorage.setItem('exchangeRatesTimestamp', now);
          } else {
            // Fallback incluant le taux pour PLN
            exRates = { USD:0.90, GBP:1.15, PLN:4.50, EUR:1 };
          }
          resolve();
        },
        onerror: () => {
          exRates = { USD:0.90, GBP:1.15, PLN:4.50, EUR:1 };
          resolve();
        }
      });
    });
  }
  function fetchPrices(){
    sites.forEach(s => {
      let url = `https://www.amazon.${s.c}/dp/${asin}?tag=${PARTNER_IDS[s.c]}`;
      GM_xmlhttpRequest({
        method:'GET',
        url,
        headers: { 'User-Agent':'Mozilla/5.0','Accept-Language':'en-US,en;q=0.5' },
        onload: r => {
          if(r && r.status === 200){
            let doc = new DOMParser().parseFromString(r.responseText, 'text/html'),
                p = getPrice(doc, s.c);
            if(p !== null){
              let d = getDelivery(doc),
                  c = getCoupon(doc, p),
                  convP = p, convD = toEUR(d, s.cur), convC = toEUR(c, s.cur);
              if(!firstLoaded){ firstLoaded = true; buildFinalUI(); }
              insertRow({ s, price: convP, del: convD, coupon: convC, cur: s.cur });
            }
          }
        },
        onerror: () => {}
      });
    });
  }
  function getDelivery(doc){
    let m = doc.body.innerHTML.match(/data-csa-c-delivery-price="[^"]*?(\d+[.,]\d{2})/);
    if(m){
      let p = parseFloat(m[1].replace(',','.'));
      return isNaN(p) ? 0 : p;
    }
    return 0;
  }
  function getCoupon(doc, curPrice){
    let lbl = doc.querySelector('label[id^="couponText"],label[id^="greenBadgepctch"]');
    if(!lbl)return 0;
    let txt = (lbl.textContent || '').replace(/\u00A0/g, ' ').toLowerCase().trim(), cp = 0,
        m = txt.match(/(\d+(?:[.,]\d+)?)\s*%/);
    if(m){
      let p = parseFloat(m[1].replace(',','.'));
      if(!isNaN(p) && p > 0 && p < 100) cp = curPrice * (p / 100);
    }
    m = txt.match(/(?:€\s*(\d+(?:[.,]\d+)?)|(\d+(?:[.,]\d+))\s*€)/);
    if(m){
      let val = parseFloat((m[1] || m[2] || '').replace(',','.'));
      if(!isNaN(val) && val > 0 && val <= curPrice) cp = Math.max(cp, val);
    }
    return cp;
  }
  function showPrice(amt, cur){
    if(!exRates || cur === 'EUR') return `€${amt.toFixed(2)}`;
    return `€${amt.toFixed(2)}<span style="font-size:0.8em; color:#888;" title="Exchange Rate: 1 EUR = ${exRates[cur]} ${cur}">ℹ️</span>`;
  }
  main();
})();