// Import a Spotify data export into music.plays (source 'spotify', origin 'export'). // // bun scripts/music-import-history.ts [...] [--min-ms 30000] [--dry-run] // // Accepts the Extended Streaming History (Streaming_History_Audio_*.json, older // endsong_*.json) and the short account-data export (StreamingHistory_music_*.json). // Podcasts/audiobooks and plays under --min-ms are skipped. Safe to re-run: a play already // stored (same artist+title within a minute, also from the live poller) is not added twice. import { readdirSync, readFileSync, statSync } from 'node:fs'; import { join } from 'node:path'; import { DEFAULT_MIN_MS, parseHistoryEntry, type HistoryPlay } from '../src/lib/music/history'; import { insertPlay, newCache, upsertTrack } from '../src/lib/music/store.server'; import { arg, musicDb } from './music-db'; const minMs = Number(arg('min-ms') ?? DEFAULT_MIN_MS); const dryRun = process.argv.includes('--dry-run'); const skip = new Set(['--min-ms', arg('min-ms')]); const inputs = process.argv.slice(2).filter((a) => !a.startsWith('--') && !skip.has(a)); if (!inputs.length) throw new Error('Usage: bun scripts/music-import-history.ts ...'); const NAME = /^(Streaming_History_Audio_.*|endsong_?\d*|StreamingHistory(_music)?_?\d*)\.json$/i; function files(path: string): string[] { if (statSync(path).isDirectory()) return readdirSync(path).flatMap((name) => { const child = join(path, name); return statSync(child).isDirectory() ? files(child) : NAME.test(name) ? [child] : []; }); return [path]; } const plays: HistoryPlay[] = []; let entries = 0; for (const file of inputs.flatMap(files)) { const data = JSON.parse(readFileSync(file, 'utf8')) as unknown[]; if (!Array.isArray(data)) continue; entries += data.length; for (const entry of data) { const play = parseHistoryEntry(entry, minMs); if (play) plays.push(play); } console.log(`${file}: ${data.length} entries`); } plays.sort((a, b) => a.playedAt.getTime() - b.playedAt.getTime()); console.log(`${plays.length} music plays of ${minMs / 1000}s or more out of ${entries} entries.`); if (dryRun || !plays.length) process.exit(0); const sql = musicDb(); try { const cache = newCache(); let stored = 0; for (const [i, play] of plays.entries()) { const trackId = await upsertTrack(sql, play.track, false, cache); if ( await insertPlay(sql, { trackId, playedAt: play.playedAt, source: 'spotify', origin: 'export', msPlayed: play.msPlayed, platform: play.platform }) ) stored++; if ((i + 1) % 5000 === 0) console.log(` ${i + 1}/${plays.length} (${stored} new)`); } console.log(`Done: ${stored} new plays stored.`); } finally { await sql.end(); }