您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add price per unit and discount percentage to products and promotion cards (updated for new page structures)
- // ==UserScript==
- // @name Albert Heijn Korting Updated (with Promotion Parsing)
- // @namespace https://wol.ph/
- // @version 1.0.6
- // @description Add price per unit and discount percentage to products and promotion cards (updated for new page structures)
- // @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 and cards to avoid duplicate processing.
- const processedProducts = new Set();
- function updateProductInfo() {
- console.log("Starting updateProductInfo");
- // --- PROCESS PRODUCT CARDS ---
- let productCards = document.querySelectorAll('article[data-testhook="product-card"]');
- console.log(`Found ${productCards.length} product cards`);
- productCards.forEach(function (productCard) {
- // Use a unique identifier for the product
- let productId =
- productCard.getAttribute("data-product-id") ||
- productCard.querySelector('a[class*="link_root__"]')?.getAttribute("href");
- if (!productId) {
- console.log("Product ID not found, skipping product-card");
- return;
- }
- if (processedProducts.has(productId)) {
- return;
- }
- console.log(`Processing product ${productId}`);
- // --- Extract current price ---
- // Look for the highlighted price element
- let priceElement = productCard.querySelector('[data-testhook="price-amount"].price-amount_highlight__ekL92');
- if (!priceElement) {
- console.log("Highlighted price element not found, skipping product-card");
- return;
- }
- // Extract integer and fractional parts
- let priceInt = priceElement.querySelector('[class*="price-amount_integer__"]');
- let priceFrac = priceElement.querySelector('[class*="price-amount_fractional__"]');
- if (!priceInt || !priceFrac) {
- console.log("Price integer or fractional part not found, skipping product-card");
- 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 product-card");
- return;
- }
- let unitSizeText = unitSizeElement.textContent.trim(); // e.g., "ca. 755 g" or "750 g"
- console.log(`Unit size text: ${unitSizeText}`);
- // Parse unit size (supports weight and count).
- let unitMatch = unitSizeText.match(/(ca\.?\s*)?([\d.,]+)\s*(g|kg)/i);
- let countMatch = unitSizeText.match(/(\d+)\s*(stuk|stuks)/i);
- if (!unitMatch && !countMatch) {
- console.log("Weight and count not found in unit size text, skipping product-card");
- 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 needed; we calculate per kg
- if (unit === "kg") {
- weight = weight * 1000;
- console.log(`Converted weight to grams: ${weight}`);
- } else {
- // We display per kg even if provided in grams.
- unit = "kg";
- }
- // Calculate price per kg
- pricePerUnit = (price * 1000) / weight;
- }
- if (countMatch) {
- let count = parseInt(countMatch[1]);
- unit = countMatch[2].toLowerCase();
- console.log(`Count: ${count}`);
- // Calculate price per item
- pricePerUnit = price / count;
- }
- console.log(`Price per ${unit}: €${pricePerUnit.toFixed(2)}`);
- // --- Extract old price, if available ---
- let oldPrice = null;
- let oldPriceElement = productCard.querySelector('[data-testhook="price-amount"]:not(.price-amount_highlight__ekL92)');
- if (oldPriceElement) {
- let oldPriceInt = oldPriceElement.querySelector('[class*="price-amount_integer__"]');
- let oldPriceFrac = oldPriceElement.querySelector('[class*="price-amount_fractional__"]');
- if (oldPriceInt && oldPriceFrac) {
- oldPrice = parseFloat(oldPriceInt.textContent + "." + oldPriceFrac.textContent);
- console.log(`Old price: ${oldPrice}`);
- }
- }
- // --- Calculate discount percentage ---
- let discountPercentage = null;
- if (oldPrice && oldPrice > price) {
- discountPercentage = (((oldPrice - price) / oldPrice) * 100).toFixed(1);
- console.log(`Calculated discount from prices: ${discountPercentage}%`);
- } else {
- // Try extracting discount percentage from shield promotion text
- let shieldElement = productCard.querySelector('[data-testhook="product-shield"]');
- if (shieldElement) {
- let shieldTextElement = shieldElement.querySelector('[class*="shield_text__"]');
- if (shieldTextElement) {
- let shieldText = shieldTextElement.textContent.trim();
- discountPercentage = parsePromotionText(shieldText, price) || "0";
- console.log(`Extracted discount percentage from shield: ${discountPercentage}%`);
- } else {
- discountPercentage = "0";
- console.log("No shield text found, setting discount to 0%");
- }
- } else {
- discountPercentage = "0";
- console.log("Shield element not found, setting discount to 0%");
- }
- }
- // --- Create or update shield element ---
- let shieldElement = productCard.querySelector('[data-testhook="product-shield"]');
- if (!shieldElement) {
- console.log("Creating shield element for product-card");
- shieldElement = document.createElement("div");
- shieldElement.className = "shield_root__SmhpN";
- shieldElement.setAttribute("data-testhook", "product-shield");
- let newShieldTextElement = document.createElement("span");
- newShieldTextElement.className = "shield_text__kNeiW";
- shieldElement.appendChild(newShieldTextElement);
- let shieldContainer = productCard.querySelector('[class*="product-card-portrait_shieldProperties__"]');
- if (!shieldContainer) {
- console.log("Creating shield container for product-card");
- shieldContainer = document.createElement("div");
- shieldContainer.className = "product-card-portrait_shieldProperties__+JZJI";
- let header = productCard.querySelector('[class*="header_root__"]');
- if (header === null)
- return;
- header.appendChild(shieldContainer);
- }
- shieldContainer.appendChild(shieldElement);
- }
- let shieldTextElement = shieldElement.querySelector('[class*="shield_text__"]');
- if (!shieldTextElement) {
- shieldTextElement = document.createElement("span");
- shieldTextElement.className = "shield_text__kNeiW";
- shieldElement.appendChild(shieldTextElement);
- }
- // Update the shield text with 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;
- // --- Update price element to include price per unit info ---
- let priceContainer = priceElement.parentElement;
- if (priceContainer) {
- 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`);
- });
- // --- PROCESS PROMOTION CARDS (NEW STRUCTURE) ---
- let promotionCards = document.querySelectorAll('a.promotion-card_root__tQA3z');
- console.log(`Found ${promotionCards.length} promotion cards`);
- promotionCards.forEach(function (card) {
- // Use the element's id or href as a unique identifier
- let cardId = card.getAttribute("id") || card.getAttribute("href");
- if (!cardId) {
- console.log("Promotion card unique id not found, skipping");
- return;
- }
- if (processedProducts.has(cardId)) {
- return;
- }
- console.log(`Processing promotion card ${cardId}`);
- // --- Extract current price ---
- let priceElem = card.querySelector('[data-testhook="price"]');
- if (!priceElem) {
- console.log("Promotion price element not found, skipping promotion card");
- return;
- }
- let priceNow = priceElem.getAttribute("data-testpricenow");
- if (!priceNow) {
- console.log("Promotion current price attribute missing, skipping");
- return;
- }
- let currentPrice = parseFloat(priceNow);
- console.log(`Promotion current price: ${currentPrice}`);
- // --- Extract old price, if available ---
- let oldPriceAttr = priceElem.getAttribute("data-testpricewas");
- let oldPrice = oldPriceAttr ? parseFloat(oldPriceAttr) : null;
- if (oldPrice) {
- console.log(`Promotion old price: ${oldPrice}`);
- }
- // --- Parse unit size from title ---
- // We assume that the card title contains unit information (e.g. "400 g", "1 l", "2 stuks")
- let titleElem = card.querySelector('[data-testhook="card-title"]');
- if (!titleElem) {
- console.log("Promotion card title not found, skipping unit extraction");
- return;
- }
- let titleText = titleElem.textContent.trim();
- console.log(`Promotion card title: "${titleText}"`);
- let unitMatch = titleText.match(/(ca\.?\s*)?([\d.,]+)\s*(g|kg)/i);
- let countMatch = titleText.match(/(\d+)\s*(stuk|stuks)/i);
- let ppu = 0;
- let unit = "";
- if (unitMatch) {
- let weight = parseFloat(unitMatch[2].replace(",", "."));
- unit = unitMatch[3].toLowerCase();
- console.log(`Parsed weight: ${weight} ${unit}`);
- if (unit === "kg") {
- weight *= 1000;
- } else {
- unit = "kg"; // we display per kg even if provided in grams
- }
- ppu = (currentPrice * 1000) / weight;
- } else if (countMatch) {
- let count = parseInt(countMatch[1]);
- unit = "stuk";
- console.log(`Parsed count: ${count} ${unit}(s)`);
- ppu = currentPrice / count;
- } else {
- console.log("No unit size found in promotion card title, skipping price per unit calculation");
- }
- // --- Calculate discount percentage ---
- let discountPercentage = null;
- if (oldPrice && oldPrice > currentPrice) {
- discountPercentage = (((oldPrice - currentPrice) / oldPrice) * 100).toFixed(1);
- console.log(`Calculated discount from promotion prices: ${discountPercentage}%`);
- } else {
- discountPercentage = "0";
- console.log("No discount available on promotion card");
- }
- // --- Update shield element ---
- // Promotion cards usually include a shield container with promotion texts.
- let shieldElem = card.querySelector('[data-testhook="promotion-shields"]');
- if (shieldElem) {
- // Look for an element that displays promotion text within shield.
- let shieldTextElem = shieldElem.querySelector('[data-testhook="promotion-text"]');
- if (shieldTextElem) {
- shieldTextElem.textContent = discountPercentage + "%";
- } else {
- shieldTextElem = document.createElement("span");
- shieldTextElem.setAttribute("data-testhook", "promotion-text");
- shieldTextElem.textContent = discountPercentage + "%";
- shieldElem.appendChild(shieldTextElem);
- }
- let colors = getDiscountColors(discountPercentage);
- shieldElem.style.backgroundColor = colors.backgroundColor;
- shieldElem.style.color = colors.textColor;
- }
- // --- Update or create price-per-unit element ---
- let priceContainer = priceElem.parentElement;
- if (priceContainer) {
- let ppuElem = priceContainer.querySelector(".price-per-unit");
- if (!ppuElem) {
- ppuElem = document.createElement("div");
- ppuElem.className = "price-per-unit";
- ppuElem.style.fontSize = "smaller";
- priceContainer.appendChild(ppuElem);
- }
- if (ppu > 0) {
- ppuElem.textContent = "€" + ppu.toFixed(2) + " per " + unit;
- }
- }
- processedProducts.add(cardId);
- console.log(`Promotion card ${cardId} processed`);
- });
- console.log("Finished updateProductInfo");
- }
- // Helper function to parse promotion text (e.g., "1 + 1 gratis")
- function parsePromotionText(shieldText, pricePerItem) {
- shieldText = shieldText.toLowerCase();
- let discountPercentage = null;
- if (shieldText.includes("%")) {
- let discountMatch = shieldText.match(/(\d+)%\s*korting/i);
- if (discountMatch) {
- discountPercentage = parseFloat(discountMatch[1]);
- }
- } else if (shieldText.includes("gratis")) {
- if (shieldText.includes("1+1") || shieldText.includes("1 + 1")) {
- discountPercentage = 50;
- } else if (shieldText.includes("2+1") || shieldText.includes("2 + 1")) {
- discountPercentage = 33.33;
- }
- } else if (shieldText.includes("2e halve prijs")) {
- discountPercentage = 25;
- }
- return discountPercentage;
- }
- function getDiscountColors(discountPercentage) {
- let backgroundColor, textColor;
- discountPercentage = parseFloat(discountPercentage);
- 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 };
- }
- // Initial run and periodic update
- window.setTimeout(updateProductInfo, 1000);
- if (!DEBUG) setInterval(updateProductInfo, 5000);
- })();