fix(mobile): don't offer share-to-story for a track with no audio - #14584
Merged
Conversation
Share-to-story builds a video out of the track's audio: it resolves the stream URL and hands it to ffmpeg as an input. When the track has nothing to play - an upload indexed without its track_cid - that URL 404s, ffmpeg exits non-zero, and the user gets a bare "Sorry, something went wrong." with no hint that the track itself is broken. That is the bug Michael hit; every other link he tried worked because those tracks had audio. The API now reports is_streamable=false for these, so stop offering the story platforms at all rather than failing halfway through. The check is an explicit `!== false` because not every track source populates the field, and an absent one must not hide the share options. Also guard the stream-URL step itself. Nothing wrapped it, so a rejection there - a failed signature, an SDK that never initialized - escaped as an unhandled promise rejection: no toast at all, and the progress drawer left spinning with no way out but backing out of it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
dylanjeffers
added a commit
that referenced
this pull request
Sep 9, 2026
## The root cause behind the dead-track incident [Michael reported](https://audius-internal.slack.com/archives/CA80RCL77/p1788456292567669) that one link failed when sharing to an Instagram story. The track behind it has no audio at all: `track_cid` is null on the indexed row, `/stream` 404s, and share-to-story hands that URL to ffmpeg, which fails. AudiusProject/api#1032 makes the API honest about it and repairs the rows; #14584 stops mobile offering a story for a track it cannot build a video from. **This PR is why the rows exist in the first place.** `pollProcessingStatus` returned the moment a storage node reported `status: 'done'`: ```ts if (resp?.status === 'done') { return resp } ``` It never checked that the result it is polling *for* is on the response. Upload rows replicate across storage nodes and `getProcessingStatus` talks to whichever node `storageNodeSelector` hands back — falling over to others on error — so a mirror can legitimately answer `done` from a row it has not finished catching up on, with an empty `results` map. `populateTrackMetadataWithUploadResponseV2` then does: ```ts trackCid: audioResponse.results['320'], ``` which is `undefined`, and the track entity is written without a cid. Nothing errors. The upload reports success, the track page loads, the artwork renders, people favorite and repost it — and there is no cid on the row pointing at the audio, so it can never be played and never records a play. That matches the failing track exactly: `duration`, `bpm`, `musical_key`, `orig_file_cid` and `audio_upload_id` were all populated from the upload response, so the response was there and carried `probe` and `audio_analysis_results` — only `results['320']` was missing. The result: **0 plays against 18 favorites and 16 reposts.** ## The change Require the `'320'` result before treating an audio poll as finished. A node that really is done will have it on the next pass three seconds later. An upload genuinely stuck in that state now times out with an error naming the cause — `Upload reported done but no transcode result appeared within...` — instead of silently publishing unplayable audio, which is a strictly better failure: the artist finds out at upload time rather than never. Image templates are untouched; they have no `'320'` to wait for. ## Testing `Storage.test.ts`: a node reporting done with no results keeps polling and picks up the cid on the retry; a response that already has the cid returns on the first call with no extra poll; image templates return immediately on an empty results map. All 5 tests in the file pass. Not reproduced against a live node — the race needs replication lag between storage nodes to trigger. ## Related - AudiusProject/api#1032 — `is_streamable` honesty plus a repair job for rows already in this state - #14584 — mobile stops offering share-to-story for tracks with no audio 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <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.
What happened
Michael reported that one specific link failed with "Sorry, something went wrong" when sharing to an Instagram story, while every other link worked.
That track has no audio.
track_cidis null on the indexed row, so the stream endpoint 404s. Share-to-story builds a video out of the track's audio — it resolves the stream URL and passes it to ffmpeg as an input (-i ${streamMp3Url}). ffmpeg gets a 404 JSON body instead of an mp3, exits non-zero, andhandleErrorshows the generic toast with no hint that the track itself is broken. Every other link he tried worked because those tracks had audio.What this changes
Don't offer the story platforms for a track with nothing to play. AudiusProject/api#1032 makes the API report
is_streamable: falsefor these rows, soisShareableTracknow excludes them — better than failing 60% of the way through a progress drawer. The check is an explicit!== falsebecause not every track source populates the field, and an absent one must not hide the share options.Guard the stream-URL step.
signGatedContentRequestandgetTrackStreamUrlwere wrapped in nothing at all, andhandleSharedoesn't catch either — so a rejection there (a failed signature, an SDK that never initialized) escaped as an unhandled promise rejection: no toast, and the progress drawer left spinning with no way out but backing out of it. Now it goes throughhandleErrorlike every other step, and the analytics error carriesError at resolve stream url stepso the failing step is identifiable.The two comment updates in
packages/commonbring theis_streamabledocs in line with its widened meaning — it now also covers an upload indexed without itstrack_cid, not just deleted/deactivated.Worth knowing
isTrackUnavailablereads the same flag, so once the API change lands, these tracks will render the "no longer available" screen on the track page instead of a dead player. That's intended, and transient for tracks the repair job in AudiusProject/api#1032 can fix.Mobile ships via OTA and iOS OTA has been broken since the 1.5.186 build failed, so this is likely to reach iOS well after the server-side fixes land. The repair job closes the loop regardless of client version.
Testing
Lint clean on all four files; typecheck clean on the changed files. Not exercised on a device — the share-to-story path needs a real Instagram hand-off to test end to end.
🤖 Generated with Claude Code