Implement artwork download and cache

This commit is contained in:
diyaa
2026-05-30 09:43:14 +02:00
parent 8caf29f186
commit 7b1952794c
23 changed files with 1261 additions and 66 deletions
@@ -5,13 +5,16 @@ import VelodyPersistence
public actor OfflineLibraryService {
private let syncService: RemoteLibrarySyncService
private let audioFileStore: any OfflineAudioFileStore
private let artworkStore: any ArtworkStore
public init(
syncService: RemoteLibrarySyncService,
audioFileStore: any OfflineAudioFileStore
audioFileStore: any OfflineAudioFileStore,
artworkStore: any ArtworkStore
) {
self.syncService = syncService
self.audioFileStore = audioFileStore
self.artworkStore = artworkStore
}
public func loadSnapshot() async throws -> OfflineLibrarySnapshot {
@@ -37,6 +40,10 @@ public actor OfflineLibraryService {
for: downloadState,
status: status
)
let localArtworkFilePath = await Self.localArtworkFilePath(
for: remoteTrack.artwork,
artworkStore: artworkStore
)
remoteLibraryTracks.append(
OfflineLibraryRemoteTrack(
@@ -45,7 +52,8 @@ public actor OfflineLibraryService {
downloadedAt: downloadedAt,
isFileAvailable: isFileAvailable,
status: status,
lastDownloadError: lastDownloadError
lastDownloadError: lastDownloadError,
localArtworkFilePath: localArtworkFilePath
)
)
@@ -63,7 +71,8 @@ public actor OfflineLibraryService {
sha256: remoteTrack.sha256,
localFilePath: localFilePath,
downloadedAt: downloadedAt,
isFileAvailable: true
isFileAvailable: true,
localArtworkFilePath: localArtworkFilePath
)
)
}
@@ -118,4 +127,15 @@ public actor OfflineLibraryService {
return downloadState?.lastDownloadError
}
private static func localArtworkFilePath(
for artwork: RemoteArtwork?,
artworkStore: any ArtworkStore
) async -> String? {
guard let artwork else {
return nil
}
return await artworkStore.cachedFilePath(for: artwork)
}
}
@@ -7,6 +7,7 @@ public protocol RemoteLibraryRepository: Actor {
func loadCachedRemoteTracks() async throws -> [RemoteTrack]
func syncRemoteTracks(deviceId: String) async throws -> [RemoteTrack]
func downloadAudioAsset(assetId: String, deviceId: String) async throws -> Data
func downloadArtwork(artworkId: String, deviceId: String) async throws -> Data
}
public actor DefaultRemoteLibraryRepository: RemoteLibraryRepository {
@@ -38,4 +39,11 @@ public actor DefaultRemoteLibraryRepository: RemoteLibraryRepository {
) async throws -> Data {
try await apiClient.downloadAudioAsset(assetId: assetId, deviceId: deviceId)
}
public func downloadArtwork(
artworkId: String,
deviceId: String
) async throws -> Data {
try await apiClient.downloadArtwork(artworkId: artworkId, deviceId: deviceId)
}
}
@@ -6,15 +6,18 @@ public actor RemoteLibrarySyncService {
private let repository: any RemoteLibraryRepository
private let downloadStateStore: any RemoteTrackDownloadStateStore
private let audioFileStore: any OfflineAudioFileStore
private let artworkStore: any ArtworkStore
public init(
repository: any RemoteLibraryRepository,
downloadStateStore: any RemoteTrackDownloadStateStore,
audioFileStore: any OfflineAudioFileStore
audioFileStore: any OfflineAudioFileStore,
artworkStore: any ArtworkStore
) {
self.repository = repository
self.downloadStateStore = downloadStateStore
self.audioFileStore = audioFileStore
self.artworkStore = artworkStore
}
public func loadCachedRemoteTracks() async throws -> [RemoteTrack] {
@@ -29,6 +32,7 @@ public actor RemoteLibrarySyncService {
public func syncRemoteLibrary(deviceId: String) async throws -> [RemoteTrack] {
let tracks = try await repository.syncRemoteTracks(deviceId: deviceId)
try await ensureDownloadStates(for: tracks)
await cacheArtwork(for: tracks, deviceId: deviceId)
return tracks
}
@@ -181,4 +185,29 @@ public actor RemoteLibrarySyncService {
return reconciledStates
}
private func cacheArtwork(
for tracks: [RemoteTrack],
deviceId: String
) async {
for track in tracks {
guard let artwork = track.artwork else {
continue
}
if await artworkStore.cachedFilePath(for: artwork) != nil {
continue
}
do {
let artworkData = try await repository.downloadArtwork(
artworkId: artwork.artworkId,
deviceId: deviceId
)
_ = try await artworkStore.saveArtwork(artworkData, artwork: artwork)
} catch {
continue
}
}
}
}