persist cache map for cover art

This commit is contained in:
austinried 2021-08-13 16:19:30 +09:00
parent d1824a70be
commit 9cacc4de36
7 changed files with 114 additions and 71 deletions

View File

@ -1,5 +1,5 @@
import { useArtistCoverArtFile, useCoverArtFile } from '@app/hooks/music' 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 colors from '@app/styles/colors'
import React, { useState } from 'react' import React, { useState } from 'react'
import { ActivityIndicator, StyleSheet, View, ViewStyle } from 'react-native' import { ActivityIndicator, StyleSheet, View, ViewStyle } from 'react-native'
@ -22,11 +22,11 @@ type CoverArtProps = BaseProps & {
coverArt?: string 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) const [error, setError] = useState(false)
let source let source
if (!error && file && file.progress === 1) { if (!error && file) {
source = { uri: `file://${file.path}` } source = { uri: `file://${file.path}` }
} else { } else {
source = require('@res/fallback.png') 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]} style={[{ height: style?.height, width: style?.width }, imageStyle]}
onError={() => setError(true)} onError={() => setError(true)}
/> />
<ActivityIndicator <ActivityIndicator animating={!file} size="large" color={colors.accent} style={styles.indicator} />
animating={file && file.progress < 1}
size="large"
color={colors.accent}
style={styles.indicator}
/>
</> </>
) )
}) })

View File

@ -63,16 +63,32 @@ export const useStarred = (id: string, type: string) => {
} }
export const useCoverArtFile = (coverArt: string = '-1') => { 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) const cacheCoverArt = useStore(selectCache.cacheCoverArt)
useEffect(() => { useEffect(() => {
if (!file) { if (!existing) {
cacheCoverArt(coverArt) cacheCoverArt(coverArt)
} }
}) })
return file if (existing && progress && progress.promise !== undefined) {
return
}
return existing
} }
export const useArtistCoverArtFile = (artistId: string) => { export const useArtistCoverArtFile = (artistId: string) => {

View File

@ -211,7 +211,7 @@ export const useSetQueue = () => {
return return
} }
const coverArtPaths: { [coverArt: string]: string } = {} const coverArtPaths: { [coverArt: string]: string | undefined } = {}
for (const s of songs) { for (const s of songs) {
if (!s.coverArt) { if (!s.coverArt) {
continue continue
@ -271,14 +271,17 @@ export const useIsPlaying = (contextId: string | undefined, track: number) => {
return contextId === queueContextId && track === currentTrackIdx 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 { return {
id: song.id, id: song.id,
title: song.title, title: song.title,
artist: song.artist || 'Unknown Artist', artist: song.artist || 'Unknown Artist',
album: song.album || 'Unknown Album', album: song.album || 'Unknown Album',
url: song.streamUri, 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, coverArt: song.coverArt,
duration: song.duration, duration: song.duration,
} }

View File

@ -19,11 +19,7 @@ export interface Artist {
export interface ArtistInfo extends Artist { export interface ArtistInfo extends Artist {
albums: Album[] albums: Album[]
smallImageUrl?: string
mediumImageUrl?: string
largeImageUrl?: string largeImageUrl?: string
topSongs: Song[] topSongs: Song[]
} }
@ -62,7 +58,6 @@ export interface PlaylistListItem {
export interface PlaylistWithSongs extends PlaylistListItem { export interface PlaylistWithSongs extends PlaylistListItem {
songs: Song[] songs: Song[]
coverArt?: string
} }
export interface Song { export interface Song {
@ -85,36 +80,27 @@ export type ListableItem = Song | AlbumListItem | Artist | PlaylistListItem
export type HomeLists = { [key: string]: AlbumListItem[] } export type HomeLists = { [key: string]: AlbumListItem[] }
export type DownloadedSong = { export type CachedFile = {
id: string path: string
type: 'song' date: number
name: string permanent: boolean
album: string
artist: string
} }
export type DownloadedAlbum = { export type DownloadedAlbum = Album & {
id: string
type: 'album'
songs: string[] songs: string[]
name: string
artist: string
} }
export type DownloadedArtist = { export type DownloadedPlaylist = PlaylistListItem & {
id: string
type: 'artist'
songs: string[] songs: string[]
name: string
} }
export type DownloadedPlaylist = { export type DownloadedArtist = Artist & {
id: string topSongs: string[]
type: 'playlist' albums: string[]
songs: string[]
name: string
} }
export type DownloadedSong = Song
export function mapArtistID3toArtist(artist: ArtistID3Element): Artist { export function mapArtistID3toArtist(artist: ArtistID3Element): Artist {
return { return {
itemType: 'artist', itemType: 'artist',
@ -138,8 +124,6 @@ export function mapArtistInfo(
return { return {
...mapArtistID3toArtist(artist), ...mapArtistID3toArtist(artist),
albums: mappedAlbums, albums: mappedAlbums,
smallImageUrl: info.smallImageUrl,
mediumImageUrl: info.mediumImageUrl,
largeImageUrl: info.largeImageUrl, largeImageUrl: info.largeImageUrl,
topSongs: topSongs.map(s => mapChildToSong(s, client)).slice(0, 5), topSongs: topSongs.map(s => mapChildToSong(s, client)).slice(0, 5),
} }

View File

@ -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 PromiseQueue from '@app/util/PromiseQueue'
import { SetState, GetState } from 'zustand' import { SetState, GetState } from 'zustand'
import { Store } from './store' import { Store } from './store'
@ -7,31 +14,40 @@ import RNFS from 'react-native-fs'
const imageDownloadQueue = new PromiseQueue(10) const imageDownloadQueue = new PromiseQueue(10)
export type DownloadFile = { type DownloadProgress = {
path: string
date: number
progress: number progress: number
promise?: Promise<void> promise?: Promise<void>
} }
export type CacheDownload = CachedFile & DownloadProgress
export type CacheSlice = { export type CacheSlice = {
coverArtDir?: string coverArtDir?: string
artistArtDir?: string artistArtDir?: string
songsDir?: string songsDir?: string
cachedCoverArt: { [coverArt: string]: DownloadFile } cache: {
downloadedCoverArt: { [coverArt: string]: DownloadFile } [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<void> cacheCoverArt: (coverArt: string) => Promise<void>
getCoverArtPath: (coverArt: string) => Promise<string> getCoverArtPath: (coverArt: string) => Promise<string | undefined>
cachedArtistArt: { [artistId: string]: DownloadFile }
downloadedArtistArt: { [artistId: string]: DownloadFile }
cachedArtistArt: { [artistId: string]: CacheDownload }
cacheArtistArt: (artistId: string, url?: string) => Promise<void> cacheArtistArt: (artistId: string, url?: string) => Promise<void>
cachedSongs: { [id: string]: DownloadFile } cachedSongs: { [id: string]: CacheDownload }
downloadedSongs: { [id: string]: DownloadFile }
albumCoverArt: { [id: string]: string | undefined } albumCoverArt: { [id: string]: string | undefined }
albumCoverArtRequests: { [id: string]: Promise<void> } albumCoverArtRequests: { [id: string]: Promise<void> }
@ -49,8 +65,9 @@ export const selectCache = {
} }
export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): CacheSlice => ({ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): CacheSlice => ({
cache: {},
cachedCoverArt: {}, cachedCoverArt: {},
downloadedCoverArt: {},
cacheCoverArt: async coverArt => { cacheCoverArt: async coverArt => {
const client = get().client const client = get().client
@ -58,17 +75,27 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
return return
} }
const path = `${get().coverArtDir}/${coverArt}` const activeServerId = get().settings.activeServer
if (!activeServerId) {
return
}
const existing = get().cachedCoverArt[coverArt] const inProgress = get().cachedCoverArt[coverArt]
if (existing) { if (inProgress) {
if (existing.promise !== undefined) { if (inProgress.promise !== undefined) {
return await existing.promise return await inProgress.promise
} else { } else {
return return
} }
} }
const existing = get().cache[activeServerId].files.coverArt[coverArt]
if (existing) {
return
}
const path = `${get().coverArtDir}/${coverArt}`
const promise = imageDownloadQueue const promise = imageDownloadQueue
.enqueue( .enqueue(
() => () =>
@ -86,10 +113,13 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
) )
}) })
set( set(
produce<CacheSlice>(state => { produce<Store>(state => {
state.cachedCoverArt[coverArt] = { state.cache[activeServerId].files.coverArt[coverArt] = {
path, path,
date: Date.now(), date: Date.now(),
permanent: false,
}
state.cachedCoverArt[coverArt] = {
progress: 0, progress: 0,
promise, promise,
} }
@ -99,20 +129,25 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
}, },
getCoverArtPath: async coverArt => { getCoverArtPath: async coverArt => {
const existing = get().cachedCoverArt[coverArt] const activeServerId = get().settings.activeServer
if (existing) { if (!activeServerId) {
if (existing.promise) { return
await existing.promise }
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 return existing.path
} }
await get().cacheCoverArt(coverArt) await get().cacheCoverArt(coverArt)
return get().cachedCoverArt[coverArt].path return get().cache[activeServerId].files.coverArt[coverArt].path
}, },
cachedArtistArt: {}, cachedArtistArt: {},
downloadedArtistArt: {},
cacheArtistArt: async (artistId, url) => { cacheArtistArt: async (artistId, url) => {
if (!url) { if (!url) {
@ -157,6 +192,7 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
path, path,
date: Date.now(), date: Date.now(),
progress: 0, progress: 0,
permanent: false,
promise, promise,
} }
}), }),
@ -164,7 +200,6 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
}, },
cachedSongs: {}, cachedSongs: {},
downloadedSongs: {},
albumCoverArt: {}, albumCoverArt: {},
albumCoverArtRequests: {}, albumCoverArtRequests: {},

View File

@ -61,11 +61,21 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
set( set(
produce<Store>(state => { produce<Store>(state => {
state.settings.activeServer = id state.settings.activeServer = newActiveServer.id
state.client = new SubsonicApiClient(newActiveServer) state.client = new SubsonicApiClient(newActiveServer)
state.coverArtDir = coverArtDir state.coverArtDir = coverArtDir
state.artistArtDir = artistArtDir state.artistArtDir = artistArtDir
state.songsDir = songsDir state.songsDir = songsDir
state.cache[newActiveServer.id] = state.cache[newActiveServer.id] || {
files: {
coverArt: {},
artistArt: {},
songs: {},
},
songs: {},
albums: {},
artists: {},
}
}), }),
) )
}, },

View File

@ -46,7 +46,7 @@ export const useStore = create<Store>(
{ {
name: '@appStore', name: '@appStore',
getStorage: () => storage, getStorage: () => storage,
whitelist: ['settings'], whitelist: ['settings', 'cache'],
onRehydrateStorage: _preState => { onRehydrateStorage: _preState => {
return async (postState, _error) => { return async (postState, _error) => {
await postState?.setActiveServer(postState.settings.activeServer, true) await postState?.setActiveServer(postState.settings.activeServer, true)