Add archive exporter for the personal BeatLeader mirror
scripts/archive is a CLI that exports BeatLeader and ScoreSaber scores, replays, score stats, leaderboard snapshots and BeatSaver map zips into an append-only archive. Anchor the .gitignore 'archive' rule to the repo root so it no longer hides scripts/archive. Record phase 1 results in the plan. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
// Shared configuration for the plebleader archive CLI.
|
||||
// Runs directly under Node >= 22 (type stripping), no build step.
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
|
||||
export const USER_AGENT = 'plebsaber-archive/0.1';
|
||||
export const BL_API = 'https://api.beatleader.com';
|
||||
export const MAIN_PLAYER_ID = '76561199407393962';
|
||||
export const LEGACY_PLAYER_ID = '76561197962107242';
|
||||
|
||||
export const PLAYER_IDS = [MAIN_PLAYER_ID, LEGACY_PLAYER_ID];
|
||||
|
||||
/** Scores, replays, score stats and profile data: all history by default. */
|
||||
export const DEFAULT_SINCE = 'all';
|
||||
/**
|
||||
* BeatSaver map files (JSON + zip) only for maps played in the last two years. Older maps can
|
||||
* be fetched later with --maps-since all (existing files are skipped).
|
||||
*/
|
||||
export const DEFAULT_MAPS_SINCE = '2024-11-08';
|
||||
|
||||
/** Where ss-scraper already downloaded ScoreSaber replays (<playerId>/<scoreId>_<name>.dat). */
|
||||
export const SS_SCRAPER_REPLAYS = process.env.SS_SCRAPER_REPLAYS ?? path.join(process.env.HOME ?? '', 'ops/beatsaber/ss-scraper/replays');
|
||||
|
||||
/** Parse --since: YYYY-MM-DD (UTC), unix seconds, or "all". */
|
||||
export function parseSince(value: string | undefined, fallback = DEFAULT_SINCE): number {
|
||||
const v = value ?? fallback;
|
||||
if (v === 'all') return 0;
|
||||
if (/^\d+$/.test(v)) return Number(v);
|
||||
const ms = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(v) ? `${v}T00:00:00Z` : v);
|
||||
if (Number.isNaN(ms)) throw new Error(`Invalid --since: ${v}`);
|
||||
return Math.floor(ms / 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Root of the plebleader state (archive/, auth/, db/, ...).
|
||||
* Production: /var/lib/plebleader. Local default: ./.data/plebleader (gitignored).
|
||||
*/
|
||||
export const ROOT = path.resolve(process.env.PLEBLEADER_DIR ?? path.join(process.cwd(), '.data', 'plebleader'));
|
||||
export const ARCHIVE_DIR = path.join(ROOT, 'archive');
|
||||
export const AUTH_DIR = path.join(ROOT, 'auth');
|
||||
export const TOKENS_FILE = path.join(AUTH_DIR, 'beatleader_tokens.json');
|
||||
|
||||
/** Load KEY=VALUE (optionally `export KEY=VALUE`) lines from .env without overriding the real environment. */
|
||||
export function loadDotEnv(file = path.join(process.cwd(), '.env')): void {
|
||||
if (!fs.existsSync(file)) return;
|
||||
for (const line of fs.readFileSync(file, 'utf-8').split('\n')) {
|
||||
const m = line.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/);
|
||||
if (!m) continue;
|
||||
const [, key, raw] = m;
|
||||
const value = raw.replace(/^(['"])(.*)\1$/, '$2');
|
||||
if (process.env[key] === undefined) process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
export function clientCredentials(): { clientId: string; clientSecret: string } {
|
||||
const clientId = process.env.BL_CLIENT_ID;
|
||||
const clientSecret = process.env.BL_CLIENT_SECRET;
|
||||
if (!clientId || !clientSecret) {
|
||||
throw new Error('BL_CLIENT_ID and BL_CLIENT_SECRET must be set (environment or .env).');
|
||||
}
|
||||
return { clientId, clientSecret };
|
||||
}
|
||||
|
||||
/** Write a file atomically: temp file in the same directory, fsync, rename. */
|
||||
export function writeFileAtomic(file: string, data: string | Uint8Array, mode = 0o644): void {
|
||||
fs.mkdirSync(path.dirname(file), { recursive: true });
|
||||
const tmp = `${file}.tmp-${process.pid}-${Date.now()}`;
|
||||
const fd = fs.openSync(tmp, 'w', mode);
|
||||
try {
|
||||
fs.writeSync(fd, typeof data === 'string' ? Buffer.from(data, 'utf-8') : data);
|
||||
fs.fsyncSync(fd);
|
||||
} finally {
|
||||
fs.closeSync(fd);
|
||||
}
|
||||
fs.renameSync(tmp, file);
|
||||
}
|
||||
Reference in New Issue
Block a user