Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<Int, List<DisplayLink>> = ConcurrentHashMap()
private val sortedLinks: ConcurrentHashMap<String, List<DisplayLink>> = ConcurrentHashMap()

/**
* The cache is guaranteed to be up to date link-wise due to the immutable links.
Expand Down Expand Up @@ -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<DisplayLink> {
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<DisplayLink> { 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -263,4 +263,6 @@ object QualityDataHelper {
sealed class ProfileSettings<T>(val key: String, val defaultValue: T) {
object HideErrorSources : ProfileSettings<Boolean>("hide_error_sources", false)
object HideNegativeSources : ProfileSettings<Boolean>("hide_negative_sources", false)
object SourceRegex : ProfileSettings<String>("source_regex", "")
object SourceRegexPriority : ProfileSettings<Boolean>("source_regex_priority", false)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@
android:textSize="20sp"
android:textStyle="bold" />

<com.google.android.material.button.MaterialButton
android:id="@+id/regex_btt"
style="@style/WhiteButton"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|end"
android:text="@string/source_regex" />

<com.google.android.material.button.MaterialButton
android:id="@+id/settings_btt"
style="@style/WhiteButton"
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/layout/player_select_source_priority.xml
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@
<!-- android:layout_gravity="center"-->
<!-- android:src="@drawable/outline_edit_24" />-->

<com.google.android.material.button.MaterialButton
android:id="@+id/regex_btt"
style="@style/WhiteButton"
android:layout_width="wrap_content"
android:layout_gravity="center_vertical|end"
android:text="@string/source_regex" />

<com.google.android.material.button.MaterialButton
android:id="@+id/settings_btt"
style="@style/WhiteButton"
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@
<string name="title_downloads">Downloads</string>
<string name="download_queue">Download queue</string>
<string name="title_settings">Settings</string>
<string name="source_regex">Source regex</string>
<string name="source_regex_hint">Comma-separated patterns. Empty = normal.</string>
<string name="source_regex_prioritize">Prioritize regex</string>
<string name="search_hint">Search…</string>
<string name="search_hint_site" formatted="true">Search %s…</string>
<string name="speech_recognition_unavailable">Speech recognition is not available</string>
Expand Down