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..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 @@ -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 = ctx.getString(R.string.source_regex_prioritize) + isChecked = currentPriority + } + val layout = android.widget.LinearLayout(ctx).apply { + orientation = android.widget.LinearLayout.VERTICAL + setPadding(40, 4, 40, 0) + addView(input) + addView(toggle) + } + android.app.AlertDialog.Builder(ctx, R.style.AlertDialogCustom) + .setTitle(ctx.getString(R.string.source_regex)) + .setMessage(ctx.getString(R.string.source_regex_hint)) + .setView(layout) + .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(ctx.getString(R.string.sort_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..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 @@ -32,6 +32,13 @@ android:textSize="20sp" android:textStyle="bold" /> + + + + Downloads Download queue Settings + Source regex + Comma-separated patterns. Empty = normal. + Prioritize regex Search… Search %s… Speech recognition is not available