import Foundation class ABSService: ObservableObject { static let shared = ABSService() func fetchLibraries(serverURL: String, token: String) async throws -> [Library] { let url = URL(string: "\(serverURL)/api/libraries")! var request = URLRequest(url: url) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let (data, _) = try await URLSession.shared.data(for: request) let response = try JSONDecoder().decode(LibrariesResponse.self, from: data) return response.libraries } func fetchBooks(serverURL: String, token: String, libraryId: String) async throws -> [AudiobookItem] { let url = URL(string: "\(serverURL)/api/libraries/\(libraryId)/items?limit=100")! var request = URLRequest(url: url) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let (data, _) = try await URLSession.shared.data(for: request) let response = try JSONDecoder().decode(LibraryItemsResponse.self, from: data) return response.results.compactMap { item in AudiobookItem( id: item.id, title: item.media.metadata.title ?? "Unbekannt", author: item.media.metadata.authorName ?? "Unbekannt", coverURL: item.media.coverPath != nil ? "\(serverURL)/api/items/\(item.id)/cover" : nil, duration: item.media.duration ?? 0, mediaFiles: item.media.audioFiles?.map { AudiobookItem.MediaFile(ino: $0.ino, name: $0.metadata.filename, path: $0.metadata.path) } ?? [] ) } } func fetchProgress(serverURL: String, token: String, itemId: String) async throws -> LibraryProgress? { let url = URL(string: "\(serverURL)/api/me/progress/\(itemId)")! var request = URLRequest(url: url) request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") let (data, response) = try await URLSession.shared.data(for: request) guard (response as? HTTPURLResponse)?.statusCode == 200 else { return nil } return try? JSONDecoder().decode(LibraryProgress.self, from: data) } func updateProgress(serverURL: String, token: String, itemId: String, currentTime: Double, duration: Double) async { guard let url = URL(string: "\(serverURL)/api/me/progress/\(itemId)") else { return } var request = URLRequest(url: url) request.httpMethod = "PATCH" request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") request.setValue("application/json", forHTTPHeaderField: "Content-Type") let body: [String: Any] = [ "currentTime": currentTime, "duration": duration, "isFinished": currentTime >= duration - 5 ] request.httpBody = try? JSONSerialization.data(withJSONObject: body) try? await URLSession.shared.data(for: request) } } // MARK: - API Response Models struct LibrariesResponse: Codable { let libraries: [Library] } struct LibraryItemsResponse: Codable { let results: [RawLibraryItem] } struct RawLibraryItem: Codable { let id: String let media: RawMedia } struct RawMedia: Codable { let metadata: RawMetadata let coverPath: String? let duration: Double? let audioFiles: [RawAudioFile]? } struct RawMetadata: Codable { let title: String? let authorName: String? } struct RawAudioFile: Codable { let ino: String let metadata: RawFileMetadata } struct RawFileMetadata: Codable { let filename: String let path: String }