41bd89d060
* feat(export): prefix exported stems with the song title 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 * fix(export): prefix the MP3 region stem download too 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. * fix(export): name the mixdown after the song too 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.