From 04609d518665608483201a45df020f56808724c6 Mon Sep 17 00:00:00 2001 From: Dylan Audius Date: Thu, 20 Aug 2026 11:59:47 -0700 Subject: [PATCH] fix(embed): honor is_streamable so inactive artists' tracks don't render apps#14570 gated the web and mobile track pages on `is_streamable`, but the embed player is its own app and was left rendering the full card - title, artist, artwork, play button - for a track whose owner deactivated their own account or was delisted by the trusted notifier. api#1023 made the stream endpoint 404, so the player was already unable to play these; it just showed the metadata and then failed silently on press. Route non-streamable tracks into the existing not-available treatment. The copy is its own message rather than reusing the deleted-by-creator string: the same flag covers a self deactivation and a delisted account, and we shouldn't tell listeners the creator removed a track when moderation suppressed it. Wording matches the web tombstone from #14570. The check is an explicit `=== false` because an absent field must not read as unavailable. Verified against audius.co/rehoxx/just-for-tonight-wmellark-hoonds in all three flavors (card, compact, tiny) and both routes (hash id and permalink), with a streamable track confirmed unaffected. Co-Authored-By: Claude Opus 5 --- packages/embed/src/components/app.jsx | 24 ++++++++++++++++++- .../src/components/deleted/DeletedContent.jsx | 22 +++++++++++++---- .../components/deleted/DeletedContentTiny.jsx | 17 +++++++++---- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/packages/embed/src/components/app.jsx b/packages/embed/src/components/app.jsx index ea82d64cc75..d089c5cd0a0 100644 --- a/packages/embed/src/components/app.jsx +++ b/packages/embed/src/components/app.jsx @@ -141,6 +141,10 @@ const App = (props) => { const searchParams = useSearchParams() const [didError, setDidError] = useState(false) // General errors const [did404, setDid404] = useState(false) // 404s indicate content was deleted + // A track whose owner is no longer active - the artist deactivated their own + // account, or the account was delisted by the trusted notifier. Rendered with + // the same "not available" treatment as a 404, but with its own copy. + const [isUnavailable, setIsUnavailable] = useState(false) const [requestState, setRequestState] = useState(null) // Parsed request state const [isRetrying, setIsRetrying] = useState(false) // Currently retrying? @@ -188,10 +192,20 @@ const App = (props) => { if (!track) { setDid404(true) + setIsUnavailable(false) + setTracksResponse(null) + } else if (track.isStreamable === false) { + // The stream endpoint refuses these, so there is nothing to play - + // don't render the title, artist and artwork either. Checked with an + // explicit `=== false` because an absent field must not read as + // unavailable. + setDid404(true) + setIsUnavailable(true) setTracksResponse(null) } else { const events = await getEntityEvents(track.id, 'track') setDid404(false) + setIsUnavailable(false) setTracksResponse({ ...track, events }) recordOpen( decodeHashId(track.id), @@ -234,9 +248,11 @@ const App = (props) => { if (!collection) { setDid404(true) + setIsUnavailable(false) setCollectionsResponse(null) } else { setDid404(false) + setIsUnavailable(false) setCollectionsResponse(collection) recordOpen( decodeHashId(collection.id), @@ -273,6 +289,7 @@ const App = (props) => { setDidError(true) setShowLoadingAnimation(false) setDid404(false) + setIsUnavailable(false) setTracksResponse(null) setCollectionsResponse(null) } @@ -334,7 +351,12 @@ const App = (props) => { // Tiny variant renders its own deleted content if (did404) { - return + return ( + + ) } if (showLoadingAnimation && !isTiny) { diff --git a/packages/embed/src/components/deleted/DeletedContent.jsx b/packages/embed/src/components/deleted/DeletedContent.jsx index 5fc1277d75b..c7b5c0ef2b2 100644 --- a/packages/embed/src/components/deleted/DeletedContent.jsx +++ b/packages/embed/src/components/deleted/DeletedContent.jsx @@ -12,22 +12,36 @@ import DeletedContentTiny from './DeletedContentTiny' const messages = { mainLabel: 'This content was removed by the creator.', deleted: 'Deleted', + unavailable: 'This track can no longer be streamed on Audius.', subLabel1: 'Unlimited Uploads.', subLabel2: '320kbps Streaming.', subLabel3: '100% Free.', buttonLabel: 'Find more on' } -const DeletedContent = ({ flavor, isBlocked }) => { +const DeletedContent = ({ flavor, isBlocked, isUnavailable }) => { const onClickFindMore = () => { window.open(getCopyableLink(), '_blank') } + // `unavailable` says nothing about the account on purpose: the same flag + // covers a self deactivation and a delisted account, and we shouldn't tell + // listeners the creator removed the track when moderation suppressed it. + const label = isUnavailable + ? messages.unavailable + : isBlocked + ? messages.deleted + : messages.mainLabel + const isCard = flavor === PlayerFlavor.CARD const isTiny = flavor === PlayerFlavor.TINY if (isTiny) { return ( - + ) } @@ -41,9 +55,7 @@ const DeletedContent = ({ flavor, isBlocked }) => { }} /> )} -
- {isBlocked ? messages.deleted : messages.mainLabel} -
+
{label}
{isCard && (
{messages.subLabel1} diff --git a/packages/embed/src/components/deleted/DeletedContentTiny.jsx b/packages/embed/src/components/deleted/DeletedContentTiny.jsx index cbdf502ff0f..62076b30a96 100644 --- a/packages/embed/src/components/deleted/DeletedContentTiny.jsx +++ b/packages/embed/src/components/deleted/DeletedContentTiny.jsx @@ -5,10 +5,19 @@ import styles from './DeletedContentTiny.module.css' const messages = { deletedBy: 'Track Deleted By Artist', - deleted: 'Deleted' + deleted: 'Deleted', + unavailable: 'Track Unavailable' } -const DeletedContentTiny = ({ onClick, isBlocked }) => { +const DeletedContentTiny = ({ onClick, isBlocked, isUnavailable }) => { + // `unavailable` says nothing about the account on purpose: the same flag + // covers a self deactivation and a delisted account. + const label = isUnavailable + ? messages.unavailable + : isBlocked + ? messages.deleted + : messages.deletedBy + return (
{ className={styles.playButton} />
-
- {isBlocked ? messages.deleted : messages.deletedBy} -
+
{label}