From 7910e069874675271ed2050048037218206dae14 Mon Sep 17 00:00:00 2001 From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com> Date: Sat, 24 Jan 2026 23:15:17 +0000 Subject: [PATCH] Fix subtitle selection --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 31 ++--- .../ui/player/PlayerSubtitleHelper.kt | 53 ++++++++ .../cloudstream3/SubtitleSelectionTest.kt | 120 ++++++++++++++++++ 3 files changed, 186 insertions(+), 18 deletions(-) create mode 100644 app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index 1bd0b158feb..7d188e55d69 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -181,17 +181,17 @@ class GeneratorPlayer : FullScreenPlayer() { binding?.playerLoadingOverlay?.isVisible = true } - private fun setSubtitles(subtitle: SubtitleData?): Boolean { - // If subtitle is changed -> Save the language - if (subtitle != currentSelectedSubtitles) { + private fun setSubtitles(subtitle: SubtitleData?, userInitiated: Boolean): Boolean { + // If subtitle is changed and user initiated -> Save the language + if (subtitle != currentSelectedSubtitles && userInitiated) { val subtitleLanguageTagIETF = if (subtitle == null) { "" // -> No Subtitles } else { - fromCodeToLangTagIETF(subtitle.languageCode) - ?: fromLanguageToTagIETF(subtitle.languageCode, halfMatch = true) + subtitle.getIETF_tag() } if (subtitleLanguageTagIETF != null) { + Log.i(TAG, "Set SUBTITLE_AUTO_SELECT_KEY to '$subtitleLanguageTagIETF'") setKey(SUBTITLE_AUTO_SELECT_KEY, subtitleLanguageTagIETF) preferredAutoSelectSubtitles = subtitleLanguageTagIETF } @@ -225,7 +225,7 @@ class GeneratorPlayer : FullScreenPlayer() { } private fun noSubtitles(): Boolean { - return setSubtitles(null) + return setSubtitles(null, true) } private fun getPos(): Long { @@ -909,7 +909,7 @@ class GeneratorPlayer : FullScreenPlayer() { player.saveData() player.reloadPlayer(ctx) - setSubtitles(selectedSubtitle) + setSubtitles(selectedSubtitle, false) viewModel.addSubtitles(subtitleData.toSet()) selectSourceDialog?.dismissSafe() @@ -1362,7 +1362,7 @@ class GeneratorPlayer : FullScreenPlayer() { subtitlesGroupedList.getOrNull(subtitleGroupIndex - 1)?.value?.getOrNull( subtitleOptionIndex )?.let { - setSubtitles(it) + setSubtitles(it, true) } ?: false } } @@ -1659,12 +1659,6 @@ class GeneratorPlayer : FullScreenPlayer() { } } - private fun SubtitleData.matchesLanguage(langCode: String): Boolean { - val langName = fromTagToEnglishLanguageName(langCode) ?: return false - val cleanedName = originalName.replace(Regex("[^\\p{L}\\p{Mn}\\p{Mc}\\p{Me} ]"), "").trim() - return languageCode == langCode || cleanedName == langName || cleanedName.contains(langName) || cleanedName == langCode - } - private fun getAutoSelectSubtitle( subtitles: Set, settings: Boolean, downloads: Boolean ): SubtitleData? { @@ -1684,8 +1678,9 @@ class GeneratorPlayer : FullScreenPlayer() { val current = player.getCurrentPreferredSubtitle() Log.i(TAG, "autoSelectFromSettings = $current") context?.let { ctx -> - if (current != null) { - if (setSubtitles(current)) { + // Only use the player preferred subtitle if it matches the available language + if (current != null && (langCode == null || current.matchesLanguage(langCode))) { + if (setSubtitles(current, false)) { player.saveData() player.reloadPlayer(ctx) player.handleEvent(CSPlayerEvent.Play) @@ -1695,7 +1690,7 @@ class GeneratorPlayer : FullScreenPlayer() { getAutoSelectSubtitle( currentSubs, settings = true, downloads = false )?.let { sub -> - if (setSubtitles(sub)) { + if (setSubtitles(sub, false)) { player.saveData() player.reloadPlayer(ctx) player.handleEvent(CSPlayerEvent.Play) @@ -1711,7 +1706,7 @@ class GeneratorPlayer : FullScreenPlayer() { if (player.getCurrentPreferredSubtitle() == null) { getAutoSelectSubtitle(currentSubs, settings = false, downloads = true)?.let { sub -> context?.let { ctx -> - if (setSubtitles(sub)) { + if (setSubtitles(sub, false)) { player.saveData() player.reloadPlayer(ctx) player.handleEvent(CSPlayerEvent.Play) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt index d9e8963e49b..ba32a19e532 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerSubtitleHelper.kt @@ -11,7 +11,10 @@ import androidx.media3.ui.SubtitleView import com.lagradost.cloudstream3.SubtitleFile import com.lagradost.cloudstream3.ui.subtitles.SaveCaptionStyle import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.setSubtitleViewStyle +import com.lagradost.cloudstream3.utils.SubtitleHelper +import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.UIHelper.toPx +import me.xdrop.fuzzywuzzy.FuzzySearch enum class SubtitleStatus { IS_ACTIVE, @@ -47,6 +50,56 @@ data class SubtitleData( else "$url|$name" } + /** Returns true if langCode is the same as the IETF tag */ + fun matchesLanguage(langCode: String): Boolean { + return getIETF_tag() == langCode + } + + /** Tries hard to figure out a valid IETF tag based on language code and name. Will return null if not found. */ + fun getIETF_tag(): String? { + val tag = fromCodeToLangTagIETF(this.languageCode) + if (tag != null) { + return tag + } + + // Remove any numbers to make matching better + val cleanedLanguage = originalName.replace(Regex("[0-9]"), "").trim() + + // First go for exact matches + SubtitleHelper.languages.forEach { language -> + if (language.languageName.equals(cleanedLanguage, ignoreCase = true) || + language.nativeName.equals(cleanedLanguage, ignoreCase = true) || + // Also match exact IETF tags + language.IETF_tag.equals(cleanedLanguage, ignoreCase = true) + ) { + return language.IETF_tag + } + } + + var closestMatch: Pair = null to 0 + // Then go for partial matches, however only use the best match + SubtitleHelper.languages.forEach { language -> + val lowerCleaned = cleanedLanguage.lowercase() + val score = maxOf( + FuzzySearch.ratio(lowerCleaned, language.languageName.lowercase()), + FuzzySearch.ratio( + lowerCleaned, language.nativeName.lowercase() + ) + ) + + // Arbitrary cutoff at 80. + if (cleanedLanguage.contains(language.languageName, ignoreCase = true) || + cleanedLanguage.contains(language.nativeName, ignoreCase = true) || score > 80 + ) { + if (score > closestMatch.second) { + closestMatch = language.IETF_tag to score + } + } + } + + return closestMatch.first + } + val name = "$originalName $nameSuffix" /** diff --git a/app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt b/app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt new file mode 100644 index 00000000000..b0233f58d5f --- /dev/null +++ b/app/src/test/java/com/lagradost/cloudstream3/SubtitleSelectionTest.kt @@ -0,0 +1,120 @@ +package com.lagradost.cloudstream3 + +import com.lagradost.cloudstream3.ui.player.SubtitleData +import com.lagradost.cloudstream3.ui.player.SubtitleOrigin +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Test + +/** Ensure partial subtitle language finding is reliable. */ +class SubtitleLanguageTagTest { + fun getQuickSubtitle(originalName: String, languageCode: String?): SubtitleData { + return SubtitleData( + originalName = originalName, + nameSuffix = "1", + url = "https://example.com/test.vtt", + origin = SubtitleOrigin.URL, + mimeType = "text/vtt", + headers = emptyMap(), + languageCode = languageCode + ) + } + + @Test + fun `returns languageCode directly if already valid IETF tag`() { + val subtitle = getQuickSubtitle( + originalName = "Anything", + languageCode = "en" + ) + + assertEquals("en", subtitle.getIETF_tag()) + } + + @Test + fun `matches exact language name`() { + val subtitle = getQuickSubtitle( + originalName = "English", + languageCode = null + ) + + assertEquals("en", subtitle.getIETF_tag()) + } + + @Test + fun `matches native language name`() { + val subtitle = getQuickSubtitle( + originalName = "Español", + languageCode = null + ) + + assertEquals("es", subtitle.getIETF_tag()) + } + + @Test + fun `matches fuzzy partial language name`() { + val subtitle = getQuickSubtitle( + originalName = "English [SUB]", + languageCode = null + ) + + assertEquals("en", subtitle.getIETF_tag()) + } + + @Test + fun `returns null when no language matches`() { + val subtitle = getQuickSubtitle( + originalName = "Klingon", + languageCode = null + ) + + assertNull(subtitle.getIETF_tag()) + } + + + @Test + fun `returns the correct language variant`() { + val subtitle1 = getQuickSubtitle( + originalName = "Chinese", + languageCode = null + ) + val subtitle2 = getQuickSubtitle( + originalName = "Chinese (subtitle)", + languageCode = null + ) + val subtitleSimplified1 = getQuickSubtitle( + originalName = "Chinese (simplified)", + languageCode = null + ) + val subtitleSimplified2 = getQuickSubtitle( + originalName = "Chinese - simplified", + languageCode = null + ) + val subtitleSimplified3 = getQuickSubtitle( + originalName = "Chinese simplified", + languageCode = "zh-" + ) + val subtitleSimplified4 = getQuickSubtitle( + originalName = "Chinese (simplified)2", + languageCode = "zh-hans" + ) + + assertEquals("zh", subtitle1.getIETF_tag()) + assertEquals("zh", subtitle2.getIETF_tag()) + assertEquals("zh-hans", subtitleSimplified1.getIETF_tag()) + assertEquals("zh-hans", subtitleSimplified2.getIETF_tag()) + assertEquals("zh-hans", subtitleSimplified3.getIETF_tag()) + assertEquals("zh-hans", subtitleSimplified4.getIETF_tag()) + } + + + @Test + fun `returns exact language matches`() { + val subtitle = getQuickSubtitle( + originalName = "en", + languageCode = null + ) + + assertEquals("en", subtitle.getIETF_tag()) + } +} +