Home Screen Refresh Time Reduction, Add Random Albums to Discover Screen (#932)

* reduce home screen refresh time

eliminate unnecessary network calls

* adds a suggested albums row to the discover screen
This commit is contained in:
Violet Caulfield
2026-01-21 15:04:29 -06:00
committed by GitHub
parent dc26c3a909
commit 68a8dd316e
14 changed files with 233 additions and 77 deletions
+4 -1
View File
@@ -1,7 +1,7 @@
import { useMutation } from '@tanstack/react-query'
import { useRecentlyAddedAlbums } from '../../queries/album'
import { usePublicPlaylists } from '../../queries/playlist'
import { useDiscoverArtists } from '../../queries/suggestions'
import { useDiscoverAlbums, useDiscoverArtists } from '../../queries/suggestions'
const useDiscoverQueries = () => {
const { refetch: refetchRecentlyAdded } = useRecentlyAddedAlbums()
@@ -10,12 +10,15 @@ const useDiscoverQueries = () => {
const { refetch: refetchArtistSuggestions } = useDiscoverArtists()
const { refetch: refetchAlbumSuggestions } = useDiscoverAlbums()
return useMutation({
mutationFn: async () =>
await Promise.allSettled([
refetchRecentlyAdded(),
refetchPublicPlaylists(),
refetchArtistSuggestions(),
refetchAlbumSuggestions(),
]),
networkMode: 'online',
})
+7 -19
View File
@@ -2,9 +2,9 @@ import { useInfiniteQuery } from '@tanstack/react-query'
import { FrequentlyPlayedArtistsQueryKey, FrequentlyPlayedTracksQueryKey } from './keys'
import { fetchFrequentlyPlayed, fetchFrequentlyPlayedArtists } from './utils/frequents'
import { ApiLimits, MaxPages } from '../../../configs/query.config'
import { isUndefined } from 'lodash'
import { useApi, useJellifyLibrary, useJellifyUser } from '../../../stores'
import { getApi, getUser, useJellifyLibrary } from '../../../stores'
import { ONE_DAY } from '../../../constants/query-client'
import { FrequentlyPlayedTracksQuery } from './queries'
const FREQUENTS_QUERY_CONFIG = {
maxPages: MaxPages.Home,
@@ -13,29 +13,18 @@ const FREQUENTS_QUERY_CONFIG = {
} as const
export const useFrequentlyPlayedTracks = () => {
const api = useApi()
const [user] = useJellifyUser()
const api = getApi()
const user = getUser()
const [library] = useJellifyLibrary()
return useInfiniteQuery({
queryKey: FrequentlyPlayedTracksQueryKey(user, library),
queryFn: ({ pageParam }) => fetchFrequentlyPlayed(api, library, pageParam),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
return lastPage.length === ApiLimits.Home ? lastPageParam + 1 : undefined
},
...FREQUENTS_QUERY_CONFIG,
})
return useInfiniteQuery(FrequentlyPlayedTracksQuery(user, library, api))
}
export const useFrequentlyPlayedArtists = () => {
const api = useApi()
const [user] = useJellifyUser()
const api = getApi()
const user = getUser()
const [library] = useJellifyLibrary()
const { data: frequentlyPlayedTracks } = useFrequentlyPlayedTracks()
return useInfiniteQuery({
queryKey: FrequentlyPlayedArtistsQueryKey(user, library),
queryFn: ({ pageParam }) => fetchFrequentlyPlayedArtists(api, user, library, pageParam),
@@ -44,7 +33,6 @@ export const useFrequentlyPlayedArtists = () => {
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
return lastPage.length > 0 ? lastPageParam + 1 : undefined
},
enabled: !isUndefined(frequentlyPlayedTracks),
...FREQUENTS_QUERY_CONFIG,
})
}
+34
View File
@@ -0,0 +1,34 @@
import { Api } from '@jellyfin/sdk'
import { FrequentlyPlayedTracksQueryKey } from './keys'
import { JellifyLibrary } from '@/src/types/JellifyLibrary'
import { JellifyUser } from '@/src/types/JellifyUser'
import { fetchFrequentlyPlayed } from './utils/frequents'
import { InfiniteData, QueryKey, UseInfiniteQueryOptions } from '@tanstack/react-query'
import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models/base-item-dto'
import { ApiLimits, MaxPages } from '../../../configs/query.config'
import { ONE_DAY } from '../../../constants/query-client'
const FREQUENTS_QUERY_CONFIG = {
maxPages: MaxPages.Home,
staleTime: ONE_DAY,
refetchOnMount: false,
} as const
export const FrequentlyPlayedTracksQuery: (
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
api: Api | undefined,
) => UseInfiniteQueryOptions<BaseItemDto[], Error, BaseItemDto[], QueryKey, number> = (
user: JellifyUser | undefined,
library: JellifyLibrary | undefined,
api: Api | undefined,
) => ({
queryKey: FrequentlyPlayedTracksQueryKey(user, library),
queryFn: ({ pageParam }) => fetchFrequentlyPlayed(api, library, pageParam),
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => {
return lastPage.length === ApiLimits.Home ? lastPageParam + 1 : undefined
},
...FREQUENTS_QUERY_CONFIG,
})
+39 -45
View File
@@ -8,12 +8,11 @@ import { getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { Api } from '@jellyfin/sdk'
import { isEmpty, isNull, isUndefined } from 'lodash'
import { JellifyLibrary } from '../../../../types/JellifyLibrary'
import { fetchItem } from '../../item'
import { ApiLimits } from '../../../../configs/query.config'
import { JellifyUser } from '@/src/types/JellifyUser'
import { queryClient } from '../../../../constants/query-client'
import { InfiniteData } from '@tanstack/react-query'
import { FrequentlyPlayedTracksQueryKey } from '../keys'
import { QueryKey } from '@tanstack/react-query'
import { FrequentlyPlayedTracksQuery } from '../queries'
/**
* Fetches the 100 most frequently played items from the user's library
@@ -69,51 +68,46 @@ export function fetchFrequentlyPlayedArtists(
if (isUndefined(api)) return reject('Client instance not set')
if (isUndefined(library)) return reject('Library instance not set')
const frequentlyPlayed = queryClient.getQueryData<InfiniteData<BaseItemDto[]>>(
FrequentlyPlayedTracksQueryKey(user, library),
)
if (isUndefined(frequentlyPlayed)) {
return reject('Frequently played tracks not found in query client')
}
const artistIdWithPlayCount = frequentlyPlayed.pages[page]
.filter(
(track) =>
!isUndefined(track.AlbumArtists) &&
!isNull(track.AlbumArtists) &&
!isEmpty(track.AlbumArtists) &&
!isUndefined(track.AlbumArtists![0].Id),
queryClient
.ensureInfiniteQueryData<BaseItemDto[], Error, BaseItemDto[], QueryKey, number>(
FrequentlyPlayedTracksQuery(user, library, api),
)
.map(({ AlbumArtists, UserData }) => {
return {
artistId: AlbumArtists![0].Id!,
playCount: UserData?.PlayCount ?? 0,
}
})
.then((frequentlyPlayed) => {
const artistWithPlayCount = frequentlyPlayed.pages[page]
.filter(
(track) =>
!isUndefined(track.AlbumArtists) &&
!isNull(track.AlbumArtists) &&
!isEmpty(track.AlbumArtists) &&
!isUndefined(track.AlbumArtists![0].Id),
)
.map(({ AlbumArtists, UserData }) => {
return {
artist: AlbumArtists![0],
playCount: UserData?.PlayCount ?? 0,
}
})
console.info('Artist IDs with play count:', artistIdWithPlayCount.length)
const sortedArtists = artistWithPlayCount
.reduce(
(acc, { artist, playCount }) => {
const existing = acc.find((a) => a.artist.Id === artist.Id)
if (existing) {
existing.playCount += playCount
} else {
acc.push({ artist, playCount })
}
return acc
},
[] as { artist: BaseItemDto; playCount: number }[],
)
.sort((a, b) => b.playCount - a.playCount)
const artistPromises = artistIdWithPlayCount
.reduce(
(acc, { artistId, playCount }) => {
const existing = acc.find((a) => a.artistId === artistId)
if (existing) {
existing.playCount += playCount
} else {
acc.push({ artistId, playCount })
}
return acc
},
[] as { artistId: string; playCount: number }[],
)
.sort((a, b) => b.playCount - a.playCount)
.map((artist) => {
return fetchItem(api, artist.artistId)
})
return Promise.all(artistPromises)
.then((artists) => {
return resolve(artists.filter((artist) => !isUndefined(artist)))
return resolve(
sortedArtists
.map(({ artist }) => artist)
.filter((artist) => !isUndefined(artist)),
)
})
.catch((error) => {
reject(error)
+24 -1
View File
@@ -1,6 +1,10 @@
import { useInfiniteQuery, useQuery } from '@tanstack/react-query'
import { SuggestionQueryKeys } from './keys'
import { fetchArtistSuggestions, fetchSearchSuggestions } from './utils/suggestions'
import {
fetchAlbumSuggestions,
fetchArtistSuggestions,
fetchSearchSuggestions,
} from './utils/suggestions'
import { getApi, getUser, useJellifyLibrary } from '../../../stores'
import { isUndefined } from 'lodash'
import fetchSimilarArtists, { fetchSimilarItems } from './utils/similar'
@@ -44,6 +48,25 @@ export const useDiscoverArtists = () => {
})
}
export const useDiscoverAlbums = () => {
const api = getApi()
const [library] = useJellifyLibrary()
const user = getUser()
return useInfiniteQuery({
queryKey: [SuggestionQueryKeys.InfiniteAlbumSuggestions, user?.id, library?.musicLibraryId],
queryFn: ({ pageParam }) =>
fetchAlbumSuggestions(api, user, library?.musicLibraryId, pageParam),
getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) =>
lastPage.length > 0 ? lastPageParam + 1 : undefined,
select: (data) => data.pages.flatMap((page) => page),
initialPageParam: 0,
maxPages: 2,
})
}
export const useSimilarItems = (item: BaseItemDto) => {
const api = getApi()
+1
View File
@@ -2,4 +2,5 @@ export enum SuggestionQueryKeys {
InfiniteArtistSuggestions,
SearchSuggestions,
SimilarItems,
InfiniteAlbumSuggestions,
}
@@ -1,5 +1,11 @@
import { getArtistsApi, getItemsApi } from '@jellyfin/sdk/lib/utils/api'
import { BaseItemDto, BaseItemKind, ItemFields } from '@jellyfin/sdk/lib/generated-client/models'
import {
BaseItemDto,
BaseItemKind,
ItemFields,
ItemSortBy,
SortOrder,
} from '@jellyfin/sdk/lib/generated-client/models'
import { Api } from '@jellyfin/sdk'
import { isUndefined } from 'lodash'
import { JellifyUser } from '../../../../types/JellifyUser'
@@ -73,3 +79,38 @@ export async function fetchArtistSuggestions(
})
})
}
export async function fetchAlbumSuggestions(
api: Api | undefined,
user: JellifyUser | undefined,
libraryId: string | undefined,
page: number,
): Promise<BaseItemDto[]> {
return new Promise((resolve, reject) => {
if (isUndefined(api)) return reject('Client instance not set')
if (isUndefined(user)) return reject('User has not been set')
if (isUndefined(libraryId)) return reject('Library has not been set')
console.debug(`fetching albums at page ${page}`)
getItemsApi(api)
.getItems({
parentId: libraryId,
recursive: true,
userId: user.id,
limit: 50,
startIndex: page * 50,
includeItemTypes: [BaseItemKind.MusicAlbum],
sortBy: [ItemSortBy.Random, ItemSortBy.SortName],
sortOrder: [SortOrder.Ascending],
})
.then(({ data }) => {
console.debug(`fetched albums at page ${page}`, data.Items)
if (data.Items) resolve(data.Items)
else resolve([])
})
.catch((error) => {
reject(error)
})
})
}
+3
View File
@@ -7,6 +7,7 @@ import useDiscoverQueries from '../../api/mutations/discover'
import { useIsRestoring } from '@tanstack/react-query'
import { useRecentlyAddedAlbums } from '../../api/queries/album'
import { Platform, RefreshControl } from 'react-native'
import SuggestedAlbums from './helpers/suggested-albums'
export default function Index(): React.JSX.Element {
const { mutateAsync: refreshAsync, isPending: refreshing } = useDiscoverQueries()
@@ -49,6 +50,8 @@ function DiscoverContent() {
<PublicPlaylists />
<SuggestedArtists />
<SuggestedAlbums />
</YStack>
)
}
@@ -30,7 +30,7 @@ export default function RecentlyAdded(): React.JSX.Element | null {
<XStack
alignItems='center'
onPress={() => {
navigation.navigate('RecentlyAdded', {
navigation.navigate('Albums', {
albumsInfiniteQuery: recentlyAddedAlbumsInfinityQuery,
})
}}
@@ -0,0 +1,69 @@
import navigationRef from '../../../../navigation'
import { formatArtistNames } from '../../../utils/formatting/artist-names'
import Animated, { Easing, FadeIn, FadeOut, LinearTransition } from 'react-native-reanimated'
import ItemCard from '../../Global/components/item-card'
import HorizontalCardList from '../../Global/components/horizontal-list'
import { XStack } from 'tamagui'
import Icon from '../../Global/components/icon'
import { H5 } from '../../Global/helpers/text'
import { useNavigation } from '@react-navigation/native'
import { NativeStackNavigationProp } from '@react-navigation/native-stack'
import DiscoverStackParamList from '../../../screens/Discover/types'
import { useDiscoverAlbums } from '../../../api/queries/suggestions'
export default function SuggestedAlbums() {
const suggestedAlbumsInfiniteQuery = useDiscoverAlbums()
const navigation = useNavigation<NativeStackNavigationProp<DiscoverStackParamList>>()
const suggestedAlbumsExist =
suggestedAlbumsInfiniteQuery.data && suggestedAlbumsInfiniteQuery.data.length > 0
return suggestedAlbumsExist ? (
<Animated.View
entering={FadeIn.easing(Easing.in(Easing.ease))}
exiting={FadeOut.easing(Easing.out(Easing.ease))}
layout={LinearTransition.springify()}
testID='discover-suggested-albums'
style={{
flex: 1,
}}
>
<XStack
alignItems='center'
onPress={() => {
navigation.navigate('Albums', {
albumsInfiniteQuery: suggestedAlbumsInfiniteQuery,
})
}}
marginLeft={'$2'}
>
<H5>More from the vault</H5>
<Icon name='arrow-right' />
</XStack>
<HorizontalCardList
data={suggestedAlbumsInfiniteQuery.data?.slice(0, 10) ?? []}
renderItem={({ item }) => (
<ItemCard
squared
caption={item.Name}
subCaption={formatArtistNames(item.Artists)}
size={'$10'}
item={item}
onPress={() => {
navigation.navigate('Album', {
album: item,
})
}}
onLongPress={() =>
navigationRef.navigate('Context', {
item,
navigation,
})
}
/>
)}
/>
</Animated.View>
) : null
}
+2 -3
View File
@@ -1,8 +1,7 @@
import { RouteProp } from '@react-navigation/native'
import Albums from '../../components/Albums/component'
import DiscoverStackParamList, { RecentlyAddedProps } from './types'
import { DiscoverAlbumsProps } from './types'
export default function RecentlyAdded({ route }: RecentlyAddedProps): React.JSX.Element {
export default function DiscoverAlbums({ route }: DiscoverAlbumsProps): React.JSX.Element {
return (
<Albums
albumsInfiniteQuery={route.params.albumsInfiniteQuery}
+3 -3
View File
@@ -3,7 +3,7 @@ import Index from '../../components/Discover/component'
import AlbumScreen from '../Album'
import ArtistScreen from '../Artist'
import { getTokenValue, useTheme } from 'tamagui'
import RecentlyAdded from './albums'
import DiscoverAlbums from './albums'
import PublicPlaylists from './playlists'
import { PlaylistScreen } from '../Playlist'
import SuggestedArtists from './artists'
@@ -65,8 +65,8 @@ export function Discover(): React.JSX.Element {
/>
<DiscoverStack.Screen
name='RecentlyAdded'
component={RecentlyAdded}
name='Albums'
component={DiscoverAlbums}
options={{
title: 'Recently Added',
headerTitleStyle: {
+2 -2
View File
@@ -5,7 +5,7 @@ import { UseInfiniteQueryResult } from '@tanstack/react-query'
type DiscoverStackParamList = BaseStackParamList & {
Discover: undefined
RecentlyAdded: {
Albums: {
albumsInfiniteQuery: UseInfiniteQueryResult<BaseItemDto[], Error>
}
PublicPlaylists: {
@@ -25,7 +25,7 @@ type DiscoverStackParamList = BaseStackParamList & {
export default DiscoverStackParamList
export type RecentlyAddedProps = NativeStackScreenProps<DiscoverStackParamList, 'RecentlyAdded'>
export type DiscoverAlbumsProps = NativeStackScreenProps<DiscoverStackParamList, 'Albums'>
export type PublicPlaylistsProps = NativeStackScreenProps<DiscoverStackParamList, 'PublicPlaylists'>
export type SuggestedArtistsProps = NativeStackScreenProps<
DiscoverStackParamList,
+2 -1
View File
@@ -3,6 +3,7 @@ export function formatArtistName(artistName: string | null | undefined): string
return artistName
}
export function formatArtistNames(artistNames: string[]): string {
export function formatArtistNames(artistNames: string[] | null | undefined): string {
if (!artistNames || artistNames.length === 0) return 'Unknown Artist'
return artistNames.map(formatArtistName).join(' • ')
}