Player Backend Improvements (#291)

Separating Queuing and Player logic, please report bugs if you experience playback issues or queue irregularities

Fetching additional track metadata for use in later features, utilizing transcoding URLs reported by Jellyfin

Disable NowPlaying in CarPlay on startup - this should be navigable yet in the CarPlay interface
This commit is contained in:
Violet Caulfield
2025-04-26 13:49:04 -05:00
committed by GitHub
parent e0b8194ca5
commit 87393f1f08
25 changed files with 636 additions and 706 deletions

View File

@@ -10,7 +10,7 @@ export async function fetchMediaInfo(itemId: string): Promise<PlaybackInfoRespon
userId: Client.user?.id,
})
.then(({ data }) => {
console.debug(data)
console.debug('Received media info response')
resolve(data)
})
.catch((error) => {

View File

@@ -1,4 +1,3 @@
import { usePlayerContext } from '../../../player/provider'
import { StackParamList } from '../../../components/types'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
@@ -9,6 +8,9 @@ import BlurhashedImage from './blurhashed-image'
import Icon from '../helpers/icon'
import { QueuingType } from '../../../enums/queuing-type'
import { RunTimeTicks } from '../helpers/time-codes'
import { useQueueContext } from '../../../player/queue-provider'
import { usePlayerContext } from '../../../player/provider'
import { State } from 'react-native-track-player'
export default function Item({
item,
@@ -19,7 +21,8 @@ export default function Item({
queueName: string
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
const { usePlayNewQueue } = usePlayerContext()
const { useStartPlayback } = usePlayerContext()
const { useLoadNewQueue } = useQueueContext()
const { width } = useSafeAreaFrame()
@@ -48,12 +51,20 @@ export default function Item({
}
case 'Audio': {
usePlayNewQueue.mutate({
track: item,
tracklist: [item],
queue: 'Search',
queuingType: QueuingType.FromSelection,
})
useLoadNewQueue.mutate(
{
track: item,
tracklist: [item],
index: 0,
queue: 'Search',
queuingType: QueuingType.FromSelection,
},
{
onSuccess: () => {
useStartPlayback.mutate()
},
},
)
break
}
}

View File

@@ -18,12 +18,14 @@ import { useNetworkContext } from '../../../components/Network/provider'
import { useQuery } from '@tanstack/react-query'
import { QueryKeys } from '../../../enums/query-keys'
import { fetchMediaInfo } from '../../../api/queries/functions/media'
import { useQueueContext } from '../../../player/queue-provider'
import { State } from 'react-native-track-player'
interface TrackProps {
track: BaseItemDto
navigation: NativeStackNavigationProp<StackParamList>
tracklist?: BaseItemDto[] | undefined
index?: number | undefined
index: number
queue: Queue
showArtwork?: boolean | undefined
onPress?: () => void | undefined
@@ -51,7 +53,8 @@ export default function Track({
onRemove,
}: TrackProps): React.JSX.Element {
const theme = useTheme()
const { nowPlaying, playQueue, usePlayNewQueue } = usePlayerContext()
const { nowPlaying, useStartPlayback } = usePlayerContext()
const { playQueue, useLoadNewQueue } = useQueueContext()
const { downloadedTracks, networkStatus } = useNetworkContext()
const isPlaying = nowPlaying?.item.Id === track.Id
@@ -76,13 +79,20 @@ export default function Track({
if (onPress) {
onPress()
} else {
usePlayNewQueue.mutate({
track,
index,
tracklist: tracklist ?? playQueue.map((track) => track.item),
queue,
queuingType: QueuingType.FromSelection,
})
useLoadNewQueue.mutate(
{
track,
index,
tracklist: tracklist ?? playQueue.map((track) => track.item),
queue,
queuingType: QueuingType.FromSelection,
},
{
onSuccess: () => {
useStartPlayback.mutate()
},
},
)
}
}}
onLongPress={

View File

@@ -6,9 +6,10 @@ import HorizontalCardList from '../../../components/Global/components/horizontal
import { ItemCard } from '../../../components/Global/components/item-card'
import { QueuingType } from '../../../enums/queuing-type'
import { trigger } from 'react-native-haptic-feedback'
import { usePlayerContext } from '../../../player/provider'
import { H2 } from '../../../components/Global/helpers/text'
import Icon from '../../../components/Global/helpers/icon'
import { useQueueContext } from '../../../player/queue-provider'
import { usePlayerContext } from '../../../player/provider'
export default function FrequentlyPlayedTracks({
navigation,
@@ -17,7 +18,8 @@ export default function FrequentlyPlayedTracks({
}): React.JSX.Element {
const { frequentlyPlayed } = useHomeContext()
const { usePlayNewQueue } = usePlayerContext()
const { useStartPlayback } = usePlayerContext()
const { useLoadNewQueue } = useQueueContext()
return (
<View>
@@ -48,13 +50,20 @@ export default function FrequentlyPlayedTracks({
subCaption={`${track.Artists?.join(', ')}`}
squared
onPress={() => {
usePlayNewQueue.mutate({
track,
index,
tracklist: frequentlyPlayed ?? [track],
queue: 'On Repeat',
queuingType: QueuingType.FromSelection,
})
useLoadNewQueue.mutate(
{
track,
index,
tracklist: frequentlyPlayed ?? [track],
queue: 'On Repeat',
queuingType: QueuingType.FromSelection,
},
{
onSuccess: () => {
useStartPlayback.mutate()
},
},
)
}}
onLongPress={() => {
trigger('impactMedium')

View File

@@ -9,15 +9,16 @@ import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { trigger } from 'react-native-haptic-feedback'
import { QueuingType } from '../../../enums/queuing-type'
import HorizontalCardList from '../../../components/Global/components/horizontal-list'
import { QueryKeys } from '../../../enums/query-keys'
import Icon from '../../../components/Global/helpers/icon'
import { useQueueContext } from '../../../player/queue-provider'
export default function RecentlyPlayed({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
const { nowPlaying, usePlayNewQueue } = usePlayerContext()
const { nowPlaying, useStartPlayback } = usePlayerContext()
const { useLoadNewQueue } = useQueueContext()
const { recentTracks } = useHomeContext()
return useMemo(() => {
@@ -49,13 +50,20 @@ export default function RecentlyPlayed({
squared
item={recentlyPlayedTrack}
onPress={() => {
usePlayNewQueue.mutate({
track: recentlyPlayedTrack,
index: index,
tracklist: recentTracks ?? [recentlyPlayedTrack],
queue: 'Recently Played',
queuingType: QueuingType.FromSelection,
})
useLoadNewQueue.mutate(
{
track: recentlyPlayedTrack,
index: index,
tracklist: recentTracks ?? [recentlyPlayedTrack],
queue: 'Recently Played',
queuingType: QueuingType.FromSelection,
},
{
onSuccess: () => {
useStartPlayback.mutate()
},
},
)
}}
onLongPress={() => {
trigger('impactMedium')

View File

@@ -6,11 +6,9 @@ import { AlbumScreen } from '../Album'
import { PlaylistScreen } from '../Playlist/screens'
import { ProvidedHome } from './component'
import DetailsScreen from '../ItemDetail/screen'
import AddPlaylist from '../Library/components/add-playlist'
import ArtistsScreen from '../Artists/screen'
import TracksScreen from '../Tracks/screen'
import { ArtistScreen } from '../Artist'
import { OfflineList } from '../offlineList'
const Stack = createNativeStackNavigator<StackParamList>()

View File

@@ -1,4 +1,3 @@
import { usePlayerContext } from '../../../player/provider'
import { StackParamList } from '../../../components/types'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
@@ -33,6 +32,7 @@ import { Image } from 'expo-image'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import Client from '../../../api/client'
import { useNetworkContext } from '../../../components/Network/provider'
import { useQueueContext } from '../../../player/queue-provider'
interface TrackOptionsProps {
track: BaseItemDto
@@ -67,7 +67,7 @@ export default function TrackOptions({
queryFn: () => fetchUserPlaylists(),
})
const { useAddToQueue } = usePlayerContext()
const { useAddToQueue } = useQueueContext()
const { width } = useSafeAreaFrame()

View File

@@ -5,11 +5,14 @@ import Icon from '../../../components/Global/helpers/icon'
import { getProgress, seekBy, skipToNext } from 'react-native-track-player/lib/src/trackPlayer'
import { usePlayerContext } from '../../../player/provider'
import { useSafeAreaFrame } from 'react-native-safe-area-context'
import { useQueueContext } from '../../../player/queue-provider'
export default function Controls(): React.JSX.Element {
const { width } = useSafeAreaFrame()
const { usePrevious, useSeekTo } = usePlayerContext()
const { useSeekTo } = usePlayerContext()
const { usePrevious } = useQueueContext()
return (
<XStack alignItems='center' justifyContent='space-evenly' marginVertical={'$2'}>
@@ -22,13 +25,7 @@ export default function Controls(): React.JSX.Element {
<Icon
color={getToken('$color.amethyst')}
name='skip-previous'
onPress={async () => {
const progress = await getProgress()
if (progress.position < 3) usePrevious.mutate()
else {
useSeekTo.mutate(0)
}
}}
onPress={() => usePrevious.mutate()}
large
/>

View File

@@ -9,12 +9,14 @@ import { usePlayerContext } from '../../../player/provider'
import { RunTimeSeconds } from '../../../components/Global/helpers/time-codes'
import { UPDATE_INTERVAL } from '../../../player/config'
import { ProgressMultiplier } from '../component.config'
import { useSharedValue } from 'react-native-reanimated'
import { useQueueContext } from '../../../player/queue-provider'
const scrubGesture = Gesture.Pan()
export default function Scrubber(): React.JSX.Element {
const { useSeekTo, useSkip, usePrevious } = usePlayerContext()
const { useSeekTo } = usePlayerContext()
const { useSkip, usePrevious } = useQueueContext()
const { width } = useSafeAreaFrame()

View File

@@ -11,6 +11,7 @@ import { TextTickerConfig } from './component.config'
import { Image } from 'expo-image'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import Client from '../../api/client'
import { useQueueContext } from '../../player/queue-provider'
export function Miniplayer({
navigation,
@@ -19,7 +20,8 @@ export function Miniplayer({
}): React.JSX.Element {
const theme = useTheme()
const { nowPlaying, useSkip } = usePlayerContext()
const { nowPlaying } = usePlayerContext()
const { useSkip } = useQueueContext()
return (
<View

View File

@@ -15,13 +15,16 @@ import Controls from '../helpers/controls'
import { Image } from 'expo-image'
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
import Client from '../../../api/client'
import { useQueueContext } from '../../../player/queue-provider'
export default function PlayerScreen({
navigation,
}: {
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
const { nowPlayingIsFavorite, setNowPlayingIsFavorite, nowPlaying, queue } = usePlayerContext()
const { nowPlayingIsFavorite, setNowPlayingIsFavorite, nowPlaying } = usePlayerContext()
const { queueRef } = useQueueContext()
const { width } = useSafeAreaFrame()
@@ -57,9 +60,9 @@ export default function PlayerScreen({
>
{
// If the Queue is a BaseItemDto, display the name of it
typeof queue === 'object'
? (queue as BaseItemDto).Name ?? 'Untitled'
: queue
typeof queueRef === 'object'
? queueRef.Name ?? 'Untitled'
: queueRef
}
</Text>
</YStack>
@@ -91,7 +94,7 @@ export default function PlayerScreen({
</XStack>
</>
)
}, [nowPlaying, queue])}
}, [nowPlaying, queueRef])}
<XStack marginHorizontal={20} paddingVertical={5}>
{/** Memoize TextTickers otherwise they won't animate due to the progress being updated in the PlayerContext */}

View File

@@ -7,6 +7,8 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context'
import DraggableFlatList from 'react-native-draggable-flatlist'
import { trigger } from 'react-native-haptic-feedback'
import { Separator } from 'tamagui'
import { useQueueContext } from '../../../player/queue-provider'
import Animated from 'react-native-reanimated'
export default function Queue({
navigation,
@@ -14,15 +16,16 @@ export default function Queue({
navigation: NativeStackNavigationProp<StackParamList>
}): React.JSX.Element {
const { width } = useSafeAreaFrame()
const { nowPlaying } = usePlayerContext()
const {
playQueue,
queue,
useClearQueue,
queueRef,
useRemoveUpcomingTracks,
useRemoveFromQueue,
useReorderQueue,
useSkip,
nowPlaying,
} = usePlayerContext()
} = useQueueContext()
navigation.setOptions({
headerRight: () => {
@@ -30,7 +33,7 @@ export default function Queue({
<Icon
name='notification-clear-all'
onPress={() => {
useClearQueue.mutate()
useRemoveUpcomingTracks.mutate()
}}
/>
)
@@ -42,50 +45,52 @@ export default function Queue({
)
return (
<DraggableFlatList
contentInsetAdjustmentBehavior='automatic'
data={playQueue}
dragHitSlop={{ left: -50 }} // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336
extraData={nowPlaying}
// enableLayoutAnimationExperimental
getItemLayout={(data, index) => ({
length: width / 9,
offset: (width / 9) * index,
index,
})}
initialScrollIndex={scrollIndex !== -1 ? scrollIndex : 0}
ItemSeparatorComponent={() => <Separator />}
// itemEnteringAnimation={FadeIn}
// itemExitingAnimation={FadeOut}
// itemLayoutAnimation={SequencedTransition}
keyExtractor={({ item }, index) => {
return `${index}-${item.Id}`
}}
numColumns={1}
onDragEnd={({ data, from, to }) => {
useReorderQueue.mutate({ newOrder: data, from, to })
}}
renderItem={({ item: queueItem, getIndex, drag, isActive }) => (
<Track
queue={queue}
navigation={navigation}
track={queueItem.item}
index={getIndex()}
showArtwork
onPress={() => {
useSkip.mutate(getIndex())
}}
onLongPress={() => {
trigger('impactLight')
drag()
}}
isNested
showRemove
onRemove={() => {
if (getIndex()) useRemoveFromQueue.mutate(getIndex()!)
}}
/>
)}
/>
<Animated.View>
<DraggableFlatList
contentInsetAdjustmentBehavior='automatic'
data={playQueue}
dragHitSlop={{ left: -50 }} // https://github.com/computerjazz/react-native-draggable-flatlist/issues/336
extraData={nowPlaying}
// enableLayoutAnimationExperimental
getItemLayout={(data, index) => ({
length: width / 9,
offset: (width / 9) * index,
index,
})}
initialScrollIndex={scrollIndex !== -1 ? scrollIndex : 0}
ItemSeparatorComponent={() => <Separator />}
// itemEnteringAnimation={FadeIn}
// itemExitingAnimation={FadeOut}
// itemLayoutAnimation={SequencedTransition}
keyExtractor={({ item }, index) => {
return `${index}-${item.Id}`
}}
numColumns={1}
onDragEnd={({ data, from, to }) => {
useReorderQueue.mutate({ newOrder: data, from, to })
}}
renderItem={({ item: queueItem, getIndex, drag, isActive }) => (
<Track
queue={queueRef}
navigation={navigation}
track={queueItem.item}
index={getIndex() ?? 0}
showArtwork
onPress={() => {
useSkip.mutate(getIndex() ?? 0)
}}
onLongPress={() => {
trigger('impactLight')
drag()
}}
isNested
showRemove
onRemove={() => {
if (getIndex()) useRemoveFromQueue.mutate(getIndex()!)
}}
/>
)}
/>
</Animated.View>
)
}

View File

@@ -3,7 +3,7 @@ import Button from '../../Global/helpers/button'
import Client from '../../../api/client'
import { useJellifyContext } from '../../../components/provider'
import TrackPlayer from 'react-native-track-player'
import { StackParamList } from '@/components/types'
import { StackParamList } from '../../../components/types'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import { useNavigation } from '@react-navigation/native'
@@ -17,7 +17,6 @@ export default function SignOut(): React.JSX.Element {
setLoggedIn(false)
Client.signOut()
TrackPlayer.reset()
// navigation.navigate('Offline')
}}
>
Sign Out

View File

@@ -10,6 +10,7 @@ import { JellifyProvider, useJellifyContext } from './provider'
import { ToastProvider } from '@tamagui/toast'
import { JellifyUserDataProvider } from './user-data-provider'
import { NetworkContextProvider } from './Network/provider'
import { QueueProvider } from '../player/queue-provider'
export default function Jellify(): React.JSX.Element {
return (
@@ -30,9 +31,11 @@ function App(): React.JSX.Element {
return loggedIn ? (
<JellifyUserDataProvider>
<NetworkContextProvider>
<PlayerProvider>
<Navigation />
</PlayerProvider>
<QueueProvider>
<PlayerProvider>
<Navigation />
</PlayerProvider>
</QueueProvider>
</NetworkContextProvider>
</JellifyUserDataProvider>
) : (

View File

@@ -3,8 +3,6 @@ import Player from './Player/stack'
import { Tabs } from './tabs'
import { StackParamList } from './types'
import { useTheme } from 'tamagui'
import DetailsScreen from './ItemDetail/screen'
import { OfflineList } from './offlineList'
export default function Navigation(): React.JSX.Element {
const RootStack = createNativeStackNavigator<StackParamList>()
@@ -29,14 +27,6 @@ export default function Navigation(): React.JSX.Element {
presentation: 'modal',
}}
/>
<RootStack.Screen
name='Offline'
component={OfflineList}
options={{
headerShown: false,
presentation: 'modal',
}}
/>
</RootStack.Navigator>
)
}

View File

@@ -1,68 +0,0 @@
import { JellifyTrack } from '@/types/JellifyTrack'
import React from 'react'
import { FlatList, Pressable } from 'react-native'
import Animated, { FadeIn, FadeOut, Layout } from 'react-native-reanimated'
import { Image, Text, View, XStack, YStack } from 'tamagui'
import { getAudioCache } from '../Network/offlineModeUtils'
import { usePlayerContext } from '../../player/provider'
import { useNavigation } from '@react-navigation/native'
interface Props {
tracks: JellifyTrack[]
onPress: (track: JellifyTrack) => void
}
export function OfflineList() {
const tracks = getAudioCache()
const navigation = useNavigation()
const { usePlayNewQueueOffline } = usePlayerContext()
const onPress = (track: JellifyTrack) => {
console.log('onPress', track)
usePlayNewQueueOffline.mutate({ trackListOffline: track })
navigation.navigate('Player')
}
const renderItem = ({ item }: { item: JellifyTrack }) => (
<Animated.View
entering={FadeIn.duration(300)}
exiting={FadeOut}
layout={Layout.springify()}
style={{
overflow: 'hidden',
marginVertical: 6,
borderRadius: 16,
backgroundColor: '#1c1c1e',
}}
>
<Pressable onPress={() => onPress(item)}>
<XStack padding={12} gap={12} alignItems='center'>
<Image
source={{ uri: item.artwork }}
style={{ width: 64, height: 64, borderRadius: 12 }}
/>
<YStack flex={1}>
<Text fontWeight='700' color='#fff' numberOfLines={1}>
{item.title || 'Unknown Title'}
</Text>
<Text color='#bbb' numberOfLines={1}>
{item.artist || 'Unknown Artist'}
</Text>
</YStack>
</XStack>
</Pressable>
</Animated.View>
)
return (
<FlatList
data={tracks}
keyExtractor={(item) => item.item.Id as string}
renderItem={renderItem}
contentContainerStyle={{
padding: 16,
paddingBottom: 100,
}}
showsVerticalScrollIndicator={false}
/>
)
}

View File

@@ -29,7 +29,6 @@ const JellifyContextInitializer = () => {
if (loggedIn) {
CarPlay.setRootTemplate(CarPlayNavigation)
CarPlay.pushTemplate(CarPlayNowPlaying)
if (Platform.OS === 'ios') {
CarPlay.enableNowPlaying(true) // https://github.com/birkir/react-native-carplay/issues/185

View File

@@ -57,7 +57,6 @@
CF6B19F92D55121E002464CB /* icon_tinted_20pt_3x.png in Resources */ = {isa = PBXBuildFile; fileRef = CF6B19EB2D55121E002464CB /* icon_tinted_20pt_3x.png */; };
CF6B19FA2D55121E002464CB /* icon_tinted_60pt_3x.png in Resources */ = {isa = PBXBuildFile; fileRef = CF6B19F12D55121E002464CB /* icon_tinted_60pt_3x.png */; };
CF6B19FB2D55121E002464CB /* icon_tinted_40pt_2x.png in Resources */ = {isa = PBXBuildFile; fileRef = CF6B19EE2D55121E002464CB /* icon_tinted_40pt_2x.png */; };
CF71790B2CBC486C0021BCA3 /* dummy-rntp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */; };
CF98CA472D3E99E0003D88B7 /* CarScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA452D3E99DF003D88B7 /* CarScene.swift */; };
CF98CA482D3E99E0003D88B7 /* PhoneScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA462D3E99DF003D88B7 /* PhoneScene.swift */; };
CF98CA492D3E99E0003D88B7 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF98CA442D3E99DF003D88B7 /* AppDelegate.swift */; };
@@ -148,7 +147,6 @@
CF6B19F12D55121E002464CB /* icon_tinted_60pt_3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_tinted_60pt_3x.png; sourceTree = "<group>"; };
CF6B19F22D55121E002464CB /* icon_tinted_original.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = icon_tinted_original.png; sourceTree = "<group>"; };
CF7179092CBC486C0021BCA3 /* Jellify-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Jellify-Bridging-Header.h"; sourceTree = "<group>"; };
CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "dummy-rntp.swift"; sourceTree = "<group>"; };
CF98CA442D3E99DF003D88B7 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
CF98CA452D3E99DF003D88B7 /* CarScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarScene.swift; sourceTree = "<group>"; };
CF98CA462D3E99DF003D88B7 /* PhoneScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneScene.swift; sourceTree = "<group>"; };
@@ -204,7 +202,6 @@
81AB9BB72411601600AC10FF /* LaunchScreen.storyboard */,
13B07FB81A68108700A75B9A /* PrivacyInfo.xcprivacy */,
F757EB73303E0AC21EF34F64 /* PrivacyInfo.xcprivacy */,
CF71790A2CBC486C0021BCA3 /* dummy-rntp.swift */,
CF7179092CBC486C0021BCA3 /* Jellify-Bridging-Header.h */,
);
name = Jellify;
@@ -522,10 +519,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-resources.sh\"\n";
@@ -539,10 +540,14 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Jellify/Pods-Jellify-frameworks.sh\"\n";
@@ -607,7 +612,6 @@
CF98CA472D3E99E0003D88B7 /* CarScene.swift in Sources */,
CF98CA482D3E99E0003D88B7 /* PhoneScene.swift in Sources */,
CF98CA492D3E99E0003D88B7 /* AppDelegate.swift in Sources */,
CF71790B2CBC486C0021BCA3 /* dummy-rntp.swift in Sources */,
66BC9C5D1B536CD0799EEC89 /* ExpoModulesProvider.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
@@ -843,10 +847,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG";
@@ -932,10 +933,7 @@
"-DFOLLY_CFG_NO_COROUTINES=1",
"-DFOLLY_HAVE_CLOCK_GETTIME=1",
);
OTHER_LDFLAGS = (
"$(inherited)",
" ",
);
OTHER_LDFLAGS = "$(inherited) ";
REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;

View File

@@ -1,8 +0,0 @@
//
// dummy-rntp.swift
// Jellify
//
// Created by Violet Caulfield on 10/13/24.
//
import Foundation

View File

@@ -49,7 +49,6 @@
"invert-color": "^2.0.0",
"jest-expo": "^52.0.6",
"lodash": "^4.17.21",
"npm-bundle": "^3.0.3",
"patch-package": "^8.0.0",
"react": "18.3.1",
"react-freeze": "^1.0.4",

View File

@@ -23,7 +23,7 @@ export function buildNewQueue(
return newQueue
}
export function networkStatusCheck(
export function filterTracksOnNetworkStatus(
networkStatus: networkStatusTypes | undefined,
queuedItems: BaseItemDto[],
downloadedTracks: JellifyDownload[],

View File

@@ -9,7 +9,6 @@ export interface QueueMutation {
tracklist: BaseItemDto[]
queue: Queue
queuingType?: QueuingType | undefined
trackListOffline?: JellifyTrack
}
export interface AddToQueueMutation {

View File

@@ -2,7 +2,6 @@ import { createContext, ReactNode, SetStateAction, useContext, useEffect, useSta
import { JellifyTrack } from '../types/JellifyTrack'
import { storage } from '../constants/storage'
import { MMKVStorageKeys } from '../enums/mmkv-storage-keys'
import { findPlayNextIndexStart, findPlayQueueIndexStart } from './helpers/index'
import TrackPlayer, {
Event,
State,
@@ -13,58 +12,27 @@ import { isEqual, isUndefined } from 'lodash'
import { handlePlaybackProgressUpdated, handlePlaybackState } from './handlers'
import { useUpdateOptions } from '../player/hooks'
import { useMutation, UseMutationResult } from '@tanstack/react-query'
import { mapDtoToTrack } from '../helpers/mappings'
import { QueuingType } from '../enums/queuing-type'
import { trigger } from 'react-native-haptic-feedback'
import {
getActiveTrackIndex,
getQueue,
pause,
seekTo,
skip,
skipToNext,
skipToPrevious,
} from 'react-native-track-player/lib/src/trackPlayer'
import { pause, seekTo } from 'react-native-track-player/lib/src/trackPlayer'
import { convertRunTimeTicksToSeconds } from '../helpers/runtimeticks'
import Client from '../api/client'
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces'
import { Section } from '../components/Player/types'
import { Queue } from './types/queue-item'
import * as Burnt from 'burnt'
import { markItemPlayed } from '../api/mutations/functions/item'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { getPlaystateApi } from '@jellyfin/sdk/lib/utils/api'
import { SKIP_TO_PREVIOUS_THRESHOLD } from './config'
import { useNetworkContext } from '../components/Network/provider'
import { networkStatusCheck } from './helpers/queue'
import { QueueProvider } from './queue-provider'
import { useQueueContext } from './queue-provider'
interface PlayerContext {
initialized: boolean
nowPlayingIsFavorite: boolean
setNowPlayingIsFavorite: React.Dispatch<SetStateAction<boolean>>
nowPlaying: JellifyTrack | undefined
playQueue: JellifyTrack[]
queue: Queue
getQueueSectionData: () => Section[]
useAddToQueue: UseMutationResult<void, Error, AddToQueueMutation, unknown>
useClearQueue: UseMutationResult<void, Error, void, unknown>
useRemoveFromQueue: UseMutationResult<void, Error, number, unknown>
useReorderQueue: UseMutationResult<void, Error, QueueOrderMutation, unknown>
useTogglePlayback: UseMutationResult<void, Error, number | undefined, unknown>
useStartPlayback: UseMutationResult<void, Error, void, unknown>
useTogglePlayback: UseMutationResult<void, Error, void, unknown>
useSeekTo: UseMutationResult<void, Error, number, unknown>
useSkip: UseMutationResult<void, Error, number | undefined, unknown>
usePrevious: UseMutationResult<void, Error, void, unknown>
usePlayNewQueue: UseMutationResult<void, Error, QueueMutation, unknown>
playbackState: State | undefined
setNowPlaying: (track: JellifyTrack) => void
}
const PlayerContextInitializer = () => {
const nowPlayingJson = storage.getString(MMKVStorageKeys.NowPlaying)
const playQueueJson = storage.getString(MMKVStorageKeys.PlayQueue)
const queueJson = storage.getString(MMKVStorageKeys.Queue)
const playStateApi = getPlaystateApi(Client.api!)
@@ -76,146 +44,27 @@ const PlayerContextInitializer = () => {
nowPlayingJson ? JSON.parse(nowPlayingJson) : undefined,
)
const [isSkipping, setIsSkipping] = useState<boolean>(false)
const { playQueue, currentIndex } = useQueueContext()
const [playQueue, setPlayQueue] = useState<JellifyTrack[]>(
playQueueJson ? JSON.parse(playQueueJson) : [],
)
const [queue, setQueue] = useState<Queue>(queueJson ? JSON.parse(queueJson) : 'Queue')
//#endregion State
//#region Functions
const play = async (index?: number | undefined) => {
if (index && index > 0) {
TrackPlayer.skip(index)
}
TrackPlayer.play()
const play = async () => {
await TrackPlayer.play()
}
const getQueueSectionData: () => Section[] = () => {
return Object.keys(QueuingType).map((type) => {
return {
title: type,
data: playQueue.filter((track) => track.QueuingType === type),
} as Section
})
}
/**
* Takes a {@link BaseItemDto} of a track on Jellyfin, and updates it's
* position in the {@link queue}
*
*
* @param track The Jellyfin track object to update and replace in the queue
*/
const replaceQueueItem: (track: BaseItemDto) => Promise<void> = async (track: BaseItemDto) => {
const queue = (await TrackPlayer.getQueue()) as JellifyTrack[]
const queueItemIndex = queue.findIndex((queuedTrack) => queuedTrack.item.Id === track.Id!)
// Update queued item at index if found, else silently do nothing
if (queueItemIndex !== -1) {
const queueItem = queue[queueItemIndex]
TrackPlayer.remove([queueItemIndex]).then(() => {
TrackPlayer.add(
mapDtoToTrack(track, downloadedTracks ?? [], queueItem.QueuingType),
queueItemIndex,
)
})
}
}
const resetQueue = async (hideMiniplayer?: boolean | undefined) => {
console.debug('Clearing queue')
await TrackPlayer.setQueue([])
setPlayQueue([])
}
const addToQueue = async (tracks: JellifyTrack[]) => {
const insertIndex = await findPlayQueueIndexStart(playQueue)
console.debug(`Adding ${tracks.length} to queue at index ${insertIndex}`)
await TrackPlayer.add(tracks, insertIndex)
setPlayQueue((await getQueue()) as JellifyTrack[])
console.debug(`Queue has ${playQueue.length} tracks`)
}
const addToNext = async (tracks: JellifyTrack[]) => {
const insertIndex = await findPlayNextIndexStart(playQueue)
console.debug(`Adding ${tracks.length} to queue at index ${insertIndex}`)
await TrackPlayer.add(tracks, insertIndex)
setPlayQueue((await getQueue()) as JellifyTrack[])
}
//#endregion Functions
//#region Hooks
const useAddToQueue = useMutation({
mutationFn: async (mutation: AddToQueueMutation) => {
trigger('impactLight')
if (mutation.queuingType === QueuingType.PlayingNext)
return addToNext([
mapDtoToTrack(mutation.track, downloadedTracks ?? [], mutation.queuingType),
])
else
return addToQueue([
mapDtoToTrack(mutation.track, downloadedTracks ?? [], mutation.queuingType),
])
},
onSuccess: (data, { queuingType }) => {
trigger('notificationSuccess')
Burnt.alert({
title: queuingType === QueuingType.PlayingNext ? 'Playing next' : 'Added to queue',
duration: 1,
preset: 'done',
})
},
onError: () => {
trigger('notificationError')
},
})
const useRemoveFromQueue = useMutation({
mutationFn: async (index: number) => {
trigger('impactMedium')
await TrackPlayer.remove([index])
setPlayQueue((await TrackPlayer.getQueue()) as JellifyTrack[])
},
})
const useClearQueue = useMutation({
mutationFn: async () => {
trigger('effectDoubleClick')
await TrackPlayer.removeUpcomingTracks()
setPlayQueue((await getQueue()) as JellifyTrack[])
},
})
const useReorderQueue = useMutation({
mutationFn: async (mutation: QueueOrderMutation) => {
setPlayQueue(mutation.newOrder)
await TrackPlayer.move(mutation.from, mutation.to)
},
const useStartPlayback = useMutation({
mutationFn: play,
})
const useTogglePlayback = useMutation({
mutationFn: (index?: number | undefined) => {
mutationFn: () => {
trigger('impactMedium')
if (playbackState === State.Playing) return pause()
else return play(index)
else return play()
},
})
@@ -231,102 +80,6 @@ const PlayerContextInitializer = () => {
})
},
})
const useSkip = useMutation({
mutationFn: async (index?: number | undefined) => {
trigger('impactMedium')
// Handle if this is the last track in the queue
if (playQueue.length - 1 === (await getActiveTrackIndex())) return
else {
if (!isUndefined(index)) {
setIsSkipping(true)
setNowPlaying(playQueue[index])
await skip(index)
setIsSkipping(false)
} else {
const nowPlayingIndex = playQueue.findIndex(
(track) => track.item.Id === nowPlaying!.item.Id,
)
setNowPlaying(playQueue[nowPlayingIndex + 1])
await skipToNext()
}
}
},
})
const usePrevious = useMutation({
mutationFn: async () => {
trigger('impactMedium')
const nowPlayingIndex = playQueue.findIndex(
(track) => track.item.Id === nowPlaying!.item.Id,
)
const { position } = await TrackPlayer.getProgress()
if (nowPlayingIndex > 0 && position < SKIP_TO_PREVIOUS_THRESHOLD) {
setNowPlaying(playQueue[nowPlayingIndex - 1])
await skipToPrevious()
} else await seekTo(0)
},
})
/**
* A mutation hook that adds tracks to the play queue
*
* Respects the network status of the app - if we are
* online, tracks are always added to the queue for streaming
* - if we are *offline* then only tracks that are downloaded
* will be added to the queue
*/
const usePlayNewQueue = useMutation({
mutationFn: async (mutation: QueueMutation) => {
trigger('effectDoubleClick')
setIsSkipping(true)
// Optimistically set now playing
setNowPlaying(
mapDtoToTrack(
mutation.tracklist[mutation.index ?? 0],
downloadedTracks ?? [],
QueuingType.FromSelection,
),
)
await resetQueue(false)
const queueItems = networkStatusCheck(
networkStatus,
mutation.tracklist,
downloadedTracks ?? [],
)
console.debug(`Adding ${queueItems.length} to the queue`)
const queueTracks = queueItems.map((queueItem) => {
return mapDtoToTrack(queueItem, downloadedTracks ?? [], QueuingType.FromSelection)
})
console.debug(`Slotting ${queueTracks.length} track(s)`)
await addToQueue(queueTracks)
setQueue(mutation.queue)
},
onSuccess: async (data, mutation: QueueMutation) => {
setIsSkipping(false)
await play(mutation.index)
if (typeof mutation.queue === 'object') await markItemPlayed(queue as BaseItemDto)
},
onError: async () => {
setIsSkipping(false)
setNowPlaying((await TrackPlayer.getActiveTrack()) as JellifyTrack)
},
})
//#endregion
//#region RNTP Setup
@@ -384,7 +137,7 @@ const PlayerContextInitializer = () => {
}
case Event.PlaybackActiveTrackChanged: {
if (initialized && !isSkipping) {
if (initialized) {
const activeTrack = (await TrackPlayer.getActiveTrack()) as
| JellifyTrack
| undefined
@@ -419,14 +172,6 @@ const PlayerContextInitializer = () => {
//#endregion RNTP Setup
//#region useEffects
useEffect(() => {
storage.set(MMKVStorageKeys.Queue, JSON.stringify(queue))
}, [queue])
useEffect(() => {
if (initialized && playQueue)
storage.set(MMKVStorageKeys.PlayQueue, JSON.stringify(playQueue))
}, [playQueue])
useEffect(() => {
if (initialized && nowPlaying)
@@ -435,15 +180,17 @@ const PlayerContextInitializer = () => {
useEffect(() => {
if (!initialized && playQueue.length > 0 && nowPlaying) {
TrackPlayer.setQueue(playQueue).then(() => {
TrackPlayer.skip(
playQueue.findIndex((track) => track.item.Id! === nowPlaying.item.Id!),
)
})
TrackPlayer.skip(playQueue.findIndex((track) => track.item.Id! === nowPlaying.item.Id!))
}
setInitialized(true)
}, [playQueue, nowPlaying])
useEffect(() => {
if (currentIndex > -1 && playQueue.length > currentIndex)
console.debug(`Setting now playing to queue index ${currentIndex}`)
setNowPlaying(playQueue[currentIndex])
}, [currentIndex])
//#endregion useEffects
//#region return
@@ -452,19 +199,9 @@ const PlayerContextInitializer = () => {
nowPlayingIsFavorite,
setNowPlayingIsFavorite,
nowPlaying,
playQueue,
queue,
getQueueSectionData,
useAddToQueue,
useClearQueue,
setNowPlaying,
useReorderQueue,
useRemoveFromQueue,
useStartPlayback,
useTogglePlayback,
useSeekTo,
useSkip,
usePrevious,
usePlayNewQueue,
playbackState,
}
//#endregion return
@@ -476,65 +213,7 @@ export const PlayerContext = createContext<PlayerContext>({
nowPlayingIsFavorite: false,
setNowPlayingIsFavorite: () => {},
nowPlaying: undefined,
setNowPlaying: () => {},
playQueue: [],
queue: 'Recently Played',
getQueueSectionData: () => [],
useAddToQueue: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useClearQueue: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useRemoveFromQueue: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useReorderQueue: {
useStartPlayback: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
@@ -588,61 +267,6 @@ export const PlayerContext = createContext<PlayerContext>({
failureReason: null,
submittedAt: 0,
},
useSkip: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
usePrevious: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
usePlayNewQueue: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
playbackState: undefined,
})
//#endregion Create PlayerContext
@@ -651,55 +275,9 @@ export const PlayerProvider: ({ children }: { children: ReactNode }) => React.JS
}: {
children: ReactNode
}) => {
const {
initialized,
nowPlayingIsFavorite,
setNowPlayingIsFavorite,
nowPlaying,
playQueue,
queue,
getQueueSectionData,
useAddToQueue,
useClearQueue,
useRemoveFromQueue,
useReorderQueue,
useTogglePlayback,
useSeekTo,
useSkip,
usePrevious,
setNowPlaying,
usePlayNewQueue,
playbackState,
} = PlayerContextInitializer()
const context = PlayerContextInitializer()
return (
<QueueProvider>
<PlayerContext.Provider
value={{
initialized,
nowPlayingIsFavorite,
setNowPlayingIsFavorite,
nowPlaying,
playQueue,
queue,
getQueueSectionData,
useAddToQueue,
useClearQueue,
useRemoveFromQueue,
useReorderQueue,
useTogglePlayback,
useSeekTo,
useSkip,
usePrevious,
setNowPlaying,
usePlayNewQueue,
playbackState,
}}
>
{children}
</PlayerContext.Provider>
</QueueProvider>
)
return <PlayerContext.Provider value={context}>{children}</PlayerContext.Provider>
}
export const usePlayerContext = () => useContext(PlayerContext)

View File

@@ -1,20 +1,448 @@
import React, { ReactNode } from 'react'
import React, { ReactNode, useContext, useEffect, useState } from 'react'
import { createContext } from 'react'
import { Queue } from './types/queue-item'
import { Section } from '../components/Player/types'
import { useMutation, UseMutationResult } from '@tanstack/react-query'
import { AddToQueueMutation, QueueMutation, QueueOrderMutation } from './interfaces'
import { storage } from '../constants/storage'
import { MMKVStorageKeys } from '../enums/mmkv-storage-keys'
import { JellifyTrack } from '../types/JellifyTrack'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
import { mapDtoToTrack } from '../helpers/mappings'
import { useNetworkContext } from '../components/Network/provider'
import { QueuingType } from '../enums/queuing-type'
import TrackPlayer from 'react-native-track-player'
import { findPlayQueueIndexStart } from './helpers'
import { getQueue, seekTo } from 'react-native-track-player/lib/src/trackPlayer'
import { trigger } from 'react-native-haptic-feedback'
import * as Burnt from 'burnt'
import { markItemPlayed } from '../api/mutations/functions/item'
import { filterTracksOnNetworkStatus } from './helpers/queue'
import { SKIP_TO_PREVIOUS_THRESHOLD } from './config'
interface QueueContext {}
const QueueContextInitailizer = () => {
return {}
interface QueueContext {
queueRef: Queue
playQueue: JellifyTrack[]
currentIndex: number
fetchQueueSectionData: () => Section[]
useAddToQueue: UseMutationResult<void, Error, AddToQueueMutation, unknown>
useLoadNewQueue: UseMutationResult<void, Error, QueueMutation, unknown>
useRemoveUpcomingTracks: UseMutationResult<void, Error, void, unknown>
useRemoveFromQueue: UseMutationResult<void, Error, number, unknown>
useReorderQueue: UseMutationResult<void, Error, QueueOrderMutation, unknown>
useSkip: UseMutationResult<void, Error, number | undefined, unknown>
usePrevious: UseMutationResult<void, Error, void, unknown>
}
export const QueueContext = createContext<QueueContext>({})
const QueueContextInitailizer = () => {
const queueRefJson = storage.getString(MMKVStorageKeys.Queue)
const playQueueJson = storage.getString(MMKVStorageKeys.PlayQueue)
const queueRefInit = queueRefJson ? JSON.parse(queueRefJson) : 'Recently Played'
const playQueueInit = playQueueJson ? JSON.parse(playQueueJson) : []
const [queueRef, setQueueRef] = useState<Queue>(queueRefInit)
const [playQueue, setPlayQueue] = useState<JellifyTrack[]>(playQueueInit)
const [updateRntp, setUpdateRntp] = useState<boolean>(true)
const [currentIndex, setCurrentIndex] = useState<number>(-1)
const { downloadedTracks, networkStatus } = useNetworkContext()
//#region Functions
const fetchQueueSectionData: () => Section[] = () => {
return Object.keys(QueuingType).map((type) => {
return {
title: type,
data: playQueue.filter((track) => track.QueuingType === type),
} as Section
})
}
const resetQueue = () => {
console.debug(`Clearing queue of ${playQueue.length}`)
setPlayQueue([])
}
/**
* Takes a {@link BaseItemDto} of a track on Jellyfin, and updates it's
* position in the {@link queue}
*
*
* @param track The Jellyfin track object to update and replace in the queue
*/
const replaceQueueItem: (track: BaseItemDto) => Promise<void> = async (track: BaseItemDto) => {
const queue = (await TrackPlayer.getQueue()) as JellifyTrack[]
const queueItemIndex = queue.findIndex((queuedTrack) => queuedTrack.item.Id === track.Id!)
// Update queued item at index if found, else silently do nothing
if (queueItemIndex !== -1) {
const queueItem = queue[queueItemIndex]
TrackPlayer.remove([queueItemIndex]).then(() => {
TrackPlayer.add(
mapDtoToTrack(track, downloadedTracks ?? [], queueItem.QueuingType),
queueItemIndex,
)
})
}
}
const loadQueue = async (
audioItems: BaseItemDto[],
queuingRef: Queue,
startIndex: number = 1,
) => {
console.debug(`Queuing ${audioItems.length} items`)
const availableAudioItems = filterTracksOnNetworkStatus(
networkStatus,
audioItems,
downloadedTracks ?? [],
)
console.debug(
`Filtered out ${
audioItems.length - availableAudioItems.length
} due to network status being ${networkStatus}`,
)
const queue = availableAudioItems.map((item) =>
mapDtoToTrack(item, downloadedTracks ?? [], QueuingType.FromSelection),
)
setPlayQueue(queue)
setQueueRef(queuingRef)
setCurrentIndex(startIndex)
console.debug(`Queued ${queue.length} tracks, starting at ${startIndex}`)
}
const playNextInQueue = async (item: BaseItemDto) => {
console.debug(`Playing item next in queue`)
const playNextTrack = mapDtoToTrack(item, downloadedTracks ?? [], QueuingType.PlayingNext)
setUpdateRntp(false)
TrackPlayer.add([playNextTrack], currentIndex + 1)
setPlayQueue([
...playQueue.slice(0, currentIndex + 1),
playNextTrack,
...playQueue.slice(currentIndex + 1),
])
}
const playInQueue = async (items: BaseItemDto[]) => {
const insertIndex = await findPlayQueueIndexStart(playQueue)
console.debug(`Adding ${items.length} to queue at index ${insertIndex}`)
setUpdateRntp(false)
await TrackPlayer.add(
items.map((item) =>
mapDtoToTrack(item, downloadedTracks ?? [], QueuingType.DirectlyQueued),
),
insertIndex,
)
setPlayQueue((await getQueue()) as JellifyTrack[])
console.debug(`Queue has ${playQueue.length} tracks`)
}
//#endregion Functions
//#region Hooks
const useAddToQueue = useMutation({
mutationFn: ({ track, queuingType }: AddToQueueMutation) => {
return queuingType === QueuingType.PlayingNext
? playNextInQueue(track)
: playInQueue([track])
},
onSuccess: (data, { queuingType }) => {
trigger('notificationSuccess')
Burnt.alert({
title: queuingType === QueuingType.PlayingNext ? 'Playing next' : 'Added to queue',
duration: 0.5,
preset: 'done',
})
},
onError: () => {
trigger('notificationError')
},
})
const useLoadNewQueue = useMutation({
mutationFn: async ({ index, track, tracklist, queuingType, queue }: QueueMutation) =>
loadQueue(tracklist, queue, index),
onSuccess: async (data, { queue }: QueueMutation) => {
trigger('notificationSuccess')
if (typeof queue === 'object') await markItemPlayed(queue)
},
})
const useRemoveFromQueue = useMutation({
mutationFn: async (index: number) => {
trigger('impactMedium')
setUpdateRntp(false)
TrackPlayer.remove([index])
setPlayQueue([
...playQueue.slice(0, index),
...playQueue.slice(index + 1, playQueue.length - 1),
])
},
})
/**
*
*/
const useRemoveUpcomingTracks = useMutation({
mutationFn: async () => {
setUpdateRntp(false)
TrackPlayer.removeUpcomingTracks()
setPlayQueue([...playQueue.slice(0, currentIndex + 1)])
},
onSuccess: () => {
trigger('notificationSuccess')
},
})
const useReorderQueue = useMutation({
mutationFn: async ({ from, to, newOrder }: QueueOrderMutation) => {
setUpdateRntp(false)
TrackPlayer.move(from, to)
setPlayQueue(newOrder)
},
onSuccess: () => {
trigger('notificationSuccess')
},
})
const useSkip = useMutation({
mutationFn: async (index?: number | undefined) => {
trigger('impactMedium')
console.debug(
`Skip to next triggered. Index is ${`using ${
index ? index : currentIndex
} as index ${index ? 'since it was provided' : ''}`}`,
)
if (index && index < playQueue.length - 1) setCurrentIndex(index)
else if (playQueue.length - 1 > currentIndex) setCurrentIndex(currentIndex + 1)
},
})
const usePrevious = useMutation({
mutationFn: async () => {
trigger('impactMedium')
const { position } = await TrackPlayer.getProgress()
console.debug(`Skip to previous triggered. Index is ${currentIndex}`)
if (currentIndex > 0 && position < SKIP_TO_PREVIOUS_THRESHOLD) {
setCurrentIndex(currentIndex - 1)
} else await seekTo(0)
},
})
const useUpdateRntpQueue = useMutation({
mutationFn: async () => {
if (updateRntp) await TrackPlayer.setQueue(playQueue)
setUpdateRntp(true)
},
})
const useUpdateRntpIndex = useMutation({
mutationFn: async (index: number) => {
await TrackPlayer.skip(index)
},
})
//#endregion Hooks
//#region useEffect(s)
/**
* Update RNTP Queue when our queue
* is updated
*/
useEffect(() => {
useUpdateRntpQueue.mutate()
storage.set(MMKVStorageKeys.PlayQueue, JSON.stringify(playQueue))
}, [playQueue])
useEffect(() => {
storage.set(MMKVStorageKeys.Queue, JSON.stringify(queueRef))
}, [queueRef])
useEffect(() => {
useUpdateRntpIndex.mutate(currentIndex)
}, [currentIndex])
//#endregion useEffect(s)
return {
queueRef,
playQueue,
currentIndex,
setCurrentIndex,
fetchQueueSectionData,
useAddToQueue,
useLoadNewQueue,
useRemoveFromQueue,
useRemoveUpcomingTracks,
useReorderQueue,
useSkip,
usePrevious,
}
}
export const QueueContext = createContext<QueueContext>({
queueRef: 'Recently Played',
playQueue: [],
currentIndex: -1,
fetchQueueSectionData: () => [],
useAddToQueue: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useLoadNewQueue: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useSkip: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
usePrevious: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useRemoveFromQueue: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useRemoveUpcomingTracks: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
useReorderQueue: {
mutate: () => {},
mutateAsync: async () => {},
data: undefined,
error: null,
variables: undefined,
isError: false,
isIdle: true,
isPaused: false,
isPending: false,
isSuccess: false,
status: 'idle',
reset: () => {},
context: {},
failureCount: 0,
failureReason: null,
submittedAt: 0,
},
})
export const QueueProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({
children,
}: {
children: ReactNode
}) => {
const urmo = QueueContextInitailizer()
const context = QueueContextInitailizer()
return <QueueContext.Provider value={{}}>{children}</QueueContext.Provider>
return <QueueContext.Provider value={context}>{children}</QueueContext.Provider>
}
export const useQueueContext = () => useContext(QueueContext)

View File

@@ -6539,17 +6539,6 @@ glob@^10.2.2, glob@^10.3.10, glob@^10.4.2:
package-json-from-dist "^1.0.0"
path-scurry "^1.11.1"
glob@^6.0.1:
version "6.0.4"
resolved "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz"
integrity sha512-MKZeRNyYZAVVVG1oZeLaWie1uweH40m9AZwIwxyPbTSX4hHrVYSzLg0Ro5Z5R7XKkIX+Cc6oD1rqeDJnwsB8/A==
dependencies:
inflight "^1.0.4"
inherits "2"
minimatch "2 || 3"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.1.1, glob@^7.1.3, glob@^7.1.4:
version "7.2.3"
resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz"
@@ -6846,11 +6835,6 @@ ini@~1.3.0:
resolved "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz"
integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==
insync@^2.1.1:
version "2.1.1"
resolved "https://registry.npmjs.org/insync/-/insync-2.1.1.tgz"
integrity sha512-UzUhOZFpCMM22Xlig9iUPqalf8n7c4eYScamce1C+jN3ad8FtmVm42ryMwVq0hAxHbwUhWFhPvTFQQpFdDUKkw==
internal-ip@^4.3.0:
version "4.3.0"
resolved "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz"
@@ -8478,13 +8462,6 @@ mimic-function@^5.0.0:
resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz"
integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==
"minimatch@2 || 3", minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimatch@^10.0.1:
version "10.0.1"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz"
@@ -8492,6 +8469,13 @@ minimatch@^10.0.1:
dependencies:
brace-expansion "^2.0.1"
minimatch@^3.0.2, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
dependencies:
brace-expansion "^1.1.7"
minimatch@^8.0.2:
version "8.0.4"
resolved "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz"
@@ -8610,11 +8594,6 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
ncp@^2.0.0:
version "2.0.0"
resolved "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz"
integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==
negotiator@0.6.3:
version "0.6.3"
resolved "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz"
@@ -8684,17 +8663,6 @@ normalize-path@^3.0.0:
resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
npm-bundle@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/npm-bundle/-/npm-bundle-3.0.3.tgz"
integrity sha512-fHF7FR32YNgjqi0MQMLnE78Ff9/wYd4/7/Cke3dLLi2QzETKotIiWGCxwDoXAZDWVoTuVRYQa2ZdiZPuBL7QnA==
dependencies:
glob "^6.0.1"
insync "^2.1.1"
mkdirp "^0.5.1"
ncp "^2.0.0"
rimraf "^2.4.4"
npm-package-arg@^11.0.0:
version "11.0.3"
resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz"
@@ -9911,7 +9879,7 @@ rfdc@^1.4.1:
resolved "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz"
integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==
rimraf@^2.4.4, rimraf@^2.6.3:
rimraf@^2.6.3:
version "2.7.1"
resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz"
integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==