Commit Graph
7 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 5b2b86b66b MediaKit: die Grundschicht, auf der Plex und Jellyfin dasselbe meinen
Erster Schritt zu Jellyfin, und er besteht vor allem aus Wegnehmen.

`PlaybackDecider` nahm bisher Plex-Modelle entgegen und las daraus sechs
Felder. Das band die Entscheidung an einen bestimmten Server, obwohl an
der Frage „kann iOS das öffnen?" nichts serverspezifisch ist. Jetzt nimmt
er ein `MediaProfile` — Container, Codecs, Bitrate, ob die Datei
untersucht ist.

Wie ernst die Kopplung war, zeigte der Player selbst: für die
Offline-Wiedergabe baute er sich **erfundene** Plex-Werte zusammen — eine
Media mit Kennung 0, ein Part mit dem Dateinamen als Schlüssel,
zusammengesetzte Spurlisten —, nur damit die Signatur passte. Für eine
Datei, die längst auf dem Gerät lag und mit Plex nichts mehr zu tun
hatte. Dieser Hack ist weg.

Neu ist das Target `MediaKit`: Typen und Rechnungen, die für jeden Server
gelten, ohne eine einzige Abhängigkeit. Darin `MediaProfile` und die
`Duration`-Brücke, die bisher in PlexKit stand — „weil es die unterste
Schicht ist". Das stimmte, solange es nur einen Server gab.

Damit kennen Wiedergabe und Downloads Plex nicht mehr. Zwei der
Abhängigkeiten waren ohnehin tot; die dritte trug nur diese Brücke.

Die wichtigste Testtabelle des Projekts ist mitgewandert und dabei
kürzer geworden: dort steht jetzt, worauf es ankommt, statt vollständiger
Modelle mit Kennungen und Dateipfaden, von denen niemand etwas las.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 12:26:19 +02:00
GuidoandClaude Opus 5 6150b018ec Vier Punkte: Player-Hänger, kaputte Downloads, Stufenliste, Kopfleiste
Player hing sich auf
  Die Qualitätsauswahl war ein Menu. Schloss man es durch einen Tipp ins
  Bild, blieb eine unsichtbare Ebene zurück, die alle weiteren Tipps
  schluckte — die Bedienelemente kamen nicht mehr, pausieren ging nicht
  mehr. Jetzt ein confirmationDialog, der sich selbst wegräumt. Nach dem
  Schließen werden die Bedienelemente eingeblendet und die Ausblendfrist
  neu gestartet.

Downloads galten als fertig, waren aber leer
  Zwei Fehler übereinander. Erstens gilt ein URLSessionDownloadTask auch
  bei 404 oder 500 als erfolgreich und legt die Fehlerseite als Datei ab.
  Zweitens überschrieb ich danach die erwartete Größe mit der empfangenen
  — womit die Anzeige auf hundert Prozent stand und der Beweis weg war.
  873 MB waren in einer Sekunde „fertig", und beim Abspielen kam nur
  „Wiedergabe wurde unterbrochen".

  Jetzt wird der Statuscode geprüft, die erwartete Größe bleibt stehen,
  und eine unplausible Datei wird verworfen statt verbucht: unter 100 KB
  ist keine Videodatei, unter der Hälfte der angekündigten Größe ist eine
  abgebrochene Übertragung. Sechs Tests dazu.

Stufenliste ergab keinen Sinn
  Für einen 720p-Inhalt standen Original, Full HD und HD mit derselben
  geschätzten Größe untereinander — das sah aus, als wäre die Schätzung
  kaputt. Sie war es nicht, die Stufen waren es: hochrechnen bringt nur
  eine größere Datei. Angeboten wird jetzt nur noch, was unter der
  Auflösung der Datei liegt, und beim Original steht dabei, was es ist:
  „Original · 720p · 873 MB".

Kopfleiste
  Kein Hintergrund mehr statt eines weichgezeichneten — die Knöpfe
  bringen ihre eigene Glasfläche mit. Der Titel ist weg: der Name der App
  gehört auf den Homescreen, nicht über jeden Bildschirm.

145 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 22:32:19 +02:00
GuidoandClaude Opus 5 bf0a63a924 Vier Punkte aus dem Praxistest
Download-Knopf tat nichts
  DownloadService.plan gab still nil zurück, wenn es keinen Auftrag
  bauen konnte, und download() reichte das als nil weiter. Der Knopf sah
  aus, als wäre er kaputt, dabei war der Grund jedes Mal bekannt. Es
  gibt jetzt einen eigenen Ablehnungsgrund mit vier unterschiedlichen
  Begründungen — keine Verbindung, keine Datei, kein abrufbarer Teil,
  keine Adresse. Eine Funktion, die kommentarlos nichts tut, ist selbst
  der Fehler.

Live-TV galt als „nicht eingerichtet"
  Plex legt die Empfängerliste unter dem Schlüssel `Dvr` ab, die Hülle
  kannte aber nur Metadata, Directory und Hub. Sie las eine leere Liste
  und zog daraus die falsche Schlussfolgerung — auf einem Server, auf
  dem ein Empfänger eingerichtet war. Dasselbe galt für die
  Anbieterliste unter `MediaProvider`, ohne die es keinen
  Programmführer gibt.

  Beim Umbau wäre beinahe die Fehlerdiagnose verloren gegangen: mit
  `try?` über alle Schlüssel verschwände auch ein echter Dekodierfehler
  und ergäbe stillschweigend eine leere Liste. Aus „ein Feld heißt jetzt
  anders" würde „die Mediathek ist leer". Jetzt wird erst geprüft, ob
  der Schlüssel da ist, und dann ohne try? dekodiert. Fünf Tests halten
  beides fest.

Qualität im Player wählbar
  Automatisch, Original, 1080p, 720p, 480p. Der Eingriff wird genau dann
  gebraucht, wenn es ruckelt — dafür will niemand den Film verlassen.
  Ein Wechsel baut den Strom neu auf (die Stufe steckt in der Adresse),
  merkt sich die Stelle und fährt sie wieder an. Der alte Transcoder
  wird vorher angehalten, sonst rechnet der Server zwei Ströme für einen
  Zuschauer.

AVPlayer-Anzeige entfernt
  Sie war eine Prüfhilfe aus Phase 1. Dieselbe Angabe steht weiterhin
  unter Einstellungen → Diagnose, wo sie hingehört.

137 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 21:44:20 +02:00
GuidoandClaude Opus 5 222ac1c968 Vier Fehler aus dem ersten echten Gebrauch
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>
2026-09-11 20:43:26 +02:00
GuidoandClaude Opus 5 5c0c1d651d Phase 6: Live-TV, plus Live Activity für Downloads
Live-TV
- An keinem echten Server geprüft: die DVR- und EPG-Endpunkte sind der am
  schlechtesten dokumentierte Teil der Plex-API. Die Modelle sind deshalb
  durchgehend nachsichtig — fehlende Felder ergeben leere Listen, nicht
  Fehler, und die Senderkennung wird als Zahl, Zeichenkette oder uuid
  akzeptiert.
- Ohne eingerichteten Empfänger erklärt die App das, statt einen Fehler
  zu zeigen. Das ist der häufigste Fall.
- Senderliste mit laufender Sendung, Fortschrittsbalken und „danach".
  Der Programmführer wird getrennt geholt: fällt er aus, bleibt das
  Umschalten möglich.
- Die Anbieterkennung wird aus /media/providers gelesen statt fest
  verdrahtet — sie wechselt je nach Server.

Live Activity
- Eigene Widget-Erweiterung; iOS zeichnet den Sperrbildschirm in einem
  getrennten Prozess, während die App meist gar nicht läuft.
- Der Presenter drosselt auf 1,5 Sekunden. ActivityKit verwirft zu
  häufige Aktualisierungen kommentarlos, und der Fortschritt kommt
  mehrmals je Sekunde — ungebremst bliebe die Anzeige stehen.
- @preconcurrency import: Activity und ActivityContent tragen keine
  Sendable-Zusage.

Zwei Stolpersteine unterwegs: canImport(ActivityKit) ist auf macOS wahr,
die Typen darin aber nicht verfügbar — es muss os(iOS) sein. Und eine
von Hand geschriebene Info.plist ohne CFBundleIdentifier führt zu einer
Erweiterung ganz ohne Kennung; der Fehler lautet dann „not prefixed with
the parent app's bundle identifier" und zeigt auf die falsche Stelle.

126 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:54:17 +02:00
GuidoandClaude Opus 5 af23372997 Phase 4 und Teile von Phase 5
Download-Regeln
- Automatisches Löschen nach dem Ansehen, mit Frist statt sofort: wer
  die letzte Szene noch einmal sucht, soll sie nicht schon verloren
  haben. „Nie" ist die Voreinstellung.
- „Halte N ungesehene Folgen offline": wartende und laufende Downloads
  zählen mit, sonst würde bei jedem Durchlauf nachgelegt, bis die ganze
  Serie auf dem Gerät liegt.
- Beides als reine Rechnung ohne Zugriff auf Datenbank oder Dateien —
  bei einer Regel, die Filme löscht, ist Prüfbarkeit keine Formsache.
  13 Tests.

Qualitätswahl beim Download
- transcodedDownloadURL über den Universal-Transcoder mit protocol=http
  statt über die umständliche Sync-API. NOCH NICHT AM SERVER GEPRÜFT:
  offen ist, ob Plex eine Content-Length mitschickt (sonst kein
  Fortschritt und kein Fortsetzen) und ob die Datei durchsuchbar ist.
  Steht so im Quelltext; Original bleibt Voreinstellung.

Erscheinungsbild
- Akzentfarbe: zehn kuratierte Kacheln plus freie Hex-Eingabe. Bei einer
  eigenen Farbe greift die Kontrastkorrektur und sagt es auch — sonst
  wirkt sie wie ein Fehler.
- Vorschaufläche mit Knopf, Symbol und Fortschrittsbalken.

Serveraktionen
- Gesehen-Status umschalten und Sterne setzen über ein Menü auf der
  Detailseite.
- Suchlauf der Mediathek aus der Werkzeugleiste.

115 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:40:10 +02:00
GuidoandClaude Opus 5 62158303a2 Phase 3: Download-Engine und Offline-Wiedergabe
Engine
- Hintergrund-URLSession: läuft weiter, wenn die App zu ist. Der ganze
  Zustand liegt in SwiftData, weil es nach einem Neustart kein anderes
  Gedächtnis gibt.
- Pausieren über cancel(byProducingResumeData:) — nur so lässt sich
  weiterladen statt neu zu beginnen. Bei 40 GB der Unterschied
  zwischen brauchbar und unbrauchbar.
- Zwei Übertragungen gleichzeitig, Folgen in Staffel-/Folgenreihenfolge.
  Alles parallel zu laden hieße: keine Folge wird fertig.
- Speicherlimit und Geräteplatz werden vor dem Annehmen geprüft, ein
  Gigabyte bleibt als Puffer frei.
- Verwaiste Dateien werden beim Start entfernt; sie entstehen, wenn die
  App genau beim Abschluss beendet wird.
- Downloads liegen in Application Support, nicht in Caches (iOS räumt
  den eigenmächtig leer) und sind von der iCloud-Sicherung ausgenommen.

Oberfläche
- Download-Knopf auf Detailseiten mit vier Zuständen in einem Knopf.
- „Alle Folgen laden" für Staffel und Serie; ein abgelehnter Titel
  stoppt den Stapel nicht.
- Downloads-Ansicht mit Belegung, WLAN-Schalter, Pausieren, Fortsetzen,
  Löschen — nach Serie gruppiert.

Offline
- Der Player bevorzugt die lokale Datei, auch ohne erreichbaren Server.
- Beim Herunterladen wird ein Abzug der Serverangaben mitgesichert:
  offline lässt sich nichts nachfragen, und der PlaybackDecider braucht
  Container und Codecs.

Ein Fehler, den die Tests aufgedeckt haben: der Koordinator hielt nur
den ModelContext, nicht den ModelContainer. Ein ModelContext hält seinen
Container nicht am Leben — wird der anderswo freigegeben, stürzt jeder
Zugriff auf einen Eintrag ab, und die Meldung zeigt auf ModelContext.reset
statt auf die Ursache. Jetzt liegt die Zuständigkeit beim Koordinator.

102 Tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 17:34:57 +02:00