feedBack/lib/routers
Byron Gamatos 329cc86315
Some checks failed
ship-ci / ci (push) Waiting to run
Nightly / build-docker (push) Has been cancelled
fix(sloppak): the full mix is a stem — drop the invented original_audio key (#946)
* fix(sloppak): the full mix is a stem — drop the invented `original_audio` key (#933)

Core read, served, and depended on `original_audio:` — a top-level manifest key
this repo invented in #583 that the feedpak spec never defined. The format
already had a home for the pre-separation mixdown: it is a stem. feedpak 1.15.0
(feedpak-spec#53) RESERVES the id `full` for it, so read it from there.

The key existed to work around a bug in our own reader. The packer's comment
said so plainly: "we must NOT list the full mix as a playable stem — the player
sums every entry in `stems` and does not gate playback on `default`, so a listed
full mix plays on top of the stems". Faced with a reader that would double the
song, the packer put the mixdown outside `stems` and invented a key to point at
it. The fix belongs in the reader, and that is what this is.

load_song() now partitions the stem list: `full` comes out as
LoadedSloppak.full_mix, the instruments stay in .stems. Nothing that sums stems
or draws one fader per stem can see the mixdown, so retaining it is safe — which
is what lets the packer put it where the format says it goes.

- ws_highway: `song_info` gains full_mix_url / has_full_mix. The old
  original_audio_url / has_original_audio remain as deprecated aliases for one
  release so an older stems plugin keeps working (#945).
- `stems` on the wire, and stem_ids / stem_count in the library index, are now
  INSTRUMENT stems only — a separated pack that retains its mixdown no longer
  advertises a bogus "full" chip or an inflated stem count.
- enrichment: fingerprint against the mixdown wherever it lives. This widens
  coverage — _song_audio_file() previously returned None for any pack without
  the invented key, so fingerprinting silently did nothing for nearly every pack.
- sloppak: `original_audio:` is still READ as a deprecated fallback, because
  every pack in the wild carries it and would otherwise lose its pristine mix.
  tools/migrate_full_mix_stem.py rewrites those packs into the spec shape
  (original/full.ogg -> stems/full.ogg, add the `full` stem at default:off, drop
  the key); the fallback and the aliases die with #945.

The spec gate keeps the debt honest: the grandfather entry now tracks #945, and
the gate fails if it goes stale.

Verified: spec gate OK (4/4, incl. ingesting the spec's new example pack that
retains `full`); 2493 python tests, 995 js tests; migrator round-tripped over
real packs from the library and the results pass the spec's reference validator.

* fix(migrate): discover directory-form packs instead of silently skipping them

iter_packs() searched only files, so a directory-form pack (`song.sloppak/`, the
authoring shape) was walked INTO and never yielded — silently missed by a run
that's meant to be exhaustive. Discover suffix-named directories too (yielded
whole, not descended into), and route packs through migrate_pack/verify_pack.

Directory packs are REPORTED as `dir-form-unsupported`, not rewritten in place:
a single-file pack is replaced atomically (a fully-built temp archive swapped in
with one os.replace), but a populated directory can't be swapped that way, so an
interrupted in-place rewrite could leave an authoring pack half-migrated. The
status is a problem status, so it counts against the run's exit code and shows
in the summary — the operator re-packs or migrates it as a `.feedpak` instead of
it vanishing from the report. Addresses a CodeRabbit review finding.

Signed-off-by: Kris Anderson <topkoa@gmail.com>

* fix(migrate): verify requires an explicit `off` on a retained full mix

verify_zip accepted any non-truthy `default` on a multi-stem `full` (missing,
empty, boolean, `false`/`no`/`0`, malformed) as "ok". But core defaults an ABSENT
`default` to True — ON (lib/sloppak.py: `s.get("default", True)`) — and treats an
empty/unrecognized string as ON too, so a migrated-shape pack whose `full` stem
has a missing or blank default beside instrument stems would actually play the
mixdown on open and double the song. verify was certifying that as safe.

Require an explicit normalized `off` beside instrument stems: `on`-ish values are
reported `full-stem-default-on` (actively plays), everything that is not a
normalized `off` is reported `full-stem-default-not-off`. The migrator already
writes the literal `off`, so its own output is unaffected; this also certifies
the pack is in the tool's canonical, most-portable shape. The len>1 gate is kept,
so a sole `full` stem (which IS the audio) is not policed.

Adds parametrized coverage for missing / empty / boolean / off-ish / malformed
defaults, and a sole-full-stem case. Addresses a CodeRabbit review finding.

Signed-off-by: Kris Anderson <topkoa@gmail.com>

---------

Signed-off-by: Kris Anderson <topkoa@gmail.com>
Co-authored-by: Kris Anderson <topkoa@gmail.com>
2026-07-13 12:22:42 -04: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 refactor(server): extract the song routes into routers/song.py (R3) (#864) 2026-07-11 13:14:52 +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