mirror of
https://github.com/anultravioletaurora/Jellify.git
synced 2025-12-21 13:30:11 -06:00
lots of context prefetching and favorites prefetching improvements
This commit is contained in:
7
App.tsx
7
App.tsx
@@ -7,7 +7,7 @@ import { TamaguiProvider } from 'tamagui'
|
||||
import { Platform, useColorScheme } from 'react-native'
|
||||
import jellifyConfig from './tamagui.config'
|
||||
import { clientPersister } from './src/constants/storage'
|
||||
import { queryClient } from './src/constants/query-client'
|
||||
import { ONE_DAY, queryClient } from './src/constants/query-client'
|
||||
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
||||
import TrackPlayer, {
|
||||
AndroidAudioContentType,
|
||||
@@ -110,10 +110,9 @@ function Container({ playerIsReady }: { playerIsReady: boolean }): React.JSX.Ele
|
||||
persister: clientPersister,
|
||||
|
||||
/**
|
||||
* Infinity, since data can remain the
|
||||
* same forever on the server
|
||||
* Maximum query data age of one day
|
||||
*/
|
||||
maxAge: Infinity,
|
||||
maxAge: ONE_DAY,
|
||||
}}
|
||||
>
|
||||
<GestureHandlerRootView>
|
||||
|
||||
@@ -50,11 +50,6 @@ const QueryConfig = {
|
||||
width: 1000,
|
||||
format: ImageFormat.Jpg,
|
||||
},
|
||||
staleTime: {
|
||||
oneDay: 1000 * 60 * 60 * 24, // 1 Day
|
||||
oneWeek: 1000 * 60 * 60 * 24 * 7, // 7 Days
|
||||
oneFortnight: 1000 * 60 * 60 * 24 * 7 * 14, // 14 Days
|
||||
},
|
||||
}
|
||||
|
||||
export default QueryConfig
|
||||
|
||||
@@ -168,6 +168,7 @@ export default function Artists({
|
||||
if (artistsInfiniteQuery.hasNextPage && !artistsInfiniteQuery.isFetching)
|
||||
artistsInfiniteQuery.fetchNextPage()
|
||||
}}
|
||||
removeClippedSubviews
|
||||
/>
|
||||
|
||||
{showAlphabeticalSelector && artistPageParams && (
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { getToken, getTokenValue, ListItem, ScrollView, View, YGroup, ZStack } from 'tamagui'
|
||||
import { getToken, ListItem, ScrollView, View, YGroup } from 'tamagui'
|
||||
import { BaseStackParamList, RootStackParamList } from '../../screens/types'
|
||||
import { Text } from '../Global/helpers/text'
|
||||
import FavoriteContextMenuRow from '../Global/components/favorite-context-menu-row'
|
||||
import { Blurhash } from 'react-native-blurhash'
|
||||
import { getPrimaryBlurhashFromDto } from '../../utils/blurhash'
|
||||
import { Platform, useColorScheme } from 'react-native'
|
||||
import { useColorScheme } from 'react-native'
|
||||
import { useThemeSettingContext } from '../../providers/Settings'
|
||||
import LinearGradient from 'react-native-linear-gradient'
|
||||
import Icon from '../Global/components/icon'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { fetchAlbumDiscs, fetchItem, fetchItems } from '../../api/queries/item'
|
||||
import { fetchAlbumDiscs, fetchItem } from '../../api/queries/item'
|
||||
import { useJellifyContext } from '../../providers'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { useAddToQueueContext } from '../../providers/Player/queue'
|
||||
import { AddToQueueMutation } from '../../providers/Player/interfaces'
|
||||
import { QueuingType } from '../../enums/queuing-type'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import navigationRef from '../../../navigation'
|
||||
import { goToAlbumFromContextSheet, goToArtistFromContextSheet } from './utils/navigation'
|
||||
import { getItemName } from '../../utils/text'
|
||||
@@ -27,6 +25,7 @@ import { StackActions } from '@react-navigation/native'
|
||||
import TextTicker from 'react-native-text-ticker'
|
||||
import { TextTickerConfig } from '../Player/component.config'
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||
import { trigger } from 'react-native-haptic-feedback'
|
||||
|
||||
type StackNavigation = Pick<NativeStackNavigationProp<BaseStackParamList>, 'navigate' | 'dispatch'>
|
||||
|
||||
@@ -47,8 +46,6 @@ export default function ItemContext({ item, stackNavigation }: ContextProps): Re
|
||||
const isTrack = item.Type === BaseItemKind.Audio
|
||||
const isPlaylist = item.Type === BaseItemKind.Playlist
|
||||
|
||||
const itemArtists = item.ArtistItems ?? []
|
||||
|
||||
const { data: album } = useQuery({
|
||||
queryKey: [QueryKeys.Album, item.AlbumId],
|
||||
queryFn: () => fetchItem(api, item.AlbumId!),
|
||||
@@ -77,7 +74,7 @@ export default function ItemContext({ item, stackNavigation }: ContextProps): Re
|
||||
|
||||
const renderAddToPlaylistRow = isTrack
|
||||
|
||||
const renderViewAlbumRow = useMemo(() => isAlbum || (isTrack && album), [album, item])
|
||||
const renderViewAlbumRow = isAlbum || (isTrack && album)
|
||||
|
||||
const artistIds = !isPlaylist
|
||||
? isArtist
|
||||
@@ -87,24 +84,22 @@ export default function ItemContext({ item, stackNavigation }: ContextProps): Re
|
||||
: []
|
||||
: []
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<YGroup unstyled marginBottom={bottom}>
|
||||
<FavoriteContextMenuRow item={item} />
|
||||
|
||||
{renderAddToQueueRow && (
|
||||
<AddToQueueMenuRow
|
||||
tracks={
|
||||
isTrack
|
||||
const itemTracks = isTrack
|
||||
? [item]
|
||||
: isAlbum && discs
|
||||
? discs.flatMap((data) => data.data)
|
||||
: isPlaylist && tracks
|
||||
? tracks
|
||||
: []
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
useEffect(() => trigger('impactLight'), [item?.Id])
|
||||
|
||||
return (
|
||||
<ScrollView>
|
||||
<YGroup unstyled marginBottom={bottom}>
|
||||
<FavoriteContextMenuRow item={item} />
|
||||
|
||||
{renderAddToQueueRow && <AddToQueueMenuRow tracks={itemTracks} />}
|
||||
|
||||
{renderAddToPlaylistRow && <AddToPlaylistRow track={item} />}
|
||||
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import React from 'react'
|
||||
import Icon from './icon'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { isUndefined } from 'lodash'
|
||||
import { getTokens, Spinner } from 'tamagui'
|
||||
import { Spinner } from 'tamagui'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchUserData } from '../../../api/queries/favorites'
|
||||
import { useJellifyUserDataContext } from '../../../providers/UserData'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
import { ONE_HOUR } from '../../../constants/query-client'
|
||||
|
||||
interface SetFavoriteMutation {
|
||||
item: BaseItemDto
|
||||
@@ -21,26 +22,21 @@ export default function FavoriteButton({
|
||||
item: BaseItemDto
|
||||
onToggle?: () => void
|
||||
}): React.JSX.Element {
|
||||
const [isFavorite, setFavorite] = useState<boolean>(isFavoriteItem(item))
|
||||
|
||||
const { api, user } = useJellifyContext()
|
||||
const { toggleFavorite } = useJellifyUserDataContext()
|
||||
|
||||
const { data, isFetching, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id!],
|
||||
const {
|
||||
data: isFavorite,
|
||||
isFetching,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id],
|
||||
queryFn: () => fetchUserData(api, user, item.Id!),
|
||||
staleTime: 1000 * 60 * 60 * 1, // 1 hour,
|
||||
select: (data) => typeof data === 'object' && data.IsFavorite,
|
||||
staleTime: ONE_HOUR,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
refetch()
|
||||
}, [item])
|
||||
|
||||
useEffect(() => {
|
||||
if (data) setFavorite(data.IsFavorite ?? false)
|
||||
}, [data])
|
||||
|
||||
return isFetching && isUndefined(item.UserData) ? (
|
||||
return isFetching ? (
|
||||
<Spinner alignSelf='center' />
|
||||
) : isFavorite ? (
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
@@ -50,7 +46,6 @@ export default function FavoriteButton({
|
||||
onPress={() =>
|
||||
toggleFavorite(isFavorite, {
|
||||
item,
|
||||
setFavorite,
|
||||
onToggle,
|
||||
})
|
||||
}
|
||||
@@ -62,9 +57,8 @@ export default function FavoriteButton({
|
||||
name={'heart-outline'}
|
||||
color={'$primary'}
|
||||
onPress={() =>
|
||||
toggleFavorite(isFavorite, {
|
||||
toggleFavorite(!!isFavorite, {
|
||||
item,
|
||||
setFavorite,
|
||||
onToggle,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,31 +3,24 @@ import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchUserData } from '../../../api/queries/favorites'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import { getToken, ListItem, XStack } from 'tamagui'
|
||||
import { ListItem, XStack } from 'tamagui'
|
||||
import Icon from './icon'
|
||||
import { useJellifyUserDataContext } from '../../../providers/UserData'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Text } from '../helpers/text'
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
import { ONE_HOUR } from '../../../constants/query-client'
|
||||
|
||||
export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
const { api, user } = useJellifyContext()
|
||||
const { toggleFavorite } = useJellifyUserDataContext()
|
||||
|
||||
const { data: userData, refetch } = useQuery({
|
||||
const { data: isFavorite, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id],
|
||||
queryFn: () => fetchUserData(api, user, item.Id!),
|
||||
staleTime: 1000 * 60 * 60 * 1, // 1 hour,
|
||||
select: (data) => typeof data === 'object' && data.IsFavorite,
|
||||
staleTime: ONE_HOUR,
|
||||
})
|
||||
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(
|
||||
userData?.IsFavorite ?? item.UserData?.IsFavorite ?? false,
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setIsFavorite(userData?.IsFavorite ?? false)
|
||||
}, [userData])
|
||||
|
||||
return isFavorite ? (
|
||||
<ListItem
|
||||
animation={'quick'}
|
||||
@@ -36,7 +29,6 @@ export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }):
|
||||
onPress={() => {
|
||||
toggleFavorite(isFavorite, {
|
||||
item,
|
||||
setFavorite: setIsFavorite,
|
||||
onToggle: () => refetch(),
|
||||
})
|
||||
}}
|
||||
@@ -47,12 +39,10 @@ export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }):
|
||||
exiting={FadeOut}
|
||||
key={`${item.Id}-remove-favorite-row`}
|
||||
>
|
||||
<XStack alignContent='center' justifyContent='flex-start' gap={'$3'}>
|
||||
<XStack alignItems='center' justifyContent='flex-start' gap={'$2'}>
|
||||
<Icon name={'heart'} small color={'$primary'} />
|
||||
|
||||
<Text marginTop={'$2'} bold>
|
||||
Remove from favorites
|
||||
</Text>
|
||||
<Text bold>Remove from favorites</Text>
|
||||
</XStack>
|
||||
</Animated.View>
|
||||
</ListItem>
|
||||
@@ -63,9 +53,8 @@ export default function FavoriteContextMenuRow({ item }: { item: BaseItemDto }):
|
||||
justifyContent='flex-start'
|
||||
gap={'$2'}
|
||||
onPress={() => {
|
||||
toggleFavorite(isFavorite, {
|
||||
toggleFavorite(!!isFavorite, {
|
||||
item,
|
||||
setFavorite: setIsFavorite,
|
||||
onToggle: () => refetch(),
|
||||
})
|
||||
}}
|
||||
|
||||
@@ -4,7 +4,7 @@ import Icon from './icon'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchUserData } from '../../../api/queries/favorites'
|
||||
import { useEffect, useState, memo } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
|
||||
@@ -16,23 +16,15 @@ import Animated, { FadeIn, FadeOut } from 'react-native-reanimated'
|
||||
* @returns A React component that displays a favorite icon for a given item.
|
||||
*/
|
||||
function FavoriteIcon({ item }: { item: BaseItemDto }): React.JSX.Element {
|
||||
const [isFavorite, setIsFavorite] = useState<boolean>(item.UserData?.IsFavorite ?? false)
|
||||
|
||||
const { api, user } = useJellifyContext()
|
||||
|
||||
const { data: userData, isPending } = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id!],
|
||||
const { data: isFavorite } = useQuery({
|
||||
queryKey: [QueryKeys.UserData, item.Id],
|
||||
queryFn: () => fetchUserData(api, user, item.Id!),
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes,
|
||||
select: (data) => typeof data === 'object' && data.IsFavorite,
|
||||
enabled: !!api && !!user && !!item.Id, // Only run if we have the required data
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPending && userData !== undefined) {
|
||||
setIsFavorite(userData?.IsFavorite ?? false)
|
||||
}
|
||||
}, [userData, isPending])
|
||||
|
||||
return isFavorite ? (
|
||||
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||
<Icon small name='heart' color={'$primary'} flex={1} />
|
||||
|
||||
@@ -20,7 +20,6 @@ export default function InstantMixButton({
|
||||
const { data, isFetching, refetch } = useQuery({
|
||||
queryKey: [QueryKeys.InstantMix, item.Id!],
|
||||
queryFn: () => fetchInstantMixFromItem(api, user, item),
|
||||
staleTime: 1000 * 60 * 60 * 24, // 24 hours
|
||||
})
|
||||
|
||||
return data ? (
|
||||
|
||||
@@ -2,28 +2,19 @@ import React, { useMemo, useCallback } from 'react'
|
||||
import { getToken, Theme, useTheme, XStack, YStack } from 'tamagui'
|
||||
import { Text } from '../helpers/text'
|
||||
import { RunTimeTicks } from '../helpers/time-codes'
|
||||
import { BaseItemDto, BaseItemKind, ImageType } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import Icon from './icon'
|
||||
import { QueuingType } from '../../../enums/queuing-type'
|
||||
import { Queue } from '../../../player/types/queue-item'
|
||||
import FavoriteIcon from './favorite-icon'
|
||||
import FastImage from 'react-native-fast-image'
|
||||
import { getImageApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { networkStatusTypes } from '../../../components/Network/internetConnectionWatcher'
|
||||
import { useNetworkContext } from '../../../providers/Network'
|
||||
import { useLoadQueueContext, usePlayQueueContext } from '../../../providers/Player/queue'
|
||||
import { useJellifyContext } from '../../../providers'
|
||||
import DownloadedIcon from './downloaded-icon'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../../enums/query-keys'
|
||||
import { fetchMediaInfo } from '../../../api/queries/media'
|
||||
import { useStreamingQualityContext } from '../../../providers/Settings'
|
||||
import { getQualityParams } from '../../../utils/mappings'
|
||||
import { useNowPlayingContext } from '../../../providers/Player'
|
||||
import navigationRef from '../../../../navigation'
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
|
||||
import { BaseStackParamList } from '../../../screens/types'
|
||||
import { fetchItem } from '../../../api/queries/item'
|
||||
import ItemImage from './image'
|
||||
import { ItemProvider } from '../../../providers/Item'
|
||||
|
||||
@@ -61,12 +52,10 @@ export default function Track({
|
||||
}: TrackProps): React.JSX.Element {
|
||||
const theme = useTheme()
|
||||
|
||||
const { api, user } = useJellifyContext()
|
||||
const nowPlaying = useNowPlayingContext()
|
||||
const playQueue = usePlayQueueContext()
|
||||
const useLoadNewQueue = useLoadQueueContext()
|
||||
const { downloadedTracks, networkStatus } = useNetworkContext()
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
// Memoize expensive computations
|
||||
const isPlaying = useMemo(
|
||||
@@ -129,29 +118,6 @@ export default function Track({
|
||||
}
|
||||
}, [showRemove, onRemove, track, isNested])
|
||||
|
||||
// Only fetch media info if needed (for streaming)
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, streamingQuality, track.Id],
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), track.Id!),
|
||||
staleTime: Infinity, // Don't refetch media info unless the user changes the quality
|
||||
enabled: !isDownloaded, // Only fetch if not downloaded
|
||||
})
|
||||
|
||||
// Fire query for fetching the track's media sources
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.MediaSources, streamingQuality, track.Id],
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), track.Id!),
|
||||
staleTime: Infinity, // Don't refetch media info unless the user changes the quality
|
||||
enabled: track.Type === 'Audio',
|
||||
})
|
||||
|
||||
// Fire query for fetching the track's album
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.Album, track.AlbumId],
|
||||
queryFn: () => fetchItem(api, track.AlbumId!),
|
||||
enabled: track.Type === BaseItemKind.Audio && !!track.AlbumId,
|
||||
})
|
||||
|
||||
// Memoize text color to prevent recalculation
|
||||
const textColor = useMemo(() => {
|
||||
if (isPlaying) return theme.primary.val
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
export const ONE_MINUTE = 1000 * 60
|
||||
export const ONE_HOUR = ONE_MINUTE * 60
|
||||
export const ONE_DAY = ONE_HOUR * 24
|
||||
|
||||
/**
|
||||
* A global instance of the Tanstack React Query client
|
||||
*
|
||||
@@ -16,18 +20,18 @@ export const queryClient = new QueryClient({
|
||||
/**
|
||||
* This needs to be set equal to or higher than the `maxAge` set in `../App.tsx`
|
||||
*
|
||||
* Because data can remain on the server forever, the `maxAge` is set to `Infinity`
|
||||
* Because we want to preserve hybrid network functionality, the `maxAge` is set to {@link ONE_DAY}
|
||||
*
|
||||
* Therefore, this also needs to be set to `Infinity`, disabling garbage collection
|
||||
* Therefore, this also needs to be set to {@link ONE_DAY}
|
||||
*
|
||||
* @see https://tanstack.com/query/latest/docs/framework/react/plugins/persistQueryClient#how-it-works
|
||||
*/
|
||||
gcTime: Infinity,
|
||||
gcTime: ONE_DAY,
|
||||
|
||||
/**
|
||||
* 1 hour as a default - reduced from 2 hours for better battery usage
|
||||
*/
|
||||
staleTime: 1000 * 60 * 60, // 1 hour
|
||||
staleTime: ONE_HOUR, // 1 hour
|
||||
retry(failureCount: number, error: Error) {
|
||||
if (failureCount > 2) return false
|
||||
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import React, {
|
||||
createContext,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react'
|
||||
import React, { createContext, ReactNode, useCallback, useContext, useState } from 'react'
|
||||
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import {
|
||||
InfiniteData,
|
||||
InfiniteQueryObserverResult,
|
||||
useInfiniteQuery,
|
||||
UseInfiniteQueryResult,
|
||||
@@ -19,7 +11,6 @@ import { queryClient } from '../../constants/query-client'
|
||||
import QueryConfig from '../../api/queries/query.config'
|
||||
import { fetchFrequentlyPlayed, fetchFrequentlyPlayedArtists } from '../../api/queries/frequents'
|
||||
import { useJellifyContext } from '..'
|
||||
import { useIsFocused } from '@react-navigation/native'
|
||||
interface HomeContext {
|
||||
refreshing: boolean
|
||||
onRefresh: () => void
|
||||
@@ -44,12 +35,6 @@ const HomeContextInitializer = () => {
|
||||
const { api, library, user } = useJellifyContext()
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||
|
||||
const isFocused = useIsFocused()
|
||||
|
||||
useEffect(() => {
|
||||
console.debug(`Home focused: ${isFocused}`)
|
||||
}, [isFocused])
|
||||
|
||||
const {
|
||||
data: recentTracks,
|
||||
isFetching: isFetchingRecentTracks,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BaseItemDto, BaseItemKind } from '@jellyfin/sdk/lib/generated-client/models'
|
||||
import { createContext, ReactNode, useMemo } from 'react'
|
||||
import { createContext, ReactNode, useEffect } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { fetchMediaInfo } from '../../api/queries/media'
|
||||
@@ -9,6 +9,8 @@ import { getQualityParams } from '../../utils/mappings'
|
||||
import { fetchAlbumDiscs, fetchItem } from '../../api/queries/item'
|
||||
import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
|
||||
import { ItemArtistProvider } from './item-artists'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
import { fetchUserData } from '../../api/queries/favorites'
|
||||
|
||||
interface ItemContext {
|
||||
item: BaseItemDto
|
||||
@@ -45,18 +47,20 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX.
|
||||
|
||||
const streamingQuality = useStreamingQualityContext()
|
||||
|
||||
const { Id, Type, AlbumId, ArtistItems } = item
|
||||
const { Id, Type, AlbumId, ArtistItems, UserData } = item
|
||||
|
||||
const artistIds = ArtistItems?.map(({ Id }) => Id) ?? []
|
||||
|
||||
useEffect(() => {
|
||||
// Fail fast if we don't have an Item ID to work with
|
||||
if (!Id) return
|
||||
/**
|
||||
* Fetch and cache the media sources if this item is a track
|
||||
*/
|
||||
useQuery({
|
||||
if (Type === BaseItemKind.Audio)
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: [QueryKeys.MediaSources, streamingQuality, Id],
|
||||
queryFn: () => fetchMediaInfo(api, user, getQualityParams(streamingQuality), Id!),
|
||||
staleTime: Infinity, // Don't refetch media info unless the user changes the quality
|
||||
enabled: !!Id && Type === BaseItemKind.Audio,
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -64,21 +68,18 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX.
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.ArtistById, Id],
|
||||
queryFn: () => item,
|
||||
enabled: !!Id && Type === BaseItemKind.MusicArtist,
|
||||
})
|
||||
if (Type === BaseItemKind.MusicArtist)
|
||||
queryClient.setQueryData([QueryKeys.ArtistById, Id], item)
|
||||
|
||||
/**
|
||||
* Fire query for a track's album...
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
if (!!AlbumId && Type === BaseItemKind.Audio)
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: [QueryKeys.Album, AlbumId],
|
||||
queryFn: () => fetchItem(api, item.AlbumId!),
|
||||
enabled: !!AlbumId && Type === BaseItemKind.Audio,
|
||||
})
|
||||
|
||||
/**
|
||||
@@ -86,29 +87,26 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX.
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
queryKey: [QueryKeys.Album, Id],
|
||||
queryFn: () => item,
|
||||
enabled: !!Id && Type === BaseItemKind.MusicAlbum,
|
||||
})
|
||||
if (Type === BaseItemKind.MusicAlbum) queryClient.setQueryData([QueryKeys.Album, Id], item)
|
||||
|
||||
/**
|
||||
* Prefetch for an album's tracks
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
if (Type === BaseItemKind.MusicAlbum)
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: [QueryKeys.ItemTracks, Id],
|
||||
queryFn: () => fetchAlbumDiscs(api, item),
|
||||
enabled: !!Id && item.Type === BaseItemKind.MusicAlbum,
|
||||
})
|
||||
|
||||
/**
|
||||
* Fire query for a playlist's tracks
|
||||
* Prefetch query for a playlist's tracks
|
||||
*
|
||||
* Referenced later in the context sheet
|
||||
*/
|
||||
useQuery({
|
||||
if (Type === BaseItemKind.Playlist)
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: [QueryKeys.ItemTracks, Id],
|
||||
queryFn: () =>
|
||||
getItemsApi(api!)
|
||||
@@ -117,16 +115,20 @@ export const ItemProvider: ({ item, children }: ItemProviderProps) => React.JSX.
|
||||
if (data.Items) return data.Items
|
||||
else return []
|
||||
}),
|
||||
enabled: !!Id && Type === BaseItemKind.Playlist,
|
||||
})
|
||||
|
||||
return useMemo(
|
||||
() => (
|
||||
if (UserData) queryClient.setQueryData([QueryKeys.UserData, Id], UserData)
|
||||
else
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: [QueryKeys.UserData, Id],
|
||||
queryFn: () => fetchUserData(api, user, Id),
|
||||
})
|
||||
}, [queryClient, api, user, Id, Type, AlbumId, UserData, item, streamingQuality])
|
||||
|
||||
return (
|
||||
<ItemContext.Provider value={{ item }}>
|
||||
{artistIds.map((Id) => Id && <ItemArtistProvider artistId={Id} key={Id} />)}
|
||||
{children}
|
||||
</ItemContext.Provider>
|
||||
),
|
||||
[item],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createContext } from 'react'
|
||||
import { createContext, useEffect } from 'react'
|
||||
import { useJellifyContext } from '..'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { QueryKeys } from '../../enums/query-keys'
|
||||
import { fetchItem } from '../../api/queries/item'
|
||||
import { queryClient } from '../../constants/query-client'
|
||||
|
||||
interface ItemArtistContext {
|
||||
artistId: string | undefined
|
||||
@@ -19,13 +19,15 @@ export const ItemArtistProvider: ({
|
||||
}) => React.JSX.Element = ({ artistId }) => {
|
||||
const { api } = useJellifyContext()
|
||||
|
||||
useEffect(() => {
|
||||
/**
|
||||
* Store queryable of artist item
|
||||
*/
|
||||
useQuery({
|
||||
if (artistId)
|
||||
queryClient.ensureQueryData({
|
||||
queryKey: [QueryKeys.ArtistById, artistId],
|
||||
queryFn: () => fetchItem(api, artistId!),
|
||||
enabled: !!artistId,
|
||||
})
|
||||
})
|
||||
|
||||
return <ItemArtistContext.Provider value={{ artistId }} />
|
||||
|
||||
@@ -100,8 +100,6 @@ const LibraryContextInitializer = () => {
|
||||
),
|
||||
select: selectArtists,
|
||||
initialPageParam: 0,
|
||||
staleTime: QueryConfig.staleTime.oneDay, // Cache for 1 day to reduce network requests
|
||||
gcTime: QueryConfig.staleTime.oneWeek, // Keep in memory for 1 week
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
return lastPage.length === QueryConfig.limits.library ? lastPageParam + 1 : undefined
|
||||
},
|
||||
@@ -123,8 +121,6 @@ const LibraryContextInitializer = () => {
|
||||
sortDescending ? SortOrder.Descending : SortOrder.Ascending,
|
||||
),
|
||||
initialPageParam: 0,
|
||||
staleTime: QueryConfig.staleTime.oneDay, // Cache for 1 day to reduce network requests
|
||||
gcTime: QueryConfig.staleTime.oneWeek, // Keep in memory for 1 week
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
console.debug(`Tracks last page length: ${lastPage.length}`)
|
||||
return lastPage.length === QueryConfig.limits.library * 2
|
||||
@@ -149,8 +145,6 @@ const LibraryContextInitializer = () => {
|
||||
initialPageParam: alphabet[0],
|
||||
select: (data) => data.pages.flatMap((page) => [page.title, ...page.data]),
|
||||
maxPages: alphabet.length,
|
||||
staleTime: QueryConfig.staleTime.oneDay, // Cache for 1 day to reduce network requests
|
||||
gcTime: QueryConfig.staleTime.oneWeek, // Keep in memory for 1 week
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
console.debug(`Albums last page length: ${lastPage.data.length}`)
|
||||
if (lastPageParam !== alphabet[alphabet.length - 1]) {
|
||||
@@ -180,8 +174,6 @@ const LibraryContextInitializer = () => {
|
||||
queryFn: () => fetchUserPlaylists(api, user, library),
|
||||
select: (data) => data.pages.flatMap((page) => page),
|
||||
initialPageParam: 0,
|
||||
staleTime: QueryConfig.staleTime.oneDay, // Cache for 1 day to reduce network requests
|
||||
gcTime: QueryConfig.staleTime.oneWeek, // Keep in memory for 1 week
|
||||
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
|
||||
return lastPage.length === QueryConfig.limits.library ? lastPageParam + 1 : undefined
|
||||
},
|
||||
|
||||
@@ -115,7 +115,6 @@ const NetworkContextInitializer = () => {
|
||||
const { data: storageUsage } = useQuery({
|
||||
queryKey: [QueryKeys.StorageInUse],
|
||||
queryFn: () => fetchStorageInUse(),
|
||||
staleTime: 1000 * 60 * 60 * 1, // 1 hour
|
||||
})
|
||||
|
||||
const { mutate: clearDownloads } = useMutation({
|
||||
|
||||
@@ -90,7 +90,6 @@ const PlayerContextInitializer = () => {
|
||||
nowPlayingJson ? JSON.parse(nowPlayingJson) : undefined,
|
||||
)
|
||||
|
||||
const [initialized, setInitialized] = useState<boolean>(false)
|
||||
const [repeatMode, setRepeatMode] = useState<RepeatMode>(
|
||||
repeatModeJson ? JSON.parse(repeatModeJson) : RepeatMode.Off,
|
||||
)
|
||||
@@ -579,26 +578,6 @@ const PlayerContextInitializer = () => {
|
||||
}
|
||||
}, [currentIndex, playQueue])
|
||||
|
||||
/**
|
||||
* Initialize the player. This is used to load the queue from the {@link QueueProvider}
|
||||
* and set it to the player if we have already completed the onboarding process
|
||||
* and the user has a valid queue in storage
|
||||
*/
|
||||
useEffect(() => {
|
||||
console.debug('Initialized', initialized)
|
||||
console.debug('Play queue length', playQueue.length)
|
||||
console.debug('Current index', currentIndex)
|
||||
if (playQueue.length > 0 && currentIndex > -1 && !initialized) {
|
||||
TrackPlayer.setQueue(playQueue)
|
||||
TrackPlayer.skip(currentIndex)
|
||||
console.debug('Loaded queue from storage')
|
||||
setInitialized(true)
|
||||
} else if (queueRef === 'Recently Played' && currentIndex === -1) {
|
||||
console.debug('Not loading queue as it is empty')
|
||||
setInitialized(true)
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Clean up prefetched track IDs when the current index changes significantly
|
||||
*/
|
||||
|
||||
@@ -161,10 +161,10 @@ const QueueContextInitailizer = () => {
|
||||
* {@link Event.PlaybackActiveTrackChanged} events until that mutation has settled
|
||||
*/
|
||||
const [skipping, setSkipping] = useState<boolean>(false)
|
||||
const [initialized, setInitialized] = useState<boolean>(false)
|
||||
|
||||
const [shuffled, setShuffled] = useState<boolean>(shuffledInit ?? false)
|
||||
|
||||
const [initialized, setInitialized] = useState<boolean>(false)
|
||||
//#endregion State
|
||||
|
||||
//#region Context
|
||||
@@ -178,13 +178,10 @@ const QueueContextInitailizer = () => {
|
||||
useTrackPlayerEvents(
|
||||
[Event.PlaybackActiveTrackChanged],
|
||||
async ({ index, track }: { index?: number | undefined; track?: Track | undefined }) => {
|
||||
console.debug(`Active Track Changed to: ${index}. Skipping: ${skipping}`)
|
||||
if (skipping) return
|
||||
|
||||
// We get an event emitted when the queue is loaded from storage for the first time
|
||||
// This is the most convenient place I could find to flip this boolean and start
|
||||
// listening to emitted updates
|
||||
if (!initialized) return setInitialized(true)
|
||||
console.debug(
|
||||
`Active Track Changed to: ${index}. Skipping: ${skipping}, Initialized: ${initialized}`,
|
||||
)
|
||||
if (skipping || !initialized) return
|
||||
|
||||
let newIndex = -1
|
||||
|
||||
@@ -547,7 +544,6 @@ const QueueContextInitailizer = () => {
|
||||
setSkipping(true)
|
||||
},
|
||||
onSuccess: async (data: void, { startPlayback }: QueueMutation) => {
|
||||
setInitialized(true)
|
||||
trigger('notificationSuccess')
|
||||
console.debug(`Loaded new queue`)
|
||||
|
||||
@@ -717,6 +713,20 @@ const QueueContextInitailizer = () => {
|
||||
|
||||
//#region useEffect(s)
|
||||
|
||||
/**
|
||||
* Initialization
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (playQueue.length > 0 && currentIndex > -1 && !initialized) {
|
||||
TrackPlayer.setQueue(playQueue)
|
||||
TrackPlayer.skip(currentIndex)
|
||||
setInitialized(true)
|
||||
} else {
|
||||
console.debug(`No queue to initialize from`)
|
||||
setInitialized(true)
|
||||
}
|
||||
}, [initialized])
|
||||
|
||||
/**
|
||||
* Store play queue in storage when it changes
|
||||
*/
|
||||
|
||||
@@ -56,7 +56,6 @@ const PlaylistContextInitializer = (playlist: BaseItemDto) => {
|
||||
return response.data.Items ? response.data.Items! : []
|
||||
})
|
||||
},
|
||||
staleTime: 1000 * 60 * 60 * 2, // 2 hours, since these are mutable
|
||||
})
|
||||
|
||||
const useUpdatePlaylist = useMutation({
|
||||
|
||||
@@ -11,7 +11,6 @@ import { useJellifyContext } from '..'
|
||||
|
||||
interface SetFavoriteMutation {
|
||||
item: BaseItemDto
|
||||
setFavorite: React.Dispatch<SetStateAction<boolean>>
|
||||
onToggle?: () => void
|
||||
}
|
||||
|
||||
@@ -27,7 +26,7 @@ const JellifyUserDataContextInitializer = () => {
|
||||
itemId: mutation.item.Id!,
|
||||
})
|
||||
},
|
||||
onSuccess: ({ data }, { item, setFavorite, onToggle }) => {
|
||||
onSuccess: ({ data }, { item, onToggle }) => {
|
||||
// Burnt.alert({
|
||||
// title: `Added favorite`,
|
||||
// duration: 1,
|
||||
@@ -40,7 +39,6 @@ const JellifyUserDataContextInitializer = () => {
|
||||
|
||||
trigger('notificationSuccess')
|
||||
|
||||
setFavorite(true)
|
||||
if (onToggle) onToggle()
|
||||
|
||||
// Force refresh of track user data
|
||||
@@ -54,7 +52,7 @@ const JellifyUserDataContextInitializer = () => {
|
||||
itemId: mutation.item.Id!,
|
||||
})
|
||||
},
|
||||
onSuccess: ({ data }, { item, setFavorite, onToggle }) => {
|
||||
onSuccess: ({ data }, { item, onToggle }) => {
|
||||
// Burnt.alert({
|
||||
// title: `Removed favorite`,
|
||||
// duration: 1,
|
||||
@@ -65,7 +63,6 @@ const JellifyUserDataContextInitializer = () => {
|
||||
type: 'error',
|
||||
})
|
||||
trigger('notificationSuccess')
|
||||
setFavorite(false)
|
||||
|
||||
if (onToggle) onToggle()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user