126 lines
3.9 KiB
Swift
126 lines
3.9 KiB
Swift
import Foundation
|
|
import VelodyDomain
|
|
import VelodyNetworking
|
|
import VelodyPersistence
|
|
|
|
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 {
|
|
private let apiClient: any VelodyAPIClient
|
|
private let store: any RemoteLibraryStore
|
|
private let syncCursorStore: any RemoteLibrarySyncCursorStore
|
|
|
|
public init(
|
|
apiClient: any VelodyAPIClient,
|
|
store: any RemoteLibraryStore,
|
|
syncCursorStore: any RemoteLibrarySyncCursorStore
|
|
) {
|
|
self.apiClient = apiClient
|
|
self.store = store
|
|
self.syncCursorStore = syncCursorStore
|
|
}
|
|
|
|
public func loadCachedRemoteTracks() async throws -> [RemoteTrack] {
|
|
try await store.loadRemoteTracks()
|
|
}
|
|
|
|
public func syncRemoteTracks(deviceId: String) async throws -> [RemoteTrack] {
|
|
_ = deviceId
|
|
|
|
if let currentCursor = try await syncCursorStore.loadCursor() {
|
|
return try await syncIncrementally(from: currentCursor)
|
|
}
|
|
|
|
return try await bootstrap()
|
|
}
|
|
|
|
public func downloadAudioAsset(
|
|
assetId: String,
|
|
deviceId: String
|
|
) 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)
|
|
}
|
|
|
|
private func bootstrap() async throws -> [RemoteTrack] {
|
|
let response = try await apiClient.fetchSyncBootstrap()
|
|
let tracks = orderTracks(response.tracks)
|
|
try await store.replaceRemoteTracks(tracks)
|
|
try await syncCursorStore.saveCursor(response.nextCursor)
|
|
return tracks
|
|
}
|
|
|
|
private func syncIncrementally(
|
|
from cursor: SyncCursor
|
|
) async throws -> [RemoteTrack] {
|
|
let cachedTracks = try await store.loadRemoteTracks()
|
|
var mergedTracks = Dictionary(
|
|
uniqueKeysWithValues: cachedTracks.map { ($0.trackId, $0) }
|
|
)
|
|
var currentCursor = cursor
|
|
|
|
while true {
|
|
let response = try await apiClient.fetchSyncChanges(cursor: currentCursor)
|
|
|
|
if response.requiresBootstrap {
|
|
return try await bootstrap()
|
|
}
|
|
|
|
mergedTracks = apply(events: response.events, to: mergedTracks)
|
|
currentCursor = response.nextCursor
|
|
|
|
if !response.hasMore {
|
|
break
|
|
}
|
|
}
|
|
|
|
let orderedTracks = orderTracks(Array(mergedTracks.values))
|
|
try await store.replaceRemoteTracks(orderedTracks)
|
|
try await syncCursorStore.saveCursor(currentCursor)
|
|
return orderedTracks
|
|
}
|
|
|
|
private func apply(
|
|
events: [SyncEvent],
|
|
to tracksByID: [String: RemoteTrack]
|
|
) -> [String: RemoteTrack] {
|
|
var nextTracksByID = tracksByID
|
|
|
|
for event in events {
|
|
if let deletedTrackID = event.deletedTrackId, !deletedTrackID.isEmpty {
|
|
nextTracksByID.removeValue(forKey: deletedTrackID)
|
|
continue
|
|
}
|
|
|
|
guard let track = event.track else {
|
|
continue
|
|
}
|
|
|
|
nextTracksByID[track.trackId] = track
|
|
}
|
|
|
|
return nextTracksByID
|
|
}
|
|
|
|
private func orderTracks(_ tracks: [RemoteTrack]) -> [RemoteTrack] {
|
|
tracks.sorted { lhs, rhs in
|
|
if lhs.createdAt == rhs.createdAt {
|
|
return lhs.trackId < rhs.trackId
|
|
}
|
|
|
|
return lhs.createdAt < rhs.createdAt
|
|
}
|
|
}
|
|
}
|