Add the plebleader daily sync and package the archive CLI
`sync` refreshes the token chain first, then fetches new BeatLeader and ScoreSaber scores (with a day of overlap), their replays, score stats, leaderboards and map zips, and re-snapshots every played hash on Sundays. The flake's plebleader-archive package runs the CLI with Node's type stripping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,11 @@
|
||||
import * as path from 'node:path';
|
||||
import { BL_API } from './config.ts';
|
||||
import { get, limiters } from './http.ts';
|
||||
import { archivePath, eachWithProgress, exists, listFiles, readJson, save, saveSnapshotIfChanged, today, updateState } from './store.ts';
|
||||
import { archivePath, eachWithProgress, exists, listFiles, readJson, readState, save, saveSnapshotIfChanged, today, updateState } from './store.ts';
|
||||
|
||||
const bl = (...p: string[]) => archivePath('beatleader', ...p);
|
||||
|
||||
type BLScore = { id: number; timepost: number; replay?: string | null; leaderboard: { song: { hash: string } } };
|
||||
export type BLScore = { id: number; playerId: string; timepost: number; replay?: string | null; leaderboard: { song: { hash: string } } };
|
||||
|
||||
async function blGet(pathAndQuery: string, accept?: number[]) {
|
||||
return get(`${BL_API}${pathAndQuery}`, { limiter: limiters.beatleader, accept });
|
||||
@@ -39,12 +39,12 @@ export async function exportProfile(playerId: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
/** Page through best scores newest-first from `sinceUnix`, saving each score object. */
|
||||
export async function exportScores(playerId: string, sinceUnix: number): Promise<void> {
|
||||
/** Page through best scores newest-first from `sinceUnix`, saving each score object. Returns the new ones. */
|
||||
export async function exportScores(playerId: string, sinceUnix: number): Promise<BLScore[]> {
|
||||
console.log(`BeatLeader scores for ${playerId} since ${new Date(sinceUnix * 1000).toISOString()}`);
|
||||
let added = 0;
|
||||
const added: BLScore[] = [];
|
||||
let seen = 0;
|
||||
let newest = 0;
|
||||
let newest = Number(readState()[`beatleader:${playerId}`]?.newestTimepost ?? 0);
|
||||
for (let page = 1; ; page++) {
|
||||
const qs = `count=100&page=${page}&sortBy=date&order=desc&includeIO=true&time_from=${sinceUnix}`;
|
||||
const res = await blGet(`/player/${playerId}/scores?${qs}`);
|
||||
@@ -56,12 +56,18 @@ export async function exportScores(playerId: string, sinceUnix: number): Promise
|
||||
if (exists(file)) continue;
|
||||
// The page is parsed and each element re-serialized: same content, no reshaping.
|
||||
save(file, JSON.stringify(score));
|
||||
added++;
|
||||
added.push(score);
|
||||
}
|
||||
console.log(` page ${page}: ${seen}/${json.metadata.total} seen, ${added} new`);
|
||||
console.log(` page ${page}: ${seen}/${json.metadata.total} seen, ${added.length} new`);
|
||||
if (json.data.length < 100 || seen >= json.metadata.total) break;
|
||||
}
|
||||
updateState(`beatleader:${playerId}`, { scoresSince: sinceUnix, newestTimepost: newest, lastScoresRun: new Date().toISOString() });
|
||||
return added;
|
||||
}
|
||||
|
||||
/** Newest archived score time for a player (unix seconds), 0 when there is none. */
|
||||
export function newestScoreTime(playerId: string): number {
|
||||
return archivedScores().reduce((max, s) => (s.playerId === playerId ? Math.max(max, s.timepost) : max), 0);
|
||||
}
|
||||
|
||||
export async function exportReplays(scores: BLScore[]): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user