Skip to content

fix(client): download stems individually instead of server-side zip - #14563

Open
dylanjeffers wants to merge 1 commit into
mainfrom
fix/stems-individual-downloads
Open

fix(client): download stems individually instead of server-side zip#14563
dylanjeffers wants to merge 1 commit into
mainfrom
fix/stems-individual-downloads

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

Closes the "downloading stems is stuck on the loading wheel" report in #eng-general, and implements Ray's follow-up ask in that thread: drop bulk download rather than keep stabilizing the archiver.

Problem

"Download All" never downloaded anything client-side. It queued a job with the archiver service, which zipped the stems server-side while the client polled getStemsArchiveJobStatus once per second for up to 15 minutes. When no BullMQ worker slot was free the job sat in waiting forever and the user watched a spinner that never resolved — exactly the {"id":"…","state":"waiting","progress":0} a Discord mod pasted into the thread during Summer Cypher Vol 2.

That queue has been patched repeatedly (pedalboard#88, #89). Ray's call was to stop:

can we disable the bulk download feature? i really don't think it's worth how unstable it is. this has been ongoing since the beginning. […] If we need to proxy the downloads through api.audius.co to make that work with one approval, that seems fine.

Approach

Hand each file to the browser as its own download, the way ChatGPT's "All 4 images in this series" works. Chrome gates a burst of programmatic downloads behind a single "Download multiple files?" prompt, so the batch still costs one approval — and there is no job to stall, nothing buffered in the tab, and each file gets the browser's own progress and resume handling.

The proxy Ray offered to build already exists. /v1/tracks/:id/download redirects to a content node that sets Content-Disposition from the filename query param, verified against production:

$ curl -sIL "https://api.audius.co/v1/tracks/<id>/download?filename=mackina.%20-%20fake%20love.%20%28JLYELI%20FLIP%29.wav"
HTTP/2 302
location: https://val010.open-audio-validator.com/tracks/cidstream/baeaaa…
HTTP/2 200
content-disposition: attachment; filename="mackina. - fake love. (JLYELI FLIP).wav"

Because the saved filename now comes from that header rather than from link.download at click time, the download saga signs its computed name into the URL, and deduping moves ahead of URL construction — a duplicate left unresolved would otherwise land as name (1).wav.

Changes

  • webTrackDownload.downloadTracks triggers one anchor click per file instead of fetch-ing everything into memory and zipping with client-zip.
  • web — "Download All" on the track page, mobile-web, and the contest card now calls the same path as a per-row download, passing parentTrackId. That is what appends the full track to the batch, and it already drops the parent when it isn't downloadable.
  • mobile — no browser to hand a batch to, so Download All returns to the pre-archiver path: fetch each file, zip on-device via react-native-zip-archive. Same saga, no queue.
  • removed — SDK ArchiverService, DownloadTrackArchiveModal / Drawer, the polling hooks, the modal slice, and ARCHIVE_ENDPOINT.

Testing

  • tsc --noEmit clean for web (0 errors); sdk, common, and mobile clean in-tree.
  • eslint clean across all changed files; fileUtil tests (covering dedupFilenames) pass.
  • The production curl above confirms the Content-Disposition behavior the change depends on.

One caveat on local verification: the worktree had no node_modules, so I symlinked them from another checkout that sits on an older branch. A handful of residual errors in common/mobile all resolved to paths outside the worktree and are artifacts of that setup — CI on a clean install is the real confirmation.

Notes for review

  • Keeps the existing "Download All" button rather than adding Ray's literal two-item menu. The track page already exposes both per-row and all-files affordances, so the menu seemed redundant — happy to add it if we want to match the screenshot exactly.
  • The archiver service is untouched and still deployed. This only removes the clients, so it can be decommissioned separately once this ships.
  • The one-time "Download multiple files?" prompt is a deliberate, visible tradeoff versus the single zip file users get today.

🤖 Generated with Claude Code

"Download All" queued a job with the archiver service, which zipped the
stems server-side while the client polled for up to 15 minutes. Jobs
routinely sat in `waiting` with no worker, so users watched a spinner
that never resolved — the failure Michael hit on Summer Cypher Vol 2.

Hand each file to the browser as its own download instead. Chrome
gates a burst of programmatic downloads behind a single "Download
multiple files?" prompt, so the batch still costs one approval, and
there's no job to stall, nothing buffered in the tab, and each file
gets the browser's own progress and resume handling.

Filenames come from the server: `/v1/tracks/:id/download` redirects to
a content node that sets `Content-Disposition: attachment` from the
`filename` query param, so the saga now signs the name it already
computes into the URL. That means deduping has to happen before the
URLs are built rather than at click time.

Native has no browser to hand a batch to, so mobile's Download All
goes back to fetching and zipping on-device — same saga, no queue.

Removes the archiver client entirely: the SDK service, the
modal/drawer, the polling hooks, and ARCHIVE_ENDPOINT.
@changeset-bot

changeset-bot Bot commented Aug 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 07ba97a

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

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