`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>
187 lines
5.3 KiB
Swift
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)
|
|
)
|
|
}
|
|
}
|