Nachsehen, was in den Tonspuren wirklich steht

Die Sprachwahl vergleicht Kürzel, und Kürzel lügen — beim ZDF-Fall ist
die als deutsch ausgezeichnete Spur die mit Audiodeskription. Ob der
**Name** der Spur das rettet, sieht man nur, wenn man beides nebeneinander
legt. Alles andere wäre geraten, und davon hatten wir heute genug.

`--spuren <host> <suchbegriff>` sucht einen Titel und druckt zu jeder
Tonspur: Kürzel, Name aus der Datei, wie Jellyfin es zusammensetzt,
Codec, Kanäle und die Kennzeichen.

Dafür holt der Decoder jetzt auch `Title`, `IsHearingImpaired` und
`IsOriginal`. `Title` ist das zweite Signal neben dem Kürzel und
manchmal das einzige richtige: wo „eng" steht, kann im Namen „Deutsch"
stehen. Wir haben es bisher weggeworfen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-27 19:11:27 +02:00
co-authored by Claude Opus 5
parent 41cd7331a9
commit 946de29893
3 changed files with 96 additions and 0 deletions
@@ -446,3 +446,80 @@ private func anfrage(_ url: URL, _ kopfzeilen: [String: String]) -> URLRequest {
for (name, wert) in kopfzeilen { request.setValue(wert, forHTTPHeaderField: name) } for (name, wert) in kopfzeilen { request.setValue(wert, forHTTPHeaderField: name) }
return request return request
} }
/// Zeigt, was der Server über die Tonspuren eines Titels wirklich sagt.
///
/// **Der Grund:** Die Sprachwahl vergleicht Kürzel, und Kürzel lügen. Ob bei
/// einer Datei der Titel das rettet — „eng" im Kürzel, „Deutsch" im Namen —,
/// sieht man nur, wenn man beides nebeneinander legt. Alles andere wäre
/// geraten.
func zeigeSpuren(host: String, suche: String) async {
guard let basis = URL(string: host.contains("://") ? host : "http://\(host)") else { return }
print("Benutzername: ", terminator: "")
guard let name = readLine(), !name.isEmpty else { return }
guard let passwort = String(validatingCString: getpass("Passwort: ")), !passwort.isEmpty else { return }
let identity = JellyfinIdentity(
client: "Flixy-Probe",
device: "Mac",
deviceID: "flixy-probe-" + UUID().uuidString,
version: "1.0"
)
let client = JellyfinClient(baseURL: basis, identity: identity)
guard let konto = try? await JellyfinAuthenticator(client: client).signIn(
userName: name,
password: passwort
) else {
print("Anmeldung gescheitert.")
return
}
print("Angemeldet als \(konto.userName).\n")
// Suchen, damit man nicht mit Kennungen hantieren muss.
let treffer = try? await client.send(
JellyfinRequest(path: "/Users/\(konto.userID)/Items", query: [
URLQueryItem(name: "searchTerm", value: suche),
URLQueryItem(name: "Recursive", value: "true"),
URLQueryItem(name: "IncludeItemTypes", value: "Movie,Episode"),
URLQueryItem(name: "Limit", value: "5"),
URLQueryItem(name: "Fields", value: "MediaSources"),
]),
as: JellyfinItemPage.self
)
guard let gefunden = treffer?.items, !gefunden.isEmpty else {
print("Nichts gefunden zu „\(suche)“.")
return
}
for eintrag in gefunden {
print("── \(eintrag.name ?? "—")")
let quellen = eintrag.mediaSources ?? []
guard let streams = quellen.first?.mediaStreams else {
print(" keine Spurangaben\n")
continue
}
for spur in streams where spur.type == "Audio" {
let kürzel = spur.language ?? "—"
let name = spur.title ?? "—"
var merkmale: [String] = []
if spur.isDefault == true { merkmale.append("Standard") }
if spur.isForced == true { merkmale.append("erzwungen") }
if spur.isHearingImpaired == true { merkmale.append("hörgeschädigt") }
if spur.isOriginal == true { merkmale.append("Original") }
print(" [\(spur.index ?? -1)] Kürzel \(kürzel)")
print(" Name in der Datei: \(name)")
print(" Wie Jellyfin es zeigt: \(spur.displayTitle ?? "—")")
print(" \(spur.codec ?? "—"), \(spur.channels ?? 0) Kanäle"
+ (merkmale.isEmpty ? "" : ", " + merkmale.joined(separator: ", ")))
}
print("")
}
print("Zu lesen ist das so: stimmt das Kürzel nicht mit dem Namen überein,")
print("lügt die Datei — und dann hilft nur der Name oder das Merken.")
}
@@ -27,6 +27,13 @@ if let index = CommandLine.arguments.firstIndex(of: "--playlist"),
exit(0) exit(0)
} }
if let index = CommandLine.arguments.firstIndex(of: "--spuren"),
let host = CommandLine.arguments.dropFirst(index + 1).first,
let suche = CommandLine.arguments.dropFirst(index + 2).first {
await zeigeSpuren(host: host, suche: suche)
exit(0)
}
if let index = CommandLine.arguments.firstIndex(of: "--jellyfin"), if let index = CommandLine.arguments.firstIndex(of: "--jellyfin"),
let host = CommandLine.arguments.dropFirst(index + 1).first { let host = CommandLine.arguments.dropFirst(index + 1).first {
await jellyfinLeiter(host: host) await jellyfinLeiter(host: host)
@@ -168,6 +168,16 @@ public struct JellyfinMediaStream: Decodable, Sendable {
public let codec: String? public let codec: String?
public let language: String? public let language: String?
public let displayTitle: String? public let displayTitle: String?
/// Was **in der Datei** steht — im Gegensatz zu `displayTitle`, das
/// Jellyfin aus Sprache, Codec und Kennzeichen zusammensetzt.
///
/// Das zweite Signal neben dem Sprachkürzel, und manchmal das einzige
/// richtige: wo „eng" steht, kann im Titel „Deutsch" stehen.
public let title: String?
/// Für Hörgeschädigte. **Nicht** Audiodeskription — dafür hat Jellyfin
/// kein Kennzeichen.
public let isHearingImpaired: Bool?
public let isOriginal: Bool?
public let channels: Int? public let channels: Int?
public let isDefault: Bool? public let isDefault: Bool?
public let isForced: Bool? public let isForced: Bool?
@@ -177,6 +187,8 @@ public struct JellyfinMediaStream: Decodable, Sendable {
private enum CodingKeys: String, CodingKey { private enum CodingKeys: String, CodingKey {
case index = "Index", type = "Type", codec = "Codec", language = "Language" case index = "Index", type = "Type", codec = "Codec", language = "Language"
case displayTitle = "DisplayTitle", channels = "Channels" case displayTitle = "DisplayTitle", channels = "Channels"
case title = "Title"
case isHearingImpaired = "IsHearingImpaired", isOriginal = "IsOriginal"
case isDefault = "IsDefault", isForced = "IsForced" case isDefault = "IsDefault", isForced = "IsForced"
case height = "Height", width = "Width" case height = "Height", width = "Width"
} }