From a8d6d8d2700245f7ccc02a5e370e013d50308a21 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 18 Jul 2026 16:54:50 +0530 Subject: [PATCH 1/3] fix(player): match forced subtitles to language preferences --- .../player/PlayerScreenRuntimeTrackActions.kt | 7 +- .../features/player/PlayerTrackSelection.kt | 10 +-- .../player/PlayerTrackSelectionTest.kt | 69 +++++++++++++++++++ 3 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt index 098c258f5..e4f5d2462 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt @@ -175,11 +175,7 @@ internal fun PlayerScreenRuntime.refreshTracks() { if (!preferredSubtitleSelectionApplied) { val preferredSubtitleTargets = resolvePreferredSubtitleLanguageTargets( - preferredSubtitleLanguage = if (subtitleStyle.useForcedSubtitles) { - SubtitleLanguageOption.FORCED - } else { - playerSettingsUiState.preferredSubtitleLanguage - }, + preferredSubtitleLanguage = playerSettingsUiState.preferredSubtitleLanguage, secondaryPreferredSubtitleLanguage = playerSettingsUiState.secondaryPreferredSubtitleLanguage, deviceLanguages = DeviceLanguagePreferences.preferredLanguageCodes(), ) @@ -196,6 +192,7 @@ internal fun PlayerScreenRuntime.refreshTracks() { val preferredSubtitleIndex = findPreferredSubtitleTrackIndex( tracks = subtitleTracks, targets = preferredSubtitleTargets, + requireForced = subtitleStyle.useForcedSubtitles, ) if (preferredSubtitleIndex >= 0 && preferredSubtitleIndex != selectedSubtitleIndex) { playerController?.selectSubtitleTrack(preferredSubtitleIndex) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt index 5bd895def..1fef16145 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt @@ -68,6 +68,7 @@ internal fun findPreferredTrackIndex( internal fun findPreferredSubtitleTrackIndex( tracks: List, targets: List, + requireForced: Boolean = false, ): Int { if (targets.isEmpty()) return -1 @@ -81,10 +82,11 @@ internal fun findPreferredSubtitleTrackIndex( } val matchIndex = tracks.indexOfFirst { track -> - languageMatchesPreference( - trackLanguage = track.language, - targetLanguage = normalizedTarget, - ) + (!requireForced || track.isForced) && + languageMatchesPreference( + trackLanguage = track.language, + targetLanguage = normalizedTarget, + ) } if (matchIndex >= 0) return matchIndex } diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt new file mode 100644 index 000000000..050969f96 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt @@ -0,0 +1,69 @@ +package com.nuvio.app.features.player + +import kotlin.test.Test +import kotlin.test.assertEquals + +class PlayerTrackSelectionTest { + + @Test + fun forcedSelectionUsesPrimaryPreferredLanguageInsteadOfTrackOrder() { + val tracks = listOf( + subtitleTrack(index = 0, language = "ja", isForced = true), + subtitleTrack(index = 1, language = "en", isForced = false), + subtitleTrack(index = 2, language = "en", isForced = true), + ) + + val selectedIndex = findPreferredSubtitleTrackIndex( + tracks = tracks, + targets = listOf("en"), + requireForced = true, + ) + + assertEquals(2, selectedIndex) + } + + @Test + fun forcedSelectionFallsBackToSecondaryPreferredLanguage() { + val tracks = listOf( + subtitleTrack(index = 0, language = "ja", isForced = true), + subtitleTrack(index = 1, language = "en", isForced = false), + subtitleTrack(index = 2, language = "fr", isForced = true), + ) + + val selectedIndex = findPreferredSubtitleTrackIndex( + tracks = tracks, + targets = listOf("en", "fr"), + requireForced = true, + ) + + assertEquals(2, selectedIndex) + } + + @Test + fun forcedSelectionRejectsTracksOutsidePreferredLanguages() { + val tracks = listOf( + subtitleTrack(index = 0, language = "ja", isForced = true), + subtitleTrack(index = 1, language = "en", isForced = false), + ) + + val selectedIndex = findPreferredSubtitleTrackIndex( + tracks = tracks, + targets = listOf("en"), + requireForced = true, + ) + + assertEquals(-1, selectedIndex) + } + + private fun subtitleTrack( + index: Int, + language: String, + isForced: Boolean, + ) = SubtitleTrack( + index = index, + id = "track-$index", + label = "Track $index", + language = language, + isForced = isForced, + ) +} From 079b3a7e97fdf6960a9a31cc908fa9df321c1bf8 Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:05:02 +0530 Subject: [PATCH 2/3] fix(player): enforce preferred-language subtitle filtering --- .../player/PlayerScreenRuntimeTrackActions.kt | 3 +- .../features/player/PlayerTrackSelection.kt | 31 ++++------- .../player/PlayerTrackSelectionTest.kt | 54 +++++++++++++++++++ 3 files changed, 64 insertions(+), 24 deletions(-) diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt index e4f5d2462..9a110d18b 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt @@ -17,11 +17,10 @@ internal val PlayerScreenRuntime.visibleAddonSubtitles: List get() = filterAddonSubtitlesForSettings( subtitles = addonSubtitles, settings = playerSettingsUiState, - selectedAddonSubtitleId = selectedAddonSubtitleId, ) internal val PlayerScreenRuntime.selectedAddonSubtitle: AddonSubtitle? - get() = addonSubtitles.firstOrNull { subtitle -> + get() = visibleAddonSubtitles.firstOrNull { subtitle -> subtitle.id == selectedAddonSubtitleId || subtitle.url == selectedAddonSubtitleId } diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt index 1fef16145..9bdfdc4c7 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt @@ -97,40 +97,27 @@ internal fun findPreferredSubtitleTrackIndex( internal fun filterAddonSubtitlesForSettings( subtitles: List, settings: PlayerSettingsUiState, - selectedAddonSubtitleId: String?, ): List { val shouldFilter = settings.subtitleStyle.showOnlyPreferredLanguages || settings.addonSubtitleStartupMode == AddonSubtitleStartupMode.PREFERRED_ONLY if (!shouldFilter) return subtitles val targets = preferredSubtitleTargetsForSettings(settings) - if (targets.isEmpty()) { - return subtitles.filter { subtitle -> - subtitle.id == selectedAddonSubtitleId || subtitle.url == selectedAddonSubtitleId + if (targets.isEmpty()) return emptyList() + + return subtitles.filter { subtitle -> + targets.any { target -> + languageMatchesPreference( + trackLanguage = subtitle.language, + targetLanguage = target, + ) } } - - val filtered = subtitles.filter { subtitle -> - subtitle.id == selectedAddonSubtitleId || - subtitle.url == selectedAddonSubtitleId || - targets.any { target -> - languageMatchesPreference( - trackLanguage = subtitle.language, - targetLanguage = target, - ) - } - } - return filtered } internal fun preferredSubtitleTargetsForSettings(settings: PlayerSettingsUiState): List { - val preferredLanguage = if (settings.subtitleStyle.useForcedSubtitles) { - SubtitleLanguageOption.FORCED - } else { - settings.preferredSubtitleLanguage - } return resolvePreferredSubtitleLanguageTargets( - preferredSubtitleLanguage = preferredLanguage, + preferredSubtitleLanguage = settings.preferredSubtitleLanguage, secondaryPreferredSubtitleLanguage = settings.secondaryPreferredSubtitleLanguage, deviceLanguages = DeviceLanguagePreferences.preferredLanguageCodes(), ).filterNot { it == SubtitleLanguageOption.FORCED } diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt index 050969f96..3e4fa2dc9 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt @@ -55,6 +55,49 @@ class PlayerTrackSelectionTest { assertEquals(-1, selectedIndex) } + @Test + fun preferredOnlyFilteringRemovesNonPreferredAddons() { + val subtitles = listOf( + addonSubtitle(id = "english", language = "en"), + addonSubtitle(id = "japanese", language = "ja"), + ) + val settings = PlayerSettingsUiState( + preferredSubtitleLanguage = "en", + subtitleStyle = SubtitleStyleState.DEFAULT.copy(showOnlyPreferredLanguages = true), + ) + + val visibleSubtitles = filterAddonSubtitlesForSettings( + subtitles = subtitles, + settings = settings, + ) + + assertEquals(listOf("english"), visibleSubtitles.map { it.id }) + } + + @Test + fun forcedToggleKeepsPreferredLanguagesForAddonFiltering() { + val subtitles = listOf( + addonSubtitle(id = "japanese", language = "ja"), + addonSubtitle(id = "french", language = "fr"), + addonSubtitle(id = "english", language = "en"), + ) + val settings = PlayerSettingsUiState( + preferredSubtitleLanguage = "en", + secondaryPreferredSubtitleLanguage = "fr", + subtitleStyle = SubtitleStyleState.DEFAULT.copy( + useForcedSubtitles = true, + showOnlyPreferredLanguages = true, + ), + ) + + val visibleSubtitles = filterAddonSubtitlesForSettings( + subtitles = subtitles, + settings = settings, + ) + + assertEquals(listOf("french", "english"), visibleSubtitles.map { it.id }) + } + private fun subtitleTrack( index: Int, language: String, @@ -66,4 +109,15 @@ class PlayerTrackSelectionTest { language = language, isForced = isForced, ) + + private fun addonSubtitle( + id: String, + language: String, + ) = AddonSubtitle( + id = id, + url = "https://example.com/$id.srt", + language = language, + display = id, + addonName = "Addon", + ) } From 9113b5567ae61943dfdf31a3a689c2a3574c74ea Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 18 Jul 2026 17:35:56 +0530 Subject: [PATCH 3/3] fix(player): make forced subtitle selection audio-aware --- .../composeResources/values/strings.xml | 2 +- .../player/PlayerScreenRuntimeTrackActions.kt | 93 ++++++---- .../features/player/PlayerTrackSelection.kt | 93 ++++++++-- .../player/PlayerTrackSelectionTest.kt | 169 ++++++++++++++++-- 4 files changed, 292 insertions(+), 65 deletions(-) diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 16bc657d6..1aef63cd2 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -1024,7 +1024,7 @@ Subtitle Size Text Color Use Forced Subtitles - Prefer forced subtitles when matching your subtitle language settings. + Prefer forced subtitles when audio matches the subtitle language; if unavailable, select nothing. Vertical Offset Skip Intro Use introdb.app to detect intros and recaps. diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt index 9a110d18b..c4715427c 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerScreenRuntimeTrackActions.kt @@ -146,23 +146,24 @@ internal fun PlayerScreenRuntime.refreshTracks() { restorePersistedTrackPreferenceIfNeeded() + val preferredAudioTargets = resolvePreferredAudioLanguageTargets( + preferredAudioLanguage = playerSettingsUiState.preferredAudioLanguage, + secondaryPreferredAudioLanguage = playerSettingsUiState.secondaryPreferredAudioLanguage, + deviceLanguages = DeviceLanguagePreferences.preferredLanguageCodes(), + contentOriginalLanguage = resolveContentLanguage( + language = metaUiState.meta?.language, + country = metaUiState.meta?.country, + ) ?: args.contentLanguage, + ) + if (!preferredAudioSelectionApplied) { - val preferredAudioTargets = resolvePreferredAudioLanguageTargets( - preferredAudioLanguage = playerSettingsUiState.preferredAudioLanguage, - secondaryPreferredAudioLanguage = playerSettingsUiState.secondaryPreferredAudioLanguage, - deviceLanguages = DeviceLanguagePreferences.preferredLanguageCodes(), - contentOriginalLanguage = resolveContentLanguage( - language = metaUiState.meta?.language, - country = metaUiState.meta?.country, - ) ?: args.contentLanguage, - ) if (preferredAudioTargets.isEmpty()) { preferredAudioSelectionApplied = true } else if (audioTracks.isNotEmpty()) { val preferredAudioIndex = findPreferredTrackIndex( tracks = audioTracks, targets = preferredAudioTargets, - language = { track -> track.language }, + language = ::resolveAudioTrackLanguageTarget, ) if (preferredAudioIndex >= 0 && preferredAudioIndex != selectedAudioIndex) { playerController?.selectAudioTrack(preferredAudioIndex) @@ -173,45 +174,69 @@ internal fun PlayerScreenRuntime.refreshTracks() { } if (!preferredSubtitleSelectionApplied) { - val preferredSubtitleTargets = resolvePreferredSubtitleLanguageTargets( - preferredSubtitleLanguage = playerSettingsUiState.preferredSubtitleLanguage, - secondaryPreferredSubtitleLanguage = playerSettingsUiState.secondaryPreferredSubtitleLanguage, - deviceLanguages = DeviceLanguagePreferences.preferredLanguageCodes(), + val preferredSubtitleLanguage = normalizeLanguageCode( + playerSettingsUiState.preferredSubtitleLanguage, ) + val preferredSubtitleTargets = if ( + preferredSubtitleLanguage == SubtitleLanguageOption.NONE || + preferredSubtitleLanguage == SubtitleLanguageOption.FORCED + ) { + emptyList() + } else { + resolvePreferredSubtitleLanguageTargets( + preferredSubtitleLanguage = playerSettingsUiState.preferredSubtitleLanguage, + secondaryPreferredSubtitleLanguage = playerSettingsUiState.secondaryPreferredSubtitleLanguage, + deviceLanguages = DeviceLanguagePreferences.preferredLanguageCodes(), + ) + } + val selectedAudioTrack = audioTracks.firstOrNull { track -> track.index == selectedAudioIndex } + ?: audioTracks.firstOrNull { it.isSelected } + val selectionPlan = resolveSubtitleAutoSelectionPlan( + selectedAudioLanguage = resolveAudioTrackLanguageTarget(selectedAudioTrack), + preferredAudioTargets = preferredAudioTargets, + preferredSubtitleTargets = preferredSubtitleTargets, + useForcedSubtitles = subtitleStyle.useForcedSubtitles, + ) + if (selectionPlan == null) { + disableAutomaticSubtitleSelection() + return + } - if (preferredSubtitleTargets.isEmpty()) { - if (selectedSubtitleIndex != -1 || subtitleTracks.any { it.isSelected }) { - playerController?.selectSubtitleTrack(-1) - } - selectedSubtitleIndex = -1 - selectedAddonSubtitleId = null - useCustomSubtitles = false + if (selectionPlan.targets.isEmpty()) { + disableAutomaticSubtitleSelection() preferredSubtitleSelectionApplied = true } else if (subtitleTracks.isNotEmpty()) { val preferredSubtitleIndex = findPreferredSubtitleTrackIndex( tracks = subtitleTracks, - targets = preferredSubtitleTargets, - requireForced = subtitleStyle.useForcedSubtitles, + targets = selectionPlan.targets, + mode = selectionPlan.mode, ) if (preferredSubtitleIndex >= 0 && preferredSubtitleIndex != selectedSubtitleIndex) { playerController?.selectSubtitleTrack(preferredSubtitleIndex) selectedSubtitleIndex = preferredSubtitleIndex selectedAddonSubtitleId = null useCustomSubtitles = false - } else if ( - preferredSubtitleIndex < 0 && - (subtitleStyle.useForcedSubtitles || - normalizeLanguageCode(playerSettingsUiState.preferredSubtitleLanguage) == - SubtitleLanguageOption.FORCED) - ) { - if (selectedSubtitleIndex != -1 || subtitleTracks.any { it.isSelected }) { - playerController?.selectSubtitleTrack(-1) + } else if (preferredSubtitleIndex < 0) { + val activeSubtitleTrack = subtitleTracks.firstOrNull { track -> + track.index == selectedSubtitleIndex + } ?: subtitleTracks.firstOrNull { it.isSelected } + if ( + selectionPlan.mode == SubtitleAutoSelectionMode.FORCED_ONLY || + activeSubtitleTrack?.isForced == true + ) { + disableAutomaticSubtitleSelection() } - selectedSubtitleIndex = -1 - selectedAddonSubtitleId = null - useCustomSubtitles = false } preferredSubtitleSelectionApplied = true } } } + +private fun PlayerScreenRuntime.disableAutomaticSubtitleSelection() { + if (selectedSubtitleIndex != -1 || subtitleTracks.any { it.isSelected }) { + playerController?.selectSubtitleTrack(-1) + } + selectedSubtitleIndex = -1 + selectedAddonSubtitleId = null + useCustomSubtitles = false +} diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt index 9bdfdc4c7..a9bdeb4aa 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/player/PlayerTrackSelection.kt @@ -65,28 +65,93 @@ internal fun findPreferredTrackIndex( return -1 } +internal enum class SubtitleAutoSelectionMode { + FORCED_ONLY, + NORMAL_ONLY, +} + +internal data class SubtitleAutoSelectionPlan( + val targets: List, + val mode: SubtitleAutoSelectionMode, +) + +internal fun resolveAudioTrackLanguageTarget(track: AudioTrack?): String? { + if (track == null) return null + + val directLanguage = normalizeLanguageCode(track.language) + ?.takeUnless { it == "und" || it == "unknown" } + if (directLanguage != null) return directLanguage + + val selectableLanguages = AvailableLanguageOptions + .mapNotNull { option -> normalizeLanguageCode(option.code) } + .toSet() + return listOf(track.label, track.id).firstNotNullOfOrNull { value -> + normalizeLanguageCode(value)?.takeIf(selectableLanguages::contains) + } +} + +internal fun resolveSubtitleAutoSelectionPlan( + selectedAudioLanguage: String?, + preferredAudioTargets: List, + preferredSubtitleTargets: List, + useForcedSubtitles: Boolean, +): SubtitleAutoSelectionPlan? { + val normalizedAudioLanguage = normalizeLanguageCode(selectedAudioLanguage) + if (useForcedSubtitles && normalizedAudioLanguage == null) return null + + val subtitleTargets = preferredSubtitleTargets + .mapNotNull(::normalizeLanguageCode) + .filterNot { target -> + target == SubtitleLanguageOption.NONE || + target == SubtitleLanguageOption.FORCED || + target == AudioLanguageOption.DEFAULT + } + .distinct() + val primarySubtitleTarget = subtitleTargets.firstOrNull() + val forcedTarget = when { + !useForcedSubtitles -> null + primarySubtitleTarget != null && + normalizedAudioLanguage != null && + languageMatchesPreference(normalizedAudioLanguage, primarySubtitleTarget) -> + primarySubtitleTarget + primarySubtitleTarget == null && + normalizedAudioLanguage != null && + preferredAudioTargets.any { target -> + languageMatchesPreference(normalizedAudioLanguage, target) + } -> normalizedAudioLanguage + else -> null + } + + return SubtitleAutoSelectionPlan( + targets = forcedTarget?.let(::listOf) ?: subtitleTargets, + mode = if (forcedTarget != null) { + SubtitleAutoSelectionMode.FORCED_ONLY + } else { + SubtitleAutoSelectionMode.NORMAL_ONLY + }, + ) +} + internal fun findPreferredSubtitleTrackIndex( tracks: List, targets: List, - requireForced: Boolean = false, + mode: SubtitleAutoSelectionMode, ): Int { if (targets.isEmpty()) return -1 - for ((targetPosition, target) in targets.withIndex()) { + for (target in targets) { val normalizedTarget = normalizeLanguageCode(target) ?: continue - if (normalizedTarget == SubtitleLanguageOption.FORCED) { - val forcedIndex = tracks.indexOfFirst { it.isForced } - if (forcedIndex >= 0) return forcedIndex - if (targetPosition == 0) return -1 - continue - } - val matchIndex = tracks.indexOfFirst { track -> - (!requireForced || track.isForced) && - languageMatchesPreference( - trackLanguage = track.language, - targetLanguage = normalizedTarget, - ) + when (mode) { + SubtitleAutoSelectionMode.FORCED_ONLY -> track.isForced + SubtitleAutoSelectionMode.NORMAL_ONLY -> !track.isForced + } && + listOf(track.language, track.label, track.id).any { trackLanguage -> + languageMatchesPreference( + trackLanguage = trackLanguage, + targetLanguage = normalizedTarget, + ) + } } if (matchIndex >= 0) return matchIndex } diff --git a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt index 3e4fa2dc9..3e6f98a8b 100644 --- a/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt +++ b/composeApp/src/commonTest/kotlin/com/nuvio/app/features/player/PlayerTrackSelectionTest.kt @@ -2,6 +2,8 @@ package com.nuvio.app.features.player import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull class PlayerTrackSelectionTest { @@ -16,27 +18,25 @@ class PlayerTrackSelectionTest { val selectedIndex = findPreferredSubtitleTrackIndex( tracks = tracks, targets = listOf("en"), - requireForced = true, + mode = SubtitleAutoSelectionMode.FORCED_ONLY, ) assertEquals(2, selectedIndex) } @Test - fun forcedSelectionFallsBackToSecondaryPreferredLanguage() { - val tracks = listOf( - subtitleTrack(index = 0, language = "ja", isForced = true), - subtitleTrack(index = 1, language = "en", isForced = false), - subtitleTrack(index = 2, language = "fr", isForced = true), + fun matchingAudioUsesForcedOnlyPrimarySubtitleTarget() { + val plan = assertNotNull( + resolveSubtitleAutoSelectionPlan( + selectedAudioLanguage = "en", + preferredAudioTargets = listOf("en"), + preferredSubtitleTargets = listOf("en", "fr"), + useForcedSubtitles = true, + ), ) - val selectedIndex = findPreferredSubtitleTrackIndex( - tracks = tracks, - targets = listOf("en", "fr"), - requireForced = true, - ) - - assertEquals(2, selectedIndex) + assertEquals(listOf("en"), plan.targets) + assertEquals(SubtitleAutoSelectionMode.FORCED_ONLY, plan.mode) } @Test @@ -49,12 +49,148 @@ class PlayerTrackSelectionTest { val selectedIndex = findPreferredSubtitleTrackIndex( tracks = tracks, targets = listOf("en"), - requireForced = true, + mode = SubtitleAutoSelectionMode.FORCED_ONLY, ) assertEquals(-1, selectedIndex) } + @Test + fun differentAudioUsesNormalPreferredSubtitles() { + val tracks = listOf( + subtitleTrack(index = 0, language = "en", isForced = true), + subtitleTrack(index = 1, language = "en", isForced = false), + ) + val plan = assertNotNull( + resolveSubtitleAutoSelectionPlan( + selectedAudioLanguage = "ja", + preferredAudioTargets = listOf("ja"), + preferredSubtitleTargets = listOf("en", "fr"), + useForcedSubtitles = true, + ), + ) + + val selectedIndex = findPreferredSubtitleTrackIndex( + tracks = tracks, + targets = plan.targets, + mode = plan.mode, + ) + + assertEquals(SubtitleAutoSelectionMode.NORMAL_ONLY, plan.mode) + assertEquals(1, selectedIndex) + } + + @Test + fun audioMatchingOnlySecondarySubtitleTargetUsesNormalSubtitles() { + val plan = assertNotNull( + resolveSubtitleAutoSelectionPlan( + selectedAudioLanguage = "fr", + preferredAudioTargets = listOf("fr"), + preferredSubtitleTargets = listOf("en", "fr"), + useForcedSubtitles = true, + ), + ) + + assertEquals(listOf("en", "fr"), plan.targets) + assertEquals(SubtitleAutoSelectionMode.NORMAL_ONLY, plan.mode) + } + + @Test + fun forcedToggleOffExcludesForcedTracks() { + val tracks = listOf( + subtitleTrack(index = 0, language = "en", isForced = true), + subtitleTrack(index = 1, language = "en", isForced = false), + ) + val plan = assertNotNull( + resolveSubtitleAutoSelectionPlan( + selectedAudioLanguage = "en", + preferredAudioTargets = listOf("en"), + preferredSubtitleTargets = listOf("en"), + useForcedSubtitles = false, + ), + ) + + val selectedIndex = findPreferredSubtitleTrackIndex( + tracks = tracks, + targets = plan.targets, + mode = plan.mode, + ) + + assertEquals(SubtitleAutoSelectionMode.NORMAL_ONLY, plan.mode) + assertEquals(1, selectedIndex) + } + + @Test + fun forcedToggleOffRejectsForcedOnlyTrackList() { + val selectedIndex = findPreferredSubtitleTrackIndex( + tracks = listOf(subtitleTrack(index = 0, language = "en", isForced = true)), + targets = listOf("en"), + mode = SubtitleAutoSelectionMode.NORMAL_ONLY, + ) + + assertEquals(-1, selectedIndex) + } + + @Test + fun forcedModeWithoutSubtitleTargetUsesMatchingSelectedAudioLanguage() { + val plan = assertNotNull( + resolveSubtitleAutoSelectionPlan( + selectedAudioLanguage = "ja", + preferredAudioTargets = listOf("ja"), + preferredSubtitleTargets = emptyList(), + useForcedSubtitles = true, + ), + ) + + assertEquals(listOf("ja"), plan.targets) + assertEquals(SubtitleAutoSelectionMode.FORCED_ONLY, plan.mode) + } + + @Test + fun forcedModeWaitsUntilSelectedAudioIsKnown() { + val plan = resolveSubtitleAutoSelectionPlan( + selectedAudioLanguage = null, + preferredAudioTargets = listOf("en"), + preferredSubtitleTargets = listOf("en"), + useForcedSubtitles = true, + ) + + assertNull(plan) + } + + @Test + fun resolvesSelectedAudioLanguageFromTrackLabel() { + val target = resolveAudioTrackLanguageTarget( + AudioTrack( + index = 0, + id = "audio-0", + label = "English Original", + language = null, + isSelected = true, + ), + ) + + assertEquals("en", target) + } + + @Test + fun forcedSelectionMatchesSubtitleLanguageFromTrackLabel() { + val selectedIndex = findPreferredSubtitleTrackIndex( + tracks = listOf( + subtitleTrack( + index = 0, + language = null, + label = "English Forced", + isForced = true, + ), + ), + targets = listOf("en"), + mode = SubtitleAutoSelectionMode.FORCED_ONLY, + ) + + assertEquals(0, selectedIndex) + } + @Test fun preferredOnlyFilteringRemovesNonPreferredAddons() { val subtitles = listOf( @@ -100,12 +236,13 @@ class PlayerTrackSelectionTest { private fun subtitleTrack( index: Int, - language: String, + language: String?, + label: String = "Track $index", isForced: Boolean, ) = SubtitleTrack( index = index, id = "track-$index", - label = "Track $index", + label = label, language = language, isForced = isForced, )