Files
Flaro/Packages/FlixyKit/Sources/JellyfinKit/JellyfinItem.swift
T
ScarriffleandClaude Opus 5 9540f26dd8 JellyfinKit: anmelden, stöbern, übersetzen
Der zweite Server. Eigenes Target neben PlexKit, nicht darin — die beiden
teilen die neutrale Schicht und sonst nichts.

Die Anmeldung geht zuerst über **Quick Connect**: die App zeigt einen
sechsstelligen Code, den man in einem bereits angemeldeten Client
bestätigt. Damit gilt hier dieselbe Zusage wie bei Plex — das Passwort
erreicht Flixy nie. Benutzername und Passwort bleiben als Rückfall, falls
der Server Quick Connect abgeschaltet hat; dann hält die App das Passwort
für die Dauer einer Anfrage und speichert es nirgends.

Zwei Stellen, an denen sich eine Übersetzung verrechnet, und die deshalb
Tests haben:

Jellyfin zählt Zeiten in Ticks zu hundert Nanosekunden. Eine Null zu viel,
und aus 44 Minuten werden siebeneinhalb Stunden.

Und es meldet die **ungesehenen** Folgen, nicht die gesehenen. Ungeprüft
übernommen stünde bei einer fast fertigen Serie „9 von 24" statt „15 von
24".

Dass die Adressen den Token tragen, steht hier von Anfang an — bei Plex
hat genau das tagelang gekostet, weil AVPlayer keine Kopfzeilen mitschickt.

Zwei Fähigkeiten sind ausdrücklich nicht da: Sterne-Bewertungen kennt
Jellyfin nicht, nur Daumen hoch und runter. Und die Adresse einer
verkleinerten Fassung gibt es erst nach einer Anfrage — sie zu erfinden
hieße, eine Adresse anzubieten, die nicht antwortet.

Die Startseite ist nachsichtig gebaut: fällt „Als Nächstes" aus, bleibt
deshalb nicht die ganze Seite leer.

Die App bietet Jellyfin noch nicht an — das ist der nächste Schritt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:59:02 +02:00

153 lines
5.6 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?
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"
}
}
/// 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 }
}