fix: missing image covers for emby

This commit is contained in:
Lei Nelissen
2024-07-26 12:01:21 +02:00
parent 399340127d
commit 01e25a89b1
16 changed files with 73 additions and 33 deletions

View File

@@ -136,7 +136,7 @@ function Downloads() {
<DownloadedTrack> <DownloadedTrack>
<View style={{ marginRight: 12 }}> <View style={{ marginRight: 12 }}>
<ShadowWrapper size="small"> <ShadowWrapper size="small">
<AlbumImage source={{ uri: getImage(item as string) }} style={defaultStyles.imageBackground} /> <AlbumImage source={{ uri: getImage(item) }} style={defaultStyles.imageBackground} />
</ShadowWrapper> </ShadowWrapper>
</View> </View>
<View style={{ flexShrink: 1, marginRight: 8 }}> <View style={{ flexShrink: 1, marginRight: 8 }}>

View File

@@ -2,7 +2,7 @@ import React, { useCallback, useEffect } from 'react';
import { useRoute, RouteProp, useNavigation } from '@react-navigation/native'; import { useRoute, RouteProp, useNavigation } from '@react-navigation/native';
import { useAppDispatch, useTypedSelector } from '@/store'; import { useAppDispatch, useTypedSelector } from '@/store';
import TrackListView from './components/TrackListView'; 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 { differenceInDays } from 'date-fns';
import { ALBUM_CACHE_AMOUNT_OF_DAYS } from '@/CONSTANTS'; import { ALBUM_CACHE_AMOUNT_OF_DAYS } from '@/CONSTANTS';
import { t } from '@/localisation'; import { t } from '@/localisation';
@@ -34,6 +34,9 @@ function SimilarAlbum({ id }: { id: string }) {
album && navigation.push('Album', { id, album }); album && navigation.push('Album', { id, album });
}, [id, album, navigation]); }, [id, album, navigation]);
console.log(getImage(album));
return ( return (
<Pressable <Pressable
style={({ pressed }) => ({ style={({ pressed }) => ({
@@ -43,7 +46,7 @@ function SimilarAlbum({ id }: { id: string }) {
})} })}
onPress={handlePress} onPress={handlePress}
> >
<Cover key={id} source={{ uri: getImage(id) }} /> <Cover key={id} source={{ uri: getImage(album) }} />
<Text numberOfLines={1} style={{ fontSize: 13, marginBottom: 2 }}>{album?.Name}</Text> <Text numberOfLines={1} style={{ fontSize: 13, marginBottom: 2 }}>{album?.Name}</Text>
<Text numberOfLines={1} style={{ opacity: 0.5, fontSize: 13 }}>{album?.Artists.join(', ')}</Text> <Text numberOfLines={1} style={{ opacity: 0.5, fontSize: 13 }}>{album?.Artists.join(', ')}</Text>
</Pressable> </Pressable>
@@ -62,6 +65,7 @@ const Album: React.FC = () => {
const refresh = useCallback(() => { const refresh = useCallback(() => {
dispatch(fetchTracksByAlbum(id)); dispatch(fetchTracksByAlbum(id));
dispatch(fetchAlbum(id)); dispatch(fetchAlbum(id));
dispatch(fetchSimilarAlbums(id));
}, [id, dispatch]); }, [id, dispatch]);
// Auto-fetch the track data periodically // Auto-fetch the track data periodically
@@ -76,7 +80,7 @@ const Album: React.FC = () => {
trackIds={albumTracks || []} trackIds={albumTracks || []}
title={album?.Name} title={album?.Name}
artist={album?.AlbumArtist} artist={album?.AlbumArtist}
entityId={id} entityId={album?.PrimaryImageItemId || album.Id}
refresh={refresh} refresh={refresh}
playButtonText={t('play-album')} playButtonText={t('play-album')}
shuffleButtonText={t('shuffle-album')} shuffleButtonText={t('shuffle-album')}

View File

@@ -149,7 +149,7 @@ const Albums: React.FC = () => {
<GeneratedAlbumItem <GeneratedAlbumItem
key={id} key={id}
id={id} id={id}
imageUrl={getImage(id as string)} imageUrl={getImage(albums[id])}
name={albums[id]?.Name || ''} name={albums[id]?.Name || ''}
artist={albums[id]?.AlbumArtist || ''} artist={albums[id]?.AlbumArtist || ''}
onPress={selectAlbum} onPress={selectAlbum}

View File

@@ -68,7 +68,7 @@ const Artist: React.FC = () => {
<GeneratedAlbumItem <GeneratedAlbumItem
key={id} key={id}
id={id} id={id}
imageUrl={getImage(id as string)} imageUrl={getImage(albums[id])}
name={albums[id]?.Name || ''} name={albums[id]?.Name || ''}
artist={albums[id]?.AlbumArtist || ''} artist={albums[id]?.AlbumArtist || ''}
onPress={selectAlbum} onPress={selectAlbum}

View File

@@ -178,7 +178,7 @@ const Artists: React.FC = () => {
key={item.Id} key={item.Id}
item={item} item={item}
onPress={selectArtist} onPress={selectArtist}
imageURL={getImage(item.Id)} imageURL={getImage(item)}
/> />
</View> </View>
); );

View File

@@ -70,14 +70,14 @@ const Playlists: React.FC = () => {
<View style={{ flexDirection: 'row', marginLeft: 10, marginRight: 10 }} key={item}> <View style={{ flexDirection: 'row', marginLeft: 10, marginRight: 10 }} key={item}>
<GeneratedPlaylistItem <GeneratedPlaylistItem
id={item} id={item}
imageUrl={getImage(item as string)} imageUrl={getImage(entities[item])}
name={entities[item]?.Name || ''} name={entities[item]?.Name || ''}
onPress={selectAlbum} onPress={selectAlbum}
/> />
{nextItem && {nextItem &&
<GeneratedPlaylistItem <GeneratedPlaylistItem
id={nextItemId} id={nextItemId}
imageUrl={getImage(nextItemId as string)} imageUrl={getImage(nextItem)}
name={nextItem.Name || ''} name={nextItem.Name || ''}
onPress={selectAlbum} onPress={selectAlbum}
/> />

View File

@@ -95,7 +95,7 @@ const RecentAlbums: React.FC = () => {
<TouchableHandler id={item} onPress={selectAlbum} testID={`select-album-${item}`}> <TouchableHandler id={item} onPress={selectAlbum} testID={`select-album-${item}`}>
<AlbumItem> <AlbumItem>
<ShadowWrapper size="medium"> <ShadowWrapper size="medium">
<AlbumImage source={{ uri: getImage(item) }} style={defaultStyles.imageBackground} /> <AlbumImage source={{ uri: getImage(albums[item]) }} style={defaultStyles.imageBackground} />
</ShadowWrapper> </ShadowWrapper>
<Text style={defaultStyles.text} numberOfLines={1}>{albums[item]?.Name}</Text> <Text style={defaultStyles.text} numberOfLines={1}>{albums[item]?.Name}</Text>
<Text style={defaultStyles.textHalfOpacity} numberOfLines={1}>{albums[item]?.AlbumArtist}</Text> <Text style={defaultStyles.textHalfOpacity} numberOfLines={1}>{albums[item]?.AlbumArtist}</Text>

View File

@@ -298,7 +298,7 @@ export default function Search() {
<TouchableHandler<string> id={album.Id} onPress={selectAlbum} testID={`search-result-${album.Id}`}> <TouchableHandler<string> id={album.Id} onPress={selectAlbum} testID={`search-result-${album.Id}`}>
<SearchResult> <SearchResult>
<ShadowWrapper> <ShadowWrapper>
<AlbumImage source={{ uri: getImage(album.Id) }} style={defaultStyles.imageBackground} /> <AlbumImage source={{ uri: getImage(album) }} style={defaultStyles.imageBackground} />
</ShadowWrapper> </ShadowWrapper>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text numberOfLines={1}> <Text numberOfLines={1}>

View File

@@ -9,6 +9,7 @@ import { t } from '@/localisation';
import useDefaultStyles from '@/components/Colors'; import useDefaultStyles from '@/components/Colors';
import { Text } from '@/components/Typography'; import { Text } from '@/components/Typography';
import { AppState, useAppDispatch } from '@/store'; import { AppState, useAppDispatch } from '@/store';
import { fetchRecentAlbums } from '@/store/music/actions';
export default function SetJellyfinServer() { export default function SetJellyfinServer() {
@@ -26,6 +27,7 @@ export default function SetJellyfinServer() {
if (credentials) { if (credentials) {
dispatch(setJellyfinCredentials(credentials)); dispatch(setJellyfinCredentials(credentials));
navigation.dispatch(StackActions.popToTop()); navigation.dispatch(StackActions.popToTop());
dispatch(fetchRecentAlbums());
} }
}, [navigation, dispatch]); }, [navigation, dispatch]);

View File

@@ -75,7 +75,7 @@ function TrackPopupMenu() {
return ( return (
<Container> <Container>
<Artwork src={getImage(track?.Id || '')} /> <Artwork src={getImage(track)} />
<Header>{track?.Name}</Header> <Header>{track?.Name}</Header>
<SubHeader style={{ marginBottom: 18 }}>{track?.AlbumArtist} {track?.Album ? '— ' + track?.Album : ''}</SubHeader> <SubHeader style={{ marginBottom: 18 }}>{track?.AlbumArtist} {track?.Album ? '— ' + track?.Album : ''}</SubHeader>
<WrappableButtonRow> <WrappableButtonRow>

View File

@@ -1,7 +1,7 @@
import { createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit'; import { createAsyncThunk, createEntityAdapter } from '@reduxjs/toolkit';
import { Album, AlbumTrack, Playlist } from './types'; import { Album, AlbumTrack, Playlist } from './types';
import { AsyncThunkAPI } from '..'; 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 { retrieveAllPlaylists, retrievePlaylistTracks } from '@/utility/JellyfinApi/playlist';
import { searchItem } from '@/utility/JellyfinApi/search'; import { searchItem } from '@/utility/JellyfinApi/search';
@@ -44,6 +44,11 @@ export const fetchAlbum = createAsyncThunk<Album, string, AsyncThunkAPI>(
retrieveAlbum, retrieveAlbum,
); );
export const fetchSimilarAlbums = createAsyncThunk<Album[], string, AsyncThunkAPI>(
'/albums/similar',
retrieveSimilarAlbums,
);
type SearchAndFetchResults = { type SearchAndFetchResults = {
albums: Album[]; albums: Album[];
results: (Album | AlbumTrack)[]; results: (Album | AlbumTrack)[];

View File

@@ -8,7 +8,8 @@ import {
playlistAdapter, playlistAdapter,
fetchAllPlaylists, fetchAllPlaylists,
fetchTracksByPlaylist, fetchTracksByPlaylist,
fetchAlbum fetchAlbum,
fetchSimilarAlbums
} from './actions'; } from './actions';
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { Album, AlbumTrack, Playlist } from './types'; import { Album, AlbumTrack, Playlist } from './types';
@@ -80,6 +81,14 @@ const music = createSlice({
builder.addCase(fetchAlbum.pending, (state) => { state.albums.isLoading = true; }); builder.addCase(fetchAlbum.pending, (state) => { state.albums.isLoading = true; });
builder.addCase(fetchAlbum.rejected, (state) => { state.albums.isLoading = false; }); 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 * Fetch most recent albums
*/ */

View File

@@ -68,6 +68,8 @@ export interface Album {
DateCreated: string; DateCreated: string;
Overview?: string; Overview?: string;
Similar?: string[]; Similar?: string[];
/** Emby potentially carries different ids for primary images */
PrimaryImageItemId?: string;
} }
export interface AlbumTrack { export interface AlbumTrack {
@@ -124,7 +126,3 @@ export interface Playlist {
Tracks?: string[]; Tracks?: string[];
lastRefreshed?: number; lastRefreshed?: number;
} }
export interface SimilarAlbum {
Id: string;
}

View File

@@ -1,4 +1,4 @@
import { Album, AlbumTrack, SimilarAlbum } from '@/store/music/types'; import { Album, AlbumTrack } from '@/store/music/types';
import { fetchApi } from './lib'; import { fetchApi } from './lib';
import {retrieveAndInjectLyricsToTracks} from '@/utility/JellyfinApi/lyrics.ts'; import {retrieveAndInjectLyricsToTracks} from '@/utility/JellyfinApi/lyrics.ts';
@@ -26,11 +26,15 @@ export async function retrieveAllAlbums() {
* Retrieve a single album * Retrieve a single album
*/ */
export async function retrieveAlbum(id: string): Promise<Album> { export async function retrieveAlbum(id: string): Promise<Album> {
const Similar = await fetchApi<{ Items: SimilarAlbum[] }>(({ user_id }) => `/Items/${id}/Similar?userId=${user_id}&limit=12`) return fetchApi<Album>(({ user_id }) => `/Users/${user_id}/Items/${id}`);
.then((albums) => albums!.Items.map((a) => a.Id)); }
return fetchApi<Album>(({ 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<Album[]> {
return fetchApi<{ Items: Album[] }>(({ user_id }) => `/Items/${id}/Similar?userId=${user_id}&limit=12`)
.then((albums) => albums!.Items);
} }
const latestAlbumsOptions = { const latestAlbumsOptions = {

View File

@@ -1,6 +1,7 @@
import type { AppState, Store } from '@/store'; import { useTypedSelector, type AppState, type Store } from '@/store';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import { version } from '../../../package.json'; import { version } from '../../../package.json';
import { Album, AlbumTrack, ArtistItem, Playlist } from '@/store/music/types';
type Credentials = AppState['settings']['credentials']; type Credentials = AppState['settings']['credentials'];
@@ -105,9 +106,13 @@ export async function fetchApi<T>(
/** /**
* Retrieve an image URL for a given ItemId * Retrieve an image URL for a given ItemId
*/ */
export function getImage(ItemId: string): string { export function getImage(ItemId: string | number, credentials?: AppState['settings']['credentials']): string {
const credentials = asyncFetchStore().getState().settings.credentials; // Either accept provided credentials, or retrieve them directly from the store
const uri = encodeURI(`${credentials?.uri}/Items/${ItemId}/Images/Primary?format=jpeg`); 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; return uri;
} }
@@ -115,5 +120,20 @@ export function getImage(ItemId: string): string {
* Create a hook that can convert ItemIds to image URLs * Create a hook that can convert ItemIds to image URLs
*/ */
export function useGetImage() { 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);
}
};
} }

View File

@@ -61,9 +61,7 @@ export async function generateTrack(track: AlbumTrack): Promise<Track> {
artist: track.Artists.join(', '), artist: track.Artists.join(', '),
album: track.Album, album: track.Album,
duration: track.RunTimeTicks, duration: track.RunTimeTicks,
artwork: track.AlbumId artwork: getImage(track.Id),
? getImage(track.AlbumId)
: getImage(track.Id),
hasLyrics: track.HasLyrics, hasLyrics: track.HasLyrics,
lyrics: track.Lyrics, lyrics: track.Lyrics,
contentType: response.headers.get('Content-Type') || undefined, contentType: response.headers.get('Content-Type') || undefined,