Skip to content

fix(mobile): don't offer share-to-story for a track with no audio - #14584

Merged
dylanjeffers merged 1 commit into
mainfrom
fix/no-audio-share-to-story
Sep 9, 2026
Merged

dylanjeffers merged 1 commit into
mainfrom
fix/no-audio-share-to-story

Conversation

@dylanjeffers

Copy link
Copy Markdown
Contributor

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_cid is 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, and handleError shows 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: false for these rows, so isShareableTrack now excludes them — better than failing 60% of the way through a progress drawer. The check is an explicit !== false because not every track source populates the field, and an absent one must not hide the share options.

Guard the stream-URL step. signGatedContentRequest and getTrackStreamUrl were wrapped in nothing at all, and handleShare doesn'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 through handleError like every other step, and the analytics error carries Error at resolve stream url step so the failing step is identifiable.

The two comment updates in packages/common bring the is_streamable docs in line with its widened meaning — it now also covers an upload indexed without its track_cid, not just deleted/deactivated.

Worth knowing

isTrackUnavailable reads 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

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

changeset-bot Bot commented Sep 3, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 8431c0a

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 2e65a95 into main Sep 9, 2026
17 checks passed
@dylanjeffers
dylanjeffers deleted the fix/no-audio-share-to-story branch September 9, 2026 18:40
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>
dylanjeffers added a commit to AudiusProject/api that referenced this pull request Sep 24, 2026
…job (#1032)

## What happened

[Michael
reported](https://audius-internal.slack.com/archives/CA80RCL77/p1788456292567669)
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_cid` is null on the indexed row, so the
stream endpoint 404s:

```
curl -sL "https://api.audius.co/v1/tracks/70YW9Og/stream?app_name=test"
{"code":404,"error":"track audio is unavailable"}
```

Mobile's 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. ffmpeg gets
a 404 JSON body instead of an mp3, exits non-zero, and the user gets the
generic toast.

Storage did its job — the mediorum upload record for this track is
`status: done`, `transcode_progress: 1`, with a valid 320kbps cid. The
track entity was written 89 seconds later without that cid, and nothing
backfills it server-side.

The worse part is that the track is completely unplayable: **0 plays
against 18 favorites and 16 reposts** since Aug 31. The artist has
exactly one track and it's dead, and nothing in the product tells them,
because `is_streamable` still returns `true`.

## What this changes

**`is_streamable` stops lying.** The stream link was already left nil
for cidless rows and `/stream` already 404s — only the flag disagreed.
Split in two: `IsAudioAllowed` keeps the old meaning (not deleted, owner
still active) and gates downloads and previews; `IsStreamable` now also
requires a cid to stream.

Downloads deliberately stay on `IsAudioAllowed`. A download falls back
to `orig_file_cid`, which a row missing its `track_cid` still has, so
losing `is_streamable` must not cost the artist their downloads.
`v1_track_download.go` switched to the new guard for that reason; the
playlist m3u8 builder correctly stays on `IsStreamable`, since it needs
a stream URL.

**A repair job for the rows already in this state.**
`jobs/repair_track_cids.go` is modeled on the existing
`RepairAudioAnalysesJob`: find current, undeleted tracks with a NULL
`track_cid` and an `audio_upload_id`, ask content nodes for the upload
record, write `results["320"]`. Runs every 15 minutes.

One deliberate departure from the job it copies: **two distinct nodes
must agree on the cid before it is written.** bpm fills in a display
field; `track_cid` decides which bytes every listener receives for the
track. Upload records are replicated across mirrors, so agreement is
cheap to obtain, and it means a single stale or misbehaving node cannot
repoint a track's audio on its own. Tracks that can't reach quorum, or
whose nodes disagree, are logged and left for the next pass. The write
also re-checks `track_cid IS NULL`, so a real indexer write always wins
a race.

## Worth knowing before merging

This widens what `is_streamable: false` means, and the clients already
act on it — `isTrackUnavailable` feeds the web and mobile track pages,
so cidless tracks will render the "no longer available" screen instead
of a dead player. I think that's the right call, but it's a visible
change beyond the reported bug. For repairable tracks it's transient
until the job runs.

This limits the damage; it does not stop it happening again. The origin
— why the upload client wrote the track entity 89s after transcode
finished without attaching the cid — is client-side and not addressed
here.

## Testing

`./api/...`, `./jobs/` and `./indexer/...` all green. New coverage: a
cidless track reports `is_streamable: false` with a null stream, the
same track is still downloadable via `orig_file_cid`, quorum repairs, a
single node cannot repair, disagreeing nodes leave the row alone,
unreachable nodes don't block a repair the reachable ones agree on, and
`applyTrackCid` never overwrites an existing cid.

## Related

- AudiusProject/apps#14584 stops mobile offering the story option for
these tracks.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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