From b9a6d76ab3f4e52a5aa2ab3a657f3e3e19d7f729 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 13 Apr 2026 07:35:33 +0200 Subject: [PATCH] Fix original audio detection so dubbed tracks don't play by default parseAudioInfo() returned isOriginal=false for all streams when the audioTrack object was present in the API response, preventing xtags parsing from correctly identifying original tracks. This caused the player to fall through to codec/bitrate sorting, often picking a locale dub (e.g. Polish) instead of the original English audio. Now determines isOriginal from both the audioTrack displayName ("original" keyword) and URL xtags (acont=original) for robustness. Also adds isDefault to InvidiousAudioTrack for future use. --- Yattee/Services/API/InvidiousAPI.swift | 61 +++++++++++++---------- Yattee/Services/API/YatteeServerAPI.swift | 50 +++++++++++++------ 2 files changed, 69 insertions(+), 42 deletions(-) diff --git a/Yattee/Services/API/InvidiousAPI.swift b/Yattee/Services/API/InvidiousAPI.swift index 04d36167..1f72f01e 100644 --- a/Yattee/Services/API/InvidiousAPI.swift +++ b/Yattee/Services/API/InvidiousAPI.swift @@ -1243,52 +1243,60 @@ private struct InvidiousAdaptiveFormat: Decodable, Sendable { /// Parse audio language, track name, and whether it's original from audioTrack object or URL xtags. /// URL xtags format: xtags=acont%3Doriginal%3Alang%3Den-US or xtags=acont%3Ddubbed-auto%3Alang%3Dde-DE private nonisolated func parseAudioInfo() -> (language: String?, trackName: String?, isOriginal: Bool) { - // Prefer explicit audioTrack if available + // When audioTrack object is available, use it for language/name but determine + // isOriginal from displayName and URL xtags (audioTrack alone is not reliable) if let audioTrack, audioTrack.id != nil || audioTrack.displayName != nil { - // Can't determine if original from audioTrack alone, assume not - return (audioTrack.id, audioTrack.displayName, false) + let displayNameIndicatesOriginal = audioTrack.displayName? + .localizedCaseInsensitiveContains("original") ?? false + let xtagsIndicateOriginal = checkXtagsForOriginal() + let isOriginal = displayNameIndicatesOriginal || xtagsIndicateOriginal + return (audioTrack.id, audioTrack.displayName, isOriginal) } - // Parse from URL xtags parameter for audio streams + // Fall back to parsing from URL xtags parameter for audio streams guard isAudioOnly, let urlString = url else { return (nil, nil, false) } - // Find xtags parameter in URL - guard let xtagsRange = urlString.range(of: "xtags=") else { + guard let xtags = parseXtags(from: urlString) else { return (nil, nil, false) } + guard let langCode = xtags["lang"] else { + return (nil, nil, false) + } + + let contentType = xtags["acont"] + let isOriginal = contentType == "original" + let trackName = generateTrackName(langCode: langCode, contentType: contentType) + + return (langCode, trackName, isOriginal) + } + + /// Check if the stream URL's xtags indicate this is the original audio track. + private nonisolated func checkXtagsForOriginal() -> Bool { + guard isAudioOnly, let urlString = url else { return false } + guard let xtags = parseXtags(from: urlString) else { return false } + return xtags["acont"] == "original" + } + + /// Parse xtags key=value pairs from a URL string. + /// Example xtags: "acont=original:drc=1:lang=en-US" + private nonisolated func parseXtags(from urlString: String) -> [String: String]? { + guard let xtagsRange = urlString.range(of: "xtags=") else { return nil } + let xtagsStart = xtagsRange.upperBound let xtagsEnd = urlString[xtagsStart...].firstIndex(of: "&") ?? urlString.endIndex let xtagsEncoded = String(urlString[xtagsStart.. (language: String?, trackName: String?, isOriginal: Bool) { + // When audioTrack object is available, use it for language/name but determine + // isOriginal from displayName and URL xtags (isDefault alone is not reliable + // because the server may mark locale-default dubs as isDefault too) if let audioTrack, audioTrack.id != nil || audioTrack.displayName != nil { - return (audioTrack.id, audioTrack.displayName, audioTrack.isDefault ?? false) + let displayNameIndicatesOriginal = audioTrack.displayName? + .localizedCaseInsensitiveContains("original") ?? false + let xtagsIndicateOriginal = checkXtagsForOriginal() + let isOriginal = displayNameIndicatesOriginal || xtagsIndicateOriginal + return (audioTrack.id, audioTrack.displayName, isOriginal) } + // Fall back to parsing from URL xtags parameter for audio streams guard isAudioOnly, let urlString = url else { return (nil, nil, false) } - guard let xtagsRange = urlString.range(of: "xtags=") else { + guard let xtags = parseXtags(from: urlString) else { return (nil, nil, false) } + guard let langCode = xtags["lang"] else { + return (nil, nil, false) + } + + let contentType = xtags["acont"] + let isOriginal = contentType == "original" + let trackName = generateTrackName(langCode: langCode, contentType: contentType) + + return (langCode, trackName, isOriginal) + } + + /// Check if the stream URL's xtags indicate this is the original audio track. + private nonisolated func checkXtagsForOriginal() -> Bool { + guard isAudioOnly, let urlString = url else { return false } + guard let xtags = parseXtags(from: urlString) else { return false } + return xtags["acont"] == "original" + } + + /// Parse xtags key=value pairs from a URL string. + private nonisolated func parseXtags(from urlString: String) -> [String: String]? { + guard let xtagsRange = urlString.range(of: "xtags=") else { return nil } + let xtagsStart = xtagsRange.upperBound let xtagsEnd = urlString[xtagsStart...].firstIndex(of: "&") ?? urlString.endIndex let xtagsEncoded = String(urlString[xtagsStart.. String {