diff --git a/maestro/flows/flow-0.yaml b/maestro/flows/flow-0.yaml index b65487ac..4d3d47d9 100644 --- a/maestro/flows/flow-0.yaml +++ b/maestro/flows/flow-0.yaml @@ -2,6 +2,6 @@ appId: com.jellify --- - clearState # clears the state of the current app - launchApp -- runFlow: ../tests/login.yaml -- runFlow: ../tests/musicplayer.yaml -- runFlow: ../tests/search.yaml \ No newline at end of file +- runFlow: ../tests/1-login.yaml +- runFlow: ../tests/2-library.yaml +- runFlow: ../tests/3-musicplayer.yaml \ No newline at end of file diff --git a/maestro/flows/flow-1.yaml b/maestro/flows/flow-1.yaml index 761c7fb3..47fc06d6 100644 --- a/maestro/flows/flow-1.yaml +++ b/maestro/flows/flow-1.yaml @@ -1,4 +1,5 @@ appId: com.jellify --- -- runFlow: ../tests/library.yaml -- runFlow: ../tests/settings.yaml +- runFlow: ../tests/4-search.yaml +- runFlow: ../tests/5-discover.yaml +- runFlow: ../tests/6-settings.yaml diff --git a/maestro/tests/login.yaml b/maestro/tests/1-login.yaml similarity index 100% rename from maestro/tests/login.yaml rename to maestro/tests/1-login.yaml diff --git a/maestro/tests/musiclibrary.yaml b/maestro/tests/2-library.yaml similarity index 100% rename from maestro/tests/musiclibrary.yaml rename to maestro/tests/2-library.yaml diff --git a/maestro/tests/search.yaml b/maestro/tests/3-search.yaml similarity index 100% rename from maestro/tests/search.yaml rename to maestro/tests/3-search.yaml diff --git a/maestro/tests/musicplayer.yaml b/maestro/tests/4-musicplayer.yaml similarity index 100% rename from maestro/tests/musicplayer.yaml rename to maestro/tests/4-musicplayer.yaml diff --git a/maestro/tests/5-discover.yaml b/maestro/tests/5-discover.yaml new file mode 100644 index 00000000..a5da8404 --- /dev/null +++ b/maestro/tests/5-discover.yaml @@ -0,0 +1,20 @@ +appId: com.jellify +--- +# Wait for app to be ready, then navigate to Settings tab +- assertVisible: + id: "discover-tab-button" + +# Navigate to Library tab using text +- tapOn: + id: "discover-tab-button" + +# Verify we're on the library page +- assertVisible: + id: "discover-recently-added" + +- assertVisible: + id: "discover-public-playlists" + +- assertVisible: + id: "discover-suggested-artists" + diff --git a/maestro/tests/settings.yaml b/maestro/tests/6-settings.yaml similarity index 100% rename from maestro/tests/settings.yaml rename to maestro/tests/6-settings.yaml diff --git a/package.json b/package.json index 9a93b746..269757d9 100644 --- a/package.json +++ b/package.json @@ -139,4 +139,4 @@ "node": ">=18" }, "packageManager": "yarn@1.22.22" -} \ No newline at end of file +} diff --git a/src/api/queries/artist.ts b/src/api/queries/artist.ts index 3c798813..fd42646a 100644 --- a/src/api/queries/artist.ts +++ b/src/api/queries/artist.ts @@ -37,7 +37,7 @@ export function fetchArtists( startIndex: page * QueryConfig.limits.library, limit: QueryConfig.limits.library, isFavorite: isFavorite, - fields: [ItemFields.SortName], + fields: [ItemFields.SortName, ItemFields.ChildCount], }) .then((response) => { console.debug('Artists Response received') diff --git a/src/api/queries/frequents.ts b/src/api/queries/frequents.ts index 45e63e3d..a97ebba8 100644 --- a/src/api/queries/frequents.ts +++ b/src/api/queries/frequents.ts @@ -93,10 +93,12 @@ export function fetchFrequentlyPlayedArtists( ) }) .then((artistsWithPlayCounts) => { - console.debug('Fetching artists', artistsWithPlayCounts) - const artists = artistsWithPlayCounts.map((artist) => { - return fetchItem(api, artist.artistId) - }) + console.debug('Fetching artists') + const artists = artistsWithPlayCounts + .sort((a, b) => b.playCount - a.playCount) + .map((artist) => { + return fetchItem(api, artist.artistId) + }) return Promise.all(artists) }) diff --git a/src/api/queries/suggestions.ts b/src/api/queries/suggestions.ts index 354f95e4..5744f48e 100644 --- a/src/api/queries/suggestions.ts +++ b/src/api/queries/suggestions.ts @@ -42,3 +42,35 @@ export async function fetchSearchSuggestions( }) }) } + +export async function fetchArtistSuggestions( + api: Api | undefined, + user: JellifyUser | undefined, + libraryId: string | undefined, + page: number, +): Promise { + 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') + + getItemsApi(api) + .getItems({ + parentId: libraryId, + userId: user.id, + recursive: true, + limit: 50, + startIndex: page * 50, + includeItemTypes: [BaseItemKind.MusicArtist], + fields: ['ChildCount'], + sortBy: ['Random'], + }) + .then(({ data }) => { + if (data.Items) resolve(data.Items) + else resolve([]) + }) + .catch((error) => { + reject(error) + }) + }) +} diff --git a/src/components/Artists/screen.tsx b/src/components/Artists/screen.tsx index d091b17c..8eac0ac3 100644 --- a/src/components/Artists/screen.tsx +++ b/src/components/Artists/screen.tsx @@ -1,6 +1,6 @@ import React from 'react' import Artists from './component' -import { ArtistsProps, StackParamList } from '../types' +import { ArtistsProps } from '../types' export default function ArtistsScreen({ navigation, diff --git a/src/components/Discover/component.tsx b/src/components/Discover/component.tsx index 7748dc2c..7f2bb0f2 100644 --- a/src/components/Discover/component.tsx +++ b/src/components/Discover/component.tsx @@ -1,18 +1,20 @@ import React from 'react' -import { getToken, ScrollView } from 'tamagui' +import { getToken, ScrollView, Separator, View } from 'tamagui' import RecentlyAdded from './helpers/just-added' import { NativeStackNavigationProp } from '@react-navigation/native-stack' import { StackParamList } from '../types' import { useDiscoverContext } from '../../providers/Discover' import { RefreshControl } from 'react-native' import PublicPlaylists from './helpers/public-playlists' +import SuggestedArtists from './helpers/suggested-artists' export default function Index({ navigation, }: { navigation: NativeStackNavigationProp }): React.JSX.Element { - const { refreshing, refresh, recentlyAdded, publicPlaylists } = useDiscoverContext() + const { refreshing, refresh, recentlyAdded, publicPlaylists, suggestedArtistsInfiniteQuery } = + useDiscoverContext() return ( } > - {recentlyAdded && } - {publicPlaylists && } + {recentlyAdded && ( + + + + + )} + + {publicPlaylists && ( + + + + + )} + + {suggestedArtistsInfiniteQuery.data && ( + + + + + )} ) } diff --git a/src/components/Discover/helpers/fresh-mixes.tsx b/src/components/Discover/helpers/fresh-mixes.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/Discover/helpers/suggested-artists.tsx b/src/components/Discover/helpers/suggested-artists.tsx new file mode 100644 index 00000000..2c84a80b --- /dev/null +++ b/src/components/Discover/helpers/suggested-artists.tsx @@ -0,0 +1,47 @@ +import { View, XStack } from 'tamagui' +import Icon from '../../Global/components/icon' +import HorizontalCardList from '../../Global/components/horizontal-list' +import { ItemCard } from '../../Global/components/item-card' +import { NativeStackNavigationProp } from '@react-navigation/native-stack' +import { useDiscoverContext } from '../../../providers/Discover' +import { H4 } from '../../Global/helpers/text' +import { StackParamList } from '../../types' + +export default function SuggestedArtists({ + navigation, +}: { + navigation: NativeStackNavigationProp +}): React.JSX.Element { + const { suggestedArtistsInfiniteQuery } = useDiscoverContext() + return ( + + { + navigation.navigate('SuggestedArtists', { + artistsInfiniteQuery: suggestedArtistsInfiniteQuery, + navigation: navigation, + }) + }} + > +

Suggested Artists

+ +
+ ( + { + navigation.navigate('Artist', { + artist: item, + }) + }} + /> + )} + /> +
+ ) +} diff --git a/src/components/Global/components/item-row.tsx b/src/components/Global/components/item-row.tsx index 1a68934c..6caed2f8 100644 --- a/src/components/Global/components/item-row.tsx +++ b/src/components/Global/components/item-row.tsx @@ -112,6 +112,11 @@ export default function ItemRow({ {item.Name ?? ''} + {item.Type === 'MusicArtist' && ( + + {`${item.ChildCount ?? 0} ${item.ChildCount === 1 ? 'Album' : 'Albums'}`} + + )} {(item.Type === 'Audio' || item.Type === 'MusicAlbum') && ( {item.AlbumArtist ?? 'Untitled Artist'} diff --git a/src/components/types.d.ts b/src/components/types.d.ts index 1e4e580d..ed35bd6f 100644 --- a/src/components/types.d.ts +++ b/src/components/types.d.ts @@ -70,6 +70,10 @@ export type StackParamList = { isFetchingNextPage: boolean refetch: () => void } + SuggestedArtists: { + artistsInfiniteQuery: UseInfiniteQueryResult + navigation: NativeStackNavigationProp + } LibraryScreen: undefined Library: undefined @@ -153,6 +157,8 @@ export type UserPlaylistsProps = NativeStackScreenProps export type RecentlyAddedProps = NativeStackScreenProps export type PublicPlaylistsProps = NativeStackScreenProps +export type SuggestedArtistsProps = NativeStackScreenProps + export type HomeArtistProps = NativeStackScreenProps export type ArtistAlbumsProps = NativeStackScreenProps export type ArtistEpsProps = NativeStackScreenProps @@ -169,7 +175,10 @@ export type TracksProps = NativeStackScreenProps export type ArtistsProps = { navigation: NativeStackNavigationProp - artistsInfiniteQuery: UseInfiniteQueryResult<(string | number | BaseItemDto)[], Error> + artistsInfiniteQuery: UseInfiniteQueryResult< + BaseItemDto[] | (string | number | BaseItemDto)[], + Error + > showAlphabeticalSelector: boolean artistPageParams?: RefObject> } diff --git a/src/enums/query-keys.ts b/src/enums/query-keys.ts index 309d48f4..7c912136 100644 --- a/src/enums/query-keys.ts +++ b/src/enums/query-keys.ts @@ -111,4 +111,9 @@ export enum QueryKeys { * Query representing the fetching of albums in an infinite query */ InfiniteAlbums = 'InfiniteAlbums', + + /** + * Query representing the fetching of suggested artists in an infinite query + */ + InfiniteSuggestedArtists = 'InfiniteSuggestedArtists', } diff --git a/src/providers/Discover/index.tsx b/src/providers/Discover/index.tsx index a00f5223..72c673b2 100644 --- a/src/providers/Discover/index.tsx +++ b/src/providers/Discover/index.tsx @@ -1,10 +1,17 @@ -import { InfiniteData, useInfiniteQuery, useQuery } from '@tanstack/react-query' +import { + InfiniteData, + InfiniteQueryObserverResult, + useInfiniteQuery, + UseInfiniteQueryResult, +} from '@tanstack/react-query' import { fetchRecentlyAdded, fetchRecentlyPlayed } from '../../api/queries/recents' import { QueryKeys } from '../../enums/query-keys' import { createContext, ReactNode, useContext, useState } from 'react' import { BaseItemDto } from '@jellyfin/sdk/lib/generated-client/models' import { useJellifyContext } from '..' import { fetchPublicPlaylists } from '../../api/queries/playlists' +import { fetchArtistSuggestions } from '../../api/queries/suggestions' + interface DiscoverContext { refreshing: boolean refresh: () => void @@ -24,6 +31,7 @@ interface DiscoverContext { isFetchingNextRecentlyPlayed: boolean isFetchingNextPublicPlaylists: boolean refetchPublicPlaylists: () => void + suggestedArtistsInfiniteQuery: UseInfiniteQueryResult } const DiscoverContextInitializer = () => { @@ -41,7 +49,8 @@ const DiscoverContextInitializer = () => { queryKey: [QueryKeys.RecentlyAddedAlbums, library?.musicLibraryId], queryFn: ({ pageParam }) => fetchRecentlyAdded(api, library, pageParam), select: (data) => data.pages.flatMap((page) => page), - getNextPageParam: (lastPage, pages) => (lastPage.length > 0 ? pages.length + 1 : undefined), + getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => + lastPage.length > 0 ? lastPageParam + 1 : undefined, initialPageParam: 0, }) @@ -56,7 +65,8 @@ const DiscoverContextInitializer = () => { queryKey: [QueryKeys.PublicPlaylists, library?.playlistLibraryId], queryFn: ({ pageParam }) => fetchPublicPlaylists(api, library, pageParam), select: (data) => data.pages.flatMap((page) => page), - getNextPageParam: (lastPage, pages) => (lastPage.length > 0 ? pages.length + 1 : undefined), + getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => + lastPage.length > 0 ? lastPageParam + 1 : undefined, initialPageParam: 0, }) @@ -70,10 +80,22 @@ const DiscoverContextInitializer = () => { } = useInfiniteQuery({ queryKey: [QueryKeys.RecentlyPlayed, library?.musicLibraryId], queryFn: ({ pageParam }) => fetchRecentlyPlayed(api, user, library, pageParam), - getNextPageParam: (lastPage, pages) => (lastPage.length > 0 ? pages.length + 1 : undefined), + getNextPageParam: (lastPage, allPages, lastPageParam, allPageParams) => + lastPage.length > 0 ? lastPageParam + 1 : undefined, initialPageParam: 0, }) + const suggestedArtistsInfiniteQuery = useInfiniteQuery({ + queryKey: [QueryKeys.InfiniteSuggestedArtists, user?.id, library?.musicLibraryId], + queryFn: ({ pageParam }) => + fetchArtistSuggestions(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, + }) + const refresh = async () => { setRefreshing(true) @@ -81,6 +103,7 @@ const DiscoverContextInitializer = () => { refetchRecentlyAdded(), refetchRecentlyPlayed(), refetchPublicPlaylists(), + suggestedArtistsInfiniteQuery.refetch(), ]) setRefreshing(false) } @@ -104,6 +127,7 @@ const DiscoverContextInitializer = () => { isFetchingNextRecentlyPlayed, isFetchingNextPublicPlaylists, refetchPublicPlaylists, + suggestedArtistsInfiniteQuery, } } @@ -126,6 +150,45 @@ const DiscoverContext = createContext({ isFetchingNextRecentlyPlayed: false, isFetchingNextPublicPlaylists: false, refetchPublicPlaylists: () => {}, + suggestedArtistsInfiniteQuery: { + data: undefined, + error: null, + isEnabled: true, + isStale: false, + isRefetching: false, + isError: false, + isLoading: true, + isPending: true, + isFetching: true, + isSuccess: false, + isFetched: false, + hasPreviousPage: false, + refetch: async () => + Promise.resolve({} as InfiniteQueryObserverResult), + fetchNextPage: async () => + Promise.resolve({} as InfiniteQueryObserverResult), + hasNextPage: false, + isFetchingNextPage: false, + isFetchPreviousPageError: false, + isFetchNextPageError: false, + isFetchingPreviousPage: false, + isLoadingError: false, + isRefetchError: false, + isPlaceholderData: false, + status: 'pending', + fetchStatus: 'idle', + dataUpdatedAt: 0, + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isFetchedAfterMount: false, + isInitialLoading: false, + isPaused: false, + fetchPreviousPage: async () => + Promise.resolve({} as InfiniteQueryObserverResult), + promise: Promise.resolve([]), + }, }) export const DiscoverProvider: ({ children }: { children: ReactNode }) => React.JSX.Element = ({ diff --git a/src/screens/Discover/artists.tsx b/src/screens/Discover/artists.tsx new file mode 100644 index 00000000..3777756c --- /dev/null +++ b/src/screens/Discover/artists.tsx @@ -0,0 +1,12 @@ +import Artists from '../../components/Artists/component' +import { SuggestedArtistsProps } from '../../components/types' + +export default function SuggestedArtists({ navigation, route }: SuggestedArtistsProps) { + return ( + + ) +} diff --git a/src/screens/Discover/index.tsx b/src/screens/Discover/index.tsx index 89541843..fcd2b226 100644 --- a/src/screens/Discover/index.tsx +++ b/src/screens/Discover/index.tsx @@ -10,6 +10,7 @@ import { useTheme } from 'tamagui' import RecentlyAdded from './albums' import PublicPlaylists from './playlists' import { PlaylistScreen } from '../Playlist' +import SuggestedArtists from './artists' export const DiscoverStack = createNativeStackNavigator() @@ -82,6 +83,14 @@ export function Discover(): React.JSX.Element { }} /> + +