mirror of
https://github.com/Jellify-Music/App.git
synced 2026-04-20 16:50:33 -05:00
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:
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,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: {
|
||||
|
||||
Vendored
+2
-2
@@ -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,
|
||||
|
||||
@@ -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(' • ')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user