import { CacheFile, CacheImageSize, CacheItemType, CacheItemTypeKey, CacheRequest } from '@app/models/cache' import { mkdir, rmdir } from '@app/util/fs' import PromiseQueue from '@app/util/PromiseQueue' import RNFS from 'react-native-fs' import { GetStore, SetStore } from './store' const queues: Record = { coverArt: new PromiseQueue(5), coverArtThumb: new PromiseQueue(50), artistArt: new PromiseQueue(5), artistArtThumb: new PromiseQueue(50), song: new PromiseQueue(1), } export type CacheDownload = CacheFile & CacheRequest export type CacheDirsByServer = Record> export type CacheFilesByServer = Record>> export type CacheRequestsByServer = Record>> export type CacheSlice = { cacheItem: ( key: CacheItemTypeKey, itemId: string, url: string | (() => string | Promise), ) => Promise // cache: DownloadedItemsByServer cacheDirs: CacheDirsByServer cacheFiles: CacheFilesByServer cacheRequests: CacheRequestsByServer fetchCoverArtFilePath: (coverArt: string, size?: CacheImageSize) => Promise createCache: (serverId: string) => Promise prepareCache: (serverId: string) => void pendingRemoval: Record removeCache: (serverId: string) => Promise clearImageCache: () => Promise } export const createCacheSlice = (set: SetStore, get: GetStore): CacheSlice => ({ // cache: {}, cacheDirs: {}, cacheFiles: {}, cacheRequests: {}, cacheItem: async (key, itemId, url) => { const client = get().client if (!client) { return } const activeServerId = get().settings.activeServerId if (!activeServerId) { return } if (get().pendingRemoval[activeServerId]) { return } const inProgress = get().cacheRequests[activeServerId][key][itemId] if (inProgress && inProgress.promise !== undefined) { return await inProgress.promise } const existing = get().cacheFiles[activeServerId][key][itemId] if (existing) { return } const path = `${get().cacheDirs[activeServerId][key]}/${itemId}` const promise = queues[key].enqueue(async () => { const urlResult = typeof url === 'string' ? url : url() const fromUrl = typeof urlResult === 'string' ? urlResult : await urlResult try { if (!fromUrl) { throw new Error('cannot resolve url for cache request') } await RNFS.downloadFile({ fromUrl, toFile: path, // progressInterval: 100, // progress: res => { // set( // produce(state => { // state.cacheRequests[activeServerId][key][itemId].progress = Math.max( // 1, // res.bytesWritten / (res.contentLength || 1), // ) // }), // ) // }, }).promise set(state => { state.cacheRequests[activeServerId][key][itemId].progress = 1 delete state.cacheRequests[activeServerId][key][itemId].promise }) } catch { set(state => { delete state.cacheFiles[activeServerId][key][itemId] delete state.cacheRequests[activeServerId][key][itemId] }) } }) set(state => { state.cacheFiles[activeServerId][key][itemId] = { path, date: Date.now(), permanent: false, } state.cacheRequests[activeServerId][key][itemId] = { progress: 0, promise, } }) return await promise }, fetchCoverArtFilePath: async (coverArt, size = 'thumbnail') => { const client = get().client if (!client) { return } const activeServerId = get().settings.activeServerId if (!activeServerId) { return } const key: CacheItemTypeKey = size === 'thumbnail' ? 'coverArtThumb' : 'coverArt' const existing = get().cacheFiles[activeServerId][key][coverArt] const inProgress = get().cacheRequests[activeServerId][key][coverArt] if (existing && inProgress) { if (inProgress.promise) { await inProgress.promise } return `file://${existing.path}` } await get().cacheItem(key, coverArt, () => client.getCoverArtUri({ id: coverArt, size: size === 'thumbnail' ? '256' : undefined, }), ) return `file://${get().cacheFiles[activeServerId][key][coverArt].path}` }, createCache: async serverId => { for (const type in CacheItemType) { await mkdir(`${RNFS.DocumentDirectoryPath}/servers/${serverId}/${type}`) } set(state => { state.cacheFiles[serverId] = { song: {}, coverArt: {}, coverArtThumb: {}, artistArt: {}, artistArtThumb: {}, } }) get().prepareCache(serverId) }, prepareCache: serverId => { set(state => { if (!state.cacheDirs[serverId]) { state.cacheDirs[serverId] = { song: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/song`, coverArt: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/coverArt`, coverArtThumb: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/coverArtThumb`, artistArt: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/artistArt`, artistArtThumb: `${RNFS.DocumentDirectoryPath}/servers/${serverId}/artistArtThumb`, } } if (!state.cacheRequests[serverId]) { state.cacheRequests[serverId] = { song: {}, coverArt: {}, coverArtThumb: {}, artistArt: {}, artistArtThumb: {}, } } }) }, pendingRemoval: {}, removeCache: async serverId => { set(state => { state.pendingRemoval[serverId] = true }) const cacheRequests = get().cacheRequests[serverId] const pendingRequests: Promise[] = [] for (const type in CacheItemType) { const requests = Object.values(cacheRequests[type as CacheItemTypeKey]) .filter(r => r.promise !== undefined) .map(r => r.promise) as Promise[] pendingRequests.push(...requests) } await Promise.all(pendingRequests) await rmdir(`${RNFS.DocumentDirectoryPath}/servers/${serverId}`) set(state => { delete state.pendingRemoval[serverId] if (state.cacheDirs[serverId]) { delete state.cacheDirs[serverId] } if (state.cacheFiles[serverId]) { delete state.cacheFiles[serverId] } if (state.cacheRequests[serverId]) { delete state.cacheRequests[serverId] } }) }, clearImageCache: async () => { const cacheRequests = get().cacheRequests for (const serverId in cacheRequests) { const coverArtRequests = cacheRequests[serverId].coverArt const artstArtRequests = cacheRequests[serverId].artistArt const requests = [...Object.values(coverArtRequests), ...Object.values(artstArtRequests)] const pendingRequests = [ ...(requests.filter(r => r.promise !== undefined).map(r => r.promise) as Promise[]), ] await Promise.all(pendingRequests) await rmdir(get().cacheDirs[serverId].coverArt) await mkdir(get().cacheDirs[serverId].coverArt) await rmdir(get().cacheDirs[serverId].artistArt) await mkdir(get().cacheDirs[serverId].artistArt) set(state => { state.cacheFiles[serverId].coverArt = {} state.cacheFiles[serverId].coverArtThumb = {} state.cacheFiles[serverId].artistArt = {} state.cacheFiles[serverId].artistArtThumb = {} }) } }, })