Files
ScarriffleandClaude Opus 5 172b38da06 Die Apps können jetzt beides: Jellyfin oder Flaro
`FlaroServerKit` steht neben `JellyfinKit`, nicht darin — genau auf dem
Steckplatz, den Plex geräumt hat. Dass `MediaBackend` zwei Server zugleich
trägt, war schon einmal bewiesen; die Naht hat gehalten, und keine einzige
Ansicht musste dafür neu geschrieben werden.

`ServerKind` ist zurück. Es wurde beim Entfernen von Plex gestrichen, weil
ein Aufzählungstyp mit einem Fall ein umständliches Bool ist. Jetzt gibt es
wieder zwei, und es ist wieder eine Unterscheidung — einer zur Zeit, nicht
beide nebeneinander.

**Flaro steht oben auf dem Anmeldebildschirm.** Nicht aus Eitelkeit: dort
bringt ein Download alle Tonspuren mit und der Fortschritt wird ab der
ersten Sekunde gemerkt. Wer Jellyfin benutzt, sucht gezielt danach — wer neu
anfängt, soll nicht in der schwächeren Hälfte landen, weil ihr Knopf
auffälliger war. Am Fernseher steht bei beiden der sechsstellige Code voran.

---

**Der Zugang geht als Cookie, nicht in der Adresse.** Jellyfin hängt sein
Token an jede Bildadresse, weil eine Bildansicht keine Kopfzeilen schickt;
es steht damit in jedem Zugriffsprotokoll und jedem weitergegebenen Link.
Flaro tut das nicht, und deshalb legt der Client sein Token selbst im
gemeinsamen Keksspeicher ab — auch beim Wiederaufnehmen aus dem
Schlüsselbund, wo gar keine Anmeldung stattfindet und der Server es nie
setzen könnte.

`URLSession` schickt es von allein mit; **AVFoundation nicht**. Der
Systemplayer bekommt die Kekse jetzt ausdrücklich. Serverneutral: wo keine
liegen, ist die Liste leer.

**`BackendCapability.allTracksDownload`** statt einer Spurwahl, die ins
Leere führt. Die Frage „welche Tonspur?" gibt es nur, weil Jellyfins
Transcoder eine je Sitzung abbildet. Bei Flaro führen beide Antworten zum
selben Ergebnis — also wird nicht gefragt, und das Umpacken mit einer
einzigen Spur entfällt gleich mit.

**Die Adressraterei wandert nach `MediaKit`.** Sechzig Zeilen sorgfältig
begründeter Reihenfolge — IP-Adressen sprechen selten HTTPS, Domänennamen
fast immer — unterscheiden sich zwischen beiden Servern nur im Standardport.
Zweimal hingeschrieben wäre die eine Fassung beim nächsten Sonderfall
nachgezogen worden und die andere nicht. Jellyfins Prüfungen laufen
unverändert gegen die verschobene Fassung; das ist der Beweis.

**Nebenbei zwei Altlasten:** `player.jellyfinPending` war unerreichbar — der
Player zeigt diese Meldung nur, wenn gar kein Server da ist, dann aber nie
die mit Jellyfin. Und `signIn.jellyfin.userName` hiess „Benutzername"; der
Schlüssel heisst jetzt so neutral wie der Text.

---

**Der Fehler, den erst der Simulator zeigte:** „Stelle wird
wiederhergestellt", 0:00, für immer.

Muss der Server umpacken, antwortet er auf den Download mit 202 und fängt
an. AVPlayer bekommt darauf kein Video, sondern eine leere Antwort — und
wartet, unbegrenzt, ohne Fehler. Die Weboberfläche fragt in einer Schleife
nach; `preparePlayback` tat es nicht. Jetzt wartet es, mit Obergrenze und
einer Begründung, wenn sie reisst. Mit Prüfung, die zwei Runden erzwingt.

413 Tests grün. Im Simulator gegen den echten Server nachgewiesen: Adresse
eintippen, Server gefunden, angemeldet, Mediathek mit Plakaten, Neustart
ohne erneute Anmeldung — und eine MKV abgespielt, die der Server dafür erst
umpacken musste. Danach stand auf dem Server: Matrix, 2,0 s von 2,0 s,
gesehen, ein Durchlauf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 22:00:02 +02:00

330 lines
13 KiB
YAML

name: Flaro
options:
bundleIdPrefix: com.scarriffleservices
deploymentTarget:
iOS: "26.0"
macOS: "26.0"
tvOS: "26.0"
createIntermediateGroups: true
groupSortPosition: top
developmentLanguage: en
settings:
base:
DEVELOPMENT_TEAM: PP34X97WS3
SWIFT_VERSION: "6.0"
MARKETING_VERSION: "0.1.0"
CURRENT_PROJECT_VERSION: "1"
SWIFT_STRICT_CONCURRENCY: complete
packages:
FlaroKit:
path: Packages/FlaroKit
targets:
Flaro:
type: application
platform: iOS
# Ausdrücklich aufgeschrieben, statt Xcode raten zu lassen.
#
# Xcode legt für jedes Ziel von selbst ein Schema an — aber im
# Benutzerordner, und `xcodegen generate` räumt den mit weg. Danach
# findet `xcodebuild` kein Schema mehr und kann die Pakete nicht
# auflösen. Hier steht es im Projekt und überlebt jedes Neuerzeugen.
scheme:
testTargets:
- FlaroUITests
sources:
# Das Icon ist ein Bündel, also technisch ein Ordner. Ohne diesen
# Ausschluss läuft XcodeGen hinein und trägt icon.json und play.svg
# einzeln ein — dann fehlt das Icon, ohne dass irgendwo ein Fehler
# auftaucht.
#
- path: Flaro
excludes:
- "AppIcon.icon"
- path: Flaro/AppIcon.icon
type: file
dependencies:
- target: DownloadWidget
# Die neutrale Grundschicht. Ausdrücklich genannt, obwohl sie über
# JellyfinKit ohnehin im Baum hängt: die App soll sie direkt benutzen und
# nicht davon abhängen, dass ein bestimmter Server sie mitschleppt.
- package: FlaroKit
product: MediaKit
# Der Server. Steht auf der neutralen Schicht, nicht darin — kommt ein
# zweiter dazu, steht er daneben.
- package: FlaroKit
product: JellyfinKit
- package: FlaroKit
product: FlaroServerKit
- package: FlaroKit
product: FlaroDesign
- package: FlaroKit
product: FlaroPlayback
- package: FlaroKit
product: FlaroDownloads
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flaro
PRODUCT_NAME: Flaro
# Verweist auf AppIcon.icon. Der frühere leere AppIcon.appiconset im
# Asset-Katalog ist entfernt — zwei Dinge namens AppIcon, und Xcode
# nimmt das falsche.
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
# iPhone und iPad aus demselben Target.
TARGETED_DEVICE_FAMILY: "1,2"
CODE_SIGN_STYLE: Automatic
info:
path: Flaro/Info.plist
properties:
CFBundleDisplayName: Flaro
UILaunchScreen:
UIColorName: LaunchBackground
# Der harte Schalter für „nur dunkel". Eine Ansicht mit
# .preferredColorScheme(.dark) allein reicht nicht: Systemdialoge,
# Statusleiste und Tastatur richten sich danach nicht.
UIUserInterfaceStyle: Dark
# Alle drei bleiben eingetragen: die Info.plist sagt, was die App
# grundsätzlich kann. Was davon gerade gilt, entscheidet zur Laufzeit
# OrientationLock über den App-Delegaten — Hochformat beim Blättern,
# Querformat im Player.
UISupportedInterfaceOrientations:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
UISupportedInterfaceOrientations~ipad:
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
# Ohne diesen Modus hält iOS die Wiedergabe an, sobald die App in den
# Hintergrund geht — und Bild-in-Bild gäbe es gar nicht.
UIBackgroundModes:
- audio
# Die Ausfuhr-Erklärung zur Verschlüsselung.
#
# `false` heißt: die App nutzt ausschließlich Verschlüsselung, die
# unter die Ausnahmeregelung fällt. Das trifft hier zu — HTTPS über
# URLSession und der Schlüsselbund für die Tokens, beides vom
# Betriebssystem gestellt. Eigene Algorithmen gibt es keine.
#
# Ohne diesen Eintrag fragt App Store Connect bei JEDEM Build erneut
# („Missing Compliance"), und der Build lässt sich bis dahin nicht
# verteilen.
#
# Zu beachten, falls sich das ändert: solange nur Dateien vom eigenen
# Server abgespielt werden, bleibt es bei Standardverfahren. Käme
# irgendwann etwas mit eigener Verschlüsselung dazu — DRM etwa —,
# gehört dieser Wert überprüft.
ITSAppUsesNonExemptEncryption: false
# Ohne diesen Schlüssel lehnt ActivityKit jede Anfrage ab — und
# zwar wortlos, was die Suche nach dem Grund unangenehm macht.
NSSupportsLiveActivities: true
NSAppTransportSecurity:
# Medienserver im Heimnetz laufen überwiegend auf blankem HTTP —
# Jellyfin ab Werk auf Port 8096. Diese Ausnahme erlaubt genau das,
# ohne die Transportsicherheit für das offene Netz abzuschalten.
NSAllowsLocalNetworking: true
# Seit iOS 14 verlangt jeder Zugriff auf Adressen im eigenen Netz eine
# Freigabe. Fehlt der Text, verweigert das System sie kommentarlos und
# die Serversuche findet dauerhaft nichts.
NSLocalNetworkUsageDescription: >-
Flaro benötigt Zugriff auf das lokale Netzwerk, um deinen
Jellyfin-Server zu erreichen und Medien direkt von ihm zu laden.
# Die Fassung für den Fernseher.
#
# **Eigene Ansichten, geteilte Modelle.** Die Quellenpfade sind die ganze
# Aussage dieses Blocks: `FlaroTV/` gehört nur hierher, `Flaro/Session/`
# gehört beiden. Keine Datei muss dafür umziehen — XcodeGen nimmt Pfade je
# Ziel, und die iOS-Fassung sieht von der ganzen Sache nichts.
FlaroTV:
type: application
platform: tvOS
# Aus demselben Grund ausgeschrieben wie bei Flaro — siehe dort.
scheme: {}
sources:
# Die eigenen Ansichten.
- path: FlaroTV
# Die Modelle.
- path: Flaro/Session
excludes:
# Ein Fernseher dreht sich nicht.
- "OrientationLock.swift"
# Heruntergeladen wird auf dem Fernseher nichts.
#
# `DownloadRefiller.swift` und `DownloadService.swift` kommen
# trotzdem mit: `PlayerModel` nennt den Nachfüller in einem Zweig,
# der auf tvOS nie läuft (`downloads` ist dort `nil`), und der
# Nachfüller wiederum den Dienst. Sie auszuschliessen hiesse, eine
# Plattformfrage mitten in zweitausend Zeilen Wiedergabelogik zu
# tragen — teurer als zwei Dateien mitzuübersetzen, die niemand
# aufruft. Beide sind reine Übersetzer ohne UIKit.
# Reines Modell trotz des Ordnernamens: LoadState, ErrorText und
# MediaRoute liegen zusammen in einer Datei.
- path: Flaro/Views/LoadState.swift
type: file
# Der Player. **Ohne PlayerView** — die zeichnet auf iOS das Bild und
# alle Bedienelemente selbst; auf dem Fernseher macht das Apples
# AVPlayerViewController. Das Modell darunter ist dasselbe.
- path: Flaro/Views/Player/PlayerModel.swift
type: file
- path: Flaro/Views/Player/VideoSurface.swift
type: file
- path: Flaro/Branding.swift
type: file
# Die Texte. Dieselben Schlüssel, dieselben Übersetzungen.
- path: Flaro/Resources/Localizable.xcstrings
type: file
dependencies:
- package: FlaroKit
product: MediaKit
- package: FlaroKit
product: JellyfinKit
- package: FlaroKit
product: FlaroServerKit
- package: FlaroKit
product: FlaroDesign
- package: FlaroKit
product: FlaroPlayback
# Nur damit `PlayerModel` den Typ nennen darf. Angelegt wird nie einer:
# FlaroTVApp reicht `nil` durch, und damit gibt es auf tvOS auch keinen
# ModelContainer und kein SwiftData.
- package: FlaroKit
product: FlaroDownloads
- target: FlaroTopShelf
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flaro.tv
PRODUCT_NAME: Flaro
# Nur Apple TV.
TARGETED_DEVICE_FAMILY: "3"
# Kein AppIcon.icon: Icon Composer deckt iOS, macOS und watchOS ab,
# tvOS nicht. Der Name zeigt auf die Brand-Assets-Gruppe im Katalog,
# und der Katalog liegt unter FlaroTV/Resources.
ASSETCATALOG_COMPILER_APPICON_NAME: "App Icon & Top Shelf Image"
CODE_SIGN_STYLE: Automatic
CODE_SIGN_ENTITLEMENTS: FlaroTV/Flaro.entitlements
info:
path: FlaroTV/Info.plist
properties:
CFBundleDisplayName: Flaro
UIUserInterfaceStyle: Dark
ITSAppUsesNonExemptEncryption: false
# Über dieses Schema schickt das Regal zurück in die App. Ohne den
# Eintrag kennt tvOS es nicht, und ein Druck auf ein Plakat tut
# nichts — ohne Fehler, ohne Hinweis.
CFBundleURLTypes:
- CFBundleURLName: com.scarriffleservices.flaro.topshelf
CFBundleURLSchemes:
- flaro
# Was hier **nicht** steht, und warum:
#
# UILaunchScreen — tvOS hat kein Startbild dieser Art.
# UISupportedInterfaceOrientations — ein Fernseher dreht sich nicht.
# NSSupportsLiveActivities — ActivityKit fehlt im tvOS-SDK.
# CFBundleURLTypes — die Anmeldung braucht kein Rückkehr-Schema.
# UIBackgroundModes: audio — ein Apple TV schickt keine App in den
# Hintergrund, aus dem sie weiterspielen müsste.
NSAppTransportSecurity:
NSAllowsLocalNetworking: true
NSLocalNetworkUsageDescription: >-
Flaro benötigt Zugriff auf das lokale Netzwerk, um deinen
Jellyfin-Server zu erreichen.
# Das Regal über der App im tvOS-Menü.
#
# Eine eigene Erweiterung, weil tvOS sie zeichnet, während die App **nicht**
# läuft — und mit weniger als einer Sekunde Zeit. Sie liest deshalb nur eine
# Datei aus der App-Gruppe, die die App beim Laden der Startseite ablegt:
# kein Netz, kein Schlüsselbund, kein Blättern.
FlaroTopShelf:
type: tv-app-extension
platform: tvOS
sources:
- path: FlaroTopShelf
dependencies:
- package: FlaroKit
product: MediaKit
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flaro.tv.topshelf
PRODUCT_NAME: FlaroTopShelf
CODE_SIGN_STYLE: Automatic
CODE_SIGN_ENTITLEMENTS: FlaroTopShelf/FlaroTopShelf.entitlements
SKIP_INSTALL: YES
info:
path: FlaroTopShelf/Info.plist
properties:
CFBundleDisplayName: Flaro
# **Die Erweiterung hat ihre eigene Info.plist.**
#
# Und damit ihre eigene Transportsicherheit: die Ausnahme in der
# Info.plist der App gilt hier nicht. Ohne sie blockt das System jedes
# Plakat weg, das über blankes HTTP vom Jellyfin im Heimnetz kommt —
# lautlos, und das Regal bleibt leer. Genau so sieht „geht halt nicht"
# aus.
NSAppTransportSecurity:
NSAllowsLocalNetworking: true
NSExtension:
NSExtensionPointIdentifier: com.apple.tv-top-shelf
NSExtensionPrincipalClass: $(PRODUCT_MODULE_NAME).TopShelfProvider
# Nur zum Hinsehen: fährt die App mit der Attrappe in einen bestimmten
# Zustand und hält sie dort an, damit von außen fotografiert werden kann.
# Kein Teil der App, wird nie mitgeliefert.
FlaroUITests:
type: bundle.ui-testing
platform: iOS
sources:
- FlaroUITests
dependencies:
- target: Flaro
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flaro.uitests
TEST_TARGET_NAME: Flaro
TARGETED_DEVICE_FAMILY: "1,2"
CODE_SIGN_STYLE: Automatic
GENERATE_INFOPLIST_FILE: YES
# Die Live Activity für Downloads. Eine eigene Erweiterung, weil iOS die
# Darstellung im Sperrbildschirm in einem getrennten Prozess zeichnet — die
# App selbst läuft zu dem Zeitpunkt meist gar nicht.
# Dieselben Quellen, dasselbe Aussehen.
DownloadWidget:
type: app-extension
platform: iOS
sources:
- path: DownloadWidget
dependencies:
- package: FlaroKit
product: FlaroDownloads
- package: FlaroKit
product: FlaroDesign
settings:
base:
PRODUCT_BUNDLE_IDENTIFIER: com.scarriffleservices.flaro.downloadwidget
PRODUCT_NAME: DownloadWidget
CODE_SIGN_STYLE: Automatic
SKIP_INSTALL: YES
# Von XcodeGen erzeugt statt von Hand geschrieben. Eine eigene Datei ohne
# CFBundleIdentifier führt zu einer Erweiterung ganz ohne Kennung — und
# der Fehler lautet dann „not prefixed with the parent app's bundle
# identifier", was auf eine falsche Fährte führt.
info:
path: DownloadWidget/Info.plist
properties:
CFBundleDisplayName: Flaro Downloads
NSExtension:
NSExtensionPointIdentifier: com.apple.widgetkit-extension