Greasy Fork

Amazon Price Checker (FR, DE, ES, IT, BE, NL, COM) + AliExpress

Compare Amazon prices across FR, DE, ES, IT, BE, NL, COM. Detect coupon from label#couponText... or label#greenBadgepctch..., fade in each row, align columns left, etc.

当前为 2025-01-05 提交的版本,查看 最新版本

// ==UserScript==
// @name         Amazon Price Checker (FR, DE, ES, IT, BE, NL, COM) + AliExpress
// @namespace    http://tampermonkey.net/
// @version      3.0
// @description  Compare Amazon prices across FR, DE, ES, IT, BE, NL, COM. Detect coupon from label#couponText... or label#greenBadgepctch..., fade in each row, align columns left, etc.
// @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.com/*
// @grant        GM_xmlhttpRequest
// @connect      amazon.fr
// @connect      amazon.es
// @connect      amazon.it
// @connect      amazon.de
// @connect      amazon.com.be
// @connect      amazon.nl
// @connect      amazon.com
// @connect      summarizer.mon-bnj.workers.dev
// @license      MIT
// ==/UserScript==

(function(){
  'use strict';

  const ASIN_REGEX = /\/([A-Z0-9]{10})(?:[/?]|$)/;
  const PARTNER_IDS = {
    fr: 'bnjmazon-21',
    es: 'bnjmazon08-21',
    it: 'bnjmazon0d-21',
    de: 'geeksince190d-21',
    'com.be': 'geeksince1900',
    nl: 'bnjmazon-21',
    com: 'bnjmazon-20'
  };
  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' },
    { name:'Amazon.be',  country:'com.be', flag:'https://flagcdn.com/w20/be.png' },
    { name:'Amazon.nl',  country:'nl',     flag:'https://flagcdn.com/w20/nl.png' },
    { name:'Amazon.com', country:'com',    flag:'https://flagcdn.com/w20/us.png' }
  ];

  let asin, basePrice, selectedTimePeriod = 'all';
  let priceResults = [], requestCount = 0, firstPriceLoaded = false;
  let tableContainer, headerRow, priceContainer;

  function main(){
    if(!extractASIN() || !getBasePrice()) return;
    injectStyles();
    createLoadingContainer();
    fetchPricesFromOtherSites();
  }

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

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

  /**
   * Cherche d’abord un label#couponText..., sinon label#greenBadgepctch...
   * Puis parse le texte pour trouver un pourcentage ou un montant.
   */
  function getCouponFromDocument(doc, currentPrice) {
    // On essaie d'abord label id^="couponText"
    let label = doc.querySelector('label[id^="couponText"]');

    // Si inexistant, on essaie label id^="greenBadgepctch"
    if(!label) {
      label = doc.querySelector('label[id^="greenBadgepctch"]');
    }
    if(!label) return 0;

    let text = label.textContent || '';
    text = text.replace(/\u00A0/g,' ').toLowerCase().trim();

    let coupon = 0;

    // 1) Pourcentage
    const pctRegex = /(\d+(?:[.,]\d+)?)\s*%/;
    const mPct = pctRegex.exec(text);
    if(mPct){
      const pctVal = parseFloat(mPct[1].replace(',', '.'));
      if(!isNaN(pctVal) && pctVal>0 && pctVal<100){
        coupon = currentPrice * (pctVal / 100);
      }
    }

    // 2) Montant fixe (ex. "2,80€", "2.80 €")
    const moneyRegex = /(?:€\s*(\d+(?:[.,]\d+)?)|(\d+(?:[.,]\d+))\s*€)/;
    const mMoney = moneyRegex.exec(text);
    if(mMoney){
      const valStr = (mMoney[1] || mMoney[2] || '').replace(',', '.');
      const val = parseFloat(valStr);
      if(!isNaN(val) && val>0 && val<=currentPrice){
        // On prend la plus grande des deux si on trouve un % et un montant
        coupon = Math.max(coupon, val);
      }
    }
    return coupon;
  }

  function injectStyles(){
    const css=`
      #amazonPriceComparisonContainer {
        margin-top:20px;
        padding:10px;
        background:#f9f9f9;
        border:1px solid #ccc;
        border-radius:8px;
        position:relative;
        font-size:11px;
        text-align:center
      }
      .comparison-row {
        cursor:pointer;
        display:flex;
        justify-content:space-between;
        padding:2px 0;
        border-bottom:1px solid #ccc
      }
      .comparison-row:hover {
        background:#f1f1f1
      }
      .comparison-row.header-row {
        border-bottom:2px solid #000;
        font-weight:bold;
        pointer-events:none
      }
      .comparison-row>div {
        flex:1;
        margin:0 2px;
        text-align:right !important;
      }
      .first-col {
      flex: 0 0 100px;
      white-space: nowrap;
      text-align: left;
      //overflow: hidden;
    }
      #loadingMessage {
        text-align:center;
        font-weight:bold;
        font-size:14px;
        display:flex;
        flex-direction:column;
        align-items:center;
        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%}
      }
      .price-difference-positive { color:green }
      .price-difference-negative { color:red }
      .controls-container {
        text-align:center;
        margin:10px;
        display:flex;
        justify-content:space-around;
        align-items:center
      }
      .aliexpress-container {
        margin-top:20px;
        padding:5px 10px;
        border:1px solid #ccc;
        border-radius:8px;
        text-align:center;
        max-width:200px;
        margin:20px auto;
        cursor:pointer;
        background:transparent;
        color:#ff5722;
        font-weight:bold;
        display:flex;
        align-items:center;
        justify-content:center
      }
      .aliexpress-icon {
        width:24px;
        margin-right:8px
      }
      .aliexpress-container:hover {
        background:#ffe6cc
      }
      .loading-text {
        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
      }
      .footer {
        text-align:right;
        font-size:.7em;
        color:#666;
        margin-top:10px
      }
      .footer-logo {
        width:20px;
        height:20px;
        vertical-align:middle;
        margin-right:5px
      }
      .chart-container {
        text-align:center;
        margin:20px 0
      }
      .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:rotation 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:rotation .5s linear infinite reverse
      }
      @keyframes rotation {
        0%{transform:rotate(0deg)}
        100%{transform:rotate(360deg)}
      }
      @keyframes fadeIn {
        from{opacity:0}
        to{opacity:1}
      }
      .fade-in {
        animation:fadeIn .4s ease-in-out
      }
    `;
    const st=document.createElement('style');
    st.type='text/css';
    st.innerText=css;
    document.head.appendChild(st);
  }

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

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

  function handleResponse(site,response){
    requestCount++;
    if(response && response.status===200){
      const doc=new DOMParser().parseFromString(response.responseText,'text/html');
      const p=getPriceFromDocument(doc);
      const d=getDeliveryPriceFromDocument(doc);
      if(p!==null){
        const c=getCouponFromDocument(doc,p);
        if(!firstPriceLoaded){
          priceContainer = document.querySelector('#amazonPriceComparisonContainer');
          if(!priceContainer) return;
          priceContainer.innerHTML='';
          createComparisonTableSkeleton(priceContainer);
          addControls(priceContainer);
          addCamelCamelCamelChart(priceContainer);
          addAliExpressLink(priceContainer);
          addFooter(priceContainer);
          firstPriceLoaded=true;
        }
        insertPriceRow({site,price:p,delivery:d,coupon:c});
      }
    }
  }

  function createComparisonTableSkeleton(container){
    tableContainer = document.createElement('div');
    headerRow = document.createElement('div');
    headerRow.className = 'comparison-row header-row';
    ['Site','Price','Coupon','Delivery','Total','Difference'].forEach(h=>{
      headerRow.appendChild(createCell(h,true));
    });
    tableContainer.appendChild(headerRow);
    container.appendChild(tableContainer);
  }

  function insertPriceRow({site,price,delivery,coupon}){
    const total = price - (coupon||0) + (delivery||0);
    const row = document.createElement('div');
    row.className = 'comparison-row fade-in';
    row.onclick = () => window.open(`https://www.amazon.${site.country}/dp/${asin}?tag=${PARTNER_IDS[site.country]}`, '_blank');

    const diff = total - basePrice;
    const perc = ((diff/basePrice)*100).toFixed(2);
    const diffClass = diff<0 ? 'price-difference-positive' :
                      diff>0 ? 'price-difference-negative' :
                      '';

    row.append(
      createCell(`
        <img src="${site.flag}"
             style="vertical-align:middle;margin-right:5px;width:20px;height:13px;">
        ${site.name}
      `, false, 'first-col'),
      createCell(`€${price.toFixed(2)}`),
      createCell(
        coupon>0
          ? `<img src="https://img.icons8.com/arcade/64/discount-ticket.png"
                   width="20"
                   style="vertical-align:middle;margin-right:5px;">
             -€${coupon.toFixed(2)}`
          : '-'
      ),
      createCell(
        delivery
          ? `<img src="https://img.icons8.com/arcade/64/in-transit.png"
                   width="20"
                   style="vertical-align:middle;margin-right:5px;">
             €${delivery.toFixed(2)}`
          : '-'
      ),
      createCell(`€${total.toFixed(2)}`),
      createCell(
        diff!==0
          ? `<span class="${diffClass}">
               ${diff>=0?'+':''}€${diff.toFixed(2)} (${perc}%)
             </span>`
          : '-'
      )
    );

    let inserted=false;
    const rows=[...tableContainer.querySelectorAll('.comparison-row:not(.header-row)')];
    for(let i=0;i<rows.length;i++){
      const cells=rows[i].querySelectorAll('div');
      const existingTotalText = cells[4].textContent.replace(/[^\d.,-]/g,'').replace(',','.');
      const existingTotal = parseFloat(existingTotalText) || 999999;
      if(total < existingTotal){
        tableContainer.insertBefore(row,rows[i]);
        inserted=true;
        break;
      }
    }
    if(!inserted) tableContainer.appendChild(row);
  }

  function createCell(content,isHeader=false,extraClass=''){
    const c=document.createElement('div');
    c.style.flex='1';
    // On aligne tout à gauche par la CSS injectée:
    // text-align:left !important;
    c.innerHTML=content;
    if(isHeader) c.style.fontWeight='bold';
    if(extraClass) c.classList.add(extraClass);
    return c;
  }

  function addControls(container){
    const ctrls=document.createElement('div');
    ctrls.className='controls-container';
    const tps=[
      {id:'btn1M',label:'1 Month',val:'1m'},
      {id:'btn3M',label:'3 Months',val:'3m'},
      {id:'btn6M',label:'6 Months',val:'6m'},
      {id:'btn1Y',label:'1 Year',val:'1y'},
      {id:'btnAll',label:'All',val:'all'}
    ];
    tps.forEach(tp=>{
      const b=document.createElement('button');
      b.id=tp.id;
      b.textContent=tp.label;
      b.className=`control-button ${tp.val===selectedTimePeriod?'active':''}`;
      b.addEventListener('click',()=>{
        selectedTimePeriod=tp.val;
        document.querySelectorAll('.control-button').forEach(x=>x.classList.remove('active'));
        b.classList.add('active');
        updateChartUrl();
      });
      ctrls.appendChild(b);
    });

    const cbs=[
      {id:'checkboxAmazon', label:'Amazon', fn:'amazon', dis:true, chk:true},
      {id:'checkboxNew',    label:'New',    fn:'new',    chk:true},
      {id:'checkboxUsed',   label:'Used',   fn:'used',   chk:false}
    ];
    cbs.forEach(cb=>{
      const wrap=document.createElement('div');
      wrap.className='checkbox-container';
      const i=document.createElement('input');
      i.type='checkbox';
      i.id=cb.id;
      i.checked=cb.chk;
      if(cb.dis) i.disabled=true;
      i.addEventListener('change',updateChartUrl);

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

      wrap.append(i,lbl);
      ctrls.appendChild(wrap);
    });
    container.appendChild(ctrls);
  }

  function addCamelCamelCamelChart(container){
    const c=document.createElement('div');
    c.className='chart-container';
    const cc=getCurrentCountryCode();
    const url=getCamelChartUrl(cc,asin,selectedTimePeriod);
    const camelUrl=`https://${cc}.camelcamelcamel.com/product/${asin}`;

    const spin=document.createElement('div');
    spin.className='loader';
    const img=document.createElement('img');
    img.alt=`Price history for ${asin}`;
    img.className='chart-image';
    img.style.display='none';

    img.addEventListener('load',()=>{
      spin.style.display='none';
      img.style.display='block';
    });
    img.addEventListener('error',()=>{
      spin.style.display='none';
      img.style.display='block';
      img.src='https://via.placeholder.com/600x300?text=Image+Unavailable';
    });
    img.src=url;

    const a=document.createElement('a');
    a.href=camelUrl;
    a.target='_blank';
    a.appendChild(img);

    c.append(spin,a);
    container.appendChild(c);
  }

  function getCamelChartUrl(cc,asin,tp){
    const f=getSelectedFilenames();
    const base=`https://charts.camelcamelcamel.com/${cc}/${asin}/${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 getSelectedFilenames(){
    const cbs=[
      {id:'checkboxAmazon',fn:'amazon'},
      {id:'checkboxNew',   fn:'new'},
      {id:'checkboxUsed',  fn:'used'}
    ];
    return Array.from(document.querySelectorAll('input[type="checkbox"]:checked'))
      .map(x=>cbs.find(z=>z.id===x.id)?.fn)
      .filter(Boolean)
      .join('-');
  }

  function updateChartUrl(){
    const cc=getCurrentCountryCode();
    const url=getCamelChartUrl(cc,asin,selectedTimePeriod);
    const camelUrl=`https://${cc}.camelcamelcamel.com/product/${asin}`;
    const i=document.querySelector('#amazonPriceComparisonContainer img.chart-image');
    if(i){
      const spin=i.parentElement.parentElement.querySelector('.loader');
      if(spin) spin.style.display='inline-block';

      i.style.display='none';
      i.src=url;
      i.parentElement.href=camelUrl;
    }
  }

  function createAliExpressLink(title){
    const d=document.createElement('div');
    d.className='aliexpress-container';
    d.innerHTML=`
      <img src="https://img.icons8.com/color/48/aliexpress.png" class="aliexpress-icon">
      <span class="aliexpress-text">Check on AliExpress</span>`;
    d.addEventListener('click',()=>{
      const t=d.querySelector('.aliexpress-text');
      t.className='loading-text';
      t.textContent='Loading...';
      GM_xmlhttpRequest({
        method:'GET',
        url:`https://summarizer.mon-bnj.workers.dev/?text=${encodeURIComponent(title)}`,
        onload:r=>handleAliExpressResponse(r,d),
        onerror:()=>{resetAliExpressButton(d);}
      });
    });
    return d;
  }

  function handleAliExpressResponse(r,c){
    try{
      const j=JSON.parse(r.responseText);
      if(j.summary){
        const u=`https://www.aliexpress.com/wholesale?SearchText=${encodeURIComponent(j.summary)}`;
        resetAliExpressButton(c);
        setTimeout(()=>{window.open(u,'_blank');},100);
      } else {
        throw new Error('No summary');
      }
    }catch(e){
      resetAliExpressButton(c);
    }
  }

  function addAliExpressLink(c){
    const t=document.querySelector('#productTitle');
    const pt=t ? t.textContent.trim() : null;
    if(!pt) return;
    const ali=createAliExpressLink(pt);
    c.appendChild(ali);
  }

  function resetAliExpressButton(c){
    const ic=c.querySelector('.aliexpress-icon');
    c.innerHTML='';
    c.appendChild(ic);
    const sp=document.createElement('span');
    sp.className='aliexpress-text';
    sp.textContent='Check on AliExpress';
    c.appendChild(sp);
  }

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

  function getCurrentCountryCode(){
    const h=window.location.hostname;
    if(h.includes('amazon.com') && !h.includes('amazon.com.be')) 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';
    return 'fr';
  }

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

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

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

  main();
})();