Files
plebsaber.stream/scripts/archive/config.ts
T
plebandClaude Opus 5.5 a759308711 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>
2026-11-08 18:30:02 -08:00

77 lines
3.2 KiB
TypeScript

// 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);
}