Files
Flaro/Packages/FlixyKit/Sources/PlexKit/Models/PlexItem+Media.swift
T
ScarriffleandClaude Opus 5 ba6be4ac79 MediaKit: die neutralen Datensätze und die Übersetzung aus Plex
`MediaItem` und die Typen darunter — die Form, in der die Oberfläche
Titel sieht, unabhängig davon, welcher Server sie geliefert hat.

Die Namen sind dort geblieben, wo sie ohnehin neutral waren: `title`,
`summary`, `year`, `index`. Umbenannt wurde, was Plex-Jargon ist.
`ratingKey` heißt `id`, `viewOffset` heißt `resumeMilliseconds`, und aus
dem „Großelternteil" ist die Serie geworden, die von Anfang an gemeint
war — ein Name, den man in jeder Ansicht im Kopf übersetzen musste.

Zwei Dinge, die dabei ehrlicher werden:

`isFavourite` gibt es, aber Plex meldet immer `false`. Ein Feld, das ein
Server nicht füllt, ist besser als eine Fähigkeit, die es nur auf einem
gibt und die sich nirgends setzen ließe.

Dateigrößen sind jetzt `Int64`. Ein Remux überschreitet vier Gigabyte
mühelos.

Die Übersetzung steht in **einer** Datei und geht in **eine** Richtung.
Die Drahtmodelle bleiben, wo sie sind — sie tragen Eigenheiten, die nur
Plex hat, und die gehören nicht nach oben.

Geprüft wird gegen echte Server-Antworten, nicht gegen erfundene Werte:
genau dort stehen die Eigenheiten, an denen eine Übersetzung scheitert.
Ein Test vergleicht außerdem beide Wege zum Wiedergabe-Profil — sagten
sie Unterschiedliches, spielte ein Titel nach dem Umbau plötzlich über
einen anderen Player.

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

187 lines
5.3 KiB
Swift

import Foundation
import MediaKit
/// Die Übersetzung der Plex-Drahtmodelle in die neutrale Form.
///
/// Eine Datei, eine Richtung. Danach ist unerheblich, von welchem Server ein
/// Titel stammt — und genau das war das Ziel: die Oberfläche soll nicht an
/// dreißig Stellen wissen müssen, wie Plex seine Felder nennt.
///
/// Die Drahtmodelle bleiben, wo sie sind. Sie tragen Eigenheiten, die nur
/// Plex hat (dynamische Schlüssel in der Hülle, `grandparent` für die Serie,
/// Sekunden seit 1970 als Zahl), und die gehören nicht nach oben.
extension PlexItem {
public var asMediaItem: MediaItem {
MediaItem(
id: ratingKey,
path: key,
kind: kind.map(Self.kind) ?? nil,
title: title,
sortTitle: titleSort,
summary: summary,
year: year,
duration: duration,
contentRating: contentRating,
rating: rating,
audienceRating: audienceRating,
thumb: thumb,
art: art,
parentThumb: parentThumb,
seriesThumb: grandparentThumb,
seriesArt: grandparentArt,
index: index,
parentIndex: parentIndex,
parentTitle: parentTitle,
parentID: parentRatingKey,
seriesTitle: grandparentTitle,
seriesID: grandparentRatingKey,
playCount: viewCount,
resumeMilliseconds: viewOffset,
lastPlayedAt: lastViewedAt.map { Date(timeIntervalSince1970: TimeInterval($0)) },
addedAt: addedAt.map { Date(timeIntervalSince1970: TimeInterval($0)) },
// Plex kennt keine Favoriten. Ehrlicher als eine erfundene
// Markierung, die sich nirgends setzen ließe.
isFavourite: false,
episodeCount: leafCount,
playedEpisodeCount: viewedLeafCount,
childCount: childCount,
versions: media.map(\.asMediaVersion),
genres: genres.map(\.tag),
directors: directors.map(\.tag),
writers: writers.map(\.tag),
cast: roles.map(\.asMediaPerson),
markers: markers.map(\.asMediaMarker),
artwork: images.map(\.asMediaArtwork)
)
}
private static func kind(_ plex: PlexItem.Kind) -> MediaItem.Kind {
switch plex {
case .movie: .movie
case .show: .show
case .season: .season
case .episode: .episode
case .clip: .clip
case .collection: .collection
}
}
}
extension PlexMedia {
public var asMediaVersion: MediaVersion {
MediaVersion(
id: String(id),
container: container,
videoCodec: videoCodec,
audioCodec: audioCodec,
videoResolution: videoResolution,
bitrate: bitrate,
width: width,
height: height,
audioChannels: audioChannels,
duration: duration,
files: parts.map(\.asMediaFile)
)
}
}
extension PlexPart {
public var asMediaFile: MediaFile {
MediaFile(
id: String(id),
key: key,
container: container,
size: size,
duration: duration,
file: file,
streams: streams.compactMap(\.asMediaStream)
)
}
}
extension PlexStream {
/// `nil`, wenn Plex eine Spurart meldet, mit der die App nichts anfängt.
public var asMediaStream: MediaStream? {
let mapped: MediaStream.Kind
switch kind {
case .video: mapped = .video
case .audio: mapped = .audio
case .subtitle: mapped = .subtitle
case nil: return nil
}
return MediaStream(
id: String(id),
kind: mapped,
codec: codec,
language: language,
languageCode: languageCode,
displayTitle: displayTitle ?? title,
channels: channels,
isSelected: isSelected,
isForced: isForced
)
}
}
extension PlexTag {
public var asMediaPerson: MediaPerson {
MediaPerson(id: id, name: tag, role: role, thumb: thumb)
}
}
extension PlexMarker {
public var asMediaMarker: MediaMarker {
MediaMarker(
kind: MediaMarker.Kind(rawValue: type) ?? .unknown,
startMilliseconds: startTimeOffset,
endMilliseconds: endTimeOffset
)
}
}
extension PlexImage {
public var asMediaArtwork: MediaArtwork {
MediaArtwork(
kind: MediaArtwork.Kind(rawValue: type) ?? .unknown,
path: url
)
}
}
extension PlexLibrary {
public var asMediaLibrary: MediaLibrary {
MediaLibrary(
id: key,
title: title,
kind: mediaKind,
thumb: thumb,
art: art
)
}
}
extension PlexLibrary {
private var mediaKind: MediaLibrary.Kind {
switch kind {
case .movie: .movie
case .show: .show
case .artist: .music
case .photo: .photo
case .livetv: .liveTV
case nil: .other
}
}
}
extension PlexHub {
public var asMediaRowGroup: MediaRowGroup {
MediaRowGroup(
id: hubIdentifier,
title: title,
moreKey: more == true ? key : nil,
items: items.map(\.asMediaItem)
)
}
}