142 lines
4.7 KiB
Swift
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)
|
|
}
|
|
}
|