@splode/obake
Version:
Check merchants for deals.
143 lines (120 loc) • 3.78 kB
text/typescript
import chalk from "chalk";
import puppeteer, { Browser, Page } from "puppeteer";
import Notifier from "src/message/Notifier";
import Logger from "../Logger";
import Store from "../Store";
import { elide, parsePrice } from "../strings";
import Good from "./Good";
export default abstract class Merchant {
protected goods: Good[] = [];
protected log: Logger | null;
protected notifier: Notifier | null;
protected store: Store;
public browser: Browser | null = null;
public constructor() {
this.store = Store.get();
this.notifier = this.store.notifier;
this.log = this.store.logger;
}
public abstract get prettyName(): string;
public abstract priceCheck(page: Page, good: Good): Promise<void>;
public get isHeadless(): boolean {
return true;
}
public get name(): string {
return this.prettyName.toLowerCase().replace(" ", "");
}
public async checkGoods(): Promise<void> {
if (this.goods.length <= 0) return;
if (!this.hasActiveGood()) return;
const verbose = this.store.config?.verbose;
this.browser = await puppeteer
.launch({
args: [!this.isHeadless ? "--window-position=-3860,0" : ""],
headless: this.isHeadless,
})
.catch((err) => {
throw err;
});
// this.store.addBrowserToPool(browser)
const check = async (good: Good) => {
if (!good.disabled) {
const page = await this.browser?.newPage().catch((err) => {
throw err;
});
if (page) {
if (verbose) {
this.log?.info(`checking for ${good.name} at ${good.URL}...`);
}
await this.priceCheck(page, good)
.catch((err) => {
this.log?.error(err.message);
})
.finally(async () => {
await page.close().catch((err) => {
this.log?.error(err.message);
throw err;
});
});
}
} else {
if (verbose) {
this.log?.info(`skipping due to disabled configuration: ${good.URL}`);
}
}
};
await Promise.all(this.goods.map(check))
.catch((err) => {
this.log?.error(err.message);
})
.finally(async () => {
await this.browser?.close().catch((err) => {
throw err;
});
});
}
public addGoods(...goods: Good[]): void {
goods.forEach((g) => this.goods.push(g));
}
public async close(): Promise<void> {
if (this.browser && this.browser.isConnected) {
await this.browser.close().catch((err) => {
throw err;
});
}
}
protected parsePrice(ps: string): number {
return parsePrice(ps.trim());
}
protected handleUnavailable(good: Good): void {
this.log?.info(
`${elide(good.name)} ${chalk.yellow("unavailable")} for purchase at ${
this.prettyName
}: ${good.URL}`
);
}
protected async handleDiscount(price: number, good: Good): Promise<void> {
const msg = good.getDiscountText(price);
this.log?.info(chalk.green(msg));
await this.notifier?.send(msg);
}
protected handleFoundPrice(price: number, good: Good): void {
this.log?.info(good.getFoundPriceText(price));
}
protected handleNotFoundPrice(good: Good): void {
this.log?.warn(good.getNotFoundPriceText());
}
protected responseError(res: puppeteer.HTTPResponse, good: Good): Error {
return new Error(
`request failed with status code ${chalk.yellow(res.status())}: ${
good.URL
}`
);
}
protected requestError(err: Error, good: Good): Error {
return new Error(`request to ${good.URL} failed: ${err.message}`);
}
protected hasActiveGood(): boolean {
return this.goods.some((good) => !good.disabled);
}
}