From 4b55823ff692a9a008439ed51d55fc16822a63d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Kru=CC=88ss?= Date: Tue, 3 Nov 2026 10:10:43 -0700 Subject: [PATCH 1/6] Show every audio and subtitle track in file details Radarr 7.0.1 reports per-track audioStreams and subtitleStreams on movie file media info (Radarr/Radarr@0cd2fc2c) and deprecates the flat primary-stream fields. Files with more than one audio track list each one, and subtitles list language, format, forced and hearing impaired flags. Older versions send no stream arrays and keep the existing rows. --- CHANGELOG.md | 3 ++ Ruddarr/Localizable.xcstrings | 20 ++++++++++ Ruddarr/Models/MediaFile.swift | 45 +++++++++++++++++++++++ Ruddarr/Views/Shared/MediaFileSheet.swift | 29 ++++++++++++++- TestFlight/WhatToTest.en-US.txt | 3 ++ 5 files changed, 99 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0293e3d..9a8f2ed8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Added +- Show every audio and subtitle track in movie file details (Radarr 7.0.1 and later) + ### Fixed - Show multi-season packs as such in history and keep their release type when importing manually diff --git a/Ruddarr/Localizable.xcstrings b/Ruddarr/Localizable.xcstrings index 8e27a4e1..4c064faf 100644 --- a/Ruddarr/Localizable.xcstrings +++ b/Ruddarr/Localizable.xcstrings @@ -2029,6 +2029,16 @@ } } }, + "Forced" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Forced" + } + } + } + }, "Framerate" : { "comment" : "Video frame rate", "localizations" : { @@ -2155,6 +2165,16 @@ } } }, + "Hearing Impaired" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hearing Impaired" + } + } + } + }, "Hide Specials" : { "localizations" : { "en" : { diff --git a/Ruddarr/Models/MediaFile.swift b/Ruddarr/Models/MediaFile.swift index 20aee83f..c7763ed5 100644 --- a/Ruddarr/Models/MediaFile.swift +++ b/Ruddarr/Models/MediaFile.swift @@ -103,6 +103,9 @@ struct FileMediaInfo: Equatable, Codable { let subtitles: String? + let audioStreams: [MediaInfoAudioStream]? + let subtitleStreams: [MediaInfoSubtitleStream]? + var videoCodecLabel: String? { guard var label = videoCodec else { return nil @@ -159,3 +162,45 @@ struct FileMediaInfo: Equatable, Codable { return codes.sorted(by: Languages.codeSort) } } + +struct MediaInfoAudioStream: Equatable, Codable { + let language: String? + let codec: String? + let bitrate: Int? + let channels: Float? + + var label: String { + Languages.name(byCode: language ?? "") + } + + var detail: String { + [ + codec, + channels.map { "\($0)" }, + bitrate.flatMap { formatBitrate($0) }, + ] + .compactMap { $0 } + .joined(separator: " · ") + } +} + +struct MediaInfoSubtitleStream: Equatable, Codable { + let language: String? + let format: String? + let forced: Bool? + let hearingImpaired: Bool? + + var label: String { + Languages.name(byCode: language ?? "") + } + + var detail: String { + [ + format, + forced == true ? String(localized: "Forced") : nil, + hearingImpaired == true ? String(localized: "Hearing Impaired") : nil, + ] + .compactMap { $0 } + .joined(separator: " · ") + } +} diff --git a/Ruddarr/Views/Shared/MediaFileSheet.swift b/Ruddarr/Views/Shared/MediaFileSheet.swift index d07b81e3..597e15ab 100644 --- a/Ruddarr/Views/Shared/MediaFileSheet.swift +++ b/Ruddarr/Views/Shared/MediaFileSheet.swift @@ -174,6 +174,8 @@ struct MediaFileSheet: View { "--" ) } + + audioTracks(media.audioStreams ?? []) } } header: { headline("Audio") @@ -182,9 +184,34 @@ struct MediaFileSheet: View { } } + @ViewBuilder + func audioTracks(_ streams: [MediaInfoAudioStream]) -> some View { + if streams.count > 1 { + ForEach(Array(streams.enumerated()), id: \.offset) { _, stream in + Divider() + row(stream.label, stream.detail) + } + } + } + @ViewBuilder var textMetadata: some View { - if let media = file.mediaInfo, let codes = media.subtitleCodes { + if let media = file.mediaInfo, let streams = media.subtitleStreams, !streams.isEmpty { + Section { + VStack(spacing: 6) { + ForEach(Array(streams.enumerated()), id: \.offset) { index, stream in + if index > 0 { + Divider() + } + + row(stream.label, stream.detail) + } + } + } header: { + headline("Subtitles") + .padding(.bottom, 4) + } + } else if let media = file.mediaInfo, let codes = media.subtitleCodes { Section { VStack(spacing: 6) { row( diff --git a/TestFlight/WhatToTest.en-US.txt b/TestFlight/WhatToTest.en-US.txt index cf05d831..c40dcd0c 100644 --- a/TestFlight/WhatToTest.en-US.txt +++ b/TestFlight/WhatToTest.en-US.txt @@ -1,2 +1,5 @@ +Added +- Show every audio and subtitle track in movie file details (Radarr 7.0.1 and later) + Fixed - Show multi-season packs as such in history and keep their release type when importing manually From a3b89f33b54d625b5636f2a7a1bfec05d462308d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Kru=CC=88ss?= Date: Tue, 3 Nov 2026 18:39:31 -0700 Subject: [PATCH 2/6] List audio tracks instead of the audio summary when streams are available The per-track rows already carry codec, channels, bitrate and language, so the summary rows only repeated the first track. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MYjz4ojbQ3WH6ZsETB2Pzg --- Ruddarr/Views/Shared/MediaFileSheet.swift | 43 +++++++++++------------ 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/Ruddarr/Views/Shared/MediaFileSheet.swift b/Ruddarr/Views/Shared/MediaFileSheet.swift index 597e15ab..dedc4a75 100644 --- a/Ruddarr/Views/Shared/MediaFileSheet.swift +++ b/Ruddarr/Views/Shared/MediaFileSheet.swift @@ -132,7 +132,14 @@ struct MediaFileSheet: View { @ViewBuilder var audioMetadata: some View { - if let media = file.mediaInfo { + if let streams = file.mediaInfo?.audioStreams, !streams.isEmpty { + Section { + tracks(streams.map { ($0.label, $0.detail) }) + } header: { + headline("Audio") + .padding(.bottom, 4) + } + } else if let media = file.mediaInfo { Section { VStack(spacing: 6) { row( @@ -174,8 +181,6 @@ struct MediaFileSheet: View { "--" ) } - - audioTracks(media.audioStreams ?? []) } } header: { headline("Audio") @@ -184,29 +189,11 @@ struct MediaFileSheet: View { } } - @ViewBuilder - func audioTracks(_ streams: [MediaInfoAudioStream]) -> some View { - if streams.count > 1 { - ForEach(Array(streams.enumerated()), id: \.offset) { _, stream in - Divider() - row(stream.label, stream.detail) - } - } - } - @ViewBuilder var textMetadata: some View { if let media = file.mediaInfo, let streams = media.subtitleStreams, !streams.isEmpty { Section { - VStack(spacing: 6) { - ForEach(Array(streams.enumerated()), id: \.offset) { index, stream in - if index > 0 { - Divider() - } - - row(stream.label, stream.detail) - } - } + tracks(streams.map { ($0.label, $0.detail) }) } header: { headline("Subtitles") .padding(.bottom, 4) @@ -233,6 +220,18 @@ struct MediaFileSheet: View { } } + func tracks(_ tracks: [(label: String, detail: String)]) -> some View { + VStack(spacing: 6) { + ForEach(Array(tracks.enumerated()), id: \.offset) { index, track in + if index > 0 { + Divider() + } + + row(track.label, track.detail) + } + } + } + func headline(_ title: LocalizedStringKey) -> some View { Text(title) .font(.title2.bold()) From a6544dd5e5c9653d2df4ffc0e81854a3010993ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Kru=CC=88ss?= Date: Tue, 3 Nov 2026 18:49:54 -0700 Subject: [PATCH 3/6] Show audio channels and subtitle flags in parentheses Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MYjz4ojbQ3WH6ZsETB2Pzg --- Ruddarr/Models/MediaFile.swift | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Ruddarr/Models/MediaFile.swift b/Ruddarr/Models/MediaFile.swift index 94557fd9..e17c4b69 100644 --- a/Ruddarr/Models/MediaFile.swift +++ b/Ruddarr/Models/MediaFile.swift @@ -179,13 +179,14 @@ struct MediaInfoAudioStream: Equatable, Codable { } var detail: String { - [ - codec, - channels.map { "\($0)" }, - bitrate.flatMap { formatBitrate($0) }, - ] - .compactMap { $0 } - .joined(separator: " · ") + let codecLabel = [codec, channels.map { "(\($0))" }] + .compactMap { $0 } + .joined(separator: " ") + + return [codecLabel, bitrate.flatMap { formatBitrate($0) }] + .compactMap { $0 } + .filter { !$0.isEmpty } + .formattedList() } } @@ -200,12 +201,13 @@ struct MediaInfoSubtitleStream: Equatable, Codable { } var detail: String { - [ - format, + let flags = [ forced == true ? String(localized: "Forced") : nil, hearingImpaired == true ? String(localized: "Hearing Impaired") : nil, - ] - .compactMap { $0 } - .joined(separator: " · ") + ].compactMap { $0 } + + return [format, flags.isEmpty ? nil : "(\(flags.formattedList()))"] + .compactMap { $0 } + .joined(separator: " ") } } From a4fc9ff7c7152c4e83c96d26329946042c5eba6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Kru=CC=88ss?= Date: Tue, 3 Nov 2026 20:23:37 -0700 Subject: [PATCH 4/6] Show audio tracks as codec, channels and bitrate in parentheses Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MYjz4ojbQ3WH6ZsETB2Pzg --- Ruddarr/Models/MediaFile.swift | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Ruddarr/Models/MediaFile.swift b/Ruddarr/Models/MediaFile.swift index e17c4b69..64bc3510 100644 --- a/Ruddarr/Models/MediaFile.swift +++ b/Ruddarr/Models/MediaFile.swift @@ -179,14 +179,13 @@ struct MediaInfoAudioStream: Equatable, Codable { } var detail: String { - let codecLabel = [codec, channels.map { "(\($0))" }] - .compactMap { $0 } - .joined(separator: " ") - - return [codecLabel, bitrate.flatMap { formatBitrate($0) }] - .compactMap { $0 } - .filter { !$0.isEmpty } - .formattedList() + [ + codec, + channels.map { "\($0)" }, + bitrate.flatMap { formatBitrate($0) }.map { "(\($0))" }, + ] + .compactMap { $0 } + .joined(separator: " ") } } From 0eebb585f7544579c5a5660a262ceaf7dce237c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Kru=CC=88ss?= Date: Tue, 3 Nov 2026 20:39:00 -0700 Subject: [PATCH 5/6] Label hearing impaired subtitles as SDH Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MYjz4ojbQ3WH6ZsETB2Pzg --- Ruddarr/Localizable.xcstrings | 21 +++++++++++---------- Ruddarr/Models/MediaFile.swift | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Ruddarr/Localizable.xcstrings b/Ruddarr/Localizable.xcstrings index 4c064faf..d2836222 100644 --- a/Ruddarr/Localizable.xcstrings +++ b/Ruddarr/Localizable.xcstrings @@ -2165,16 +2165,6 @@ } } }, - "Hearing Impaired" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Hearing Impaired" - } - } - } - }, "Hide Specials" : { "localizations" : { "en" : { @@ -4700,6 +4690,17 @@ } } }, + "SDH" : { + "comment" : "Subtitles for the deaf and hard of hearing", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "SDH" + } + } + } + }, "Search" : { "comment" : "Source of the release", "localizations" : { diff --git a/Ruddarr/Models/MediaFile.swift b/Ruddarr/Models/MediaFile.swift index 64bc3510..9318fdb9 100644 --- a/Ruddarr/Models/MediaFile.swift +++ b/Ruddarr/Models/MediaFile.swift @@ -202,7 +202,7 @@ struct MediaInfoSubtitleStream: Equatable, Codable { var detail: String { let flags = [ forced == true ? String(localized: "Forced") : nil, - hearingImpaired == true ? String(localized: "Hearing Impaired") : nil, + hearingImpaired == true ? String(localized: "SDH", comment: "Subtitles for the deaf and hard of hearing") : nil, ].compactMap { $0 } return [format, flags.isEmpty ? nil : "(\(flags.formattedList()))"] From cbe06af89f3aff8b7409ceab69bbdd591f18f9de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Till=20Kru=CC=88ss?= Date: Tue, 3 Nov 2026 20:40:59 -0700 Subject: [PATCH 6/6] Add a translator comment to the Forced subtitle flag Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MYjz4ojbQ3WH6ZsETB2Pzg --- Ruddarr/Localizable.xcstrings | 1 + Ruddarr/Models/MediaFile.swift | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Ruddarr/Localizable.xcstrings b/Ruddarr/Localizable.xcstrings index d2836222..0e1c7f7f 100644 --- a/Ruddarr/Localizable.xcstrings +++ b/Ruddarr/Localizable.xcstrings @@ -2030,6 +2030,7 @@ } }, "Forced" : { + "comment" : "Subtitles shown only for foreign dialogue and on-screen text", "localizations" : { "en" : { "stringUnit" : { diff --git a/Ruddarr/Models/MediaFile.swift b/Ruddarr/Models/MediaFile.swift index 9318fdb9..be1e40ba 100644 --- a/Ruddarr/Models/MediaFile.swift +++ b/Ruddarr/Models/MediaFile.swift @@ -201,7 +201,7 @@ struct MediaInfoSubtitleStream: Equatable, Codable { var detail: String { let flags = [ - forced == true ? String(localized: "Forced") : nil, + forced == true ? String(localized: "Forced", comment: "Subtitles shown only for foreign dialogue and on-screen text") : nil, hearingImpaired == true ? String(localized: "SDH", comment: "Subtitles for the deaf and hard of hearing") : nil, ].compactMap { $0 }