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..dd0ee6881db 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,44 @@ 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 regex pattern and priority boost from profile. Priority 0 = disabled. + val sourceRegex = + QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.SourceRegex) + val regexPriority = + QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.SourceRegexPriority) + + // Parse comma-separated patterns into case-insensitive regex. Invalid = null (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 + priority to avoid stale results. + 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 } + } + + // Boost matching sources by regexPriority*1000 so they sort first. + // Within matching sources, quality priority still determines order. + // Priority 0 = disabled (regex ignored). No regex = original sort. + val sorted = if (regex != null && regexPriority > 0) { + displayLinks.sortedByDescending { link -> + val isMatch = link.shouldUseLink && regex.containsMatchIn(link.link?.first?.name ?: "") + link.priority + if (isMatch) regexPriority * 1000 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..61e3c0a653a 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", 0) } 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..672a6c026c8 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 @@ -11,6 +11,9 @@ import com.lagradost.cloudstream3.utils.txt import com.lagradost.cloudstream3.utils.Qualities import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding +import android.widget.EditText +import android.widget.LinearLayout +import android.widget.TextView class SourcePriorityDialog( val ctx: Context, @@ -29,6 +32,57 @@ class SourcePriorityDialog( setContentView(binding.root) fixSystemBarsPadding(binding.root) + val regexInput = EditText(ctx).apply { + hint = ctx.getString(R.string.source_regex_hint) + setText(QualityDataHelper.getProfileSetting(profile.id, ProfileSettings.SourceRegex)) + isSingleLine = true + setTextColor(ctx.getColor(android.R.color.white)) + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) + } + val regexPriority = QualityDataHelper.getProfileSetting(profile.id, ProfileSettings.SourceRegexPriority) + val priorityText = TextView(ctx).apply { + text = regexPriority.toString() + gravity = android.view.Gravity.CENTER + minWidth = (40 * ctx.resources.displayMetrics.density).toInt() + setTextColor(ctx.getColor(android.R.color.white)) + } + val minusBtn = android.widget.ImageView(ctx).apply { + setImageResource(com.lagradost.cloudstream3.R.drawable.baseline_remove_24) + background = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackgroundBorderless)).getDrawable(0) + setPadding(8, 8, 8, 8) + isFocusable = true + setOnClickListener { + val v = (priorityText.text.toString().toIntOrNull() ?: 0) - 1 + priorityText.text = v.coerceAtLeast(0).toString() + } + } + val plusBtn = android.widget.ImageView(ctx).apply { + setImageResource(com.lagradost.cloudstream3.R.drawable.ic_baseline_add_24) + background = ctx.obtainStyledAttributes(intArrayOf(android.R.attr.selectableItemBackgroundBorderless)).getDrawable(0) + setPadding(8, 8, 8, 8) + isFocusable = true + setOnClickListener { + val v = (priorityText.text.toString().toIntOrNull() ?: 0) + 1 + priorityText.text = v.toString() + } + } + val regexRow = LinearLayout(ctx).apply { + orientation = LinearLayout.HORIZONTAL + gravity = android.view.Gravity.CENTER_VERTICAL + setPadding( + (12 * ctx.resources.displayMetrics.density).toInt(), + (8 * ctx.resources.displayMetrics.density).toInt(), + (12 * ctx.resources.displayMetrics.density).toInt(), + (4 * ctx.resources.displayMetrics.density).toInt() + ) + addView(regexInput) + addView(minusBtn) + addView(priorityText) + addView(plusBtn) + } + // Insert regex row into sort_sources_holder, after the "Sources" title + binding.sortSourcesHolder.addView(regexRow, 1) + binding.apply { profileTextEditable.setText( QualityDataHelper.getProfileName(profile.id).asString(context) @@ -75,6 +129,17 @@ class SourcePriorityDialog( QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) } + val pattern = regexInput.text.toString().trim() + if (pattern.isNotEmpty()) { + val joined = pattern.split(",").map { it.trim() }.filter { it.isNotEmpty() }.joinToString("|") + if (runCatching { Regex(joined, RegexOption.IGNORE_CASE) }.isFailure) { + android.widget.Toast.makeText(ctx, ctx.getString(R.string.source_regex_invalid), android.widget.Toast.LENGTH_SHORT).show() + return@setOnClickListener + } + } + QualityDataHelper.setProfileSetting(profile.id, ProfileSettings.SourceRegex, pattern) + QualityDataHelper.setProfileSetting(profile.id, ProfileSettings.SourceRegexPriority, priorityText.text.toString().toIntOrNull() ?: 0) + qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) 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..6d24f02a606 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 @@ -31,7 +31,6 @@ android:textColor="?attr/textColor" android:textSize="20sp" android:textStyle="bold" /> - - Downloads Download queue Settings + Regex Comma-separated + Invalid regex Search… Search %s… Speech recognition is not available