Skip to content

fix(stems): repair contest stem downloads and stem add/remove on edit#14535

Merged
dylanjeffers merged 1 commit into
mainfrom
fix/contest-stems-download-and-edit
Jul 27, 2026
Merged

fix(stems): repair contest stem downloads and stem add/remove on edit#14535
dylanjeffers merged 1 commit into
mainfrom
fix/contest-stems-download-and-edit

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

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). ContestStemsCard passed parentTrackId on 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:

GET /v1/tracks/JBKl7R6/download   →  404   (parent, is_downloadable: false)
GET /v1/tracks/rq1YRQ/download    →  206   (a stem, fine)

track-download.ts then 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 — DownloadRow has always passed only the row's own id.

Download All. includeParent was computed from access.download, which is the gating check ("this user may download") and is true for any ungated track regardless of whether a downloadable file exists. For this track access.download is true while is_downloadable is false, so the archiver was asked to bundle a file that 404s.

The archive modal spun forever. The 5-minute poll cap returned false from refetchInterval, which stops polling without re-rendering and leaves the job reported as active — so hasError stayed false and the modal showed an endless spinner with no error and no retry.

Changes

  • ContestStemsCard no longer passes parentTrackId for a single-stem download, matching DownloadRow.
  • track-download.ts downloads what it can reach, logs what it skipped and why, and only reports failure when nothing at all could be fetched. Abort handling preserved.
  • includeParent now requires is_downloadable === true and access.download === true.
  • The poll timeout is real state surfaced as isTimedOut and 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

handleStemUpdates read currentTrack._stems to work out which stems had been removed. Nothing in the repo has ever written _stems — it's a leftover from before stems moved to the useStems query, with 5 readers and zero writers. So the existing-stem list was always [], no removal was ever detected, and deleteTrack was never called. The artist removed a stem, got a successful save, and found it still there on reload.

That also blocked his actual goal. StemFilesView rejects a dropped file whose name matches an existing stem's orig_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

  • handleStemUpdates takes existingStems and trackId explicitly; useUpdateTrack supplies the stems from getStemsQueryKey. An empty list still means "no removals detected", so a cache miss can't cause deletions.
  • EditTrackPage waits 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.
  • Dropped _stems from the model and pointed its readers at the stems query via a new usePurchaseableStemCount. Those readers were the web and mobile purchase modals, which had been itemizing 0 stems on every download-gated purchase.

Testing

  • New unit tests for 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 --noEmit clean on common, web, and mobile; eslint clean on all changed files; existing contest-page tests pass.
  • Not verified at runtime. Reproducing the failures needs an authenticated session and multi-GB transfers. The root causes were confirmed against the live API (the 404 and the access.download / is_downloadable divergence above) rather than by exercising the UI. Worth a manual pass on a contest track before release.

Follow-ups (not in this PR)

  • Server-side: confirm how the archiver handles include_parent for 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.
  • invalidAudioFile in StemsAndDownloadsMenuFields is console.error + // TODO: show file error, so oversized or wrong-type stems vanish from the dropzone with no feedback.
  • ContestStemsCard has no follow-gate guard on its download buttons where DownloadRow disables 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

  • Stem downloads from the contest page were broken by a client-side bug in that page's download button — not his file sizes, and nothing he did wrong. Downloads from the regular track page should work today, which is an immediate workaround for contest participants.
  • "Download All" is separately unreliable at 2.48 GB and will likely hang until this ships; per-stem downloads from the track page are the reliable path meanwhile.
  • Stems are meant to be removable. His removals were being silently dropped on save by a dead field reference. Until this ships he can't self-serve it, so clearing the 48 existing stems needs a manual pass on our end — and same-name re-uploads will keep being rejected while the originals are still there.

🤖 Generated with Claude Code

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>
@changeset-bot

changeset-bot Bot commented Jul 27, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: ac52010

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@dylanjeffers
dylanjeffers merged commit ef46488 into main Jul 27, 2026
17 checks passed
@dylanjeffers
dylanjeffers deleted the fix/contest-stems-download-and-edit branch July 27, 2026 02:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant