fix: missing image covers for emby
This commit is contained in:
@@ -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 }}>
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -33,6 +33,9 @@ function SimilarAlbum({ id }: { id: string }) {
|
|||||||
const handlePress = useCallback(() => {
|
const handlePress = useCallback(() => {
|
||||||
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
|
||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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() {
|
||||||
@@ -25,7 +26,8 @@ export default function SetJellyfinServer() {
|
|||||||
const saveCredentials = useCallback((credentials: AppState['settings']['credentials']) => {
|
const saveCredentials = useCallback((credentials: AppState['settings']['credentials']) => {
|
||||||
if (credentials) {
|
if (credentials) {
|
||||||
dispatch(setJellyfinCredentials(credentials));
|
dispatch(setJellyfinCredentials(credentials));
|
||||||
navigation.dispatch(StackActions.popToTop());
|
navigation.dispatch(StackActions.popToTop());
|
||||||
|
dispatch(fetchRecentAlbums());
|
||||||
}
|
}
|
||||||
}, [navigation, dispatch]);
|
}, [navigation, dispatch]);
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)[];
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -79,7 +80,15 @@ 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
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user