import Foundation struct Chapter: Codable, Identifiable, Hashable { let id: Int let start: Double let end: Double let title: String } struct Library: Codable, Identifiable, Hashable { let id: String let name: String let mediaType: String? } /// Wichtig für `ForEach`: Synthetische Podcast-Episoden-Items (z.B. aus der /// library-weiten Folgensuche) teilen sich denselben `id` (Podcast-Container-ID) /// und unterscheiden sich nur über `episodeId`. Default-`Identifiable` würde /// hier kollidieren. **Immer** `ForEach(items, id: \.syncKey)` benutzen wenn /// die Liste sowohl Container als auch Episoden-Items enthalten kann. struct LibraryItem: Codable, Identifiable, Hashable { let id: String let title: String let author: String let durationSeconds: Double let audioFiles: [AudioFile] var mediaType: String = "book" var episodeId: String? = nil var description: String? = nil var chapters: [Chapter] = [] var isPodcast: Bool { mediaType == "podcast" } var isPodcastContainer: Bool { isPodcast && episodeId == nil } var isPodcastEpisode: Bool { isPodcast && episodeId != nil } static func == (lhs: LibraryItem, rhs: LibraryItem) -> Bool { lhs.id == rhs.id && lhs.episodeId == rhs.episodeId } func hash(into hasher: inout Hasher) { hasher.combine(id) hasher.combine(episodeId) } } struct PodcastEpisode: Identifiable, Hashable, Codable { let id: String let title: String let pubDate: String? let publishedAtMillis: Double? let season: String? let episode: String? let durationSeconds: Double let audioFile: AudioFile var formattedDate: String? { if let ms = publishedAtMillis, ms > 0 { let date = Date(timeIntervalSince1970: ms / 1000.0) let df = DateFormatter() df.dateStyle = .medium df.timeStyle = .none return df.string(from: date) } return pubDate } } struct AudioFile: Codable, Hashable { let ino: String let filename: String let ext: String let durationSeconds: Double let index: Int } struct PlaybackProgress: Codable, Hashable { let itemId: String var episodeId: String? var currentTime: Double var duration: Double var isFinished: Bool var updatedAt: Date /// Server-assigned UUID for this progress entry. Required to delete it — /// Audiobookshelf's `DELETE /api/me/progress/:id` matches by this id, NOT /// by libraryItemId. Optional so older persisted caches still decode. var id: String? var syncKey: String { if let episodeId { return "\(itemId)|\(episodeId)" } return itemId } } enum DownloadState: Equatable { case notDownloaded case downloading(progress: Double) case downloaded case failed(message: String) }