initial commit

This commit is contained in:
Gregory Tertyshny 2024-07-14 16:49:40 +03:00
commit 5afe932247
11 changed files with 6667 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
dist
.vscode

23
eslint.config.js Normal file
View 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
View 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
View 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
View 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
View 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
View file

@ -0,0 +1 @@
globalThis.l = (...m) => console.log("KOBOPRICE", ...m);

17
manifest.json Normal file
View 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

File diff suppressed because it is too large Load diff

31
package.json Normal file
View 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
View file

@ -0,0 +1,6 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"semi": false,
"singleQuote": true
}