refactored download system for better preference + bugfixes

This commit is contained in:
LagradOst 2023-08-19 00:48:00 +02:00
parent e95dc1db2a
commit 56cb3d7181
11 changed files with 604 additions and 852 deletions

View File

@ -50,7 +50,7 @@ class DownloaderTestImpl private constructor(builder: OkHttpClient.Builder) : Do
companion object {
private const val USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
private var instance: DownloaderTestImpl? = null
/**

View File

@ -29,7 +29,7 @@ import java.util.*
import kotlin.math.absoluteValue
const val USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
//val baseHeader = mapOf("User-Agent" to USER_AGENT)
val mapper = JsonMapper.builder().addModule(KotlinModule())

View File

@ -23,7 +23,7 @@ data class VisualDownloadChildCached(
val data: VideoDownloadHelper.DownloadEpisodeCached,
)
data class DownloadClickEvent(val action: Int, val data: EasyDownloadButton.IMinimumData)
data class DownloadClickEvent(val action: Int, val data: VideoDownloadHelper.DownloadEpisodeCached)
class DownloadChildAdapter(
var cardList: List<VisualDownloadChildCached>,

View File

@ -1,264 +0,0 @@
package com.lagradost.cloudstream3.ui.download
import android.animation.ObjectAnimator
import android.text.format.Formatter.formatShortFileSize
import android.view.View
import android.view.animation.DecelerateInterpolator
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isGone
import androidx.core.view.isVisible
import androidx.core.widget.ContentLoadingProgressBar
import com.google.android.material.button.MaterialButton
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.utils.Coroutines
import com.lagradost.cloudstream3.utils.IDisposable
import com.lagradost.cloudstream3.utils.UIHelper.popupMenuNoIcons
import com.lagradost.cloudstream3.utils.VideoDownloadManager
class EasyDownloadButton : IDisposable {
interface IMinimumData {
val id: Int
}
private var _clickCallback: ((DownloadClickEvent) -> Unit)? = null
private var _imageChangeCallback: ((Pair<Int, String>) -> Unit)? = null
override fun dispose() {
try {
_clickCallback = null
_imageChangeCallback = null
downloadProgressEventListener?.let { VideoDownloadManager.downloadProgressEvent -= it }
downloadStatusEventListener?.let { VideoDownloadManager.downloadStatusEvent -= it }
} catch (e: Exception) {
e.printStackTrace()
}
}
private var downloadProgressEventListener: ((Triple<Int, Long, Long>) -> Unit)? = null
private var downloadStatusEventListener: ((Pair<Int, VideoDownloadManager.DownloadType>) -> Unit)? =
null
fun setUpMaterialButton(
setupCurrentBytes: Long?,
setupTotalBytes: Long?,
progressBar: ContentLoadingProgressBar,
downloadButton: MaterialButton,
textView: TextView?,
data: IMinimumData,
clickCallback: (DownloadClickEvent) -> Unit,
) {
setUpDownloadButton(
setupCurrentBytes,
setupTotalBytes,
progressBar,
textView,
data,
downloadButton,
{
downloadButton.setIconResource(it.first)
downloadButton.text = it.second
},
clickCallback
)
}
fun setUpMoreButton(
setupCurrentBytes: Long?,
setupTotalBytes: Long?,
progressBar: ContentLoadingProgressBar,
downloadImage: ImageView,
textView: TextView?,
textViewProgress: TextView?,
clickableView: View,
isTextPercentage: Boolean,
data: IMinimumData,
clickCallback: (DownloadClickEvent) -> Unit,
) {
setUpDownloadButton(
setupCurrentBytes,
setupTotalBytes,
progressBar,
textViewProgress,
data,
clickableView,
{ (image, text) ->
downloadImage.isVisible = textViewProgress?.isGone ?: true
downloadImage.setImageResource(image)
textView?.text = text
},
clickCallback, isTextPercentage
)
}
fun setUpButton(
setupCurrentBytes: Long?,
setupTotalBytes: Long?,
progressBar: ContentLoadingProgressBar,
downloadImage: ImageView,
textView: TextView?,
data: IMinimumData,
clickCallback: (DownloadClickEvent) -> Unit,
) {
setUpDownloadButton(
setupCurrentBytes,
setupTotalBytes,
progressBar,
textView,
data,
downloadImage,
{
downloadImage.setImageResource(it.first)
},
clickCallback
)
}
private fun setUpDownloadButton(
setupCurrentBytes: Long?,
setupTotalBytes: Long?,
progressBar: ContentLoadingProgressBar,
textView: TextView?,
data: IMinimumData,
downloadView: View,
downloadImageChangeCallback: (Pair<Int, String>) -> Unit,
clickCallback: (DownloadClickEvent) -> Unit,
isTextPercentage: Boolean = false
) {
_clickCallback = clickCallback
_imageChangeCallback = downloadImageChangeCallback
var lastState: VideoDownloadManager.DownloadType? = null
var currentBytes = setupCurrentBytes ?: 0
var totalBytes = setupTotalBytes ?: 0
var needImageUpdate = true
fun changeDownloadImage(state: VideoDownloadManager.DownloadType) {
lastState = state
if (currentBytes <= 0) needImageUpdate = true
val img = if (currentBytes > 0) {
when (state) {
VideoDownloadManager.DownloadType.IsPaused -> Pair(
R.drawable.ic_baseline_play_arrow_24,
R.string.download_paused
)
VideoDownloadManager.DownloadType.IsDownloading -> Pair(
R.drawable.netflix_pause,
R.string.downloading
)
else -> Pair(R.drawable.ic_baseline_delete_outline_24, R.string.downloaded)
}
} else {
Pair(R.drawable.netflix_download, R.string.download)
}
_imageChangeCallback?.invoke(
Pair(
img.first,
downloadView.context.getString(img.second)
)
)
}
fun fixDownloadedBytes(setCurrentBytes: Long, setTotalBytes: Long, animate: Boolean) {
currentBytes = setCurrentBytes
totalBytes = setTotalBytes
if (currentBytes == 0L) {
changeDownloadImage(VideoDownloadManager.DownloadType.IsStopped)
textView?.visibility = View.GONE
progressBar.visibility = View.GONE
} else {
if (lastState == VideoDownloadManager.DownloadType.IsStopped) {
changeDownloadImage(VideoDownloadManager.getDownloadState(data.id))
}
textView?.visibility = View.VISIBLE
progressBar.visibility = View.VISIBLE
val currentMbString = formatShortFileSize(textView?.context, setCurrentBytes)
val totalMbString = formatShortFileSize(textView?.context, setTotalBytes)
textView?.text =
if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
textView?.context?.getString(R.string.download_size_format)
?.format(currentMbString, totalMbString)
progressBar.let { bar ->
bar.max = (setTotalBytes / 1000).toInt()
if (animate) {
val animation: ObjectAnimator = ObjectAnimator.ofInt(
bar,
"progress",
bar.progress,
(setCurrentBytes / 1000).toInt()
)
animation.duration = 500
animation.setAutoCancel(true)
animation.interpolator = DecelerateInterpolator()
animation.start()
} else {
bar.progress = (setCurrentBytes / 1000).toInt()
}
}
}
}
fixDownloadedBytes(currentBytes, totalBytes, false)
changeDownloadImage(VideoDownloadManager.getDownloadState(data.id))
downloadProgressEventListener = { downloadData: Triple<Int, Long, Long> ->
if (data.id == downloadData.first) {
if (downloadData.second != currentBytes || downloadData.third != totalBytes) { // TO PREVENT WASTING UI TIME
Coroutines.runOnMainThread {
fixDownloadedBytes(downloadData.second, downloadData.third, true)
changeDownloadImage(VideoDownloadManager.getDownloadState(data.id))
}
}
}
}
downloadStatusEventListener =
{ downloadData: Pair<Int, VideoDownloadManager.DownloadType> ->
if (data.id == downloadData.first) {
if (lastState != downloadData.second || needImageUpdate) { // TO PREVENT WASTING UI TIME
Coroutines.runOnMainThread {
changeDownloadImage(downloadData.second)
}
}
}
}
downloadProgressEventListener?.let { VideoDownloadManager.downloadProgressEvent += it }
downloadStatusEventListener?.let { VideoDownloadManager.downloadStatusEvent += it }
downloadView.setOnClickListener {
if (currentBytes <= 0 || totalBytes <= 0) {
_clickCallback?.invoke(DownloadClickEvent(DOWNLOAD_ACTION_DOWNLOAD, data))
} else {
val list = arrayListOf(
Pair(DOWNLOAD_ACTION_PLAY_FILE, R.string.popup_play_file),
Pair(DOWNLOAD_ACTION_DELETE_FILE, R.string.popup_delete_file),
)
// DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone &&
if ((currentBytes * 100 / totalBytes) < 98) {
list.add(
if (lastState == VideoDownloadManager.DownloadType.IsDownloading)
Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download)
else
Pair(DOWNLOAD_ACTION_RESUME_DOWNLOAD, R.string.popup_resume_download)
)
}
it.popupMenuNoIcons(
list
) {
_clickCallback?.invoke(DownloadClickEvent(itemId, data))
}
}
}
downloadView.setOnLongClickListener {
clickCallback.invoke(DownloadClickEvent(DOWNLOAD_ACTION_LONG_CLICK, data))
return@setOnLongClickListener true
}
}
}

View File

@ -22,7 +22,7 @@ data class DownloadMetadata(
val progressPercentage: Long
get() = if (downloadedLength < 1024) 0 else maxOf(
0,
minOf(100, (downloadedLength * 100L) / totalLength)
minOf(100, (downloadedLength * 100L) / (totalLength + 1))
)
}
@ -101,38 +101,41 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
open fun setProgress(downloadedBytes: Long, totalBytes: Long) {
isZeroBytes = downloadedBytes == 0L
val steps = 10000L
progressBar.max = steps.toInt()
// div by zero error and 1 byte off is ok impo
val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt()
progressBar.post {
val steps = 10000L
progressBar.max = steps.toInt()
// div by zero error and 1 byte off is ok impo
val animation = ProgressBarAnimation(
progressBar,
progressBar.progress.toFloat(),
progress.toFloat()
).apply {
fillAfter = true
duration =
if (progress > progressBar.progress) // we don't want to animate backward changes in progress
100
else
0L
}
val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt()
if (isZeroBytes) {
progressText?.isVisible = false
} else {
progressText?.apply {
val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes)
val totalMbString = Formatter.formatShortFileSize(context, totalBytes)
text =
//if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
context?.getString(R.string.download_size_format)
?.format(currentMbString, totalMbString)
val animation = ProgressBarAnimation(
progressBar,
progressBar.progress.toFloat(),
progress.toFloat()
).apply {
fillAfter = true
duration =
if (progress > progressBar.progress) // we don't want to animate backward changes in progress
100
else
0L
}
}
progressBar.startAnimation(animation)
if (isZeroBytes) {
progressText?.isVisible = false
} else {
progressText?.apply {
val currentMbString = Formatter.formatShortFileSize(context, downloadedBytes)
val totalMbString = Formatter.formatShortFileSize(context, totalBytes)
text =
//if (isTextPercentage) "%d%%".format(setCurrentBytes * 100L / setTotalBytes) else
context?.getString(R.string.download_size_format)
?.format(currentMbString, totalMbString)
}
}
progressBar.startAnimation(animation)
}
}
fun downloadStatusEvent(data: Pair<Int, VideoDownloadManager.DownloadType>) {

View File

@ -21,14 +21,17 @@ class DownloadButton(context: Context, attributeSet: AttributeSet) :
}
override fun setStatus(status: DownloadStatusTell?) {
super.setStatus(status)
val txt = when (status) {
DownloadStatusTell.IsPaused -> R.string.download_paused
DownloadStatusTell.IsDownloading -> R.string.downloading
DownloadStatusTell.IsDone -> R.string.downloaded
else -> R.string.download
mainText?.post {
val txt = when (status) {
DownloadStatusTell.IsPaused -> R.string.download_paused
DownloadStatusTell.IsDownloading -> R.string.downloading
DownloadStatusTell.IsDone -> R.string.downloaded
else -> R.string.download
}
mainText?.setText(txt)
}
mainText?.setText(txt)
super.setStatus(status)
}
override fun setDefaultClickListener(

View File

@ -174,7 +174,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
currentMetaData.apply {
// DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone &&
if ((downloadedLength * 100 / totalLength) < 98) {
if (progressPercentage < 98) {
list.add(
if (status == VideoDownloadManager.DownloadType.IsDownloading)
Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download)
@ -248,33 +248,34 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
//progressBar.isVisible =
// status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error
//progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete
val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading
progressBarBackground.post {
val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading
if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) {
val animation = AnimationUtils.loadAnimation(context, waitingAnimation)
progressBarBackground.startAnimation(animation)
} else {
progressBarBackground.clearAnimation()
}
if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) {
val animation = AnimationUtils.loadAnimation(context, waitingAnimation)
progressBarBackground.startAnimation(animation)
} else {
progressBarBackground.clearAnimation()
val progressDrawable =
if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline
progressBarBackground.background =
ContextCompat.getDrawable(context, progressDrawable)
val drawable = getDrawableFromStatus(status)
statusView.setImageDrawable(drawable)
val isDrawable = drawable != null
statusView.isVisible = isDrawable
val hide = hideWhenIcon && isDrawable
if (hide) {
progressBar.clearAnimation()
progressBarBackground.clearAnimation()
}
progressBarBackground.isGone = hide
progressBar.isGone = hide
}
val progressDrawable =
if (status == DownloadStatusTell.IsDownloading && !isPreActive) activeOutline else nonActiveOutline
progressBarBackground.background =
ContextCompat.getDrawable(context, progressDrawable)
val drawable = getDrawableFromStatus(status)
statusView.setImageDrawable(drawable)
val isDrawable = drawable != null
statusView.isVisible = isDrawable
val hide = hideWhenIcon && isDrawable
if (hide) {
progressBar.clearAnimation()
progressBarBackground.clearAnimation()
}
progressBarBackground.isGone = hide
progressBar.isGone = hide
}
override fun resetView() {

View File

@ -186,6 +186,27 @@ object M3u8Helper2 {
) {
val size get() = allTsLinks.size
suspend fun resolveLinkWhileSafe(
index: Int,
tries: Int = 3,
failDelay: Long = 3000,
condition : (() -> Boolean)
): ByteArray? {
for (i in 0 until tries) {
if(!condition()) return null
try {
val out = resolveLink(index)
return if(condition()) out else null
} catch (e: IllegalArgumentException) {
return null
} catch (t: Throwable) {
delay(failDelay)
}
}
return null
}
suspend fun resolveLinkSafe(
index: Int,
tries: Int = 3,
@ -240,8 +261,6 @@ object M3u8Helper2 {
verify = false
).text
println("m3u8Response=$m3u8Response")
// encryption, this is because crunchy uses it
var encryptionIv = byteArrayOf()
var encryptionData = byteArrayOf()

View File

@ -2,20 +2,18 @@ package com.lagradost.cloudstream3.utils
import com.fasterxml.jackson.annotation.JsonProperty
import com.lagradost.cloudstream3.TvType
import com.lagradost.cloudstream3.ui.download.EasyDownloadButton
object VideoDownloadHelper {
data class DownloadEpisodeCached(
@JsonProperty("name") val name: String?,
@JsonProperty("poster") val poster: String?,
@JsonProperty("episode") val episode: Int,
@JsonProperty("season") val season: Int?,
@JsonProperty("id") override val id: Int,
@JsonProperty("id") val id: Int,
@JsonProperty("parentId") val parentId: Int,
@JsonProperty("rating") val rating: Int?,
@JsonProperty("description") val description: String?,
@JsonProperty("cacheTime") val cacheTime: Long,
) : EasyDownloadButton.IMinimumData
)
data class DownloadHeaderCached(
@JsonProperty("apiName") val apiName: String,

View File

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/white"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,6h12v12H6z" />
</vector>