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>
263 lines
11 KiB
Markdown
263 lines
11 KiB
Markdown
> **Nachtrag vom selben Tag: das Projekt heisst inzwischen Flaro.**
|
|
>
|
|
> „Halo" liess sich in App Store Connect nicht eintragen — der Name ist von
|
|
> KaVo-ähnlichen Fällen abgesehen vor allem eine Marke von Microsoft, und der
|
|
> Upload scheiterte daran. Dieses Dokument bleibt im Wortlaut stehen: es ist
|
|
> ein Protokoll der Entscheidungen, nicht ihre Fortschreibung. Überall, wo
|
|
> hier „Halo" steht, heisst es heute „Flaro"; die Begründungen gelten
|
|
> unverändert.
|
|
|
|
# Halo-Server — Entwurf
|
|
|
|
*3. Oktober 2026*
|
|
|
|
## Warum es ihn gibt
|
|
|
|
Ein Download brach die Sprachwahl. Jellyfins Transcode-Endpunkt bildet **eine
|
|
Tonspur je Sitzung** ab; was offline landet, ist einspurig, und welche Spur es
|
|
ist, entscheidet der Server. Einmal war es die Audiodeskription — drei Gigabyte
|
|
Mobilfunk für eine Datei, die niemand hören wollte.
|
|
|
|
Am Server gemessen, nicht vermutet:
|
|
|
|
- `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 führt **eine** Variante und **keine** `EXT-X-MEDIA`-Tonspuren.
|
|
- Beim Umpacken verliert Jellyfin die Sprachkennungen: alle Spuren kommen als
|
|
`und` an.
|
|
|
|
Das liegt weder an MP4 — das trägt mehrere Tonspuren — noch an AVFoundation.
|
|
Es liegt am Transcoder. Ein Jellyfin-Plugin könnte es beheben; die Entscheidung
|
|
fiel für einen eigenen Server.
|
|
|
|
**Was Halo werden soll:** ein Server, der *„eigentlich nichts mit Jellyfin zu
|
|
tun hat, aber grundsätzlich die gleichen Funktionen bietet"*. Funktionsgleich,
|
|
unabhängig, ohne Nachziehen bei fremden Veröffentlichungen. Open Source, für
|
|
jeden, nicht für einen Haushalt.
|
|
|
|
---
|
|
|
|
## Entscheidungen
|
|
|
|
| | | Preis, der bewusst gezahlt wird |
|
|
|---|---|---|
|
|
| **Sprache** | C# / .NET 10 | — |
|
|
| **Schnittstelle** | eigene, von Anfang an | Die App braucht ein eigenes Backend-Modul; bis es steht, gibt es keinen Zwischenstand zum Ausprobieren |
|
|
| **Datenbank** | SQLite | Schreibzugriffe laufen nacheinander. Bei 40 000 Titeln und laufendem Scan spürbar — Jellyfins bekanntester Schmerzpunkt |
|
|
| **Metadaten** | TMDB | Jeder Betreiber braucht einen eigenen Schlüssel; gepflegte Bestände werden unnötig neu nachgeschlagen |
|
|
| **Anmeldung** | Passwort, Geräte-Token, Code-Verfahren | Zwei Anmeldewege statt einem, jeder mit eigenen Sicherheitsüberlegungen |
|
|
| **Oberfläche** | Client **und** `/admin`, getrennte Bereiche, vom Server ausgeliefert | — |
|
|
| **Live TV** | HDHomeRun und IPTV (m3u + XMLTV) | Keine DVB-Karte. Wer eine hat, bekommt sie nicht unterstützt |
|
|
| **Übernahme** | keine | Jeder Umsteiger verliert Gesehen-Status und Fortschritt |
|
|
| **Laufzeit** | Docker, amd64 und arm64 | — |
|
|
| **Hardware** | VAAPI über `/dev/dri` | Keine NVENC-Unterstützung in der ersten Fassung |
|
|
| **Lizenz** | GPL-2.0-only | Siehe unten — die folgenreichste Zeile dieses Dokuments |
|
|
|
|
### Die Lizenz ist eine Bauvorschrift
|
|
|
|
GPL-2.0-only ist dieselbe Lizenz wie Jellyfin. Deren Quellcode darf also
|
|
gelesen **und übernommen** werden.
|
|
|
|
**Der Preis trifft jede Abhängigkeit, die je hinzukommt:** GPL-2.0-only ist
|
|
**unverträglich mit Apache-2.0**. Die Patentklausel der Apache-Lizenz gilt
|
|
unter GPL-2.0 Abschnitt 6 als „weitere Einschränkung". Ein nennenswerter Teil
|
|
des .NET-Umfelds steht unter Apache-2.0.
|
|
|
|
> **Regel für jede neue Abhängigkeit: MIT, BSD oder GPL-2.0-verträglich.
|
|
> Apache-2.0 ist ausgeschlossen.** Vor jedem `PackageReference` die Lizenz
|
|
> prüfen und im Commit nennen.
|
|
|
|
Microsofts eigene Pakete — ASP.NET Core, EF Core — stehen unter MIT und sind
|
|
unbedenklich.
|
|
|
|
ffmpeg wird als **eigener Prozess** aufgerufen, nicht über Bindings
|
|
eingebunden. Das hält die Lizenzen sauber getrennt und erlaubt nebenbei, die
|
|
Argumente zu protokollieren — ohne das ist „warum ruckelt das" nicht zu
|
|
beantworten.
|
|
|
|
**Die App** steht unter **MPL-2.0**, nicht unter GPL: GPL und App Store
|
|
vertragen sich nicht. Apple entfernte 2011 VLC aus dem Store, nachdem ein
|
|
Rechteinhaber sich auf Abschnitt 6 berufen hatte — Apples Bedingungen
|
|
schreiben Gerätezahl und Kopierschutz vor. MPL-2.0 ist Copyleft je Datei, ohne
|
|
Relink-Pflicht, und App-Store-tauglich.
|
|
|
|
*Umlizenzieren geht nur, solange ein einziger Rechteinhaber da ist. Sobald
|
|
Fremde beitragen, braucht jede Änderung deren Zustimmung.*
|
|
|
|
---
|
|
|
|
## Grundsätze
|
|
|
|
- **Nichts fest verdrahtet.** Jeder Pfad, jeder Port, jede Kennung aus
|
|
Konfiguration — Umgebungsvariablen und `appsettings`, beides überschreibbar.
|
|
Das Setup des Autors ist ein *Beispiel in der Dokumentation*, keine Annahme
|
|
im Code.
|
|
- **Kein Zustand im Image.** Datenbank, Konfiguration und Bilder-Cache liegen
|
|
in eingehängten Verzeichnissen.
|
|
- **Medien werden nur gelesen.** Schreibgeschützt eingehängt.
|
|
- **Mehrbenutzer von Anfang an.** Fortschritt und Gesehen-Status je Benutzer
|
|
nachträglich einzuziehen ist ein Umbau am Datenmodell, kein Zusatz.
|
|
|
|
---
|
|
|
|
## Aufbau
|
|
|
|
```
|
|
server/
|
|
Halo.sln
|
|
src/
|
|
Halo.Api/ HTTP-Schnittstelle, Authentifizierung, Auslieferung der Oberfläche
|
|
Halo.Core/ Domäne: Benutzer, Bibliothek, Fortschritt. Kennt kein HTTP
|
|
Halo.Data/ EF Core, SQLite, Migrationen
|
|
tests/
|
|
Halo.Core.Tests/
|
|
Halo.Api.Tests/
|
|
Dockerfile
|
|
docker-compose.example.yml
|
|
web/ Client (vorhanden) + /admin (neu)
|
|
```
|
|
|
|
Später, je Stufe: `Halo.Media` (ffmpeg), `Halo.Metadata` (TMDB),
|
|
`Halo.LiveTV` (HDHomeRun, m3u).
|
|
|
|
**Der Node-Proxy unter `web/server` entfällt.** Er löste CORS und das
|
|
http/https-Problem, weil Jellyfin woanders lief. Liefert Halo die Seite selbst
|
|
aus, ist er die Adresse, an der alles hängt — beide Probleme verschwinden mit
|
|
ihm.
|
|
|
|
---
|
|
|
|
## Stufen
|
|
|
|
| | Inhalt |
|
|
|---|---|
|
|
| **1** | Gerüst: Benutzer, Anmeldung, Einrichtung, Oberflächen-Gerüst, Docker |
|
|
| **2** | Bibliothek und Metadaten: Scanner, Dateinamen-Erkennung, TMDB, Bilder |
|
|
| **3** | Ausliefern: direktes Abspielen, Umpacken, VAAPI, **Download mit allen Spuren** |
|
|
| **4** | Fortschritt, Gesehen-Status, Favoriten, Vorschläge |
|
|
| **5** | Live TV: HDHomeRun, m3u mit XMLTV |
|
|
|
|
Gewählt in Kenntnis des Preises: Stufe 1 spielt noch keinen Film ab. Für ein
|
|
offenes Projekt, bei dem Fremde den Server in Betrieb nehmen müssen, ist es
|
|
trotzdem die ehrliche Reihenfolge — ein Server ohne Einrichtung ist für einen
|
|
Fremden kein Server.
|
|
|
|
### Was Stufe 3 anders macht als Jellyfin
|
|
|
|
```
|
|
ffmpeg -i datei.mkv -map 0 -c copy -c:s mov_text out.mp4
|
|
```
|
|
|
|
`-map 0` nimmt **alle** Ströme mit, `-c copy` fasst Bild und Ton nicht an. Das
|
|
kostet den Server fast nichts und liefert eine MP4 mit allen Ton- und
|
|
Textspuren — offline wählbar, wie bei jedem anderen Anbieter auch.
|
|
Sprachkennungen werden dabei ausdrücklich durchgereicht.
|
|
|
|
### Was Stufe 4 anders macht als Jellyfin
|
|
|
|
**Fortschritt ohne Schwelle.** Jellyfin merkt sich unterhalb eines
|
|
Mindestanteils und einer Mindestlaufzeit gar nichts; wer zehn Minuten eines
|
|
Films sieht und abbricht, findet beim nächsten Mal keinen Anknüpfungspunkt.
|
|
Halo speichert von der ersten Sekunde an, laufend, je Benutzer.
|
|
|
|
*Die genauen Jellyfin-Werte stehen aus dem Gedächtnis hier nicht; sie werden
|
|
vor dem Bauen gegen die laufende Installation geprüft, damit die Begründung
|
|
belegt ist und nicht behauptet.*
|
|
|
|
---
|
|
|
|
## Stufe 1 im Einzelnen
|
|
|
|
### Daten
|
|
|
|
| Tabelle | Felder |
|
|
|---|---|
|
|
| `User` | Id, UserName, PasswordHash, IsAdministrator, CreatedAt |
|
|
| `DeviceToken` | Id, UserId, TokenHash, DeviceName, DeviceId, CreatedAt, LastSeenAt, RevokedAt |
|
|
| `PendingAuthCode` | Id, Code, Handle, DeviceName, CreatedAt, ExpiresAt, ApprovedByUserId, ConsumedAt |
|
|
| `Library` | Id, Name, Kind, Path, CreatedAt |
|
|
| `ServerSetting` | Key, Value |
|
|
|
|
**Token werden gehasht abgelegt, nie im Klartext.** Was der Client bekommt,
|
|
sieht der Server danach nie wieder — gestohlene Datenbank heißt dann gestohlene
|
|
Hashes, nicht gestohlene Zugänge.
|
|
|
|
### Schnittstelle
|
|
|
|
```
|
|
GET /api/v1/system/info Fassung, ob eingerichtet werden muss
|
|
POST /api/v1/system/setup Erstes Administratorkonto — nur solange keines existiert
|
|
|
|
POST /api/v1/auth/login Benutzername und Passwort → Token
|
|
POST /api/v1/auth/logout Widerruft das eigene Token
|
|
GET /api/v1/auth/me Wer bin ich
|
|
|
|
POST /api/v1/auth/code Fernseher startet: → sechsstelliger Code + Abholschein
|
|
GET /api/v1/auth/code/{schein} Fernseher fragt nach → wartend | bestätigt + Token | abgelaufen
|
|
POST /api/v1/auth/code/{code} Telefon bestätigt (angemeldet)
|
|
|
|
GET /api/v1/users Liste (Administrator)
|
|
POST /api/v1/users Anlegen (Administrator)
|
|
GET /api/v1/libraries Bibliotheken
|
|
POST /api/v1/libraries Anlegen (Administrator)
|
|
|
|
GET /health Gesundheit, ohne Anmeldung
|
|
```
|
|
|
|
### Anmeldung mit Code
|
|
|
|
Für die Fernbedienung: Passwörter dort einzutippen ist eine Zumutung, die man
|
|
selbst jedes Mal spürt.
|
|
|
|
1. Der Fernseher fordert einen Code an und bekommt zusätzlich einen
|
|
**Abholschein**, den nur er kennt.
|
|
2. Auf dem Bildschirm steht der sechsstellige Code.
|
|
3. Am Telefon, angemeldet, wird er bestätigt.
|
|
4. Der Fernseher fragt mit seinem Abholschein nach und bekommt sein Token.
|
|
|
|
**Der Code allein genügt nie**, um ein Token abzuholen — sonst könnte jemand
|
|
sechsstellige Zahlen durchprobieren. Dazu: Gültigkeit wenige Minuten,
|
|
Versuchsbegrenzung, einmalige Verwendung.
|
|
|
|
### Einrichtung
|
|
|
|
`POST /api/v1/system/setup` arbeitet **nur**, solange kein Benutzer existiert.
|
|
Danach antwortet es ablehnend, auch unangemeldet — sonst wäre ein frisch
|
|
gestarteter Server für den ersten Fremden im Netz offen.
|
|
|
|
### Konfiguration
|
|
|
|
| Variable | Bedeutung | Voreinstellung |
|
|
|---|---|---|
|
|
| `HALO_DATA` | Datenbank und Konfiguration | `/config` |
|
|
| `HALO_CACHE` | Bilder und Zwischenstände | `/cache` |
|
|
| `HALO_TRANSCODE` | Kratzplatz fürs Umrechnen | `/transcode` |
|
|
| `HALO_PORT` | Port | `8080` |
|
|
| `TZ` | Zeitzone | `UTC` |
|
|
| `HALO_RATELIMIT_SIGNIN` | Anmeldeversuche je Minute und Adresse | `10` |
|
|
|
|
Die Benutzerkennung steht **nicht** in der Umgebung: das Image läuft als
|
|
`1000:1000`, und wer andere Kennungen hat, setzt Dockers eigenes
|
|
`user: "<uid>:<gid>"`. Ein eigenes Startskript mit `PUID`/`PGID` — wie es im
|
|
selbstgehosteten Umfeld üblich ist — bräuchte root im Container, und dafür gibt
|
|
es hier keinen Grund.
|
|
|
|
Medienpfade stehen **nicht** hier: sie werden in der Oberfläche als
|
|
Bibliotheken angelegt und liegen in der Datenbank. Wer sie in die Compose-Datei
|
|
schriebe, müsste den Container neu starten, um eine Bibliothek hinzuzufügen.
|
|
|
|
---
|
|
|
|
## Nachweis
|
|
|
|
- `dotnet test` für Domäne und Schnittstelle.
|
|
- Der Einrichtungsassistent wird gegen eine **leere** Datenbank gefahren: ein
|
|
frisch gestarteter Container führt durch die Einrichtung und legt einen
|
|
Administrator an.
|
|
- `POST /api/v1/system/setup` ein zweites Mal muss **abgelehnt** werden.
|
|
- Ein Code wird am Telefon bestätigt und am Fernseher abgeholt, ohne dass ein
|
|
Passwort getippt wurde.
|
|
- Ein Abholschein, der nicht zum Code gehört, bekommt **kein** Token.
|
|
- Das Image baut für `linux/amd64` und `linux/arm64`.
|