diff --git a/app/components/CoverArt.tsx b/app/components/CoverArt.tsx index fa69875..37e2866 100644 --- a/app/components/CoverArt.tsx +++ b/app/components/CoverArt.tsx @@ -1,5 +1,5 @@ import { useArtistCoverArtFile, useCoverArtFile } from '@app/hooks/music' -import { DownloadFile } from '@app/state/cache' +import { CachedFile } from '@app/models/music' import colors from '@app/styles/colors' import React, { useState } from 'react' import { ActivityIndicator, StyleSheet, View, ViewStyle } from 'react-native' @@ -22,11 +22,11 @@ type CoverArtProps = BaseProps & { coverArt?: string } -const Image = React.memo<{ file?: DownloadFile } & BaseProps>(({ file, style, imageStyle, resizeMode }) => { +const Image = React.memo<{ file?: CachedFile } & BaseProps>(({ file, style, imageStyle, resizeMode }) => { const [error, setError] = useState(false) let source - if (!error && file && file.progress === 1) { + if (!error && file) { source = { uri: `file://${file.path}` } } else { source = require('@res/fallback.png') @@ -40,12 +40,7 @@ const Image = React.memo<{ file?: DownloadFile } & BaseProps>(({ file, style, im style={[{ height: style?.height, width: style?.width }, imageStyle]} onError={() => setError(true)} /> - + ) }) diff --git a/app/hooks/music.ts b/app/hooks/music.ts index b9dee9c..f5d2de4 100644 --- a/app/hooks/music.ts +++ b/app/hooks/music.ts @@ -63,16 +63,32 @@ export const useStarred = (id: string, type: string) => { } export const useCoverArtFile = (coverArt: string = '-1') => { - const file = useStore(useCallback((state: Store) => state.cachedCoverArt[coverArt], [coverArt])) + const existing = useStore( + useCallback( + (state: Store) => { + const activeServerId = state.settings.activeServer + if (!activeServerId) { + return + } + return state.cache[activeServerId].files.coverArt[coverArt] + }, + [coverArt], + ), + ) + const progress = useStore(useCallback((state: Store) => state.cachedCoverArt[coverArt], [coverArt])) const cacheCoverArt = useStore(selectCache.cacheCoverArt) useEffect(() => { - if (!file) { + if (!existing) { cacheCoverArt(coverArt) } }) - return file + if (existing && progress && progress.promise !== undefined) { + return + } + + return existing } export const useArtistCoverArtFile = (artistId: string) => { diff --git a/app/hooks/trackplayer.ts b/app/hooks/trackplayer.ts index 55cbe3d..cb42cff 100644 --- a/app/hooks/trackplayer.ts +++ b/app/hooks/trackplayer.ts @@ -211,7 +211,7 @@ export const useSetQueue = () => { return } - const coverArtPaths: { [coverArt: string]: string } = {} + const coverArtPaths: { [coverArt: string]: string | undefined } = {} for (const s of songs) { if (!s.coverArt) { continue @@ -271,14 +271,17 @@ export const useIsPlaying = (contextId: string | undefined, track: number) => { return contextId === queueContextId && track === currentTrackIdx } -function mapSongToTrack(song: Song, coverArtPaths: { [coverArt: string]: string }): TrackExt { +function mapSongToTrack(song: Song, coverArtPaths: { [coverArt: string]: string | undefined }): TrackExt { return { id: song.id, title: song.title, artist: song.artist || 'Unknown Artist', album: song.album || 'Unknown Album', url: song.streamUri, - artwork: song.coverArt ? `file://${coverArtPaths[song.coverArt]}` : require('@res/fallback.png'), + artwork: + song.coverArt && coverArtPaths[song.coverArt] + ? `file://${coverArtPaths[song.coverArt]}` + : require('@res/fallback.png'), coverArt: song.coverArt, duration: song.duration, } diff --git a/app/models/music.ts b/app/models/music.ts index 6a0f59f..b1b1113 100644 --- a/app/models/music.ts +++ b/app/models/music.ts @@ -19,11 +19,7 @@ export interface Artist { export interface ArtistInfo extends Artist { albums: Album[] - - smallImageUrl?: string - mediumImageUrl?: string largeImageUrl?: string - topSongs: Song[] } @@ -62,7 +58,6 @@ export interface PlaylistListItem { export interface PlaylistWithSongs extends PlaylistListItem { songs: Song[] - coverArt?: string } export interface Song { @@ -85,36 +80,27 @@ export type ListableItem = Song | AlbumListItem | Artist | PlaylistListItem export type HomeLists = { [key: string]: AlbumListItem[] } -export type DownloadedSong = { - id: string - type: 'song' - name: string - album: string - artist: string +export type CachedFile = { + path: string + date: number + permanent: boolean } -export type DownloadedAlbum = { - id: string - type: 'album' +export type DownloadedAlbum = Album & { songs: string[] - name: string - artist: string } -export type DownloadedArtist = { - id: string - type: 'artist' +export type DownloadedPlaylist = PlaylistListItem & { songs: string[] - name: string } -export type DownloadedPlaylist = { - id: string - type: 'playlist' - songs: string[] - name: string +export type DownloadedArtist = Artist & { + topSongs: string[] + albums: string[] } +export type DownloadedSong = Song + export function mapArtistID3toArtist(artist: ArtistID3Element): Artist { return { itemType: 'artist', @@ -138,8 +124,6 @@ export function mapArtistInfo( return { ...mapArtistID3toArtist(artist), albums: mappedAlbums, - smallImageUrl: info.smallImageUrl, - mediumImageUrl: info.mediumImageUrl, largeImageUrl: info.largeImageUrl, topSongs: topSongs.map(s => mapChildToSong(s, client)).slice(0, 5), } diff --git a/app/state/cache.ts b/app/state/cache.ts index 7f5614c..e25880b 100644 --- a/app/state/cache.ts +++ b/app/state/cache.ts @@ -1,4 +1,11 @@ -import { Song } from '@app/models/music' +import { + CachedFile, + DownloadedAlbum, + DownloadedArtist, + DownloadedPlaylist, + DownloadedSong, + Song, +} from '@app/models/music' import PromiseQueue from '@app/util/PromiseQueue' import { SetState, GetState } from 'zustand' import { Store } from './store' @@ -7,31 +14,40 @@ import RNFS from 'react-native-fs' const imageDownloadQueue = new PromiseQueue(10) -export type DownloadFile = { - path: string - date: number +type DownloadProgress = { progress: number promise?: Promise } +export type CacheDownload = CachedFile & DownloadProgress + export type CacheSlice = { coverArtDir?: string artistArtDir?: string songsDir?: string - cachedCoverArt: { [coverArt: string]: DownloadFile } - downloadedCoverArt: { [coverArt: string]: DownloadFile } + cache: { + [serverId: string]: { + files: { + coverArt: { [coverArt: string]: CachedFile } + artistArt: { [artistId: string]: CachedFile } + songs: { [songId: string]: CachedFile } + } + songs: { [songId: string]: DownloadedSong } + albums: { [albumId: string]: DownloadedAlbum } + artists: { [songId: string]: DownloadedArtist } + playlists: { [playlistId: string]: DownloadedPlaylist } + } + } + cachedCoverArt: { [coverArt: string]: DownloadProgress } cacheCoverArt: (coverArt: string) => Promise - getCoverArtPath: (coverArt: string) => Promise - - cachedArtistArt: { [artistId: string]: DownloadFile } - downloadedArtistArt: { [artistId: string]: DownloadFile } + getCoverArtPath: (coverArt: string) => Promise + cachedArtistArt: { [artistId: string]: CacheDownload } cacheArtistArt: (artistId: string, url?: string) => Promise - cachedSongs: { [id: string]: DownloadFile } - downloadedSongs: { [id: string]: DownloadFile } + cachedSongs: { [id: string]: CacheDownload } albumCoverArt: { [id: string]: string | undefined } albumCoverArtRequests: { [id: string]: Promise } @@ -49,8 +65,9 @@ export const selectCache = { } export const createCacheSlice = (set: SetState, get: GetState): CacheSlice => ({ + cache: {}, + cachedCoverArt: {}, - downloadedCoverArt: {}, cacheCoverArt: async coverArt => { const client = get().client @@ -58,17 +75,27 @@ export const createCacheSlice = (set: SetState, get: GetState): Ca return } - const path = `${get().coverArtDir}/${coverArt}` + const activeServerId = get().settings.activeServer + if (!activeServerId) { + return + } - const existing = get().cachedCoverArt[coverArt] - if (existing) { - if (existing.promise !== undefined) { - return await existing.promise + const inProgress = get().cachedCoverArt[coverArt] + if (inProgress) { + if (inProgress.promise !== undefined) { + return await inProgress.promise } else { return } } + const existing = get().cache[activeServerId].files.coverArt[coverArt] + if (existing) { + return + } + + const path = `${get().coverArtDir}/${coverArt}` + const promise = imageDownloadQueue .enqueue( () => @@ -86,10 +113,13 @@ export const createCacheSlice = (set: SetState, get: GetState): Ca ) }) set( - produce(state => { - state.cachedCoverArt[coverArt] = { + produce(state => { + state.cache[activeServerId].files.coverArt[coverArt] = { path, date: Date.now(), + permanent: false, + } + state.cachedCoverArt[coverArt] = { progress: 0, promise, } @@ -99,20 +129,25 @@ export const createCacheSlice = (set: SetState, get: GetState): Ca }, getCoverArtPath: async coverArt => { - const existing = get().cachedCoverArt[coverArt] - if (existing) { - if (existing.promise) { - await existing.promise + const activeServerId = get().settings.activeServer + if (!activeServerId) { + return + } + + const existing = get().cache[activeServerId].files.coverArt[coverArt] + const inProgress = get().cachedCoverArt[coverArt] + if (existing && inProgress) { + if (inProgress.promise) { + await inProgress.promise } return existing.path } await get().cacheCoverArt(coverArt) - return get().cachedCoverArt[coverArt].path + return get().cache[activeServerId].files.coverArt[coverArt].path }, cachedArtistArt: {}, - downloadedArtistArt: {}, cacheArtistArt: async (artistId, url) => { if (!url) { @@ -157,6 +192,7 @@ export const createCacheSlice = (set: SetState, get: GetState): Ca path, date: Date.now(), progress: 0, + permanent: false, promise, } }), @@ -164,7 +200,6 @@ export const createCacheSlice = (set: SetState, get: GetState): Ca }, cachedSongs: {}, - downloadedSongs: {}, albumCoverArt: {}, albumCoverArtRequests: {}, diff --git a/app/state/settings.ts b/app/state/settings.ts index 0cdddf0..c489834 100644 --- a/app/state/settings.ts +++ b/app/state/settings.ts @@ -61,11 +61,21 @@ export const createSettingsSlice = (set: SetState, get: GetState): set( produce(state => { - state.settings.activeServer = id + state.settings.activeServer = newActiveServer.id state.client = new SubsonicApiClient(newActiveServer) state.coverArtDir = coverArtDir state.artistArtDir = artistArtDir state.songsDir = songsDir + state.cache[newActiveServer.id] = state.cache[newActiveServer.id] || { + files: { + coverArt: {}, + artistArt: {}, + songs: {}, + }, + songs: {}, + albums: {}, + artists: {}, + } }), ) }, diff --git a/app/state/store.ts b/app/state/store.ts index 80a05cf..11eed8e 100644 --- a/app/state/store.ts +++ b/app/state/store.ts @@ -46,7 +46,7 @@ export const useStore = create( { name: '@appStore', getStorage: () => storage, - whitelist: ['settings'], + whitelist: ['settings', 'cache'], onRehydrateStorage: _preState => { return async (postState, _error) => { await postState?.setActiveServer(postState.settings.activeServer, true)