Skip to content
Open
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 @@ -8,6 +8,7 @@ import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.Typeface
import android.os.Build
import android.os.Bundle
import android.text.Spanned
Expand Down Expand Up @@ -79,6 +80,7 @@ import com.lagradost.cloudstream3.ui.player.CS3IPlayer.Companion.preferredAudioT
import com.lagradost.cloudstream3.ui.player.CustomDecoder.Companion.updateForcedEncoding
import com.lagradost.cloudstream3.ui.player.PlayerSubtitleHelper.Companion.toSubtitleMimeType
import com.lagradost.cloudstream3.ui.player.source_priority.LinkSource
import com.lagradost.cloudstream3.ui.player.source_priority.ProfileSettings
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper
import com.lagradost.cloudstream3.ui.player.source_priority.QualityDataHelper.getLinkPriority
import com.lagradost.cloudstream3.ui.player.source_priority.QualityProfileDialog
Expand Down Expand Up @@ -128,6 +130,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import java.io.Serializable
import java.lang.ref.WeakReference
import java.util.Calendar

@OptIn(UnstableApi::class)
Expand Down Expand Up @@ -180,6 +183,7 @@ class GeneratorPlayer : FullScreenPlayer() {
isActive = false
binding?.overlayLoadingSkipButton?.isVisible = false
binding?.playerLoadingOverlay?.isVisible = true
erroredLinks.clear()
}

private fun setSubtitles(subtitle: SubtitleData?, userInitiated: Boolean): Boolean {
Expand Down Expand Up @@ -546,10 +550,32 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}

private fun sortLinks(qualityProfile: Int): List<Pair<ExtractorLink?, ExtractorUri?>> {
return currentLinks.sortedBy {
class DisplayLink(
val link: Pair<ExtractorLink?, ExtractorUri?>,
// If the link should be displayed and used by the player
val shouldUseLink: Boolean,
val priority: Int
)

fun Pair<ExtractorLink?, ExtractorUri?>.toDisplayLink(hideNegativeSources: Boolean, hideErrorSources: Boolean): DisplayLink {
val currentProfile = currentQualityProfile

val priority = getLinkPriority(currentProfile, this.first)
val shouldHideLink = (hideNegativeSources && priority < 0) || (hideErrorSources && hasLinkErrored(this))
val displayLink = DisplayLink(this, !shouldHideLink, priority)

return displayLink
}

private fun sortLinks(qualityProfile: Int): List<DisplayLink> {
val hideNegativeSources = QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideNegativeSources)
val hideErrorSources = QualityDataHelper.getProfileSetting(qualityProfile, ProfileSettings.HideErrorSources)

return currentLinks.map {
it.toDisplayLink(hideNegativeSources, hideErrorSources)
}.sortedBy {
// negative because we want to sort highest quality first
-getLinkPriority(qualityProfile, it.first)
-it.priority
}
}

Expand Down Expand Up @@ -1111,21 +1137,32 @@ class GeneratorPlayer : FullScreenPlayer() {

var sourceIndex = 0
var startSource = 0
var sortedUrls = emptyList<Pair<ExtractorLink?, ExtractorUri?>>()
// Filtered and sorted links
var sortedLinks = emptyList<DisplayLink>()
// Unfiltered and sorted links
var fullSortedLinks = emptyList<DisplayLink>()

var currentHiddenFooter: View? = null

fun refreshLinks(qualityProfile: Int) {
sortedUrls = sortLinks(qualityProfile)
if (sortedUrls.isEmpty()) {
val currentLinkUsed = currentSelectedLink
// Always display current link
fullSortedLinks = sortLinks(qualityProfile)
sortedLinks =
fullSortedLinks.filter { it.shouldUseLink || it.link == currentLinkUsed }

if (sortedLinks.isEmpty()) {
sourceDialog.findViewById<LinearLayout>(R.id.sort_sources_holder)?.isGone =
true
} else {
startSource = sortedUrls.indexOf(currentSelectedLink)
startSource = sortedLinks.indexOfFirst { it.link == currentLinkUsed }
sourceIndex = startSource

val sourcesArrayAdapter =
ArrayAdapter<String>(ctx, R.layout.sort_bottom_single_choice)

sourcesArrayAdapter.addAll(sortedUrls.map { (link, uri) ->
sourcesArrayAdapter.addAll(sortedLinks.map { displayLink ->
val (link, uri) = displayLink.link
val name = link?.name ?: uri?.name ?: "NULL"
"$name ${Qualities.getStringByInt(link?.quality)}"
})
Expand All @@ -1141,14 +1178,33 @@ class GeneratorPlayer : FullScreenPlayer() {
}

providerList.setOnItemLongClickListener { _, _, position, _ ->
sortedUrls.getOrNull(position)?.first?.url?.let {
sortedLinks.getOrNull(position)?.link?.first?.url?.let {
clipboardHelper(
txt(R.string.video_source),
it
)
}
true
}

val hiddenLinks = fullSortedLinks.size - sortedLinks.size
providerList.removeFooterView(currentHiddenFooter)

if (hiddenLinks > 0) {
val hiddenLinksFooter: TextView = layoutInflater.inflate(
R.layout.sort_bottom_footer_add_choice, null
) as TextView

providerList.addFooterView(hiddenLinksFooter, null, false)
currentHiddenFooter = hiddenLinksFooter

val hiddenLinksText =
ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks)
.format(hiddenLinks)
hiddenLinksFooter.text = hiddenLinksText
hiddenLinksFooter.setCompoundDrawables(null, null, null, null)
hiddenLinksFooter.setTypeface(null, Typeface.ITALIC)
}
}
}

Expand Down Expand Up @@ -1281,7 +1337,13 @@ class GeneratorPlayer : FullScreenPlayer() {
QualityProfileDialog(
activity,
R.style.DialogFullscreenPlayer,
currentLinks.mapNotNull { it.first?.let { extractorLink -> LinkSource(extractorLink) } },
currentLinks.mapNotNull {
it.first?.let { extractorLink ->
LinkSource(
extractorLink
)
}
},
currentQualityProfile
) { profile ->
currentQualityProfile = profile.id
Expand Down Expand Up @@ -1348,8 +1410,8 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
if (init) {
sortedUrls.getOrNull(sourceIndex)?.let {
loadLink(it, true)
sortedLinks.getOrNull(sourceIndex)?.let {
loadLink(it.link, true)
}
}
sourceDialog.dismissSafe(activity)
Expand Down Expand Up @@ -1511,8 +1573,14 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}

var erroredLinks: MutableList<WeakReference< Pair<ExtractorLink?, ExtractorUri?> >> = mutableListOf()
fun hasLinkErrored(link: Pair<ExtractorLink?, ExtractorUri?>): Boolean {
return erroredLinks.any { it.get() == link }
}

override fun playerError(exception: Throwable) {
erroredLinks.add(WeakReference(currentSelectedLink))

val currentUrl =
currentSelectedLink?.let { it.first?.url ?: it.second?.uri?.toString() } ?: "unknown"
val headers = currentSelectedLink?.first?.headers?.toString() ?: "none"
Expand All @@ -1535,20 +1603,35 @@ class GeneratorPlayer : FullScreenPlayer() {

private fun noLinksFound() {
viewModel.forceClearCache = true
val hiddenLinks = sortLinks(currentQualityProfile).count { !it.shouldUseLink }

context?.let { ctx ->
// Display that there are hidden links to the user.
if (hiddenLinks > 0) {
val noLinksString = ctx.getString(R.string.no_links_found_toast)
val hiddenString =
ctx.resources.getQuantityString(R.plurals.links_hidden, hiddenLinks)
.format(hiddenLinks)
val toastText = "$noLinksString\n($hiddenString)"
showToast(toastText, Toast.LENGTH_SHORT)
} else {
showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
}
}

showToast(R.string.no_links_found_toast, Toast.LENGTH_SHORT)
activity?.popCurrentPage()
}

private fun startPlayer() {
if (isActive) return // we don't want double load when you skip loading

val links = sortLinks(currentQualityProfile)
if (links.isEmpty()) {
val firstAvailableLink = links.firstOrNull { it.shouldUseLink }?.link
if (firstAvailableLink == null) {
noLinksFound()
return
}
loadLink(links.first(), false)
loadLink(firstAvailableLink, false)
}

override fun nextEpisode() {
Expand All @@ -1567,25 +1650,27 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}

override fun hasNextMirror(): Boolean {
private fun getNextLink(): DisplayLink? {
val links = sortLinks(currentQualityProfile)
return links.isNotEmpty() && links.indexOf(currentSelectedLink) + 1 < links.size

val currentIndex = links.indexOfFirst { it.link == currentSelectedLink }
val nextPotentialLink =
links.withIndex().firstOrNull { it.index > currentIndex && it.value.shouldUseLink }
return nextPotentialLink?.value
}

override fun nextMirror() {
val links = sortLinks(currentQualityProfile)
if (links.isEmpty()) {
noLinksFound()
return
}
override fun hasNextMirror(): Boolean {
return getNextLink() != null
}

val newIndex = links.indexOf(currentSelectedLink) + 1
if (newIndex >= links.size) {
override fun nextMirror() {
val nextLink = getNextLink()
if (nextLink == null) {
noLinksFound()
return
}

loadLink(links[newIndex], true)
loadLink(nextLink.link, true)
}

override fun onDestroy() {
Expand Down Expand Up @@ -2122,23 +2207,26 @@ class GeneratorPlayer : FullScreenPlayer() {

observe(viewModel.currentLinks) {
currentLinks = it
val turnVisible = it.isNotEmpty() && lastUsedGenerator?.canSkipLoading == true

val displayLinks = sortLinks(currentQualityProfile)
val usableLinks = displayLinks.count { link -> link.shouldUseLink }

val turnVisible = usableLinks > 0 && lastUsedGenerator?.canSkipLoading == true
val wasGone = binding?.overlayLoadingSkipButton?.isGone == true

binding?.overlayLoadingSkipButton?.apply {
isVisible = turnVisible
val value = viewModel.currentLinks.value
if (value.isNullOrEmpty()) {

if (usableLinks == 0) {
setText(R.string.skip_loading)
} else {
text = "${context.getString(R.string.skip_loading)} (${value.size})"
text = "${context.getString(R.string.skip_loading)} (${usableLinks})"
}
}

safe {
if (currentLinks.any { link ->
getLinkPriority(currentQualityProfile, link.first) >=
QualityDataHelper.AUTO_SKIP_PRIORITY
if (displayLinks.any { link ->
link.priority >= QualityDataHelper.AUTO_SKIP_PRIORITY
}
) {
startPlayer()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import com.lagradost.cloudstream3.utils.txt
import com.lagradost.cloudstream3.utils.DataStoreHelper.currentAccount
import com.lagradost.cloudstream3.utils.ExtractorLink
import com.lagradost.cloudstream3.utils.Qualities
import java.util.EnumMap
import kotlin.also
import kotlin.math.abs

object QualityDataHelper {
private const val VIDEO_SOURCE_PRIORITY = "video_source_priority"
private const val VIDEO_PROFILE_NAME = "video_profile_name"
private const val VIDEO_QUALITY_PRIORITY = "video_quality_priority"
const val VIDEO_PROFILE_SETTINGS = "video_profile_settings"

// Old key only supporting one type per profile
@Deprecated("Changed to support multiple types per profile")
Expand Down Expand Up @@ -53,13 +56,21 @@ object QualityDataHelper {
val types: Set<QualityProfileType>
)


// Map profile and name to priority
val sourcePriorityCache: HashMap<Int, HashMap<String, Int>> = hashMapOf()
Copy link
Copy Markdown
Collaborator

@fire-light42 fire-light42 Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a ConcurrentHashMap for better thread safety.


fun getSourcePriority(profile: Int, name: String?): Int {
if (name == null) return DEFAULT_SOURCE_PRIORITY
return getKey(

return sourcePriorityCache[profile]?.get(name) ?: (getKey(
"$currentAccount/$VIDEO_SOURCE_PRIORITY/$profile",
name,
DEFAULT_SOURCE_PRIORITY
) ?: DEFAULT_SOURCE_PRIORITY
) ?: DEFAULT_SOURCE_PRIORITY).also {
sourcePriorityCache[profile] = sourcePriorityCache[profile] ?: hashMapOf()
sourcePriorityCache[profile]?.set(name, it)
}
}

fun getAllSourcePriorityNames(profile: Int): List<String> {
Expand All @@ -77,6 +88,8 @@ object QualityDataHelper {
} else {
setKey(folder, name, priority)
}

sourcePriorityCache[profile]?.set(name, priority)
}

fun setProfileName(profile: Int, name: String?) {
Expand All @@ -93,12 +106,18 @@ object QualityDataHelper {
?: txt(R.string.profile_number, profile)
}

// Map profile and quality to priority
val qualityPriorityCache: HashMap<Int, EnumMap<Qualities, Int>> = hashMapOf()
fun getQualityPriority(profile: Int, quality: Qualities): Int {
return getKey(
return qualityPriorityCache[profile]?.get(quality) ?: (getKey(
"$currentAccount/$VIDEO_QUALITY_PRIORITY/$profile",
quality.value.toString(),
quality.defaultPriority
) ?: quality.defaultPriority
)?.also {
qualityPriorityCache[profile] =
qualityPriorityCache[profile] ?: EnumMap(Qualities::class.java)
qualityPriorityCache[profile]?.set(quality, it)
}) ?: quality.defaultPriority
}

fun setQualityPriority(profile: Int, quality: Qualities, priority: Int) {
Expand All @@ -107,8 +126,24 @@ object QualityDataHelper {
quality.value.toString(),
priority
)
qualityPriorityCache[profile]?.set(quality, priority)
}

fun <T> setProfileSetting(profile: Int, setting: ProfileSettings<T>, value: T) {
val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile"
// Prevent unnecessary keys
if (value == setting.defaultValue) {
removeKey(folder, setting.key)
} else {
setKey(folder, setting.key, value)
}
}

inline fun <reified T : Any> getProfileSetting(profile: Int, setting: ProfileSettings<T>): T {
val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile"
val value = getKey<T>(folder, setting.key)
return value ?: setting.defaultValue
}

@Suppress("DEPRECATION")
fun getQualityProfileTypes(profile: Int): Set<QualityProfileType> {
Expand Down Expand Up @@ -223,4 +258,9 @@ object QualityDataHelper {
if (target == null) return Qualities.Unknown
return Qualities.entries.minBy { abs(it.value - target) }
}
}

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)
}
Loading