Skip to content

feat(export): name every export after its song - #340

Merged
thcp merged 3 commits into
mainfrom
feat/stem-exports-song-prefix
Aug 9, 2026
Merged

thcp merged 3 commits into
mainfrom
feat/stem-exports-song-prefix

Conversation

@thcp

@thcp thcp commented Aug 9, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #336.

Problem

Stems export as bass.wav, vocals.wav, drums.wav. As the reporter put it, that makes them unusable for file management: drop two songs' stems into one project folder and they are indistinguishable, and identically-named files overwrite each other.

Tracing it turned up the same defect in the mix export, which downloaded as mixdown.wav for every song.

What changed

Every file the user exports is now named after its song:

05_Welcome_To_Paradise_Green_Day_Dookie_bass.wav
05_Welcome_To_Paradise_Green_Day_Dookie_vocals.wav
05_Welcome_To_Paradise_Green_Day_Dookie_exported_mix.wav

Covering every path that hands a file to the user:

Path Before After
ZIP members bass.wav <Song>_bass.wav
Single stem bass.wav <Song>_bass.wav
Single stem, region bass_region.wav <Song>_bass_region.wav
Stem MP3 bass.mp3 <Song>_bass.mp3
Stem MP3, region bass_region.mp3 <Song>_bass_region.mp3
Mixdown mixdown.wav <Song>_exported_mix.wav
Mixdown, region mixdown.wav <Song>_region.wav
Video already correct unchanged

Two things worth reviewing

The server has to carry the name. Setting the <a download> attribute alone did nothing: Content-Disposition wins over it for same-origin requests, and the routes were already sending filename="bass.wav" / filename="mixdown.wav". This was caught in testing, when a correct download attribute still produced a bass.wav download. The attribute is set as well, as the fallback for any response without the header.

This is also why the mix bug survived unnoticed: desktop users never saw it, because save_audio_file uses the frontend's filename rather than the header.

The desktop per-stem download did not download. Clicking the stem download icon routed through open_url, which spawns the OS handler, so the stem opened in a browser or media player and was never saved. No filename applied at all, meaning this feature would have done nothing for per-stem downloads on the reporter's platform. It now goes through save_audio_file like every other export, giving a real Save dialog with the prefixed name.

Naming

_ and lowercase, matching what the app already produced (Song_exported_mix.wav, Song_region.wav, Song_stems.zip). The reporter asked for [TrackName]-Bass; the prefix is the substance of the request and house consistency won on the separator.

_safe_title is split in two:

  • _title_slug returns "" for a title that sanitizes to nothing
  • _safe_title keeps the "stems" fallback for the whole-archive filename

A per-file prefix must be droppable, otherwise an untitled job produces _bass.wav on every member.

Verification

Nine new tests in tests/test_stems_api.py:

  • ZIP members carry the prefix and the payload is unchanged
  • Untitled and punctuation-only titles fall back to bare names, no leading underscore
  • Single-stem, stem-region, stem-MP3-region and untitled Content-Disposition names
  • Mixdown, mixdown-region and untitled mixdown names
  • Member names cannot escape on extraction: a title of ../../etc/passwd yields etc_passwd_vocals.wav, and no member contains a separator or ..

Manual, against a real job with real audio:

$ curl .../stems/all.zip?stems=vocals,bass
   05_Welcome_To_Paradise_Green_Day_Dookie_vocals.wav  39634992 bytes
   05_Welcome_To_Paradise_Green_Day_Dookie_bass.wav    39634992 bytes

$ curl -I .../stems/bass.wav
content-disposition: attachment; filename="05_Welcome_To_Paradise_Green_Day_Dookie_bass.wav"

Playwright, driving the real controls in both host modes:

per-stem, browser : 05_Welcome_To_Paradise_Green_Day_Dookie_bass.wav
per-stem, tauri   : {"cmd":"save_audio_file",
                     "filename":"05_Welcome_To_Paradise_Green_Day_Dookie_bass.wav"}
Export Mix, browser: 05_Welcome_To_Paradise_Green_Day_Dookie_exported_mix.wav   (was mixdown.wav)

398 passed on the full suite. One pre-existing failure, test_all_stems_zip_ogg, fails identically on main because the local ffmpeg has no libvorbis encoder; it passes in CI. ruff check, ruff format --check and node --check all clean.

Scope

app/api/stems.py, static/js/player.js, static/js/main.js, tests/test_stems_api.py. No Rust changes; save_audio_file already did the right thing and simply was not being called.

thcp added 3 commits August 9, 2026 08:21
Stems exported as "bass.wav" or "vocals.wav" are ambiguous the moment they
leave the app. Dropping several songs' stems into one project folder makes
them indistinguishable and they overwrite each other.

Every stem the user receives is now named "<Song>_<stem>.<ext>":

- ZIP members, via a new prefix argument to _build_stems_zip
- Single-stem downloads, via the Content-Disposition filename
- Single-stem region trims, which keep both the song and the _region marker
- The MP3 variant of a stem

The server carries the name because Content-Disposition wins over an
<a download> attribute for same-origin requests, so setting the attribute
alone had no effect. The attribute is set too, as the fallback for any
response that does not send the header.

_safe_title is split into _title_slug, which returns "" for a title that
sanitizes to nothing, and _safe_title, which keeps the "stems" fallback for
the whole-archive filename. A per-file prefix has to be droppable, otherwise
an untitled job yields a leading underscore on every member. The slug is
restricted to [A-Za-z0-9_], so it stays safe as a ZIP member name.

Also fixes the desktop per-stem download, which routed through open_url and
handed the file to the OS handler: the stem opened in a browser or media
player and was never saved, so no filename applied at all. It now goes
through save_audio_file like every other export.

Closes #336
Missed in the previous commit: the trimmed-region branch of the MP3 stem
route still built a bare "{name}_region.mp3", so it was the one stem file
the user could receive without the song prefix.

Its ternary also had an unreachable branch. Only the trimmed case reaches
that line; the untrimmed one returns from the cached-file branch above.
The mixdown endpoint hardcoded filename="mixdown.{ext}", so every song's
mix and every region export downloaded as "mixdown.wav". Exporting a few
songs into one folder produced mixdown.wav, mixdown(1).wav, mixdown(2).wav
-- the same collision #336 reports for stems.

Content-Disposition overrides the <a download> attribute, so the name the
frontend already built was discarded. Only desktop escaped it, because
save_audio_file uses the frontend's name rather than the header.

The video export was already doing this correctly, which left the mixdown
as the only export not named after its song.

Names mirror the frontend's: <Song>_exported_mix.<ext>, and <Song>_region.<ext>
when start/end trim to a loop region.
@thcp thcp changed the title feat(export): prefix exported stems with the song title feat(export): name every export after its song Aug 9, 2026
@thcp
thcp marked this pull request as ready for review August 9, 2026 07:42
@thcp
thcp merged commit 41bd89d into main Aug 9, 2026
8 checks passed
@thcp
thcp deleted the feat/stem-exports-song-prefix branch August 9, 2026 07:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Exported Stems have their track name as a prefix

1 participant