added: progress, live price updates, do not check already bought books
This commit is contained in:
parent
94b688843c
commit
d7676019d0
8 changed files with 140 additions and 76 deletions
|
@ -2,7 +2,7 @@ const timeout = (duration) => new Promise((r) => setTimeout(r, duration));
|
||||||
|
|
||||||
const observePriceOnPage = (page) =>
|
const observePriceOnPage = (page) =>
|
||||||
new Promise((res, rej) => {
|
new Promise((res, rej) => {
|
||||||
timeout(10000).then(() => rej("price not found"));
|
timeout(5000).then(() => rej("price not found"));
|
||||||
|
|
||||||
var observer = new MutationObserver(() => {
|
var observer = new MutationObserver(() => {
|
||||||
const price = page?.querySelector(
|
const price = page?.querySelector(
|
||||||
|
|
154
lib/index.js
154
lib/index.js
|
@ -1,17 +1,13 @@
|
||||||
import "./logger";
|
import "./logger";
|
||||||
import { COUNTRIES } from "./countries";
|
|
||||||
import { initCache } from "./cache";
|
import { initCache } from "./cache";
|
||||||
import { bookPriceFor } from "./bookPriceFor";
|
import { createElement, h, render } from "preact";
|
||||||
|
import { usePrices } from "./usePrices";
|
||||||
|
import { useCallback, useState } from "preact/hooks";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
TODO:
|
TODO:
|
||||||
- do not run for books you already bought
|
|
||||||
- more durable source for rates
|
- more durable source for rates
|
||||||
- React for UI
|
|
||||||
- More informative UI, show loading progress
|
|
||||||
- clear stale cache
|
|
||||||
- readme how to use and debug
|
- readme how to use and debug
|
||||||
- configuration (purge cache, change base currency)
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const createPricesContainer = () => {
|
const createPricesContainer = () => {
|
||||||
|
@ -28,79 +24,101 @@ const createPricesContainer = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const container = document.createElement("div");
|
return visible.parentNode.insertBefore(
|
||||||
|
document.createElement("div"),
|
||||||
container.style.display = "flex";
|
visible,
|
||||||
container.style.flexDirection = "column";
|
);
|
||||||
container.style.border = "1px solid black";
|
|
||||||
container.style.padding = "10px";
|
|
||||||
|
|
||||||
return visible.parentNode.insertBefore(container, visible);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const getPrices = async () => {
|
const isInLibrary = () => document.querySelectorAll(".read-now").length;
|
||||||
const prices = [];
|
|
||||||
for (const country of COUNTRIES) {
|
const formatPrice = (countryPrice, convertedPrice) => {
|
||||||
// intentionally blocking execution
|
if (countryPrice == undefined) {
|
||||||
// to resolve sequentially.
|
return `LOADING`;
|
||||||
// It should prevent DOS and triggering captcha
|
|
||||||
prices.push(await bookPriceFor(country));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return prices;
|
if (convertedPrice) {
|
||||||
|
return `${countryPrice} => ${convertedPrice?.formatted}`;
|
||||||
|
}
|
||||||
|
return `NOT FOUND`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sortPrices = (prices) =>
|
const Price = ({ props }) => {
|
||||||
prices.sort(
|
const { convertedPrice, countryCode, countryPrice, url } = props;
|
||||||
(a, b) =>
|
const [isHovered, setHover] = useState(false);
|
||||||
(a?.convertedPrice?.intValue || Infinity) -
|
|
||||||
(b?.convertedPrice?.intValue || Infinity),
|
const hover = useCallback(() => setHover(true));
|
||||||
|
const leave = useCallback(() => setHover(false));
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
backgroundColor: isHovered ? "#d7d7d7" : "",
|
||||||
|
padding: "10px 10px 0 10px",
|
||||||
|
};
|
||||||
|
|
||||||
|
return h(
|
||||||
|
"a",
|
||||||
|
{
|
||||||
|
style,
|
||||||
|
href: url,
|
||||||
|
target: "_blank",
|
||||||
|
onMouseEnter: hover,
|
||||||
|
onMouseLeave: leave,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
h("p", null, countryCode.toUpperCase()),
|
||||||
|
h(
|
||||||
|
"p",
|
||||||
|
{ style: { fontWeight: "bold" } },
|
||||||
|
formatPrice(countryPrice, convertedPrice),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const InLibrary = () =>
|
||||||
|
h(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
style: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
border: "1px solid black",
|
||||||
|
textAlign: "center",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
h("h2", null, "Already in Library!"),
|
||||||
);
|
);
|
||||||
|
|
||||||
const showPrices = (container, prices) => {
|
const Percent = (percent) => {
|
||||||
container.innerText = null;
|
const style = { padding: "10px 10px 0 10px", textAlign: "center" };
|
||||||
prices.forEach((price) => {
|
|
||||||
const link = document.createElement("a");
|
if (percent == 100) return h("h2", { style }, "all prices are loaded!");
|
||||||
link.href = price.url;
|
|
||||||
link.target = "_blank";
|
return h("h2", { style }, `${percent}%`);
|
||||||
link.style.marginBottom = "5px";
|
|
||||||
link.style.display = "flex";
|
|
||||||
link.style.justifyContent = "space-between";
|
|
||||||
const oldPrice = document.createElement("p");
|
|
||||||
oldPrice.innerText = `${price.countryCode.toUpperCase()}: ${price?.countryPrice || "NO PRICE"}`;
|
|
||||||
const newPrice = document.createElement("p");
|
|
||||||
newPrice.innerText = price?.convertedPrice?.formatted ?? "NO PRICE";
|
|
||||||
newPrice.style.fontWeight = "bold";
|
|
||||||
link.appendChild(oldPrice);
|
|
||||||
link.appendChild(newPrice);
|
|
||||||
container.appendChild(link);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async function main() {
|
const App = () => {
|
||||||
const container = createPricesContainer();
|
const [prices, percentChecked] = usePrices();
|
||||||
|
|
||||||
container.innerText = "LOADING PRICES...";
|
const style = {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
border: "1px solid black",
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
if (isInLibrary()) {
|
||||||
initCache();
|
return InLibrary();
|
||||||
|
|
||||||
const countriesPrice = await getPrices();
|
|
||||||
|
|
||||||
l("country prices", countriesPrice);
|
|
||||||
|
|
||||||
sortPrices(countriesPrice);
|
|
||||||
|
|
||||||
l("sorted prices", countriesPrice);
|
|
||||||
|
|
||||||
showPrices(container, countriesPrice);
|
|
||||||
} catch (e) {
|
|
||||||
l("error", e);
|
|
||||||
container.innerText = "FAILED TO LOAD PRICES. CHECK CONSOLE FOR MORE INFO";
|
|
||||||
} finally {
|
|
||||||
l("done");
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
return h("div", { style }, [
|
||||||
|
Percent(percentChecked),
|
||||||
|
...prices.map((price) => h(Price, { props: price })),
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
l("starting...");
|
l("starting...");
|
||||||
void main();
|
initCache();
|
||||||
|
render(createElement(App), createPricesContainer());
|
||||||
|
|
35
lib/usePrices.js
Normal file
35
lib/usePrices.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { COUNTRIES } from "./countries";
|
||||||
|
import { bookPriceFor } from "./bookPriceFor";
|
||||||
|
|
||||||
|
const sortPrices = (prices) =>
|
||||||
|
prices.sort(
|
||||||
|
(a, b) =>
|
||||||
|
(a?.convertedPrice?.intValue || Infinity) -
|
||||||
|
(b?.convertedPrice?.intValue || Infinity),
|
||||||
|
);
|
||||||
|
|
||||||
|
export const usePrices = () => {
|
||||||
|
const [prices, setPrices] = useState(COUNTRIES);
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchAll = async () => {
|
||||||
|
for (const [index, country] of COUNTRIES.entries()) {
|
||||||
|
// intentionally blocking execution
|
||||||
|
// to resolve sequentially.
|
||||||
|
// It should prevent DOS and triggering captcha
|
||||||
|
const found = await bookPriceFor(country);
|
||||||
|
prices[index] = found;
|
||||||
|
sortPrices(prices);
|
||||||
|
setPrices([...prices]);
|
||||||
|
}
|
||||||
|
l("DONE");
|
||||||
|
};
|
||||||
|
fetchAll();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const percentChecked =
|
||||||
|
(prices.filter((p) => p.countryPrice != undefined).length / prices.length) *
|
||||||
|
100;
|
||||||
|
|
||||||
|
return [prices, percentChecked.toFixed(0)];
|
||||||
|
};
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "Kobo Price",
|
"name": "Kobo Price",
|
||||||
"description": "Find lowest book price on kobo.com",
|
"description": "Find lowest book price on kobo.com",
|
||||||
"version": "1.1.9",
|
"version": "1.2.0",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
|
|
12
package-lock.json
generated
12
package-lock.json
generated
|
@ -10,7 +10,8 @@
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"currency.js": "^2.0.4",
|
"currency.js": "^2.0.4",
|
||||||
"date-fns": "^3.6.0"
|
"date-fns": "^3.6.0",
|
||||||
|
"preact": "^10.22.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.6.0",
|
"@eslint/js": "^9.6.0",
|
||||||
|
@ -4868,6 +4869,15 @@
|
||||||
"integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==",
|
"integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/preact": {
|
||||||
|
"version": "10.22.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/preact/-/preact-10.22.1.tgz",
|
||||||
|
"integrity": "sha512-jRYbDDgMpIb5LHq3hkI0bbl+l/TQ9UnkdQ0ww+lp+4MMOdqaUYdFc5qeyP+IV8FAd/2Em7drVPeKdQxsiWCf/A==",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/preact"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
|
|
|
@ -30,6 +30,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"currency.js": "^2.0.4",
|
"currency.js": "^2.0.4",
|
||||||
"date-fns": "^3.6.0"
|
"date-fns": "^3.6.0",
|
||||||
|
"preact": "^10.22.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in a new issue