import Foundation /// What ipx sends, as Swift types. The names are the server's, so a field here can be checked /// against src/web.rs without translating first. enum IPX {} extension IPX { /// One subscribed feed, as GET /api/feeds sends it. struct Feed: Decodable, Identifiable, Hashable { let id: String let url: String let title: String? let image: String? /// The OPML subscription it came from, if any. Feeds in one are drawn as a folder. let group: String? /// In a group, but the OPML no longer lists it. Kept because it has downloads. let orphaned: Bool let folder: String? let category: String? let feedCategory: String? let lastChecked: Int? let nextCheck: Int? let lastError: String? let entries: Int let downloaded: Int let unread: Int /// Including you. More than one means every file here is shared. let subscribers: Int let pinned: Bool var name: String { title?.isEmpty == false ? title! : id } enum CodingKeys: String, CodingKey { case id, url, title, image, group, orphaned, folder, category, entries, downloaded, unread, subscribers, pinned case feedCategory = "feed_category" case lastChecked = "last_checked" case nextCheck = "next_check" case lastError = "last_error" } } /// A file on an entry. `path` is the only thing that says it is downloaded. struct Enclosure: Decodable, Identifiable, Hashable { let id: Int let feedId: String let guid: String let url: String let mime: String? let length: Int? let path: String? let state: String let lastError: String? var isDownloaded: Bool { path?.isEmpty == false } /// Whether it is worth handing to a player. Having a file is not the same as being /// playable: blog feeds put article images in enclosures. var isPlayable: Bool { guard isDownloaded else { return false } let m = (mime ?? "").lowercased() if m.hasPrefix("audio/") || m.hasPrefix("video/") { return true } if !m.isEmpty { return false } let name = (path ?? url).split(separator: "?").first.map(String.init) ?? "" let ext = (name as NSString).pathExtension.lowercased() return ["mp3","m4a","m4b","aac","ogg","oga","opus","flac","wav", "mp4","m4v","mov","webm","mkv"].contains(ext) } var isVideo: Bool { (mime ?? "").lowercased().hasPrefix("video/") } enum CodingKeys: String, CodingKey { case id, guid, url, mime, length, path, state case feedId = "feed_id" case lastError = "last_error" } } /// One item. Read, pinned and position are this person's; the entry itself is shared. struct Entry: Decodable, Identifiable, Hashable { let guid: String let feedId: String let title: String? let link: String? let published: Int? /// Feed-supplied HTML, already sanitized with ammonia server-side. let description: String? let read: Bool let flagged: Bool let image: String? let duration: Int? let episode: Int? let season: Int? let position: Int let enclosures: [Enclosure] /// Unique across feeds; guid alone is not. var id: String { feedId + "\n" + guid } var playable: Enclosure? { enclosures.first(where: \.isPlayable) } enum CodingKeys: String, CodingKey { case guid, title, link, published, description, read, flagged, image, duration, episode, season, position, enclosures case feedId = "feed_id" } } /// A page of items. `total` is the whole result, not the page. struct EntryPage: Decodable { let total: Int let entries: [Entry] } struct Me: Decodable { let name: String let admin: Bool /// Where to send someone the proxy signed in; signing out of ipx alone cannot stick. let signOut: String? let theme: String? let mode: String? enum CodingKeys: String, CodingKey { case name, admin, theme, mode case signOut = "sign_out" } } /// Which items to ask for. The server does the work; these go on the query. enum Filter: String, CaseIterable { case all, unread, downloaded, pinned } enum Sort: String { case published, title, feed, kept, type, size } enum Direction: String { case asc, desc } }