velody/packages/apple/VelodySync/Sources/VelodySync/OfflineLibraryService.swift
2026-05-30 09:43:14 +02:00

142 lines
4.7 KiB
Swift

import Foundation
import VelodyDomain
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,
artworkStore: any ArtworkStore
) {
self.syncService = syncService
self.audioFileStore = audioFileStore
self.artworkStore = artworkStore
}
public func loadSnapshot() async throws -> OfflineLibrarySnapshot {
let remoteTracks = try await syncService.loadCachedRemoteTracks()
let downloadStates = try await syncService.loadDownloadStates()
let downloadStatesByTrackID = Dictionary(
uniqueKeysWithValues: downloadStates.map { ($0.remoteTrackId, $0) }
)
var remoteLibraryTracks: [OfflineLibraryRemoteTrack] = []
var availableOfflineTracks: [OfflineLibraryTrack] = []
for remoteTrack in remoteTracks {
let downloadState = downloadStatesByTrackID[remoteTrack.trackId]
let isFileAvailable = await Self.isFileAvailable(
for: downloadState,
audioFileStore: audioFileStore
)
let localFilePath = downloadState?.localFilePath ?? ""
let downloadedAt = downloadState?.downloadedAt
let status = Self.remoteStatus(for: downloadState, isFileAvailable: isFileAvailable)
let lastDownloadError = Self.lastDownloadError(
for: downloadState,
status: status
)
let localArtworkFilePath = await Self.localArtworkFilePath(
for: remoteTrack.artwork,
artworkStore: artworkStore
)
remoteLibraryTracks.append(
OfflineLibraryRemoteTrack(
remoteTrack: remoteTrack,
localFilePath: localFilePath,
downloadedAt: downloadedAt,
isFileAvailable: isFileAvailable,
status: status,
lastDownloadError: lastDownloadError,
localArtworkFilePath: localArtworkFilePath
)
)
guard isFileAvailable else {
continue
}
availableOfflineTracks.append(
OfflineLibraryTrack(
remoteTrackId: remoteTrack.trackId,
assetId: remoteTrack.assetId,
title: remoteTrack.title,
artist: remoteTrack.artist,
durationSeconds: remoteTrack.durationSeconds,
sha256: remoteTrack.sha256,
localFilePath: localFilePath,
downloadedAt: downloadedAt,
isFileAvailable: true,
localArtworkFilePath: localArtworkFilePath
)
)
}
return OfflineLibrarySnapshot(
remoteTracks: remoteLibraryTracks,
availableTracks: availableOfflineTracks
)
}
private static func isFileAvailable(
for downloadState: RemoteTrackDownloadState?,
audioFileStore: any OfflineAudioFileStore
) async -> Bool {
guard let downloadState,
downloadState.downloadStatus == .downloaded,
downloadState.hasLocalFile
else {
return false
}
return await audioFileStore.fileExists(at: downloadState.localFilePath)
}
private static func remoteStatus(
for downloadState: RemoteTrackDownloadState?,
isFileAvailable: Bool
) -> OfflineLibraryRemoteTrackStatus {
guard let downloadState else {
return .notDownloaded
}
switch downloadState.downloadStatus {
case .notDownloaded:
return .notDownloaded
case .downloading:
return .downloading
case .downloaded:
return isFileAvailable ? .downloaded : .missing
case .failed:
return .failed
}
}
private static func lastDownloadError(
for downloadState: RemoteTrackDownloadState?,
status: OfflineLibraryRemoteTrackStatus
) -> String? {
if status == .missing {
return "The downloaded MP3 file is missing."
}
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)
}
}