Zwei Protokolle statt eines: LiveTVBackend für Sender, Programm und Einschalten, LiveTVRecordingBackend für Aufnahmen. Getrennt, weil Plex das eine kann und das andere nicht — ein as? sagt die Wahrheit, eine Methode, die bei Plex nur wirft, verspricht etwas und bricht es dann. Plex zieht um, ohne umgebaut zu werden. Der Suchlauf, der die Senderliste findet, bleibt Zeile für Zeile, wo er ist; davor liegt jetzt ein Actor, der sich Empfänger und Sender merkt. Nötig, weil PlexBackend ein struct ist und tune() den echten PlexChannel braucht — ohne Gedächtnis liefe der Suchlauf bei jedem Antippen neu. Jellyfin ist neu und deutlich kürzer, weil es dort nichts zu suchen gibt. Die Endpunkte stammen aus der OpenAPI-Spec des Servers. Beim Vormerken wird die Vorlage des Servers roh weitergereicht statt entschlüsselt und neu zusammengesetzt: welche Felder Pflicht sind, sagt die Beschreibung nicht, und ein verlorenes fiele erst an einer Aufnahme auf, die nicht startet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
207 lines
7.8 KiB
Swift
207 lines
7.8 KiB
Swift
import Foundation
|
|
import MediaKit
|
|
|
|
/// Ein Eintrag, wie Jellyfin ihn über die Leitung schickt.
|
|
///
|
|
/// Die Felder heißen groß und die Zeiten sind Ticks zu hundert Nanosekunden —
|
|
/// beides bleibt hier unten. Nach oben geht nur `MediaItem`.
|
|
public struct JellyfinItem: Decodable, Sendable {
|
|
public let id: String
|
|
public let name: String?
|
|
public let type: String?
|
|
public let overview: String?
|
|
public let productionYear: Int?
|
|
/// Laufzeit in Ticks zu 100 ns.
|
|
public let runTimeTicks: Int64?
|
|
public let indexNumber: Int?
|
|
public let parentIndexNumber: Int?
|
|
public let seriesName: String?
|
|
public let seriesID: String?
|
|
public let seasonID: String?
|
|
public let seasonName: String?
|
|
public let officialRating: String?
|
|
public let communityRating: Double?
|
|
public let criticRating: Double?
|
|
public let dateCreated: Date?
|
|
public let childCount: Int?
|
|
public let recursiveItemCount: Int?
|
|
public let genres: [String]?
|
|
public let people: [Person]?
|
|
public let userData: UserData?
|
|
public let imageTags: [String: String]?
|
|
public let backdropImageTags: [String]?
|
|
public let parentBackdropItemID: String?
|
|
public let seriesPrimaryImageTag: String?
|
|
public let mediaSources: [JellyfinMediaSource]?
|
|
public let collectionType: String?
|
|
|
|
// MARK: - Live-TV
|
|
//
|
|
// Sender **und** Sendungen kommen bei Jellyfin als derselbe Typ wie
|
|
// Filme und Folgen. Deshalb stehen ihre Felder hier und nicht in einem
|
|
// eigenen Modell: ein zweites, das zu neunzig Prozent dasselbe trägt,
|
|
// wäre eine Kopie mit Pflegeaufwand.
|
|
|
|
/// Zu welchem Sender eine Sendung gehört.
|
|
public let channelID: String?
|
|
public let channelName: String?
|
|
/// Die Nummer, unter der ein Sender im Programm steht — „3" oder „11.1".
|
|
public let channelNumber: String?
|
|
/// Dasselbe, aber am Sender selbst statt an der Sendung.
|
|
public let number: String?
|
|
public let startDate: Date?
|
|
public let endDate: Date?
|
|
/// Was auf diesem Sender gerade läuft.
|
|
///
|
|
/// Kommt nur, wenn die Senderliste mit `addCurrentProgram` geholt wurde —
|
|
/// dann aber gleich mit, und die Liste braucht keinen zweiten Aufruf.
|
|
public let currentProgram: JellyfinCurrentProgram?
|
|
/// Ob die Sendung zu einem laufenden Serien-Timer gehört.
|
|
public let seriesTimerID: String?
|
|
public let isMovie: Bool?
|
|
public let isSeries: Bool?
|
|
public let isNews: Bool?
|
|
public let isSports: Bool?
|
|
public let isKids: Bool?
|
|
public let isLive: Bool?
|
|
|
|
public struct Person: Decodable, Sendable {
|
|
public let id: String?
|
|
public let name: String?
|
|
public let role: String?
|
|
public let type: String?
|
|
public let primaryImageTag: String?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id = "Id", name = "Name", role = "Role", type = "Type"
|
|
case primaryImageTag = "PrimaryImageTag"
|
|
}
|
|
}
|
|
|
|
public struct UserData: Decodable, Sendable {
|
|
public let playbackPositionTicks: Int64?
|
|
public let playCount: Int?
|
|
public let played: Bool?
|
|
public let isFavorite: Bool?
|
|
public let unplayedItemCount: Int?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case playbackPositionTicks = "PlaybackPositionTicks"
|
|
case playCount = "PlayCount"
|
|
case played = "Played"
|
|
case isFavorite = "IsFavorite"
|
|
case unplayedItemCount = "UnplayedItemCount"
|
|
}
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id = "Id", name = "Name", type = "Type", overview = "Overview"
|
|
case productionYear = "ProductionYear", runTimeTicks = "RunTimeTicks"
|
|
case indexNumber = "IndexNumber", parentIndexNumber = "ParentIndexNumber"
|
|
case seriesName = "SeriesName", seriesID = "SeriesId"
|
|
case seasonID = "SeasonId", seasonName = "SeasonName"
|
|
case officialRating = "OfficialRating", communityRating = "CommunityRating"
|
|
case criticRating = "CriticRating", dateCreated = "DateCreated"
|
|
case childCount = "ChildCount", recursiveItemCount = "RecursiveItemCount"
|
|
case genres = "Genres", people = "People", userData = "UserData"
|
|
case imageTags = "ImageTags", backdropImageTags = "BackdropImageTags"
|
|
case parentBackdropItemID = "ParentBackdropItemId"
|
|
case seriesPrimaryImageTag = "SeriesPrimaryImageTag"
|
|
case mediaSources = "MediaSources", collectionType = "CollectionType"
|
|
case channelID = "ChannelId", channelName = "ChannelName"
|
|
case channelNumber = "ChannelNumber", number = "Number"
|
|
case startDate = "StartDate", endDate = "EndDate"
|
|
case currentProgram = "CurrentProgram", seriesTimerID = "SeriesTimerId"
|
|
case isMovie = "IsMovie", isSeries = "IsSeries", isNews = "IsNews"
|
|
case isSports = "IsSports", isKids = "IsKids", isLive = "IsLive"
|
|
}
|
|
}
|
|
|
|
/// Was auf einem Sender gerade läuft.
|
|
///
|
|
/// Ein eigenes, schmales Modell statt eines verschachtelten `JellyfinItem`:
|
|
/// ein Struct darf sich nicht selbst enthalten — seine Größe wäre unendlich.
|
|
/// Gebraucht werden hier ohnehin nur fünf Felder.
|
|
public struct JellyfinCurrentProgram: Decodable, Sendable {
|
|
public let id: String
|
|
public let name: String?
|
|
public let overview: String?
|
|
public let startDate: Date?
|
|
public let endDate: Date?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id = "Id", name = "Name", overview = "Overview"
|
|
case startDate = "StartDate", endDate = "EndDate"
|
|
}
|
|
}
|
|
|
|
/// Eine Fassung, wie Jellyfin sie meldet.
|
|
public struct JellyfinMediaSource: Decodable, Sendable {
|
|
public let id: String?
|
|
public let container: String?
|
|
public let size: Int64?
|
|
public let bitrate: Int?
|
|
public let runTimeTicks: Int64?
|
|
public let mediaStreams: [JellyfinMediaStream]?
|
|
public let supportsDirectPlay: Bool?
|
|
public let supportsDirectStream: Bool?
|
|
public let supportsTranscoding: Bool?
|
|
public let transcodingURL: String?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id = "Id", container = "Container", size = "Size", bitrate = "Bitrate"
|
|
case runTimeTicks = "RunTimeTicks", mediaStreams = "MediaStreams"
|
|
case supportsDirectPlay = "SupportsDirectPlay"
|
|
case supportsDirectStream = "SupportsDirectStream"
|
|
case supportsTranscoding = "SupportsTranscoding"
|
|
case transcodingURL = "TranscodingUrl"
|
|
}
|
|
}
|
|
|
|
/// Eine Spur, wie Jellyfin sie meldet.
|
|
public struct JellyfinMediaStream: Decodable, Sendable {
|
|
public let index: Int?
|
|
public let type: String?
|
|
public let codec: String?
|
|
public let language: String?
|
|
public let displayTitle: String?
|
|
public let channels: Int?
|
|
public let isDefault: Bool?
|
|
public let isForced: Bool?
|
|
public let height: Int?
|
|
public let width: Int?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case index = "Index", type = "Type", codec = "Codec", language = "Language"
|
|
case displayTitle = "DisplayTitle", channels = "Channels"
|
|
case isDefault = "IsDefault", isForced = "IsForced"
|
|
case height = "Height", width = "Width"
|
|
}
|
|
}
|
|
|
|
/// Die Hülle, in der Jellyfin Listen schickt.
|
|
public struct JellyfinItemPage: Decodable, Sendable {
|
|
public let items: [JellyfinItem]
|
|
public let totalRecordCount: Int?
|
|
public let startIndex: Int?
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case items = "Items"
|
|
case totalRecordCount = "TotalRecordCount"
|
|
case startIndex = "StartIndex"
|
|
}
|
|
}
|
|
|
|
extension Int64 {
|
|
/// Ticks zu hundert Nanosekunden in Millisekunden.
|
|
///
|
|
/// Jellyfins Einheit. Zehntausend Ticks sind eine Millisekunde — eine
|
|
/// Zahl, die man beim Lesen zweimal prüft und deshalb genau einmal
|
|
/// aufschreibt.
|
|
var millisecondsFromTicks: Int { Int(self / 10_000) }
|
|
}
|
|
|
|
extension Int {
|
|
var ticksFromMilliseconds: Int64 { Int64(self) * 10_000 }
|
|
}
|