mirror of
https://github.com/Jellify-Music/App.git
synced 2026-03-17 18:51:24 -05:00
bump nitro player - incorporate extra params
This commit is contained in:
4
bun.lock
4
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=="],
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<NativeStackNavigationProp<PlayerParamList>>()
|
||||
@@ -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 || '' }],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -88,7 +88,10 @@ function PlayerArtwork(): React.JSX.Element {
|
||||
{nowPlaying && (
|
||||
<Animated.View key={`${nowPlaying.id}-item-image`} style={animatedStyle}>
|
||||
<ItemImage
|
||||
item={nowPlaying.item}
|
||||
item={{
|
||||
Name: nowPlaying!.title,
|
||||
Id: nowPlaying!.id,
|
||||
}}
|
||||
testID='player-image-test-id'
|
||||
imageOptions={{ maxWidth: 800, maxHeight: 800 }}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
<TextTicker
|
||||
{...TextTickerConfig}
|
||||
style={{ height: getToken('$9') }}
|
||||
key={`${nowPlaying?.id ?? 'no-track'}-title`}
|
||||
key={`${currentTrack?.id ?? 'no-track'}-title`}
|
||||
>
|
||||
<Text bold fontSize={'$6'} onPress={handleTrackPress}>
|
||||
{trackTitle}
|
||||
@@ -127,12 +120,12 @@ export default function SongInfo({ swipeX }: SongInfoProps = {}): React.JSX.Elem
|
||||
<TextTicker
|
||||
{...TextTickerConfig}
|
||||
style={{ height: getToken('$8') }}
|
||||
key={`${nowPlaying?.id ?? 'no-track'}-artist`}
|
||||
key={`${currentTrack?.id ?? 'no-track'}-artist`}
|
||||
>
|
||||
<Text fontSize={'$6'} color={'$color'} onPress={handleArtistPress}>
|
||||
{nowPlaying?.artist ?? 'Unknown Artist'}
|
||||
{currentTrack?.artist ?? 'Unknown Artist'}
|
||||
</Text>
|
||||
{isExplicit(nowPlaying) && (
|
||||
{isExplicit(currentTrack) && (
|
||||
<XStack alignSelf='center' paddingTop={5.3} paddingLeft='$1'>
|
||||
<Icon name='alpha-e-box-outline' color={'$color'} xsmall />
|
||||
</XStack>
|
||||
@@ -144,22 +137,34 @@ export default function SongInfo({ swipeX }: SongInfoProps = {}): React.JSX.Elem
|
||||
<Icon
|
||||
name='dots-horizontal-circle-outline'
|
||||
onPress={() =>
|
||||
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 && <FavoriteButton item={nowPlaying.item} />}
|
||||
{currentTrack && currentTrack.extraPayload && (
|
||||
<FavoriteButton
|
||||
item={{
|
||||
Id: currentTrack.id,
|
||||
Name: currentTrack.title,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</XStack>
|
||||
</XStack>
|
||||
)
|
||||
|
||||
@@ -95,17 +95,20 @@ export default function Miniplayer(): React.JSX.Element | null {
|
||||
<MiniPlayerProgress />
|
||||
<XStack alignItems='center' padding={'$2'}>
|
||||
<YStack justify='center' alignItems='center'>
|
||||
{/* <Animated.View
|
||||
<Animated.View
|
||||
entering={FadeIn.easing(Easing.in(Easing.ease))}
|
||||
exiting={FadeOut.easing(Easing.out(Easing.ease))}
|
||||
>
|
||||
<ItemImage
|
||||
item={nowPlaying!.item}
|
||||
item={{
|
||||
Name: nowPlaying!.title,
|
||||
Id: nowPlaying!.id,
|
||||
}}
|
||||
width={'$11'}
|
||||
height={'$11'}
|
||||
imageOptions={{ maxWidth: 120, maxHeight: 120 }}
|
||||
/>
|
||||
</Animated.View> */}
|
||||
</Animated.View>
|
||||
</YStack>
|
||||
|
||||
<YStack
|
||||
|
||||
@@ -7,7 +7,7 @@ import JellifyTrack, {
|
||||
import { createVersionedMmkvStorage } from '../../constants/versioned-storage'
|
||||
import { create } from 'zustand'
|
||||
import { devtools, persist, PersistStorage, StorageValue } from 'zustand/middleware'
|
||||
import { RepeatMode, useNowPlaying } from 'react-native-nitro-player'
|
||||
import { RepeatMode, TrackItem, useNowPlaying } from 'react-native-nitro-player'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
/**
|
||||
@@ -26,32 +26,19 @@ type PlayerQueueStore = {
|
||||
queueRef: Queue
|
||||
setQueueRef: (queueRef: Queue) => 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<PlayerQueueStore> = {
|
||||
if (!str) return null
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(str) as StorageValue<PersistedPlayerQueueState>
|
||||
const parsed = JSON.parse(str) as StorageValue<PlayerQueueStore>
|
||||
const state = parsed.state
|
||||
|
||||
// Hydrate persisted tracks back to full JellifyTrack format
|
||||
@@ -71,11 +58,9 @@ const queueStorage: PersistStorage<PlayerQueueStore> = {
|
||||
...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<PlayerQueueStore> = {
|
||||
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<PersistedPlayerQueueState> = {
|
||||
const toStore: StorageValue<PlayerQueueStore> = {
|
||||
...value,
|
||||
state: persistedState,
|
||||
}
|
||||
@@ -131,19 +110,19 @@ export const usePlayerQueueStore = create<PlayerQueueStore>()(
|
||||
}),
|
||||
|
||||
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,
|
||||
}),
|
||||
|
||||
@@ -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<string, unknown> & {
|
||||
/** 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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
117
src/utils/track-extra-payload.ts
Normal file
117
src/utils/track-extra-payload.ts
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user