From ea84b93bf91b73541a3be50567f94902a6927d4b Mon Sep 17 00:00:00 2001 From: Violet Caulfield <42452695+anultravioletaurora@users.noreply.github.com> Date: Thu, 5 Feb 2026 23:54:13 -0600 Subject: [PATCH] bump nitro player - incorporate extra params --- bun.lock | 4 +- ios/Podfile.lock | 4 +- package.json | 2 +- ...t-native-nitro-player+0.3.0-alpha.14.patch | 13 -- src/api/queries/lyrics/index.ts | 18 +-- src/api/queries/lyrics/keys.ts | 3 +- src/components/Player/components/footer.tsx | 13 +- src/components/Player/components/header.tsx | 5 +- .../Player/components/song-info.tsx | 59 +++++---- src/components/Player/mini-player.tsx | 9 +- src/stores/player/queue.ts | 59 +++------ src/types/JellifyTrack.ts | 57 ++++++++- src/utils/mapping/item-to-track.ts | 32 ++++- src/utils/track-extra-payload.ts | 117 ++++++++++++++++++ src/utils/trackDetails.ts | 10 +- 15 files changed, 282 insertions(+), 123 deletions(-) delete mode 100644 patches/react-native-nitro-player+0.3.0-alpha.14.patch create mode 100644 src/utils/track-extra-payload.ts diff --git a/bun.lock b/bun.lock index 95f3723c..a6089742 100644 --- a/bun.lock +++ b/bun.lock @@ -43,7 +43,7 @@ "react-native-nitro-fetch": "0.1.7", "react-native-nitro-modules": "0.33.2", "react-native-nitro-ota": "^0.10.0", - "react-native-nitro-player": "0.3.0-alpha.14", + "react-native-nitro-player": "0.3.0-alpha.16", "react-native-pager-view": "8.0.0", "react-native-reanimated": "4.1.6", "react-native-safe-area-context": "5.6.2", @@ -1920,7 +1920,7 @@ "react-native-nitro-ota": ["react-native-nitro-ota@0.10.0", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "0.32.0" } }, "sha512-pxmdaeNdUdnYdD1M8BpbtQo4mZrtljWFg0gspuIohTJqi97JYIRq0b+SReN0sMMo0w912k4XXSGMr/IduGoMNg=="], - "react-native-nitro-player": ["react-native-nitro-player@0.3.0-alpha.14", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-nR1RPWr7dh4htLa5nyWLGxavoo7f3HmVfFzsLwiCeE3g8belZoY5HHMJVu72eb04oIHHDomcit99WiBQfOOQ5w=="], + "react-native-nitro-player": ["react-native-nitro-player@0.3.0-alpha.16", "", { "peerDependencies": { "react": "*", "react-native": "*", "react-native-nitro-modules": "*" } }, "sha512-fpkJAuxrGuaZa/xjnzmDvrcxSCASP7AfQZ8nfZkFzIq1P/QqPaiSSsD/SWrRglu/jd+LrX2vWhjq3qzxTSk6sQ=="], "react-native-pager-view": ["react-native-pager-view@8.0.0", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-oAwlWT1lhTkIs9HhODnjNNl/owxzn9DP1MbP+az6OTUdgbmzA16Up83sBH8NRKwrH8rNm7iuWnX1qMqiiWOLhg=="], diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 9c616ebc..eb935273 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -139,7 +139,7 @@ PODS: - SSZipArchive - Yoga - NitroOtaBundleManager (0.10.0) - - NitroPlayer (0.3.0-alpha.14): + - NitroPlayer (0.3.0-alpha.16): - boost - DoubleConversion - fast_float @@ -3655,7 +3655,7 @@ SPEC CHECKSUMS: NitroModules: 11bba9d065af151eae51e38a6425e04c3b223ff3 NitroOta: 92d4eb528566b6babf5e4a30adbda44bfa803a9b NitroOtaBundleManager: 8fad871db2daf6b9ee6f04a100c79605cfa81e8d - NitroPlayer: 8bc7be5caa2240ed636e4c1128791473eaf07a8b + NitroPlayer: 0dd9fb5af8b18fb603a1db487fca3eb9326be47b NitroSuperconfig: 54d86ee90bb78cbca09d119ea775a53ffbedb0fc PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 RCT-Folly: 846fda9475e61ec7bcbf8a3fe81edfcaeb090669 diff --git a/package.json b/package.json index f043027b..74babdcd 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "react-native-nitro-fetch": "0.1.7", "react-native-nitro-modules": "0.33.2", "react-native-nitro-ota": "^0.10.0", - "react-native-nitro-player": "0.3.0-alpha.14", + "react-native-nitro-player": "0.3.0-alpha.16", "react-native-pager-view": "8.0.0", "react-native-reanimated": "4.1.6", "react-native-safe-area-context": "5.6.2", diff --git a/patches/react-native-nitro-player+0.3.0-alpha.14.patch b/patches/react-native-nitro-player+0.3.0-alpha.14.patch deleted file mode 100644 index 1a358b9c..00000000 --- a/patches/react-native-nitro-player+0.3.0-alpha.14.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/node_modules/react-native-nitro-player/ios/core/TrackPlayerCore.swift b/node_modules/react-native-nitro-player/ios/core/TrackPlayerCore.swift -index 7d57df3..3b684ab 100644 ---- a/node_modules/react-native-nitro-player/ios/core/TrackPlayerCore.swift -+++ b/node_modules/react-native-nitro-player/ios/core/TrackPlayerCore.swift -@@ -630,8 +630,6 @@ class TrackPlayerCore: NSObject { - self.updatePlayerQueue(tracks: playlist.tracks) - // Emit initial state (paused/stopped before play) - self.emitStateChange() -- // Automatically start playback after loading -- self.play() - } else { - print(" ❌ Playlist NOT FOUND") - print(String(repeating: "🎼", count: Constants.playlistSeparatorLength) + "\n") diff --git a/src/api/queries/lyrics/index.ts b/src/api/queries/lyrics/index.ts index bd5f44f7..2bae67ec 100644 --- a/src/api/queries/lyrics/index.ts +++ b/src/api/queries/lyrics/index.ts @@ -2,7 +2,7 @@ import { useQuery, UseQueryResult } from '@tanstack/react-query' import LyricsQueryKey from './keys' import { isUndefined } from 'lodash' import { fetchRawLyrics } from './utils' -import { useApi } from '../../../stores' +import { getApi, useApi } from '../../../stores' import { usePlayerQueueStore } from '../../../stores/player/queue' import { useNowPlaying } from 'react-native-nitro-player' import JellifyTrack from '../../../types/JellifyTrack' @@ -13,19 +13,13 @@ import JellifyTrack from '../../../types/JellifyTrack' * @returns a {@link UseQueryResult} for the */ const useRawLyrics = () => { - const api = useApi() - const playerState = useNowPlaying() - const currentTrack = playerState.currentTrack - const queue = usePlayerQueueStore((state) => state.queue) - // Find the full JellifyTrack in the queue by ID - const nowPlaying = currentTrack - ? ((queue.find((t) => t.id === currentTrack.id) as JellifyTrack | undefined) ?? undefined) - : undefined + const api = getApi() + const { currentTrack } = useNowPlaying() return useQuery({ - queryKey: LyricsQueryKey(nowPlaying), - queryFn: () => fetchRawLyrics(api, nowPlaying!.item.Id!), - enabled: !isUndefined(nowPlaying), + queryKey: LyricsQueryKey(currentTrack), + queryFn: () => fetchRawLyrics(api, currentTrack!.id!), + enabled: !isUndefined(currentTrack), staleTime: (data) => (!isUndefined(data) ? Infinity : 0), }) } diff --git a/src/api/queries/lyrics/keys.ts b/src/api/queries/lyrics/keys.ts index f727dd02..9a7fd1e1 100644 --- a/src/api/queries/lyrics/keys.ts +++ b/src/api/queries/lyrics/keys.ts @@ -1,5 +1,6 @@ import JellifyTrack from '@/src/types/JellifyTrack' +import { TrackItem } from 'react-native-nitro-player' -const LyricsQueryKey = (track: JellifyTrack | undefined) => ['TRACK_LYRICS', track?.item.Id] +const LyricsQueryKey = (track: TrackItem | null) => ['TRACK_LYRICS', track?.id] export default LyricsQueryKey diff --git a/src/components/Player/components/footer.tsx b/src/components/Player/components/footer.tsx index 25e0538a..b3d25c4f 100644 --- a/src/components/Player/components/footer.tsx +++ b/src/components/Player/components/footer.tsx @@ -10,9 +10,7 @@ import { useEffect } from 'react' import usePlayerEngineStore from '../../../stores/player/engine' import useRawLyrics from '../../../api/queries/lyrics' import Animated, { Easing, FadeIn, FadeOut } from 'react-native-reanimated' -import { usePlayerQueueStore } from '../../../stores/player/queue' -import { useNowPlaying } from 'react-native-nitro-player' -import JellifyTrack from '../../../types/JellifyTrack' +import { useCurrentTrack } from '../../../stores/player/queue' export default function Footer(): React.JSX.Element { const navigation = useNavigation>() @@ -21,13 +19,7 @@ export default function Footer(): React.JSX.Element { const remoteMediaClient = useRemoteMediaClient() - const playerState = useNowPlaying() - const currentTrack = playerState.currentTrack - const queue = usePlayerQueueStore((state) => state.queue) - // Find the full JellifyTrack in the queue by ID - const nowPlaying = currentTrack - ? ((queue.find((t) => t.id === currentTrack.id) as JellifyTrack | undefined) ?? undefined) - : undefined + const nowPlaying = useCurrentTrack() const { data: lyrics } = useRawLyrics() @@ -85,7 +77,6 @@ export default function Footer(): React.JSX.Element { title: nowPlaying?.title, artist: nowPlaying?.artist, albumTitle: nowPlaying?.album || '', - releaseDate: nowPlaying?.date || '', images: [{ url: nowPlaying?.artwork || '' }], }, }, diff --git a/src/components/Player/components/header.tsx b/src/components/Player/components/header.tsx index af904be2..5b77fe68 100644 --- a/src/components/Player/components/header.tsx +++ b/src/components/Player/components/header.tsx @@ -88,7 +88,10 @@ function PlayerArtwork(): React.JSX.Element { {nowPlaying && ( diff --git a/src/components/Player/components/song-info.tsx b/src/components/Player/components/song-info.tsx index 3f9666a1..422ef6e9 100644 --- a/src/components/Player/components/song-info.tsx +++ b/src/components/Player/components/song-info.tsx @@ -16,13 +16,14 @@ import { useSharedValue, withDelay, withSpring } from 'react-native-reanimated' import type { SharedValue } from 'react-native-reanimated' import { runOnJS } from 'react-native-worklets' import { usePrevious, useSkip } from '../../../hooks/player/callbacks' -import { usePlayerQueueStore } from '../../../stores/player/queue' +import { useCurrentTrack, usePlayerQueueStore } from '../../../stores/player/queue' import { useNowPlaying } from 'react-native-nitro-player' import JellifyTrack from '../../../types/JellifyTrack' import { useApi } from '../../../stores' import { formatArtistNames } from '../../../utils/formatting/artist-names' import { isExplicit } from '../../../utils/trackDetails' import { triggerHaptic } from '../../../hooks/use-haptic-feedback' +import { MediaSourceInfo, NameGuidPair } from '@jellyfin/sdk/lib/generated-client' type SongInfoProps = { // Shared animated value coming from Player to drive overlay icons @@ -67,28 +68,20 @@ export default function SongInfo({ swipeX }: SongInfoProps = {}): React.JSX.Elem } }) - const playerState = useNowPlaying() - const currentTrack = playerState.currentTrack - const queue = usePlayerQueueStore((state) => state.queue) - // Find the full JellifyTrack in the queue by ID - const nowPlaying = currentTrack - ? ((queue.find((t) => t.id === currentTrack.id) as JellifyTrack | undefined) ?? undefined) - : undefined + const currentTrack = useCurrentTrack() const { data: album } = useQuery({ - queryKey: [QueryKeys.Album, nowPlaying?.item.AlbumId], - queryFn: () => fetchItem(api, nowPlaying!.item.AlbumId!), - enabled: !!nowPlaying?.item.AlbumId && !!api, + queryKey: [QueryKeys.Album, currentTrack?.extraPayload?.AlbumId], + queryFn: () => fetchItem(api, currentTrack!.extraPayload!.AlbumId! as string), + enabled: !!currentTrack?.extraPayload?.AlbumId && !!api, }) // Memoize expensive computations - const trackTitle = nowPlaying?.title ?? 'Untitled Track' + const trackTitle = currentTrack?.title ?? 'Untitled Track' const { artistItems, artists } = { - artistItems: nowPlaying?.item.ArtistItems, - artists: formatArtistNames( - nowPlaying?.item.ArtistItems?.map((artist) => getItemName(artist)) ?? [], - ), + artistItems: currentTrack?.extraPayload?.ArtistItems as NameGuidPair[], + artists: currentTrack?.artist, } const handleTrackPress = () => { @@ -117,7 +110,7 @@ export default function SongInfo({ swipeX }: SongInfoProps = {}): React.JSX.Elem {trackTitle} @@ -127,12 +120,12 @@ export default function SongInfo({ swipeX }: SongInfoProps = {}): React.JSX.Elem - {nowPlaying?.artist ?? 'Unknown Artist'} + {currentTrack?.artist ?? 'Unknown Artist'} - {isExplicit(nowPlaying) && ( + {isExplicit(currentTrack) && ( @@ -144,22 +137,34 @@ export default function SongInfo({ swipeX }: SongInfoProps = {}): React.JSX.Elem - nowPlaying && + currentTrack && navigationRef.navigate('Context', { - item: nowPlaying.item, + item: { + Id: currentTrack?.id, + Name: currentTrack?.title, + }, streamingMediaSourceInfo: - nowPlaying.sourceType === 'stream' - ? nowPlaying.mediaSourceInfo + currentTrack.extraPayload?.sourceType === 'stream' + ? (currentTrack.extraPayload + ?.mediaSourceInfo as MediaSourceInfo) : undefined, downloadedMediaSourceInfo: - nowPlaying.sourceType === 'download' - ? nowPlaying.mediaSourceInfo + currentTrack.extraPayload?.sourceType === 'download' + ? (currentTrack.extraPayload + ?.mediaSourceInfo as MediaSourceInfo) : undefined, }) } /> - {nowPlaying && } + {currentTrack && currentTrack.extraPayload && ( + + )} ) diff --git a/src/components/Player/mini-player.tsx b/src/components/Player/mini-player.tsx index 969d643a..18152fee 100644 --- a/src/components/Player/mini-player.tsx +++ b/src/components/Player/mini-player.tsx @@ -95,17 +95,20 @@ export default function Miniplayer(): React.JSX.Element | null { - {/* - */} + void - unShuffledQueue: JellifyTrack[] - setUnshuffledQueue: (unShuffledQueue: JellifyTrack[]) => void + unShuffledQueue: TrackItem[] + setUnshuffledQueue: (unShuffledQueue: TrackItem[]) => void - queue: JellifyTrack[] - setQueue: (queue: JellifyTrack[]) => void + queue: TrackItem[] + setQueue: (queue: TrackItem[]) => void - currentTrack: JellifyTrack | undefined - setCurrentTrack: (track: JellifyTrack | undefined) => void + currentTrack: TrackItem | undefined + setCurrentTrack: (track: TrackItem | undefined) => void currentIndex: number | undefined setCurrentIndex: (index: number | undefined) => void } -/** - * Persisted state shape - uses slimmed track types to reduce storage size - */ -type PersistedPlayerQueueState = { - shuffled: boolean - repeatMode: RepeatMode - queueRef: Queue - unShuffledQueue: PersistedJellifyTrack[] - queue: PersistedJellifyTrack[] - currentTrack: PersistedJellifyTrack | undefined - currentIndex: number | undefined -} - /** * Custom storage that serializes/deserializes tracks to their slim form * This prevents the "RangeError: String length exceeds limit" error @@ -63,7 +50,7 @@ const queueStorage: PersistStorage = { if (!str) return null try { - const parsed = JSON.parse(str) as StorageValue + const parsed = JSON.parse(str) as StorageValue const state = parsed.state // Hydrate persisted tracks back to full JellifyTrack format @@ -71,11 +58,9 @@ const queueStorage: PersistStorage = { ...parsed, state: { ...state, - queue: (state.queue ?? []).map(fromPersistedTrack), - unShuffledQueue: (state.unShuffledQueue ?? []).map(fromPersistedTrack), - currentTrack: state.currentTrack - ? fromPersistedTrack(state.currentTrack) - : undefined, + queue: state.queue ?? [], + unShuffledQueue: state.unShuffledQueue ?? [], + currentTrack: state.currentTrack, } as unknown as PlayerQueueStore, } } catch (e) { @@ -88,20 +73,14 @@ const queueStorage: PersistStorage = { const state = value.state // Slim down tracks before persisting to prevent storage overflow - const persistedState: PersistedPlayerQueueState = { - shuffled: state.shuffled, - repeatMode: state.repeatMode, - queueRef: state.queueRef, + const persistedState = { + ...state, // Limit queue size to prevent storage overflow - queue: (state.queue ?? []).slice(0, MAX_PERSISTED_QUEUE_SIZE).map(toPersistedTrack), - unShuffledQueue: (state.unShuffledQueue ?? []) - .slice(0, MAX_PERSISTED_QUEUE_SIZE) - .map(toPersistedTrack), - currentTrack: state.currentTrack ? toPersistedTrack(state.currentTrack) : undefined, - currentIndex: state.currentIndex, + queue: (state.queue ?? []).slice(0, MAX_PERSISTED_QUEUE_SIZE), + unShuffledQueue: (state.unShuffledQueue ?? []).slice(0, MAX_PERSISTED_QUEUE_SIZE), } - const toStore: StorageValue = { + const toStore: StorageValue = { ...value, state: persistedState, } @@ -131,19 +110,19 @@ export const usePlayerQueueStore = create()( }), unShuffledQueue: [], - setUnshuffledQueue: (unShuffledQueue: JellifyTrack[]) => + setUnshuffledQueue: (unShuffledQueue: TrackItem[]) => set({ unShuffledQueue, }), queue: [], - setQueue: (queue: JellifyTrack[]) => + setQueue: (queue: TrackItem[]) => set({ queue, }), currentTrack: undefined, - setCurrentTrack: (currentTrack: JellifyTrack | undefined) => + setCurrentTrack: (currentTrack: TrackItem | undefined) => set({ currentTrack, }), diff --git a/src/types/JellifyTrack.ts b/src/types/JellifyTrack.ts index 8f1d8609..bb16052b 100644 --- a/src/types/JellifyTrack.ts +++ b/src/types/JellifyTrack.ts @@ -1,6 +1,10 @@ import { TrackItem } from 'react-native-nitro-player' import { QueuingType } from '../enums/queuing-type' -import { BaseItemDto, MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client/models' +import { + BaseItemDto, + MediaSourceInfo, + NameGuidPair, +} from '@jellyfin/sdk/lib/generated-client/models' export type SourceType = 'stream' | 'download' @@ -18,6 +22,41 @@ export type BaseItemDtoSlimified = Pick< | 'CustomRating' > +/** + * Type-safe representation of extra metadata attached to a track. + * This ensures consistent typing when accessing track extraPayload throughout the app. + * + * Note: Properties that come from the API may be null, so they're typed with null | undefined + * to match the source data. When accessing these values, use optional chaining (?.) and + * nullish coalescing (??) to handle both null and undefined safely. + */ +export type TrackExtraPayload = Record & { + /** List of artist items associated with the track */ + artistItems?: NameGuidPair[] | null | undefined + /** Album information for the track */ + albumItem?: + | { + Id?: string | null | undefined + Album?: string | null | undefined + } + | undefined + /** Playback source type (streaming or downloaded) */ + sourceType?: SourceType | undefined + /** Media source information for detailed codec/quality info */ + mediaSourceInfo?: MediaSourceInfo | undefined + /** Official rating for content (e.g. "G", "PG", "M") */ + officialRating?: string | null | undefined + /** Custom rating applied by server/admin (e.g. "Adults Only") */ + customRating?: string | null | undefined + /** Album ID for looking up album details */ + AlbumId?: string | null | undefined + /** Artist items - accessible by alternative key name */ + ArtistItems?: NameGuidPair[] | null | undefined +} + +/** + * @deprecated Use {@link TrackItem} directly + */ interface JellifyTrack extends TrackItem { description?: string | undefined genre?: string | undefined @@ -39,6 +78,22 @@ interface JellifyTrack extends TrackItem { QueuingType?: QueuingType | undefined } +/** + * Get the extra payload from a track with proper typing. + * This ensures type-safe access to the extraPayload field which comes from react-native-nitro-player. + * + * @param track The track to get the extra payload from + * @returns The properly typed extra payload, or undefined + * + * @example + * const payload = getTrackExtraPayload(currentTrack); + * const artists = payload?.artistItems; + * const albumId = payload?.AlbumId; + */ +export function getTrackExtraPayload(track: TrackItem | undefined): TrackExtraPayload | undefined { + return track?.extraPayload as TrackExtraPayload | undefined +} + /** * A slimmed-down version of JellifyTrack for persistence. * Excludes large fields like mediaSourceInfo and transient data diff --git a/src/utils/mapping/item-to-track.ts b/src/utils/mapping/item-to-track.ts index cd21cb39..68f268c2 100644 --- a/src/utils/mapping/item-to-track.ts +++ b/src/utils/mapping/item-to-track.ts @@ -5,7 +5,7 @@ import { MediaSourceInfo, PlaybackInfoResponse, } from '@jellyfin/sdk/lib/generated-client/models' -import JellifyTrack from '../../types/JellifyTrack' +import JellifyTrack, { TrackExtraPayload } from '../../types/JellifyTrack' import { QueuingType } from '../../enums/queuing-type' import { getImageApi } from '@jellyfin/sdk/lib/utils/api' import { AudioApi } from '@jellyfin/sdk/lib/generated-client/api' @@ -22,6 +22,8 @@ import StreamingQuality from '../../enums/audio-quality' import { getAudioCache } from '../../api/mutations/download/offlineModeUtils' import RNFS from 'react-native-fs' import { getApi } from '../../stores' +import { TrackItem } from 'react-native-nitro-player' +import { formatArtistNames } from '../formatting/artist-names' /** * Ensures a valid session ID is returned. @@ -120,7 +122,7 @@ export function mapDtoToTrack( item: BaseItemDto, deviceProfile: DeviceProfile, queuingType?: QueuingType, -): JellifyTrack { +): TrackItem { const api = getApi()! const downloadedTracks = getAudioCache() @@ -164,16 +166,34 @@ export function mapDtoToTrack( ? { AUTHORIZATION: (api as Api).accessToken } : undefined + // Build extraPayload - omit undefined values to avoid native serialization issues + const extraPayload: TrackExtraPayload = {} + + if (item.ArtistItems) extraPayload.artistItems = item.ArtistItems + if (item.AlbumId) extraPayload.AlbumId = item.AlbumId + if (item.AlbumId || item.Album) { + extraPayload.albumItem = { + ...(item.AlbumId && { Id: item.AlbumId }), + ...(item.Album && { Album: item.Album }), + } + } + if (trackMediaInfo.sourceType) extraPayload.sourceType = trackMediaInfo.sourceType + if (trackMediaInfo.mediaSourceInfo) + extraPayload.mediaSourceInfo = trackMediaInfo.mediaSourceInfo + if (item.OfficialRating) extraPayload.officialRating = item.OfficialRating + if (item.CustomRating) extraPayload.customRating = item.CustomRating + return { ...(headers ? { headers } : {}), - ...trackMediaInfo, id: item.Id, title: item.Name, + artist: formatArtistNames(item.Artists), album: item.Album, - artist: item.Artists?.join(' • '), + duration: trackMediaInfo.duration, + url: trackMediaInfo.url, artwork: trackMediaInfo.artwork, - QueuingType: queuingType ?? QueuingType.DirectlyQueued, - } as JellifyTrack + ...(Object.keys(extraPayload).length > 0 && { extraPayload }), + } as TrackItem } function ensureFileUri(path?: string): string | undefined { diff --git a/src/utils/track-extra-payload.ts b/src/utils/track-extra-payload.ts new file mode 100644 index 00000000..5908d6bb --- /dev/null +++ b/src/utils/track-extra-payload.ts @@ -0,0 +1,117 @@ +/** + * Type-safe utilities for accessing extraPayload data from tracks. + * This module provides helper functions to safely access and type the extraPayload field. + */ + +import { TrackExtraPayload, getTrackExtraPayload } from '../types/JellifyTrack' +import { NameGuidPair, MediaSourceInfo } from '@jellyfin/sdk/lib/generated-client/models' +import { SourceType } from '../types/JellifyTrack' +import { TrackItem } from 'react-native-nitro-player' + +/** + * Get the artist items from a track's extra payload. + * + * @param track The track to get artist items from + * @returns Array of artist items, or undefined if not available + */ +export function getTrackArtists(track: TrackItem | undefined): NameGuidPair[] | undefined { + const payload = getTrackExtraPayload(track) + return (payload?.artistItems ?? payload?.ArtistItems) || undefined +} + +/** + * Get the album ID from a track's extra payload. + * + * @param track The track to get album ID from + * @returns The album ID, or undefined if not available + */ +export function getTrackAlbumId(track: TrackItem | undefined): string | undefined { + const payload = getTrackExtraPayload(track) + return payload?.AlbumId ?? undefined +} + +/** + * Get the album information from a track's extra payload. + * + * @param track The track to get album info from + * @returns Object with album Id and Album name, or undefined if not available + */ +export function getTrackAlbumInfo( + track: TrackItem | undefined, +): { Id?: string; Album?: string } | undefined { + const payload = getTrackExtraPayload(track) + const albumItem = payload?.albumItem + + // Return undefined if no albumItem exists or if it has no useful data + if (!albumItem || (!albumItem.Id && !albumItem.Album)) return undefined + + return { + ...(albumItem.Id && { Id: albumItem.Id }), + ...(albumItem.Album && { Album: albumItem.Album }), + } +} + +/** + * Get the source type from a track's extra payload. + * + * @param track The track to get source type from + * @returns The source type ('stream' or 'download'), or undefined if not available + */ +export function getTrackSourceType(track: TrackItem | undefined): SourceType | undefined { + const payload = getTrackExtraPayload(track) + return payload?.sourceType +} + +/** + * Get the media source information from a track's extra payload. + * + * @param track The track to get media source info from + * @returns The media source info, or undefined if not available + */ +export function getTrackMediaSourceInfo(track: TrackItem | undefined): MediaSourceInfo | undefined { + const payload = getTrackExtraPayload(track) + return payload?.mediaSourceInfo +} + +/** + * Get the official rating from a track's extra payload. + * + * @param track The track to get rating from + * @returns The official rating (e.g. "G", "PG", "M"), or undefined if not available + */ +export function getTrackOfficialRating(track: TrackItem | undefined): string | undefined { + const payload = getTrackExtraPayload(track) + return payload?.officialRating ?? undefined +} + +/** + * Get the custom rating from a track's extra payload. + * + * @param track The track to get custom rating from + * @returns The custom rating, or undefined if not available + */ +export function getTrackCustomRating(track: TrackItem | undefined): string | undefined { + const payload = getTrackExtraPayload(track) + return payload?.customRating ?? undefined +} + +/** + * Get both official and custom ratings from a track's extra payload. + * Prioritizes official rating if available. + * + * @param track The track to get ratings from + * @returns The first available rating (official or custom), or undefined if neither available + */ +export function getTrackRating(track: TrackItem | undefined): string | undefined { + return getTrackOfficialRating(track) ?? getTrackCustomRating(track) +} + +/** + * Get the extra payload with full type safety. + * + * @param track The track to get extra payload from + * @returns The properly typed extra payload, or undefined if track is undefined + */ +export function getTypedExtraPayload(track: TrackItem | undefined): TrackExtraPayload | undefined { + return getTrackExtraPayload(track) +} diff --git a/src/utils/trackDetails.ts b/src/utils/trackDetails.ts index 4b916fb2..1362ac63 100644 --- a/src/utils/trackDetails.ts +++ b/src/utils/trackDetails.ts @@ -1,6 +1,7 @@ -import JellifyTrack from '@/src/types/JellifyTrack' +import { ValueType } from 'react-native-nitro-modules' +import { TrackItem } from 'react-native-nitro-player' -export function isExplicit(nowPlaying: JellifyTrack | undefined) { +export function isExplicit(nowPlaying: TrackItem | undefined) { if (!nowPlaying) return false const ADULT_RATINGS = new Set([ 'R', @@ -48,5 +49,8 @@ export function isExplicit(nowPlaying: JellifyTrack | undefined) { return false } - return isExplicitByRating(nowPlaying?.officialRating || nowPlaying?.customRating) + return isExplicitByRating( + (nowPlaying?.extraPayload?.officialRating as string) || + (nowPlaying?.extraPayload?.customRating as string), + ) }