**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>
188 lines
5.4 KiB
Swift
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)
|
|
)
|
|
}
|
|
}
|