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 @@ -132,6 +134,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
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
Expand Down Expand Up @@ -510,7 +513,7 @@ class GeneratorPlayer : FullScreenPlayer() {
showDownloadProgress(DownloadEvent(0, 0, 0, null))

// uiReset() // Removed due to UX

currentSelectedLink = link
// setEpisodes(viewModel.getAllMeta() ?: emptyList())
setPlayerDimen(null)
Expand Down Expand Up @@ -1112,21 +1115,29 @@ class GeneratorPlayer : FullScreenPlayer() {

var sourceIndex = 0
var startSource = 0
var sortedUrls = emptyList<Pair<ExtractorLink?, ExtractorUri?>>()
// Filtered and sorted links
var currentHiddenFooter: View? = null
var filteredLinks: List<DisplayLink> = emptyList()

fun refreshLinks(qualityProfile: Int) {
sortedUrls = viewModel.state.sortLinks(qualityProfile)
if (sortedUrls.isEmpty()) {
val currentLinkUsed = currentSelectedLink
// Always display current linkFooter
val sortedLinks = viewModel.state.sortLinks(qualityProfile)

filteredLinks = sortedLinks.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 = filteredLinks.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(filteredLinks.map { displayLink ->
val (link, uri) = displayLink.link
val name = link?.name ?: uri?.name ?: "NULL"
"$name ${Qualities.getStringByInt(link?.quality)}"
})
Expand All @@ -1142,14 +1153,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 = sortedLinks.size - filteredLinks.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 @@ -1363,8 +1393,8 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}
if (init) {
sortedUrls.getOrNull(sourceIndex)?.let {
loadLink(it, true)
filteredLinks.getOrNull(sourceIndex)?.let {
loadLink(it.link, true)
}
}
sourceDialog.dismissSafe(activity)
Expand Down Expand Up @@ -1532,6 +1562,10 @@ class GeneratorPlayer : FullScreenPlayer() {
}

override fun playerError(exception: Throwable) {
currentSelectedLink?.let { link ->
viewModel.modifyState { this.addError(link) }
}

val currentUrl =
currentSelectedLink?.let { it.first?.url ?: it.second?.uri?.toString() } ?: "unknown"
val headers = currentSelectedLink?.first?.headers?.toString() ?: "none"
Expand All @@ -1554,8 +1588,22 @@ class GeneratorPlayer : FullScreenPlayer() {

private fun noLinksFound() {
viewModel.forceClearCache = true
val hiddenLinks = viewModel.state.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()
}

Expand All @@ -1566,15 +1614,17 @@ class GeneratorPlayer : FullScreenPlayer() {
}

val links = viewModel.state.sortLinks(currentQualityProfile)
if (links.isEmpty()) {

val firstAvailableLink = links.firstOrNull { it.shouldUseLink }?.link
if (firstAvailableLink == null) {
noLinksFound()
return
}
// Atomic operation to prevent double loading
if (!isPlayerActive.compareAndSet(false, true)) {
return
}
loadLink(links.first(), false)
loadLink(firstAvailableLink, false)
showPlayerMetadata()
}

Expand Down Expand Up @@ -1641,25 +1691,26 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}

override fun hasNextMirror(): Boolean {
private fun getNextLink(): DisplayLink? {
val links = viewModel.state.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 = viewModel.state.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 @@ -2166,6 +2217,7 @@ class GeneratorPlayer : FullScreenPlayer() {
isPlayerActive.set(false)
binding?.overlayLoadingSkipButton?.isVisible = false
binding?.playerLoadingOverlay?.isVisible = true
viewModel.modifyState { setError(emptyList()) }
uiReset()
}

Expand Down Expand Up @@ -2297,19 +2349,23 @@ class GeneratorPlayer : FullScreenPlayer() {
}
}

observe(viewModel.currentLinks) { (links, instance) ->
observe(viewModel.currentLinks) { (_, instance) ->
if (instance != viewModel.state.instance) return@observe // Outdated observe

val turnVisible = links.isNotEmpty() && viewModel.generator?.canSkipLoading == true
val sortedLinks = viewModel.state.sortLinks(currentQualityProfile)
val usableLinks = sortedLinks.count { link -> link.shouldUseLink }

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

binding.overlayLoadingSkipButton.apply {
isVisible = turnVisible
if (links.isEmpty()) {

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import com.lagradost.cloudstream3.mvvm.Resource
import com.lagradost.cloudstream3.mvvm.launchSafe
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.safeApiCall
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.result.ResultEpisode
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
Expand Down Expand Up @@ -40,12 +42,20 @@ data class GeneratorState(
val id: Int?,
)

data class DisplayLink(
val link: VideoLink,
// If the link should be displayed and used by the player
val shouldUseLink: Boolean,
val priority: Int
)

/** Immutable state of all current links relevant to displaying the video */
// @MustUseReturnValues
// @Immutable
data class VideoState(
val subtitles: PersistentSet<SubtitleData> = persistentSetOf(),
val links: PersistentSet<VideoLink> = persistentSetOf(),
val erroredLinks: PersistentSet<VideoLink> = persistentSetOf(),
val stamps: PersistentList<VideoSkipStamp> = persistentListOf(),
val loading: Resource<Unit> = Resource.Loading(),
val generatorState: GeneratorState? = null,
Expand All @@ -56,19 +66,52 @@ 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<VideoLink>> = ConcurrentHashMap()
private val sortedLinks: ConcurrentHashMap<Int, List<DisplayLink>> = ConcurrentHashMap()

/**
* The cache is guaranteed to be up to date link-wise due to the immutable links.
* However, hideNegativeSources and hideErrorSources could be updated, which requires clearing the cache.
*/
fun clearSortedLinksCache() = sortedLinks.clear()

private fun hasLinkErrored(link: VideoLink): Boolean {
return erroredLinks.any { it == link }
}

private fun VideoLink.toDisplayLink(
qualityProfile: Int,
hideNegativeSources: Boolean,
hideErrorSources: Boolean
): DisplayLink {
val priority = getLinkPriority(qualityProfile, this.first)
val shouldHideLink =
(hideNegativeSources && priority < 0) || (hideErrorSources && hasLinkErrored(this))
val displayLink = DisplayLink(this, !shouldHideLink, priority)

return displayLink
}

// Modifying sortedLinks is not considered a "visible" side effect, and rerunning it does not change the result
// 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<VideoLink> {
return sortedLinks[qualityProfile] ?: links.sortedBy { link ->
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)

return links.map { link ->
// negative because we want to sort highest quality first
link.toDisplayLink(qualityProfile, hideNegativeSources, hideErrorSources)
}.sortedBy {
// negative because we want to sort highest quality first
-getLinkPriority(qualityProfile, link.first)
-it.priority
}.also { value -> sortedLinks[qualityProfile] = value }
}

Expand Down Expand Up @@ -113,6 +156,12 @@ data class VideoState(
@JvmName("setVideoSkipStamp")
@Contract(pure = true)
fun set(items: Collection<VideoSkipStamp>): VideoState = copy(stamps = items.toPersistentList())

@Contract(pure = true)
fun addError(item: VideoLink): VideoState = copy(erroredLinks = erroredLinks.add(item))

@Contract(pure = true)
fun setError(items: Collection<VideoLink>): VideoState = copy(erroredLinks = items.toPersistentSet())
}

data class VideoLive<T>(
Expand Down Expand Up @@ -141,9 +190,8 @@ class PlayerGeneratorViewModel : ViewModel() {
var state = VideoState(instance = 0)
private set

private val _currentLinks =
MutableLiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>>(null)
val currentLinks: LiveData<VideoLive<Set<Pair<ExtractorLink?, ExtractorUri?>>>> = _currentLinks
private val _currentLinks = MutableLiveData<VideoLive<Set<VideoLink>>>(null)
val currentLinks: LiveData<VideoLive<Set<VideoLink>>> = _currentLinks

private val _currentSubtitles = MutableLiveData<VideoLive<Set<SubtitleData>>>(null)
val currentSubtitles: LiveData<VideoLive<Set<SubtitleData>>> = _currentSubtitles
Expand Down
Loading