music: the server picks the random tracks
The app shuffled by flattening every album's meta.json in scope into one array and choosing from it. On a hundred-thousand-track library that meant a six-figure object array living in the phone's JS heap — marked by every garbage collection — and copied again on each draw. Measured on device: the JS thread never went idle during playback and the phone reached 'serious' thermal state. Picking twenty songs never required any of that. The manifest already holds per-album track counts, so the choice is arithmetic over numbers and only the albums actually picked are read. Uniform over TRACKS, not albums: choosing an album first would make a two-track EP as likely as a sixty-track box set, so albums are weighted by count via a running total and a binary search. Same distribution as shuffling everything and taking the first n, which is what a shuffle means. Results are re-shuffled before returning, because grouping the reads by album would otherwise hand back something sorted by album.
This commit is contained in:
@@ -3,6 +3,7 @@ import { join, basename } from 'node:path';
|
||||
import type { SidecarCommand, SidecarEvent } from '@@/sidecar/protocol';
|
||||
import { createSidecarConnector } from '@@/sidecar/connect';
|
||||
import { streamAudioFile } from './stream-audio';
|
||||
import { randomSongs } from './random';
|
||||
import { cliampUpgradeData, musicWebsocket } from '../cliamp/cliamp-ws';
|
||||
import { ensurePulseAudio } from '../cliamp/pulse-audio';
|
||||
import { startNightlyReindex, stopNightlyReindex } from './nightly-reindex';
|
||||
@@ -73,6 +74,8 @@ const asKeys = (v: unknown): string[] | null =>
|
||||
//
|
||||
// GET /stream?path=<home-relative> audio with Range→206 (Content-Range/Length/Accept-Ranges)
|
||||
// + `X-Audio-Duration` (seconds, ffprobe). 400/404/416.
|
||||
// GET /random?path=<rel>&n=<1..100> n random tracks from that scope, uniform over TRACKS (albums
|
||||
// weighted by count). Reads only the albums picked — see random.ts.
|
||||
// GET /manifest pure read of the last completed index (NO build triggered) —
|
||||
// { version, generatedAt, albums: { "<rel>": { v, cover, tracks, videos?, disco? } } }
|
||||
// GET /meta?path=<rel> album meta.json (IndexMeta). ETag: <v>; If-None-Match → 304.
|
||||
@@ -251,6 +254,23 @@ const server = Bun.serve({
|
||||
}
|
||||
|
||||
// ── Sync surface ──
|
||||
/*
|
||||
A random handful of tracks, chosen here rather than on the phone.
|
||||
|
||||
See random.ts for why: the app used to flatten every album's meta into one array to pick twenty
|
||||
songs, which on a large library meant a six-figure object array living in the JS heap and being
|
||||
copied on every draw. The manifest already holds per-album track counts, so the choice costs
|
||||
arithmetic plus a read of only the albums actually picked.
|
||||
*/
|
||||
if (url.pathname === '/random') {
|
||||
const scope = url.searchParams.get('path') ?? '';
|
||||
const raw = Number(url.searchParams.get('n') ?? '20');
|
||||
// Clamped: this reads one meta.json per distinct album picked, so an unbounded n is an
|
||||
// unbounded amount of disk work from a query string.
|
||||
const n = Number.isFinite(raw) ? Math.max(1, Math.min(100, Math.floor(raw))) : 20;
|
||||
return json({ songs: await randomSongs(scope, n) }, { headers: NO_CACHE });
|
||||
}
|
||||
|
||||
if (url.pathname === '/manifest') {
|
||||
// Pure read — returns the last completed index. It does NOT trigger a build (that could kick off a
|
||||
// long/full rebuild on a plain app refresh); use POST /reindex explicitly to pick up disk changes.
|
||||
|
||||
Reference in New Issue
Block a user