Sprachwahl in der App — bei Serien und bei Filmen

Auf der Serienseite stand ein Menü für den Ton, gefüllt mit zwanzig
Sprachen, von denen die Serie zwei hatte. Für Untertitel gab es keines,
und auf der Filmseite überhaupt nichts.

Jetzt beides an beiden Orten, und zwar nur mit dem, was es wirklich gibt:

Bei einer **Serie** über den neuen Endpunkt — die Schnittmenge über alle
Folgen. Was dadurch wegfällt, steht als Satz darunter, sonst sähe eine
gekürzte Liste aus wie die ganze Wahrheit.

Bei einem **Film** aus den Spuren der Datei. Kein Serverweg nötig: dort
ist die Datei der Titel, und ihre Spuren stehen in der Antwort, die die
Seite ohnehin geholt hat.

Ton und Untertitel teilen sich einen Typ. Sie unterscheiden sich in genau
zwei Punkten — woher der Wert kommt und ob es „Keine" gibt; ein zweiter
Typ wäre dieselbe Datei mit zwei geänderten Zeilen. „Keine" gibt es nur
beim Untertitel: ein Film ohne Tonspur ist kein Wunsch, sondern ein
Fehler.

Die Untertitelsprache liegt je Titel, in einem Topf für Serien und Filme
— eine Filmkennung kollidiert mit keiner Serie, und zwei Wörterbücher für
dieselbe Frage liefen auseinander. Der Spieler liest sie jetzt: vorher
kannte er nur die allgemeine, und eine Serie im Original mit deutschen
Untertiteln liess sich nicht einstellen, ohne sie überall einzuschalten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-10 21:56:15 +02:00
co-authored by Claude Opus 5
parent 95d7eee8c3
commit 84612335ec
9 changed files with 377 additions and 22 deletions
@@ -147,6 +147,18 @@ public actor FlaroBackend: MediaBackend {
items: titel)
}
public func availableLanguages(inSeries id: String) async -> AvailableLanguages? {
guard let roh = try? await client.send(
FlaroRequest(path: "/api/v1/series/\(id)/languages"),
as: Wire.Sprachauswahl.self)
else { return nil }
return AvailableLanguages(
audio: roh.audio.map { LanguageOption(code: $0.code, name: $0.name) },
subtitles: roh.subtitles.map { LanguageOption(code: $0.code, name: $0.name) },
partial: roh.partial)
}
public func items(
inLibrary library: MediaLibrary,
query: MediaQuery,
@@ -212,6 +212,17 @@ enum Wire {
let subtitleStreams: [Stromspur]
}
struct Sprachangabe: Decodable, Sendable {
let code: String
let name: String
}
struct Sprachauswahl: Decodable, Sendable {
let audio: [Sprachangabe]
let subtitles: [Sprachangabe]
let partial: Int
}
struct LoginRequest: Encodable, Sendable {
let userName: String
let password: String
@@ -60,6 +60,9 @@ public protocol MediaBackend: Sendable {
/// dreizehn Anfragen — für einen Klick auf eine Kachel ist das zu viel.
/// Ein Server, der es in einer Anfrage kann, soll das dürfen.
func episodes(inSeries id: String) async throws(MediaBackendError) -> [MediaItem]
/// Welche Sprachen eine Serie durchgehend führt — für die Vorwahl.
func availableLanguages(inSeries id: String) async -> AvailableLanguages?
/// Die nächste Folge nach dieser, sofern es eine gibt.
func nextEpisode(after item: MediaItem) async throws(MediaBackendError) -> MediaItem?
@@ -227,6 +230,13 @@ public extension MediaBackend {
subtitleStreamIndex: Int?
) -> URL? { nil }
/// Welche Sprachen eine Serie durchgehend führt.
///
/// **`nil` heisst: der Server weiss es nicht.** Dann bleibt es bei der
/// vollen Liste — eine leere Auswahl anzubieten wäre schlimmer als eine
/// zu lange.
func availableLanguages(inSeries id: String) async -> AvailableLanguages? { nil }
/// Der allgemeine Weg: Staffeln, dann je Staffel die Folgen.
///
/// Langsam, aber überall gültig — wer es besser kann, überschreibt es.
@@ -261,3 +271,41 @@ public struct MediaFilterOption: Sendable, Equatable, Identifiable {
self.title = title
}
}
/// Eine Sprache, wie sie in einem Menü steht.
public struct LanguageOption: Sendable, Equatable, Identifiable, Hashable {
public let code: String
public let name: String
public var id: String { code }
public init(code: String, name: String) {
self.code = code
self.name = name
}
}
/// Welche Sprachen ein Titel durchgehend führt.
///
/// **Durchgehend, nicht irgendwo.** Bei einer Serie ist das die
/// Schnittmenge über alle Folgen: eine Sprache anzubieten, die es nur in
/// Staffel 1 gibt, hiesse eine Einstellung für die Serie, die ab Staffel 2
/// stillschweigend etwas anderes tut.
public struct AvailableLanguages: Sendable, Equatable {
public let audio: [LanguageOption]
public let subtitles: [LanguageOption]
/// Wie viele Sprachen es nur in einem Teil der Folgen gibt.
///
/// **Damit die Oberfläche es sagen kann.** Eine gekürzte Liste sieht
/// sonst aus wie die ganze Wahrheit, und wer eine Sprache vermisst, die
/// er in Staffel 1 gesehen hat, hält es für einen Fehler.
public let partial: Int
public init(audio: [LanguageOption], subtitles: [LanguageOption], partial: Int) {
self.audio = audio
self.subtitles = subtitles
self.partial = partial
}
}