import Foundation // ---- lenient JSON ---------------------------------------------------------------- /// Plex is loose with types (numbers as strings, "0"/"1" booleans); never fail a whole page over it. struct AnyKey: CodingKey { var stringValue: String var intValue: Int? { nil } init(_ s: String) { stringValue = s } init?(stringValue: String) { self.stringValue = stringValue } init?(intValue: Int) { nil } } extension KeyedDecodingContainer where Key == AnyKey { func str(_ k: String) -> String? { let key = AnyKey(k) if let s = try? decodeIfPresent(String.self, forKey: key) { return s } if let i = try? decodeIfPresent(Int64.self, forKey: key) { return String(i) } if let d = try? decodeIfPresent(Double.self, forKey: key) { return String(d) } return nil } func int64(_ k: String) -> Int64? { let key = AnyKey(k) if let i = try? decodeIfPresent(Int64.self, forKey: key) { return i } if let d = try? decodeIfPresent(Double.self, forKey: key) { return Int64(d) } if let s = try? decodeIfPresent(String.self, forKey: key) { return Int64(s) ?? Double(s).map { Int64($0) } } return nil } func int(_ k: String) -> Int? { int64(k).map { Int(clamping: $0) } } func double(_ k: String) -> Double? { let key = AnyKey(k) if let d = try? decodeIfPresent(Double.self, forKey: key) { return d } if let s = try? decodeIfPresent(String.self, forKey: key) { return Double(s) } return nil } func bool(_ k: String) -> Bool? { let key = AnyKey(k) if let b = try? decodeIfPresent(Bool.self, forKey: key) { return b } if let i = try? decodeIfPresent(Int.self, forKey: key) { return i != 0 } if let s = try? decodeIfPresent(String.self, forKey: key) { return s == "1" || s == "true" } return nil } func list(_ k: String) -> [T] { (try? decodeIfPresent([Lenient].self, forKey: AnyKey(k)))?.compactMap(\.value) ?? [] } } /// Drops a single bad array element instead of the array. struct Lenient: Decodable { let value: T? init(from decoder: Decoder) throws { value = try? T(from: decoder) } } // ---- wire format (Accept: application/json) -------------------------------------- struct PlexResponse: Decodable { let container: MediaContainer init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) container = (try? c.decodeIfPresent(MediaContainer.self, forKey: AnyKey("MediaContainer"))) ?? MediaContainer() } } struct MediaContainer: Decodable { var size = 0 var totalSize: Int? var machineIdentifier: String? var metadata: [PlexItem] = [] var directories: [PlexDirectory] = [] var hubs: [PlexHub] = [] var levels: [PlexLevel] = [] init() {} init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) size = c.int("size") ?? 0 totalSize = c.int("totalSize") machineIdentifier = c.str("machineIdentifier") metadata = c.list("Metadata") directories = c.list("Directory") hubs = c.list("Hub") levels = c.list("Level") } } struct PlexItem: Decodable { var ratingKey = "" var key = "" var type = "" var title = "" var titleSort: String? var originalTitle: String? var parentTitle: String? var grandparentTitle: String? var parentRatingKey: String? var grandparentRatingKey: String? var index: Int? var parentIndex: Int? var duration: Int64? var thumb: String? var parentThumb: String? var grandparentThumb: String? var composite: String? var year: Int? var parentYear: Int? /// Release date, "1975-11-14", when Plex knows the day. var originallyAvailableAt: String? var addedAt: Int64? var leafCount: Int? var playlistItemID: Int64? var playlistType: String? /// Boolean in current servers, "0"/"1" in some older ones. var smart: Bool? var media: [PlexMedia] = [] var genres: [PlexTag] = [] init() {} init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) ratingKey = c.str("ratingKey") ?? "" key = c.str("key") ?? "" type = c.str("type") ?? "" title = c.str("title") ?? "" titleSort = c.str("titleSort") originalTitle = c.str("originalTitle") parentTitle = c.str("parentTitle") grandparentTitle = c.str("grandparentTitle") parentRatingKey = c.str("parentRatingKey") grandparentRatingKey = c.str("grandparentRatingKey") index = c.int("index") parentIndex = c.int("parentIndex") duration = c.int64("duration") thumb = c.str("thumb") parentThumb = c.str("parentThumb") grandparentThumb = c.str("grandparentThumb") composite = c.str("composite") year = c.int("year") parentYear = c.int("parentYear") originallyAvailableAt = c.str("originallyAvailableAt") addedAt = c.int64("addedAt") leafCount = c.int("leafCount") playlistItemID = c.int64("playlistItemID") playlistType = c.str("playlistType") smart = c.bool("smart") media = c.list("Media") genres = c.list("Genre") } } struct PlexMedia: Decodable { var id: Int64 = 0 var duration: Int64? var bitrate: Int? var audioCodec: String? var container: String? var parts: [PlexPart] = [] init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) id = c.int64("id") ?? 0 duration = c.int64("duration") bitrate = c.int("bitrate") audioCodec = c.str("audioCodec") container = c.str("container") parts = c.list("Part") } } struct PlexPart: Decodable { var id: Int64 = 0 var key = "" var duration: Int64? var file: String? var size: Int64? var container: String? var streams: [PlexStream] = [] init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) id = c.int64("id") ?? 0 key = c.str("key") ?? "" duration = c.int64("duration") file = c.str("file") size = c.int64("size") container = c.str("container") streams = c.list("Stream") } } struct PlexStream: Decodable { var id: Int64 = 0 var streamType = 0 init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) id = c.int64("id") ?? 0 streamType = c.int("streamType") ?? 0 } } struct PlexTag: Decodable { var tag = "" init(from decoder: Decoder) throws { tag = (try decoder.container(keyedBy: AnyKey.self)).str("tag") ?? "" } } struct PlexDirectory: Decodable { var key = "" var type = "" var title = "" var locations: [PlexLocation] = [] init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) key = c.str("key") ?? "" type = c.str("type") ?? "" title = c.str("title") ?? "" locations = c.list("Location") } } struct PlexLocation: Decodable { var path = "" init(from decoder: Decoder) throws { path = (try decoder.container(keyedBy: AnyKey.self)).str("path") ?? "" } } struct PlexHub: Decodable { var type = "" var hubIdentifier = "" var title = "" var metadata: [PlexItem] = [] init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) type = c.str("type") ?? "" hubIdentifier = c.str("hubIdentifier") ?? "" title = c.str("title") ?? "" metadata = c.list("Metadata") } } struct PlexLevel: Decodable { var v: Float = 0 init(from decoder: Decoder) throws { v = Float((try decoder.container(keyedBy: AnyKey.self)).double("v") ?? 0) } } struct PlexPin: Decodable { var id: Int64 = 0 var code = "" var authToken: String? init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) id = c.int64("id") ?? 0 code = c.str("code") ?? "" authToken = c.str("authToken") } } struct PlexUser: Decodable { var username: String? var title: String? var email: String? init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) username = c.str("username") title = c.str("title") email = c.str("email") } } // ---- app models ------------------------------------------------------------------ struct Track: Codable, Hashable, Identifiable { var ratingKey: String var title: String var artist: String var album: String var albumKey: String? var artistKey: String? var index: Int? var disc: Int? var durationMs: Int64 = 0 var thumb: String? var partKey: String? var partId: Int64? var partFile: String? var partSize: Int64? var codec: String? var year: Int? var playlistItemId: Int64? /// Set for tracks known only from the stems index (no Plex match): play this stem folder. var stemKey: String? var id: String { playlistItemId.map { "\(ratingKey)#\($0)" } ?? ratingKey } init(ratingKey: String, title: String, artist: String, album: String, albumKey: String? = nil, artistKey: String? = nil, index: Int? = nil, disc: Int? = nil, durationMs: Int64 = 0, thumb: String? = nil, partKey: String? = nil, partId: Int64? = nil, partFile: String? = nil, partSize: Int64? = nil, codec: String? = nil, year: Int? = nil, playlistItemId: Int64? = nil, stemKey: String? = nil) { self.ratingKey = ratingKey; self.title = title; self.artist = artist; self.album = album self.albumKey = albumKey; self.artistKey = artistKey; self.index = index; self.disc = disc self.durationMs = durationMs; self.thumb = thumb; self.partKey = partKey; self.partId = partId self.partFile = partFile; self.partSize = partSize; self.codec = codec; self.year = year self.playlistItemId = playlistItemId; self.stemKey = stemKey } init(from decoder: Decoder) throws { let c = try decoder.container(keyedBy: AnyKey.self) self.init(ratingKey: c.str("ratingKey") ?? "", title: c.str("title") ?? "", artist: c.str("artist") ?? "", album: c.str("album") ?? "", albumKey: c.str("albumKey"), artistKey: c.str("artistKey"), index: c.int("index"), disc: c.int("disc"), durationMs: c.int64("durationMs") ?? 0, thumb: c.str("thumb"), partKey: c.str("partKey"), partId: c.int64("partId"), partFile: c.str("partFile"), partSize: c.int64("partSize"), codec: c.str("codec"), year: c.int("year"), playlistItemId: c.int64("playlistItemId"), stemKey: c.str("stemKey")) } } struct Album: Codable, Hashable, Identifiable { var ratingKey: String var title: String var artist: String var artistKey: String? var year: Int? var thumb: String? var trackCount: Int? var genre: String? /// "1975-11-14" when Plex knows the release day. var released: String? var id: String { ratingKey } } struct Artist: Codable, Hashable, Identifiable { var ratingKey: String var title: String var sortTitle: String var thumb: String? var id: String { ratingKey } } struct Playlist: Codable, Hashable, Identifiable { var ratingKey: String var title: String var thumb: String? var trackCount = 0 var durationMs: Int64 = 0 var smart = false var id: String { ratingKey } } struct MusicSection: Hashable, Identifiable { var key: String var title: String var locations: [String] var id: String { key } } struct SearchResults { var artists: [Artist] = [] var albums: [Album] = [] var tracks: [Track] = [] var playlists: [Playlist] = [] var isEmpty: Bool { artists.isEmpty && albums.isEmpty && tracks.isEmpty && playlists.isEmpty } } extension PlexItem { func toTrack() -> Track { let m = media.first let part = m?.parts.first let orig = originalTitle.flatMap { $0.trimmingCharacters(in: .whitespaces).isEmpty ? nil : $0 } return Track( ratingKey: ratingKey, title: title, artist: orig ?? grandparentTitle ?? "Unknown Artist", album: parentTitle ?? "", albumKey: parentRatingKey, artistKey: grandparentRatingKey, index: index, disc: parentIndex, durationMs: duration ?? m?.duration ?? part?.duration ?? 0, thumb: thumb ?? parentThumb ?? grandparentThumb, partKey: part?.key, partId: part?.id, partFile: part?.file, partSize: part?.size, codec: m?.audioCodec ?? part?.container, year: parentYear ?? year, playlistItemId: playlistItemID ) } func toAlbum() -> Album { Album(ratingKey: ratingKey, title: title, artist: parentTitle ?? "Unknown Artist", artistKey: parentRatingKey, year: year, thumb: thumb, trackCount: leafCount, genre: genres.first?.tag, released: originallyAvailableAt.flatMap { $0.count >= 10 ? $0 : nil }) } func toArtist() -> Artist { Artist(ratingKey: ratingKey, title: title, sortTitle: titleSort ?? title, thumb: thumb) } func toPlaylist() -> Playlist { Playlist(ratingKey: ratingKey, title: title, thumb: thumb ?? composite, trackCount: leafCount ?? 0, durationMs: duration ?? 0, smart: smart ?? false) } }