From e07df088844f13b17620496394e5a203e5db9412 Mon Sep 17 00:00:00 2001 From: forbiders <339140124+forbiders@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:55:12 +0000 Subject: [PATCH 1/3] Add source regex to source priority system --- .../ui/player/PlayerGeneratorViewModel.kt | 58 +++++++++++++++---- .../source_priority/QualityDataHelper.kt | 2 + .../source_priority/SourcePriorityDialog.kt | 29 ++++++++++ .../player_select_source_priority.xml | 7 +++ .../layout/player_select_source_priority.xml | 7 +++ 5 files changed, 91 insertions(+), 12 deletions(-) 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..7392c7d4529 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 @@ -66,7 +66,7 @@ data class VideoState( * * sortedBy is not exactly expensive, but each hasNextMirror does it again, so this alleviates unnecessary recomputation * */ - private val sortedLinks: ConcurrentHashMap> = ConcurrentHashMap() + private val sortedLinks: ConcurrentHashMap> = ConcurrentHashMap() /** * The cache is guaranteed to be up to date link-wise due to the immutable links. @@ -95,24 +95,58 @@ data class VideoState( // It is by all standards, idempotent and by extension also pure as it has no "visible" side effect /** Returns .links in the sorted order according to the qualityProfile. * Use .links if order is not needed */ - @Contract(pure = true) fun sortLinks(qualityProfile: Int): List { - sortedLinks[qualityProfile]?.let { - return it - } - val hideNegativeSources = QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideNegativeSources) val hideErrorSources = QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideErrorSources) + // Read user's regex pattern and priority toggle from the quality profile. + // sourceRegex: comma-separated patterns. Empty = no filter. + // regexPriority: when true, ALL matching sources sort first (regardless of quality priority). + // when false, regex only breaks ties among equal-priority sources. + val sourceRegex = + QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.SourceRegex) + val regexPriority = + QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.SourceRegexPriority) + + // Parse comma-separated patterns into a single case-insensitive regex. + // Invalid regex returns null (no filtering, safe fallback). + val regex = sourceRegex.trim().takeIf { it.isNotEmpty() }?.let { pattern -> + val joined = pattern.split(",").map { it.trim() }.filter { it.isNotEmpty() }.joinToString("|") + runCatching { Regex(joined, RegexOption.IGNORE_CASE) }.getOrNull() + } + + // Cache key includes regex and priority flag so stale results aren't returned + // after the user changes either setting. + val cacheKey = if (regex != null) "$qualityProfile|${sourceRegex.trim()}|$regexPriority" else qualityProfile.toString() + + sortedLinks[cacheKey]?.let { + return it + } - return links.map { link -> - // negative because we want to sort highest quality first + val displayLinks = links.map { link -> link.toDisplayLink(qualityProfile, hideNegativeSources, hideErrorSources) - }.sortedBy { - // negative because we want to sort highest quality first - -it.priority - }.also { value -> sortedLinks[qualityProfile] = value } + } + + // Three modes: + // 1. No regex (or invalid): sort by priority only (original behavior). + // 2. Regex + prioritize ON: matching sources first (sorted by priority), then non-matching (by priority). + // This ensures all matching sources play before any non-matching source, regardless of quality priority. + // 3. Regex + prioritize OFF: priority is primary, regex breaks ties among equal-priority sources. + // This respects the user's quality priority while preferring matching sources within the same tier. + val sorted = if (regex != null && regexPriority) { + val (matched, rest) = displayLinks.partition { it.shouldUseLink && regex.containsMatchIn(it.link?.first?.name ?: "") } + matched.sortedBy { -it.priority } + rest.sortedBy { -it.priority } + } else if (regex != null) { + displayLinks.sortedWith( + compareByDescending { it.priority } + .thenByDescending { if (it.shouldUseLink && regex.containsMatchIn(it.link?.first?.name ?: "")) 1 else 0 } + ) + } else { + displayLinks.sortedBy { -it.priority } + } + + return sorted.also { value -> sortedLinks[cacheKey] = value } } @Contract(pure = true) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt index d8d47bd1023..7d60f6851da 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/QualityDataHelper.kt @@ -263,4 +263,6 @@ object QualityDataHelper { sealed class ProfileSettings(val key: String, val defaultValue: T) { object HideErrorSources : ProfileSettings("hide_error_sources", false) object HideNegativeSources : ProfileSettings("hide_negative_sources", false) + object SourceRegex : ProfileSettings("source_regex", "") + object SourceRegexPriority : ProfileSettings("source_regex_priority", false) } diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index 604ec314921..ba45b3f17ff 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -100,6 +100,35 @@ class SourcePriorityDialog( settingsBtt.setOnClickListener { SourceProfileSettingsDialog(ctx, themeRes, profile.id).show() } + + regexBtt?.setOnClickListener { + val currentRegex = QualityDataHelper.getProfileSetting(profile.id, ProfileSettings.SourceRegex) + val currentPriority = QualityDataHelper.getProfileSetting(profile.id, ProfileSettings.SourceRegexPriority) + val input = android.widget.EditText(ctx).apply { + setText(currentRegex) + isSingleLine = true + } + val toggle = android.widget.Switch(ctx).apply { + text = "Prioritize regex" + isChecked = currentPriority + } + val layout = android.widget.LinearLayout(ctx).apply { + orientation = android.widget.LinearLayout.VERTICAL + setPadding(40, 20, 40, 0) + addView(input) + addView(toggle) + } + android.app.AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + .setTitle("Source regex") + .setMessage("Comma-separated patterns. Empty = normal.") + .setView(layout) + .setPositiveButton("Save") { _, _ -> + QualityDataHelper.setProfileSetting(profile.id, ProfileSettings.SourceRegex, input.text.toString().trim()) + QualityDataHelper.setProfileSetting(profile.id, ProfileSettings.SourceRegexPriority, toggle.isChecked) + } + .setNegativeButton("Cancel", null) + .show() + } } super.show() } diff --git a/app/src/main/res/layout-port/player_select_source_priority.xml b/app/src/main/res/layout-port/player_select_source_priority.xml index 926227c58e9..6f162c5d429 100644 --- a/app/src/main/res/layout-port/player_select_source_priority.xml +++ b/app/src/main/res/layout-port/player_select_source_priority.xml @@ -32,6 +32,13 @@ android:textSize="20sp" android:textStyle="bold" /> + + + + Date: Fri, 23 Oct 2026 15:44:42 +0000 Subject: [PATCH 2/3] Update: add prioritize toggle, R.string, cache fix, gap fix --- .../ui/player/PlayerGeneratorViewModel.kt | 2 +- .../player/source_priority/SourcePriorityDialog.kt | 12 ++++++------ .../layout-port/player_select_source_priority.xml | 2 +- .../res/layout/player_select_source_priority.xml | 2 +- app/src/main/res/values/strings.xml | 3 +++ 5 files changed, 12 insertions(+), 9 deletions(-) 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 7392c7d4529..ab6921fc42e 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 @@ -101,7 +101,7 @@ data class VideoState( val hideErrorSources = QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideErrorSources) // Read user's regex pattern and priority toggle from the quality profile. - // sourceRegex: comma-separated patterns. Empty = no filter. + // sourceRegex: comma-separated patterns (e.g. "telugu,tel"). Empty = no filter. // regexPriority: when true, ALL matching sources sort first (regardless of quality priority). // when false, regex only breaks ties among equal-priority sources. val sourceRegex = diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt index ba45b3f17ff..9ec022142ed 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourcePriorityDialog.kt @@ -109,24 +109,24 @@ class SourcePriorityDialog( isSingleLine = true } val toggle = android.widget.Switch(ctx).apply { - text = "Prioritize regex" + text = ctx.getString(R.string.source_regex_prioritize) isChecked = currentPriority } val layout = android.widget.LinearLayout(ctx).apply { orientation = android.widget.LinearLayout.VERTICAL - setPadding(40, 20, 40, 0) + setPadding(40, 4, 40, 0) addView(input) addView(toggle) } android.app.AlertDialog.Builder(ctx, R.style.AlertDialogCustom) - .setTitle("Source regex") - .setMessage("Comma-separated patterns. Empty = normal.") + .setTitle(ctx.getString(R.string.source_regex)) + .setMessage(ctx.getString(R.string.source_regex_hint)) .setView(layout) - .setPositiveButton("Save") { _, _ -> + .setPositiveButton(ctx.getString(R.string.sort_apply)) { _, _ -> QualityDataHelper.setProfileSetting(profile.id, ProfileSettings.SourceRegex, input.text.toString().trim()) QualityDataHelper.setProfileSetting(profile.id, ProfileSettings.SourceRegexPriority, toggle.isChecked) } - .setNegativeButton("Cancel", null) + .setNegativeButton(ctx.getString(R.string.sort_cancel), null) .show() } } diff --git a/app/src/main/res/layout-port/player_select_source_priority.xml b/app/src/main/res/layout-port/player_select_source_priority.xml index 6f162c5d429..bde49faea1d 100644 --- a/app/src/main/res/layout-port/player_select_source_priority.xml +++ b/app/src/main/res/layout-port/player_select_source_priority.xml @@ -37,7 +37,7 @@ style="@style/WhiteButton" android:layout_width="wrap_content" android:layout_gravity="center_vertical|end" - android:text="Regex" /> + android:text="@string/source_regex" /> + android:text="@string/source_regex" /> Downloads Download queue Settings + Source regex + Comma-separated patterns. Empty = normal. + Prioritize regex Search… Search %s… Speech recognition is not available From a25f616b5cefc58c6d88ad32d5f5acd2ce3721a7 Mon Sep 17 00:00:00 2001 From: forbiders <339140124+forbiders@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:53:40 +0000 Subject: [PATCH 3/3] Remove language name from comments --- .../cloudstream3/ui/player/PlayerGeneratorViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ab6921fc42e..7392c7d4529 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 @@ -101,7 +101,7 @@ data class VideoState( val hideErrorSources = QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideErrorSources) // Read user's regex pattern and priority toggle from the quality profile. - // sourceRegex: comma-separated patterns (e.g. "telugu,tel"). Empty = no filter. + // sourceRegex: comma-separated patterns. Empty = no filter. // regexPriority: when true, ALL matching sources sort first (regardless of quality priority). // when false, regex only breaks ties among equal-priority sources. val sourceRegex =