NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Mandarake EUR Shipping estimate
// @namespace http://order.mandarake.co.jp/
// @version 0.7
// @description Converts Mandarake pricing to EUR, tracks weight & package extend to estimate the shipping cost.
// @author Behemoth
// @author EIREXE
// @match https://order.mandarake.co.jp/*
// @grant none
// @require http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// @license AGPL-3.0-only
// ==/UserScript==
// jshint esversion: 6
// Stackoverflow format unicorn
String.prototype.formatUnicorn = String.prototype.formatUnicorn ||
function () {
"use strict";
var str = this.toString();
if (arguments.length) {
var t = typeof arguments[0];
var key;
var args = ("string" === t || "number" === t) ?
Array.prototype.slice.call(arguments)
: arguments[0];
for (key in args) {
str = str.replace(new RegExp("\\{" + key + "\\}", "gi"), args[key]);
}
}
return str;
};
const STORAGE_KEY_WEIGHT = 'ext_itemWeight';
const STORAGE_KEY_EXTENDS = 'ext_itemExtends';
function price_to_html(price_jpy, exchange_rate) {
var price_converted = (price_jpy/exchange_rate);
var price_converted_format = price_converted.toLocaleString(undefined, {maximumFractionDigits: 2});
return "<strong>{price_euro} €</strong> ({price_jpy} ¥)".formatUnicorn({"price_euro": price_converted_format, "price_jpy": price_jpy.toLocaleString()});
}
// API URL
var JPY_data = "https://api.frankfurter.dev/v1/latest?base=EUR&symbols=JPY";
const EXCHANGE_KEY = 'eur_jpy_rate';
const TIMESTAMP_KEY = 'eur_jpy_timestamp';
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
async function getExchangeRate() {
const savedRate = localStorage.getItem(EXCHANGE_KEY);
const savedTimestamp = localStorage.getItem(TIMESTAMP_KEY);
const now = Date.now();
const isExpired = !savedTimestamp || now - Number(savedTimestamp) > ONE_DAY_MS;
if (savedRate && !isExpired) {
console.log('Using cached rate:', savedRate);
return parseFloat(savedRate);
}
try {
const response = await fetch(JPY_data);
const data = await response.json();
const rate = data.rates.JPY;
localStorage.setItem(EXCHANGE_KEY, rate);
localStorage.setItem(TIMESTAMP_KEY, now.toString());
console.log('Fetched new rate:', rate);
return rate;
} catch (error) {
console.error('Error fetching exchange rate:', error);
// Fallback to saved rate if available
return savedRate ? parseFloat(savedRate) : null;
}
}
function convert_prices(exchange_rate)
{
$('p:contains(" yen")').filter(function() {
if ($(this).text().trim().startsWith("Also available")) {
var text = $(this).text();
var child = $(this).children("a");
console.log(child);
text = text.split("Also available between ")[1];
var between_1 = text.split(" and")[0];
var between_2 = text.split("and ")[1];
between_2 = between_2.split(" yen.")[0];
between_1 = parseFloat(between_1.replace(/,/g, ""));
between_2 = parseFloat(between_2.replace(/,/g, ""));
child.text("");
child.append("Also available between {between_1} and {between_2}".formatUnicorn({"between_1": price_to_html(between_1, exchange_rate), "between_2": price_to_html(between_2, exchange_rate)}))
} else {
var price_jpy_str = $(this).text().split(" ")[0];
var price_jpy = parseFloat(price_jpy_str.replace(/,/g, ""));
$(this).text("");
$(this).append(price_to_html(price_jpy, exchange_rate));
}
return;
});
}
function getItemCodeFromURL() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('itemCode');
}
// Step 2: Extract weight from text like "210mm x 148mm x 3mm / 42g"
function extractWeight(text) {
const match = text.match(/\/\s*([\d.]+)g/);
return match ? parseInt(match[1]) : null;
}
function extractExtends(text) {
const match = text.match(/([\d]+)mm x ([\d]+)mm x ([\d]+)mm/);
return match ? [parseInt(match[1]), parseInt(match[2]), parseInt(match[3])] : null;
}
// Step 3: Loop over all <tr class="size"> and extract weight
function storeWeightsByItemCode() {
const itemCode = getItemCodeFromURL();
if (!itemCode) {
console.warn('Item code not found in URL');
return;
}
const row = document.querySelector('tr.size > td');
if (!row)
{
console.warn('item weight not found');
return;
}
// Store weight
const lastTdText = row.textContent.trim();
const itemWeight = extractWeight(lastTdText);
if (!itemWeight)
{
console.warn('Failed to parse weight');
return;
}
const weights = JSON.parse(localStorage.getItem(STORAGE_KEY_WEIGHT) || '{}');
weights[itemCode] = itemWeight;
localStorage.setItem(STORAGE_KEY_WEIGHT, JSON.stringify(weights));
console.log(`Stored weights for ${itemCode}:`, itemWeight);
// Store extends (used to estimate box weight)
const itemSize = extractExtends(lastTdText);
if (!itemSize) {
console.warn('no extends');
}
const sizes = JSON.parse(localStorage.getItem(STORAGE_KEY_EXTENDS) || '{}');
sizes[itemCode] = itemSize;
localStorage.setItem(STORAGE_KEY_EXTENDS, JSON.stringify(sizes));
console.log(`Stored extends for ${itemCode}:`, itemSize);
}
function calculateShippingcostInBasket(exchange_rate) {
'use strict';
// Parse item weights from localStorage
const weights = JSON.parse(localStorage.getItem(STORAGE_KEY_WEIGHT) || '{}');
const sizes = JSON.parse(localStorage.getItem(STORAGE_KEY_EXTENDS) || '{}');
// Monkey-patch XHR to detect when basket is loaded
const origOpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
if (url.includes('https://tools.mandarake.co.jp/basket/get/')) {
this.addEventListener('load', function () {
console.log('loaded basket');
// Wait a bit for DOM to update
setTimeout(() => {
getExchangeRate().then(rate => {
if (rate !== null) {
console.log(`Current EUR to JPY rate: ${rate}`);
patchBasketWeights();
} else {
console.log('Unable to get exchange rate.');
}
});
}, 200); // Adjust if needed
});
}
return origOpen.apply(this, arguments);
};
function patchBasketWeights() {
const totalWeightsByShop = {};
const totalSizeByShop = {};
// Loop through all item divs
document.querySelectorAll('div[id^="item_webshop_"]').forEach(div => {
const match = div.id.match(/^item_webshop_(\d+)$/);
if (!match) return;
const itemCode = match[1];
const weight = weights[itemCode];
if (!weight) {
console.warn(`weight not known for ${itemCode}. Find out by clicking here: https://order.mandarake.co.jp/order/detailPage/item?itemCode=${itemCode}&lang=en`);
return;
}
const count = parseInt(document.querySelector(`span[id="item_webshop_${itemCode}_count"]`).textContent);
console.log(`${itemCode} x ${count}`);
const li = div.querySelector('li.basic');
if (li) {
const weightDiv = li.querySelector('.weight') || document.createElement('div');
weightDiv.setAttribute('class', 'weight');
weightDiv.textContent = `${weight}g`;
if (count != 1)
{
weightDiv.textContent = `${count}x${weight}g (${weight * count}g)`;
}
li.appendChild(weightDiv);
}
const shop_id = div.closest('div.shop').querySelector('input#shop_id').value;
if (!shop_id){
console.warn('shop_id not found!');
return;
}
var subTotal = totalWeightsByShop[shop_id] || 0;
subTotal += weight * count;
totalWeightsByShop[shop_id] = subTotal;
console.info(`Shop ${shop_id} subtotal ${subTotal}`);
const itemSize = sizes[itemCode];
if (!itemSize || itemSize.length != 3) {
console.warn(`item size not known for ${itemCode}. Find out by clicking here: https://order.mandarake.co.jp/order/detailPage/item?itemCode=${itemCode}&lang=en`);
return;
}
const floatSize = new Float64Array(itemSize);
floatSize.sort();
var subTotalExtend = totalSizeByShop[shop_id] || new Float64Array([0,0,0]);
subTotalExtend[0] += floatSize[0];
subTotalExtend[1] = Math.max(subTotalExtend[1], floatSize[1]);
subTotalExtend[2] = Math.max(subTotalExtend[2], floatSize[2]);
totalSizeByShop[shop_id] = subTotalExtend;
console.info(`Shop ${shop_id} subtotalExtend ${subTotalExtend}`);
});
for (const [shop_id, totalWeight] of Object.entries(totalWeightsByShop)) {
const shop = document.querySelector(`div#shop_${shop_id}`);
const total = shop.querySelector('.nextpage');
// Order price
const orderValueElm = shop.querySelector(`span#shop_${shop_id}_price`);
const orderValueYen = parseFloat(orderValueElm.textContent.replace(/,/g, ""));
orderValueElm.innerHTML = price_to_html(orderValueYen, exchange_rate);
// Import cost
const orderValueEur = (orderValueYen / exchange_rate)
const eUSt = (orderValueEur * 0.07).toLocaleString(undefined, {maximumFractionDigits: 2}); // Guess 7% for books
const eUStDiv = total.querySelector('.eUSt') || document.createElement('p');
eUStDiv.setAttribute('class', 'eUSt');
eUStDiv.innerHTML = `EUSt: ${eUSt}€`;
total.prepend(eUStDiv);
// Shipping weight
const itemSize = totalSizeByShop[shop_id];
const boxWeight = itemSize ? getMinBoxWeight(itemSize) : 0;
const totalWeightDiv = total.querySelector('.totalWeight') || document.createElement('p');
totalWeightDiv.setAttribute('class', 'totalWeight');
totalWeightDiv.textContent = `Weight ${totalWeight}g + ${boxWeight}g box (${itemSize[0]}x${itemSize[1]}x${itemSize[2]}mm)`;
total.prepend(totalWeightDiv);
// Shipping price
const shippingPriceYen = getShippingPrice(totalWeight + boxWeight);
const shippingPrice = price_to_html(shippingPriceYen, exchange_rate);
const shippingPriceDiv = total.querySelector('.shippingPrice') || document.createElement('p');
shippingPriceDiv.setAttribute('class', 'shippingPrice');
shippingPriceDiv.innerHTML = 'Estimated shipping: ' + shippingPrice;
total.prepend(shippingPriceDiv);
}
}
}
function patchSite(exchange_rate) {
'use strict';
convert_prices(exchange_rate);
storeWeightsByItemCode();
calculateShippingcostInBasket(exchange_rate);
}
// Entry Point
// Fetch exchange rate, run everything else
(function() {
'use strict';
getExchangeRate().then(rate => {
if (rate === null) {
console.log('Unable to get exchange rate.');
return;
}
console.log(`Current EUR to JPY rate: ${rate}`);
patchSite(rate);
});
})();
// Determine used box in shipping (margin 5cm)
function getMinBoxWeight(totalSize) {
var minWeight = 1100;
const neededPackageSize = [totalSize[0] + 50, totalSize[1] + 50, totalSize[2] + 50];
totalSize.sort();
console.info(`searching for box with extends ${totalSize}`);
for (const [size, weight] of box_sizes) {
const floatSize = new Float64Array(size);
floatSize.sort();
if (weight < minWeight &&
totalSize[0] < floatSize[0] &&
totalSize[1] < floatSize[1] &&
totalSize[2] < floatSize[2]) {
console.info(`box ${size} x ${weight}g is lighter`);
minWeight = weight;
}
}
console.info(`lightest box: ${minWeight}g`);
return minWeight;
}
const box_sizes =
[
[ [ 210, 180, 135 ], 120 ],
[ [ 575, 390, 435 ], 1100 ],
[ [ 640, 210, 170 ], 440 ],
[ [ 330, 235, 310 ], 420 ],
[ [ 300, 210, 165 ], 180 ],
[ [ 350, 350, 270 ], 540 ],
[ [ 410, 290, 85 ], 250 ],
[ [ 500, 345, 330 ], 580 ],
[ [ 745, 245, 270 ], 680 ],
[ [ 430, 362, 97 ], 340 ],
[ [ 410, 330, 140 ], 460 ],
[ [ 365, 240, 215 ], 260 ],
[ [ 455, 320, 250 ], 440 ]
];
// Determine Shipping cost for basket weight
function getShippingPrice(totalWeight) {
for (const [weight, price] of shipping_table) {
if (totalWeight <= weight) {
return price;
}
}
// If weight exceeds all brackets, return the highest price or handle as needed
return shipping_table[shipping_table.length - 1][1];
}
const shipping_table =
[
[500, 2400],
[1000, 2900],
[1500, 3300],
[2000, 3700],
[2500, 4200],
[3000, 4700],
[3500, 5100],
[4000, 5500],
[4500, 5900],
[5000, 6400],
[5500, 7000],
[6000, 7500],
[6500, 8100],
[7000, 8700],
[7500, 9300],
[8000, 9900],
[8500, 10500],
[9000, 11000],
[9500, 11600],
[10000, 12200],
[10500, 12800],
[11000, 13400],
[11500, 14000],
[12000, 14500],
[12500, 15100],
[13000, 15700],
[13500, 16300],
[14000, 16900],
[14500, 17500],
[15000, 18000],
[15500, 18600],
[16000, 19200],
[16500, 19800],
[17000, 20400],
[17500, 21000],
[18000, 21500],
[18500, 22100],
[19000, 22700],
[19500, 23300],
[20000, 23900],
[20500, 25100],
[21000, 26400],
[21500, 27700],
[22000, 28900],
[22500, 30200],
[23000, 31500],
[23500, 32700],
[24000, 34000],
[24500, 35300],
[25000, 36500],
[25500, 37800],
[26000, 39100],
[26500, 40300],
[27000, 41600],
[27500, 42900],
[28000, 44100],
[28500, 45400],
[29000, 46700],
[29500, 47900],
[30000, 49200],
[30500, 50600],
[31000, 52000],
[31500, 53300],
[32000, 54700],
[32500, 56100],
[33000, 57500],
[33500, 58800],
[34000, 60200],
[34500, 61600],
[35000, 63000],
[35500, 64400],
[36000, 65700],
[36500, 67100],
[37000, 68500],
[37500, 69900],
[38000, 71300],
[38500, 72600],
[39000, 74000],
[39500, 75400],
[40000, 76800],
[40500, 78100],
[41000, 79500],
[41500, 80900],
[42000, 82300],
[42500, 83700],
[43000, 85000],
[43500, 86400],
[44000, 87800],
[44500, 89200],
[45000, 90600],
[45500, 91900],
[46000, 93300],
[46500, 94700],
[47000, 96100],
[47500, 97400],
[48000, 98800],
[48500, 100200],
[49000, 101600],
[49500, 103000],
[50000, 104300],
[50500, 105700],
[51000, 107100],
[51500, 108500],
[52000, 109900],
[52500, 111200],
[53000, 112600],
[53500, 114000],
[54000, 115400],
[54500, 116700],
[55000, 118100],
[55500, 119500],
[56000, 120900],
[56500, 122300],
[57000, 123600],
[57500, 125000],
[58000, 126400],
[58500, 127800],
[59000, 129100],
[59500, 130500],
[60000, 131900],
[60500, 133300],
[61000, 134700],
[61500, 136000],
[62000, 137400],
[62500, 138800],
[63000, 140200],
[63500, 141600],
[64000, 142900],
[64500, 144300],
[65000, 145700],
[65500, 147100],
[66000, 148400],
[66500, 149800],
[67000, 151200],
[67500, 152600],
[68000, 154000],
[68500, 155300],
[69000, 156700],
[69500, 158100],
[70000, 159500],
[70500, 160900],
[71000, 162200],
[71500, 163600],
[72000, 165000],
[72500, 166400],
[73000, 167700],
[73500, 169100],
[74000, 170500],
[74500, 171900],
[75000, 173300],
[75500, 174600],
[76000, 176000],
[76500, 177400],
[77000, 178800],
[77500, 180200],
[78000, 181500],
[78500, 182900],
[79000, 184300],
[79500, 185700],
[80000, 187000],
[80500, 188400],
[81000, 189800],
[81500, 191200],
[82000, 192600],
[82500, 193900],
[83000, 195300],
[83500, 196700],
[84000, 198100],
[84500, 199500],
[85000, 200800],
[85500, 202200],
[86000, 203600],
[86500, 205000],
[87000, 206300],
[87500, 207700],
[88000, 209100],
[88500, 210500],
[89000, 211900],
[89500, 213200],
[90000, 214600],
[90500, 216000],
[91000, 217400],
[91500, 218700],
[92000, 220100],
[92500, 221500],
[93000, 222900],
[93500, 224300],
[94000, 225600],
[94500, 227000],
[95000, 228400],
[95500, 229800],
[96000, 231200],
[96500, 232500],
[97000, 233900],
[97500, 235300],
[98000, 236700],
[98500, 238000],
[99000, 239400],
[99500, 240800],
[100000, 242200]
];