From 2298d6e41acb73e8a79166712ac9b8492222bd5a Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Tue, 17 Feb 2026 11:54:55 -0800 Subject: [PATCH 1/3] Fix genres --- packages/common/src/adapters/track.ts | 5 +++- .../src/api/tan-query/lineups/useTrending.ts | 2 +- .../api/tan-query/tracks/useUpdateTrack.ts | 4 +-- .../api/tan-query/upload/usePublishStems.ts | 10 ++----- .../api/tan-query/upload/usePublishTracks.ts | 9 ++++-- .../src/api/tan-query/upload/useUpload.ts | 2 +- .../src/store/pages/trending/reducer.ts | 11 ++----- packages/common/src/store/upload/types.ts | 3 +- packages/common/src/utils/genres.ts | 29 +++++++++++++++++-- .../common/src/utils/isLongFormContent.ts | 2 +- packages/common/src/utils/route.ts | 4 ++- .../src/components/audio/AudioPlayer.tsx | 10 +++---- .../src/components/lineup-tile/TrackTile.tsx | 4 +-- .../now-playing-drawer/ActionsBar.tsx | 2 +- .../now-playing-drawer/TrackingBar.tsx | 2 +- .../mobile/src/components/scrubber/Slider.tsx | 2 +- .../src/components/scrubber/usePosition.ts | 2 +- .../components/track-list/TrackListItem.tsx | 2 +- .../track-screen/TrackScreenDetailsTile.tsx | 4 +-- packages/web/src/common/store/player/sagas.ts | 4 +-- .../web/src/components/menu/TrackMenu.tsx | 2 +- .../components/play-bar/desktop/PlayBar.tsx | 4 +-- .../next-button/NextButtonProvider.tsx | 2 +- .../PreviousButtonProvider.tsx | 2 +- .../src/components/track/GiantTrackTile.tsx | 4 +-- packages/web/src/components/track/helpers.ts | 2 +- .../TrendingGenreSelectionPage.tsx | 11 ++----- .../user-generated-text/UserGeneratedText.tsx | 3 +- .../sign-up-page/pages/SelectArtistsPage.tsx | 4 +-- .../desktop/TrendingGenreFilters.tsx | 2 +- .../desktop/TrendingPageContent.tsx | 6 ++-- .../components/mobile/TrendingPageContent.tsx | 2 +- 32 files changed, 88 insertions(+), 69 deletions(-) diff --git a/packages/common/src/adapters/track.ts b/packages/common/src/adapters/track.ts index c03ab98e4d6..68137ad5c46 100644 --- a/packages/common/src/adapters/track.ts +++ b/packages/common/src/adapters/track.ts @@ -1,6 +1,7 @@ import { type full, type CrossPlatformFile, + type Genre, type NativeFile, type Track, HashId, @@ -380,7 +381,9 @@ export const trackMetadataForUploadToSdk = (input: TrackMetadataForUpload) => ({ description: squashNewLines(input.description) ?? undefined, mood: input.mood, tags: input.tags ?? undefined, - genre: input.genre || undefined, + ...(input.genre !== undefined && input.genre !== '' + ? { genre: input.genre as Genre } + : {}), releaseDate: input.release_date ? new Date(input.release_date) : undefined, previewStartSeconds: input.preview_start_seconds ?? undefined, previewCid: input.preview_cid ?? '', diff --git a/packages/common/src/api/tan-query/lineups/useTrending.ts b/packages/common/src/api/tan-query/lineups/useTrending.ts index a2ce81498ea..86df58cbdd7 100644 --- a/packages/common/src/api/tan-query/lineups/useTrending.ts +++ b/packages/common/src/api/tan-query/lineups/useTrending.ts @@ -86,7 +86,7 @@ export const useTrending = ( const { data: sdkResponse = [] } = await sdk.tracks.getTrendingTracks({ time: timeRange, - genre: (genre as string) || undefined, + genre: genre ?? undefined, userId: OptionalId.parse(currentUserId), limit: currentPageSize, offset: pageParam diff --git a/packages/common/src/api/tan-query/tracks/useUpdateTrack.ts b/packages/common/src/api/tan-query/tracks/useUpdateTrack.ts index a0dd5669bac..04d1c69f262 100644 --- a/packages/common/src/api/tan-query/tracks/useUpdateTrack.ts +++ b/packages/common/src/api/tan-query/tracks/useUpdateTrack.ts @@ -1,4 +1,4 @@ -import { Id, type CrossPlatformFile } from '@audius/sdk' +import { type UpdateTrackRequestBody, Id, type CrossPlatformFile } from '@audius/sdk' import { useMutation, useQueryClient } from '@tanstack/react-query' import { useDispatch, useStore } from 'react-redux' @@ -63,7 +63,7 @@ export const useUpdateTrack = () => { imageFile, trackId: Id.parse(trackId), userId: Id.parse(userId), - metadata: sdkMetadata, + metadata: sdkMetadata as UpdateTrackRequestBody, onProgress: (_, progress) => { if (progress.key === 'audio') { dispatch( diff --git a/packages/common/src/api/tan-query/upload/usePublishStems.ts b/packages/common/src/api/tan-query/upload/usePublishStems.ts index 56439c46b4f..7437cacad5e 100644 --- a/packages/common/src/api/tan-query/upload/usePublishStems.ts +++ b/packages/common/src/api/tan-query/upload/usePublishStems.ts @@ -1,13 +1,9 @@ import { HashId, Id, type UploadResponse } from '@audius/sdk' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { - StemCategory, - Name, - type StemUpload, - type TrackMetadata -} from '~/models' +import { StemCategory, Name, type StemUpload } from '~/models' import { ProgressStatus, uploadActions } from '~/store' +import type { TrackMetadataForUpload } from '~/store/upload/types' import { getStemsQueryKey } from '../tracks/useStems' import { useCurrentUserId } from '../users/account/useCurrentUserId' @@ -27,7 +23,7 @@ type PublishStemsContext = Pick< type PublishStemsParams = { clientId: string parentTrackId: number - parentMetadata: Omit + parentMetadata: TrackMetadataForUpload stems: { metadata: StemUpload audioUploadResponse: UploadResponse diff --git a/packages/common/src/api/tan-query/upload/usePublishTracks.ts b/packages/common/src/api/tan-query/upload/usePublishTracks.ts index 4fff4340bd8..2c03558e281 100644 --- a/packages/common/src/api/tan-query/upload/usePublishTracks.ts +++ b/packages/common/src/api/tan-query/upload/usePublishTracks.ts @@ -1,5 +1,5 @@ import { USDC } from '@audius/fixed-decimal' -import { HashId, Id, type UploadResponse } from '@audius/sdk' +import { type Genre, HashId, Id, type UploadResponse } from '@audius/sdk' import { useMutation, useQueryClient } from '@tanstack/react-query' import { trackMetadataForUploadToSdk } from '~/adapters' @@ -79,9 +79,14 @@ export const publishTracks = async ( const publishParentTrack = async () => { try { + // Publish requires genre; form validation ensures it's set at runtime + const metadata = { + ...camelMetadata, + ...(param.metadata.genre ? { genre: param.metadata.genre } : {}) + } as typeof camelMetadata & { genre: Genre } const res = await sdk.tracks.publishTrack({ userId: Id.parse(userId), - metadata: camelMetadata, + metadata, audioUploadResponse: param.audioUploadResponse, imageUploadResponse: param.imageUploadResponse }) diff --git a/packages/common/src/api/tan-query/upload/useUpload.ts b/packages/common/src/api/tan-query/upload/useUpload.ts index 6e3927b8126..a05e5ddedc1 100644 --- a/packages/common/src/api/tan-query/upload/useUpload.ts +++ b/packages/common/src/api/tan-query/upload/useUpload.ts @@ -200,7 +200,7 @@ export const useUpload = ( ? (t.metadata.artwork.source as 'unsplash' | 'original') : 'original', trackId: t.metadata.track_id!, - genre: t.metadata.genre, + genre: t.metadata.genre ?? '', mood: t.metadata.mood ?? undefined, size: t.file.size ?? -1, fileType: t.file.type ?? '', diff --git a/packages/common/src/store/pages/trending/reducer.ts b/packages/common/src/store/pages/trending/reducer.ts index 17af3e72d31..4396e80e4db 100644 --- a/packages/common/src/store/pages/trending/reducer.ts +++ b/packages/common/src/store/pages/trending/reducer.ts @@ -17,7 +17,7 @@ import { TRENDING_MONTH_PREFIX, TRENDING_ALL_TIME_PREFIX } from '~/store/pages/trending/lineup/actions' -import { ALL_GENRES, GENRES, Genre } from '~/utils/genres' +import { parseTrendingGenreFromUrl } from '~/utils/genres' import { TimeRange, Track } from '../../../models' @@ -94,7 +94,7 @@ const reducer = if (history) { const urlParams = new URLSearchParams(history.location.search) - const genre = urlParams.get('genre') as Genre | null + const genreParam = urlParams.get('genre') const timeRange = urlParams.get('timeRange') as TimeRange | null return { ...initialState, @@ -102,12 +102,7 @@ const reducer = timeRange && Object.values(TimeRange).includes(timeRange) ? timeRange : TimeRange.WEEK, - trendingGenre: - genre === ALL_GENRES - ? null - : genre && GENRES.includes(genre) - ? genre - : null + trendingGenre: parseTrendingGenreFromUrl(genreParam) } } diff --git a/packages/common/src/store/upload/types.ts b/packages/common/src/store/upload/types.ts index 8ca5de6174b..cd55f4fa40c 100644 --- a/packages/common/src/store/upload/types.ts +++ b/packages/common/src/store/upload/types.ts @@ -43,7 +43,8 @@ export const isTrackForUpload = ( */ export interface TrackMetadataForUpload extends Omit { - genre?: Genre | '' + /** API tracks use genre: string; form empty state is ''. */ + genre?: Genre | '' | string mood?: Mood | null artwork?: | Nullable<{ diff --git a/packages/common/src/utils/genres.ts b/packages/common/src/utils/genres.ts index f4de94d1d23..7d3ca52b913 100644 --- a/packages/common/src/utils/genres.ts +++ b/packages/common/src/utils/genres.ts @@ -81,8 +81,33 @@ export const GENRES = [ export const convertGenreLabelToValue = ( genreLabel: (typeof GENRES)[number] -) => { - return genreLabel.replace(ELECTRONIC_PREFIX, '') +): SDKGenre => { + return genreLabel.replace(ELECTRONIC_PREFIX, '') as SDKGenre +} + +/** + * Converts a string from the trending genre UI (e.g. from URL or genre list) + * into Genre | null for Redux state. Returns null for null, empty, or ALL_GENRES. + */ +export const parseTrendingGenreFromUrl = (param: string | null): SDKGenre | null => { + if (param === null || param === '' || param === ALL_GENRES) return null + const genresList = GENRES as readonly string[] + if (!genresList.includes(param)) return null + const trimmed = param.startsWith(ELECTRONIC_PREFIX) + ? param.slice(ELECTRONIC_PREFIX.length) + : param + return trimmed as SDKGenre +} + +/** + * Converts a genre string from UI (e.g. from GenreSelectionList) to Genre | null + * for setTrendingGenre. Use when the value is known to come from GENRES. + */ +export const toTrendingGenre = (value: string | null): SDKGenre | null => { + if (value === null || value === '' || value === ALL_GENRES) return null + const genresList = GENRES as readonly string[] + if (!genresList.includes(value)) return null + return convertGenreLabelToValue(value as (typeof GENRES)[number]) } const NEWLY_ADDED_GENRES: string[] = [] diff --git a/packages/common/src/utils/isLongFormContent.ts b/packages/common/src/utils/isLongFormContent.ts index 5e3764b1049..84140d0fe64 100644 --- a/packages/common/src/utils/isLongFormContent.ts +++ b/packages/common/src/utils/isLongFormContent.ts @@ -5,4 +5,4 @@ import { Maybe } from './typeUtils' export const isLongFormContent = ( track: Maybe | null> -) => track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS +) => track?.genre === Genre.Podcasts || track?.genre === Genre.Audiobooks diff --git a/packages/common/src/utils/route.ts b/packages/common/src/utils/route.ts index f84f327fefd..763634bb32e 100644 --- a/packages/common/src/utils/route.ts +++ b/packages/common/src/utils/route.ts @@ -441,7 +441,9 @@ export const searchPage = (searchOptions: SearchOptions) => { const { category, ...searchParams } = searchOptions if (searchParams.genre) { - searchParams.genre = convertGenreLabelToValue(searchParams.genre) as Genre + searchParams.genre = convertGenreLabelToValue( + searchParams.genre as Parameters[0] + ) as Genre } // Build the search path - category is optional diff --git a/packages/mobile/src/components/audio/AudioPlayer.tsx b/packages/mobile/src/components/audio/AudioPlayer.tsx index cd913ffb736..515c3284b69 100644 --- a/packages/mobile/src/components/audio/AudioPlayer.tsx +++ b/packages/mobile/src/components/audio/AudioPlayer.tsx @@ -489,8 +489,8 @@ export const AudioPlayer = () => { }) const isLongFormContent = - track?.genre === Genre.PODCASTS || - track?.genre === Genre.AUDIOBOOKS + track?.genre === Genre.Podcasts || + track?.genre === Genre.Audiobooks const trackPosition = trackPositions?.[track.track_id] if (trackPosition?.status === 'IN_PROGRESS') { dispatch( @@ -513,8 +513,8 @@ export const AudioPlayer = () => { } const isLongFormContent = - queueTracks[playerIndex]?.track?.genre === Genre.PODCASTS || - queueTracks[playerIndex]?.track?.genre === Genre.AUDIOBOOKS + queueTracks[playerIndex]?.track?.genre === Genre.Podcasts || + queueTracks[playerIndex]?.track?.genre === Genre.Audiobooks // Always set the correct playback rate when the active track changes const newRate = isLongFormContent @@ -532,7 +532,7 @@ export const AudioPlayer = () => { if (event?.lastPosition !== undefined && event?.index !== undefined) { const { track } = queueTracks[event.index] ?? {} const isLongFormContent = - track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS + track?.genre === Genre.Podcasts || track?.genre === Genre.Audiobooks const isAtEndOfTrack = track?.duration && event.lastPosition >= track.duration - TRACK_END_BUFFER diff --git a/packages/mobile/src/components/lineup-tile/TrackTile.tsx b/packages/mobile/src/components/lineup-tile/TrackTile.tsx index b82251f904f..49f66b42ca8 100644 --- a/packages/mobile/src/components/lineup-tile/TrackTile.tsx +++ b/packages/mobile/src/components/lineup-tile/TrackTile.tsx @@ -194,7 +194,7 @@ const TrackTileComponent = (props: TrackTileProps) => { const handlePressOverflow = useCallback(() => { if (!track) return const isLongFormContent = - track.genre === Genre.PODCASTS || track.genre === Genre.AUDIOBOOKS + track.genre === Genre.Podcasts || track.genre === Genre.Audiobooks const isOwner = currentUserId === track.owner_id const isArtistPick = isOwner && user?.artist_pick_track_id === track.track_id @@ -303,7 +303,7 @@ const TrackTileComponent = (props: TrackTileProps) => { trackId={track.track_id} duration={track.duration} isLongFormContent={ - track.genre === Genre.PODCASTS || track.genre === Genre.AUDIOBOOKS + track.genre === Genre.Podcasts || track.genre === Genre.Audiobooks } isArtistPick={showArtistPick && isArtistPick} /> diff --git a/packages/mobile/src/components/now-playing-drawer/ActionsBar.tsx b/packages/mobile/src/components/now-playing-drawer/ActionsBar.tsx index e0e7901b4c7..1c4ee617937 100644 --- a/packages/mobile/src/components/now-playing-drawer/ActionsBar.tsx +++ b/packages/mobile/src/components/now-playing-drawer/ActionsBar.tsx @@ -183,7 +183,7 @@ export const ActionsBar = ({ track }: ActionsBarProps) => { const onPressOverflow = useCallback(() => { if (track) { const isLongFormContent = - track.genre === Genre.PODCASTS || track.genre === Genre.AUDIOBOOKS + track.genre === Genre.Podcasts || track.genre === Genre.Audiobooks const overflowActions = [ OverflowAction.VIEW_COMMENTS, OverflowAction.SHARE, diff --git a/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx b/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx index 6b5a2cb926b..26c21ca3849 100644 --- a/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx +++ b/packages/mobile/src/components/now-playing-drawer/TrackingBar.tsx @@ -68,7 +68,7 @@ export const TrackingBar = (props: TrackingBarProps) => { // Calculate the actual playback rate based on track type const isLongFormContent = - trackGenre === Genre.PODCASTS || trackGenre === Genre.AUDIOBOOKS + trackGenre === Genre.Podcasts || trackGenre === Genre.Audiobooks const actualPlaybackRate = isLongFormContent ? playbackRateValueMap[playbackRate] : 1.0 diff --git a/packages/mobile/src/components/scrubber/Slider.tsx b/packages/mobile/src/components/scrubber/Slider.tsx index b5bde348c10..a5a52537a1e 100644 --- a/packages/mobile/src/components/scrubber/Slider.tsx +++ b/packages/mobile/src/components/scrubber/Slider.tsx @@ -161,7 +161,7 @@ export const Slider = memo(function Slider(props: SliderProps) { // Calculate the actual playback rate based on track type const isLongFormContent = - trackGenre === Genre.PODCASTS || trackGenre === Genre.AUDIOBOOKS + trackGenre === Genre.Podcasts || trackGenre === Genre.Audiobooks const actualPlaybackRate = isLongFormContent ? playbackRateValueMap[playbackRate] : 1.0 diff --git a/packages/mobile/src/components/scrubber/usePosition.ts b/packages/mobile/src/components/scrubber/usePosition.ts index 5f3f5ccffb2..2f30a67db3b 100644 --- a/packages/mobile/src/components/scrubber/usePosition.ts +++ b/packages/mobile/src/components/scrubber/usePosition.ts @@ -41,7 +41,7 @@ export const usePosition = ( // Calculate the actual playback rate based on track type const isLongFormContent = - trackGenre === Genre.PODCASTS || trackGenre === Genre.AUDIOBOOKS + trackGenre === Genre.Podcasts || trackGenre === Genre.Audiobooks const actualPlaybackRate = isLongFormContent ? playbackRateValueMap[playbackRate] : 1.0 diff --git a/packages/mobile/src/components/track-list/TrackListItem.tsx b/packages/mobile/src/components/track-list/TrackListItem.tsx index be048eb0766..5d906f08462 100644 --- a/packages/mobile/src/components/track-list/TrackListItem.tsx +++ b/packages/mobile/src/components/track-list/TrackListItem.tsx @@ -261,7 +261,7 @@ const TrackListItemComponent = (props: TrackListItemComponentProps) => { currentUserId && contextPlaylist?.playlist_owner_id === currentUserId const isLongFormContent = - track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS + track?.genre === Genre.Podcasts || track?.genre === Genre.Audiobooks const playbackPositionInfo = useSelector((state) => getTrackPosition(state, { trackId: track_id, userId: currentUserId }) ) diff --git a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx index e433d4baf84..0cae222ae8b 100644 --- a/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx +++ b/packages/mobile/src/screens/track-screen/TrackScreenDetailsTile.tsx @@ -227,7 +227,7 @@ export const TrackScreenDetailsTile = ({ const { open: openCommentDrawer } = useCommentDrawer() const isLongFormContent = - track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS + track?.genre === Genre.Podcasts || track?.genre === Genre.Audiobooks const isUSDCPurchaseGated = isContentUSDCPurchaseGated(streamConditions) const { data: remixContest } = useRemixContest(trackId) const isRemixContest = !!remixContest @@ -433,7 +433,7 @@ export const TrackScreenDetailsTile = ({ const handlePressOverflow = () => { const isLongFormContent = - genre === Genre.PODCASTS || genre === Genre.AUDIOBOOKS + genre === Genre.Podcasts || genre === Genre.Audiobooks const addToAlbumAction = isOwner && !ddexApp ? OverflowAction.ADD_TO_ALBUM : null const overflowActions = [ diff --git a/packages/web/src/common/store/player/sagas.ts b/packages/web/src/common/store/player/sagas.ts index 6fcefe6a2a7..aed5cae9cbf 100644 --- a/packages/web/src/common/store/player/sagas.ts +++ b/packages/web/src/common/store/player/sagas.ts @@ -144,7 +144,7 @@ export function* watchPlay() { ) const isLongFormContent = - track.genre === Genre.PODCASTS || track.genre === Genre.AUDIOBOOKS + track.genre === Genre.Podcasts || track.genre === Genre.Audiobooks const createEndChannel = async (url: string) => { const endChannel = eventChannel((emitter) => { @@ -325,7 +325,7 @@ export function* watchSeek() { const track = yield* queryTrack(trackId) const currentUserId = yield* call(queryCurrentUserId) const isLongFormContent = - track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS + track?.genre === Genre.Podcasts || track?.genre === Genre.Audiobooks if (isLongFormContent) { yield* put( diff --git a/packages/web/src/components/menu/TrackMenu.tsx b/packages/web/src/components/menu/TrackMenu.tsx index ea3e979fd16..c1994ca48c1 100644 --- a/packages/web/src/components/menu/TrackMenu.tsx +++ b/packages/web/src/components/menu/TrackMenu.tsx @@ -186,7 +186,7 @@ const TrackMenu = ({ const albumInfo = partialTrack?.album_backlink const isLongFormContent = - genre === Genre.PODCASTS || genre === Genre.AUDIOBOOKS + genre === Genre.Podcasts || genre === Genre.Audiobooks const shareMenuItem = { text: messages.share, diff --git a/packages/web/src/components/play-bar/desktop/PlayBar.tsx b/packages/web/src/components/play-bar/desktop/PlayBar.tsx index 4ae13dad39a..60f6aa0f3f0 100644 --- a/packages/web/src/components/play-bar/desktop/PlayBar.tsx +++ b/packages/web/src/components/play-bar/desktop/PlayBar.tsx @@ -95,8 +95,8 @@ const PlayBar = () => { const isStreamGated = currentTrack?.is_stream_gated || false const trackPermalink = currentTrack?.permalink || '' const isLongFormContent = - currentTrack?.genre === Genre.PODCASTS || - currentTrack?.genre === Genre.AUDIOBOOKS + currentTrack?.genre === Genre.Podcasts || + currentTrack?.genre === Genre.Audiobooks const playable = !!uid diff --git a/packages/web/src/components/play-bar/next-button/NextButtonProvider.tsx b/packages/web/src/components/play-bar/next-button/NextButtonProvider.tsx index 71a9d64d37d..50acc5a1456 100644 --- a/packages/web/src/components/play-bar/next-button/NextButtonProvider.tsx +++ b/packages/web/src/components/play-bar/next-button/NextButtonProvider.tsx @@ -9,7 +9,7 @@ type NextButtonProviderProps = NextButtonProps | ForwardSkipButtonProps const NextButtonProvider = (props: NextButtonProviderProps) => { const track = useCurrentTrack() const isLongFormContent = - track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS + track?.genre === Genre.Podcasts || track?.genre === Genre.Audiobooks return isLongFormContent ? ( ) : ( diff --git a/packages/web/src/components/play-bar/previous-button/PreviousButtonProvider.tsx b/packages/web/src/components/play-bar/previous-button/PreviousButtonProvider.tsx index afe029089a1..4980718686c 100644 --- a/packages/web/src/components/play-bar/previous-button/PreviousButtonProvider.tsx +++ b/packages/web/src/components/play-bar/previous-button/PreviousButtonProvider.tsx @@ -11,7 +11,7 @@ type PreviousButtonProviderProps = PreviousButtonProps | BackwardSkipButtonProps const PreviousButtonProvider = (props: PreviousButtonProviderProps) => { const track = useCurrentTrack() const isLongFormContent = - track?.genre === Genre.PODCASTS || track?.genre === Genre.AUDIOBOOKS + track?.genre === Genre.Podcasts || track?.genre === Genre.Audiobooks return isLongFormContent ? ( ) : ( diff --git a/packages/web/src/components/track/GiantTrackTile.tsx b/packages/web/src/components/track/GiantTrackTile.tsx index 3654462830f..abcc9c8bd1c 100644 --- a/packages/web/src/components/track/GiantTrackTile.tsx +++ b/packages/web/src/components/track/GiantTrackTile.tsx @@ -214,7 +214,7 @@ export const GiantTrackTile = ({ const isRemixContest = !!remixContest const isLongFormContent = - genre === Genre.PODCASTS || genre === Genre.AUDIOBOOKS + genre === Genre.Podcasts || genre === Genre.Audiobooks const isUSDCPurchaseGated = isContentUSDCPurchaseGated(streamConditions) const { data: track } = useTrack(trackId, { select: (track) => pick(track, ['is_downloadable', 'preview_cid']) @@ -272,7 +272,7 @@ export const GiantTrackTile = ({ isScheduledRelease={isScheduledRelease} isRemix={isRemix} isStreamGated={isStreamGated} - isPodcast={genre === Genre.PODCASTS} + isPodcast={genre === Genre.Podcasts} streamConditions={streamConditions} isRemixContest={!!isRemixContest} /> diff --git a/packages/web/src/components/track/helpers.ts b/packages/web/src/components/track/helpers.ts index 9802cd0699d..7179591a617 100644 --- a/packages/web/src/components/track/helpers.ts +++ b/packages/web/src/components/track/helpers.ts @@ -22,7 +22,7 @@ export const getTrackWithFallback = (track: Track | null | undefined) => { followee_saves: [], duration: 0, save_count: 0, - genre: Genre.ALL, + genre: '', field_visibility: defaultFieldVisibility, has_current_user_reposted: false, has_current_user_saved: false, diff --git a/packages/web/src/components/trending-genre-selection/TrendingGenreSelectionPage.tsx b/packages/web/src/components/trending-genre-selection/TrendingGenreSelectionPage.tsx index a301b15ae15..4bdba3843dc 100644 --- a/packages/web/src/components/trending-genre-selection/TrendingGenreSelectionPage.tsx +++ b/packages/web/src/components/trending-genre-selection/TrendingGenreSelectionPage.tsx @@ -4,12 +4,7 @@ import { trendingPageActions, trendingPageSelectors } from '@audius/common/store' -import { - Genre, - ELECTRONIC_PREFIX, - TRENDING_GENRES, - route -} from '@audius/common/utils' +import { Genre, TRENDING_GENRES, toTrendingGenre, route } from '@audius/common/utils' import { connect } from 'react-redux' import { Dispatch } from 'redux' @@ -37,9 +32,7 @@ const ConnectedTrendingGenreSelectionPage = ({ resetAllTrending }: ConnectedTrendingGenreSelectionPageProps) => { const setTrimmedGenre = (genre: string | null) => { - const trimmedGenre = - genre !== null ? genre.replace(ELECTRONIC_PREFIX, '') : genre - setTrendingGenre(trimmedGenre as Genre | null) + setTrendingGenre(toTrendingGenre(genre)) resetAllTrending() setTrendingTimeRange(timeRange) goToTrending() diff --git a/packages/web/src/components/user-generated-text/UserGeneratedText.tsx b/packages/web/src/components/user-generated-text/UserGeneratedText.tsx index eeecf185c0f..ec3948ec187 100644 --- a/packages/web/src/components/user-generated-text/UserGeneratedText.tsx +++ b/packages/web/src/components/user-generated-text/UserGeneratedText.tsx @@ -69,7 +69,8 @@ const RenderLink = ({ attributes, content }: IntermediateRepresentation) => { } else if (instanceOfPlaylistResponse(res)) { setUnfurledContent(formatCollectionName({ collection: res.data[0] })) } else if (instanceOfUserResponse(res)) { - setUnfurledContent(formatUserName({ user: res.data })) + const user = Array.isArray(res.data) ? res.data[0] : res.data + if (user) setUnfurledContent(formatUserName({ user })) } } } diff --git a/packages/web/src/pages/sign-up-page/pages/SelectArtistsPage.tsx b/packages/web/src/pages/sign-up-page/pages/SelectArtistsPage.tsx index 53c3bbfb708..bd5e2ed4b9c 100644 --- a/packages/web/src/pages/sign-up-page/pages/SelectArtistsPage.tsx +++ b/packages/web/src/pages/sign-up-page/pages/SelectArtistsPage.tsx @@ -5,7 +5,7 @@ import { useSuggestedArtists, useTopArtistsInGenre } from '@audius/common/api' import { selectArtistsPageMessages } from '@audius/common/messages' import { UserMetadata } from '@audius/common/models' import { selectArtistsSchema } from '@audius/common/schemas' -import { Genre, convertGenreLabelToValue, route } from '@audius/common/utils' +import { GENRES, convertGenreLabelToValue, route } from '@audius/common/utils' import { Flex, Paper, SelectablePill, Text, useTheme } from '@audius/harmony' import { animated, useSpring } from '@react-spring/web' import { Form, Formik, useFormikContext } from 'formik' @@ -210,7 +210,7 @@ export const SelectArtistsPage = () => { disableScroll={!isMobile} > {artistGenres.map((genre) => { - const genreValue = convertGenreLabelToValue(genre as Genre) + const genreValue = convertGenreLabelToValue(genre as (typeof GENRES)[number]) return ( // TODO: max of 6, kebab overflow { key={genre} name='trending-genre-filter' type='radio' - label={getCanonicalName(genre)} + label={getCanonicalName(genre ?? '') ?? ''} value={genre} size='large' isSelected={genre === currentGenre} diff --git a/packages/web/src/pages/trending-page/components/desktop/TrendingPageContent.tsx b/packages/web/src/pages/trending-page/components/desktop/TrendingPageContent.tsx index b1e42625e16..3692d83c741 100644 --- a/packages/web/src/pages/trending-page/components/desktop/TrendingPageContent.tsx +++ b/packages/web/src/pages/trending-page/components/desktop/TrendingPageContent.tsx @@ -7,8 +7,8 @@ import { trendingUndergroundPageLineupSelectors } from '@audius/common/store' import { - ELECTRONIC_PREFIX, getCanonicalName, + toTrendingGenre, TRENDING_GENRES } from '@audius/common/utils' import { @@ -188,9 +188,7 @@ const TrendingPageContent = ({ containerRef }: TrendingPageContentProps) => { const setGenreAndRefresh = useCallback( (genre: string | null) => { - const trimmedGenre = - genre !== null ? genre.replace(ELECTRONIC_PREFIX, '') : genre - setTrendingGenre(trimmedGenre) + setTrendingGenre(toTrendingGenre(genre)) // Call reset to change everything everything to skeleton tiles makeResetTrending(TimeRange.WEEK)() diff --git a/packages/web/src/pages/trending-page/components/mobile/TrendingPageContent.tsx b/packages/web/src/pages/trending-page/components/mobile/TrendingPageContent.tsx index 70466487458..8b0228c7268 100644 --- a/packages/web/src/pages/trending-page/components/mobile/TrendingPageContent.tsx +++ b/packages/web/src/pages/trending-page/components/mobile/TrendingPageContent.tsx @@ -312,7 +312,7 @@ const TrendingPageMobileContent = ({ Date: Tue, 17 Feb 2026 12:07:00 -0800 Subject: [PATCH 2/3] Finalize --- .../common/src/api/tan-query/upload/usePublishTracks.ts | 9 ++------- packages/common/src/utils/genres.ts | 8 ++++---- packages/common/src/utils/route.ts | 4 ++-- .../src/pages/sign-up-page/pages/SelectArtistsPage.tsx | 4 ++-- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/packages/common/src/api/tan-query/upload/usePublishTracks.ts b/packages/common/src/api/tan-query/upload/usePublishTracks.ts index 2c03558e281..4fff4340bd8 100644 --- a/packages/common/src/api/tan-query/upload/usePublishTracks.ts +++ b/packages/common/src/api/tan-query/upload/usePublishTracks.ts @@ -1,5 +1,5 @@ import { USDC } from '@audius/fixed-decimal' -import { type Genre, HashId, Id, type UploadResponse } from '@audius/sdk' +import { HashId, Id, type UploadResponse } from '@audius/sdk' import { useMutation, useQueryClient } from '@tanstack/react-query' import { trackMetadataForUploadToSdk } from '~/adapters' @@ -79,14 +79,9 @@ export const publishTracks = async ( const publishParentTrack = async () => { try { - // Publish requires genre; form validation ensures it's set at runtime - const metadata = { - ...camelMetadata, - ...(param.metadata.genre ? { genre: param.metadata.genre } : {}) - } as typeof camelMetadata & { genre: Genre } const res = await sdk.tracks.publishTrack({ userId: Id.parse(userId), - metadata, + metadata: camelMetadata, audioUploadResponse: param.audioUploadResponse, imageUploadResponse: param.imageUploadResponse }) diff --git a/packages/common/src/utils/genres.ts b/packages/common/src/utils/genres.ts index 7d3ca52b913..d1088bbe648 100644 --- a/packages/common/src/utils/genres.ts +++ b/packages/common/src/utils/genres.ts @@ -79,9 +79,9 @@ export const GENRES = [ ...Object.values(ELECTRONIC_SUBGENRES) ] as const -export const convertGenreLabelToValue = ( - genreLabel: (typeof GENRES)[number] -): SDKGenre => { +export type GenreLabel = (typeof GENRES)[number] + +export const convertGenreLabelToValue = (genreLabel: GenreLabel): SDKGenre => { return genreLabel.replace(ELECTRONIC_PREFIX, '') as SDKGenre } @@ -107,7 +107,7 @@ export const toTrendingGenre = (value: string | null): SDKGenre | null => { if (value === null || value === '' || value === ALL_GENRES) return null const genresList = GENRES as readonly string[] if (!genresList.includes(value)) return null - return convertGenreLabelToValue(value as (typeof GENRES)[number]) + return convertGenreLabelToValue(value as GenreLabel) } const NEWLY_ADDED_GENRES: string[] = [] diff --git a/packages/common/src/utils/route.ts b/packages/common/src/utils/route.ts index 763634bb32e..c024d85c2dc 100644 --- a/packages/common/src/utils/route.ts +++ b/packages/common/src/utils/route.ts @@ -3,7 +3,7 @@ import qs from 'query-string' import { ID, SearchCategory, SearchFilters } from '~/models' import { encodeUrlName, formatTickerForUrl } from './formatUtil' -import { convertGenreLabelToValue, Genre } from './genres' +import { convertGenreLabelToValue, type GenreLabel, Genre } from './genres' // External Routes export const PRIVACY_POLICY = '/legal/privacy-policy' @@ -442,7 +442,7 @@ export const searchPage = (searchOptions: SearchOptions) => { if (searchParams.genre) { searchParams.genre = convertGenreLabelToValue( - searchParams.genre as Parameters[0] + searchParams.genre as GenreLabel ) as Genre } diff --git a/packages/web/src/pages/sign-up-page/pages/SelectArtistsPage.tsx b/packages/web/src/pages/sign-up-page/pages/SelectArtistsPage.tsx index bd5e2ed4b9c..4743aa7e078 100644 --- a/packages/web/src/pages/sign-up-page/pages/SelectArtistsPage.tsx +++ b/packages/web/src/pages/sign-up-page/pages/SelectArtistsPage.tsx @@ -5,7 +5,7 @@ import { useSuggestedArtists, useTopArtistsInGenre } from '@audius/common/api' import { selectArtistsPageMessages } from '@audius/common/messages' import { UserMetadata } from '@audius/common/models' import { selectArtistsSchema } from '@audius/common/schemas' -import { GENRES, convertGenreLabelToValue, route } from '@audius/common/utils' +import { type GenreLabel, convertGenreLabelToValue, route } from '@audius/common/utils' import { Flex, Paper, SelectablePill, Text, useTheme } from '@audius/harmony' import { animated, useSpring } from '@react-spring/web' import { Form, Formik, useFormikContext } from 'formik' @@ -210,7 +210,7 @@ export const SelectArtistsPage = () => { disableScroll={!isMobile} > {artistGenres.map((genre) => { - const genreValue = convertGenreLabelToValue(genre as (typeof GENRES)[number]) + const genreValue = convertGenreLabelToValue(genre as GenreLabel) return ( // TODO: max of 6, kebab overflow Date: Tue, 17 Feb 2026 12:16:14 -0800 Subject: [PATCH 3/3] simplify --- packages/common/src/adapters/track.ts | 190 ++++++++++-------- packages/common/src/utils/route.ts | 4 +- .../desktop/TrendingGenreFilters.tsx | 2 +- 3 files changed, 104 insertions(+), 92 deletions(-) diff --git a/packages/common/src/adapters/track.ts b/packages/common/src/adapters/track.ts index 68137ad5c46..cb79cb4e774 100644 --- a/packages/common/src/adapters/track.ts +++ b/packages/common/src/adapters/track.ts @@ -1,7 +1,7 @@ import { type full, type CrossPlatformFile, - type Genre, + Genre, type NativeFile, type Track, HashId, @@ -35,6 +35,17 @@ import { repostFromSDK } from './repost' import { userMetadataFromSDK } from './user' import { transformAndCleanList } from './utils' +const VALID_GENRES = new Set(Object.values(Genre)) + +function toSdkGenre( + value: string | undefined | '' +): (typeof Genre)[keyof typeof Genre] | undefined { + if (value === undefined || value === '') return undefined + return VALID_GENRES.has(value) + ? (value as (typeof Genre)[keyof typeof Genre]) + : undefined +} + export const trackSegmentFromSDK = ({ duration, multihash @@ -353,94 +364,95 @@ export const stemTrackMetadataFromSDK = ( } } -export const trackMetadataForUploadToSdk = (input: TrackMetadataForUpload) => ({ - ...camelcaseKeys( - pick(input, [ - 'license', - 'isrc', - 'iswc', - 'is_unlisted', - 'is_premium', - 'premium_conditions', - 'is_stream_gated', - 'stream_conditions', - 'is_download_gated', - 'is_downloadable', - 'is_original_available', - 'is_scheduled_release', - 'bpm', - 'is_custom_bpm', - 'is_custom_musical_key', - 'comments_disabled', - 'ddex_release_ids', - 'parental_warning_type' - ]) - ), - trackId: OptionalId.parse(input.track_id), - title: input.title, - description: squashNewLines(input.description) ?? undefined, - mood: input.mood, - tags: input.tags ?? undefined, - ...(input.genre !== undefined && input.genre !== '' - ? { genre: input.genre as Genre } - : {}), - releaseDate: input.release_date ? new Date(input.release_date) : undefined, - previewStartSeconds: input.preview_start_seconds ?? undefined, - previewCid: input.preview_cid ?? '', - ddexApp: input.ddex_app ?? '', - audioUploadId: input.audio_upload_id ?? undefined, - duration: input.duration ?? undefined, - musicalKey: input.musical_key - ? formatMusicalKey(input.musical_key) - : undefined, - trackCid: input.track_cid ?? '', - origFileCid: input.orig_file_cid ?? '', - origFilename: input.orig_filename ?? undefined, - fieldVisibility: input.field_visibility - ? mapValues( - camelcaseKeys(input.field_visibility), - (value: Maybe) => (value === null ? undefined : value) - ) - : undefined, - downloadConditions: input.download_conditions - ? accessConditionsToSDK(input.download_conditions) - : null, - streamConditions: input.stream_conditions - ? accessConditionsToSDK(input.stream_conditions) - : null, - remixOf: input.remix_of - ? { - tracks: input.remix_of.tracks.map((track) => ({ - parentTrackId: Id.parse(track.parent_track_id) - })) - } - : undefined, - stemOf: input.stem_of - ? { - category: input.stem_of.category, - parentTrackId: Id.parse(input.stem_of.parent_track_id) - } - : undefined, - copyrightLine: input.copyright_line - ? camelcaseKeys(input.copyright_line) - : undefined, - producerCopyrightLine: input.producer_copyright_line - ? camelcaseKeys(input.producer_copyright_line) - : undefined, - rightsController: input.rights_controller - ? camelcaseKeys(input.rights_controller) - : undefined, - resourceContributors: input.resource_contributors - ? input.resource_contributors.map((contributor) => - camelcaseKeys(contributor) - ) - : undefined, - indirectResourceContributors: input.indirect_resource_contributors - ? input.indirect_resource_contributors.map((contributor) => - camelcaseKeys(contributor) - ) - : undefined -}) +export const trackMetadataForUploadToSdk = (input: TrackMetadataForUpload) => { + const sdkGenre = toSdkGenre(input.genre) + return { + ...camelcaseKeys( + pick(input, [ + 'license', + 'isrc', + 'iswc', + 'is_unlisted', + 'is_premium', + 'premium_conditions', + 'is_stream_gated', + 'stream_conditions', + 'is_download_gated', + 'is_downloadable', + 'is_original_available', + 'is_scheduled_release', + 'bpm', + 'is_custom_bpm', + 'is_custom_musical_key', + 'comments_disabled', + 'ddex_release_ids', + 'parental_warning_type' + ]) + ), + trackId: OptionalId.parse(input.track_id), + title: input.title, + description: squashNewLines(input.description) ?? undefined, + mood: input.mood, + tags: input.tags ?? undefined, + ...(sdkGenre !== undefined ? { genre: sdkGenre } : {}), + releaseDate: input.release_date ? new Date(input.release_date) : undefined, + previewStartSeconds: input.preview_start_seconds ?? undefined, + previewCid: input.preview_cid ?? '', + ddexApp: input.ddex_app ?? '', + audioUploadId: input.audio_upload_id ?? undefined, + duration: input.duration ?? undefined, + musicalKey: input.musical_key + ? formatMusicalKey(input.musical_key) + : undefined, + trackCid: input.track_cid ?? '', + origFileCid: input.orig_file_cid ?? '', + origFilename: input.orig_filename ?? undefined, + fieldVisibility: input.field_visibility + ? mapValues( + camelcaseKeys(input.field_visibility), + (value: Maybe) => (value === null ? undefined : value) + ) + : undefined, + downloadConditions: input.download_conditions + ? accessConditionsToSDK(input.download_conditions) + : null, + streamConditions: input.stream_conditions + ? accessConditionsToSDK(input.stream_conditions) + : null, + remixOf: input.remix_of + ? { + tracks: input.remix_of.tracks.map((track) => ({ + parentTrackId: Id.parse(track.parent_track_id) + })) + } + : undefined, + stemOf: input.stem_of + ? { + category: input.stem_of.category, + parentTrackId: Id.parse(input.stem_of.parent_track_id) + } + : undefined, + copyrightLine: input.copyright_line + ? camelcaseKeys(input.copyright_line) + : undefined, + producerCopyrightLine: input.producer_copyright_line + ? camelcaseKeys(input.producer_copyright_line) + : undefined, + rightsController: input.rights_controller + ? camelcaseKeys(input.rights_controller) + : undefined, + resourceContributors: input.resource_contributors + ? input.resource_contributors.map((contributor) => + camelcaseKeys(contributor) + ) + : undefined, + indirectResourceContributors: input.indirect_resource_contributors + ? input.indirect_resource_contributors.map((contributor) => + camelcaseKeys(contributor) + ) + : undefined + } +} export const fileToSdk = ( file: Blob | File | NativeFile, diff --git a/packages/common/src/utils/route.ts b/packages/common/src/utils/route.ts index c024d85c2dc..3aeb681ad07 100644 --- a/packages/common/src/utils/route.ts +++ b/packages/common/src/utils/route.ts @@ -3,7 +3,7 @@ import qs from 'query-string' import { ID, SearchCategory, SearchFilters } from '~/models' import { encodeUrlName, formatTickerForUrl } from './formatUtil' -import { convertGenreLabelToValue, type GenreLabel, Genre } from './genres' +import { convertGenreLabelToValue, type GenreLabel } from './genres' // External Routes export const PRIVACY_POLICY = '/legal/privacy-policy' @@ -443,7 +443,7 @@ export const searchPage = (searchOptions: SearchOptions) => { if (searchParams.genre) { searchParams.genre = convertGenreLabelToValue( searchParams.genre as GenreLabel - ) as Genre + ) } // Build the search path - category is optional diff --git a/packages/web/src/pages/trending-page/components/desktop/TrendingGenreFilters.tsx b/packages/web/src/pages/trending-page/components/desktop/TrendingGenreFilters.tsx index 8b25416e57c..f628c14bd5d 100644 --- a/packages/web/src/pages/trending-page/components/desktop/TrendingGenreFilters.tsx +++ b/packages/web/src/pages/trending-page/components/desktop/TrendingGenreFilters.tsx @@ -83,7 +83,7 @@ export const TrendingGenreFilters = (props: TrendingGenreFiltersProps) => { key={genre} name='trending-genre-filter' type='radio' - label={getCanonicalName(genre ?? '') ?? ''} + label={getCanonicalName(genre) ?? ''} value={genre} size='large' isSelected={genre === currentGenre}