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 { 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)}
/>
<ActivityIndicator
animating={file && file.progress < 1}
size="large"
color={colors.accent}
style={styles.indicator}
/>
<ActivityIndicator animating={!file} 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') => {
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) => {

View File

@ -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,
}

View File

@ -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),
}

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 { 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<void>
}
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<void>
getCoverArtPath: (coverArt: string) => Promise<string>
cachedArtistArt: { [artistId: string]: DownloadFile }
downloadedArtistArt: { [artistId: string]: DownloadFile }
getCoverArtPath: (coverArt: string) => Promise<string | undefined>
cachedArtistArt: { [artistId: string]: CacheDownload }
cacheArtistArt: (artistId: string, url?: string) => Promise<void>
cachedSongs: { [id: string]: DownloadFile }
downloadedSongs: { [id: string]: DownloadFile }
cachedSongs: { [id: string]: CacheDownload }
albumCoverArt: { [id: string]: string | undefined }
albumCoverArtRequests: { [id: string]: Promise<void> }
@ -49,8 +65,9 @@ export const selectCache = {
}
export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): CacheSlice => ({
cache: {},
cachedCoverArt: {},
downloadedCoverArt: {},
cacheCoverArt: async coverArt => {
const client = get().client
@ -58,17 +75,27 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): 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<Store>, get: GetState<Store>): Ca
)
})
set(
produce<CacheSlice>(state => {
state.cachedCoverArt[coverArt] = {
produce<Store>(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<Store>, get: GetState<Store>): 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<Store>, get: GetState<Store>): Ca
path,
date: Date.now(),
progress: 0,
permanent: false,
promise,
}
}),
@ -164,7 +200,6 @@ export const createCacheSlice = (set: SetState<Store>, get: GetState<Store>): Ca
},
cachedSongs: {},
downloadedSongs: {},
albumCoverArt: {},
albumCoverArtRequests: {},

View File

@ -61,11 +61,21 @@ export const createSettingsSlice = (set: SetState<Store>, get: GetState<Store>):
set(
produce<Store>(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: {},
}
}),
)
},

View File

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