// ==UserScript==
// @name Amazon - Prix au kilo
// @name:en Amazon - Price per weight
// @namespace http://tampermonkey.net/
// @version 1.2
// @description Affiche les prix au kilo et tri par prix croissant
// @description:en Display price per weight & sort ascending
// @author Shuunen
// @match https://*.amazon.fr/*
// @grant none
// ==/UserScript==
(function() {
"use strict";
// Used https://babeljs.io/repl/ to convert this code to es2015
var app = {
id: "amz-kg",
processOne: false,
processOnce: false,
hideStuff: false,
showDebug: false,
injectRealPrice: true,
sortProducts: true
};
app.debug = app.processOne;
var cls = {
handled: app.id + "-handled",
avoided: app.id + "-avoided",
debug: app.id + "-debug",
pricePer: app.id + "-price-per"
};
var selectors = {
list: "ul.s-result-list",
item:
".s-result-item:not(.aok-hidden):not(." +
cls.handled +
"):not(." +
cls.avoided +
")",
itemTitle: ".s-access-title",
otherPrice: ".a-size-base.a-color-price.s-price.a-text-bold",
pricePer:
".a-size-base.a-color-price:not(.s-price):not(.a-text-bold)," +
"." +
cls.pricePer,
debugContainer:
".a-fixed-left-grid-col.a-col-right .a-row:not(.a-spacing-small) .a-column.a-span7",
debug: "." + cls.debug,
pantry: "img.sprPantry",
stuffToHide: ".s-result-item .a-column.a-span7 .a-row:not(." + cls.debug + ")"
};
selectors.price = selectors.debugContainer + " div:first-child .a-link-normal";
var regex = {
price: /EUR (\d+,\d\d)/i,
weight: /(\d+)\s?(g|-)/i,
bulk: /Lot de (\d+)/i,
pricePer: /EUR (\d+,\d\d)\/(\w+)(\s\w+)?/i
};
var templates = {
debug:
'<div class="a-row ' +
cls.debug +
'"><div class="a-column a-span12">\n <p class="a-spacing-micro">Price : {{price}} \u20AC</p>\n <p class="a-spacing-micro">Weight : {{weight}} {{unit}}</p> \n <p class="a-spacing-small">Bulk : {{bulk}}</p>\n <p class="a-spacing-micro a-size-base a-color-price s-price a-text-bold">P/Kg : {{pricePerKilo}} \u20AC/kg</p>\n </div></div>',
price: '<span class="s-price a-text-bold">EUR {{price}}</span>',
pricePerKilo:
'<span class="a-color-price s-price a-text-bold ' +
cls.pricePer +
'">EUR {{pricePerKilo}}/kg</span>'
};
var products = [];
function findOne(selector, context, dontYell) {
context = context || document;
var item = context.querySelector(selector);
if (item && app.debug) {
console.log('found element matching "' + selector + '"');
} else if (!item && !dontYell) {
console.warn('found no element for selector "' + selector + '"');
}
return item;
}
function findFirst(selector, context) {
return findAll(selector, context)[0];
}
function findAll(selector, context) {
context = context || document;
var items = Array.prototype.slice.call(context.querySelectorAll(selector));
if (items.length && app.debug) {
console.log("found", items.length, 'elements matching "' + selector + '"');
} else if (!items.length) {
console.warn('found no elements for selector "' + selector + '"');
}
return items;
}
function shadeBadProducts() {
findAll(selectors.pantry).forEach(function(el) {
var item =
el.parentElement.parentElement.parentElement.parentElement.parentElement
.parentElement.parentElement.parentElement;
item.style.filter = "grayscale(100%)";
item.style.opacity = 0.5;
item.style.order = 1000;
item.classList.add(cls.avoided);
if (app.debug) {
console.log("shaded item", item);
}
});
}
function priceStrToFloat(str) {
var price = str.replace(",", ".");
price = parseFloat(price);
return price;
}
function priceFloatToStr(num) {
var price = num.toFixed(1);
price = price.replace(".", ",") + "0";
return price;
}
function getPrice(text) {
var matches = text.match(regex.price);
if (app.debug) {
console.log("found price matches :", matches);
}
var price = matches && matches.length === 2 ? matches[1] : "0";
price = priceStrToFloat(price);
if (app.debug) {
console.log("found price", price);
}
return price;
}
function getWeightAndUnit(text) {
var matches = text.match(regex.weight);
// console.log('found weight matches & unit :', matches)
var data = {
weight: 0,
unit: ""
};
if (matches && matches.length === 3) {
data.weight = matches[1];
data.unit = matches[2];
}
if (data.unit === "-") {
data.unit = "g";
}
// console.log('found weight & unit :', data)
return data;
}
function getBulk(text) {
var matches = text.match(regex.bulk);
// console.log('found bulk matches :', matches)
var bulk = matches && matches.length === 2 ? matches[1] : "1";
bulk = parseInt(bulk);
// console.log('found bulk', bulk)
return bulk;
}
function getProductDataViaPricePer(text) {
var matches = text.match(regex.pricePer);
// console.log('found pricePer matches :', matches)
var data = {
price: 0,
weight: 0,
unit: "",
bulk: 1
};
if (matches && matches.length === 4) {
data.price = priceStrToFloat(matches[1]);
if (matches[3]) {
data.weight = matches[2];
data.unit = matches[3].trim();
} else {
data.weight = 1;
data.unit = matches[2];
}
}
if (app.debug) {
console.log("found pricePer :", data);
}
return data;
}
function getTitle(text) {
return text
.split(" ")
.slice(0, 5)
.join(" ");
}
function getProductData(item, data) {
var text = item.textContent;
var weightAndUnit = getWeightAndUnit(text);
data = data || {};
data.price = getPrice(text);
data.weight = weightAndUnit.weight;
data.unit = weightAndUnit.unit;
data.bulk = getBulk(text);
data.title = getTitle(text);
return data;
}
function fill(template, data) {
var tpl = template + "";
Object.keys(data).forEach(function(key) {
var str = "{{" + key + "}}";
var val = data[key];
if (key.indexOf("price") > -1 && val > 0) {
val = priceFloatToStr(val);
}
// console.log('looking for', str)
tpl = tpl.replace(new RegExp(str, "gi"), val);
});
return tpl;
}
function showDebugData(item, data) {
var debug = findOne(selectors.debug, item, true);
if (!app.showDebug) {
if (debug) {
// if existing debug zone found
debug.style.display = "none";
}
return;
}
var html = fill(templates.debug, data);
// console.log('debug html', html)
if (debug) {
// if existing debug zone found
debug.style.display = "inherit";
debug.outerHTML = html;
return;
}
debug = document.createElement("div");
debug.innerHTML = html;
var container = findOne(selectors.debugContainer, item);
if (container) {
container.append(debug);
} else {
console.error(data.title, ": failed at finding debug container", item);
}
}
function getPricePerKilo(data) {
data.pricePerKilo = 0;
if (data.weight === 0) {
return data;
}
var w = data.weight * data.bulk;
if (data.unit === "g") {
data.pricePerKilo = (1000 / w) * data.price;
} else if (data.unit === "kg") {
data.pricePerKilo = w * data.price;
} else {
console.error(data.title, ": unit not handled :", data.unit);
}
if (data.pricePerKilo >= 0) {
data.pricePerKilo = priceStrToFloat(data.pricePerKilo.toFixed(1));
}
if (app.debug) {
console.log("found pricePerKilo :", data);
}
return data;
}
function injectRealPrice(item, data) {
if (!app.injectRealPrice) {
return;
}
if (app.debug) {
console.log("injecting real price :", data);
}
var price = findOne(selectors.price, item);
var text = "";
if (data.pricePerKilo > 0) {
text = fill(templates.pricePerKilo, data);
} else if (data.price > 0) {
text = fill(templates.price, data);
}
var pricePer = findOne(selectors.pricePer, item, true);
if (pricePer) {
pricePer.style.display = "none";
}
price.innerHTML = text;
var otherPrice = findOne(selectors.otherPrice, item, true);
if (otherPrice) {
otherPrice.classList.remove("a-color-price", "a-text-bold");
}
}
function avoidProduct(item) {
var nbAttr = item.getAttributeNames().length;
if (nbAttr === 5) {
if (app.debug) {
console.warn("detected ad product", item);
}
return true;
}
// all good
return false;
}
function augmentProducts() {
findAll(selectors.item).forEach(function(item) {
return augmentProduct(item);
});
}
function augmentProduct(item) {
if (avoidProduct(item)) {
return;
}
var pricePer = findOne(selectors.pricePer, item, true);
var data = {};
if (pricePer) {
pricePer.style.display = "inherit";
data = getProductDataViaPricePer(pricePer.textContent);
} else {
data = getProductData(item);
}
data = getPricePerKilo(data);
if (pricePer) {
data = getProductData(item, data);
}
showDebugData(item, data);
injectRealPrice(item, data);
if (app.processOnce) {
item.classList.add(cls.handled);
}
data.el = item;
products.push(data);
}
function hideStuff() {
findAll(selectors.stuffToHide).forEach(function(el) {
return (el.style.display = app.hideStuff ? "none" : "inherit");
});
}
function sortProducts() {
var list = findOne(selectors.list);
if (!list) {
return console.error("cannot sort without list");
}
list.style.display = "flex";
list.style.flexDirection = "column";
// trick to have products without pricePerKilo at bottom
products.map(function(p) {
return (p.pricePerKilo = p.pricePerKilo || p.price + 1000);
});
// sort by pricePerKilo
products = products.sort(function(a, b) {
return a.pricePerKilo - b.pricePerKilo;
});
products.forEach(function(p, i) {
return (p.el.style.order = i);
});
}
function init() {
console.log(app.id, "is starting...");
shadeBadProducts();
if (app.processOne) {
augmentProduct(findFirst(selectors.item));
} else {
augmentProducts();
sortProducts();
}
hideStuff();
console.log(app.id, "processed", products.length, "products");
}
init();
})();