Files
Flaro/Packages/FlixyKit/Sources/PlexKit/Models/PlexItem+Media.swift
T
ScarriffleandClaude Opus 5 a665d79e6d Antippen spielt ab — und vier Dinge, die daneben lagen
**Antippen startet, langes Drücken führt auf die Detailseite.** Am Telefon
und am iPad; am Mac bleibt der Klick die Seite, denn dort gibt es den
Abspielknopf beim Überfahren und langes Drücken ist keine übliche Geste.

Die Hürde war nicht die Geste, sondern das Schieben: ein `NavigationLink`
schiebt selbst, ein Menüeintrag kann es nicht. Der Stapel ist deshalb ein
eigener Typ mit Pfad (`MediaStack`) und reicht eine Schiebefunktion über
die Umgebung nach innen — denselben Weg nimmt schon die Meldung, dass sich
am Server etwas geändert hat.

**Zwei Knöpfe übereinander auf der Detailseite.** Das Monogramm hing als
Überzug an `mediaStack` — also über **jedem** Bildschirm dieses Stapels,
auch den geschobenen, und dort über deren Drei-Punkte-Menü. Es gilt jetzt
für die Startseite und endet mit ihr.

**Der Kopf der Detailseite war viel zu flach.** Dort stand kein
Seitenverhältnis, also galt die Vorgabe 16∶9 — während `HeroArtwork` auf
einem schmalen Gerät ein **Plakat** anfordert, also hochkant. Zu sehen war
ein Streifen aus der Mitte. Der Aufmacher auf der Startseite macht es seit
Langem richtig; die beiden Detailseiten nicht.

**Die Mediathekenleiste stand linksbündig in einer halbleeren Kapsel.**
Jetzt ist sie so breit wie ihr Inhalt und mittig. `ViewThatFits` nimmt die
ungescrollte Reihe, solange sie passt, sonst die scrollbare — damit steht
nirgends eine Zahl, ab wann es kippt.

**Und der Grund, warum die Mediathekenauswahl beim Nutzer nie griff:**
Plex. Dessen Hubs sagten nicht, aus welcher Mediathek sie stammen, also
warf der Filter jede Reihe weg und fiel auf „alles" zurück — ein Schalter,
der nichts tut. `PlexHub` liest jetzt `librarySectionID`, als Zahl oder
als Zeichenkette, je nachdem was der Server liefert. Vier Tests dazu.

Das ist **kein** Wiederaufnehmen der Plex-Arbeit: es repariert eine
Funktion, die dort gelogen hat. Neues kommt weiterhin nicht dazu.

iOS und tvOS bauen, 476 Tests grün.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-01 10:39:45 +02:00

188 lines
5.4 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,
libraryID: librarySectionID,
items: items.map(\.asMediaItem)
)
}
}