fix(stems): repair contest stem downloads and stem add/remove on edit#14535
Merged
Conversation
Two unrelated defects, both surfaced by the Andrew Lux "Alone" contest
track (48 lossless stems, ~2.5GB, parent not downloadable).
Downloads failing from the contest page:
- ContestStemsCard passed `parentTrackId` on every per-row download. The
download saga appends the parent to the file list, so each single-stem
download also fetched the full track. "Alone" has `is_downloadable:
false`, so that request 404s — and the batch was all-or-nothing, which
failed every stem download on the page. The track page was unaffected
because DownloadRow has always passed only the row's own id.
- track-download now downloads the files it can reach and only reports a
failure when nothing at all could be fetched, logging what it skipped.
- `includeParent` for the stems archive keyed off `access.download`,
which is the gating check ("not gated") and is true even when no
downloadable file exists. It now also requires `is_downloadable`.
- The archive poll's hard stop returned false from `refetchInterval`,
which stops polling without re-rendering and leaves the job reported
as `active` — so the modal spun forever with no error. The timeout is
now real state surfaced as `isTimedOut`, and the budget goes from 5 to
15 minutes so multi-GB archives aren't failed while still progressing.
Stem removals silently discarded:
- handleStemUpdates read `currentTrack._stems` to decide what had been
removed. Nothing in the repo ever wrote `_stems`, so the existing-stem
list was always empty and no removal was ever detected: the artist
removed a stem, saved successfully, and it was still there on reload.
Because same-name files are rejected while the old stem remains, this
also blocked re-uploading a stem at a smaller size.
- The existing stems now come from the stems query and are passed in
explicitly. EditTrackPage waits for that query before initializing the
form, so a half-loaded form can't submit an empty list and delete
everything.
- Dropped `_stems` from the model and pointed its readers at the stems
query via usePurchaseableStemCount. Those were the purchase modals,
which had been reporting zero stems on every gated purchase.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two unrelated bugs reported against the Andrew Lux contest track (
/Andrewluxmusic/contest/alone): participants couldn't download stems, and the artist couldn't remove or replace them. Both are client-side defects, not product limitations and not caused by the artist's file sizes.The track is a useful stress case: 48 lossless stems, ~51.7 MB each, ~2.48 GB total, and the parent track has
is_downloadable: false.Bug 1 — stem downloads failed from the contest page
Per-stem downloads (the one users hit).
ContestStemsCardpassedparentTrackIdon every per-row download. The download saga expands that into[...trackIds, parentTrackId], so clicking a single stem also fetched the full track. Verified against the live API:track-download.tsthen failed the whole batch on any non-ok response, so every per-stem download on the contest page died on the parent's 404 and showed the misleading "check your connection and storage" error. The track page was never affected —DownloadRowhas always passed only the row's own id.Download All.
includeParentwas computed fromaccess.download, which is the gating check ("this user may download") and istruefor any ungated track regardless of whether a downloadable file exists. For this trackaccess.downloadistruewhileis_downloadableisfalse, so the archiver was asked to bundle a file that 404s.The archive modal spun forever. The 5-minute poll cap returned
falsefromrefetchInterval, which stops polling without re-rendering and leaves the job reported asactive— sohasErrorstayedfalseand the modal showed an endless spinner with no error and no retry.Changes
ContestStemsCardno longer passesparentTrackIdfor a single-stem download, matchingDownloadRow.track-download.tsdownloads what it can reach, logs what it skipped and why, and only reports failure when nothing at all could be fetched. Abort handling preserved.includeParentnow requiresis_downloadable === trueandaccess.download === true.isTimedOutand wired into the modal's error path. Budget raised 5 → 15 min so multi-GB archives aren't failed while still making progress.Bug 2 — stem removals were silently discarded
handleStemUpdatesreadcurrentTrack._stemsto work out which stems had been removed. Nothing in the repo has ever written_stems— it's a leftover from before stems moved to theuseStemsquery, with 5 readers and zero writers. So the existing-stem list was always[], no removal was ever detected, anddeleteTrackwas never called. The artist removed a stem, got a successful save, and found it still there on reload.That also blocked his actual goal.
StemFilesViewrejects a dropped file whose name matches an existing stem'sorig_filename, so re-uploading the same stems at a smaller size was refused while the originals remained — and they could never be removed. Adds and removes deadlocked each other.Changes
handleStemUpdatestakesexistingStemsandtrackIdexplicitly;useUpdateTracksupplies the stems fromgetStemsQueryKey. An empty list still means "no removals detected", so a cache miss can't cause deletions.EditTrackPagewaits for the stems query before initializing the form. Without this the fix would be dangerous: the form previously rendered while stems were still loading, and submitting it would now read as "remove everything." This was a latent data-loss path masked only by removals never working._stemsfrom the model and pointed its readers at the stems query via a newusePurchaseableStemCount. Those readers were the web and mobile purchase modals, which had been itemizing 0 stems on every download-gated purchase.Testing
handleStemUpdates(7 cases): removal detection, empty submitted list, partial updates that don't touch stems, unknown existing-stem list, in-flight uploads excluded from deletion, adds not re-uploading existing stems, and the artist's same-name replace-with-smaller-file workflow.tsc --noEmitclean oncommon,web, andmobile; eslint clean on all changed files; existing contest-page tests pass.access.download/is_downloadabledivergence above) rather than by exercising the UI. Worth a manual pass on a contest track before release.Follow-ups (not in this PR)
include_parentfor a non-downloadable parent. The client no longer asks for it, but the archiver's own behaviour is unverified — worth checking job logs for this track.downloadZip(...).blob()buffers the entire archive in memory, so a client-side "download all" of ~2.5 GB will still OOM. Only the server-side archiver path is viable at that size.invalidAudioFileinStemsAndDownloadsMenuFieldsisconsole.error+// TODO: show file error, so oversized or wrong-type stems vanish from the dropzone with no feedback.ContestStemsCardhas no follow-gate guard on its download buttons whereDownloadRowdisables them. Not triggered here (this track is ungated) but it will produce the same misleading error on a gated contest track.What to tell the artist
🤖 Generated with Claude Code