From c05301bae8d5065d301cbd7a3c761a5c58d774ee Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 17 Aug 2026 21:02:47 +0200 Subject: [PATCH 1/3] fix: group player subtitles by resolved language name - Group subtitle tracks by language name derived from IETF tag instead of raw originalName (e.g. "fr" and "French [SUB]" now land in the same "French" group) - Fall back to "Unknown" group when language cannot be resolved - Label tracks within a group by originalName instead of nameSuffix - Filter subtitles by IETF tag match instead of name substring, matching primary subtag so regional variants (pt-br) match (pt) - Keep subtitles with unresolvable language instead of dropping them - Only exempt DOWNLOADED_FILE from filtering so embedded tracks are also filtered across player reloads - Use getApiProviderLangSettings + fromCodeToLangTagIETF for proper tag normalization in the filter list --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 30 +++++++++++-------- .../ui/player/PlayerGeneratorViewModel.kt | 12 ++++++-- app/src/main/res/values/strings.xml | 1 + 3 files changed, 28 insertions(+), 15 deletions(-) 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 4495a560262..7df9d814221 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 @@ -47,6 +47,7 @@ import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull +import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.CloudStreamApp import com.lagradost.cloudstream3.CloudStreamApp.Companion.setKey import com.lagradost.cloudstream3.CommonActivity.showToast @@ -101,6 +102,7 @@ import com.lagradost.cloudstream3.ui.settings.Globals.isLayout import com.lagradost.cloudstream3.ui.subtitles.SUBTITLE_AUTO_SELECT_KEY import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment.Companion.getAutoSelectLanguageTagIETF +import com.lagradost.cloudstream3.utils.AppContextUtils.getApiProviderLangSettings import com.lagradost.cloudstream3.utils.AppContextUtils.getShortSeasonText import com.lagradost.cloudstream3.utils.AppContextUtils.html import com.lagradost.cloudstream3.utils.AppContextUtils.sortSubs @@ -112,7 +114,7 @@ import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.ExtractorLinkType import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog -import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToEnglishLanguageName +import com.lagradost.cloudstream3.utils.SubtitleHelper.fromCodeToLangTagIETF import com.lagradost.cloudstream3.utils.SubtitleHelper.fromTagToLanguageName import com.lagradost.cloudstream3.utils.SubtitleHelper.languages import com.lagradost.cloudstream3.utils.UIHelper.clipboardHelper @@ -1195,8 +1197,14 @@ class GeneratorPlayer : FullScreenPlayer() { ArrayAdapter(ctx, R.layout.sort_bottom_single_choice) subsArrayAdapter.add(ctx.getString(R.string.no_subtitles).html()) + val unknownGroupName = ctx.getString(R.string.subtitles_group_unknown) + fun groupName(sub: SubtitleData): String { + return fromTagToLanguageName(sub.getIETF_tag())?.takeIf { it.isNotBlank() } + ?: unknownGroupName + } + val subtitlesGrouped = - currentSubtitles.groupBy { it.originalName }.map { (key, value) -> + currentSubtitles.groupBy { groupName(it) }.map { (key, value) -> key to value.sortedBy { it.nameSuffix.toIntOrNull() ?: 0 } }.toMap() val subtitlesGroupedList = subtitlesGrouped.entries.toList() @@ -1204,11 +1212,11 @@ class GeneratorPlayer : FullScreenPlayer() { val subtitles = subtitlesGrouped.map { it.key.html() } val subtitleGroupIndexStart = - subtitlesGrouped.keys.indexOf(currentSelectedSubtitles?.originalName) + 1 + subtitlesGrouped.keys.indexOf(currentSelectedSubtitles?.let { groupName(it) }) + 1 var subtitleGroupIndex = subtitleGroupIndexStart val subtitleOptionIndexStart = - subtitlesGrouped[currentSelectedSubtitles?.originalName]?.indexOfFirst { it.nameSuffix == currentSelectedSubtitles?.nameSuffix } + subtitlesGrouped[currentSelectedSubtitles?.let { groupName(it) }]?.indexOfFirst { it.nameSuffix == currentSelectedSubtitles?.nameSuffix } ?: 0 var subtitleOptionIndex = subtitleOptionIndexStart @@ -1232,8 +1240,8 @@ class GeneratorPlayer : FullScreenPlayer() { val subtitleOptions = subtitlesGroupedList .getOrNull(subtitleGroupIndex - 1)?.value?.map { subtitle -> - val nameSuffix = subtitle.nameSuffix.html() - nameSuffix.ifBlank { + val label = subtitle.originalName.html() + label.ifBlank { when (subtitle.origin) { SubtitleOrigin.URL -> txt(R.string.subtitles_from_online) SubtitleOrigin.DOWNLOADED_FILE -> txt(R.string.downloaded) @@ -2264,12 +2272,10 @@ class GeneratorPlayer : FullScreenPlayer() { viewModel.filterSubByLang = settingsManager.getBoolean(getString(R.string.filter_sub_lang_key), false) if (viewModel.filterSubByLang) { - val langFromPrefMedia = settingsManager.getStringSet( - this.getString(R.string.provider_lang_key), mutableSetOf("en") - ) - viewModel.langFilterList = langFromPrefMedia?.mapNotNull { - fromTagToEnglishLanguageName(it)?.lowercase() ?: return@mapNotNull null - } ?: listOf() + viewModel.langFilterList = ctx.getApiProviderLangSettings().map { tag -> + if (tag == AllLanguagesName) tag + else fromCodeToLangTagIETF(tag)?.lowercase() ?: tag.lowercase() + } } // Set up TV clock visibility diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt index cb8cf8bfff5..0e1e381c88b 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/PlayerGeneratorViewModel.kt @@ -5,6 +5,7 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.lagradost.cloudstream3.AllLanguagesName import com.lagradost.cloudstream3.LoadResponse import com.lagradost.cloudstream3.mvvm.Resource import com.lagradost.cloudstream3.mvvm.launchSafe @@ -371,13 +372,18 @@ class PlayerGeneratorViewModel : ViewModel() { return true } - /** Only filter out subtitles fetched online */ - if (subtitle.origin != SubtitleOrigin.URL) { + if (langFilterList.contains(AllLanguagesName)) { return true } + if (subtitle.origin == SubtitleOrigin.DOWNLOADED_FILE) { + return true + } + + val subtitleTag = subtitle.getIETF_tag()?.lowercase() ?: return true + return langFilterList.any { lang -> - subtitle.originalName.contains(lang, ignoreCase = true) + subtitleTag == lang || subtitleTag.substringBefore('-') == lang.substringBefore('-') } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7d530b59f6e..3b90ebc5a61 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -487,6 +487,7 @@ Remove closed captions from subtitles Remove bloat from subtitles Filter by preferred media language + Unknown Extras Trailer https://example.com/example.mp4 From c248b4ee697eeebc43969cc66bae9cca6e442061 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 8 Sep 2026 15:48:15 +0200 Subject: [PATCH 2/3] fix: compute subtitle group names once into a list Replace the groupName() helper with a single pass that pairs each subtitle with its resolved language name, then derive the grouping, the selected group and both start indices from that list instead of recomputing the name on demand. Generated with AI --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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 7df9d814221..7f693bb6784 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 @@ -1198,25 +1198,28 @@ class GeneratorPlayer : FullScreenPlayer() { subsArrayAdapter.add(ctx.getString(R.string.no_subtitles).html()) val unknownGroupName = ctx.getString(R.string.subtitles_group_unknown) - fun groupName(sub: SubtitleData): String { - return fromTagToLanguageName(sub.getIETF_tag())?.takeIf { it.isNotBlank() } - ?: unknownGroupName + val subtitlesWithGroup = currentSubtitles.map { sub -> + (fromTagToLanguageName(sub.getIETF_tag())?.takeIf { it.isNotBlank() } + ?: unknownGroupName) to sub } val subtitlesGrouped = - currentSubtitles.groupBy { groupName(it) }.map { (key, value) -> + subtitlesWithGroup.groupBy({ it.first }, { it.second }).map { (key, value) -> key to value.sortedBy { it.nameSuffix.toIntOrNull() ?: 0 } }.toMap() val subtitlesGroupedList = subtitlesGrouped.entries.toList() val subtitles = subtitlesGrouped.map { it.key.html() } + val selectedGroup = + subtitlesWithGroup.firstOrNull { it.second == currentSelectedSubtitles }?.first + val subtitleGroupIndexStart = - subtitlesGrouped.keys.indexOf(currentSelectedSubtitles?.let { groupName(it) }) + 1 + subtitlesGrouped.keys.indexOf(selectedGroup) + 1 var subtitleGroupIndex = subtitleGroupIndexStart val subtitleOptionIndexStart = - subtitlesGrouped[currentSelectedSubtitles?.let { groupName(it) }]?.indexOfFirst { it.nameSuffix == currentSelectedSubtitles?.nameSuffix } + subtitlesGrouped[selectedGroup]?.indexOfFirst { it.nameSuffix == currentSelectedSubtitles?.nameSuffix } ?: 0 var subtitleOptionIndex = subtitleOptionIndexStart From e01c5f39602864c25ace2d51d764d3065b393c9b Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 8 Sep 2026 15:48:15 +0200 Subject: [PATCH 3/3] fix: disambiguate subtitle tracks with identical original names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Number tracks that share the exact same original name within a language group (e.g. two "Français" tracks become "Français 1" and "Français 2"), reusing the track's own nameSuffix when it has one and generating a sequential number otherwise. Unique names are untouched. Generated with AI --- .../cloudstream3/ui/player/GeneratorPlayer.kt | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) 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 7f693bb6784..b05eb3488b7 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 @@ -1240,19 +1240,30 @@ class GeneratorPlayer : FullScreenPlayer() { fun updateSubtitleOptionList() { subsOptionsArrayAdapter.clear() + val groupSubtitles = subtitlesGroupedList.getOrNull(subtitleGroupIndex - 1)?.value + + val duplicateNames = groupSubtitles.orEmpty() + .groupingBy { it.originalName }.eachCount().filterValues { it > 1 }.keys + val nameIndex = mutableMapOf() + val subtitleOptions = - subtitlesGroupedList - .getOrNull(subtitleGroupIndex - 1)?.value?.map { subtitle -> - val label = subtitle.originalName.html() - label.ifBlank { - when (subtitle.origin) { - SubtitleOrigin.URL -> txt(R.string.subtitles_from_online) - SubtitleOrigin.DOWNLOADED_FILE -> txt(R.string.downloaded) - SubtitleOrigin.EMBEDDED_IN_VIDEO -> txt(R.string.subtitles_from_embedded) - }.asString(ctx).toSpanned() + groupSubtitles?.map { subtitle -> + val label = if (subtitle.originalName in duplicateNames) { + val suffix = subtitle.nameSuffix.ifBlank { + val next = (nameIndex[subtitle.originalName] ?: 0) + 1 + nameIndex[subtitle.originalName] = next + next.toString() } + "${subtitle.originalName} $suffix" + } else subtitle.originalName + label.html().ifBlank { + when (subtitle.origin) { + SubtitleOrigin.URL -> txt(R.string.subtitles_from_online) + SubtitleOrigin.DOWNLOADED_FILE -> txt(R.string.downloaded) + SubtitleOrigin.EMBEDDED_IN_VIDEO -> txt(R.string.subtitles_from_embedded) + }.asString(ctx).toSpanned() } - ?: emptyList() + } ?: emptyList() // Show nothing if there is nothing to select val shouldHide = subtitleOptions.size < 2