feedBack/lib/routers
Byron Gamatos 939c98214b
feat(song-info): publish the playable stem list so stems can preload (fixes the 698ms freeze) (#972)
* feat(song-info): publish the playable stem list, so stems can preload

The stems plugin could only learn its stem list from the highway's WS `ready`,
which arrives once the highway is already up. So it fetched, decoded, and then
handed every stem's PCM to its audio worklet — copying the WHOLE SONG — with the
player already on screen.

For a 4-minute 6-stem pack that is over half a GIGABYTE of memcpy, in one frame,
on the main thread. Measured on a real load: a 698 ms frame, right as the
song-credits card appeared, with the venue video visibly stopping. That is the
"the video pauses when the author appears" report.

GET /api/song/{f}?stems=1 now returns the same list — [{id, url, default}] plus
full_mix_url — so the plugin can start the whole load at `song:loading`, before
the highway (and the venue) is drawn, where a stalled frame costs nothing.
Nothing about the work changes; only WHEN.

Opt-in via the query param so the library's own metadata calls — the hot path —
pay nothing. Deliberately NOT stored in the metadata cache: that is a
fixed-column table, and widening it would mean a schema migration plus a stale
row for every song already scanned, to cache something that is a plain manifest
read on an already-unpacked pack.

The safety property: REST and the WS must publish the SAME list. If they
disagreed the plugin would preload a graph and then throw it away and rebuild —
strictly worse than not preloading. So both now resolve `default` through one
shared helper (stem_default_on, extracted from load_song), and a test rebuilds
the WS's payload from load_song and requires the REST helper to produce the
identical list, rather than pinning either against a snapshot.

Also pinned: the mixdown is lifted OUT of the stem list (spec 5.3 — `full` is
not a layer; listing it beside the instruments would play the whole song on top
of the stems) while staying reachable as full_mix_url, a single-`full` pack keeps
it as its only playable stem, and an unreadable pack yields an empty list rather
than failing the request. Full suite 2608 passed.

Consumed by feedBack-plugin-stems (preloadSong).

* fix(song-info): call load_song for the stem payload — do not reimplement it

CodeRabbit caught a real bug, and it would have hit most real libraries.

load_song() falls back to the DEPRECATED `original_audio:` key when a pack has no
reserved `full` stem — which is every pack written before feedpak 1.15.0. My
payload rebuilt the full-mix rule from extract_meta and returned None for those:
REST would say "no full mix" while the WS said there was one.

Worse than a wrong field: the plugin would preload a graph WITHOUT the pristine
mix and — because the stem signature still matched — never rebuild. Unity
playback would silently downgrade to the lossy stem recombination.

That is exactly the drift this PR claims to prevent, and my test had a hole: I
only covered packs that carry a `full` stem.

So stop reimplementing. The payload now calls load_song, whose LoadedSloppak
already carries the partitioned stems and the resolved full mix, and builds the
URLs exactly as ws_highway does. Drift is now impossible by construction rather
than by agreement. extract_meta is reverted to its original shape (it never
needed to change), and the shared stem_default_on helper stays as the one place
`default: off` is resolved.

Tests rewritten to compare against load_song — the WS's own function — for a
reserved-`full` pack, a LEGACY original_audio pack (the case that was broken), and
a single-`full` pack. Also documents the `?stems=1` contract in CHANGELOG.md.
Full suite green.
2026-07-15 00:36:13 +02:00
..
__init__.py fix(build): move appstate.py + routers/ under lib/ so the desktop app ships them (#836) 2026-07-10 17:16:34 +02:00
art.py refactor(server): extract the album-art routes into routers/art.py (R3) (#862) 2026-07-11 11:56:53 +02:00
artist_aliases.py refactor(server): extract the artist-alias routes into routers/artist_aliases.py (R3) (#838) 2026-07-10 18:55:03 +02:00
artist.py refactor(server): extract the artist routes into routers/artist.py (R3) (#870) 2026-07-11 15:49:37 +02:00
audio_effects.py fix(build): move appstate.py + routers/ under lib/ so the desktop app ships them (#836) 2026-07-10 17:16:34 +02:00
chart.py refactor(server): extract the chart routes into routers/chart.py (R3) (#846) 2026-07-10 22:15:25 +02:00
diagnostics.py refactor(server): extract the diagnostics routes into routers/diagnostics.py (R3) (#857) 2026-07-11 01:27:52 +02:00
enrichment.py refactor(server): extract the enrichment route handlers into routers/enrichment.py (R3) (#868) 2026-07-11 15:19:34 +02:00
library_extras.py refactor(server): batch the small meta_db user-state endpoints into routers/library_extras.py (R3) (#850) 2026-07-10 23:37:24 +02:00
library.py refactor(server): extract library + collections routes + the provider registry (R3) (#866) 2026-07-11 14:56:12 +02:00
loops.py fix(loops): take the DB lock for the count+insert and the list read (#840) 2026-07-10 19:16:01 +02:00
media.py refactor(server): extract the media/file-serving routes into routers/media.py (R3) (#869) 2026-07-11 15:36:22 +02:00
playlists.py fix(playlists): split cover decode (400) from persist (500), unique temp, existence re-check (#842) 2026-07-10 20:16:22 +02:00
profile.py refactor(server): extract the profile routes into routers/profile.py (R3) (#854) 2026-07-11 00:38:03 +02:00
progression.py refactor(server): extract the progression routes into routers/progression.py (R3) (#853) 2026-07-11 00:25:53 +02:00
settings.py refactor(server): extract the settings routes into routers/settings.py (R3) (#863) 2026-07-11 12:35:05 +02:00
shop.py refactor(server): extract the shop routes + inject get_progression_content into the seam (R3) (#851) 2026-07-10 23:42:02 +02:00
song.py feat(song-info): publish the playable stem list so stems can preload (fixes the 698ms freeze) (#972) 2026-07-15 00:36:13 +02:00
stats.py feat(career): hours-per-genre odometer — honest wall-clock play time (#942) 2026-07-13 15:12:23 +02:00
tunings.py refactor(server): extract /api/tunings into routers/tunings.py + lib/appconfig.py (R3) (#858) 2026-07-11 01:43:19 +02:00
version.py refactor(server): extract the version route into routers/version.py (R3) (#856) 2026-07-11 00:56:36 +02:00
wanted.py refactor(server): extract the wishlist routes into routers/wanted.py (R3) (#847) 2026-07-10 22:20:10 +02:00
ws_highway.py fix(sloppak): the full mix is a stem — drop the invented original_audio key (#946) 2026-07-13 12:22:42 -04:00