initial commit
This commit is contained in:
commit
5afe932247
11 changed files with 6667 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.vscode
|
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import js from "@eslint/js";
|
||||||
|
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
|
||||||
|
import globals from "globals";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
ignores: ["dist"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...js.configs.recommended,
|
||||||
|
...eslintPluginPrettierRecommended,
|
||||||
|
rules: {
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
"no-undef": "warn",
|
||||||
|
},
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.browser,
|
||||||
|
l: "readonly",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
49
lib/countries.js
Normal file
49
lib/countries.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
export const COUNTRIES = [
|
||||||
|
{ countryCode: "ww", currencyCode: "usd" },
|
||||||
|
{ countryCode: "ca", currencyCode: "cad" },
|
||||||
|
// { countryCode: "us", currencyCode: "usd" },
|
||||||
|
{ countryCode: "in", currencyCode: "inr" },
|
||||||
|
// { countryCode: "za", currencyCode: "zar" },
|
||||||
|
// { countryCode: "au", currencyCode: "aud" },
|
||||||
|
// { countryCode: "hk", currencyCode: "hkd" },
|
||||||
|
// { countryCode: "jp", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "my", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "nz", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "ph", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "sg", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "tw", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "th", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "at", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "be", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "cy", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "cz", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "dk", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "ee", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "fi", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "fr", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "de", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "gr", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "ie", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "it", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "lt", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "lu", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "mt", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "nl", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "no", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "pl", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "pt", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "ro", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "sk", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "si", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "es", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "se", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "ch", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "tr", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "gb", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "ar", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "br", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "cl", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "co", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "mx", currencyCode: "EUR" },
|
||||||
|
// { countryCode: "pe", currencyCode: "EUR" },
|
||||||
|
];
|
20
lib/currency.js
Normal file
20
lib/currency.js
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import currency from "currency.js";
|
||||||
|
|
||||||
|
export const baseCurrency = "usd";
|
||||||
|
|
||||||
|
export const convertCurrency = async (price, outCurrency, rates) => {
|
||||||
|
return currency(price).divide(rates[baseCurrency][outCurrency]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadCurrencyRates = async () => {
|
||||||
|
l("loading currency rates");
|
||||||
|
try {
|
||||||
|
const res = await fetch(
|
||||||
|
`https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@latest/v1/currencies/${baseCurrency}.json`,
|
||||||
|
);
|
||||||
|
return await res.json();
|
||||||
|
} catch (e) {
|
||||||
|
l("error", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
56
lib/getPriceForCountry.js
Normal file
56
lib/getPriceForCountry.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
const timeout = (duration) => new Promise((r) => setTimeout(r, duration));
|
||||||
|
|
||||||
|
const observePriceOnPage = (page) =>
|
||||||
|
new Promise((res, rej) => {
|
||||||
|
timeout(10000).then(() => rej(""));
|
||||||
|
|
||||||
|
var observer = new MutationObserver(() => {
|
||||||
|
const price = page
|
||||||
|
.querySelector(".active-price")
|
||||||
|
.querySelector("span").textContent;
|
||||||
|
if (price) {
|
||||||
|
observer.disconnect();
|
||||||
|
res(price);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(page, {
|
||||||
|
attributes: true,
|
||||||
|
childList: true,
|
||||||
|
characterData: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const bookUrlForCountry = (country) => {
|
||||||
|
const urlPattern = /^https:\/\/www\.kobo\.com\/../;
|
||||||
|
const newPath = `https://www.kobo.com/${country}`;
|
||||||
|
|
||||||
|
return window.location.href.replace(urlPattern, newPath);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPriceForCountry = (country) =>
|
||||||
|
new Promise((res) => {
|
||||||
|
const url = bookUrlForCountry(country);
|
||||||
|
|
||||||
|
l("going to", url);
|
||||||
|
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
|
|
||||||
|
iframe.src = url;
|
||||||
|
|
||||||
|
iframe.hidden = true;
|
||||||
|
|
||||||
|
document.body.append(iframe);
|
||||||
|
|
||||||
|
iframe.contentWindow.onload = () => {
|
||||||
|
l("starting observing price on", url);
|
||||||
|
observePriceOnPage(iframe.contentDocument.body, url)
|
||||||
|
.then(res)
|
||||||
|
.catch(() => {
|
||||||
|
l(`failed to find price for ${url}`);
|
||||||
|
res("");
|
||||||
|
})
|
||||||
|
.finally(() => document.body.removeChild(iframe));
|
||||||
|
};
|
||||||
|
});
|
39
lib/index.js
Normal file
39
lib/index.js
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import './logger'
|
||||||
|
import { COUNTRIES } from "./countries";
|
||||||
|
import { getPriceForCountry } from "./getPriceForCountry";
|
||||||
|
import { loadCurrencyRates, convertCurrency } from "./currency";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
const rates = await loadCurrencyRates();
|
||||||
|
|
||||||
|
l('currency rates', rates)
|
||||||
|
|
||||||
|
const prices = await Promise.all(
|
||||||
|
COUNTRIES.map(async (c) => {
|
||||||
|
l("looking price for", c.countryCode);
|
||||||
|
const originalPrice = await getPriceForCountry(c.countryCode);
|
||||||
|
const convertedPrice = await convertCurrency(
|
||||||
|
originalPrice,
|
||||||
|
c.currencyCode,
|
||||||
|
rates,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { ...c, originalPrice, convertedPrice };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
l(prices);
|
||||||
|
} catch (e) {
|
||||||
|
l("error", e);
|
||||||
|
} finally {
|
||||||
|
l("done");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
l("starting...");
|
||||||
|
|
||||||
|
window.onload = () => {
|
||||||
|
l("page is fully loaded");
|
||||||
|
void main();
|
||||||
|
};
|
1
lib/logger.js
Normal file
1
lib/logger.js
Normal file
|
@ -0,0 +1 @@
|
||||||
|
globalThis.l = (...m) => console.log("KOBOPRICE", ...m);
|
17
manifest.json
Normal file
17
manifest.json
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{
|
||||||
|
"name": "Kobo Price",
|
||||||
|
"description": "Lowest book price finder",
|
||||||
|
"version": "1.0",
|
||||||
|
"manifest_version": 3,
|
||||||
|
"permissions": ["activeTab"],
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"js": ["dist/index.js"],
|
||||||
|
"run_at": "document_end",
|
||||||
|
"matches": [
|
||||||
|
"https://www.kobo.com/*/*/ebook/*",
|
||||||
|
"https://www.kobo.com/*/*/audiobook/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
6422
package-lock.json
generated
Normal file
6422
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
31
package.json
Normal file
31
package.json
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
"type": "module",
|
||||||
|
"name": "koboprice",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "find lowest price on Kobo",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"lint": "eslint --fix",
|
||||||
|
"build": "esbuild lib/index.js --bundle --minify --sourcemap --target=chrome58,firefox57,safari11,edge16 --outfile=dist/index.js",
|
||||||
|
"watch-build": "npm run build -- --watch",
|
||||||
|
"watch-ext": "web-ext run",
|
||||||
|
"watch": "concurrently npm:watch-build npm:watch-ext"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.6.0",
|
||||||
|
"concurrently": "^8.2.2",
|
||||||
|
"esbuild": "0.23.0",
|
||||||
|
"eslint": "^9.6.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
|
"globals": "^15.8.0",
|
||||||
|
"prettier": "3.3.2",
|
||||||
|
"web-ext": "^8.2.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"currency.js": "^2.0.4"
|
||||||
|
}
|
||||||
|
}
|
6
prettierrc.json
Normal file
6
prettierrc.json
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
Loading…
Reference in a new issue