您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add price per gram and discount percentage to products
当前为
// ==UserScript== // @name Albert Heijn Korting // @namespace https://wol.ph/ // @version 1.0.3 // @description Add price per gram and discount percentage to products // @author wolph // @match https://www.ah.nl/* // @icon https://icons.duckduckgo.com/ip2/ah.nl.ico // @grant none // @license BSD // ==/UserScript== const DEBUG = false; (function() { 'use strict'; // Set to keep track of processed products const processedProducts = new Set(); function updateProductInfo() { console.log('Starting updateProductInfo'); // Handle normal product cards let productCards; if(DEBUG) productCards = [document.querySelector('article[data-testhook="product-card"]')]; else productCards = document.querySelectorAll('article[data-testhook="product-card"]'); console.log(`Found ${productCards.length} product cards`); productCards.forEach(function(productCard, index) { // console.debug(`Processing product card ${index}`); // Use a unique identifier for the product let productId = productCard.getAttribute('data-product-id') || productCard.querySelector('a.link_root__EqRHd')?.getAttribute('href'); if (!productId) { console.log('Product ID not found, skipping'); return; } if (processedProducts.has(productId)) { // console.debug(`Product ${productId} already processed, skipping`); return; } console.log(`Processing product ${productId}`); // Extract price let priceElement = productCard.querySelector('[data-testhook="price-amount"]'); if (!priceElement) { console.log('Price element not found, skipping'); return; } let priceInt = priceElement.querySelector('.price-amount_integer__\\+e2XO'); let priceFrac = priceElement.querySelector('.price-amount_fractional__kjJ7u'); if (!priceInt || !priceFrac) { console.log('Price integer or fractional part not found, skipping'); return; } let price = parseFloat(priceInt.textContent + '.' + priceFrac.textContent); console.log(`Price: ${price}`); // Extract unit size let unitSizeElement = productCard.querySelector('[data-testhook="product-unit-size"]'); if (!unitSizeElement) { console.log('Unit size element not found, skipping'); return; } let unitSizeText = unitSizeElement.textContent.trim(); // e.g., "300 g" console.log(`Unit size text: ${unitSizeText}`); // Parse unit size let unitMatch = unitSizeText.match(/(ca\. |)([\d.,]+)\s*(g|kg)/i); let countMatch = unitSizeText.match(/(\d+)\s*(stuk)s/i); if (!unitMatch && !countMatch) { console.log('Weight and count not found in unit size text, skipping'); return; } let pricePerUnit = 0; let unit = ''; if(unitMatch){ let weight = parseFloat(unitMatch[2].replace(',', '.')); unit = unitMatch[3].toLowerCase(); console.log(`Weight: ${weight}, Unit: ${unit}`); // Convert weight to grams if (unit === 'kg') { weight = weight * 1000; console.log(`Converted weight to grams: ${weight}`); } else { // We calculate with grams but we display per kg in all cases. unit = 'kg'; } // Calculate price per kg pricePerUnit = (price * 1000) / weight; // Price per kg } if(countMatch){ let count = parseInt(countMatch[1]); unit = countMatch[2].toLowerCase(); console.log(`Count: ${count}`); // Calculate price per item pricePerUnit = price / count; // Price per item } console.log(`Price per ${unit}: €${pricePerUnit.toFixed(2)}`); // Extract old price if available let oldPrice = null; // Assuming there's an element for the old price let oldPriceElement = priceElement.querySelector('.price-amount_original__A6_Ck'); if (oldPriceElement) { let oldPriceInt = oldPriceElement.querySelector('.price-amount_integer__+e2XO'); let oldPriceFrac = oldPriceElement.querySelector('.price-amount_fractional__kjJ7u'); if (oldPriceInt && oldPriceFrac) { oldPrice = parseFloat(oldPriceInt.textContent + '.' + oldPriceFrac.textContent); console.log(`Old price: ${oldPrice}`); } } // Calculate discount percentage let discountPercentage = null; if (oldPrice) { discountPercentage = ((oldPrice - price) / oldPrice) * 100; discountPercentage = discountPercentage.toFixed(1); console.log(`Calculated discount percentage: ${discountPercentage}%`); } else { // Try extracting from shield let shieldElement = productCard.querySelector('[data-testhook="product-shield"]'); if (shieldElement) { let shieldTextElement = shieldElement.querySelector('.shield_text__kNeiW'); if (shieldTextElement) { let shieldText = shieldTextElement.textContent.trim(); console.log(`Shield text: ${shieldText}`); discountPercentage = parsePromotionText(shieldText); if (discountPercentage) { console.log(`Extracted discount percentage from shield: ${discountPercentage}%`); } } } } // If discount percentage is not found, set it to 0% if (!discountPercentage) { discountPercentage = 0; console.log('Discount percentage not found, setting to 0%'); } // Add or update shield element with discount percentage let shieldElement = productCard.querySelector('[data-testhook="product-shield"]'); if (!shieldElement) { // Create shield element console.log('Creating shield element'); shieldElement = document.createElement('div'); shieldElement.className = 'shield_root__SmhpN'; shieldElement.setAttribute('data-testhook', 'product-shield'); let shieldTextElement = document.createElement('span'); shieldTextElement.className = 'shield_text__kNeiW'; shieldElement.appendChild(shieldTextElement); let shieldContainer = productCard.querySelector('.product-card-portrait_shieldProperties__+JZJI'); if (!shieldContainer) { // Create shield container console.log('Creating shield container'); shieldContainer = document.createElement('div'); shieldContainer.className = 'product-card-portrait_shieldProperties__+JZJI'; let header = productCard.querySelector('.header_root__ilMls'); if(header === null) // Albert Heijn Ad products, skipping for now. return; header.appendChild(shieldContainer); } shieldContainer.appendChild(shieldElement); } // Update shield text with discount percentage let shieldTextElement = shieldElement.querySelector('.shield_text__kNeiW'); if (!shieldTextElement) { shieldTextElement = document.createElement('span'); shieldTextElement.className = 'shield_text__kNeiW'; shieldElement.appendChild(shieldTextElement); } // Just set the text to the discount percentage shieldTextElement.textContent = `${discountPercentage}%`; // Set background and text color based on discount percentage let { backgroundColor, textColor } = getDiscountColors(discountPercentage); shieldElement.style.backgroundColor = backgroundColor; shieldElement.style.color = textColor; // Ensure text is readable // Modify price element to include price per unit in small letters next to discounted price and old price let priceContainer = priceElement.parentElement; // Should be '.price_portrait__pcgwD' if (priceContainer) { // Create or update price per unit element let pricePerUnitElement = priceContainer.querySelector('.price-per-unit'); if (!pricePerUnitElement) { pricePerUnitElement = document.createElement('div'); pricePerUnitElement.className = 'price-per-unit'; pricePerUnitElement.style.fontSize = 'smaller'; priceContainer.appendChild(pricePerUnitElement); } pricePerUnitElement.textContent = `€${pricePerUnit.toFixed(2)} per ${unit}`; } // Mark this product as processed processedProducts.add(productId); console.log(`Product ${productId} processed`); }); // Handle promotion cards let promotionCards = document.querySelectorAll('.promotion-card_root__ENX4w'); console.log(`Found ${promotionCards.length} promotion cards`); promotionCards.forEach(function(promotionCard, index) { // console.debug(`Processing promotion card ${index}`); // Use a unique identifier for the promotion let promotionId = promotionCard.getAttribute('id') || promotionCard.querySelector('a').getAttribute('href'); if (!promotionId) { console.log('Promotion ID not found, skipping'); return; } if (processedProducts.has(promotionId)) { // console.debug(`Promotion ${promotionId} already processed, skipping`); return; } // Try to extract from description and title let cardDescription = promotionCard.querySelector('[data-testhook="card-description"]'); let cardTitle = promotionCard.querySelector('[data-testhook="card-title"]'); let descriptionText = cardDescription?.textContent.trim() || ''; let titleText = cardTitle?.textContent.trim() || ''; //console.log(`Processing promotion ${promotionId}`); // Extract current price and previous price let priceElement = promotionCard.querySelector('[data-testhook="price"]'); if (!priceElement) { console.log(`Price element not found for ${titleText}, skipping`); return; } let priceNow = parseFloat(priceElement.getAttribute('data-testpricenow')); let priceWas = parseFloat(priceElement.getAttribute('data-testpricewas')); console.log(`Price now: ${priceNow}, Price was: ${priceWas} for ${titleText}`); // Extract unit size, if available let count = null; let unit = null; console.log(`Description text: ${descriptionText}`); console.log(`Title text: ${titleText}`); // Try to extract weight from description or title const unitRe = /(^|\s|-)([\d.,]+|\bPer)\s+\b(gram|zak|kilo|ml|stuk)s?(\s|$)/i; let unitMatch; unitMatch = titleText.match(unitRe); if(!unitMatch) unitMatch = descriptionText.match(unitRe); console.log(unitMatch); if (unitMatch) { if(unitMatch[2] == 'Per'){ count = 1; }else{ count = parseFloat(unitMatch[2].replace(',', '.')); } unit = unitMatch[3].toLowerCase(); if(unit === 'gram'){ count *= 0.001; } if (unit === 'kilo' || unit == 'gram') { unit = 'kg'; } console.log(`${count} ${unit}s`); } else { console.log('Unit not found in description or title'); } // Calculate discount percentage let discountPercentage = null; if (priceWas && priceNow) { discountPercentage = ((priceWas - priceNow) / priceWas) * 100; discountPercentage = discountPercentage.toFixed(1); console.log(`Calculated discount percentage: ${discountPercentage}%`); } else { // Try extracting from promotion shield let promotionShields = promotionCard.querySelector('[data-testhook="promotion-shields"]'); if (promotionShields) { let shieldText = promotionShields.textContent.trim(); console.log(`Promotion shield text: ${shieldText}`); discountPercentage = parsePromotionText(shieldText); if (discountPercentage) { console.log(`Extracted discount percentage from shield: ${discountPercentage}%`); } } } if (!discountPercentage) { discountPercentage = 0; console.log('Discount percentage not found, setting to 0%'); } // Add or update promotion shield with discount percentage let promotionShields = promotionCard.querySelector('[data-testhook="promotion-shields"]'); if (!promotionShields) { // Create promotion shields element console.log('Creating promotion shields element'); promotionShields = document.createElement('div'); promotionShields.className = 'promotion-shields_root__cVEfN'; promotionShields.setAttribute('data-testhook', 'promotion-shields'); promotionShields.style = promotionCard.querySelector('.promotion-card-content_root__A5Fda')?.style || ''; let cardContent = promotionCard.querySelector('.promotion-card-content_root__A5Fda'); cardContent.insertBefore(promotionShields, cardContent.firstChild); } // Update promotion shield text with discount percentage // Replace the entire element with discount percentage promotionShields.innerHTML = ''; // Clear existing content let shieldP = document.createElement('p'); shieldP.className = 'typography_root__Om3Wh typography_variant-paragraph__T5ZAU typography_hasMargin__4EaQi promotion-shield_root__mIDdK'; shieldP.setAttribute('data-testhook', 'promotion-shield'); let promotionShieldText = document.createElement('span'); promotionShieldText.className = 'promotion-text_root__1sn7K promotion-text_large__lTZOA'; promotionShieldText.setAttribute('data-testhook', 'promotion-text'); // Set the discount percentage as text promotionShieldText.textContent = `${discountPercentage}%`; // Set background and text color based on discount percentage let { backgroundColor, textColor } = getDiscountColors(discountPercentage * 1.5); shieldP.style.backgroundColor = backgroundColor; shieldP.style.color = textColor; // Ensure text is readable shieldP.appendChild(promotionShieldText); promotionShields.appendChild(shieldP); // If unit is available, calculate price per unit if (count) { let pricePerUnit = priceNow / count; console.log(`Price per ${unit}: €${pricePerUnit.toFixed(2)}`); // Modify price element to include price per kg in small letters next to discounted price and old price let priceContainer = priceElement; // Assuming this is the correct container if (priceContainer) { // Create or update price per kg element let pricePerUnitElement = priceContainer.querySelector('.price-per-unit'); if (!pricePerUnitElement) { pricePerUnitElement = document.createElement('div'); pricePerUnitElement.className = 'price-per-unit'; pricePerUnitElement.style.fontSize = 'smaller'; pricePerUnitElement.style.marginTop = '4px'; priceContainer.appendChild(pricePerUnitElement); } pricePerUnitElement.textContent = `€${pricePerUnit.toFixed(2)} per ${unit}`; } } // Mark this promotion as processed processedProducts.add(promotionId); console.log(`Promotion ${promotionId} processed`); }); console.log('Finished updateProductInfo'); } function parsePromotionText(shieldText) { let discountPercentage = null; if (shieldText.includes('%')) { // e.g., "25% korting" let discountMatch = shieldText.match(/(\d+)%\s*korting/i); if (discountMatch) { discountPercentage = parseFloat(discountMatch[1]); } } else if (shieldText.toLowerCase().includes('gratis')) { if (shieldText.includes('1+1') || shieldText.includes('1 + 1') || shieldText.includes('1+1 gratis')) { discountPercentage = 50; } else if (shieldText.includes('2+1') || shieldText.includes('2 + 1')) { discountPercentage = 33.33; } // Add more cases as necessary } else if (shieldText.includes('2e halve prijs')) { discountPercentage = 25; // Assuming 25% off on 2 items } return discountPercentage; } function getDiscountColors(discountPercentage) { // Use predefined colors and text colors based on discount percentage ranges let backgroundColor, textColor; if (discountPercentage >= 80) { backgroundColor = '#008000'; // Dark Green textColor = '#FFFFFF'; // White } else if (discountPercentage >= 60) { backgroundColor = '#32CD32'; // Lime Green textColor = '#000000'; // Black } else if (discountPercentage >= 40) { backgroundColor = '#FFFF00'; // Yellow textColor = '#000000'; // Black } else if (discountPercentage >= 20) { backgroundColor = '#FFA500'; // Orange textColor = '#000000'; // Black } else { backgroundColor = '#FF0000'; // Red textColor = '#FFFFFF'; // White } return { backgroundColor, textColor }; } // Run immediately window.setTimeout(updateProductInfo, 1000); // Run the update function every 5 seconds if(!DEBUG)setInterval(updateProductInfo, 5000); })();