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 { companion object {
private const val USER_AGENT = 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 private var instance: DownloaderTestImpl? = null
/** /**

View file

@ -29,7 +29,7 @@ import java.util.*
import kotlin.math.absoluteValue import kotlin.math.absoluteValue
const val USER_AGENT = 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 baseHeader = mapOf("User-Agent" to USER_AGENT)
val mapper = JsonMapper.builder().addModule(KotlinModule()) val mapper = JsonMapper.builder().addModule(KotlinModule())

View file

@ -23,7 +23,7 @@ data class VisualDownloadChildCached(
val data: VideoDownloadHelper.DownloadEpisodeCached, 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( class DownloadChildAdapter(
var cardList: List<VisualDownloadChildCached>, 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 val progressPercentage: Long
get() = if (downloadedLength < 1024) 0 else maxOf( get() = if (downloadedLength < 1024) 0 else maxOf(
0, 0,
minOf(100, (downloadedLength * 100L) / totalLength) minOf(100, (downloadedLength * 100L) / (totalLength + 1))
) )
} }
@ -101,9 +101,11 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
open fun setProgress(downloadedBytes: Long, totalBytes: Long) { open fun setProgress(downloadedBytes: Long, totalBytes: Long) {
isZeroBytes = downloadedBytes == 0L isZeroBytes = downloadedBytes == 0L
progressBar.post {
val steps = 10000L val steps = 10000L
progressBar.max = steps.toInt() progressBar.max = steps.toInt()
// div by zero error and 1 byte off is ok impo // div by zero error and 1 byte off is ok impo
val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt() val progress = (downloadedBytes * steps / (totalBytes + 1L)).toInt()
val animation = ProgressBarAnimation( val animation = ProgressBarAnimation(
@ -134,6 +136,7 @@ abstract class BaseFetchButton(context: Context, attributeSet: AttributeSet) :
progressBar.startAnimation(animation) progressBar.startAnimation(animation)
} }
}
fun downloadStatusEvent(data: Pair<Int, VideoDownloadManager.DownloadType>) { fun downloadStatusEvent(data: Pair<Int, VideoDownloadManager.DownloadType>) {
val (id, status) = data val (id, status) = data

View file

@ -21,7 +21,7 @@ class DownloadButton(context: Context, attributeSet: AttributeSet) :
} }
override fun setStatus(status: DownloadStatusTell?) { override fun setStatus(status: DownloadStatusTell?) {
super.setStatus(status) mainText?.post {
val txt = when (status) { val txt = when (status) {
DownloadStatusTell.IsPaused -> R.string.download_paused DownloadStatusTell.IsPaused -> R.string.download_paused
DownloadStatusTell.IsDownloading -> R.string.downloading DownloadStatusTell.IsDownloading -> R.string.downloading
@ -30,6 +30,9 @@ class DownloadButton(context: Context, attributeSet: AttributeSet) :
} }
mainText?.setText(txt) mainText?.setText(txt)
} }
super.setStatus(status)
}
override fun setDefaultClickListener( override fun setDefaultClickListener(
card: VideoDownloadHelper.DownloadEpisodeCached, card: VideoDownloadHelper.DownloadEpisodeCached,

View file

@ -174,7 +174,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
currentMetaData.apply { currentMetaData.apply {
// DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone && // DON'T RESUME A DOWNLOADED FILE lastState != VideoDownloadManager.DownloadType.IsDone &&
if ((downloadedLength * 100 / totalLength) < 98) { if (progressPercentage < 98) {
list.add( list.add(
if (status == VideoDownloadManager.DownloadType.IsDownloading) if (status == VideoDownloadManager.DownloadType.IsDownloading)
Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download) Pair(DOWNLOAD_ACTION_PAUSE_DOWNLOAD, R.string.popup_pause_download)
@ -248,8 +248,8 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
//progressBar.isVisible = //progressBar.isVisible =
// status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error // status != null && status != DownloadStatusTell.Complete && status != DownloadStatusTell.Error
//progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete //progressBarBackground.isVisible = status != null && status != DownloadStatusTell.Complete
progressBarBackground.post {
val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading val isPreActive = isZeroBytes && status == DownloadStatusTell.IsDownloading
if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) { if (animateWaiting && (status == DownloadStatusTell.IsPending || isPreActive)) {
val animation = AnimationUtils.loadAnimation(context, waitingAnimation) val animation = AnimationUtils.loadAnimation(context, waitingAnimation)
progressBarBackground.startAnimation(animation) progressBarBackground.startAnimation(animation)
@ -276,6 +276,7 @@ open class PieFetchButton(context: Context, attributeSet: AttributeSet) :
progressBarBackground.isGone = hide progressBarBackground.isGone = hide
progressBar.isGone = hide progressBar.isGone = hide
} }
}
override fun resetView() { override fun resetView() {
setStatus(null) setStatus(null)

View file

@ -186,6 +186,27 @@ object M3u8Helper2 {
) { ) {
val size get() = allTsLinks.size 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( suspend fun resolveLinkSafe(
index: Int, index: Int,
tries: Int = 3, tries: Int = 3,
@ -240,8 +261,6 @@ object M3u8Helper2 {
verify = false verify = false
).text ).text
println("m3u8Response=$m3u8Response")
// encryption, this is because crunchy uses it // encryption, this is because crunchy uses it
var encryptionIv = byteArrayOf() var encryptionIv = byteArrayOf()
var encryptionData = byteArrayOf() var encryptionData = byteArrayOf()

View file

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