Vollständig entfernt: das Paket `PlexKit` samt 108 Tests, der
Web-Authenticator, die Anmeldung über plex.tv, die Serverauswahl und alles,
was daran hing. 43 Dateien gelöscht, 68 Stellen in 28 weiteren bereinigt.
**Zwei Dinge hätten beim blossen Streichen still das Falsche getan.**
`playbackUnavailableMessage` deckte im Else-Zweig zwei Lagen gleichzeitig ab
— „es ist Plex" und „niemand ist angemeldet". Wer die Serverart naiv
herausstreicht, bekommt als nicht angemeldeter Nutzer „Jellyfin-Wiedergabe
steht aus" statt „Server nicht erreichbar". Die Unterscheidung hängt jetzt
daran, ob überhaupt ein Server da ist.
Die Schlüsselbund-Einträge `plex.account` und `plex.server` wurden allein
über den Authenticator aus dem gelöschten Paket entfernt. Danach stünde
`plex.account` in keiner Zeile dieses Projekts mehr: wer sich nie abgemeldet
hat, trüge ein gültiges Token am fremden Konto bis in alle Ewigkeit mit sich,
ohne dass irgendein Weg es noch löschen könnte. `purgePlexLeftovers()` räumt
beide Token und fünf Voreinstellungen einmalig ab — und fasst nichts an, wo
nichts liegt, damit kein Jellyfin-Nutzer dafür seinen Mediathekenspeicher
verliert.
**Was Nutzer sehen.** Die Berechtigungsfrage fürs lokale Netz sagte wörtlich,
die App brauche es, „um deinen Plex-Server zu finden" — das stand so im
Systemdialog. Der Anmeldebildschirm hatte zwei Knöpfe, und der hervorgehobene
war der für Plex; der verbliebene erbt die Hervorhebung, denn ein Knopf, der
aussieht wie die zweite Wahl, lässt nach der ersten suchen. 14 verwaiste
Textbausteine sind weg, vier umformuliert.
Darunter einer, der schlicht nicht mehr stimmte: `signIn.privacy` versprach,
das Passwort erreiche diese App nie. Beim Jellyfin-Rückfall auf das Passwort
tut es das sehr wohl. Die Zusage, die weiterhin gilt, ist die über fremde
Dienste — und nur die steht jetzt da.
**Was ausdrücklich bleibt:** `MediaBackend`. Diese Abstraktion hat zwei
Server gleichzeitig getragen, und der Steckplatz, den Plex räumt, ist genau
der, auf den Halo kommt. `ServerKind` fällt dagegen weg — mit einem Fall ist
es ein Bool, das `backend != nil` schon beantwortet.
Nebenbei zog die README nach. Sie beschrieb noch einen „nativen Plex-Client"
mit zwei Playern und VLCKit, beides seit Längerem nicht mehr wahr.
368 Tests grün (von 476 minus die 108 aus PlexKitTests, kein anderer).
iOS und tvOS bauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Jeder Aufruf einer Mediathek war ein Serverabruf mit Ladekreis davor. Die
Plakate lagen längst auf der Platte; was fehlte, war die Liste, die sagt,
welche davon hierhergehören.
Jetzt liegt die erste Seite als Abzug daneben: beim Aufrufen steht sie
sofort da, der Server wird still nachgefragt, und man sieht in aller Regel
nichts davon. Antwortet er nicht, bleibt der Abzug stehen — eine Liste von
gestern ist im Zug brauchbarer als ein Fehlerbildschirm.
Bewusst nur die erste Seite. Stünden 240 gespeicherte Titel da und der
Server antwortet mit 60, müsste entweder die Liste vor den Augen schrumpfen
oder der Versatz fürs Nachladen wäre falsch.
Gespeichert wird ausserdem nur, was eine Kachel zeigt. Ein Titel aus einer
Listenabfrage bringt jede Spur jeder Datei mit — im Raster steht davon
nichts, und ausgepackt werden müsste es bei jedem Aufrufen, also genau da,
wo der Abzug Zeit sparen soll.
Die Mediathekenliste selbst wird nur als Rettungsanker gemerkt, nicht als
Beschleuniger: sie sofort zu zeigen hiesse, eine Sitzung für fertig zu
erklären, bevor feststeht, dass der Token noch gilt.
Abmelden räumt alles weg. Titel, Zusammenfassungen und Sehfortschritt sind
persönlich.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Beinahe-Regression, gefunden bevor sie jemanden traf: `isLocalConnection`
kam aus `session.server?.connection.local`, und `server` ist ein
`PlexServer` — bei Jellyfin immer `nil`. Jede Jellyfin-Wiedergabe galt
damit als entfernt, auch zuhause am Kabel. Der neue Deckel hätte dort ein
67-Mbit-Remux ohne Not auf acht Megabit heruntergerechnet.
Jetzt wird gefragt statt geraten: `/System/Endpoint` beantwortet es mit
**demselben** Aufruf, mit dem Jellyfin auch selbst entscheidet, ob es eine
Bitratenleiter ausliefert. Ein Blick auf die Adresse hätte nicht genügt —
wer über einen Reverse Proxy geht, erreicht seinen Server auch von der
Couch aus unter einem öffentlichen Namen, und über ein VPN kommt eine
private Adresse von weit her.
Gefragt wird bei beiden Wegen, beim Anmelden und beim Wiederherstellen:
dasselbe Konto kann heute zuhause und morgen im Zug aufwachen.
Im Zweifel `false` — lieber einmal zu vorsichtig gedeckelt als eine
Wiedergabe, die über eine Fernverbindung gar nicht erst anläuft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Keine Datei unter Views/LiveTV importiert noch PlexKit. Sender, Programm
und Einschalten laufen über den neutralen Vertrag, und supportsLiveTV
fragt endlich die Fähigkeit ab statt "server != nil" — womit die Zusage
in JellyfinBackend.capabilities zum ersten Mal stimmt.
Der Player holt die laufende Sendung jetzt für genau einen Sender nach.
Vorher ging dafür das Raster über alle Sender hinaus, weil der Plex-Weg
keinen schmaleren kannte; im neutralen Vertrag ist die Senderliste ein
Parameter.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der gespiegelte Ausschnitt hinter der Seitenleiste war nicht das Problem —
JellyTV macht es genauso. Auffällig wurde er erst durch die Bewegung: beim
seitlichen Durchblättern wanderten das echte und das gespiegelte Stück
gegeneinander, und die Naht dazwischen war das auffälligste Ding auf dem
Bildschirm.
Jetzt rutscht das neue Bild 32 Punkte herein und blendet dabei über das alte.
Nichts wandert weit genug, als dass ein Versatz auffiele. Wischen blättert
weiter, nach vorn wie zurück; das nächste Bild wird geholt, solange das
aktuelle noch steht, sonst blendet der Aufmacher auf eine graue Fläche über.
Dazu ein Testziel, das die App mit der Attrappe in einen Zustand fährt und
fotografiert. Der Grund ist ein wiederkehrender Fehler: Aussagen über das
Aussehen waren Vermutungen, weil niemand hinsehen konnte — der Simulator kennt
keinen Befehl fürs Antippen, und dieses Xcode bringt kein Simulator-Fenster
mehr mit. Damit ist „sieht es so aus?" eine Frage, die sich beantworten lässt.
Die Startargument-Anmeldung an der Attrappe steht unter #if DEBUG und führt am
Schlüsselbund vorbei: kein echter Server, kein echtes Konto, kein echter Token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Bis hierher konnte man sich bei Jellyfin anmelden und stöbern, aber nichts
abspielen. Der Player war die letzte große Stelle, die Plex unmittelbar
kannte.
Drei Mitglieder kommen dafür in den Vertrag: eine Wiedergabe vorbereiten,
sie beenden, den Fortschritt melden. Die Entscheidung **ob** umgerechnet
wird, fällt weiterhin vorher im `PlaybackDecider` — der Server führt sie nur
aus. Genau diese Trennung fehlte: der Player hat Plex' Transcoder
angesprochen und konnte deshalb mit nichts anderem reden.
`MediaBackendError` bekommt einen siebten Fall: `refused(reason:)`. Der
Server hat verstanden und sagt nein, in eigenen Worten. Bei Plex war genau
dieser Satz die einzige brauchbare Spur, und er ging tagelang verloren, weil
es dafür keinen Fall gab.
Bei Jellyfin kommt die Adresse einer umgerechneten Fassung vom Server selbst
(`PlaybackInfo` → `TranscodingUrl`), nicht von uns. Er weiß, welche Spuren er
kopieren kann und welche er neu rechnen muss.
Drei Dinge, die beim Hinsehen auffielen:
- **`startTimeTicks` gehört nicht in die Anfrage.** Sonst entscheiden zwei
Stellen über den Startpunkt: der Server ließe den Strom dort beginnen, und
der Player spränge noch einmal um dieselbe Strecke vor. Bei einundzwanzig
Minuten Fortschritt landet man bei zweiundvierzig.
- **Der erste Bericht braucht einen eigenen Endpunkt.** Ohne ihn erscheint
der Titel bei Jellyfin nirgends als „läuft gerade". Er fiel zunächst aus,
weil er die Laufzeit verlangte — und die steht erst Sekunden nach dem
Öffnen fest. Jetzt tut es für die erste Meldung die des Servers.
- **In der Download-Adresse standen Titel und Fassung vertauscht.** Bei einem
Film mit einer Fassung sind beide Kennungen dieselbe und nichts fällt auf;
bei zweien schon. `MediaFile` sagt jetzt, zu welchem Titel sie gehört.
Live-TV bleibt bei Plex und wird als Einziges noch unmittelbar durchgereicht.
Ein Tuner ist ein Stück Hardware, und die fünfhundert Zeilen, die es gekostet
hat, ihn zu bändigen, gehören in keinen gemeinsamen Vertrag.
Nachgesehen, nicht nur gebaut: gegen die Attrappe läuft ein echtes Video, der
Beginn wird gemeldet, der Fortschritt kommt in Ticks mit Sitzung und Fassung
an. Was die Attrappe **nicht** beantworten kann, ist, ob ein echter Jellyfin
das Geräteprofil annimmt und eine Umrechnung anbietet — das entscheidet erst
ein richtiger Server.
257 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Anmeldebildschirm kannte nur Plex. JellyfinKit war fertig, aber von der
App aus nicht erreichbar — „ich sehe nix von Jellyfin".
Quick Connect steht vorn: der Server nennt einen sechsstelligen Code, den man
in einem bereits angemeldeten Client bestätigt. Das Passwort erreicht Flixy
dabei nie, dieselbe Zusage wie bei Plex. Benutzername und Passwort sind der
Rückfall für Server, auf denen Quick Connect abgeschaltet ist; das Passwort
wird dann für die Dauer einer Anfrage gehalten und danach weggeworfen.
Die Adresse wird nicht erraten, sondern durchprobiert. „192.168.1.50" kann
alles Mögliche heißen, und die Reihenfolge ist der Kern von JellyfinAddress:
eine IP-Adresse im eigenen Netz spricht fast nie HTTPS, ein Domänenname fast
immer. Andersherum wartet man bei jedem Server ohne Zertifikat erst auf eine
Zeitüberschreitung.
Die Sitzung hält von hier an einen Server hinter dem neutralen Vertrag, nicht
mehr einen Plex-Server. Die Ansichten haben das nicht gemerkt — genau dafür
war MediaKit da. Dabei fällt ab, was Jellyfin kann und Plex nicht: Favoriten
stehen im Aktionsmenü, sobald der Server sie kennt, Sterne nur dort, wo es
sie gibt.
Was noch nicht geht und deshalb auch nicht behauptet wird: Wiedergabe und
Live-TV sprechen weiter unmittelbar mit Plex. Bei Jellyfin verschwindet der
Live-TV-Eintrag aus der Seitenleiste, und der Player sagt, dass die
Wiedergabe der nächste Schritt ist, statt „Server nicht erreichbar" zu
melden — er ist ja erreichbar.
Nebenbei zwei Stellen, die der Übersetzer aus Plex hinterlassen hat:
`catch is CancellationError` griff seit den getippten Fehlern nie mehr, weil
eine abgebrochene Netzanfrage als Übertragungsfehler zurückkommt; und das
Ersatz-Hintergrundbild einer Folge war toter Code, weil die linke Seite des
`??` gar nicht optional war.
TokenStore wandert nach MediaKit — Tokens sind nichts Plex-Eigenes. Die
Konten sind nach Server getrennt, damit ein späteres Umschalten nicht jedes
Mal eine neue Anmeldung kostet; „Abmelden" räumt trotzdem beide ab.
248 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Start, Suche, Mediathek, Detailseiten und Downloads arbeiten jetzt mit
`MediaItem` und dem `MediaBackend`-Vertrag. Damit sind sie fertig für
einen zweiten Server, ohne dass an ihnen noch etwas zu tun wäre.
Dabei sind ein paar Namen ehrlicher geworden: aus `grandparentTitle`
wurde `seriesTitle`, aus `viewedLeafCount` `playedEpisodeCount`. Und
`TagStrip` nahm bisher Plex-Marken mit Kennung und Rolle entgegen, um
davon nur den Namen anzuzeigen — Genres sind jetzt schlicht Zeichenketten.
`Arrangement` ist aus PlexKit heraus und generisch: dieselbe Rechnung
gilt für jede Liste, die jemand selbst zusammenstellt. Phase 3 baut die
konfigurierbare Seitenleiste darauf, statt sie ein zweites Mal — und ein
zweites Mal falsch — zu schreiben.
Die Fehlerübersetzung gibt es nur noch einmal. Es waren zwei, und sie
waren bereits auseinandergelaufen: derselbe Fehler bekam je nach
Bildschirm eine andere Erklärung.
Das Nachladen der Mediathek bleibt unangetastet: eine lange Liste, die
fünf Einträge vor dem Ende nachlädt. `MediaPage` heißt zwar Seite, ist
aber eine technische Portion — Blättern mit „weiter zu Seite 2" gehört
nicht in eine Mediathek, in der man stöbert. Der Kommentar dort sagt das
jetzt ausdrücklich, damit es niemand später „aufräumt".
Player, Live-TV und die Download-Engine sprechen noch unmittelbar mit
Plex. Sie folgen als Nächstes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
iPad meldete sich nie an
Die Ursache ist unangenehm speziell: Wer im Browser schon bei Plex
angemeldet ist, wird von app.plex.tv/auth sofort auf watch.plex.tv
weitergeschickt — OHNE dass der Code verknüpft wird. Man sieht sich
angemeldet, die App wartet vergeblich, und es gibt keinerlei Hinweis.
Jetzt wird plex.tv/link mit eingetragenem Code geöffnet. Diese Seite
tut nur eine Sache: den Code bestätigen. Die Gerätekennung steckt
ohnehin schon im PIN.
Dazu ein Ausweg: bleibt es nach fünfzehn Sekunden ohne Token, erscheint
der Code im Anmeldebildschirm zum Abtippen. Schließt man das Fenster
ohne Erfolg, bleibt er stehen statt abzumelden — er gilt weiter.
Download fragte nicht nach der Qualität
Jetzt Original, Full HD, HD oder Sparsam, jeweils mit geschätzter
Größe. Die Größe ist das eigentliche Entscheidungskriterium — „Full HD"
allein sagt nicht, ob es aufs Telefon passt.
Die komprimierten Stufen laufen über den Universal-Transcoder und sind
weiterhin NICHT am Server geprüft; steht so im Quelltext.
Schwarzes Bild ohne Anzeige
Der Player kannte nur „spielt" und „spielt nicht". Wartete er auf
Daten, stand man vor einem Standbild ohne Hinweis. Es gibt jetzt einen
eigenen Pufferzustand mit Anzeige.
Aufmacher war beschnitten
Er stand auf 3:4, Plex liefert als `art` aber 16:9 — mit scaledToFill
ging links und rechts je ein Viertel verloren, ohne dass man es dem
Bild ansah. Die Seitenverhältnisse liegen jetzt an einer Stelle in
FlixyAspectRatio.
Schwarze Leiste über dem Aufmacher
.ultraThinMaterial statt undurchsichtigem Grund: das Bild läuft
unscharf hinter der Leiste weiter, statt an einer harten Kante zu
enden. Weichgezeichnet, nicht abgedunkelt.
139 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Der Crashlog vom Gerät:
_dispatch_assert_queue_fail
_swift_task_checkIsolatedSwift
MainActor.assumeIsolated
VLCPlayerEngine.mediaPlayerStateChanged(_:)
VLCKit input_thread_Events
Im Code stand als Begründung für assumeIsolated, VLCKit stelle seine
Rückrufe über die Hauptwarteschlange zu. Das ist falsch — es ruft den
Delegaten von seinem eigenen Input-Thread. Die App brach deshalb hart
ab, sobald VLC den ersten Zustandswechsel meldete, also unmittelbar
beim Drücken auf Play.
Alle drei Delegat-Methoden springen jetzt mit Task { @MainActor in }
sauber hinüber. Die Reihenfolge bleibt erhalten: die Aufgaben werden aus
demselben Thread nacheinander eingereiht.
Dieselbe Annahme steckte im AVPlayer-Motor in den KVO-Beobachtern —
KVO meldet auf dem Thread, der die Änderung ausgelöst hat, und AVPlayer
ändert seinen Zustand auch aus internen Threads. Ebenfalls umgestellt.
Die beiden Rückrufe mit ausdrücklichem queue: .main behalten
assumeIsolated: dort ist die Annahme belegt statt geraten, und der
Zeitbeobachter feuert viermal je Sekunde.
Dazu die Anmeldung: Plex führt den Rücksprung über forwardUrl nicht
zuverlässig aus. Auf dem iPad landet der Browser nach dem Anmelden auf
watch.plex.tv und bleibt stehen — der Token liegt bereit, aber die App
wartete auf einen Rückruf, der nie kam. Das Fenster wird jetzt geöffnet,
ohne darauf zu warten; parallel wird im Sekundentakt beim Server
nachgefragt. Der Rücksprung ist damit eine Annehmlichkeit, keine
Voraussetzung.
132 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kein Video lief jemals an
AVPlayerEngine.load() wartete auf asset.load(.duration) und die
Spurenliste, BEVOR es zurückkehrte — und erst danach rief das Modell
play(). AVFoundation muss für die Dauer aber so viel von der Datei
holen, bis es den Index findet, und der sitzt bei MP4 wie MKV oft am
Ende. Über eine langsame Leitung sind das Minuten, in denen nur ein
Ladekreis zu sehen war, obwohl der Player längst hätte anfangen können.
Jetzt kehrt load() zurück, sobald das Item hängt; Dauer und Spuren
kommen nach und melden sich über den Ereignisstrom.
Dazu AVURLAssetPreferPreciseDurationAndTimingKey: false — sonst
durchsucht AVFoundation bei manchen Formaten die ganze Datei.
Zweiter Fund an derselben Stelle: VLCMedia(url:) ist nullable. Ging es
schief, blieb media still nil, play() tat nichts und es gab keinen
Fehler — nur einen Ladekreis ohne Ende. Wird jetzt gemeldet.
Kein Querformat im Player
requestGeometryUpdate allein dreht nicht: das System dreht sofort
zurück, solange die erlaubten Ausrichtungen es nicht hergeben. Die
Erlaubnis kommt nur über den App-Delegaten, deshalb OrientationLock
als Brücke. Player dreht ins Querformat, beim Verlassen zurück. iPad
bleibt frei — dort ist Querformat beim Blättern der Normalfall.
Uhrzeit über dem Film
Die Statusleiste war nur ausgeblendet, solange die Bedienelemente weg
waren. Jetzt immer.
Sehr langer Start
Zwei Ursachen. Erstens legte der Initialisierer der App die
Hintergrund-URLSession an — die spricht über XPC mit nsurlsessiond und
verzögerte den allerersten Bildaufbau. Jetzt erst bei Bedarf, und wer
keine Downloads hat, braucht sie nie. Zweitens fragte jeder Start erst
plex.tv nach der Serverliste und suchte die Verbindung neu. Die zuletzt
funktionierende wird jetzt gemerkt (Server-Token im Schlüsselbund) und
mit kurzer Zeitgrenze zuerst probiert; die vollständige Suche greift
nur noch, wenn sie nicht mehr antwortet — etwa wenn man von zuhause
weggeht.
Dazu ein Hinweis im Player, wenn die Verbindung über die gedrosselte
Plex-Vermittlung läuft. Sonst sucht man den Fehler in der App.
132 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kein Code mehr zum Abtippen. Die Anmeldemaske kommt von Plex und öffnet
sich über ASWebAuthenticationSession in einem Fenster innerhalb der App;
nach dem Anmelden schickt Plex über forwardUrl auf ein eigenes Schema
zurück, das Fenster schließt sich von selbst.
Der PIN steckt weiterhin darunter — er ist der einzige Weg, auf dem Plex
einen Token herausgibt. Sichtbar ist er nicht mehr.
Was das bringt:
- Kein Fernseher-Gefühl mehr.
- Zwei-Faktor läuft mit, weil Plex die Seite selbst ausliefert.
- Das Passwort erreicht Flixy nie. Steht auch so unter dem Knopf.
- prefersEphemeralWebBrowserSession = false: wer in Safari schon bei
Plex angemeldet ist, braucht nur zu bestätigen.
Zwei Details, die sonst still scheitern:
- Die Rückadresse wird strenger kodiert als die übrigen Werte. `:` und
`/` lässt urlQueryAllowed durch, und für Plex endete der Wert dann
beim ersten Schrägstrich — der Rücksprung käme nie.
- Das Schema muss in CFBundleURLTypes stehen, sonst kennt iOS es nicht
und das Fenster bliebe offen stehen.
Als Schema die Bundle-Kennung statt eines kurzen Worts wie „flixy":
ein kurzes könnte eine andere App beanspruchen und die Anmeldung
abfangen.
132 Tests.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Die Datenschicht:
- plex.tv-Anmeldung über das PIN-Verfahren, Token im Schlüsselbund
- Serversuche und Verbindungsauswahl nach Rang (lokal vor direkt vor
Vermittlung), mit Abgleich der Server-Kennung gegen veraltete
plex.direct-Adressen
- PlexServer als einzige Stelle, an der die App Endpunkte kennt:
Mediatheken, Hubs, seitenweises Blättern mit Filtern, Metadaten,
Staffeln/Folgen, Suche
Die Oberfläche:
- Startseite mit Aufmacher und den Reihen vom Server
- Mediathek als Raster oder Liste, mit Filter- und Sortierblatt
- Detailseiten für Filme, Folgen, Serien und Staffeln inkl. Besetzung,
Fassungsauswahl und aufklappbaren technischen Angaben
- Suche mit gemerkten Anfragen
- Vollbildplayer mit eigenem Chrome
Der Player:
- AVPlayerEngine und VLCPlayerEngine hinter einem gemeinsamen Protokoll
- VLCKit liegt in einem eigenen Package (FlixyVLC), damit `swift test`
in FlixyKit nicht das 920-MB-Binärpaket mitzieht
Am echten plex.tv geprüft: der Anmeldecode kommt an. Dabei fiel auf,
dass `strong=true` einen dreißigstelligen Code liefert, der sich bei
plex.tv/link nicht eintippen lässt — jetzt der kurze.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>