import Foundation import UIKit /// The bits of ipx's API the native side writes back to, and the artwork it shows. /// /// Small on purpose. The page does the browsing; this is only what the host has to do when the /// page cannot -- which is any time the screen is locked, and all the time in CarPlay. final class Library { private let cookies: CookieBridge private let session: URLSession init(cookies: CookieBridge) { self.cookies = cookies let cfg = URLSessionConfiguration.default cfg.httpCookieStorage = HTTPCookieStorage.shared cfg.httpShouldSetCookies = true cfg.waitsForConnectivity = true self.session = URLSession(configuration: cfg) } struct Unauthorized: Error {} /// Where you are in an episode. The page sends this too, but only while it is running: /// a backgrounded web view is frozen, and this is the drive it would miss. func savePosition(feedId: String, guid: String, secs: Int, duration: Int?) { post("/api/entries/\(esc(feedId))/\(esc(guid))/position", position(secs, duration)) } func markRead(feedId: String, guid: String) { post("/api/entries/\(esc(feedId))/\(esc(guid))/flags", ["read": true]) } /// A path the page handed over, used as-is. It already names the right item, and rebuilding it /// here would be a second place for the escaping to be wrong. func savePosition(path: String, secs: Int, duration: Int?) { post(path, position(secs, duration)) } /// `duration` is left out when there is none rather than sent as null: it is Option on /// the server either way, and `duration as Any` on a nil Optional is an Any wrapping nil, /// which JSONSerialization refuses -- leaving the request with no body at all. private func position(_ secs: Int, _ duration: Int?) -> [String: Any] { var body: [String: Any] = ["secs": secs] if let duration { body["duration"] = duration } return body } func artwork(_ path: String, done: @escaping (UIImage?) -> Void) { guard let url = ServerSettings.url(path) else { return done(nil) } session.dataTask(with: url) { data, _, _ in done(data.flatMap(UIImage.init(data:))) }.resume() } /// Whether the session is still good, so a failure can say "sign in again" rather than stall. func check(done: @escaping (Bool) -> Void) { guard let url = ServerSettings.url("/api/me") else { return done(false) } session.dataTask(with: url) { _, resp, _ in done((resp as? HTTPURLResponse)?.statusCode == 200) }.resume() } private func post(_ path: String, _ body: [String: Any]) { guard let url = ServerSettings.url(path) else { return } var req = URLRequest(url: url) req.httpMethod = "POST" req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.httpBody = try? JSONSerialization.data(withJSONObject: body) session.dataTask(with: req) { _, resp, err in if let code = (resp as? HTTPURLResponse)?.statusCode, code >= 400 { NSLog("ipx: %@ -> %d", path, code) } else if let err { NSLog("ipx: %@ -> %@", path, err.localizedDescription) } }.resume() } private func esc(_ s: String) -> String { s.addingPercentEncoding(withAllowedCharacters: .alphanumerics.union(.init(charactersIn: "-._~"))) ?? s } }