Implement artwork download and cache
This commit is contained in:
+94
-8
@@ -31,7 +31,8 @@ final class RemoteLibrarySyncServiceTests: XCTestCase {
|
||||
store: store
|
||||
),
|
||||
downloadStateStore: downloadStateStore,
|
||||
audioFileStore: InMemoryOfflineAudioFileStore()
|
||||
audioFileStore: InMemoryOfflineAudioFileStore(),
|
||||
artworkStore: InMemoryArtworkStore()
|
||||
)
|
||||
|
||||
let tracks = try await service.syncRemoteLibrary(deviceId: "device-123")
|
||||
@@ -76,7 +77,8 @@ final class RemoteLibrarySyncServiceTests: XCTestCase {
|
||||
store: store
|
||||
),
|
||||
downloadStateStore: downloadStateStore,
|
||||
audioFileStore: InMemoryOfflineAudioFileStore()
|
||||
audioFileStore: InMemoryOfflineAudioFileStore(),
|
||||
artworkStore: InMemoryArtworkStore()
|
||||
)
|
||||
|
||||
let tracks = try await service.syncRemoteLibrary(deviceId: "device-123")
|
||||
@@ -109,7 +111,8 @@ final class RemoteLibrarySyncServiceTests: XCTestCase {
|
||||
store: store
|
||||
),
|
||||
downloadStateStore: downloadStateStore,
|
||||
audioFileStore: InMemoryOfflineAudioFileStore()
|
||||
audioFileStore: InMemoryOfflineAudioFileStore(),
|
||||
artworkStore: InMemoryArtworkStore()
|
||||
)
|
||||
|
||||
await XCTAssertThrowsErrorAsync {
|
||||
@@ -132,7 +135,8 @@ final class RemoteLibrarySyncServiceTests: XCTestCase {
|
||||
store: InMemoryRemoteLibraryStore()
|
||||
),
|
||||
downloadStateStore: downloadStateStore,
|
||||
audioFileStore: audioFileStore
|
||||
audioFileStore: audioFileStore,
|
||||
artworkStore: InMemoryArtworkStore()
|
||||
)
|
||||
let track = RemoteTrack(
|
||||
trackId: "track-123",
|
||||
@@ -166,7 +170,8 @@ final class RemoteLibrarySyncServiceTests: XCTestCase {
|
||||
store: InMemoryRemoteLibraryStore()
|
||||
),
|
||||
downloadStateStore: InMemoryRemoteTrackDownloadStateStore(),
|
||||
audioFileStore: InMemoryOfflineAudioFileStore()
|
||||
audioFileStore: InMemoryOfflineAudioFileStore(),
|
||||
artworkStore: InMemoryArtworkStore()
|
||||
)
|
||||
let track = RemoteTrack(
|
||||
trackId: "track-123",
|
||||
@@ -222,7 +227,8 @@ final class RemoteLibrarySyncServiceTests: XCTestCase {
|
||||
store: InMemoryRemoteLibraryStore()
|
||||
),
|
||||
downloadStateStore: try FileRemoteTrackDownloadStateStore(fileURL: stateFileURL),
|
||||
audioFileStore: try FileOfflineAudioFileStore(baseDirectoryURL: firstAudioDirectory)
|
||||
audioFileStore: try FileOfflineAudioFileStore(baseDirectoryURL: firstAudioDirectory),
|
||||
artworkStore: InMemoryArtworkStore()
|
||||
)
|
||||
|
||||
let originalState = try await firstService.downloadTrack(track, deviceId: "device-123")
|
||||
@@ -238,7 +244,8 @@ final class RemoteLibrarySyncServiceTests: XCTestCase {
|
||||
store: InMemoryRemoteLibraryStore()
|
||||
),
|
||||
downloadStateStore: try FileRemoteTrackDownloadStateStore(fileURL: stateFileURL),
|
||||
audioFileStore: relaunchedAudioStore
|
||||
audioFileStore: relaunchedAudioStore,
|
||||
artworkStore: InMemoryArtworkStore()
|
||||
)
|
||||
|
||||
let restoredStates = try await relaunchedService.loadDownloadStates()
|
||||
@@ -254,6 +261,58 @@ final class RemoteLibrarySyncServiceTests: XCTestCase {
|
||||
XCTAssertTrue(fileManager.fileExists(atPath: restoredState.localFilePath))
|
||||
XCTAssertEqual(restoredBytes, audioData)
|
||||
}
|
||||
|
||||
func testSyncCachesArtworkIndependentlyFromAudioDownloads() async throws {
|
||||
let artwork = RemoteArtwork(
|
||||
artworkId: "artwork-123",
|
||||
sha256: String(repeating: "d", count: 64),
|
||||
mimeType: "image/png",
|
||||
width: 1,
|
||||
height: 1
|
||||
)
|
||||
let artworkStore = InMemoryArtworkStore()
|
||||
let service = RemoteLibrarySyncService(
|
||||
repository: DefaultRemoteLibraryRepository(
|
||||
apiClient: MockVelodyAPIClient(
|
||||
remoteLibraryResponse: RemoteLibraryResponseDTO(
|
||||
tracks: [
|
||||
RemoteTrackDTO(
|
||||
trackId: "track-123",
|
||||
title: "Remote Title",
|
||||
artist: "Remote Artist",
|
||||
durationSeconds: 245,
|
||||
sha256: String(repeating: "a", count: 64),
|
||||
assetId: "asset-456",
|
||||
createdAt: "2026-05-29T08:00:00.000Z",
|
||||
updatedAt: "2026-05-29T08:05:00.000Z",
|
||||
artwork: RemoteArtworkDTO(
|
||||
artworkId: artwork.artworkId,
|
||||
sha256: artwork.sha256,
|
||||
mimeType: artwork.mimeType,
|
||||
width: artwork.width,
|
||||
height: artwork.height
|
||||
)
|
||||
),
|
||||
]
|
||||
),
|
||||
artworkData: sampleArtworkData()
|
||||
),
|
||||
store: InMemoryRemoteLibraryStore()
|
||||
),
|
||||
downloadStateStore: InMemoryRemoteTrackDownloadStateStore(),
|
||||
audioFileStore: InMemoryOfflineAudioFileStore(),
|
||||
artworkStore: artworkStore
|
||||
)
|
||||
|
||||
let tracks = try await service.syncRemoteLibrary(deviceId: "device-123")
|
||||
let cachedArtworkPath = await artworkStore.cachedFilePath(for: artwork)
|
||||
let cachedArtworkBytes = try await artworkStore.readArtwork(
|
||||
at: try XCTUnwrap(cachedArtworkPath)
|
||||
)
|
||||
|
||||
XCTAssertEqual(tracks.first?.artwork, artwork)
|
||||
XCTAssertEqual(cachedArtworkBytes, sampleArtworkData())
|
||||
}
|
||||
}
|
||||
|
||||
private struct MockVelodyAPIClient: VelodyAPIClient {
|
||||
@@ -261,17 +320,23 @@ private struct MockVelodyAPIClient: VelodyAPIClient {
|
||||
let remoteLibraryError: VelodyAPIError?
|
||||
let audioAssetData: Data?
|
||||
let downloadError: VelodyAPIError?
|
||||
let artworkData: Data?
|
||||
let artworkDownloadError: VelodyAPIError?
|
||||
|
||||
init(
|
||||
remoteLibraryResponse: RemoteLibraryResponseDTO? = nil,
|
||||
remoteLibraryError: VelodyAPIError? = nil,
|
||||
audioAssetData: Data? = nil,
|
||||
downloadError: VelodyAPIError? = nil
|
||||
downloadError: VelodyAPIError? = nil,
|
||||
artworkData: Data? = nil,
|
||||
artworkDownloadError: VelodyAPIError? = nil
|
||||
) {
|
||||
self.remoteLibraryResponse = remoteLibraryResponse
|
||||
self.remoteLibraryError = remoteLibraryError
|
||||
self.audioAssetData = audioAssetData
|
||||
self.downloadError = downloadError
|
||||
self.artworkData = artworkData
|
||||
self.artworkDownloadError = artworkDownloadError
|
||||
}
|
||||
|
||||
func registerDevice(
|
||||
@@ -331,6 +396,20 @@ private struct MockVelodyAPIClient: VelodyAPIClient {
|
||||
return audioAssetData ?? Data()
|
||||
}
|
||||
|
||||
func downloadArtwork(
|
||||
artworkId: String,
|
||||
deviceId: String
|
||||
) async throws -> Data {
|
||||
_ = artworkId
|
||||
_ = deviceId
|
||||
|
||||
if let artworkDownloadError {
|
||||
throw artworkDownloadError
|
||||
}
|
||||
|
||||
return artworkData ?? Data()
|
||||
}
|
||||
|
||||
func prepareUpload(
|
||||
_ payload: UploadPrepareRequest
|
||||
) async throws -> UploadPrepareResponse {
|
||||
@@ -385,6 +464,13 @@ private func sampleMp3Data(seed: String) -> Data {
|
||||
] + Array(seed.utf8))
|
||||
}
|
||||
|
||||
private func sampleArtworkData() -> Data {
|
||||
Data(
|
||||
base64Encoded:
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQIW2P8z8DwHwAFgwJ/lBi4NwAAAABJRU5ErkJggg=="
|
||||
)!
|
||||
}
|
||||
|
||||
private func sha256Hex(_ data: Data) -> String {
|
||||
SHA256.hash(data: data).map { String(format: "%02x", $0) }.joined()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user