2022-11-17 06:19:11 -05:00
|
|
|
import { loginManually } from "./iksm.ts";
|
2023-06-06 09:47:18 -04:00
|
|
|
import { MultiProgressBar, Mutex } from "../deps.ts";
|
2022-11-17 06:19:11 -05:00
|
|
|
import { FileStateBackend, Profile, StateBackend } from "./state.ts";
|
|
|
|
|
import { Splatnet3 } from "./splatnet3.ts";
|
2023-06-06 09:47:18 -04:00
|
|
|
import { BattleListType, Game, GameExporter, ListMethod } from "./types.ts";
|
2022-10-28 03:32:35 -04:00
|
|
|
import { Cache, FileCache } from "./cache.ts";
|
2022-10-21 07:09:30 -04:00
|
|
|
import { StatInkExporter } from "./exporters/stat.ink.ts";
|
|
|
|
|
import { FileExporter } from "./exporters/file.ts";
|
2022-11-17 06:19:11 -05:00
|
|
|
import { delay, showError } from "./utils.ts";
|
2022-10-28 03:32:35 -04:00
|
|
|
import { GameFetcher } from "./GameFetcher.ts";
|
2022-11-17 06:19:11 -05:00
|
|
|
import { DEFAULT_ENV, Env } from "./env.ts";
|
2023-02-27 23:52:40 -05:00
|
|
|
import { MongoDBExporter } from "./exporters/mongodb.ts";
|
2023-06-12 14:16:13 -04:00
|
|
|
import { SplashcatExporter } from "./exporters/splashcat.ts";
|
2022-10-21 07:09:30 -04:00
|
|
|
|
|
|
|
|
export type Opts = {
|
|
|
|
|
profilePath: string;
|
|
|
|
|
exporter: string;
|
|
|
|
|
noProgress: boolean;
|
2022-10-22 09:00:45 -04:00
|
|
|
monitor: boolean;
|
2022-11-26 10:01:42 -05:00
|
|
|
withSummary: boolean;
|
2023-03-07 10:09:32 -05:00
|
|
|
withStages: boolean;
|
2022-10-24 14:45:36 -04:00
|
|
|
skipMode?: string;
|
2023-06-06 09:47:18 -04:00
|
|
|
listMethod?: string;
|
2022-10-23 04:00:16 -04:00
|
|
|
cache?: Cache;
|
2022-10-23 04:25:10 -04:00
|
|
|
stateBackend?: StateBackend;
|
2022-11-17 06:19:11 -05:00
|
|
|
env: Env;
|
2022-10-21 07:09:30 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const DEFAULT_OPTS: Opts = {
|
|
|
|
|
profilePath: "./profile.json",
|
|
|
|
|
exporter: "stat.ink",
|
|
|
|
|
noProgress: false,
|
2022-10-22 09:00:45 -04:00
|
|
|
monitor: false,
|
2022-11-26 10:01:42 -05:00
|
|
|
withSummary: false,
|
2023-03-07 10:09:32 -05:00
|
|
|
withStages: true,
|
2023-06-15 03:59:45 -04:00
|
|
|
listMethod: "auto",
|
2022-11-17 06:19:11 -05:00
|
|
|
env: DEFAULT_ENV,
|
2022-10-21 07:09:30 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type Progress = {
|
2022-10-31 00:33:00 -04:00
|
|
|
currentUrl?: string;
|
2022-10-21 07:09:30 -04:00
|
|
|
current: number;
|
|
|
|
|
total: number;
|
|
|
|
|
};
|
|
|
|
|
|
2022-11-26 09:45:44 -05:00
|
|
|
class StepProgress {
|
|
|
|
|
currentUrl?: string;
|
|
|
|
|
total: number;
|
|
|
|
|
exported: number;
|
|
|
|
|
done: number;
|
|
|
|
|
skipped: Record<string, number>;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this.total = 1;
|
|
|
|
|
this.exported = 0;
|
|
|
|
|
this.done = 0;
|
|
|
|
|
this.skipped = {};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-06 09:47:18 -04:00
|
|
|
interface GameListFetcher {
|
|
|
|
|
/**
|
|
|
|
|
* Return not exported game list.
|
|
|
|
|
* [0] is the latest game.
|
|
|
|
|
* @param exporter GameExporter
|
|
|
|
|
*/
|
|
|
|
|
fetch(exporter: GameExporter): Promise<string[]>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class BattleListFetcher implements GameListFetcher {
|
|
|
|
|
protected listMethod: ListMethod;
|
|
|
|
|
protected allBattleList?: string[];
|
|
|
|
|
protected latestBattleList?: string[];
|
|
|
|
|
protected allLock = new Mutex();
|
|
|
|
|
protected latestLock = new Mutex();
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
listMethod: string,
|
|
|
|
|
protected splatnet: Splatnet3,
|
|
|
|
|
) {
|
|
|
|
|
if (listMethod === "all") {
|
|
|
|
|
this.listMethod = "all";
|
|
|
|
|
} else if (listMethod === "latest") {
|
|
|
|
|
this.listMethod = "latest";
|
|
|
|
|
} else {
|
|
|
|
|
this.listMethod = "auto";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected getAllBattleList() {
|
|
|
|
|
return this.allLock.use(async () => {
|
|
|
|
|
if (!this.allBattleList) {
|
|
|
|
|
this.allBattleList = await this.splatnet.getAllBattleList();
|
|
|
|
|
}
|
|
|
|
|
return this.allBattleList;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
protected getLatestBattleList() {
|
|
|
|
|
return this.latestLock.use(async () => {
|
|
|
|
|
if (!this.latestBattleList) {
|
|
|
|
|
this.latestBattleList = await this.splatnet.getBattleList();
|
|
|
|
|
}
|
|
|
|
|
return this.latestBattleList;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async innerFetch(exporter: GameExporter) {
|
|
|
|
|
if (this.listMethod === "latest") {
|
|
|
|
|
return await exporter.notExported({
|
|
|
|
|
type: "VsInfo",
|
|
|
|
|
list: await this.getLatestBattleList(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (this.listMethod === "all") {
|
|
|
|
|
return await exporter.notExported({
|
|
|
|
|
type: "VsInfo",
|
|
|
|
|
list: await this.getAllBattleList(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (this.listMethod === "auto") {
|
|
|
|
|
const latestList = await exporter.notExported({
|
|
|
|
|
type: "VsInfo",
|
|
|
|
|
list: await this.getLatestBattleList(),
|
|
|
|
|
});
|
|
|
|
|
if (latestList.length === 50) {
|
|
|
|
|
return await exporter.notExported({
|
|
|
|
|
type: "VsInfo",
|
|
|
|
|
list: await this.getAllBattleList(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
return latestList;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new TypeError(`Unknown listMethod: ${this.listMethod}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fetch(exporter: GameExporter) {
|
|
|
|
|
return [...await this.innerFetch(exporter)].reverse();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class CoopListFetcher implements GameListFetcher {
|
|
|
|
|
constructor(
|
|
|
|
|
protected splatnet: Splatnet3,
|
|
|
|
|
) {}
|
|
|
|
|
|
|
|
|
|
async fetch(exporter: GameExporter) {
|
|
|
|
|
return [
|
|
|
|
|
...await exporter.notExported({
|
|
|
|
|
type: "CoopInfo",
|
|
|
|
|
list: await this.splatnet.getBattleList(BattleListType.Coop),
|
|
|
|
|
}),
|
|
|
|
|
].reverse();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-26 09:45:44 -05:00
|
|
|
function progress({ total, currentUrl, done }: StepProgress): Progress {
|
|
|
|
|
return {
|
|
|
|
|
total,
|
|
|
|
|
currentUrl,
|
|
|
|
|
current: done,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-21 07:09:30 -04:00
|
|
|
export class App {
|
2022-11-17 06:19:11 -05:00
|
|
|
profile: Profile;
|
|
|
|
|
env: Env;
|
2022-10-21 07:09:30 -04:00
|
|
|
|
|
|
|
|
constructor(public opts: Opts) {
|
2022-11-17 06:19:11 -05:00
|
|
|
const stateBackend = opts.stateBackend ??
|
2022-10-24 08:46:21 -04:00
|
|
|
new FileStateBackend(opts.profilePath);
|
2022-11-17 06:19:11 -05:00
|
|
|
this.profile = new Profile({
|
|
|
|
|
stateBackend,
|
|
|
|
|
env: opts.env,
|
|
|
|
|
});
|
|
|
|
|
this.env = opts.env;
|
2023-06-06 09:47:18 -04:00
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
opts.listMethod && !["all", "auto", "latest"].includes(opts.listMethod)
|
|
|
|
|
) {
|
|
|
|
|
throw new TypeError(`Unknown listMethod: ${opts.listMethod}`);
|
|
|
|
|
}
|
2022-10-21 07:09:30 -04:00
|
|
|
}
|
2022-11-17 06:19:11 -05:00
|
|
|
|
2022-10-24 14:45:36 -04:00
|
|
|
getSkipMode(): ("vs" | "coop")[] {
|
|
|
|
|
const mode = this.opts.skipMode;
|
|
|
|
|
if (mode === "vs") {
|
|
|
|
|
return ["vs"];
|
|
|
|
|
} else if (mode === "coop") {
|
|
|
|
|
return ["coop"];
|
|
|
|
|
}
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
async getExporters(): Promise<GameExporter[]> {
|
2022-11-17 06:19:11 -05:00
|
|
|
const state = this.profile.state;
|
2022-10-21 07:09:30 -04:00
|
|
|
const exporters = this.opts.exporter.split(",");
|
2022-10-24 14:45:36 -04:00
|
|
|
const out: GameExporter[] = [];
|
2022-10-21 07:09:30 -04:00
|
|
|
|
|
|
|
|
if (exporters.includes("stat.ink")) {
|
2022-11-17 06:19:11 -05:00
|
|
|
if (!state.statInkApiKey) {
|
2022-11-18 07:27:23 -05:00
|
|
|
const key = (await this.env.prompts.prompt(
|
|
|
|
|
"stat.ink API key is not set. Please enter below.",
|
|
|
|
|
)).trim();
|
2022-10-21 07:09:30 -04:00
|
|
|
if (!key) {
|
2022-11-17 06:19:11 -05:00
|
|
|
this.env.logger.error("API key is required.");
|
2022-10-21 07:09:30 -04:00
|
|
|
Deno.exit(1);
|
|
|
|
|
}
|
2022-11-17 06:19:11 -05:00
|
|
|
await this.profile.writeState({
|
|
|
|
|
...state,
|
2022-10-21 07:09:30 -04:00
|
|
|
statInkApiKey: key,
|
|
|
|
|
});
|
|
|
|
|
}
|
2022-10-22 18:52:08 -04:00
|
|
|
out.push(
|
2022-11-05 00:57:29 -04:00
|
|
|
new StatInkExporter({
|
2022-11-18 07:42:07 -05:00
|
|
|
statInkApiKey: this.profile.state.statInkApiKey!,
|
2022-11-05 00:57:29 -04:00
|
|
|
uploadMode: this.opts.monitor ? "Monitoring" : "Manual",
|
2022-11-25 06:07:39 -05:00
|
|
|
env: this.env,
|
2022-11-05 00:57:29 -04:00
|
|
|
}),
|
2022-10-22 18:52:08 -04:00
|
|
|
);
|
2022-10-21 07:09:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (exporters.includes("file")) {
|
2022-11-17 06:19:11 -05:00
|
|
|
out.push(new FileExporter(state.fileExportPath));
|
2022-10-21 07:09:30 -04:00
|
|
|
}
|
|
|
|
|
|
2023-02-27 23:52:40 -05:00
|
|
|
if (exporters.includes("mongodb")) {
|
|
|
|
|
if (!state.mongoDbUri) {
|
|
|
|
|
const uri = (await this.env.prompts.prompt(
|
|
|
|
|
"MongoDB URI is not set. Please enter below.",
|
|
|
|
|
)).trim();
|
|
|
|
|
if (!uri) {
|
|
|
|
|
this.env.logger.error("MongoDB URI is required.");
|
|
|
|
|
Deno.exit(1);
|
|
|
|
|
}
|
|
|
|
|
await this.profile.writeState({
|
|
|
|
|
...state,
|
|
|
|
|
mongoDbUri: uri,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
out.push(
|
|
|
|
|
new MongoDBExporter(this.profile.state.mongoDbUri!),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-06-12 14:16:13 -04:00
|
|
|
if (exporters.includes("splashcat")) {
|
|
|
|
|
out.push(new SplashcatExporter({
|
|
|
|
|
env: this.env,
|
|
|
|
|
uploadMode: this.opts.monitor ? "Monitoring" : "Manual",
|
|
|
|
|
splashcatApiKey: this.profile.state.splashcatApiKey!,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-21 07:09:30 -04:00
|
|
|
return out;
|
|
|
|
|
}
|
2022-10-24 14:45:36 -04:00
|
|
|
exporterProgress(title: string) {
|
2022-10-21 07:09:30 -04:00
|
|
|
const bar = !this.opts.noProgress
|
|
|
|
|
? new MultiProgressBar({
|
2022-10-24 14:45:36 -04:00
|
|
|
title,
|
2022-10-21 07:09:30 -04:00
|
|
|
display: "[:bar] :text :percent :time eta: :eta :completed/:total",
|
|
|
|
|
})
|
|
|
|
|
: undefined;
|
|
|
|
|
|
2022-10-24 14:45:36 -04:00
|
|
|
const allProgress: Record<string, Progress> = {};
|
|
|
|
|
const redraw = (name: string, progress: Progress) => {
|
|
|
|
|
allProgress[name] = progress;
|
2022-10-31 00:33:00 -04:00
|
|
|
if (bar) {
|
|
|
|
|
bar.render(
|
|
|
|
|
Object.entries(allProgress).map(([name, progress]) => ({
|
|
|
|
|
completed: progress.current,
|
|
|
|
|
total: progress.total,
|
|
|
|
|
text: name,
|
|
|
|
|
})),
|
|
|
|
|
);
|
|
|
|
|
} else if (progress.currentUrl) {
|
2022-11-17 06:19:11 -05:00
|
|
|
this.env.logger.log(
|
2022-11-04 04:31:07 -04:00
|
|
|
`Battle exported to ${progress.currentUrl} (${progress.current}/${progress.total})`,
|
|
|
|
|
);
|
2022-10-31 00:33:00 -04:00
|
|
|
}
|
2022-10-24 14:45:36 -04:00
|
|
|
};
|
|
|
|
|
const endBar = () => {
|
|
|
|
|
bar?.end();
|
|
|
|
|
};
|
2022-10-21 07:09:30 -04:00
|
|
|
|
2022-10-24 14:45:36 -04:00
|
|
|
return { redraw, endBar };
|
|
|
|
|
}
|
2022-11-17 06:19:11 -05:00
|
|
|
private async exportOnce() {
|
|
|
|
|
const splatnet = new Splatnet3({ profile: this.profile, env: this.env });
|
2022-10-24 14:45:36 -04:00
|
|
|
const exporters = await this.getExporters();
|
2022-11-04 04:31:07 -04:00
|
|
|
const initStats = () =>
|
|
|
|
|
Object.fromEntries(
|
2022-11-26 09:45:44 -05:00
|
|
|
exporters.map((e) => [e.name, new StepProgress()]),
|
2022-11-04 04:31:07 -04:00
|
|
|
);
|
|
|
|
|
let stats = initStats();
|
2022-10-24 14:45:36 -04:00
|
|
|
const skipMode = this.getSkipMode();
|
2022-11-04 04:31:07 -04:00
|
|
|
const errors: unknown[] = [];
|
2022-10-24 14:45:36 -04:00
|
|
|
|
2022-11-26 10:01:42 -05:00
|
|
|
if (skipMode.includes("vs") || exporters.length === 0) {
|
2022-11-17 06:19:11 -05:00
|
|
|
this.env.logger.log("Skip exporting VS games.");
|
2022-10-24 14:45:36 -04:00
|
|
|
} else {
|
2023-06-06 09:47:18 -04:00
|
|
|
const gameListFetcher = new BattleListFetcher(
|
|
|
|
|
this.opts.listMethod ?? "auto",
|
|
|
|
|
splatnet,
|
|
|
|
|
);
|
2022-10-24 14:45:36 -04:00
|
|
|
|
2022-10-24 14:48:49 -04:00
|
|
|
const { redraw, endBar } = this.exporterProgress("Export vs games");
|
2022-10-24 14:45:36 -04:00
|
|
|
const fetcher = new GameFetcher({
|
2022-11-17 06:19:11 -05:00
|
|
|
cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir),
|
|
|
|
|
state: this.profile.state,
|
|
|
|
|
splatnet,
|
2022-10-24 08:46:21 -04:00
|
|
|
});
|
2022-10-21 07:09:30 -04:00
|
|
|
|
2022-10-28 08:45:56 -04:00
|
|
|
const finalRankState = await fetcher.updateRank();
|
|
|
|
|
|
2022-10-24 08:46:21 -04:00
|
|
|
await Promise.all(
|
|
|
|
|
exporters.map((e) =>
|
|
|
|
|
showError(
|
2022-11-17 06:19:11 -05:00
|
|
|
this.env,
|
2022-10-24 14:45:36 -04:00
|
|
|
this.exportGameList({
|
|
|
|
|
type: "VsInfo",
|
2022-10-24 08:46:21 -04:00
|
|
|
fetcher,
|
|
|
|
|
exporter: e,
|
2023-06-06 09:47:18 -04:00
|
|
|
gameListFetcher,
|
2022-11-26 09:45:44 -05:00
|
|
|
stepProgress: stats[e.name],
|
|
|
|
|
onStep: () => {
|
|
|
|
|
redraw(e.name, progress(stats[e.name]));
|
2022-11-04 04:31:07 -04:00
|
|
|
},
|
2022-11-26 09:45:44 -05:00
|
|
|
}),
|
2022-10-24 08:46:21 -04:00
|
|
|
)
|
|
|
|
|
.catch((err) => {
|
2022-11-04 04:31:07 -04:00
|
|
|
errors.push(err);
|
2022-11-17 06:19:11 -05:00
|
|
|
this.env.logger.error(`\nFailed to export to ${e.name}:`, err);
|
2022-10-24 08:46:21 -04:00
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
);
|
2022-10-21 07:09:30 -04:00
|
|
|
|
2022-11-04 11:34:07 -04:00
|
|
|
endBar();
|
|
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
this.printStats(stats);
|
2022-11-04 04:31:07 -04:00
|
|
|
if (errors.length > 0) {
|
|
|
|
|
throw errors[0];
|
|
|
|
|
}
|
|
|
|
|
|
2022-10-28 08:45:56 -04:00
|
|
|
// save rankState only if all exporters succeeded
|
|
|
|
|
fetcher.setRankState(finalRankState);
|
2022-11-17 06:19:11 -05:00
|
|
|
await this.profile.writeState({
|
|
|
|
|
...this.profile.state,
|
2022-10-28 08:45:56 -04:00
|
|
|
rankState: finalRankState,
|
|
|
|
|
});
|
2022-10-24 14:45:36 -04:00
|
|
|
}
|
2022-10-22 18:52:08 -04:00
|
|
|
|
2022-11-04 04:31:07 -04:00
|
|
|
stats = initStats();
|
|
|
|
|
|
2022-11-26 10:01:42 -05:00
|
|
|
if (skipMode.includes("coop") || exporters.length === 0) {
|
2022-11-17 06:19:11 -05:00
|
|
|
this.env.logger.log("Skip exporting coop games.");
|
2022-10-24 14:45:36 -04:00
|
|
|
} else {
|
2023-06-06 09:47:18 -04:00
|
|
|
const gameListFetcher = new CoopListFetcher(splatnet);
|
2022-10-24 14:45:36 -04:00
|
|
|
|
2022-10-24 14:48:49 -04:00
|
|
|
const { redraw, endBar } = this.exporterProgress("Export coop games");
|
2022-10-24 14:45:36 -04:00
|
|
|
const fetcher = new GameFetcher({
|
2022-11-17 06:19:11 -05:00
|
|
|
cache: this.opts.cache ?? new FileCache(this.profile.state.cacheDir),
|
|
|
|
|
state: this.profile.state,
|
|
|
|
|
splatnet,
|
2022-10-24 14:45:36 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await Promise.all(
|
2022-11-25 06:07:39 -05:00
|
|
|
exporters.map((e) =>
|
2022-10-24 14:45:36 -04:00
|
|
|
showError(
|
2022-11-17 06:19:11 -05:00
|
|
|
this.env,
|
2022-10-24 14:45:36 -04:00
|
|
|
this.exportGameList({
|
|
|
|
|
type: "CoopInfo",
|
|
|
|
|
fetcher,
|
|
|
|
|
exporter: e,
|
2023-06-06 09:47:18 -04:00
|
|
|
gameListFetcher,
|
2022-11-26 09:45:44 -05:00
|
|
|
stepProgress: stats[e.name],
|
|
|
|
|
onStep: () => {
|
|
|
|
|
redraw(e.name, progress(stats[e.name]));
|
2022-11-04 04:31:07 -04:00
|
|
|
},
|
2022-11-26 09:45:44 -05:00
|
|
|
}),
|
2022-10-24 14:45:36 -04:00
|
|
|
)
|
|
|
|
|
.catch((err) => {
|
2022-11-04 04:31:07 -04:00
|
|
|
errors.push(err);
|
2022-11-17 06:19:11 -05:00
|
|
|
this.env.logger.error(`\nFailed to export to ${e.name}:`, err);
|
2022-10-24 14:45:36 -04:00
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
2022-11-04 11:34:07 -04:00
|
|
|
endBar();
|
|
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
this.printStats(stats);
|
2022-11-04 04:31:07 -04:00
|
|
|
if (errors.length > 0) {
|
|
|
|
|
throw errors[0];
|
|
|
|
|
}
|
2022-10-24 08:46:21 -04:00
|
|
|
}
|
2022-11-26 10:01:42 -05:00
|
|
|
|
|
|
|
|
const summaryExporters = exporters.filter((e) => e.exportSummary);
|
|
|
|
|
if (!this.opts.withSummary || summaryExporters.length === 0) {
|
|
|
|
|
this.env.logger.log("Skip exporting summary.");
|
|
|
|
|
} else {
|
|
|
|
|
this.env.logger.log("Fetching summary...");
|
|
|
|
|
const summary = await splatnet.getSummary();
|
|
|
|
|
|
|
|
|
|
await Promise.all(
|
|
|
|
|
summaryExporters.map((e) =>
|
|
|
|
|
showError(
|
|
|
|
|
this.env,
|
|
|
|
|
e.exportSummary!(summary),
|
|
|
|
|
).then((result) => {
|
|
|
|
|
if (result.status === "success") {
|
|
|
|
|
this.env.logger.log(`Exported summary to ${result.url}`);
|
|
|
|
|
} else if (result.status === "skip") {
|
|
|
|
|
this.env.logger.log(`Skipped exporting summary to ${e.name}`);
|
|
|
|
|
} else {
|
|
|
|
|
const _never: never = result;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
errors.push(err);
|
|
|
|
|
this.env.logger.error(`\nFailed to export to ${e.name}:`, err);
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
throw errors[0];
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-07 10:09:32 -05:00
|
|
|
|
|
|
|
|
const stageExporters = exporters.filter((e) => e.exportStages);
|
|
|
|
|
if (!this.opts.withStages || stageExporters.length === 0) {
|
|
|
|
|
this.env.logger.log("Skip exporting stages.");
|
|
|
|
|
} else {
|
|
|
|
|
const stageRecords = await splatnet.getStageRecords();
|
|
|
|
|
|
|
|
|
|
await Promise.all(
|
|
|
|
|
stageExporters.map((e) =>
|
|
|
|
|
showError(
|
|
|
|
|
this.env,
|
|
|
|
|
e.exportStages!(stageRecords.stageRecords.nodes),
|
|
|
|
|
).then((result) => {
|
|
|
|
|
if (result.status === "success") {
|
|
|
|
|
this.env.logger.log(`Exported stages to ${result.url}`);
|
|
|
|
|
} else if (result.status === "skip") {
|
|
|
|
|
this.env.logger.log(`Skipped exporting stages to ${e.name}`);
|
|
|
|
|
} else {
|
|
|
|
|
const _never: never = result;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
errors.push(err);
|
|
|
|
|
this.env.logger.error(`\nFailed to export to ${e.name}:`, err);
|
|
|
|
|
})
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2022-10-22 18:52:08 -04:00
|
|
|
}
|
|
|
|
|
async monitor() {
|
|
|
|
|
while (true) {
|
|
|
|
|
await this.exportOnce();
|
2022-11-17 06:19:11 -05:00
|
|
|
await this.countDown(this.profile.state.monitorInterval);
|
2022-10-22 18:52:08 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
async countDown(sec: number) {
|
|
|
|
|
const bar = !this.opts.noProgress
|
|
|
|
|
? new MultiProgressBar({
|
|
|
|
|
title: "Killing time...",
|
|
|
|
|
display: "[:bar] :completed/:total",
|
|
|
|
|
})
|
|
|
|
|
: undefined;
|
|
|
|
|
for (const i of Array(sec).keys()) {
|
|
|
|
|
bar?.render([{
|
|
|
|
|
completed: i,
|
|
|
|
|
total: sec,
|
|
|
|
|
}]);
|
|
|
|
|
await delay(1000);
|
|
|
|
|
}
|
|
|
|
|
bar?.end();
|
|
|
|
|
}
|
|
|
|
|
async run() {
|
2022-11-17 06:19:11 -05:00
|
|
|
await this.profile.readState();
|
2022-10-22 18:52:08 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
if (!this.profile.state.loginState?.sessionToken) {
|
2022-11-18 07:27:23 -05:00
|
|
|
const sessionToken = await loginManually(this.env);
|
2022-10-22 18:52:08 -04:00
|
|
|
|
2022-11-17 06:19:11 -05:00
|
|
|
await this.profile.writeState({
|
|
|
|
|
...this.profile.state,
|
2022-10-22 18:52:08 -04:00
|
|
|
loginState: {
|
2022-11-17 06:19:11 -05:00
|
|
|
...this.profile.state.loginState,
|
2022-10-22 18:52:08 -04:00
|
|
|
sessionToken,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (this.opts.monitor) {
|
|
|
|
|
await this.monitor();
|
|
|
|
|
} else {
|
|
|
|
|
await this.exportOnce();
|
|
|
|
|
}
|
2022-10-21 07:09:30 -04:00
|
|
|
}
|
|
|
|
|
/**
|
2022-10-24 14:45:36 -04:00
|
|
|
* Export game list.
|
2022-10-21 07:09:30 -04:00
|
|
|
*
|
|
|
|
|
* @param fetcher BattleFetcher
|
|
|
|
|
* @param exporter BattleExporter
|
2022-10-24 14:45:36 -04:00
|
|
|
* @param gameList ID list of games, sorted by date, newest first
|
|
|
|
|
* @param onStep Callback function called when a game is exported
|
2022-10-21 07:09:30 -04:00
|
|
|
*/
|
2023-06-06 09:47:18 -04:00
|
|
|
private async exportGameList({
|
2022-10-24 14:45:36 -04:00
|
|
|
type,
|
|
|
|
|
fetcher,
|
|
|
|
|
exporter,
|
2023-06-06 09:47:18 -04:00
|
|
|
gameListFetcher,
|
2022-11-26 09:45:44 -05:00
|
|
|
stepProgress,
|
2022-10-24 14:45:36 -04:00
|
|
|
onStep,
|
|
|
|
|
}: {
|
|
|
|
|
type: Game["type"];
|
|
|
|
|
exporter: GameExporter;
|
|
|
|
|
fetcher: GameFetcher;
|
2023-06-06 09:47:18 -04:00
|
|
|
gameListFetcher: GameListFetcher;
|
2022-11-26 09:45:44 -05:00
|
|
|
stepProgress: StepProgress;
|
|
|
|
|
onStep: () => void;
|
|
|
|
|
}): Promise<StepProgress> {
|
|
|
|
|
onStep?.();
|
2022-10-21 07:09:30 -04:00
|
|
|
|
2023-06-06 09:47:18 -04:00
|
|
|
const workQueue = await gameListFetcher.fetch(exporter);
|
2022-10-24 14:45:36 -04:00
|
|
|
|
|
|
|
|
const step = async (id: string) => {
|
|
|
|
|
const detail = await fetcher.fetch(type, id);
|
2022-11-26 09:45:44 -05:00
|
|
|
const result = await exporter.exportGame(detail);
|
|
|
|
|
|
|
|
|
|
stepProgress.done += 1;
|
|
|
|
|
stepProgress.currentUrl = undefined;
|
|
|
|
|
|
|
|
|
|
if (result.status === "success") {
|
|
|
|
|
stepProgress.exported += 1;
|
|
|
|
|
stepProgress.currentUrl = result.url;
|
|
|
|
|
} else if (result.status === "skip") {
|
|
|
|
|
const { skipped } = stepProgress;
|
|
|
|
|
skipped[result.reason] = (skipped[result.reason] ?? 0) + 1;
|
|
|
|
|
} else {
|
|
|
|
|
const _never: never = result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onStep?.();
|
2022-10-21 07:09:30 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (workQueue.length > 0) {
|
2022-11-26 09:45:44 -05:00
|
|
|
stepProgress.total = workQueue.length;
|
|
|
|
|
onStep?.();
|
2022-10-21 07:09:30 -04:00
|
|
|
for (const battle of workQueue) {
|
|
|
|
|
await step(battle);
|
|
|
|
|
}
|
2022-10-24 15:11:53 -04:00
|
|
|
} else {
|
2022-11-26 09:45:44 -05:00
|
|
|
stepProgress.done = 1;
|
|
|
|
|
onStep?.();
|
2022-10-21 07:09:30 -04:00
|
|
|
}
|
|
|
|
|
|
2022-11-26 09:45:44 -05:00
|
|
|
return stepProgress;
|
2022-10-21 07:09:30 -04:00
|
|
|
}
|
2022-11-26 09:45:44 -05:00
|
|
|
printStats(stats: Record<string, StepProgress>) {
|
2022-11-17 06:19:11 -05:00
|
|
|
this.env.logger.log(
|
|
|
|
|
`Exported ${
|
|
|
|
|
Object.entries(stats)
|
2022-11-26 09:45:44 -05:00
|
|
|
.map(([name, { exported }]) => `${name}: ${exported}`)
|
2022-11-17 06:19:11 -05:00
|
|
|
.join(", ")
|
|
|
|
|
}`,
|
|
|
|
|
);
|
2022-11-26 09:45:44 -05:00
|
|
|
if (Object.values(stats).some((i) => Object.keys(i.skipped).length > 0)) {
|
|
|
|
|
this.env.logger.log(
|
|
|
|
|
`Skipped ${
|
|
|
|
|
Object.entries(stats)
|
|
|
|
|
.map(([name, { skipped }]) =>
|
|
|
|
|
Object.entries(skipped).map(([reason, count]) =>
|
|
|
|
|
`${name}: ${reason} (${count})`
|
|
|
|
|
).join(", ")
|
|
|
|
|
)
|
|
|
|
|
}`,
|
|
|
|
|
);
|
|
|
|
|
}
|
2022-11-17 06:19:11 -05:00
|
|
|
}
|
2022-10-21 07:09:30 -04:00
|
|
|
}
|