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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6d5824b889
commit
172b38da06
@@ -173,6 +173,18 @@ public enum BackendCapability: String, Sendable, Hashable {
|
||||
/// Die Verwaltung des Servers aus der App heraus.
|
||||
case administration
|
||||
case transcoding
|
||||
/// Ein Download bringt **alle** Ton- und Textspuren mit.
|
||||
///
|
||||
/// **Der Unterschied, um dessentwillen Flaro gebaut wurde.** Jellyfins
|
||||
/// Transcode-Endpunkt bildet eine Tonspur je Sitzung ab: was
|
||||
/// heruntergeladen wird, ist einspurig, und die Sprachwahl ist offline
|
||||
/// weg. Einmal hat die App dabei still die Audiodeskription genommen —
|
||||
/// drei Gigabyte Mobilfunk für eine unbrauchbare Datei.
|
||||
///
|
||||
/// Kann der Server es besser, soll die App nicht so tun, als müsste
|
||||
/// vorher gewählt werden: `remuxDownloadURL` entfällt, die Spurauswahl
|
||||
/// im Sheet entfällt, und der gewöhnliche Download liefert alles.
|
||||
case allTracksDownload
|
||||
}
|
||||
|
||||
/// Ein Ausschnitt aus einer Mediathek.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import Foundation
|
||||
|
||||
/// Macht aus dem, was jemand eintippt, eine Adresse, die antwortet.
|
||||
///
|
||||
/// Weder Jellyfin noch Flaro haben ein Verzeichnis wie plex.tv — die Adresse
|
||||
/// kommt vom Nutzer. Und die tippt niemand vollständig: „192.168.1.50" ohne
|
||||
/// Schema, ohne Port, oder „medien.example.com" ohne „https://". Dieser Typ
|
||||
/// rät nicht **eine** Ergänzung, sondern nennt die plausiblen in der
|
||||
/// Reihenfolge, in der sie es wahrscheinlich sind; der Aufrufer probiert sie
|
||||
/// durch und nimmt die erste, die sich ausweist.
|
||||
///
|
||||
/// Die Reihenfolge ist der ganze Inhalt dieser Datei, und sie ist nicht
|
||||
/// beliebig: eine IP-Adresse im eigenen Netz spricht fast nie HTTPS, ein
|
||||
/// Domänenname fast immer.
|
||||
///
|
||||
/// **In der neutralen Schicht, weil beide Server dasselbe Problem haben.**
|
||||
/// Nur der Standardport unterscheidet sich, und der ist ein Wert, kein
|
||||
/// Gedanke. Diese Überlegungen zweimal hinzuschreiben hiesse, dass die eine
|
||||
/// Fassung beim nächsten Sonderfall nachgezogen wird und die andere nicht.
|
||||
public enum ServerAddress {
|
||||
public static func candidates(for input: String, defaultPort: Int) -> [URL] {
|
||||
var text = input.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
while text.hasSuffix("/") { text.removeLast() }
|
||||
guard !text.isEmpty else { return [] }
|
||||
|
||||
// Wer ein Schema hinschreibt, meint es auch so. Daran wird nichts
|
||||
// ergänzt — sonst landet eine Anfrage unverschlüsselt, obwohl jemand
|
||||
// ausdrücklich „https" getippt hat.
|
||||
if text.contains("://") {
|
||||
return URL(string: text).map { [$0] } ?? []
|
||||
}
|
||||
|
||||
// IPv6 in eckigen Klammern: hier wird nicht geraten, weil die
|
||||
// Doppelpunkte sonst als Port gelesen würden.
|
||||
if text.hasPrefix("[") {
|
||||
return ["http://\(text)", "https://\(text)"].compactMap(URL.init(string:))
|
||||
}
|
||||
|
||||
let authority = String(text.split(separator: "/", maxSplits: 1).first ?? "")
|
||||
let host = String(authority.split(separator: ":").first ?? "")
|
||||
let hasExplicitPort = authority.contains(":")
|
||||
&& Int(authority.split(separator: ":").last ?? "") != nil
|
||||
|
||||
if hasExplicitPort {
|
||||
// Port genannt, Schema nicht: erst unverschlüsselt, weil ein
|
||||
// eigener Port fast immer ein Server ohne Zertifikat ist.
|
||||
return ["http://\(text)", "https://\(text)"].compactMap(URL.init(string:))
|
||||
}
|
||||
|
||||
// Eine reine Zahlenadresse, ein Bonjour-Name oder ein Wort ohne Punkt
|
||||
// — alles Dinge, die es nur im eigenen Netz gibt.
|
||||
let isLocalLooking = host.allSatisfy { $0.isNumber || $0 == "." }
|
||||
|| host.hasSuffix(".local")
|
||||
|| !host.contains(".")
|
||||
|
||||
let mitPort = withDefaultPort(text, port: defaultPort)
|
||||
let forms = isLocalLooking
|
||||
? ["http://\(mitPort)", "http://\(text)", "https://\(text)"]
|
||||
: ["https://\(text)", "http://\(text)", "http://\(mitPort)"]
|
||||
return forms.compactMap(URL.init(string:))
|
||||
}
|
||||
|
||||
/// Hängt den Standardport an — vor einen etwaigen Pfad, nicht dahinter.
|
||||
private static func withDefaultPort(_ text: String, port: Int) -> String {
|
||||
guard let slash = text.firstIndex(of: "/") else { return "\(text):\(port)" }
|
||||
return "\(text[text.startIndex..<slash]):\(port)\(text[slash...])"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user