import Foundation import Observation @Observable @MainActor final class ProgressSyncManager { private let client: ABSClient private(set) var queuedCount: Int = 0 private(set) var lastSyncError: String? /// Latest progress per itemId, persisted to disk. private var queue: [String: PlaybackProgress] = [:] private let queueFile: URL init(client: ABSClient) { self.client = client let dir = AppPaths.supportDirectory try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) self.queueFile = dir.appendingPathComponent("progress-queue.json") loadQueue() } func report(itemId: String, episodeId: String? = nil, currentTime: Double, duration: Double, isFinished: Bool, isOnline: Bool) async { let progress = PlaybackProgress( itemId: itemId, episodeId: episodeId, currentTime: currentTime, duration: duration, isFinished: isFinished, updatedAt: Date() ) let key = progress.syncKey if isOnline { do { try await client.saveProgress(progress) queue.removeValue(forKey: key) persist() lastSyncError = nil return } catch { lastSyncError = error.localizedDescription } } queue[key] = progress persist() } func drain() async { guard !queue.isEmpty else { return } let snapshot = queue for (id, progress) in snapshot { do { try await client.saveProgress(progress) queue.removeValue(forKey: id) } catch { lastSyncError = error.localizedDescription break } } persist() } private func loadQueue() { guard let data = try? Data(contentsOf: queueFile), let decoded = try? JSONDecoder().decode([String: PlaybackProgress].self, from: data) else { return } queue = decoded queuedCount = decoded.count } private func persist() { queuedCount = queue.count do { let data = try JSONEncoder().encode(queue) try data.write(to: queueFile, options: .atomic) } catch { lastSyncError = "Queue konnte nicht gespeichert werden: \(error.localizedDescription)" } } } enum AppPaths { static var supportDirectory: URL { let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first ?? URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Library/Application Support") return base.appendingPathComponent("AudiobookshelfClient", isDirectory: true) } static var downloadsDirectory: URL { supportDirectory.appendingPathComponent("downloads", isDirectory: true) } }