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:
2026-11-09 07:55:58 -08:00
co-authored by Claude Opus 5.5
parent 51c1e6d3d1
commit d182fd808c
4 changed files with 119 additions and 17 deletions
+11 -5
View File
@@ -26,10 +26,10 @@ export async function exportSSPlayer(playerId: string): Promise<void> {
console.log(`ScoreSaber player ${playerId}: ${saveSnapshotIfChanged(ss(playerId, 'player'), r.body) ? 'saved' : 'unchanged'}`);
}
/** Page `sort=recent` until scores are older than `sinceUnix`. */
export async function exportSSScores(playerId: string, sinceUnix: number): Promise<void> {
/** Page `sort=recent` until scores are older than `sinceUnix`. Returns the new items. */
export async function exportSSScores(playerId: string, sinceUnix: number): Promise<SSItem[]> {
console.log(`ScoreSaber scores for ${playerId} since ${new Date(sinceUnix * 1000).toISOString()}`);
let added = 0;
const added: SSItem[] = [];
let seen = 0;
for (let page = 1; ; page++) {
const r = await get(`${SS_API}/player/${playerId}/scores?limit=100&page=${page}&sort=recent`, { limiter: limiters.scoresaber });
@@ -44,12 +44,18 @@ export async function exportSSScores(playerId: string, sinceUnix: number): Promi
const file = ss(playerId, 'scores', `${item.score.id}.json`);
if (exists(file)) continue;
save(file, JSON.stringify(item));
added++;
added.push(item);
}
console.log(` page ${page}: ${seen} in window, ${added} new (total on server ${json.metadata.total})`);
console.log(` page ${page}: ${seen} in window, ${added.length} new (total on server ${json.metadata.total})`);
if (reachedCutoff || json.playerScores.length < 100) break;
}
updateState(`scoresaber:${playerId}`, { scoresSince: sinceUnix, lastScoresRun: new Date().toISOString() });
return added;
}
/** Newest archived ScoreSaber score time for a player (unix seconds), 0 when there is none. */
export function newestSSScoreTime(playerId: string): number {
return archivedSSScores(playerId).reduce((max, it) => Math.max(max, Math.floor(Date.parse(it.score.timeSet) / 1000)), 0);
}
/** Download replays for archived scores with hasReplay, oldest first, ~10.5 s apart. */