import Foundation struct PodcastEntry: Codable, Identifiable { var id: String { articleUrl } let articleUrl: String let filename: String var title: String? let createdAt: Date var parentBookmarkUrl: String? /// When the episode was finished. `nil` means unplayed/new. Absent in /// pre-existing index files, which decode as `nil` (i.e. unplayed). var playedAt: Date? = nil var isPlayed: Bool { playedAt != nil } } enum PodcastIndex { private static let indexURL: URL = { let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] .appendingPathComponent("podcasts") return dir.appendingPathComponent("index.json") }() private static let encoder: JSONEncoder = { let e = JSONEncoder() e.dateEncodingStrategy = .iso8601 e.outputFormatting = .prettyPrinted return e }() private static let decoder: JSONDecoder = { let d = JSONDecoder() d.dateDecodingStrategy = .iso8601 return d }() static func all() -> [PodcastEntry] { guard let data = try? Data(contentsOf: indexURL), let entries = try? decoder.decode([PodcastEntry].self, from: data) else { return [] } return entries.sorted { $0.createdAt > $1.createdAt } } static func upsert(articleUrl: String, filename: String, title: String?, parentBookmarkUrl: String? = nil) { var entries = all() entries.removeAll { $0.articleUrl == articleUrl } entries.insert(PodcastEntry(articleUrl: articleUrl, filename: filename, title: title, createdAt: Date(), parentBookmarkUrl: parentBookmarkUrl), at: 0) save(entries) } static func find(for bookmarkUrl: String) -> [PodcastEntry] { all().filter { $0.parentBookmarkUrl == bookmarkUrl || $0.articleUrl == bookmarkUrl } } static func isPlayed(articleUrl: String) -> Bool { all().first { $0.articleUrl == articleUrl }?.isPlayed ?? false } /// Mark an episode played (default) or back to unplayed. No-op if unknown. static func setPlayed(articleUrl: String, _ played: Bool = true) { var entries = all() guard let i = entries.firstIndex(where: { $0.articleUrl == articleUrl }) else { return } entries[i].playedAt = played ? Date() : nil save(entries) } static func remove(articleUrl: String) { var entries = all() entries.removeAll { $0.articleUrl == articleUrl } save(entries) } private static func save(_ entries: [PodcastEntry]) { guard let data = try? encoder.encode(entries) else { return } try? data.write(to: indexURL, options: .atomic) WidgetDataStore.savePodcasts(entries.prefix(10).map { WidgetPodcast(articleUrl: $0.articleUrl, title: $0.title ?? $0.articleUrl, createdAt: $0.createdAt) }) } }