c4081ff251c796b49d038abc1449abf592d1db49
266
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
c4081ff251 |
Stufe 4: Fortschritt ab der ersten Sekunde, ohne Schwelle
Der Anlass für diesen Server, jetzt als Code. Fortschritt, Gesehen-Status, Weiterschauen, Nächste Folge, Favoriten und Bewertungen — alles je Benutzer. **Die Jellyfin-Werte sind jetzt belegt statt behauptet.** Das Entwurfsdokument hatte das ausdrücklich offengelassen („aus dem Gedächtnis"); hier stehen sie aus `MediaBrowser.Model/Configuration/ServerConfiguration.cs` und `Emby.Server.Implementations/Library/UserDataManager.cs`: | | Vorgabe | Was passiert | |---|---|---| | `MinResumePct` | 5 | darunter `positionTicks = 0` — Kommentar im Quelltext: „ignore progress during the beginning" | | `MaxResumePct` | 90 | darüber gesehen, Position **auf 0** | | `MinResumeDurationSeconds` | 300 | alles unter fünf Minuten gilt nach dem ersten Lebenszeichen als gesehen | Bei einem Zweistünder sind die ersten sechs Minuten damit weg. Eine vierminütige Folge gilt nach dreissig Sekunden als vollständig gesehen. Und fehlt die Laufzeit, nimmt Jellyfin an, der Titel sei durch — wörtlich: „we'll just have to assume it was fully played". **Flaro legt jede Meldung ab und wirft nie eine Position weg.** Eine Grenze fürs Häkchen gibt es trotzdem — 90 %, sonst stünde jeder zu Ende geschaute Film für immer bei 98 % in Weiterschauen. Der Unterschied ist nicht, *dass* es die Grenze gibt, sondern was sie anrichtet: hier wird ein Häkchen gesetzt, dort zusätzlich die Stelle vernichtet. Wer einen gesehenen Film noch einmal anfängt, hat beides — das Häkchen und den Punkt, an dem er steht. Ohne bekannte Laufzeit wird nichts vermutet. Das ist der Fehler, den man nicht zurücknehmen kann, ohne ihn zu bemerken. --- **Eine Spalte statt einer zweiten Regel.** „Gehört in Weiterschauen" vergleicht die Stelle mit einem Anteil der Laufzeit, und diese Rechnung lässt sich nicht nach SQL übersetzen. Die Regel daneben noch einmal als Abfrage zu schreiben wäre die Variante, bei der beide irgendwann auseinanderlaufen — unbemerkt, weil keine von beiden dabei falsch aussieht. Stattdessen hält `InProgress` das Ergebnis fest, geschrieben an genau einer Stelle. Eine Prüfung hält Reihe und Regel über vier Stände gegeneinander. **„Weiterschauen" und „Nächste Folge" überschneiden sich nicht.** Was angefangen ist, steht links; was ansteht, rechts. Jellyfin zeigt beides doppelt, und auf einer Startseite mit zwei Reihen nebeneinander sieht das nach einem Fehler aus. Dazu, weil die App sie braucht: Einzelabruf für Film und Serie, die nächste Folge nach einer gegebenen, und der Zustand hängt an jeder Liste mit dran — in einer Abfrage je Seite, nicht in einer je Titel. Ein Titel wird über seine Kennung allein angesprochen, ohne dass der Aufrufer sagen muss, ob es ein Film oder eine Folge ist. Die App hat diese Auskunft an der Stelle nicht. 348 Tests grün. Im Container nachgewiesen: fünf Minuten „Der Untergang" von 2:36 bleiben stehen, bei Minute 150 kommt das Häkchen und die Stelle bleibt, beim zweiten Anlauf steht der Film mit beidem wieder in der Reihe — und nach Folge 1 von Dark zeigt „Nächste Folge" S01E02. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
705ec87416 |
Ein lokaler Server mit Beispielmedien, zum Ausprobieren
`Tools/demo-medien.sh` legt unter `~/.flaro-medien` einen kleinen Bestand an: Der Untergang, Matrix, Blade Runner 2049, Inception, Dark mit drei Folgen, Notruf Hafenkante. Echte Namen, damit TMDB etwas zu finden hat — aber jede Datei ist zwei Sekunden Testbild mit Ton und wiegt ein paar Kilobyte. **Eine davon hat zwei Tonspuren**, Deutsch und Audiodeskription, mit gesetzter Sprache und gesetztem Namen. Das ist der Fall, um dessentwillen der ganze Server existiert: Jellyfins Transcode-Endpunkt bildet eine Tonspur je Sitzung ab, und einmal hat die App dabei still die Audiodeskription gewählt — drei Gigabyte Mobilfunk für eine unbrauchbare Datei. Wer das nachstellen will, braucht eine Datei, die es kann. `docker-compose.local.yml` daneben: benannte Volumes statt NAS-Pfaden, Port 18080 statt 8080, Beiwerk eingeschaltet. **`user: "0:0"` ist eine Mac-Eigenheit und steht deshalb kommentiert da.** Colimas Dateifreigabe reicht die Besitzverhältnisse des Wirts nicht in die VM durch — eingehängte Ordner erscheinen im Container als root, und ein Server, der als 1000 läuft, kann dort keine NFO ablegen. Auf einem Linux-NAS gehört die Zeile weg. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
91eb934048 |
Die Weboberfläche — im Zuschnitt von Plex, vom Server selbst ausgeliefert
Einrichtung, Anmeldung, Mediathek, Detailseite mit Abspielen und Herunterladen, Zuordnung korrigieren, Verwaltung. Die Oberfläche aus der Jellyfin-Zeit ist dabei vollständig ersetzt: `types.ts`, `prefs.ts`, `contrast.ts`, `views/cards.ts`, `views/player.ts` und `views/settings.ts` sprachen Jellyfins Schnittstelle und hatten hier nichts mehr zu suchen. Seitenleiste links, waagerechte Reihen auf der Startseite, Plakate im Verhältnis 2:3, unscharfer Hintergrund hinter dem Kopf der Detailseite — das ist der Zuschnitt von Plex, weil er gefällt. **Die Farbe bleibt Flaros Orchidee**, nicht Plex' Orange; der Aufbau ist das Vorbild, nicht das Aussehen. Welche Bibliothek gezeigt wird, entscheidet die Seitenleiste. Eine zweite Umschaltung innerhalb der Seite wäre dieselbe Entscheidung an zwei Orten. --- **Der Zugang liegt jetzt auch in einem Cookie.** Ein `<img>`, ein `<video>` und ein Download-Link senden keine eigenen Kopfzeilen — eine App kann ihr Token in jede Anfrage legen, ein Browser nicht. Das Token stattdessen in die Adresse zu schreiben wäre der naheliegende Ausweg und stünde danach in jedem Zugriffsprotokoll, in jedem Verlauf und in jedem weitergegebenen Link. `HttpOnly`, damit kein eingeschlichenes Skript herankommt. `SameSite=Strict` gegen Anfragen von fremden Seiten. `Secure` nur über https — sonst liesse sich die Oberfläche im Heimnetz über http gar nicht benutzen. Die Apps benutzen weiterhin die Kopfzeile: ein zweiter Weg zum selben Token, kein zweites Verfahren. --- **Der Fehler, den erst der echte Container zeigte:** eine weisse Seite, ohne Hinweis worauf. Die Rückfallregel verlangt für jede Anfrage ohne eigenen Endpunkt einen angemeldeten Benutzer, und `/assets/index.js` hat keinen. Die statischen Dateien standen unter `UseAuthorization` — ein neuer Besucher bekam 401 auf genau das Skript, das ihm die Anmeldemaske zeichnen sollte. `/` kam durch, weil der SPA-Rückfall `AllowAnonymous` trägt; alles mit Dateiendung nicht. Jetzt liegen sie davor. Mit Prüfung. Beim Bau desselben Images der zweite: seit der Kontext die Projektwurzel ist, überschrieb `COPY server/` die `obj/`-Verzeichnisse aus dem Container mit denen vom Arbeitsrechner — die zeigen auf osx-arm64, gebaut wird linux-arm64, `dotnet publish` brach mit NETSDK1047 ab. Dafür gibt es jetzt eine `.dockerignore` in der Wurzel; die unter `server/` sucht Docker seit dem Umzug nicht mehr. --- Vier Prüfungen schlugen danach fehl, und zwar zu Recht: sie hiessen „ohne Anmeldung gibt es nichts", benutzten aber den Client, der nach dem Anmelden ein Cookie hält. Sie prüften also den Angemeldeten. Jetzt sagen sie ausdrücklich, dass sie ohne Zugang fragen. 313 Tests grün. Im Container nachgewiesen: Anmelden, Plakate, Abspielen, Staffeln und Folgen, Verwaltung — und ein Neuladen auf `/verwaltung`, das nicht in einem 404 endet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a7cf6c5def |
Halo heisst ab hier Flaro — die Marke stand im Weg
Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging daneben. **Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler. Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim letzten Mal stattfand. --- 2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`, `Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen `FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und das Image. **Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes Ersetzen hätte den sonst zerlegt. `docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden in die Irre führt. **Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag: `com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe. 308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS bauen, das Image baut, und ein frischer Container läuft durch Einrichtung und Anmeldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
f98b995050 |
Beiwerk im Medienordner, und eine Zuordnung lässt sich korrigieren
Plakate, Hintergründe und NFO-Dateien landen jetzt neben dem Film — und eine
falsche Zuordnung lässt sich richtigstellen, ohne dass der nächste Scan sie
wieder überschreibt.
Der Untergang (2004)/
├── Der Untergang (2004).mkv
├── poster.jpg
├── fanart.jpg
└── movie.nfo
Bei Serien liegt `tvshow.nfo` eine Ebene über dem Staffelordner, je Folge
kommen NFO und Standbild daneben. Liegen mehrere Filme in einem Ordner, heisst
das Beiwerk `Titel (Jahr)-poster.jpg` — sonst überschrieben sie sich
gegenseitig.
**Die Erlaubnisliste ist der ganze Schutz.** Damit das geht, muss die
Medieneinhängung ohne `:ro` auskommen, und damit könnte der Server dort alles
anstellen. Dass er es nicht tut, hängt an `SidecarNames`: sechs Arten von
Datei, nichts sonst. Ein Name, der nicht daraufsteht, wirft — laut, nicht
still, denn das wäre ein Fehler im Programm. Mediendateien werden nie
geändert, umbenannt oder gelöscht; es gibt keine Stelle, die das könnte.
Geprüft an echten Dateien, einschliesslich der Versuche mit `../`, mit
absoluten Pfaden und mit einer Mediendatei als Ziel.
Standardmässig **aus**. In fremde Medienordner zu schreiben ist nichts, was
ein Server ungefragt tun sollte.
**Gelesen wird auch ohne Schreiberlaubnis.** Eine vorhandene NFO ist eine
Zuordnung, die jemand getroffen hat — Halo nimmt sie und sucht gar nicht
erst. Dasselbe gilt für Ordnernamen der Form `[tmdbid-613]`. Halo liest sie
und benennt **nichts** um: Umbenennen ist der einzige Eingriff, der eine
Bibliothek wirklich zerlegen kann.
Nebenbei: eine NFO aus dem Medienordner hat Halo nicht geschrieben. DTDs sind
beim Lesen abgeschaltet — eine untergeschobene Datei mit externer Entität
könnte sonst den Server dazu bringen, `/config/halo.db` einzulesen und als
Titel auszuliefern. Mit Prüfung.
---
**Der Fehler, den erst der echte Lauf zeigte** — und er ist die zweite
Auflage eines schon einmal behobenen:
Nach der Korrektur stand im Ordner „Dark (2017)", in der Datenbank aber 2004.
Der nächste Scan suchte nach *(Dark, 2017)*, fand nichts, legte den Film neu
an und löschte den korrigierten als verwaist. Überlebt hat die Zuordnung nur,
weil sie in der NFO stand — das System hat sich selbst gerettet, aber der
Eintrag war ein anderer und das Jahr wieder falsch.
Dieselbe Klasse wie bei `ParsedTitle`: zugeordnet wurde über einen Wert, den
die Metadatenquelle verändert. Jetzt gibt es `ParsedYear`, und das ändert sich
nie. Mit Prüfung, die genau diesen Ablauf festhält.
---
Dazu von TMDB: die IMDb-Kennung wird nachgeschlagen, damit sie in der NFO
steht — wer später auf ein anderes Werkzeug wechselt, findet denselben Titel
wieder, auch wenn das TMDB gar nicht kennt.
308 Tests grün. Im Container nachgewiesen: Zuordnung falsch, korrigiert,
zweiter Scan — derselbe Eintrag, dieselbe Kennung, Jahr bleibt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
79b6001f86 |
Metadaten ohne Einrichtung — mit Lese-Token statt API-Schlüssel
Halo bringt einen TMDB-Zugang mit. Wer den Server startet, bekommt Titel, Beschreibungen und Plakate, ohne irgendwo ein Konto anzulegen. Am echten Dienst nachgewiesen: „Der Untergang" (2004) mit deutscher Beschreibung, „Blade Runner 2049" (2017), Plakate als JPEG aus Halos eigener Ablage. **Warum überhaupt die Frage stand.** Jellyfin liefert ebenfalls einen Schlüssel mit — fest im Quellcode, offen im Repository, Konstante `ApiKey` in `TmdbUtils.cs`. Ein Abkommen mit TMDB dazu liess sich nicht belegen: deren Bedingungen erwähnen das Teilen von Schlüsseln mit keinem Wort, und die einzige Äusserung eines Moderators zu genau dieser Frage im TMDB-Forum ging nicht darauf ein, sondern wies nur darauf hin, dass Missbrauch zur Sperrung führt. Geduldet also, nicht gesegnet. Deshalb **mit Ausweg**: wer einen eigenen Zugang hinterlegt, benutzt den. Würde der mitgelieferte je gesperrt, steht damit nicht jede Installation still. **Ein Lese-Token, kein API-Schlüssel.** TMDBs eigene Dokumentation bestätigt, dass er auch an den v3-Endpunkten gilt. Zwei Gründe, und beide zählen bei einem Zugang, der öffentlich im Repo steht: er kann nur lesen, und er geht als `Authorization`-Kopfzeile hinaus statt als `?api_key=` in der Adresse — und eine Adresse steht in jedem Zugriffsprotokoll und jedem Reverse Proxy. Der ältere Schlüssel wird weiterhin angenommen; er kann nur in die Adresse. **Der Hinweis auf TMDB ist Bedingung.** Logo und ein fester Satz, so verlangt es deren Lizenz. `system/info` liefert beides ohne Anmeldung, damit ein Client es auch auf dem Anmeldebildschirm zeigen kann. Das fehlte bisher. --- **Zwei Funde, beide nur durch einen echten Lauf sichtbar:** *„Dark" wurde zu „Dark Shadows" von 1966.* TMDB sortiert nach Beliebtheit, und ohne Jahresangabe führt das in die Irre. Jetzt sticht ein Treffer mit genau diesem Namen den beliebteren — danach: „Dark" (2017), deutsche Beschreibung, Folgentitel „Geheimnisse" und „Lügen". Nur bei Gleichheit, nicht bei Ähnlichkeit: eine eigene Bewertung wäre TMDBs Rangfolge nachgebaut, mit weniger Daten. *Die Tests griffen plötzlich übers Netz zu.* Sobald ein Schlüssel mitgeliefert wird, fragt jeder Scan wirklich bei TMDB nach — und prompt hiess „Der Untergang" im Test „Downfall", weil die Voreinstellung Englisch ist. Genau das, was ein Testwerk nicht tun darf: am WLAN hängen und an fremden Daten. Der Anbieter ist jetzt für alle Tests abgeschaltet, ausser ein Test schiebt ausdrücklich einen eigenen nach. 238 Tests grün, keiner davon geht ins Netz. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e4a90fce4e |
Stufe 3: Download mit allen Ton- und Textspuren
**Der Grund, warum es diesen Server gibt.** Eine MKV mit zwei Tonspuren geht
hinein, eine MP4 mit zwei Tonspuren kommt heraus — mitsamt Sprachkennung und
Spurnamen. Im Container nachgewiesen, nicht nur im Test:
0 video h264 und
1 audio aac deu Deutsch
2 audio aac deu Audiodeskription
3 subtitle mov_text deu
Jellyfins Transcode-Endpunkt bildet je Sitzung **eine** Tonspur ab und
verliert beim Umpacken zusätzlich die Sprachkennungen. Der Plan hier hat gar
kein Feld „welche Tonspur" — es gibt nichts auszuwählen, weil alle mitgehen.
**Vorher fragen können.** `GET /files/{id}/delivery` sagt, was geschehen wird:
umgepackt oder nicht, welche Tonspuren neu gerechnet werden müssen, welche
Untertitel nicht mitkönnen. Drei Gigabyte zu laden und erst danach zu merken,
dass die falsche Tonspur drin ist, war der Anlass für das Ganze.
**Umgepackt wird auf die Platte, nicht in die Antwort.** Der bequeme Weg wäre,
ffmpeg durchzureichen. Dann kennt aber niemand die Länge im Voraus: kein
Fortschrittsbalken, kein Fortsetzen, kein Springen. Jetzt kommt beim ersten
Abruf ein 202 mit Fortschritt, danach die fertige Datei mit `Content-Length`
und `Accept-Ranges`.
**Bildbasierte Untertitel fallen weg, und das steht im Plan.** PGS passt in
keine MP4 — betrifft fast jeden Blu-ray-Rip. Stillschweigend wegzulassen
hiesse, jemanden nach Untertiteln suchen zu lassen, die der Server bewusst
entfernt hat.
---
**Zwei Funde aus echten Messungen:**
*Die Spurnamen überlebten das Umpacken nicht* — „Deutsch" kam als `null` an,
und damit wären zwei deutsche Tonspuren hinterher nicht mehr zu unterscheiden
gewesen. Ein Versuch an einer echten Datei zeigte den Grund: **MP4 nennt das
Feld `name`, Matroska nennt es `title`.** ffmpeg schreibt das eine ins andere
um; mein Leser kannte nur `title`. Keine Grenze des Formats, ein Lesefehler.
`handler_name` steht ausdrücklich nicht in der Suche — dort schreibt ffmpeg
„SoundHandler" hinein.
*Der Image-Bau brach mit Code 100 ab.* `intel-media-va-driver` gibt es für
arm64 nicht, und die Meldung verrät nicht, welches von sechs Paketen fehlt.
Die Intel-Treiber kommen jetzt nur auf amd64 dazu; `mesa-va-drivers` bedient
AMD auf beiden.
---
**Die ffmpeg-Zeile ist eine reine Funktion und wird ohne Ausführen geprüft.**
Hier liegt die Erfahrung dieser Woche: eine falsche Parameterkombination
kostete Tage, weil sie nur gegen einen echten Server zu entdecken war. Dazu
gehört die Verwechslung, vor der jetzt ein Test steht: `-c:a:1` meint die
zweite Tonspur der **Ausgabe**, nicht Spur 1 der Datei.
ffmpeg läuft als eigener Prozess, nicht als eingebundene Bibliothek — das
hält die Lizenzen getrennt und lässt einen Absturz dort den Server nicht
mitnehmen.
223 Tests grün (98 Kern, 31 Metadaten, 26 Medien, 68 Schnittstelle).
Image baut für amd64 und arm64, mit ffmpeg 6.1.1 und VAAPI.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
deefc79fc1 |
Das Image nahm das neue Projekt nicht mit
`Halo.Metadata` fehlte in der Liste der Projektdateien, die vor dem Wiederherstellen kopiert werden — der Bau auf dem Arbeitsrechner lief weiter, der im Container wäre gescheitert. Genau die Art Fehler, die eine Liste erzeugt: beim nächsten Projekt wird sie wieder vergessen. Jetzt ein Muster statt einer Liste (`COPY --parents src/*/*.csproj ./`). Die Ebenenzwischenablage für das Wiederherstellen bleibt dabei erhalten. **Gegen den laufenden Container geprüft**, nicht nur gebaut: zwei Bibliotheken angelegt, gescannt, Ergebnis abgerufen. - Drei Filme gefunden, die danebenliegende `.srt` nicht. - „Blade Runner 2049" behielt seine Zahl und bekam 2017 als Jahr. - Sortiert ohne Artikel: Blade Runner, Matrix, Untergang. - „Dark" mit zwei Folgen — mit `@eaDir` wären es drei gewesen. - Folgentitel „Geheimnisse" und „Lügen" aus den Dateinamen gelesen. - Ohne TMDB-Schlüssel meldet der Bericht das Nachschlagen als übersprungen, und die Mediathek steht trotzdem da. Nebenbei bestätigt: die Meldung für einen Pfad, den es im Container nicht gibt, traf zu, als Colimas Dateifreigabe `/private/tmp` nicht durchreichte. Sie nannte genau den Grund. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
acfe2668a8 |
Stufe 2b: Metadaten von TMDB — und ein Testlauf, der 19-mal schneller ist
Titel, Beschreibungen und Bilder kommen jetzt von TMDB. Dazu zwei Fehler, die erst das Zusammenspiel zeigte. **Der Anbieter steckt hinter einer Schnittstelle, und das ist kein Vorrat für einen zweiten.** Es geht darum, dass alles, was darauf aufbaut, *ohne Netz und ohne Schlüssel* prüfbar bleibt. Ein Projekt, dessen Tests einen fremden Dienst brauchen, hat am Ende keine Tests: sie scheitern, wenn das WLAN hakt, und irgendwann schaltet sie jemand ab. **Ein Fehlschlag bei TMDB ist kein Abbruch.** Kein Netz, Kontingent erschöpft, ein Anmeldeportal im Hotel-WLAN, das HTML mit Status 200 schickt — in allen Fällen bleibt der Titel beim Dateinamen und der Scan läuft durch. Die eine Ausnahme ist ein zurückgewiesener Schlüssel: „nichts gefunden" und „dein Schlüssel ist falsch" sehen in der Mediathek gleich aus, und ohne diese Unterscheidung sucht jemand stundenlang. Der Schlüssel steht im Scanbericht. **Der Schlüssel kommt nie wieder heraus.** Abgefragt wird nur, ob einer da ist — sonst gäbe die Oberfläche jedem Angemeldeten das TMDB-Kontingent des Betreibers in die Hand. Geprüft, dass er nicht einmal versehentlich im Antwortkörper steht. **Bilder holt die App bei Halo, nicht bei TMDB.** Sonst bräuchte sie Internet für ein Plakat, das zehn Zentimeter weiter auf der Platte liegt — und TMDB erführe, wer wann was ansieht. Der Schlüssel ist der Hash der Quelladresse, also lädt dasselbe Plakat nie zweimal. Dass daraus kein Weg ins Dateisystem wird, ist eigens geprüft: ohne die Formprüfung wäre `/api/v1/images/../../config/halo.db` ein Weg, die Datenbank samt aller Token-Fingerabdrücke herunterzuladen. --- **Der erste Fehler: nach dem Eintragen der Metadaten legte der nächste Scan den Film erneut an.** Zugeordnet wurde über den Titel — und den hatte TMDB gerade von „The Matrix" auf „Matrix" geändert. Beim übernächsten Lauf dasselbe noch einmal. Das ist die Sorte Fehler, die man erst nach Wochen bemerkt, wenn jeder Film fünfmal dasteht. Jetzt gibt es `ParsedTitle`: den Titel aus dem Dateinamen, der sich nie ändert, und nur der entscheidet beim Zuordnen. **Der zweite Fehler kostete 15 Minuten je Testlauf.** Zwei Tests löschen die Pfadvariablen, um den Rückfall auf die Voreinstellung zu prüfen — und liessen sie gelöscht zurück. Jeder danach gestartete Testserver fiel dann auf `<Projektverzeichnis>/data` zurück, und Dutzende Server teilten sich **eine** SQLite-Datei, auf die sie der Reihe nach warteten. Dazu wartete jeder Host beim Herunterfahren die vollen fünf Sekunden. Gefunden durch Halbieren, nicht durch Raten: einzeln war jede Testklasse schnell, zusammen nicht. Der Beweis lag am Ende als leeres Verzeichnis `src/Halo.Api/data` auf der Platte. **15 Minuten 32 → 49 Sekunden.** 180 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
78b7e3bcb8 |
Stufe 2a: Der Bestand wird gelesen, nicht mehr nur verwaltet
Dateinamen erkennen, Verzeichnisse durchlaufen, abgleichen, blättern. Noch ohne TMDB — die Titel kommen vorerst aus den Dateinamen, und genau daran entscheidet sich später, ob die Metadatenquelle etwas findet. **Der Dateiname ist die Stelle, an der Medienserver in der Praxis scheitern.** Was auf der Platte liegt, heisst nicht „Der Fall", sondern `Notruf.Hafenkante.S01E01.German.1080p.WEB-DL.x264-GRP.mkv`. Das Verfahren schneidet ab dem ersten Technik-Merkmal ab, statt einzelne Tags herauszulösen: Letzteres klingt gründlicher, lässt aber bei jedem unbekannten Tag Bruchstücke stehen — und unbekannte Tags gibt es immer. Drei Fälle, an denen sich die Regeln entscheiden, alle geprüft: - **„Blade Runner 2049" behält seine Zahl.** Eine Jahreszahl muss plausibel sein; was weit in der Zukunft liegt, gehört zum Titel. Ohne diese Grenze hiesse der Film „Blade Runner", erschienen 2049, und keine Metadatenquelle fände ihn so. - **„S.W.A.T." behält seine Punkte.** Trennpunkte werden nur dort zu Leerzeichen, wo sonst keine sind — stehen welche im Namen, hat jemand von Hand benannt, und seine Punkte sind gewollt. (Der Schlusspunkt fiel erst einem Test auf.) - **Ohne Folgennummer wird nichts geraten.** Eine erfundene Staffel 1 landete unter einer Serie, zu der die Datei nicht gehört. **`@eaDir` ist der Grund, warum der Walker von Hand absteigt.** Synologys Vorschauablage enthält eine Kopie *jeder* Datei; mit `SearchOption.AllDirectories` — der bequemen Fassung — stünde jeder Film zweimal in der Mediathek, zur Hälfte als unbrauchbare Kopie. Äste beschneiden kann nur, wer selbst absteigt. Ein gesperrter Unterordner bricht den Lauf nicht ab, sondern wird vermerkt. **Die Datei zeigt auf den Titel, nicht umgekehrt.** Sieht herumgedreht aus und ist Absicht: ein Film liegt oft als 4K *und* als 1080p vor. Trüge der Film den Verweis auf seine eine Datei, wäre die zweite Fassung ein Umbau am Datenmodell — so ist sie eine Zeile mehr. Geprüft. **Der Scan läuft im Hintergrund**, nicht in der Anfrage: ein Bestand mit 40 000 Titeln braucht Minuten, und eine so lange offene Anfrage läuft hinter jedem Reverse Proxy ins Zeitlimit. Einer je Bibliothek, sonst löschen sich zwei Läufe gegenseitig die Einträge. Aufgeräumt wird über den Zeitstempel des Laufs — billiger, als sich alle gesehenen Pfade zu merken. Zweimal scannen ergibt dasselbe wie einmal. Das ist der Fehler, den man sonst erst nach Wochen bemerkt, wenn jeder Film fünfmal dasteht. 131 Tests grün (88 Kern, 43 Schnittstelle). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
156c740330 |
Halo-Server, Stufe 1: Benutzer, Anmeldung, Einrichtung
Der Anfang eines eigenen Medienservers — C#, .NET 10, SQLite, im Container für amd64 und arm64. Stufe 1 von fünf: er spielt noch keinen Film ab, aber ein Fremder kann ihn in Betrieb nehmen, und genau daran scheitern solche Projekte sonst als Erstes. **Was läuft, gegen den laufenden Container gemessen** — nicht gegen einen Prüfstand: Gesundheitsfrage, Einrichtungsassistent, zweiter Einrichtungsversuch abgelehnt (409), Anmeldung, falsches Passwort abgelehnt (401), und der ganze Weg der Code-Anmeldung vom Bildschirm bis zum Token. **Drei Entscheidungen, die beim Lesen Fragen aufwerfen:** *Zeitpunkte liegen als Unix-Millisekunden in der Datenbank.* SQLite hat keinen Zeittyp; EF legt einen `DateTimeOffset` sonst als Text mitsamt Zonenversatz ab, und dann steht `…T12:00+02:00` neben `…T11:00Z` — derselbe Augenblick, als Text falsch herum sortiert. EF weigert sich deshalb, `ExpiresAt > jetzt` überhaupt zu übersetzen; das war der erste echte Fehlschlag und ist jetzt eine Typumwandlung für das ganze Modell statt je Eigenschaft. *Token sind keine JWT.* Der Server schlägt sie nach, statt sie zu prüfen — nur so lässt sich ein verlorenes Telefon sofort aussperren. In der Datenbank steht allein der Fingerabdruck. *Der Code am Fernseher holt nie selbst ein Token ab.* Dafür gibt es den Abholschein, den nur das anfragende Gerät kennt. Ohne ihn wären sechsstellige Codes ein Türschloss mit einer Million Schlüsseln — in Minuten durchprobiert. Dass ein erratener Schein nichts bekommt, ist die wichtigste der 55 Prüfungen. **Zwei Fehler, die erst der echte Container zeigte:** `EnsureExists` legte nur an und prüfte nicht, ob sich schreiben lässt — bei einem vorhandenen, aber gesperrten Verzeichnis meldete es Erfolg, und der Server starb zehn Zeilen später an „unable to open database file". Diese Meldung nennt weder das Verzeichnis noch verrät sie, dass es um Dateirechte geht. Jetzt wird tatsächlich geschrieben, und die Meldung nennt Variable, Pfad, die eigene Kennung und den nächsten Schritt. Und: Docker legt den Einhängepunkt eines frischen benannten Volumes als `root` an, wenn das Verzeichnis im Image fehlt. Das traf jeden, der ohne Bind Mount startet. Die drei Verzeichnisse werden jetzt im Image angelegt und übereignet. **Die Lizenz ist eine Bauvorschrift, keine Fussnote.** GPL-2.0-only verträgt sich nicht mit Apache-2.0 — dessen Patentklausel gilt unter Abschnitt 6 als „weitere Einschränkung". Darum steht in `LICENSES.md`, dass vor jedem `PackageReference` die Lizenz zu prüfen ist, und darum ist das Testwerk NUnit (MIT) und nicht xUnit (Apache-2.0). Alle vier Pakete sind geprüft MIT. Die Apps stehen unter MPL-2.0 statt GPL, weil GPL und App Store sich nicht vertragen — Apple entfernte 2011 VLC aus dem Store, nachdem ein Rechteinhaber sich auf Abschnitt 6 berufen hatte. 55 Tests grün. Image baut für linux/amd64 und linux/arm64. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4b9a1e7b67 |
Flixy heißt ab hier Halo
Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit, damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind. 712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25 Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit `HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`, `JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben. `docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben hiesse, die Vergangenheit zu glätten. **Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis: `com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container; Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen. **Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter `…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet. Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht statt umbenannt — sie steht ohnehin nicht im Repo. Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist ein Handgriff ausserhalb des Projekts. 368 Tests grün. iOS und tvOS bauen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
2fcf6821c8 |
Plex ist raus — und nahm zwei stille Fehler mit
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> |
||
|
|
93695dc332 |
Beim Umpacken wird nach der Tonspur gefragt, nicht geraten
Mein Fehler, und ein vermeidbarer. Beim Umpacken legt der Server genau eine Tonspur in die Datei, und welche, hat bisher allein die Abwägung entschieden — bei einer Aufnahme mit Audiodeskription die falsche. Nach dem Herunterladen gibt es nichts mehr zu korrigieren; drei Gigabyte Datenvolumen waren weg. Wo eine Spur verlorengeht, wird jetzt gefragt. Nur dort: bei einer Datei, die der Systemplayer ohnehin öffnet, kommt alles mit, und eine Frage ohne Folgen ist nur ein Klick mehr. **Der Vorschlag kommt aus dem, was schon bekannt ist.** Die gemerkte Sprache der Serie zuerst, dann die Serieneinstellung, dann die allgemeine — dieselbe Abwägung wie beim Abspielen (`AudioTrackChooser`), über denselben Weg. Sie ist jetzt Vorschlag statt Entscheidung und steht als „· empfohlen" dabei. Beim Stapel aus der Mehrfachauswahl wird einmal gefragt, nicht je Folge: die Folgen einer Staffel sind gleich aufgebaut, und zwanzigmal dieselbe Frage wäre keine Wahl, sondern eine Prüfung der Geduld. **Was am Server gemessen wurde, und warum es nicht besser geht:** - `static=true` liefert die Datei unverändert — alle Spuren, aber Matroska; die Magie am Dateianfang ist `1a45dfa3`, auch wenn im Pfad `.mp4` steht. - `stream.mp4` bildet genau eine Tonspur ab. - Die HLS-Liste des Servers führt **eine** Variante und **keine** `EXT-X-MEDIA`-Tonspuren. Mehrere Tonspuren offline gibt dieser Server auf keinem seiner Wege her. Das liegt nicht an MP4 — das kann mehrere — und nicht an AVFoundation, sondern am Transcoder, der je Sitzung eine Spur abbildet. iOS und tvOS bauen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
24d80e5c95 |
Offline fragte die Spurauswahl den Server — den es dort nicht gibt
Deine Beobachtung, Schritt für Schritt nachgemessen: der Untertitel-Knopf war
da, das Sheet zeigte nur „Aus".
**Zwei Quellen für dieselbe Frage.** Der Knopf richtete sich nach den Spuren,
die der Player in der Datei findet (`subtitleTracks`); die Liste nach den
Spuren, die der Server gemeldet hat (`subtitleStreams`). Die kommen aus
`chooseAudioStream`, und das läuft nur auf dem Online-Weg. Offline war die
Liste deshalb leer — Knopf da, nichts dahinter.
Offline listet das Sheet jetzt die Spuren der Datei und schaltet sie
unmittelbar in der Engine um. Online bleibt es beim bisherigen Weg: dort
heisst Umstellen „neu bestellen", weil ein umgerechneter Stream genau eine
Spur mitbringt.
**Der Tonspur-Knopf verschwindet, wo es nichts zu wählen gibt.** Eine Spur
ist keine Wahl — ein Angebot, das nach dem Antippen nichts anbietet. Bei den
Untertiteln genügt dagegen eine: „an oder aus" ist auch eine Wahl.
**Belegt, dass die Spur wirklich ankommt.** Die heruntergeladene Datei direkt
mit AVFoundation gelesen:
Spuren in der Datei: 3
vide / soun / sbtl (sbtl = tx3g, also mov_text)
Gruppe AVMediaCharacteristicLegible: 2 Auswahlmöglichkeiten
Der Untertitel ist drin — er wurde nur nie angeboten.
**Was dabei auffällt und offen bleibt:** alle Spuren der umgepackten Datei
tragen die Sprache `und`. Jellyfin reicht die Sprachkennung beim Umpacken
nicht durch, also heissen sie „Unknown language". Das ist eine eigene Sache;
die App weiss, welche Sprache sie bestellt hat, und könnte sie beschriften.
iOS und tvOS bauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
25e6ada0ce |
Herunterladen steht jetzt im Kontextmenü
Bisher führte der Weg über die Detailseite — zwei Schritte für etwas, das man beim Vorbeiscrollen entscheidet. Langes Drücken auf eine Kachel bietet es jetzt direkt an, samt derselben Qualitätsauswahl. Die Kachel kennt nur eine Zusammenfassung ohne Dateiangaben; geholt wird deshalb erst beim Tippen. Eine Reihe aus zwanzig Kacheln wären sonst zwanzig Anfragen für ein Menü, das selten aufgeht. **Nur bei Filmen und Folgen.** Bei einer Serie oder Staffel lädt man nicht *den* Titel, sondern seine Folgen — dafür gibt es auf der Seite „Alle Folgen laden". Und nicht bei dem, was schon in der Warteschlange steht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6b6bc6ad02 |
Der Server lieferte nichts — drei Angaben fehlten in der Adresse
Am echten Server gemessen, nachdem der erste Versuch mit „Fehler 500" abgelehnt wurde. Drei Befunde, alle reproduziert: **`deviceId` und `playSessionId` fehlten.** Ohne sie antwortet Jellyfin mit `200` und `Content-Length: 0` — kein Fehler, keine Datei, nichts. Mit ihnen kamen in fünfzehn Sekunden 66 MB. Das betraf **auch die verkleinerten Fassungen**: `compressedDownloadURL` hatte sie nie. „In 480p herunterladen" war damit die ganze Zeit folgenlos — ein Fehler, der älter ist als alles von heute. **Die Sitzungskennung muss je Auftrag neu sein.** Zuerst hatte ich sie an die Titelkennung gehängt, damit ein Wiederholversuch dieselbe Sitzung bleibt. Zwei Anfragen unter derselben Kennung hielt der Server aber für dieselbe Sitzung und antwortete der zweiten mit einer leeren Datei. Jetzt eine UUID beim Anlegen des Auftrags — eindeutig je Download, gleich über alle Wiederholversuche, weil die Adresse gespeichert ist. **`subtitleCodec=mov_text` muss dabeistehen.** Das war der Fehler 500. Mit `subtitleMethod=Embed` allein lehnt der Server ab; derselbe Aufruf ohne Untertitel liefert 61 MB, mit Untertitel und `mov_text` 42 MB samt Spur. MP4 kennt kein SubRip — wer den Untertitel hineinlegen will, muss sagen, als was. Nachgewiesen im Simulator gegen den echten Server: „100,0 MB / 917,3 MB · 6,1 MB/s · noch 2 Min · 11 %". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a432592271 |
Beim Umpacken fällt die Spurwahl an — also wird sie dort getroffen
Dein Befund an „Notruf Hafenkante": keine Spuren, nichts auswählbar. Die
technischen Angaben derselben Folge sagen, warum:
Container MKV
Tonspur AAC - Stereo - Standard
Tonspur Deutsch - German - AAC - Stereo - Standard
Untertitel German - Standard - SUBRIP
Die Quelle ist Matroska. AVFoundation öffnet das nicht, also lief der
Download bisher über die zweite Engine — und die meldete die Spurliste leer.
Offline wählen ging damit nie.
**Umpacken allein hätte es nicht gelöst.** Jellyfin bildet in die neue Hülle
genau **eine** Tonspur ab und übernimmt den Untertitel nur, wenn man ihn
nennt. Ohne Angabe nimmt der Server seine eigene Vorgabe — und das ist bei
deutschen Aufnahmen regelmäßig die Spur mit Audiodeskription.
Also fällt die Wahl jetzt beim Herunterladen, mit derselben Abwägung wie beim
Abspielen (`AudioTrackChooser`, der für genau diesen Fall gebaut wurde: die
als deutsch ausgezeichnete Spur ist die mit Audiodeskription). Der Untertitel
geht als `subtitleMethod=Embed` mit — als **Spur**, nicht ins Bild gebrannt,
sonst liesse er sich nicht abschalten.
Die Mediathek bleibt bei der Abwägung aussen vor: sie nachzuschlagen kostet
eine Anfrage je Folge, und Serie und allgemeine Einstellung tragen den Wunsch
ohnehin.
Der Offline-Abzug behauptet nicht mehr, alle Sprachen seien dabei — beim
Umpacken bleibt je eine übrig, und alle zu notieren wäre ein Versprechen, das
die Datei nicht hält.
**Nebenbei richtiggestellt:** die technischen Angaben rechneten mit
`.directPlayOnly` und zeigten deshalb „Direkt (Systemplayer)" direkt über
„iOS öffnet den Container MKV nicht" — zwei Angaben, die sich widersprechen.
Das war früher richtig (ohne Serverhilfe übernahm die zweite Engine und hiess
auch so); jetzt wird mit Serverhilfe gerechnet.
NOCH NICHT AN EINEM ECHTEN SERVER GEPRÜFT.
iOS und tvOS bauen, 494 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6b927c21b0 |
VLC ist weg — es gibt nur noch einen Spieler
Du hattest recht mit der Erwartung, nur nicht mit dem Stand: entfernt war er nicht. Was wir gemacht haben, war, ihm Arbeit wegzunehmen — DTS, TrueHD und Vorbis gehen seit Kurzem über das Umpacken beim Server. Übrig blieb er an einer Stelle, `PlaybackDecider.fallback`. Jetzt ist er ganz weg, samt dem 920-MB-Binärpaket. **Was an seine Stelle tritt.** Die Entscheidung sagt nicht mehr, *wer* spielt, sondern *was ankommt*: direkt, umgepackt oder umgerechnet. `PlaybackEngineKind` fällt damit weg, `directPlay` trägt keine Engine mehr, und `PlaybackPolicy` kennt kein `allowsFallbackEngine`. **Das Auffangnetz bleibt, nur anders geknüpft.** Scheiterte das Umpacken am Player, sprang bisher VLC ein — der Server entscheidet erst beim Ausliefern, ob er die Ströme wirklich kopieren kann, und manche Antworten nimmt AVPlayer nicht an. Statt der zweiten Engine rechnet der Server jetzt neu (`fallBackToTranscode`). Teurer für ihn, aber es kommt etwas an, statt einer schwarzen Wand mit einer Fehlernummer. **Offline wäre das ein Rückschritt gewesen** — dort gibt es keinen Server, der einspringt. Deshalb ging der vorige Commit voraus: was heruntergeladen wird, kommt schon in einer Hülle, die der Systemplayer öffnet. Was dadurch überall gilt, auch für Dateien, die bisher bei VLC landeten: Bild-in-Bild, AirPlay, Bedienung im Sperrbildschirm — und dasselbe Verhalten auf dem Apple TV, wo es VLC ohnehin nie gab. Die Attrappe heißt ihren Schalter jetzt `--dts` statt `--vlc`: der Fall ist geblieben, sein Ziel nicht. iOS und tvOS bauen, 494 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ef2596e2bb |
Was heruntergeladen wird, muss der Systemplayer öffnen können
**Vorbereitung dafür, dass VLC verschwindet.** Online ist der Fall längst
gelöst: steht nur die Schachtel im Weg, packt der Server um — dieselben
Ströme, eine MP4-Hülle. Offline gibt es diesen Ausweg nicht. Was einmal als
Matroska auf dem Gerät liegt, kann hinterher niemand mehr umpacken, und ohne
zweite Engine bliebe die Datei schwarz.
Also entscheidet sich das beim Herunterladen. Fragt jemand das Original an und
AVFoundation könnte die Datei nicht öffnen, wird `remuxDownloadURL` benutzt:
`/Videos/{id}/stream.mp4` mit `allowVideoStreamCopy` und
`allowAudioStreamCopy`. Der Server reicht durch, was passt, und rechnet nur
um, was nicht passt — das Bild bleibt das Original, nur DTS oder TrueHD
werden zu AC3 oder AAC.
Drei Dinge hängen daran und sind mitgezogen:
- Die **Größe** ist dann keine Zusage mehr: dieselben Ströme in einer anderen
Hülle wiegen anders. `expectedIsExact` fällt auf falsch, und es gilt wieder
die weiche Prüfung.
- Der **Container** im Offline-Abzug ist „mp4", nicht die alte Schachtel.
Sonst entschiede der Player anhand einer Datei, die es so nicht mehr gibt.
- Der **Tonkodierer** bleibt leer. Ob aus DTS dann AAC oder AC3 wird, sagt der
Server vorher nicht; eine Behauptung wäre schlechter als die Lücke, und die
Spuren liest der Player ohnehin aus der Datei.
**Die Frage „kann das der Systemplayer?" steht jetzt an einer Stelle.**
`DeviceCapabilities.opens(container:videoCodec:audioCodec:)` — dieselbe Liste,
nach der auch beim Abspielen entschieden wird. Was der Server nicht nennt,
gilt als passend: vorsichtshalber umzupacken hiesse, aus einer tadellosen MP4
eine neu gerechnete Datei zu machen. Fünf Tests.
NOCH NICHT AN EINEM ECHTEN SERVER GEPRÜFT: ob Jellyfin das Kopieren hier
tatsächlich wählt, verrät erst die Dateigröße des ersten Downloads.
140 Tests in FlixyPlayback, iOS und tvOS bauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b1973c6890 |
Bild-in-Bild gab es nur beim Weggehen — jetzt auch auf Zuruf
Du hast keinen Knopf gefunden, weil es keinen gab: `AVPictureInPictureController` stand da, aber nur mit `canStartPictureInPictureAutomaticallyFromInline`. Das greift, wenn man die App verlässt — und auf dem Mac gar nicht. Der Knopf erscheint nur, wenn es wirklich ginge. Das ist nicht immer so: `attachSurface` steigt für alles ausser `AVPlayerEngine` sofort aus, und `AVPictureInPictureController` braucht eine `AVPlayerLayer`. Läuft ein Titel über VLC, gibt es kein Bild-in-Bild — nur Ton im Hintergrund. Genau das war deine Beobachtung. **Beobachtet, nicht abgefragt.** `isPictureInPicturePossible` steht erst fest, wenn der Player ein Bild hat; wer beim Zeichnen fragt, bekommt `false` und zeichnet den Knopf nie wieder. Also meldet es sich über KVO — vom Input-Thread, deshalb der Sprung auf den Hauptakteur, wie bei den übrigen Beobachtungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
261cdee2f2 |
Mehrere Folgen ankreuzen und in einem Zug laden
Je Folge einzeln zu tippen ist bei einer halb gesehenen Staffel die falsche Arbeit. „Auswählen" im Kopf der Folgenliste schaltet um: jede Zeile bekommt einen Kreis, unten erscheint „N Folgen laden". **Der Knopf steht bei dem, was er auswählt** — in der Zeile mit „Folgen", nicht oben in der Werkzeugleiste. Dort wäre er weiter von der Liste entfernt als von allem anderen auf der Seite. **Nur bei Folgen.** Eine Staffelliste zeigt ihn nicht; für ganze Staffeln gibt es den Knopf oben im Kopf. **Die Qualität wird einmal gefragt, nicht je Folge.** Dafür werden die Dateiangaben **einer** Folge nachgeholt — die Folgen einer Staffel sind in derselben Fassung kodiert, und zwanzig Anfragen für eine Frage, die einmal gestellt wird, wären zwanzig zu viel. Geladen wird danach jede Folge einzeln; dort zählt wieder ihre eigene Größe. **Was zur Wahl steht, steht jetzt an einer Stelle.** Die beiden Rechnungen — welche Stufen angeboten werden dürfen und wie sie beschriftet sind — lagen im Knopf einer einzelnen Folge. Mit der Leiste gäbe es sie zweimal, und die zweite Kopie wäre die mit dem Fehler. Sie sind jetzt `DownloadQualityChoice`, ohne eine Zeile SwiftUI darin. `DownloadService.downloadAll` und der neue Weg teilen sich dieselbe Schleife; „alle Folgen" ist nur noch „diese Folgen" mit einer anderen Liste. Im Simulator geprüft: Serienseite ohne Knopf, Staffelseite mit; zwei Folgen angekreuzt → „2 Folgen laden". iOS und tvOS bauen, 489 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
915bdcd277 |
Die Downloadzeile stand nicht still — und sagte nicht, wie weit sie ist
Aus deiner Aufnahme: „181,2 MB … 3,4 MB/s" wird „190,1 MB … 5,8 MB/s", und dabei wandert alles dahinter mit. In einer Proportionalschrift ist die 1 schmal und die 8 breit; zweimal je Sekunde ergibt das ein Zappeln. Drei Dinge halten die Zeile jetzt still: **Ziffern in fester Breite** (`monospacedDigit`) — damit kostet jede Ziffer gleich viel Platz, egal welche. **Eine feste Nachkommastelle.** `.byteCount` lässt die Null hinter dem Komma weg: aus „186,0 MB" wird „186 MB", und die Zeile ist zwei Zeichen kürzer. Gemessen an sechs aufeinanderfolgenden Sekunden war das die einzige, die noch sprang. Die Zahlen kommen deshalb über `Measurement` mit fester Stelle. **Die Prozente am rechten Rand.** Dort wachsen sie nach innen und schieben nichts; die Zeile links wächst nach rechts und schiebt ebenfalls nichts. Gemessen an acht aufeinanderfolgenden Sekunden: „343.1 / 347.0 / 350.9 / 355.0 / 359.1 / 363.5 / 368.1 / 373.0 MB" — jede Spalte beginnt auf demselben Punkt. **Ein Umweg, der nicht blieb.** Beide Zahlen in das Mass des Ziels zu zwingen hätte auch den Wechsel von MB auf GB beseitigt — machte den Anfang aber unlesbar: bei 2 GB Ziel stand dort „0,03 GB / 2 GB". Der Wechsel kommt einmal je Download; das ist der kleinere Preis. **Und die Prozente**, nach denen man eigentlich sieht. „181,2 MB von 890,2 MB" sagt dasselbe, aber man muss es ausrechnen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ad556c9034 |
Ein Knopf an jeder Folge, und die Suche spielt nicht mehr sofort ab
**Staffelansicht: Download je Folge.** Für den Fall, den du beschrieben hast — Staffel halb gesehen, Rest nachholen. Ein Tipp öffnet dieselbe Qualitätsauswahl wie auf der Detailseite. Der Knopf steht **neben** dem Link, nicht darin: ein `Button` im Label eines `NavigationLink` löst auf iOS nie aus, und `.plain` lässt das lautlos geschehen. Dieselbe Falle steht in `QuickPlayTile` schon beschrieben. Als Geschwister in einer Reihe teilt das Layout den Platz auf, und der Titel kann gar nicht erst unter dem Knopf verschwinden. Die Kinderliste trägt keine Dateiangaben — ohne Nachladen wäre die Qualitätsliste leer und die Prüfung auf freien Speicher übersprungen. `DownloadButton` holt sie deshalb beim ersten Tipp nach (`loadsDetails`), mit Kreisel und fester Breite, damit die Zeile nicht springt. Nicht beim Erscheinen: zwanzig Zeilen wären zwanzig Anfragen für einen Knopf, den man meistens nicht drückt. Nur bei Folgen. Für eine ganze Staffel gibt es den Knopf oben schon. **Suche: Antippen führt auf die Seite.** Die Suche baut dieselbe Reihe wie die Startseite und war damit auch dieselbe Geste. Wer sucht, weiß aber noch nicht, was er sehen will — der erste Treffer ist eine Vermutung des Servers. Abspielen steht weiter im Menü unter langem Drücken. Dabei aufgefallen: `SearchView` hinterlegte `flixyItemChanged` nicht, anders als Startseite und Mediathek. „Als gesehen markieren" wirkte dort am Server, aber die Kachel lud nie neu — es sah aus, als wäre nichts passiert. Im Simulator geprüft: Knopf an jeder Folge, Auswahl mit echten Größen („Original · HD · 906,9 MB"), Suchtreffer öffnet die Detailseite. iOS und tvOS bauen, 489 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
e86c0a6e95 |
Ein Download riss ab, galt als fertig — und setzte nie fort
Dein Bericht: bei rund 3 GB abgebrochen, beim Fortsetzen wieder bei null,
und später „plötzlich bei 60 Prozent fertig". Beide Hälften standen so im
Code.
**Fertig war sie nicht, sie war halb.**
if expected > 0, Double(written) < Double(expected) * 0.5 { verwerfen }
Alles über der Hälfte galt als vollständig. Diese Regel war nie für diesen
Fall gedacht, sondern für die **hochgerechnete** Größe der Transcoder-Stufen
(Bitrate × Dauer) — dort ist Abweichung normal. Beim Original nennt der
Server die Dateigröße in den Metadaten, und da ist jedes fehlende Byte ein
Abriss.
Das unterscheidet jetzt `expectedIsExact`, gesetzt beim Planen und
mitgeführt am Eintrag. Dazu merkt sich der Koordinator, für welche Titel der
Server eine `Content-Length` geschickt hat — auch das ist eine Zusage.
**„Fortsetzen" war eine Zusage, die der Code nicht halten konnte.**
`NSURLSessionDownloadTaskResumeData` kam im ganzen Projekt nicht vor; den
Wiederaufnahme-Datensatz gab es nur beim Pausieren von Hand. Nach einem
Netzfehler war er weg, und der Knopf daneben hieß trotzdem „Fortsetzen".
Jetzt wird er aus dem Fehler gerettet, und wo es ihn nicht gibt, heißt der
Knopf „Erneut versuchen".
**Und es merkte niemand, wenn das WLAN zurückkam.** `RootView` reicht das
jetzt weiter — nur für Einträge mit Wiederaufnahme-Datensatz, damit ein 404
sich nicht im Kreis versucht.
**Rate und Restzeit** stehen in der Zeile: „12 MB / 20 MB · 3,4 MB/s ·
gleich fertig". `DownloadRate` ist ein reiner Wertetyp mit Uhr von außen —
geglättet, weil rohe Differenzen um ein Vielfaches springen, und ohne Zahl,
solange nichts gemessen ist. Neun Tests.
**Zwei Dinge, die erst die Messung gezeigt hat** — und die beide nicht in
der App liegen:
Erstens: reißt eine Übertragung ab, **die eine `Content-Length` angekündigt
hatte**, wiederholt die Hintergrundsitzung von iOS sie selbst — stumm, ohne
den Delegaten zu fragen, und **jedes Mal ab Byte null**. Gemessen: sieben
Anläufe, jeder die vollen 12 MB. Das ist genau dein „fing wieder bei null
an", und dagegen hilft kein Wiederaufnahme-Datensatz.
Zweitens: ein Fortsetzen braucht eine Prüfmarke vom Server. Ohne `ETag`
oder `Last-Modified` kann iOS sein `If-Range` nicht stellen und lädt alles
noch einmal — gemessen, 20 MB zweimal. Mit Marke kam die zweite Anfrage als
`Range: bytes=5636096-` und holte nur den Rest.
Die Attrappe kann das jetzt alles nachstellen: eine beliebig große Datei,
die auf Wunsch mittendrin abreißt, mit oder ohne angekündigte Länge, und
gedrosselt. Ohne sie ist nichts davon beweisbar — und genau daran ist der
Fehler vorbeigekommen.
Im Simulator gegen die Attrappe: 12 von 20 MB → „Fehlgeschlagen:
Übertragung unvollständig", nicht Haken. Vollständig → Haken. iOS und tvOS
bauen, 489 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
c37087fd34 |
Auf dem Telefon stand ein Plakat im Aufmacher — zweimal derselbe Name
Der Aufmacher auf dem iPhone hat nicht das Hintergrundbild geholt,
sondern das **Plakat**. Dafür gab es einen Grund, und er stimmte: ein
16:9-Bild in einem 2:3-Rahmen ist ein Streifen aus der Mitte.
Nur war der Rahmen das Problem, nicht das Bild. Ein Plakat trägt den
Titel selbst, und darüber legt der Aufmacher noch den freigestellten
Schriftzug — derselbe Name zweimal, einmal gesetzt und einmal gemalt.
**Jetzt ein Querbild in einem 4:3-Rahmen.** 16:9 ginge nicht: das sind
auf 440 Punkten Breite 248 Punkte Höhe, und darin sollen Schriftzug,
Zeile, ein Satz Beschreibung und zwei Knöpfe stehen. Sie standen schon
einmal übereinander, das ist der dokumentierte Grund für das Hochformat.
4:3 gibt 330 Punkte — gemessen passt der Text hinein; dafür fehlt dem
Bild links und rechts zusammen ein Viertel.
**Die Detailseite bleibt hochkant.** Dort hat man sich schon entschieden;
das Plakat ist das Bild dieses Films, und du wolltest den Kopf der Seite
ausdrücklich höher. Beide Seiten lasen bisher aus **derselben**
Funktion — eine Änderung für den Aufmacher hätte die Detailseite von 660
auf 330 Punkte gestutzt, ohne dass irgendwo ein Fehler aufgetaucht wäre.
`HeroArtwork` kennt deshalb jetzt den Verwendungszweck:
HeroArtwork.aspectRatio(.detailHeader, compact: …) // 2:3
HeroArtwork.aspectRatio(compact: …) // 4:3 bzw. 16:9
Maße, Verhältnis und Reihenfolge der Bildpfade hängen alle an **einer**
privaten Abfrage — sie müssen zusammenpassen, sonst beschneidet der
Server anders als der Rahmen.
Die Maße schmal: 1200×900 statt 1920×1080. Der Rahmen ist dort höchstens
1320 Bildpunkte breit; das spart auf einer Mobilfunkverbindung ein
Drittel.
Gemessen im Simulator (iPhone 17 Pro Max): Aufmacher 4:3, Text passt,
Detailseite unverändert hochkant. iOS und tvOS bauen, 476 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2ab05ee8f6 |
Der Schriftzug gehörte zum vorigen Film — ein Zustand ohne Adresse
„Werner Beinhart" über dem Bild von John Wick. Von mir eingebaut, im
Commit, der die Überblendung umgestellt hat.
**Der Grund ist der Umbau selbst.** Vorher bekam der Aufmacher je Titel
eine neue Identität; sein Zustand begann damit jedes Mal von vorn. Seit
er bleibt und nur seine Angaben wechseln, überlebt auch der geladene
Schriftzug den Wechsel — und `availableLogo` gab ihn heraus, ohne zu
prüfen, ob er überhaupt zum aktuellen Titel gehört:
if let logo { return logo } // ← zu irgendeinem Film
Jetzt trägt der Zustand die Adresse mit, zu der er gehört, und wird nur
herausgegeben, wenn sie zur aktuellen passt. Passt sie nicht, gilt der
Zwischenspeicher — und wenn auch der nichts hat, der gesetzte Titel. Nie
der falsche Schriftzug.
**Dieselbe Falle steckte in `FlixyArtwork`**, nur unauffälliger: auch
dort hielt `loaded` ein Bild ohne Angabe, wozu es gehört, und `load()`
brach deswegen früh ab („ist ja schon eins da"). Solange jede Kachel ihre
eigene Identität hat, fällt das nicht auf. Der Aufmacher hat seit dem
Umbau keine mehr.
Die Regel dahinter gilt allgemein: **ein Zustand, der etwas
zwischenspeichert, muss sagen, wozu es gehört** — sonst gilt er für
alles.
Die Attrappe liefert jetzt Schriftzüge (`Logo`-Bildmarke). Ohne sie fällt
der Aufmacher auf den gesetzten Titel zurück, und dieser ganze Weg wäre
im Simulator gar nicht zu sehen — genau deshalb ist der Fehler erst beim
Nutzer aufgefallen.
iOS und tvOS bauen, 476 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
37bb153dd8 |
Der Schriftzug wandert wieder mit — über dem Verlauf statt in ihm
Im Commit davor blendete nur noch das Bild über; Schriftzug, Zeile und
Knöpfe standen still und wechselten schlagartig. Das war zu viel
weggenommen: die Bewegung gehört dazu.
Jetzt wandert der Text wieder mit — aber an der richtigen Stelle im
Stapel. Der Aufmacher zeichnet seinen Inhalt während eines Wechsels
zweimal: den weichenden deckend, den kommenden mit demselben Einblenden
und demselben Hereinrutschen wie das Bild darunter. Beides liegt **über**
dem Verlauf, und dort kostet es ihn nichts.
Der Unterschied zum alten Aufbau ist genau diese Reihenfolge. Vorher
lagen zwei **vollständige** Aufmacher übereinander, jeder mit eigenem
Verlauf — und zwei halbdurchsichtige Verläufe ergeben zusammen keinen
ganzen. Jetzt gibt es einen Verlauf, einmal, in voller Stärke.
Nachgemessen über einen auf sechs Sekunden gestreckten Wechsel, an
derselben Stelle im Bild:
ursprünglich 68 .. 154
jetzt 63 .. 75
Dafür ist der Textsatz in `HeroBanner` eine eigene Funktion geworden —
es gibt ihn während eines Wechsels zweimal — und `HeroSection.metadata`
ist `static`, weil die Zeile auch für den weichenden Titel gebraucht
wird. `titleView` und `plainTitle` sind darin aufgegangen.
iOS und tvOS bauen, 476 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
63c664e5a9 |
Der Aufmacher hellte beim Wechsel auf — zwei Verläufe ergeben keinen
Beim Wechsel im Karussell war der Verlauf nach unten für einen Moment
weg: das Bild stand bis unten hell da, und erst nach dem Übergang kam das
Abdunkeln zurück.
**Der Grund steckt in der Form der Überblendung.** Das Karussell legte
zwei vollständige Aufmacher übereinander und blendete den oberen ein. Nur
bringt jeder von beiden seinen eigenen Verlauf mit — und zwei
halbdurchsichtige Verläufe ergeben zusammen keinen ganzen.
Gemessen, an derselben Stelle im Bild, über einen auf sechs Sekunden
gestreckten Wechsel:
Ruhezustand 68 .. 75
Mitte des Wechsels 137 ← fast doppelt so hell
Zwei naheliegende Reparaturen habe ich davor versucht und verworfen,
weil die Messung sie widerlegt hat: dem weichenden Aufmacher die
Animation zu nehmen (wurde schlechter, 152) und beide Ebenen
gegenläufig zu blenden (brachte einen dunklen Einbruch auf 35 dazu).
Beide behandelten das Symptom, nicht die Form.
**Jetzt wechselt nur das Bild.** `HeroBanner` nimmt zusätzlich die
weichende Bildadresse entgegen und blendet **innen** über; Verlauf,
Schriftzug und Knöpfe liegen einmal darüber und ändern sich dabei nicht.
Damit kann der Verlauf gar nicht mehr schwächer werden — nicht weil es
jetzt richtig gerechnet wird, sondern weil es nur noch einen gibt.
Nachgemessen, derselbe Wechsel:
vorher 68 .. 154
jetzt 63 .. 75
Die Aufmacher-Ansicht behält dabei ihre Identität, statt je Titel eine
neue zu bekommen: eine neue hiesse Einfügen und Entfernen, und SwiftUI
blendete sie dann zusätzlich ein — noch eine Überblendung über der
gemeinten.
iOS und tvOS bauen, 476 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
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> |
||
|
|
1fa8011094 |
Eine Mediathek fehlte ganz — „Heimvideos" lagen bei den Fotos
Jellyfin nennt „Heimvideos & Fotos" `collectionType: homevideos`. Die Zuordnung legte das zusammen mit `photos` auf `.photo` — und damit fiel die ganze Mediathek unter `MediaLibrary.isSupported` heraus: keine Reihe auf der Startseite, kein Eintrag in der Mediathekenleiste, nicht einmal eine Zeile in der Auswahl für den Aufmacher. Aus Sicht der App gab es sie nicht. Dass Fotos im Namen stehen, macht den Inhalt nicht zu Fotos. `photos` bleibt bei den Fotos, `homevideos` ist jetzt `.other` — wie alles, was der Server nicht einsortiert. Die Zuordnung steht dafür nicht mehr mitten in `libraries()`, sondern als eigene Funktion mit Tests. Sie entscheidet mehr, als sie aussieht: was hier als `.photo` oder `.music` landet, ist in der App unsichtbar, und das sieht aus wie ein Server, der nichts liefert — nicht wie eine Zuordnung, die zu scharf ist. Drei Tests halten es fest, darunter der Fall ohne Angabe: eine gemischte Mediathek meldet gar keine Art und muss trotzdem bleiben. iOS und tvOS bauen, 472 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0a59b8e82b |
Die Attrappe unterscheidet die Mediatheken — und der Aufmacher sagt, wenn eine Auswahl ins Leere greift
Gemessen, weil „es tut nichts" und „es tut etwas anderes" von aussen
gleich aussehen. Gegen die Attrappe, mit laufendem Mitschnitt:
Auswahl ["lib-shows","lib-movies"] → übrig [latest.lib-movies, latest.lib-shows]
Auswahl ["lib-shows"] → übrig [latest.lib-shows]
Und der Aufmacher wechselte dabei **sofort** von „Nachtzug nach Genf"
(Film) zu „Küstenwache" (Serie) — ohne Neustart, mit offener App. Der
Weg stimmt also; was gemeldet wurde, muss eine andere Ursache haben.
Zwei Dinge, die diese Messung erst möglich gemacht haben:
**Die Attrappe hatte in jeder Mediathek dasselbe stehen.** „Neu in Filme",
„Neu in Serien" und „Neu in Gemischtes" lieferten dieselben Titel — damit
sieht ein wirkungsloser Filter genauso aus wie ein wirksamer. Jetzt gibt
es zwei Titel, die es **nur** in „Gemischtes" gibt, und jede Mediathek
führt, was zu ihr gehört.
**Ein Zwischenergebnis, das ich fast für den Fehler gehalten hätte:** ein
Neuinstallieren über `simctl install` löscht die Voreinstellungen nicht.
Die vermeintlich frische Auswahl war in Wahrheit die alte plus die neue —
zwei Messungen lang.
**Und der Punkt, der dem gemeldeten Verhalten am nächsten kommt:** trifft
die Auswahl keine einzige Reihe, gilt sie seit gestern als wirkungslos und
der Aufmacher zeigt alles. Das verhindert einen leeren Aufmacher, sieht
aber genau aus wie ein Schalter, der nichts tut. Jetzt steht es dort, wo
man es einstellt: ein Hinweis unter der Auswahl, sobald für die
angehakten Mediatheken keine eigene Reihe auf der Startseite existiert.
Welche Mediatheken überhaupt eine Reihe stellen, wird dafür aus den
geladenen Reihen gelesen, nicht angenommen.
iOS und tvOS bauen, 469 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7f380e5f4d |
Die Bibliothek beginnt jetzt in einer Mediathek
Vorher stand dort eine Liste aller Mediatheken: auf „Bibliothek" tippen, dann auf „Filme", dann endlich ein Raster. In eine andere wechseln hiess zurück und wieder hinein. Der Zwischenbildschirm hat nie etwas erzählt, was eine Leiste nicht auch zeigen kann — er war ein Platzproblem, kein Entwurf. Jetzt steht man sofort in einer Mediathek, und darüber der unteren Leiste liegt eine zweite mit den Mediatheken und Live-TV. Auf dem iPad und am Mac ändert sich nichts: dort steht jede Mediathek einzeln in der Seitenleiste, auf dem Fernseher nebeneinander in der oberen Leiste — es ist dieselbe Idee in der Form, die ein Telefon hergibt. **Warum eine zweite Leiste und nicht Plätze in der unteren.** Die hat fünf, und alles darüber schiebt das System in ein „Mehr"-Verzeichnis, dessen Einträge zwei Navigationsleisten übereinander tragen. Genau daran ist der Zwischenbildschirm seinerzeit entstanden. Eine selbst gezeichnete Leiste kennt diese Grenze nicht — und sie sieht aus wie die Insel auf dem iPad, weil sie dieselbe Fläche benutzt: `FlixySidebar.surface(in:)` ist ein Bauteil, kein nachgebautes Aussehen, und wird jetzt zum dritten Mal gerufen. Als Sicherheitsrand und nicht als Überzug, damit die Liste über der Leiste endet statt dahinter zu verschwinden. Bei mehr als vier Mediatheken wird sie waagerecht scrollbar. **Das Monogramm steht jetzt in der Werkzeugleiste**, am rechten Ende, auf derselben Höhe wie Suche, Filter und Ansicht — die rücken dafür nach links. Als Überzug lag es über dem Inhalt, auf einem Raster also über einem Plakat. Dass es dabei an den Anfang der Reihe rutschte, hat einen Grund, der im Quelltext steht: SwiftUI reiht Werkzeugleisten von aussen nach innen, ein von aussen angehängter Eintrag steht deshalb **vor** den übrigen. Wo in der Reihe etwas steht, kann nur der entscheiden, der die Reihe schreibt. Auf der Startseite bleibt es ein Überzug — dort gibt es keine Navigationsleiste. Dazu: die Zuordnung Mediathekenart → Symbol stand dreimal im Projekt, eine davon falsch (jede Mediathek bekam „film", auch eine mit Serien). Jetzt einmal, neben den übrigen Ableitungen aus den Modellen. Geprüft im Simulator: „Bibliothek" öffnet sofort das Raster, die Leiste wechselt zwischen Filme, Serien, Gemischtes und Live-TV, und das Monogramm steht ganz rechts in der Werkzeugleiste. iOS und tvOS bauen, 469 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0aa11e1c25 |
Die Einstellungen verlassen die Leiste — oben rechts steht jetzt das Konto
Auf dem iPhone belegten die Einstellungen einen von fünf Plätzen in der unteren Leiste, gleichrangig neben Start und Bibliothek. Das unterstellt, dass man zwischen ihnen hin und her wechselt, und das tut niemand. Jetzt stehen dort drei Einträge — Start, Bibliothek, Downloads — und der Weg in die Einstellungen führt über das Monogramm oben rechts. Auf dem iPad und am Mac bleibt alles, wie es war: dort hat die Seitenleiste Platz, und den Weg über das Konto gibt es ohnehin schon. Drei Entscheidungen, die nicht offensichtlich sind: **Ein Überzug, kein Werkzeugleisten-Knopf.** Die Startseite schaltet ihre Navigationsleiste ganz ab — sie war leer und belegte trotzdem Höhe —, ein `ToolbarItem` erschiene dort gar nicht. Der Knopf liegt deshalb einmal über allen Bildschirmen statt dreimal in deren Leisten und sitzt überall an derselben Stelle. Beim Scrollen blendet er sich aus, wie die Insel auf dem iPad: über einem Film, den man gerade sucht, hat ein Kontoknopf nichts verloren. **Ein Blatt, kein Bereich.** `SettingsView` ist ein Formular aus geschobenen Unterbildschirmen und braucht nur einen Navigationsstapel um sich; die Medienziele aus `mediaStack` ruft es nie auf. Beim Schliessen steht man wieder dort, wo man war. **Ein Buchstabe, kein Bild.** Den Avatar gab es schon — er steckte in der Kontozeile der Seitenleiste und wird jetzt von beiden benutzt. Ein Serverbild führt das Konto heute nirgends mit, weder bei Jellyfin noch bei Plex; eine graue Silhouette, die bei jedem gleich aussieht, sagt weniger als der Anfangsbuchstabe. Dazu gehört `.settings` in die Rückfallliste beim Grössenwechsel: wer am iPad in den Einstellungen steht und das Fenster schmal zieht, stünde sonst vor einem Bereich, den die Leiste gar nicht mehr führt. Geprüft im Simulator: drei Einträge unten, Monogramm oben rechts, Tippen öffnet die Einstellungen, Schliessen führt zurück. iOS und tvOS bauen, 469 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
19c9dc843c |
„Aus Weiterschauen entfernen" tut jetzt etwas — und man kann es sehen
Der Knopf war wirkungslos, und zwar aus zwei Gründen, die beide auf
dasselbe hinauslaufen: er war nie geprüft worden.
**Der Weg war falsch.** Die Methode las erst ein Benutzerdatenblatt über
`GET /Users/{id}/Items/{id}/UserData`, änderte es und schrieb es zurück.
Diese Stelle war die **einzige im ganzen Projekt**, die ein solches Blatt
eigens liest — überall sonst kommt es am Titel mit. Es gab also kein
zweites Beispiel, an dem der Weg je funktioniert hätte, und ein 404
beendete die Sache, bevor überhaupt etwas geschrieben wurde.
Jetzt wird das Blatt **gebaut**, nicht erfragt: „gesehen", „Favorit" und
die Zählung stehen am Titel, den der Aufrufer ohnehin in der Hand hat —
deshalb nimmt `removeFromContinueWatching` jetzt den Titel statt nur
seiner Kennung. Gelesen wird trotzdem noch, aber unverbindlich: gelingt
es, bleiben auch Felder erhalten, die diese App gar nicht kennt. Und
geschrieben wird über beide Wege — erst der gebündelte
`/UserItems/{id}/UserData`, bei einem 404 der ältere. Welcher gilt, hängt
an der Serverfassung.
**Und man konnte es nicht sehen.** Die Attrappe kannte die Route nicht;
ihr Auffangzweig antwortete auf jeden Schreibversuch mit Status 200, und
die Weiterschauen-Reihe war fest verdrahtet. Der Aufruf gelang also immer
und änderte nie etwas — genau das Bild eines kaputten Knopfes, nur aus
einem anderen Grund. Genau deshalb ist der Fehler durchgerutscht.
Sie führt jetzt echte Benutzerdatenblätter, beantwortet beide Wege und
liefert unter „Weiterschauen" nur noch, was eine gemerkte Stelle grösser
null hat. Unter `/__userdata` steht, was geschrieben wurde und wohin.
Gemessen im Simulator, nach einem Tipp auf den Eintrag:
/UserItems/m2/UserData
{"Played":false,"IsFavorite":false,"PlaybackPositionTicks":0,…}
Danach ist „Weiterschauen" leer und die Reihe verschwindet von der
Startseite.
**Zwei Dinge, die danach trotzdem auffallen können**, und beide sind
keine Fehler: Jellyfins „Als Nächstes" führt angefangene Folgen mit, eine
entfernte Folge kann dort wieder auftauchen. Und bei zusammengelegten
Reihen stehen auch Folgen ohne gemerkte Stelle — dort gibt es nichts zu
entfernen, und der Eintrag fehlt.
Plex bleibt unberührt: der Weg dort war in Ordnung und bekommt nur die
neue Form.
iOS und tvOS bauen, 469 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
ac62e8bf19 |
Der Aufmacher war weg, weil die Mediathek unterwegs verlorenging
Zwei Schalter, die im selben Bildschirm untereinander stehen, ergaben zusammen gar keinen Aufmacher mehr: „Gesehenes aus Neuzugängen ausblenden" und eine angehakte Mediathek. Der Grund steht in `HomeFeed.prepared`. Der Zweig für das Ausblenden baut die betroffenen Reihen neu — und baute sie **ohne `libraryID`** neu, weil der neue Parameter dort nicht mitgegeben war. Getroffen hat es dabei ausgerechnet die Reihen `latest.<mediathek>`, die als einzige überhaupt eine tragen. Danach warf der Filter des Aufmachers jede Reihe ohne Mediathek weg, und übrig blieb nichts. Dasselbe an zweiter Stelle: `combiningContinueRows` verlor `libraryID` **und** `moreKey` — Letzteres ein eigener, älterer Fehler, durch den bei zusammengelegten Weiterschauen-Reihen „Alle anzeigen" wegfiel. **Dazu eine Regel, die ich zurücknehme.** Gestern stand hier: trifft die Auswahl nichts, bleibt nichts. Das war als Ehrlichkeit gedacht und ist in der Praxis der schlechtere Fehler — ein Aufmacher, der lautlos verschwindet, wird überall gesucht, nur nicht in einer Einstellung zwei Bildschirme weiter. Trifft die Auswahl keine einzige Reihe, gilt sie jetzt als wirkungslos. Das deckt auch den Fall ab, dass eine gespeicherte Kennung nach einem Serverwechsel ins Leere zeigt. Und damit solche Kennungen nicht unsichtbar weiterwirken, werden sie beim Öffnen des Bildschirms gegen die vorhandenen Mediatheken abgeglichen — vorher standen sie in keiner Zeile und liessen sich nicht abwählen. Geprüft im Simulator: „Gesehenes ausblenden" an, „Filme" angehakt — der Aufmacher ist da und zeigt einen Film. Vorher: nichts. iOS und tvOS bauen, 469 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3a6a6c4242 |
Intro und Abspann bei Jellyfin, und drei Dinge aus dem Gebrauch
**Intro überspringen gab es für Jellyfin nie.** Der Knopf, die Marken,
das automatische Überspringen, die Einstellungen dazu — alles stand seit
Langem im Player und auf dem Fernseher. Nur kam bei Jellyfin nie eine
Marke an: `JellyfinItem+Media.swift` setzte `markers: []`, weil Jellyfin
sie nicht am Titel liefert.
Seit 10.10 gibt es `/MediaSegments/{itemId}`, gefüllt von einem Anbieter
im Server wie dem Intro-Skipper. Die Anfrage läuft **neben** dem Abruf
des Titels, nicht danach — sonst wartete die Detailseite zweimal.
Zwei Dinge, die sonst eine Stunde kosten: `includeSegmentTypes` muss
**zweimal** stehen, nicht einmal mit Komma (mit Komma antwortet Jellyfin
mit null Treffern), und die Abfrage wirft nicht. Ein Server ohne Anbieter
antwortet leer, einer vor 10.10 mit einem 404 — und ein Film ohne Intro
ist kein Fehler, sondern der Normalfall.
Geprüft im Simulator: die Attrappe meldet jetzt Segmente, und im Player
steht „Intro überspringen".
---
**„Aus Weiterschauen entfernen".** Eine halbe Stunde gesehen und keine
Lust mehr — der Titel ist weder gesehen noch ungesehen, er soll nur nicht
ewig oben stehen. Beide Server können es, auf verschiedenen Wegen:
- **Plex** hat einen eigenen: `PUT /actions/removeFromContinueWatching`.
**Nicht** `/:/unscrobble` — das setzt zugleich auf ungesehen.
- **Jellyfin** hat keinen. Was es gibt, ist das Benutzerdatenblatt; die
Liste führt, was eine gemerkte Stelle grösser null hat. Also wird das
Blatt gelesen, bekommt eine Null und geht so, wie es war, zurück.
**Erst lesen, dann schreiben**, weil dort auch „gesehen", „Favorit" und
die Bewertung stehen — wer seinen Favoriten verliert, weil er einen
Film aus einer Liste genommen hat, sucht den Grund nie dort.
Der Eintrag erscheint nur, wenn es eine gemerkte Stelle gibt.
---
**Die Mediatheken für den Aufmacher taten zu wenig.** Die Regel „Reihen
ohne Mediathek bleiben immer drin" war freundlich gemeint und falsch: bei
„nur Filme" stand weiter eine Serie oben, weil sie aus „Weiterschauen"
kam. Gemessen und nicht vermutet — es lag nicht am Zeitfenster der
Auswahl, wie zuerst angenommen.
Jetzt gilt: ist etwas gewählt, speist sich der Aufmacher **nur** daraus.
Wer Weiterschauen oben haben will, wählt keine Mediathek. Der Hinweis
unter der Auswahl sagt das auch.
---
**Der „Bearbeiten"-Knopf stand über allem und tat dort nichts.** Er gilt
nur für die Reihen; jetzt steht er an deren Überschrift.
---
Die Attrappe kann drei Dinge mehr, sonst wäre nichts davon prüfbar:
Segmente, `parentId` bei den Neuzugängen (vorher stand in jeder Mediathek
dasselbe) und damit überhaupt eine Mediathekenauswahl, die man ansehen
kann.
iOS und tvOS bauen, 468 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
dc93d6e22e |
Der Aufmacher lässt sich auf Mediatheken einschränken
Mehrfachauswahl in den Einstellungen unter „Startseite": welche Mediatheken den Aufmacher speisen dürfen. Haken statt Auswahlliste — eine Liste könnte nur eine, und „keine" wäre dort ein Eintrag, den man versehentlich trifft. Dafür musste eine Reihe sagen können, woher sie kommt: `MediaRowGroup` trägt jetzt eine `libraryID`. Jellyfin setzt sie an den „Neu in …"-Reihen — die baut die App ohnehin je Mediathek. Zwei Regeln, und beide sind Entscheidungen, keine Nebenwirkungen: **Leer heisst alle.** Wer nichts ankreuzt, meint „mir ist es gleich", nicht „nimm nichts". Ein leerer Aufmacher wäre der Fehler, den niemand mit dieser Einstellung in Verbindung brächte. **Reihen ohne Mediathek bleiben immer drin.** Weiterschauen, Als Nächstes und Favoriten gehen quer durch alle, und aus welcher Mediathek eine angefangene Folge stammt, sagt der Server an keiner Stelle. Sie wegzuwerfen hiesse, jemandem das Angefangene zu nehmen, weil er „Musikvideos" abgewählt hat. Der Hinweis unter der Auswahl sagt das auch dem, der nicht in den Quelltext sieht. Eingeschränkt wird **nur der Aufmacher**, nicht die Startseite: die Reihe selbst bleibt stehen. Wer sie ganz loswerden will, blendet sie aus — das ist eine andere Frage und hat ihren eigenen Schalter. Fünf Tests an `HeroSelection.rows(_:limitedTo:)` halten die Regeln fest, darunter der Fall einer gespeicherten Kennung, die es nicht mehr gibt. **Plex bleibt aussen vor:** dort kommen die Reihen als fertige Hubs vom Server, und `librarySectionID` wird in `PlexHub` nicht gelesen. Die Auswahl schränkt dort also nichts ein. Nachzuholen, wenn Plex wieder dran ist. iOS und tvOS bauen, 468 Tests grün. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
3f680c7426 |
Gesehen und Favorit direkt an der Kachel — und die Pinch-Geste greift wieder
Drei Dinge aus dem Gebrauch.
**Das Kontextmenü einer Kachel trug genau einen Eintrag.** „Abspielen",
sonst nichts; der Kommentar daneben nannte es seit Längerem den
natürlichen Ort für „Als gesehen markieren". Jetzt steht es dort —
zusammen mit Favorit und Sternen, soweit der Server sie kennt. Es sind
dieselben Einträge wie im Menü der Detailseite: `ItemActionEntries` wird
von beiden benutzt, statt sie zweimal zu beschreiben.
Zwei Dinge daran sind nicht offensichtlich. Der Inhalt eines
Kontextmenüs lebt nur, solange das Menü offen ist — ein `@State` darin
könnte nichts anzeigen, was nach dem Tippen passiert. Ausgeführt wird
deshalb von der Kachel, die länger lebt, und dort hängt auch die
Fehlermeldung. Und danach muss die Seite neu laden: eine abgehakte Folge
gehört nicht mehr in „Weiterschauen", und der Fortschrittsbalken stimmte
sonst nicht mehr. Dafür gibt es `flixyItemChanged` in der Umgebung —
sonst müsste der Rückweg durch jede Zwischenebene gereicht werden.
**Die Pinch-Geste griff fast nie.** Sie hing an der Bildfläche, und über
der liegen die Sprungflächen — und sobald man einmal getippt hat, die
ganze Bedienung. Eine Zwei-Finger-Geste bildet sich aber nur, wenn
**beide** Finger auf der Ansicht landen, die sie trägt; in dem schmalen
Streifen dazwischen gelang das kaum. Sie hängt jetzt als
`simultaneousGesture` am ganzen Player: sie erkennt mit, statt
dazwischenzugehen, und Tippen, Doppeltippen und Scrubber bleiben
unberührt.
Gemessen im Simulator, Querformat auf einem Pro Max, mit einer echten
Zwei-Finger-Geste:
auseinander 956 × 440 pt, Rand 0,0 / 0,0
zusammen 782 × 440 pt, Rand 87,0 / 87,0
**Der Drehknopf zeigte ein Telefon.** Das las sich als Zustandsanzeige —
„so liegt es" — statt als Knopf. Zwei Pfeile im Kreis sagen, was
passiert.
iOS und tvOS bauen, 463 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
46b0117984 |
Füllend heisst füllend — der schwarze Streifen unten ist weg
Im Füllmodus wurde die Bildebene unten um den Sicherheitsrand eingerückt,
damit die Untertitel nicht unter die Bildschirmkante geraten. Das stand
dort unbedingt — auch wenn gar kein Untertitel lief. Übrig blieb dann ein
schwarzer Streifen unter einem Bild, das gerade füllen soll: genau das
Gegenteil dessen, wofür der Knopf da ist.
Der Einzug hängt jetzt daran, ob überhaupt eine Untertitelspur gewählt
ist. Ohne Untertitel reicht das Bild bis an alle vier Kanten, mit
Untertiteln bleibt die Schrift gerettet.
Gemessen auf einem Pro Max, füllend, ohne Untertitel:
hochkant 440 × 956 pt Rand 0,0 / 0,0 / 0,0 / 0,0
quer 956 × 440 pt Rand 0,0 / 0,0 / 0,0 / 0,0
`PlayerModel.subtitleBottomInset` fällt weg. Die Eigenschaft beschrieb
diese Rechnung, las sie aber niemand — die Ansicht rechnete selbst. Eine
Beschreibung, die nichts beschreibt, zeigt beim nächsten Mal in die
falsche Richtung.
**Nicht geprüft:** wie es mit eingeschalteten Untertiteln aussieht. Die
Attrappe liefert keine Untertitelspur, und ohne eine lässt sich der Fall
im Simulator nicht herstellen. Der Weg ist unverändert geblieben, aber
belegt ist er damit nicht.
iOS und tvOS bauen, 463 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
9e7f3393b4 |
DTS, TrueHD und Vorbis gehen jetzt an den Server statt an VLC
Bisher entschied der Decider: steht nur die Schachtel im Weg, wird
umgepackt — stimmt ein Codec nicht, geht es an VLC. Das war beim Ton zu
grob gedacht.
Beim Umpacken nennt das Geräteprofil dem Server, was ankommen darf:
"VideoCodec": "h264,hevc"
"AudioCodec": "aac,ac3,eac3,mp3"
Trifft ein Strom eine dieser Angaben, kopiert der Server ihn; trifft er
sie nicht, rechnet er **genau diesen einen** um. Eine Matroska mit HEVC
und DTS braucht also kein VLC: das Bild geht Byte für Byte durch, und nur
die Tonspur wird nach AC3 oder AAC kodiert. Das kostet Tonqualität und
sonst nichts.
Die Bedingung heisst deshalb nicht mehr „nur die Schachtel", sondern
„das Bild bleibt": Container und Ton dürfen im Weg stehen, der
Videocodec nicht. Dazu musste die Prüfung der Schachtel aufhören, sich
immer zu stellen — eine MP4 mit DTS stand mit einem reinen Tonhindernis
da und fiel trotzdem durch die Liste umpackbarer Container.
**Der grössere Gewinn liegt auf dem Apple TV.** Dort gibt es VLC nicht,
und bisher landete eine Datei mit DTS-Ton beim vollen Neurechnen — Bild
inbegriffen, obwohl mit dem Bild nichts war. Jetzt bleibt das Bild
unberührt. Auf iPhone, iPad und Mac verhält sich derselbe Film jetzt
gleich, und zwar über AVPlayer: mit Bild-in-Bild, AirPlay und
Sperrbildschirm, die es auf dem VLC-Weg nie gab.
Geprüft im Simulator gegen die Attrappe mit `--vlc` (meldet DTS): der
Film läuft, und in der Leiste steht jetzt der AirPlay-Knopf, der vorher
fehlte — der sichtbarste Beleg dafür, dass AVPlayer spielt und nicht VLC.
„Umgepackt" und „umgepackt, Ton umgerechnet" sind in den technischen
Infos und in der Diagnose auseinandergehalten. Ein Weg, der den Ton neu
kodiert, darf nicht „ohne Neukodierung" heissen.
Was bleibt bei VLC: nicht unterstützte **Bild**codecs, allen voran AV1,
und Dateien, die der Server noch nicht untersucht hat. Dafür gäbe es nur
das volle Neurechnen — eine eigene Entscheidung, nicht diese.
iOS und tvOS bauen, 463 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
3ed682dd00 |
Der Sicherheitsrand am Bild war falsch — er schützte vor nichts
Eingepasst bekam das Bild ein Polster in Grösse des Sicherheitsrandes,
damit es nicht unter die Dynamic Island läuft. Nachgerechnet hält das
nicht.
Querformat auf einem Pro Max: 956 × 440 Punkt. Ein Bild in 16∶9 wird bei
voller Höhe 782 Punkt breit — es bleiben 87 Punkte Balken auf jeder
Seite, und die Island braucht 62. Das Bild kommt ihr von selbst nicht
nahe. Das Polster hat also nichts abgewehrt und dabei vierzig Punkte
Höhe gekostet, also jedes zehnte Bildpunktband:
vorher 695,7 × 400 pt Balken 130,0 / 130,3
jetzt 765,3 × 440 pt Balken 95,3 / 95,3
Beides über VLC gemessen, dieselbe Datei, dieselbe Lage.
Erst was breiter ist als der Schirm (2,17∶1) läuft an die Kanten — bei
21∶9 über die ganze Breite, und dann liegt ein Streifen hinter der
Island. Dafür ist der Zoomknopf da. Ein Film, der nie an den Rand darf,
wäre der schlechtere Tausch: er kostet bei *jeder* Datei Höhe, um bei
wenigen einen Streifen zu retten.
`EdgeInsets.symmetrical` fällt damit weg, samt seiner fünf Tests. Es
hatte genau eine Aufgabe — ein unsymmetrisches Polster mittig zu halten
—, und das Polster gibt es nicht mehr. Eine geprüfte Funktion, die
niemand ruft, ist trotzdem nur Ballast.
iOS und tvOS bauen, 460 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b0db4f8f5f |
Bei VLC sass das Bild nie richtig — und der Knopf wirkte nie
Zwei Fehler, die dieselbe Ursache haben: alles, was über VLC läuft, ging
an der Bildebene vorbei, an der die App gedreht hat.
**Das Bild sass nicht mittig.** Nicht ein paar Punkte daneben — gemessen
eine Anzeigefläche von 1200×480 Punkt in einer Zeichenfläche von 711×400,
unten rechts angeschlagen, im falschen Seitenverhältnis. Der Grund steht
im Vertrag von `VLCDrawable`: VLCKit fragt die Grösse **einmal** beim
Anlegen ab und überlässt seine Anzeigefläche danach der
Autoresizing-Maske. Eine Maske hält aber den Unterschied fest, nicht das
Verhältnis — was beim Anlegen zu gross war, bleibt für immer um denselben
Betrag zu gross. Und angelegt wird sie mitten in der Drehung ins
Querformat, während SwiftUI sie einblendet: also regelmässig falsch.
Die Zeichenfläche ist jetzt eine eigene Klasse, die ihrer Anzeigefläche
bei jedem Layout wieder die eigene Grösse gibt. Danach gemessen: 695,7 ×
400 Punkt, Rand links 130,0, rechts 130,3 — mittig.
**Der Knopf für einpassen/füllen tat nichts.** Er schrieb `videoGravity`
auf eine `AVPlayerLayer`, die bei VLC gar nicht im Baum hängt. Die
Einstellung geht deshalb jetzt durch die Engine: `setVideoFit` am
Protokoll, beim Systemplayer eine Voreinstellung, die nichts tut — dort
macht es die Bildebene —, bei VLC `videoFitMode`. `smaller` heisst bei
libvlc „fit inside", `larger` „fit outside".
Gemessen, im Querformat auf einem Pro Max, über VLC:
eingepasst 695,7 × 400 pt Rand l/r 130,0/130,3 o/u 20,0/20,0
füllend 935,3 × 440 pt Rand l/r 10,3/ 10,3 o/u 0,0/ 0,0
**Zurückgenommen:** im Kommentar an `videoFit` stand, ein `didSet` auf
einer `@Observable`-Klasse werde nicht gemeldet. Das stimmt nicht —
`HomeLayoutSettings` lebt davon. Ein Kommentar, der einen falschen
Mechanismus behauptet, schickt den nächsten in die falsche Richtung;
jetzt steht der belegte Grund dort.
Dazu, damit dieser Weg überhaupt prüfbar ist: die Attrappe meldet mit
`--vlc` DTS-Ton. AVFoundation kann das nicht, also fällt die App auf
VLCKit zurück. Die Beispieldatei bleibt dieselbe — geprüft wird der Weg,
nicht der Ton. Ohne diesen Schalter ist der VLC-Zweig im Simulator gar
nicht zu erreichen, und genau deshalb lagen beide Fehler so lange da.
Ebenfalls dabei: `EdgeInsets.symmetrical` mit fünf Tests und das Polster
im Player, das damit nicht mehr verschiebt statt einrückt.
iOS und tvOS bauen, 465 Tests grün.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
51abcabb27 |
Die Rechnung nachgetragen — und eine falsche Erklärung zurückgenommen
Eine unabhängige Gegenprüfung (zweiunddreissig Agenten, jede Behauptung
von Skeptikern angegriffen) bestätigt die Ursache und rechnet alle vier
gemessenen Zahlen nach:
scrollbarer Teil 421,0 (8 Zeilen à 36,33 + Überschrift 22,33
+ 8 Zwischenräume à 12 + 12 unten)
starrer Rest 137,6 (Kopfzeile 48,33 + Trenner 1
+ Kontozeile 64,27 + 2 × 12 aussen)
zusammen 558,6 gemessen 558,67
zentriert (420 − 558,67) / 2 = −69,335 gemessen −69,33
Die Rechnung steht jetzt im Quelltext, damit beim nächsten Eintrag in der
Leiste jemand nachsehen kann, wie viel Luft noch ist: starr bleiben 137,6
von 420.
**Und eine Berichtigung.** Ich hatte den Zwischenwert 620,67 einer
Rückkopplung zugeschrieben — gemessener Rand geht über `topInset` in
einen `safeAreaInset` und verändert den Rand. Das stimmt nicht. Die
Differenz von zweiundsechzig kommt schlicht aus
`.padding(.top, statusBarHeight)` an der Seitenleiste, das während der
Drehung noch den Hochformatwert trug.
Die Umstellung auf den Fensterwert bleibt richtig und nützlich — sie
spart eine Layoutrunde. Sie war nur nicht die Reparatur, und der
Kommentar sagte etwas anderes. Ein Kommentar, der einen falschen
Mechanismus behauptet, ist schlimmer als keiner: er schickt den nächsten
in die falsche Richtung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
5f635c699a |
Die Seitenleiste war zu hoch — und der Player ging deshalb hinaus
Dreimal habe ich an den Rändern geschraubt, und dreimal war es nicht das.
Der Fehler sass in der Navigation und zeigte sich im Player.
**Gemessen statt vermutet.** Auf einem iPhone Pro Max im Querformat:
Fenster 956×440, Rand 0,62,62,20 → erwartet 832×420
Wurzel 832×558,67 @62,−69,33 → 138,67 zu hoch
Der Wurzelbaum ist zu gross für den Vorschlag und wird darin zentriert;
(420 − 558,67) / 2 ergibt exakt −69,33. Der Player liegt als Überlagerung
darin und erbt den Kasten — oben fielen neunundsechzig Punkte heraus,
unten neunundvierzig. Genau dort sitzen Kopfzeile und Zeitangaben.
**Die Ursache:** `FlixySidebar` war eine `VStack` ohne Bildlaufansicht,
mit dem Kommentar „die Einträge sind wenige und passen". Auf einem iPad
und im Hochformat stimmt das. Ein grosses iPhone ist quer
vierhundertvierzig Punkte hoch, und die Leiste braucht mit Kopf, Gruppen,
Trenner und Kontozeile gut fünfhundertdreissig. Eine `VStack` schrumpft
dann nicht — sie überragt, der `HStack` nimmt das Maximum, und alles
darüber wächst mit.
Deshalb war es nur dort zu sehen: nur auf einem grossen iPhone ist die
Anordnung quer `regular`, und nur dann gibt es überhaupt eine
Seitenleiste. Auf einem 17 Pro ist quer `compact` — dort stimmte jede
Messung, und genau deshalb konnte ich es dreimal nicht nachstellen.
Jetzt scrollt die Leiste, wenn sie muss (`basedOnSize`). Wo die Einträge
passen, sieht man keinen Unterschied. Nachgemessen: Wurzel 832×420,
Rahmen des Players 956×440 @0,0 — deckungsgleich mit dem Fenster.
**Mitgenommen:** `statusBarHeight` kam aus `proxy.safeAreaInsets.top` und
ging über `topInset` in einen `safeAreaInset` am selben Baum zurück. Das
Layout lief dadurch in Runden auf die richtige Grösse zu, jede Runde
zweiundsechzig Punkte. Der Wert kommt jetzt aus dem Fenster, das ihn ohne
Rückkopplung kennt — eine Runde statt drei.
`ScreenSafeArea` liegt dafür nicht mehr beim Player, sondern bei den
Modellen: es beantwortet jetzt für zwei Stellen dieselbe Frage.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
7bcef03c25 |
Eingepasst heisst ganz zu sehen — und der Rahmen wird mitgemessen
**Das Bild bleibt im sicheren Bereich, wenn es eingepasst ist.** Vorher lief es bis an die Bildschirmkante und damit unter die Dynamic Island — im Querformat frisst sie eine Ecke des Films. „Einpassen" und „ein Stück fehlt" schliessen sich aus. Das kostet quer gut hundert Punkte Breite; wem das zu klein ist, legt mit zwei Fingern auf **füllen** um, wo es bewusst bis an die Kanten läuft und nur der Untertitel ausgespart bleibt. **Und die Diagnosezeile misst jetzt den Rahmen mit.** Das ist die Frage, an der alles hängt, und ich habe sie zweimal übersprungen: ist der Rahmen grösser als der Bildschirm, liegt es nicht an den Rändern — dann wird der Player in einen zu grossen Kasten gelegt und mittig darin ausgerichtet, und oben wie unten fällt gleich viel heraus. Genau so sieht es aus, wenn die Kopfzeile fehlt **und** die Zeitangaben fehlen, während die Knöpfe in der Mitte stehen. Der Verdacht, warum ich es nie nachstellen konnte: die Bildschirmfotos sind 2796×1290, also ein Pro Max. Der ist im Querformat `regular` und nimmt damit in `RootView` einen **anderen Zweig** — Seitenleiste statt Leiste unten, mit `safeAreaInset` und `ignoresSafeArea(.container)`. Mein Simulator ist ein 17 Pro und läuft quer über den anderen Zweig. Belegt ist das noch nicht; die Zeile wird es sagen. Im Simulator steht dort „Rahmen 874×402 @0,0 · Schirm 874×402" — gleich gross, Ursprung in der Ecke, alles liegt drin. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c7b901b103 |
Der Player sagt jetzt, was er über seinen Platz weiss
Zweimal habe ich behauptet, die Ränder seien behoben, und zweimal sassen die Knöpfe weiter ausserhalb. Im Simulator steht dort „SA 0,62,62,20 · Schirm 874×402" und alles liegt drin — nachstellen lässt es sich hier also nicht. Damit ist Raten der falsche Weg. **Eine Zeile im Entwicklerbau**, unter dem Schieberegler: die Ränder, wie sie ankommen, die Bildschirmgrösse daneben und der Skalierungsmodus. Sitzen die Knöpfe daneben, gibt es genau drei Möglichkeiten — die Ränder kommen als null an, sie kommen falsch an, oder sie stimmen und der Rahmen ist grösser als der Schirm. Von aussen sehen alle drei gleich aus. Die Zeile unterscheidet sie. **Und eine Härtung, die beim Nachlesen auffiel.** `ScreenSafeArea` suchte nur die Szene, die `foregroundActive` ist. Genau die gibt es in zwei Momenten nicht: während einer Drehung und kurz nach dem Zurückholen der App. Wer dann fragt, bekommt `nil`, daraus werden Ränder von null — und die bleiben stehen, bis sich die Fläche wieder ändert. Die Knöpfe sitzen dann dauerhaft im Home-Indikator, obwohl die Rechnung stimmt. Ein Fenster mit den richtigen Rändern ist in dem Moment trotzdem da, es ist nur nicht „aktiv". Jetzt der Reihe nach: aktiv, sonst sichtbar, sonst irgendeines. Dazu wird beim Zurückholen der App nachgefasst — eine Drehung im Hintergrund ändert die Fläche, ohne dass die Ansicht davon etwas merkt. Ob das der Fehler war, weiss ich nicht. Die Zeile wird es sagen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
41719595b9 |
Mediatheken nach oben — und das Regal sagt endlich, was es sieht
**Die Mediatheken sind Reiter geworden.** Sie standen unten auf der Startseite, und dorthin musste man erst scrollen — an allen Vorschlägen vorbei, obwohl „ich will in die Filme" genau der Fall ist, in dem einem die Vorschläge egal sind. Damit fällt die Reihe unten weg: zwei Wege zum selben Raster sind auf einem Fernseher kein Komfort, sondern eine Frage, welcher der richtige ist. Die Leiste liest sich jetzt „Start · Filme · Serien · Gemischtes · Suchen" und wird von tvOS mittig gehalten, egal wie viele Mediatheken dazukommen. Die Stelle der Suche bestimmt Apple, nicht diese Datei: `role: .search` setzt sie dorthin, wo sie in jeder anderen App auch steht, und gibt ihr die Lupe. **Und eine Statuszeile fürs Regal.** Bleibt es leer, gibt es genau drei Möglichkeiten: die App-Gruppe fehlt, die Datei ist nicht da, oder sie ist da und das System fragt trotzdem nicht. Von aussen sehen alle drei gleich aus — nämlich nach dem grossen Plakat aus dem Asset-Katalog. Nach zwei Runden Vermuten steht unten links jetzt, welcher Fall es ist: „3 Reihen, 9 Einträge, zuletzt 21:55" oder eben „Keine App-Gruppe". Nur im Entwicklerbau (`#if DEBUG`) und als Überlagerung, nicht als letzte Zeile der Liste: dorthin müsste man erst an allen Reihen vorbeiscrollen, und wer nachsieht, warum das Regal leer ist, soll es sofort sehen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
08514eb564 |
Und iOS wieder gebaut — TVServices gibt es dort nicht
Mein Fehler: `TopShelfWriter` liegt in `Flixy/Session/`, und den Ordner übersetzen beide Ziele. Mit `import TVServices` brach damit das iPhone-Ziel ab. Die ganze Datei steht jetzt in `#if os(tvOS)`. Ein `#if` um den einen Import hätte nicht genügt: der Abhängigkeits-Sucher von Clang prüft ihn trotzdem und bricht mit „Unable to resolve module dependency: TVServices" ab, ohne den Fernseher zu erwähnen — dieselbe Falle wie vorhin bei VLCKit. Committet hatte ich vorher, ohne iOS zu bauen. Das war der eigentliche Fehler. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
47c93d89bb |
Niemand hat dem System gesagt, dass es neu fragen soll
Das Regal zeigte weiter das grosse Plakat aus dem Asset-Katalog, obwohl die Datei daneben längst drei Reihen enthielt. Der Grund steht in Apples eigenem Header, zwei Zeilen über der Methode, die wir überschreiben: > If the completion handler is not called in a timely manner or is called > with empty content the system will display the static image instead. Genau das war zu sehen. Und darunter steht, was fehlte: > + (void)topShelfContentDidChange; > Can be called from an application or extension to tell the system that > your Top Shelf content has changed. Die Erweiterung kann sich nicht selbst anstossen — sie läuft nur, wenn sie gefragt wird. Gefragt wird sie, wenn die App es sagt. Die App sagt es jetzt, unmittelbar nachdem sie den Abzug geschrieben hat. Ausgeschlossen, bevor ich hier landete, und zwar nachgemessen statt vermutet: das System kennt die Erweiterung (`pluginkit` listet sie unter `com.apple.tv-top-shelf`), die Hauptklasse ist im Binärcode auffindbar (`_TtC13FlixyTopShelf16TopShelfProvider`), die Gerätefamilie stimmt, und die Datei liegt mit drei Reihen in der App-Gruppe. Nicht nachgewiesen: dass es jetzt zeichnet. Die Erweiterung startet nur, wenn das System sie fragt, und das tut es nur, wenn die App im Menü oben steht und fokussiert ist — beides kann ich im Simulator nicht auslösen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |