diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt index d20e8570784..07cb688a37c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/GeneratorPlayer.kt @@ -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 @@ -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 @@ -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) @@ -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 { @@ -546,10 +550,32 @@ class GeneratorPlayer : FullScreenPlayer() { } } - private fun sortLinks(qualityProfile: Int): List> { - return currentLinks.sortedBy { + class DisplayLink( + val link: Pair, + // If the link should be displayed and used by the player + val shouldUseLink: Boolean, + val priority: Int + ) + + fun Pair.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 { + 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 } } @@ -1111,21 +1137,32 @@ class GeneratorPlayer : FullScreenPlayer() { var sourceIndex = 0 var startSource = 0 - var sortedUrls = emptyList>() + // Filtered and sorted links + var sortedLinks = emptyList() + // Unfiltered and sorted links + var fullSortedLinks = emptyList() + + 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(R.id.sort_sources_holder)?.isGone = true } else { - startSource = sortedUrls.indexOf(currentSelectedLink) + startSource = sortedLinks.indexOfFirst { it.link == currentLinkUsed } sourceIndex = startSource val sourcesArrayAdapter = ArrayAdapter(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)}" }) @@ -1141,7 +1178,7 @@ 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 @@ -1149,6 +1186,25 @@ class GeneratorPlayer : FullScreenPlayer() { } 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) + } } } @@ -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 @@ -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) @@ -1511,8 +1573,14 @@ class GeneratorPlayer : FullScreenPlayer() { } } + var erroredLinks: MutableList >> = mutableListOf() + fun hasLinkErrored(link: Pair): 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" @@ -1535,8 +1603,22 @@ 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() } @@ -1544,11 +1626,12 @@ class GeneratorPlayer : FullScreenPlayer() { 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() { @@ -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() { @@ -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() 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 02470484ea1..ab666e882eb 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 @@ -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") @@ -53,13 +56,21 @@ object QualityDataHelper { val types: Set ) + + // Map profile and name to priority + val sourcePriorityCache: HashMap> = hashMapOf() + 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 { @@ -77,6 +88,8 @@ object QualityDataHelper { } else { setKey(folder, name, priority) } + + sourcePriorityCache[profile]?.set(name, priority) } fun setProfileName(profile: Int, name: String?) { @@ -93,12 +106,18 @@ object QualityDataHelper { ?: txt(R.string.profile_number, profile) } + // Map profile and quality to priority + val qualityPriorityCache: HashMap> = 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) { @@ -107,8 +126,24 @@ object QualityDataHelper { quality.value.toString(), priority ) + qualityPriorityCache[profile]?.set(quality, priority) + } + + fun setProfileSetting(profile: Int, setting: ProfileSettings, 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 getProfileSetting(profile: Int, setting: ProfileSettings): T { + val folder = "$currentAccount/$VIDEO_PROFILE_SETTINGS/$profile" + val value = getKey(folder, setting.key) + return value ?: setting.defaultValue + } @Suppress("DEPRECATION") fun getQualityProfileTypes(profile: Int): Set { @@ -223,4 +258,9 @@ object QualityDataHelper { if (target == null) return Qualities.Unknown return Qualities.entries.minBy { abs(it.value - target) } } +} + +sealed class ProfileSettings(val key: String, val defaultValue: T) { + object HideErrorSources : ProfileSettings("hide_error_sources", false) + object HideNegativeSources : ProfileSettings("hide_negative_sources", false) } \ No newline at end of file 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 c8ac96ebbf6..604ec314921 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 @@ -14,7 +14,7 @@ import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding class SourcePriorityDialog( val ctx: Context, - @StyleRes themeRes: Int, + @StyleRes val themeRes: Int, val links: List, private val profile: QualityDataHelper.QualityProfile, /** @@ -28,76 +28,79 @@ class SourcePriorityDialog( PlayerSelectSourcePriorityBinding.inflate(LayoutInflater.from(ctx), null, false) setContentView(binding.root) fixSystemBarsPadding(binding.root) - val sourcesRecyclerView = binding.sortSources - val qualitiesRecyclerView = binding.sortQualities - val profileText = binding.profileTextEditable - val saveBtt = binding.saveBtt - val exitBtt = binding.closeBtt - val helpBtt = binding.helpBtt - profileText.setText(QualityDataHelper.getProfileName(profile.id).asString(context)) - profileText.hint = txt(R.string.profile_number, profile.id).asString(context) + binding.apply { + profileTextEditable.setText( + QualityDataHelper.getProfileName(profile.id).asString(context) + ) + profileTextEditable.hint = txt(R.string.profile_number, profile.id).asString(context) - sourcesRecyclerView.adapter = PriorityAdapter( - ).apply { - submitList(links.map { link -> - SourcePriority( - null, - link.source, - QualityDataHelper.getSourcePriority(profile.id, link.source) - ) - }.distinctBy { it.name }.sortedBy { -it.priority }) - } + sortSources.adapter = PriorityAdapter( + ).apply { + val sortedLinks = links.map { link -> + SourcePriority( + null, + link.source, + QualityDataHelper.getSourcePriority(profile.id, link.source) + ) + }.distinctBy { it.name }.sortedBy { -it.priority } - qualitiesRecyclerView.adapter = PriorityAdapter( - ).apply { - submitList(Qualities.entries.mapNotNull { - SourcePriority( - it, - Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, - QualityDataHelper.getQualityPriority(profile.id, it) - ) - }.sortedBy { -it.priority }) - } + submitList(sortedLinks) + } - @Suppress("UNCHECKED_CAST") // We know the types - saveBtt.setOnClickListener { - val qualityAdapter = qualitiesRecyclerView.adapter as? PriorityAdapter - val sourcesAdapter = sourcesRecyclerView.adapter as? PriorityAdapter + sortQualities.adapter = PriorityAdapter( + ).apply { + submitList(Qualities.entries.mapNotNull { + SourcePriority( + it, + Qualities.getStringByIntFull(it.value).ifBlank { return@mapNotNull null }, + QualityDataHelper.getQualityPriority(profile.id, it) + ) + }.sortedBy { -it.priority }) + } - val qualities = qualityAdapter?.immutableCurrentList ?: emptyList() - val sources = sourcesAdapter?.immutableCurrentList ?: emptyList() + @Suppress("UNCHECKED_CAST") // We know the types + saveBtt.setOnClickListener { + val qualityAdapter = sortQualities.adapter as? PriorityAdapter + val sourcesAdapter = sortSources.adapter as? PriorityAdapter - qualities.forEach { - QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority) - } + val qualities = qualityAdapter?.immutableCurrentList ?: emptyList() + val sources = sourcesAdapter?.immutableCurrentList ?: emptyList() - sources.forEach { - QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) - } + qualities.forEach { + QualityDataHelper.setQualityPriority(profile.id, it.data, it.priority) + } + + sources.forEach { + QualityDataHelper.setSourcePriority(profile.id, it.name, it.priority) + } - qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) - sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) + qualityAdapter?.submitList(qualities.sortedBy { -it.priority }) + sourcesAdapter?.submitList(sources.sortedBy { -it.priority }) - val savedProfileName = profileText.text.toString() - if (savedProfileName.isBlank()) { - QualityDataHelper.setProfileName(profile.id, null) - } else { - QualityDataHelper.setProfileName(profile.id, savedProfileName) + val savedProfileName = profileTextEditable.text.toString() + if (savedProfileName.isBlank()) { + QualityDataHelper.setProfileName(profile.id, null) + } else { + QualityDataHelper.setProfileName(profile.id, savedProfileName) + } + updatedCallback.invoke() } - updatedCallback.invoke() - } - exitBtt.setOnClickListener { - this.dismissSafe() - } + closeBtt.setOnClickListener { + dismissSafe() + } - helpBtt.setOnClickListener { - AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { - setMessage(R.string.quality_profile_help) - }.show() - } + helpBtt.setOnClickListener { + AlertDialog.Builder(context, R.style.AlertDialogCustom).apply { + setMessage(R.string.quality_profile_help) + }.show() + } + settingsBtt.setOnClickListener { + SourceProfileSettingsDialog(ctx, themeRes, profile.id).show() + } + } super.show() } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt new file mode 100644 index 00000000000..c26955b5efe --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/player/source_priority/SourceProfileSettingsDialog.kt @@ -0,0 +1,48 @@ +package com.lagradost.cloudstream3.ui.player.source_priority + +import android.app.Dialog +import android.content.Context +import android.view.LayoutInflater +import androidx.annotation.StyleRes +import com.lagradost.cloudstream3.databinding.SourceProfileSettingsDialogBinding +import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe +import com.lagradost.cloudstream3.utils.UIHelper.fixSystemBarsPadding + +class SourceProfileSettingsDialog( + val ctx: Context, + @StyleRes themeRes: Int, + val profile: Int +) : Dialog(ctx, themeRes) { + override fun show() { + val binding = + SourceProfileSettingsDialogBinding.inflate(LayoutInflater.from(ctx), null, false) + setContentView(binding.root) + fixSystemBarsPadding(binding.root) + + binding.apply { + var hideErrorSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideErrorSources) + var hideNegativeSources = QualityDataHelper.getProfileSetting(profile, ProfileSettings.HideNegativeSources) + + profileHideErrorSources.isChecked = hideErrorSources + profileHideErrorSources.setOnCheckedChangeListener { _, bool -> + hideErrorSources = bool + } + + profileHideNegativeSources.isChecked = hideNegativeSources + profileHideNegativeSources.setOnCheckedChangeListener { _, bool -> + hideNegativeSources = bool + } + + applyBtt.setOnClickListener { + QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideErrorSources, hideErrorSources) + QualityDataHelper.setProfileSetting(profile, ProfileSettings.HideNegativeSources, hideNegativeSources) + dismissSafe() + } + + cancelBtt.setOnClickListener { + dismissSafe() + } + } + super.show() + } +} \ No newline at end of file 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 2cba9c869bb..926227c58e9 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 @@ -1,6 +1,5 @@ - - + @@ -33,14 +32,21 @@ android:textSize="20sp" android:textStyle="bold" /> + + + android:src="@drawable/baseline_help_outline_24" /> - + + tools:ignore="LabelFor" + tools:text="@string/profile_number" /> + android:text="@string/sort_save" /> + android:text="@string/sort_close" /> diff --git a/app/src/main/res/layout/player_select_source_priority.xml b/app/src/main/res/layout/player_select_source_priority.xml index 182cd186141..0d825de77e8 100644 --- a/app/src/main/res/layout/player_select_source_priority.xml +++ b/app/src/main/res/layout/player_select_source_priority.xml @@ -160,6 +160,13 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a1a4fdc3f40..6e5f5a6a877 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -619,6 +619,14 @@ Subscribe Unsubscribe Profile %d + Profile settings + Hide sources with a negative priority + Hide sources with errors + + %d hidden link + %d hidden links + + Wi-Fi Mobile data Set default