diff --git a/src/screens/Downloads/index.tsx b/src/screens/Downloads/index.tsx index 1e74e02..efa918e 100644 --- a/src/screens/Downloads/index.tsx +++ b/src/screens/Downloads/index.tsx @@ -136,7 +136,7 @@ function Downloads() { - + diff --git a/src/screens/Music/stacks/Album.tsx b/src/screens/Music/stacks/Album.tsx index 66d2d1e..26fabb4 100644 --- a/src/screens/Music/stacks/Album.tsx +++ b/src/screens/Music/stacks/Album.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect } from 'react'; import { useRoute, RouteProp, useNavigation } from '@react-navigation/native'; import { useAppDispatch, useTypedSelector } from '@/store'; import TrackListView from './components/TrackListView'; -import { fetchAlbum, fetchTracksByAlbum } from '@/store/music/actions'; +import { fetchAlbum, fetchSimilarAlbums, fetchTracksByAlbum } from '@/store/music/actions'; import { differenceInDays } from 'date-fns'; import { ALBUM_CACHE_AMOUNT_OF_DAYS } from '@/CONSTANTS'; import { t } from '@/localisation'; @@ -33,6 +33,9 @@ function SimilarAlbum({ id }: { id: string }) { const handlePress = useCallback(() => { album && navigation.push('Album', { id, album }); }, [id, album, navigation]); + + console.log(getImage(album)); + return ( - + {album?.Name} {album?.Artists.join(', ')} @@ -62,6 +65,7 @@ const Album: React.FC = () => { const refresh = useCallback(() => { dispatch(fetchTracksByAlbum(id)); dispatch(fetchAlbum(id)); + dispatch(fetchSimilarAlbums(id)); }, [id, dispatch]); // Auto-fetch the track data periodically @@ -76,7 +80,7 @@ const Album: React.FC = () => { trackIds={albumTracks || []} title={album?.Name} artist={album?.AlbumArtist} - entityId={id} + entityId={album?.PrimaryImageItemId || album.Id} refresh={refresh} playButtonText={t('play-album')} shuffleButtonText={t('shuffle-album')} diff --git a/src/screens/Music/stacks/Albums.tsx b/src/screens/Music/stacks/Albums.tsx index 277b6b0..bfe944f 100644 --- a/src/screens/Music/stacks/Albums.tsx +++ b/src/screens/Music/stacks/Albums.tsx @@ -149,7 +149,7 @@ const Albums: React.FC = () => { { { key={item.Id} item={item} onPress={selectArtist} - imageURL={getImage(item.Id)} + imageURL={getImage(item)} /> ); diff --git a/src/screens/Music/stacks/Playlists.tsx b/src/screens/Music/stacks/Playlists.tsx index 4171177..25b85ce 100644 --- a/src/screens/Music/stacks/Playlists.tsx +++ b/src/screens/Music/stacks/Playlists.tsx @@ -70,14 +70,14 @@ const Playlists: React.FC = () => { {nextItem && diff --git a/src/screens/Music/stacks/RecentAlbums.tsx b/src/screens/Music/stacks/RecentAlbums.tsx index 810a30d..ffa8580 100644 --- a/src/screens/Music/stacks/RecentAlbums.tsx +++ b/src/screens/Music/stacks/RecentAlbums.tsx @@ -95,7 +95,7 @@ const RecentAlbums: React.FC = () => { - + {albums[item]?.Name} {albums[item]?.AlbumArtist} diff --git a/src/screens/Search/stacks/Search/index.tsx b/src/screens/Search/stacks/Search/index.tsx index 6074e64..7f364fd 100644 --- a/src/screens/Search/stacks/Search/index.tsx +++ b/src/screens/Search/stacks/Search/index.tsx @@ -298,7 +298,7 @@ export default function Search() { id={album.Id} onPress={selectAlbum} testID={`search-result-${album.Id}`}> - + diff --git a/src/screens/modals/SetJellyfinServer/index.tsx b/src/screens/modals/SetJellyfinServer/index.tsx index 8f9749c..1ddfdf6 100644 --- a/src/screens/modals/SetJellyfinServer/index.tsx +++ b/src/screens/modals/SetJellyfinServer/index.tsx @@ -9,6 +9,7 @@ import { t } from '@/localisation'; import useDefaultStyles from '@/components/Colors'; import { Text } from '@/components/Typography'; import { AppState, useAppDispatch } from '@/store'; +import { fetchRecentAlbums } from '@/store/music/actions'; export default function SetJellyfinServer() { @@ -25,7 +26,8 @@ export default function SetJellyfinServer() { const saveCredentials = useCallback((credentials: AppState['settings']['credentials']) => { if (credentials) { dispatch(setJellyfinCredentials(credentials)); - navigation.dispatch(StackActions.popToTop()); + navigation.dispatch(StackActions.popToTop()); + dispatch(fetchRecentAlbums()); } }, [navigation, dispatch]); diff --git a/src/screens/modals/TrackPopupMenu.tsx b/src/screens/modals/TrackPopupMenu.tsx index b5a0e12..7ed5ad7 100644 --- a/src/screens/modals/TrackPopupMenu.tsx +++ b/src/screens/modals/TrackPopupMenu.tsx @@ -75,7 +75,7 @@ function TrackPopupMenu() { return ( - +
{track?.Name}
{track?.AlbumArtist} {track?.Album ? '— ' + track?.Album : ''} diff --git a/src/store/music/actions.ts b/src/store/music/actions.ts index aaa3762..7f3083f 100644 --- a/src/store/music/actions.ts +++ b/src/store/music/actions.ts @@ -1,7 +1,7 @@ import { createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit'; import { Album, AlbumTrack, Playlist } from './types'; import { AsyncThunkAPI } from '..'; -import { retrieveAllAlbums, retrieveRecentAlbums, retrieveAlbumTracks, retrieveAlbum } from '@/utility/JellyfinApi/album'; +import { retrieveAllAlbums, retrieveRecentAlbums, retrieveAlbumTracks, retrieveAlbum, retrieveSimilarAlbums } from '@/utility/JellyfinApi/album'; import { retrieveAllPlaylists, retrievePlaylistTracks } from '@/utility/JellyfinApi/playlist'; import { searchItem } from '@/utility/JellyfinApi/search'; @@ -44,6 +44,11 @@ export const fetchAlbum = createAsyncThunk( retrieveAlbum, ); +export const fetchSimilarAlbums = createAsyncThunk( + '/albums/similar', + retrieveSimilarAlbums, +); + type SearchAndFetchResults = { albums: Album[]; results: (Album | AlbumTrack)[]; diff --git a/src/store/music/index.ts b/src/store/music/index.ts index ae42546..3b452d0 100644 --- a/src/store/music/index.ts +++ b/src/store/music/index.ts @@ -8,7 +8,8 @@ import { playlistAdapter, fetchAllPlaylists, fetchTracksByPlaylist, - fetchAlbum + fetchAlbum, + fetchSimilarAlbums } from './actions'; import { createSlice } from '@reduxjs/toolkit'; import { Album, AlbumTrack, Playlist } from './types'; @@ -79,7 +80,15 @@ const music = createSlice({ }); builder.addCase(fetchAlbum.pending, (state) => { state.albums.isLoading = true; }); builder.addCase(fetchAlbum.rejected, (state) => { state.albums.isLoading = false; }); - + + /** + * Fetch similar albums + */ + builder.addCase(fetchSimilarAlbums.fulfilled, (state, { payload, meta }) => { + albumAdapter.upsertMany(state.albums, payload); + state.albums.entities[meta.arg].Similar = payload.map((a) => a.Id); + }); + /** * Fetch most recent albums */ diff --git a/src/store/music/types.ts b/src/store/music/types.ts index 109d0dd..c908774 100644 --- a/src/store/music/types.ts +++ b/src/store/music/types.ts @@ -68,6 +68,8 @@ export interface Album { DateCreated: string; Overview?: string; Similar?: string[]; + /** Emby potentially carries different ids for primary images */ + PrimaryImageItemId?: string; } export interface AlbumTrack { @@ -124,7 +126,3 @@ export interface Playlist { Tracks?: string[]; lastRefreshed?: number; } - -export interface SimilarAlbum { - Id: string; -} diff --git a/src/utility/JellyfinApi/album.ts b/src/utility/JellyfinApi/album.ts index 1db69c7..9c0dc89 100644 --- a/src/utility/JellyfinApi/album.ts +++ b/src/utility/JellyfinApi/album.ts @@ -1,4 +1,4 @@ -import { Album, AlbumTrack, SimilarAlbum } from '@/store/music/types'; +import { Album, AlbumTrack } from '@/store/music/types'; import { fetchApi } from './lib'; import {retrieveAndInjectLyricsToTracks} from '@/utility/JellyfinApi/lyrics.ts'; @@ -26,11 +26,15 @@ export async function retrieveAllAlbums() { * Retrieve a single album */ export async function retrieveAlbum(id: string): Promise { - const Similar = await fetchApi<{ Items: SimilarAlbum[] }>(({ user_id }) => `/Items/${id}/Similar?userId=${user_id}&limit=12`) - .then((albums) => albums!.Items.map((a) => a.Id)); + return fetchApi(({ user_id }) => `/Users/${user_id}/Items/${id}`); +} - return fetchApi(({ user_id }) => `/Users/${user_id}/Items/${id}`) - .then(album => ({ ...album!, Similar })); +/** + * Retrieve albums that are similar to the provided album + */ +export async function retrieveSimilarAlbums(id: string): Promise { + return fetchApi<{ Items: Album[] }>(({ user_id }) => `/Items/${id}/Similar?userId=${user_id}&limit=12`) + .then((albums) => albums!.Items); } const latestAlbumsOptions = { diff --git a/src/utility/JellyfinApi/lib.ts b/src/utility/JellyfinApi/lib.ts index db93383..0ce110f 100644 --- a/src/utility/JellyfinApi/lib.ts +++ b/src/utility/JellyfinApi/lib.ts @@ -1,6 +1,7 @@ -import type { AppState, Store } from '@/store'; +import { useTypedSelector, type AppState, type Store } from '@/store'; import { Platform } from 'react-native'; import { version } from '../../../package.json'; +import { Album, AlbumTrack, ArtistItem, Playlist } from '@/store/music/types'; type Credentials = AppState['settings']['credentials']; @@ -105,9 +106,13 @@ export async function fetchApi( /** * Retrieve an image URL for a given ItemId */ -export function getImage(ItemId: string): string { - const credentials = asyncFetchStore().getState().settings.credentials; - const uri = encodeURI(`${credentials?.uri}/Items/${ItemId}/Images/Primary?format=jpeg`); +export function getImage(ItemId: string | number, credentials?: AppState['settings']['credentials']): string { + // Either accept provided credentials, or retrieve them directly from the store + const { uri: serverUri } = credentials + ?? asyncFetchStore().getState().settings.credentials ?? {}; + + // Generate the uri and return + const uri = encodeURI(`${serverUri}/Items/${ItemId}/Images/Primary?format=jpeg`); return uri; } @@ -115,5 +120,20 @@ export function getImage(ItemId: string): string { * Create a hook that can convert ItemIds to image URLs */ export function useGetImage() { - return (ItemId: string) => getImage(ItemId); + const credentials = useTypedSelector((state) => state.settings.credentials); + + return (item: string | number | Album | AlbumTrack | Playlist | ArtistItem | null) => { + if (!item) { + return ''; + // GUARD: If the item's just the id, we'll pass it on directly. + } else if (typeof item === 'string' || typeof item === 'number') { + return getImage(item, credentials); + // GUARD: If the item has an `PrimaryImageItemId` (for Emby servers), + // we'll attemp to return that + } else if ('PrimaryImageItemId' in item) { + return getImage(item.PrimaryImageItemId || item.Id, credentials); + } else { + return getImage(item.Id); + } + }; } \ No newline at end of file diff --git a/src/utility/JellyfinApi/track.ts b/src/utility/JellyfinApi/track.ts index 3fb71ae..ac08c44 100644 --- a/src/utility/JellyfinApi/track.ts +++ b/src/utility/JellyfinApi/track.ts @@ -61,9 +61,7 @@ export async function generateTrack(track: AlbumTrack): Promise { artist: track.Artists.join(', '), album: track.Album, duration: track.RunTimeTicks, - artwork: track.AlbumId - ? getImage(track.AlbumId) - : getImage(track.Id), + artwork: getImage(track.Id), hasLyrics: track.HasLyrics, lyrics: track.Lyrics, contentType: response.headers.get('Content-Type') || undefined,