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.
This commit is contained in:
byrongamatos
2026-07-15 00:17:35 +02:00
parent b7b28deb53
commit 98a270255b
4 changed files with 124 additions and 116 deletions
+30 -26
View File
@@ -829,45 +829,49 @@ def post_song_gap_fill(filename: str, data: dict):
return {"ok": True, "written": additions, "skipped": skipped}
def _playable_stems_payload(song_path, filename: str) -> dict:
def _playable_stems_payload(filename: str, dlc) -> dict:
"""The playable stems (id/url/default) + full-mix URL for a sloppak.
Byte-for-byte the same shape the highway's WS `ready` builds — same
partition (the mixdown lifted out), same default resolution, same URL form —
because the stems plugin now preloads from THIS and then has to agree with
what the WS says a moment later, or it rebuilds the graph for nothing.
Why it exists: 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
decoded, and then copied the whole song's PCM to its worklet, with the player
on screen — half a gigabyte of memcpy in one frame, ~700 ms, freezing the
venue video. Given the list at `song:loading` it can do all of that BEFORE the
highway appears, behind the loading overlay where a stall costs nothing.
Why it exists: the stems plugin could only learn its stem list from the WS
`ready`, which arrives once the highway is up. So it decoded, and then copied
the whole song's PCM to its worklet, with the player already on screen — half
a gigabyte of memcpy in one frame, ~700 ms, freezing the venue video (#…).
Given the list at `song:loading` it can do all of that BEFORE the highway
appears, behind the loading overlay where a stall costs nothing.
The list MUST be the same one the WS sends a moment later. If it is not, the
plugin preloads a graph and then throws it away and rebuilds — strictly worse
than not preloading. So this does not reimplement the WS's construction, it
calls THE SAME FUNCTION: load_song, whose LoadedSloppak already carries the
partitioned stems and the resolved full mix, and then builds the URLs exactly
as ws_highway does. Drift is impossible by construction rather than by
agreement — which matters, because `full_mix` in particular is not simply the
`full` stem: load_song falls back to the deprecated `original_audio:` key for
every pack written before feedpak 1.15.0, and reimplementing that (I did, at
first) silently dropped the pristine full mix for most real libraries.
Opt-in (`?stems=1`) so the library's own metadata calls — the hot path — pay
nothing for it.
nothing for it. Non-sloppak sources (archives, loose folders) have no stems
to preload: load_song raises and we return the empty list.
"""
from urllib.parse import quote
try:
meta = sloppak_mod.extract_meta(song_path)
loaded = sloppak_mod.load_song(filename, dlc, appstate.sloppak_cache_dir)
except Exception:
return {"stems": [], "full_mix_url": None}
q_fn = quote(filename, safe="")
stems = [
{
"id": s["id"],
"url": f"/api/sloppak/{q_fn}/file/{quote(s['file'])}",
"default": bool(s.get("default", True)),
}
for s in (meta.get("stems") or [])
if s.get("id") and s.get("file")
]
full_file = meta.get("full_mix_file")
def _url(rel: str) -> str:
return f"/api/sloppak/{q_fn}/file/{quote(rel)}"
return {
"stems": stems,
"full_mix_url": f"/api/sloppak/{q_fn}/file/{quote(full_file)}" if full_file else None,
"stems": [
{"id": s["id"], "url": _url(s["file"]), "default": s["default"]}
for s in loaded.stems
],
"full_mix_url": _url(loaded.full_mix) if loaded.full_mix else None,
}
@@ -911,7 +915,7 @@ async def get_song_info(filename: str, stems: int = 0):
if not stems:
return meta
extra = await loop.run_in_executor(
None, _playable_stems_payload, song_path, filename)
None, _playable_stems_payload, filename, dlc)
return {**meta, **extra}
if cached: