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>
This commit is contained in:
Scarriffle
2026-09-13 12:33:55 +02:00
co-authored by Claude Opus 5
parent 5b2b86b66b
commit ba6be4ac79
6 changed files with 779 additions and 0 deletions
@@ -0,0 +1,79 @@
import Foundation
import Testing
import MediaKit
@testable import PlexKit
/// Die Übersetzung der Plex-Modelle in die neutrale Form.
///
/// Gegen eine echte Server-Antwort, nicht gegen erfundene Werte: die
/// Eigenheiten, an denen eine Übersetzung scheitert, stehen genau dort —
/// „grandparent" für die Serie, Sekunden seit 1970 als Zahl, Fassungen und
/// Dateien ineinander verschachtelt.
@Suite("Übersetzung nach MediaKit")
struct MediaTranslationTests {
private func item(_ fixture: String) throws -> PlexItem {
let url = try #require(Bundle.module.url(forResource: "Fixtures/\(fixture)", withExtension: "json"))
let container = try PlexResponse.decode(
PlexMediaContainer<PlexItem>.self,
from: try Data(contentsOf: url)
)
return try #require(container.content.first)
}
@Test("Aus dem Großelternteil wird die Serie")
func translatesTheGrandparent() throws {
let episode = try item("episodes-unanalyzed").asMediaItem
// Der Name war von Anfang an irreführend: gemeint ist die Serie, und
// in jeder Ansicht musste man ihn im Kopf übersetzen.
#expect(episode.kind == .episode)
#expect(episode.title == "Die erste Folge")
#expect(episode.seriesTitle == "Eine Serie")
#expect(episode.index == 1)
#expect(episode.parentIndex == 1)
}
@Test("Fassungen und Dateien kommen vollständig mit")
func translatesVersions() throws {
let movie = try item("movie-with-two-versions").asMediaItem
#expect(movie.versions.count == 2)
#expect(movie.hasMultipleVersions)
let first = try #require(movie.versions.first)
#expect(first.files.isEmpty == false)
#expect(first.container != nil)
}
@Test("Die Wiedergabe-Entscheidung bekommt dasselbe Profil wie vorher")
func buildsTheSameProfile() throws {
// Beide Wege müssen dasselbe sagen — sonst spielte ein Titel nach dem
// Umbau plötzlich über einen anderen Player.
let plex = try item("movie-with-two-versions")
let media = try #require(plex.media.first)
let part = try #require(media.parts.first)
let viaPlex = media.profile(part: part)
let viaMedia = try #require(plex.asMediaItem.versions.first)
.profile(file: plex.asMediaItem.versions.first?.files.first)
#expect(viaPlex == viaMedia)
}
@Test("Ohne Fortschritt gibt es keinen Balken")
func leavesUnstartedItemsAlone() throws {
let movie = try item("movie-with-two-versions").asMediaItem
// `watchProgress` ist bewusst `nil` statt 0: ein Balken bei null
// Prozent sieht aus wie ein Fehler, nicht wie „noch nicht begonnen".
if movie.resumeMilliseconds == nil || movie.resumeMilliseconds == 0 {
#expect(movie.watchProgress == nil)
}
}
@Test("Plex kennt keine Favoriten und behauptet auch keine")
func doesNotInventFavourites() throws {
#expect(try item("movie-with-two-versions").asMediaItem.isFavourite == false)
}
}