Skip to content

[Mobile] Fix offline album track downloads after v1 SDK migration#14275

Merged
dylanjeffers merged 1 commit intomainfrom
claude/thirsty-poitras-f2fcc7
May 7, 2026
Merged

[Mobile] Fix offline album track downloads after v1 SDK migration#14275
dylanjeffers merged 1 commit intomainfrom
claude/thirsty-poitras-f2fcc7

Conversation

@dylanjeffers
Copy link
Copy Markdown
Contributor

@dylanjeffers dylanjeffers commented May 7, 2026

Summary

Track downloads were failing in offline mode — the user-visible symptom is "clicks download, sees a track attempt, then a failure icon." This affects both individual track downloads (favoriting a track with auto-download on, or any other path that queues a single track) and tracks inside an album/collection download, because every track-job entry point lands in the same queue and is processed by the same worker.

Two compounding issues in downloadTrackWorker.ts, both effectively introduced by the v1 SDK migration (#13728):

  1. downloadTrackAudio skipped the v1 pre-signed stream URL. It built the URL via sdk.tracks.getTrackStreamUrl(), ignoring track.stream.url. The v1 API returns a pre-signed stream URL (with mirrors) on the track itself; AudioPlayer prefers it for online streaming and only falls back to manual URL construction when stream.url is missing. The offline worker skipped that working path entirely, so it relied on the fallback for every track.

  2. downloadTrackCoverArt was fatal. It threw when no artwork URI returned 200, and because cover art runs in all([cover, audio, json]), a single flaky artwork host failed the entire track download — even though audio is the only essential payload. The collection cover art download follows the opposite (best-effort) pattern; this PR brings the track version in line.

Plus downloadTrackAsync's catch block silently swallowed the error — no logs, no Sentry breadcrumb. That's why this had been hard to diagnose.

Why this fixes both individual tracks and collections

Every track-job source — requestDownloadCollectionSaga, requestDownloadFavoritedCollectionSaga, requestDownloadAllFavoritesSaga, watchSaveTrackSaga (standalone track favorite), watchAddTrackToPlaylistSaga, syncCollectionWorker — dispatches addOfflineEntries({ items: [..., { type: 'track' }, ...] }). The queue processor routes every type === 'track' job to a single downloadTrackWorker, which calls the two functions patched here. So both flows share the same broken code path.

Changes

  • downloadTrackAudio: prefer track.stream.url (with mirror substitution), keep getTrackStreamUrl() + manual signing as a last-resort fallback. Mirrors AudioPlayer.tsx logic.
  • downloadTrackCoverArt: best-effort — return silently if all URIs fail rather than throwing.
  • Extract shared buildMirrorUris(primary, mirrors) helper used by both audio and cover art.
  • Add a console.warn in the catch block of downloadTrackAsync so future failures are diagnosable instead of silent.

Test plan

  • Offline mode → favorite a single track with "download all favorites" enabled. Track reaches SUCCESS, plays from local file.
  • Offline mode → download an album you don't own (where you've favorited it). All tracks reach SUCCESS, audio plays from local file.
  • Offline mode → download an album where the artwork host is degraded. Audio still downloads successfully; cover art is best-effort.
  • Offline mode → download a single track via the track menu. Still works (no regression).
  • Offline mode → toggle "Download all favorites." Track downloads succeed.
  • Offline mode → play a downloaded track while offline. File-system playback still works.
  • Gated content (USDC / NFT) → confirm the manual-URL fallback path still signs correctly when stream.url is unset.

🤖 Generated with Claude Code

Track downloads inside an album/collection were failing with a generic
error icon since the v1 SDK migration. Two issues in
downloadTrackWorker.ts compounded:

1. downloadTrackAudio() built the stream URL via getTrackStreamUrl(),
   ignoring track.stream.url. The v1 API returns a pre-signed stream
   URL (with mirrors) on the track itself; AudioPlayer prefers it for
   online streaming and only falls back to manual URL construction when
   stream.url is missing. The offline worker skipped that working path,
   so it relied on the fallback for every track.

2. downloadTrackCoverArt() throws when no artwork URI returns 200.
   Because cover art runs in `all([cover, audio, json])`, a single
   flaky artwork host fails the entire track download — even though
   audio is the only essential payload. The collection cover art
   download follows the opposite (best-effort) pattern.

Fix both: prefer track.stream.url with mirror fallback (matches the
AudioPlayer logic), keep the manual URL construction as a last resort,
and make cover art best-effort. Also log the underlying error instead
of swallowing it silently in the catch block, so future regressions
are diagnosable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 7, 2026

⚠️ No Changeset found

Latest commit: c6da07b

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 3e40dec into main May 7, 2026
3 checks passed
@dylanjeffers dylanjeffers deleted the claude/thirsty-poitras-f2fcc7 branch May 7, 2026 23:50
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