Files
Flaro/Packages/FlixyKit/Sources/PlexKit/Models/PlexMedia.swift
T
ScarriffleandClaude Opus 5 f47c6385e3 Auflösungen heissen jetzt, wie Menschen sie nennen
Auf der Detailseite stand „1080" — die Zeichenkette des Servers in
Grossbuchstaben. Weder eine Zahl mit Bedeutung noch ein Name, den jemand
benutzt. Jetzt steht dort „Full HD", „4K", „HD", „SD".

Die Einstufung geht neu über die **Breite**, nicht über die Höhe. Ein
Kinofilm in 2,39:1 misst 1920 × 804; nach der Höhe war das bisher „720p",
und zwar ausgerechnet bei den grossen Filmen. Die Höhe bleibt als Notnagel
für Server, die keine Breite melden, und der Name des Servers als letzter.

Nebenbei fällt damit der 4K-Filter bei Jellyfin nicht mehr durch, wenn ein
breitwandiges 4K nur 1608 Zeilen hat, und die doppelte Einstufung in
PlexMedia verschwindet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-28 06:33:26 +02:00

166 lines
6.6 KiB
Swift

import Foundation
import MediaKit
/// Eine Datei auf der Platte des Servers.
///
/// Plex schachtelt hier zweifach: ein Titel hat mehrere `Media` (dieselbe
/// Fassung in 4K und 1080p), und jedes `Media` hat `Part` (ein Film, der auf
/// zwei Dateien verteilt ist). Für die Wiedergabe zählt der `Part` — er trägt
/// den Pfad.
public struct PlexPart: Sendable, Equatable, Codable, Identifiable {
public let id: Int
/// Der Pfad, unter dem die Datei abrufbar ist.
public let key: String
public let container: String?
public let size: Int64?
public let duration: Int?
public let file: String?
public let streams: [PlexStream]
private enum CodingKeys: String, CodingKey {
case id, key, container, size, duration, file
// Plex schreibt verschachtelte Sammlungen groß.
case streams = "Stream"
}
public init(
id: Int,
key: String,
container: String? = nil,
size: Int64? = nil,
duration: Int? = nil,
file: String? = nil,
streams: [PlexStream] = []
) {
self.id = id
self.key = key
self.container = container
self.size = size
self.duration = duration
self.file = file
self.streams = streams
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.key = try container.decode(String.self, forKey: .key)
self.container = try container.decodeIfPresent(String.self, forKey: .container)
self.size = try container.decodeIfPresent(Int64.self, forKey: .size)
self.duration = try container.decodeIfPresent(Int.self, forKey: .duration)
self.file = try container.decodeIfPresent(String.self, forKey: .file)
// Fehlt die Spurenliste, ist die Datei noch nicht analysiert. Das ist
// kein Fehler — es heißt nur, dass wir nicht über DirectPlay
// entscheiden können und im Zweifel transcodieren lassen.
self.streams = try container.decodeIfPresent([PlexStream].self, forKey: .streams) ?? []
}
public var videoStream: PlexStream? { streams.first { $0.kind == .video } }
public var audioStreams: [PlexStream] { streams.filter { $0.kind == .audio } }
public var subtitleStreams: [PlexStream] { streams.filter { $0.kind == .subtitle } }
/// Die Tonspur, die der Server als aktiv meldet — sonst die erste.
public var selectedAudioStream: PlexStream? {
audioStreams.first { $0.isSelected } ?? audioStreams.first
}
}
/// Eine Fassung eines Titels.
public struct PlexMedia: Sendable, Equatable, Codable, Identifiable {
public let id: Int
public let container: String?
public let videoCodec: String?
public let audioCodec: String?
/// Plex meldet hier Werte wie `"1080"`, `"4k"` oder `"sd"` — keine Zahl.
public let videoResolution: String?
public let videoProfile: String?
/// In Kilobit pro Sekunde.
public let bitrate: Int?
public let width: Int?
public let height: Int?
public let audioChannels: Int?
public let duration: Int?
public let parts: [PlexPart]
private enum CodingKeys: String, CodingKey {
case id, container, videoCodec, audioCodec, videoResolution, videoProfile
case bitrate, width, height, audioChannels, duration
case parts = "Part"
}
public init(
id: Int,
container: String? = nil,
videoCodec: String? = nil,
audioCodec: String? = nil,
videoResolution: String? = nil,
videoProfile: String? = nil,
bitrate: Int? = nil,
width: Int? = nil,
height: Int? = nil,
audioChannels: Int? = nil,
duration: Int? = nil,
parts: [PlexPart] = []
) {
self.id = id
self.container = container
self.videoCodec = videoCodec
self.audioCodec = audioCodec
self.videoResolution = videoResolution
self.videoProfile = videoProfile
self.bitrate = bitrate
self.width = width
self.height = height
self.audioChannels = audioChannels
self.duration = duration
self.parts = parts
}
public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.container = try container.decodeIfPresent(String.self, forKey: .container)
self.videoCodec = try container.decodeIfPresent(String.self, forKey: .videoCodec)
self.audioCodec = try container.decodeIfPresent(String.self, forKey: .audioCodec)
self.videoResolution = try container.decodeIfPresent(String.self, forKey: .videoResolution)
self.videoProfile = try container.decodeIfPresent(String.self, forKey: .videoProfile)
self.bitrate = try container.decodeIfPresent(Int.self, forKey: .bitrate)
self.width = try container.decodeIfPresent(Int.self, forKey: .width)
self.height = try container.decodeIfPresent(Int.self, forKey: .height)
self.audioChannels = try container.decodeIfPresent(Int.self, forKey: .audioChannels)
self.duration = try container.decodeIfPresent(Int.self, forKey: .duration)
self.parts = try container.decodeIfPresent([PlexPart].self, forKey: .parts) ?? []
}
/// Die Bildhöhe in Bildpunkten.
///
/// Bevorzugt aus `height`. Fehlt die, wird `videoResolution` gedeutet —
/// dort steht bei Plex kein Zahlenwert, sondern „1080", „4k" oder „sd".
public var pixelHeight: Int? {
if let height, height > 0 { return height }
guard let videoResolution = videoResolution?.lowercased() else { return nil }
switch videoResolution {
case "4k": return 2160
case "sd": return 480
default: return Int(videoResolution)
}
}
/// Die Stufe, in der diese Fassung vorliegt.
public var resolution: VideoResolution? {
VideoResolution(width: width, height: height, serverLabel: videoResolution)
}
/// Die Auflösung in der Schreibweise, die Menschen erwarten.
public var resolutionLabel: String? { resolution?.name }
/// Eine kurze Beschreibung für die Versionsauswahl auf der Detailseite,
/// etwa „4K · HEVC · 38,2 Mbit/s".
public var qualityLabel: String {
var pieces: [String] = []
if let label = resolutionLabel { pieces.append(label) }
if let videoCodec { pieces.append(videoCodec.uppercased()) }
if let bitrate { pieces.append("\(bitrate / 1000) Mbit/s") }
return pieces.joined(separator: " · ")
}
}