From 56cb3d718188bf95e16cc062280bc5a16e42ea24 Mon Sep 17 00:00:00 2001 From: LagradOst <11805592+LagradOst@users.noreply.github.com> Date: Sat, 19 Aug 2023 00:48:00 +0200 Subject: [PATCH] refactored download system for better preference + bugfixes --- .../cloudstream3/DownloaderTestImpl.kt | 2 +- .../com/lagradost/cloudstream3/MainAPI.kt | 2 +- .../ui/download/DownloadChildAdapter.kt | 2 +- .../ui/download/EasyDownloadButton.kt | 264 ----- .../ui/download/button/BaseFetchButton.kt | 61 +- .../ui/download/button/DownloadButton.kt | 17 +- .../ui/download/button/PieFetchButton.kt | 53 +- .../cloudstream3/utils/M3u8Helper.kt | 23 +- .../cloudstream3/utils/VideoDownloadHelper.kt | 6 +- .../utils/VideoDownloadManager.kt | 1016 ++++++++--------- .../main/res/drawable/baseline_stop_24.xml | 10 + 11 files changed, 604 insertions(+), 852 deletions(-) delete mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt create mode 100644 app/src/main/res/drawable/baseline_stop_24.xml diff --git a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt index 379a91e4..0a2db2bd 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/DownloaderTestImpl.kt @@ -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 /** diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt index 7790f047..80332445 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainAPI.kt @@ -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()) diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt index b4774cf8..1d7b5a83 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadChildAdapter.kt @@ -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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt deleted file mode 100644 index 77878432..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/EasyDownloadButton.kt +++ /dev/null @@ -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) -> 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) -> Unit)? = null - private var downloadStatusEventListener: ((Pair) -> 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) -> 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 -> - 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 -> - 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 - } - } -} diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt index 05f630a0..b43f1aac 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/BaseFetchButton.kt @@ -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) { diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt index bb2ba7b1..d97a4b88 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/DownloadButton.kt @@ -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( diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt index 0b7a7fea..d20fcf93 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/button/PieFetchButton.kt @@ -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() { diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt index 6770e303..1fb3a72d 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/M3u8Helper.kt @@ -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() diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt index a76cc115..d1614bc1 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadHelper.kt @@ -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, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index f4eb37b7..0334103f 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -15,7 +15,6 @@ import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri -import androidx.lifecycle.MutableLiveData import androidx.preference.PreferenceManager import androidx.work.Data import androidx.work.ExistingWorkPolicy @@ -30,6 +29,8 @@ import com.lagradost.cloudstream3.AcraApplication.Companion.setKey import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.TvType +import com.lagradost.cloudstream3.amap +import com.lagradost.cloudstream3.app import com.lagradost.cloudstream3.mvvm.logError import com.lagradost.cloudstream3.mvvm.suspendSafeApiCall import com.lagradost.cloudstream3.services.VideoDownloadService @@ -42,13 +43,12 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import okhttp3.internal.closeQuietly -import java.io.BufferedInputStream +import java.io.Closeable import java.io.File +import java.io.IOException import java.io.InputStream import java.io.OutputStream -import java.lang.Thread.sleep import java.net.URL -import java.net.URLConnection import java.util.* const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general" @@ -60,34 +60,31 @@ object VideoDownloadManager { private var currentDownloads = mutableListOf() private 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" - @DrawableRes - const val imgDone = R.drawable.rddone + @get:DrawableRes + val imgDone get() = R.drawable.rddone - @DrawableRes - const val imgDownloading = R.drawable.rdload + @get:DrawableRes + val imgDownloading get() = R.drawable.rdload - @DrawableRes - const val imgPaused = R.drawable.rdpause + @get:DrawableRes + val imgPaused get() = R.drawable.rdpause - @DrawableRes - const val imgStopped = R.drawable.rderror + @get:DrawableRes + val imgStopped get() = R.drawable.rderror - @DrawableRes - const val imgError = R.drawable.rderror + @get:DrawableRes + val imgError get() = R.drawable.rderror - @DrawableRes - const val pressToPauseIcon = R.drawable.ic_baseline_pause_24 + @get:DrawableRes + val pressToPauseIcon get() = R.drawable.ic_baseline_pause_24 - @DrawableRes - const val pressToResumeIcon = R.drawable.ic_baseline_play_arrow_24 + @get:DrawableRes + val pressToResumeIcon get() = R.drawable.ic_baseline_play_arrow_24 - @DrawableRes - const val pressToStopIcon = R.drawable.exo_icon_stop - - private var updateCount: Int = 0 - private val downloadDataUpdateCount = MutableLiveData() + @get:DrawableRes + val pressToStopIcon get() = R.drawable.baseline_stop_24 enum class DownloadType { IsPaused, @@ -251,9 +248,8 @@ object VideoDownloadManager { total: Long, notificationCallback: (Int, Notification) -> Unit, hlsProgress: Long? = null, - hlsTotal: Long? = null, - - ): Notification? { + hlsTotal: Long? = null + ): Notification? { try { if (total <= 0) return null// crash, invalid data @@ -336,14 +332,28 @@ object VideoDownloadManager { } val bigText = - if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" - } else if (state == DownloadType.IsFailed) { - downloadFormat.format(context.getString(R.string.download_failed), rowTwo) - } else if (state == DownloadType.IsDone) { - downloadFormat.format(context.getString(R.string.download_done), rowTwo) - } else { - downloadFormat.format(context.getString(R.string.download_canceled), rowTwo) + when (state) { + DownloadType.IsDownloading, DownloadType.IsPaused -> { + (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix" + } + + DownloadType.IsFailed -> { + downloadFormat.format( + context.getString(R.string.download_failed), + rowTwo + ) + } + + DownloadType.IsDone -> { + downloadFormat.format(context.getString(R.string.download_done), rowTwo) + } + + else -> { + downloadFormat.format( + context.getString(R.string.download_canceled), + rowTwo + ) + } } val bodyStyle = NotificationCompat.BigTextStyle() @@ -351,14 +361,28 @@ object VideoDownloadManager { builder.setStyle(bodyStyle) } else { val txt = - if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - rowTwo - } else if (state == DownloadType.IsFailed) { - downloadFormat.format(context.getString(R.string.download_failed), rowTwo) - } else if (state == DownloadType.IsDone) { - downloadFormat.format(context.getString(R.string.download_done), rowTwo) - } else { - downloadFormat.format(context.getString(R.string.download_canceled), rowTwo) + when (state) { + DownloadType.IsDownloading, DownloadType.IsPaused -> { + rowTwo + } + + DownloadType.IsFailed -> { + downloadFormat.format( + context.getString(R.string.download_failed), + rowTwo + ) + } + + DownloadType.IsDone -> { + downloadFormat.format(context.getString(R.string.download_done), rowTwo) + } + + else -> { + downloadFormat.format( + context.getString(R.string.download_canceled), + rowTwo + ) + } } builder.setContentText(txt) @@ -681,6 +705,171 @@ object VideoDownloadManager { return StreamData(SUCCESS_STREAM, resume, fileLength, fileStream) } + /** This class handles the notifications, as well as the relevant key */ + data class DownloadMetaData( + private val id: Int?, + var bytesDownloaded: Long = 0, + var totalBytes: Long? = null, + + // notification metadata + private var lastUpdatedMs: Long = 0, + private val createNotificationCallback: (CreateNotificationMetadata) -> Unit, + + private var internalType: DownloadType = DownloadType.IsPending, + + // how many segments that we have downloaded + var hlsProgress: Int = 0, + // how many segments that exist + var hlsTotal: Int? = null, + // this is how many segments that has been written to the file + // will always be <= hlsProgress as we may keep some in a buffer + var hlsWrittenProgress: Long = 0, + + // this is used for copy with metadata on how much we have downloaded for setKey + private var downloadFileInfoTemplate: DownloadedFileInfo? = null + ) : Closeable { + val approxTotalBytes: Long + get() = totalBytes ?: hlsTotal?.let { total -> + (bytesDownloaded * (total / hlsProgress.toFloat())).toLong() + } ?: 0L + + private val isHLS get() = hlsTotal != null + + private val downloadEventListener = { event: Pair -> + if (event.first == id) { + when (event.second) { + DownloadActionType.Pause -> { + type = DownloadType.IsPaused + } + + DownloadActionType.Stop -> { + type = DownloadType.IsStopped + removeKey(KEY_RESUME_PACKAGES, event.first.toString()) + saveQueue() + } + + DownloadActionType.Resume -> { + type = DownloadType.IsDownloading + } + } + } + } + + private fun updateFileInfo() { + if (id == null) return + downloadFileInfoTemplate?.let { template -> + setKey( + KEY_DOWNLOAD_INFO, + id.toString(), + template.copy( + totalBytes = approxTotalBytes, + extraInfo = if (isHLS) hlsWrittenProgress.toString() else null + ) + ) + } + } + + fun setDownloadFileInfoTemplate(template: DownloadedFileInfo) { + downloadFileInfoTemplate = template + updateFileInfo() + } + + init { + if (id != null) { + downloadEvent += downloadEventListener + } + } + + override fun close() { + // as we may need to resume hls downloads, we save the current written index + if (isHLS) { + updateFileInfo() + } + if (id != null) { + downloadEvent -= downloadEventListener + downloadStatus -= id + } + } + + var type + get() = internalType + set(value) { + internalType = value + notify() + } + + companion object { + const val UPDATE_RATE_MS: Long = 1000L + } + + @JvmName("DownloadMetaDataNotify") + private fun notify() { + lastUpdatedMs = System.currentTimeMillis() + try { + val bytes = approxTotalBytes + + // notification creation + if (isHLS) { + createNotificationCallback( + CreateNotificationMetadata( + internalType, + bytesDownloaded, + bytes, + hlsTotal = hlsTotal?.toLong(), + hlsProgress = hlsProgress.toLong() + ) + ) + } else { + createNotificationCallback( + CreateNotificationMetadata( + internalType, + bytesDownloaded, + bytes, + ) + ) + } + + // as hls has an approx file size we want to update this metadata + if (isHLS) { + updateFileInfo() + } + + // push all events, this *should* not crash, TODO MUTEX? + if (id != null) { + downloadStatus[id] = type + downloadProgressEvent(Triple(id, bytesDownloaded, bytes)) + downloadStatusEvent(id to type) + } + } catch (t: Throwable) { + logError(t) + } + } + + private fun checkNotification() { + if (lastUpdatedMs + UPDATE_RATE_MS > System.currentTimeMillis()) return + notify() + } + + + /** adds the length and pushes a notification if necessary */ + fun addBytes(length: Long) { + bytesDownloaded += length + // we don't want to update the notification after it is paused, + // download progress may not stop directly when we "pause" it + if (type == DownloadType.IsDownloading) checkNotification() + } + + /** adds the length + hsl progress and pushes a notification if necessary */ + fun addSegment(length: Long) { + hlsProgress += 1 + addBytes(length) + } + + fun setWrittenSegment(segmentIndex: Int) { + hlsWrittenProgress = segmentIndex.toLong() + 1L + } + } + @Throws suspend fun downloadThing( context: Context, @@ -692,253 +881,225 @@ object VideoDownloadManager { parentId: Int?, createNotificationCallback: (CreateNotificationMetadata) -> Unit, ): Int = withContext(Dispatchers.IO) { + // we cant download torrents with this implementation, aria2c might be used in the future if (link.url.startsWith("magnet") || link.url.endsWith(".torrent")) { return@withContext ERROR_UNKNOWN } - val basePath = context.getBasePath() - - val displayName = getDisplayName(name, extension) - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath( - folder - ) else folder - - fun deleteFile(): Int { - return delete(context, name, relativePath, extension, parentId, basePath.first) - } - - val stream = setupStream(context, name, relativePath, extension, tryResume) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - - val resume = stream.resume!! - val fileStream = stream.fileStream!! - val fileLength = stream.fileLength!! - - // CONNECT - val connection: URLConnection = - URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK - - // SET CONNECTION SETTINGS - connection.connectTimeout = 10000 - connection.setRequestProperty("Accept-Encoding", "identity") - connection.setRequestProperty("user-agent", USER_AGENT) - if (link.referer.isNotEmpty()) connection.setRequestProperty("referer", link.referer) - - // extra stuff - connection.setRequestProperty( - "sec-ch-ua", - "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"" + var fileStream: OutputStream? = null + var requestStream: InputStream? = null + val metadata = DownloadMetaData( + totalBytes = 0, + bytesDownloaded = 0, + createNotificationCallback = createNotificationCallback, + id = parentId, ) + try { + // get the file path + val (baseFile, basePath) = context.getBasePath() + val displayName = getDisplayName(name, extension) + val relativePath = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( + folder + ) else folder - connection.setRequestProperty("sec-ch-ua-mobile", "?0") - connection.setRequestProperty("accept", "*/*") - // dataSource.setRequestProperty("Sec-Fetch-Site", "none") //same-site - connection.setRequestProperty("sec-fetch-user", "?1") - connection.setRequestProperty("sec-fetch-mode", "navigate") - connection.setRequestProperty("sec-fetch-dest", "video") - link.headers.entries.forEach { - connection.setRequestProperty(it.key, it.value) - } + // set up the download file + val stream = setupStream(context, name, relativePath, extension, tryResume) + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode + fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN + val resume = stream.resume ?: return@withContext ERROR_UNKNOWN + val fileLength = stream.fileLength ?: return@withContext ERROR_UNKNOWN + val resumeAt = (if (resume) fileLength else 0) + metadata.bytesDownloaded = resumeAt + metadata.type = DownloadType.IsPending - if (resume) - connection.setRequestProperty("Range", "bytes=${fileLength}-") - val resumeLength = (if (resume) fileLength else 0) + // set up a connection + val request = app.get( + link.url.replace(" ", "%20"), + headers = link.headers + mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + "sec-ch-ua" to "\"Chromium\";v=\"91\", \" Not;A Brand\";v=\"99\"", + "sec-fetch-mode" to "navigate", + "sec-fetch-dest" to "video", + "sec-fetch-user" to "?1", + "sec-ch-ua-mobile" to "?0", + ) + if (resumeAt > 0) mapOf("Range" to "bytes=${resumeAt}-") else emptyMap(), + referer = link.referer, + verify = false + ) - // ON CONNECTION - connection.connect() + // init variables + val contentLength = request.size ?: 0 + metadata.totalBytes = contentLength + resumeAt - val contentLength = try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // fuck android - connection.contentLengthLong - } else { - connection.getHeaderField("content-length").toLongOrNull() - ?: connection.contentLength.toLong() - } - } catch (e: Exception) { - logError(e) - 0L - } - val bytesTotal = contentLength + resumeLength - - if (extension == "mp4" && bytesTotal < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG - - parentId?.let { - setKey( - KEY_DOWNLOAD_INFO, - it.toString(), + // save + metadata.setDownloadFileInfoTemplate( DownloadedFileInfo( - bytesTotal, - relativePath ?: "", - displayName, - basePath = basePath.second + totalBytes = metadata.approxTotalBytes, + relativePath = relativePath ?: "", + displayName = displayName, + basePath = basePath ) ) + + // total length is less than 5mb, that is too short and something has gone wrong + if (extension == "mp4" && metadata.approxTotalBytes < 5000000) return@withContext ERROR_TOO_SMALL_CONNECTION + + // read the buffer into the filestream, this is equivalent of transferTo + requestStream = request.body.byteStream() + metadata.type = DownloadType.IsDownloading + + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var read: Int + while (requestStream.read(buffer, 0, DEFAULT_BUFFER_SIZE).also { read = it } >= 0) { + fileStream.write(buffer, 0, read) + + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped) break + metadata.addBytes(read.toLong()) + } + + if (metadata.type == DownloadType.IsStopped) { + return@withContext delete( + context, + name, + relativePath, + extension, + parentId, + baseFile + ) + } + + metadata.type = DownloadType.IsDone + return@withContext SUCCESS_DOWNLOAD_DONE + } catch (e: IOException) { + // some sort of IO error, this should not happened + // we just rethrow it + logError(e) + throw e + } catch (t: Throwable) { + // some sort of network error, will error + + // note that when failing we don't want to delete the file, + // only user interaction has that power + metadata.type = DownloadType.IsFailed + return@withContext ERROR_CONNECTION_ERROR + } finally { + fileStream?.closeQuietly() + requestStream?.closeQuietly() + metadata.close() } + } - // Could use connection.contentType for mime types when creating the file, - // however file is already created and players don't go of file type - // https://stackoverflow.com/questions/23714383/what-are-all-the-possible-values-for-http-content-type-header - // might receive application/octet-stream - /*if (!connection.contentType.isNullOrEmpty() && !connection.contentType.startsWith("video")) { - return ERROR_WRONG_CONTENT // CONTENT IS NOT VIDEO, SHOULD NEVER HAPPENED, BUT JUST IN CASE - }*/ + @Throws + private suspend fun downloadHLS( + context: Context, + link: ExtractorLink, + name: String, + folder: String?, + parentId: Int?, + startIndex: Int?, + createNotificationCallback: (CreateNotificationMetadata) -> Unit, + parallelConnections: Int = 3 + ): Int = withContext(Dispatchers.IO) { + require(parallelConnections >= 1) - // READ DATA FROM CONNECTION - val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream) - val buffer = ByteArray(1024) - var count: Int - var bytesDownloaded = resumeLength + val metadata = DownloadMetaData( + createNotificationCallback = createNotificationCallback, + id = parentId + ) + val extension = "mp4" - var isPaused = false - var isStopped = false - var isDone = false - var isFailed = false + var fileStream: OutputStream? = null + try { + // the start .ts index + var startAt = startIndex ?: 0 - // TO NOT REUSE CODE - fun updateNotification() { - val type = when { - isDone -> DownloadType.IsDone - isStopped -> DownloadType.IsStopped - isFailed -> DownloadType.IsFailed - isPaused -> DownloadType.IsPaused - else -> DownloadType.IsDownloading - } + // set up the file data + val (baseFile, basePath) = context.getBasePath() + val relativePath = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && baseFile.isDownloadDir()) getRelativePath( + folder + ) else folder + val displayName = getDisplayName(name, extension) + val stream = setupStream(context, name, relativePath, extension, startAt > 0) + if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode + if (stream.resume != true) startAt = 0 + fileStream = stream.fileStream ?: return@withContext ERROR_UNKNOWN - parentId?.let { id -> - try { - downloadStatus[id] = type - downloadStatusEvent.invoke(Pair(id, type)) - downloadProgressEvent.invoke(Triple(id, bytesDownloaded, bytesTotal)) - } catch (e: Exception) { - // IDK MIGHT ERROR - } - } - - createNotificationCallback.invoke( - CreateNotificationMetadata( - type, - bytesDownloaded, - bytesTotal + // push the metadata + metadata.bytesDownloaded = stream.fileLength ?: 0 + metadata.hlsProgress = startAt + metadata.type = DownloadType.IsPending + metadata.setDownloadFileInfoTemplate( + DownloadedFileInfo( + totalBytes = 0, + relativePath = relativePath ?: "", + displayName = displayName, + basePath = basePath ) ) - /*createNotification( - context, - source, - link.name, - ep, - type, - bytesDownloaded, - bytesTotal - )*/ - } - val downloadEventListener = { event: Pair -> - if (event.first == parentId) { - when (event.second) { - DownloadActionType.Pause -> { - isPaused = true; updateNotification() + // do the initial get request to fetch the segments + val m3u8 = M3u8Helper.M3u8Stream( + link.url, link.quality, mapOf( + "Accept-Encoding" to "identity", + "accept" to "*/*", + "user-agent" to USER_AGENT, + ) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap() + ) + val items = M3u8Helper2.hslLazy(listOf(m3u8)) + + metadata.hlsTotal = items.size + metadata.type = DownloadType.IsDownloading + + // does several connections in parallel instead of a regular for loop to improve + // download speed + (startAt until items.size).chunked(parallelConnections).forEach { subset -> + // wait until not paused + while (metadata.type == DownloadType.IsPaused) delay(100) + // if stopped then break to delete + if (metadata.type == DownloadType.IsStopped) return@forEach + + subset.amap { idx -> + idx to items.resolveLinkSafe(idx)?.also { bytes -> + metadata.addSegment(bytes.size.toLong()) } - - DownloadActionType.Stop -> { - isStopped = true; updateNotification() - removeKey(KEY_RESUME_PACKAGES, event.first.toString()) - saveQueue() - } - - DownloadActionType.Resume -> { - isPaused = false; updateNotification() + }.forEach { (idx, bytes) -> + if (bytes == null) { + metadata.type = DownloadType.IsFailed + return@withContext ERROR_CONNECTION_ERROR } + fileStream.write(bytes) + metadata.setWrittenSegment(idx) } } - } - if (parentId != null) - downloadEvent += downloadEventListener - - // UPDATE DOWNLOAD NOTIFICATION - val notificationCoroutine = main { - while (true) { - if (!isPaused) { - updateNotification() - } - for (i in 1..10) { - delay(100) - } - } - } - - // THE REAL READ - try { - while (true) { - count = connectionInputStream.read(buffer) - if (count < 0) break - bytesDownloaded += count - // downloadProgressEvent.invoke(Pair(id, bytesDownloaded)) // Updates too much for any UI to keep up with - while (isPaused) { - sleep(100) - if (isStopped) { - break - } - } - if (isStopped) { - break - } - fileStream.write(buffer, 0, count) - } - } catch (e: Exception) { - logError(e) - isFailed = true - updateNotification() - } - - // REMOVE AND EXIT ALL - fileStream.close() - connectionInputStream.close() - notificationCoroutine.cancel() - - try { - if (parentId != null) - downloadEvent -= downloadEventListener - } catch (e: Exception) { - logError(e) - } - - try { - parentId?.let { - downloadStatus.remove(it) - } - } catch (e: Exception) { - // IDK MIGHT ERROR - } - - // RETURN MESSAGE - return@withContext when { - isFailed -> { - parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } - ERROR_CONNECTION_ERROR + if (metadata.type == DownloadType.IsStopped) { + return@withContext delete( + context, + name, + relativePath, + extension, + parentId, + baseFile + ) } - isStopped -> { - parentId?.let { id -> downloadProgressEvent.invoke(Triple(id, 0, 0)) } - deleteFile() - } - - else -> { - parentId?.let { id -> - downloadProgressEvent.invoke( - Triple( - id, - bytesDownloaded, - bytesTotal - ) - ) - } - isDone = true - updateNotification() - SUCCESS_DOWNLOAD_DONE - } + metadata.type = DownloadType.IsDone + return@withContext SUCCESS_DOWNLOAD_DONE + } catch (t: Throwable) { + logError(t) + metadata.type = DownloadType.IsFailed + return@withContext ERROR_UNKNOWN + } finally { + fileStream?.closeQuietly() + metadata.close() } } @@ -1107,192 +1268,6 @@ object VideoDownloadManager { return SUCCESS_STOPPED } - @Throws - private suspend fun downloadHLS( - context: Context, - link: ExtractorLink, - name: String, - folder: String?, - parentId: Int?, - startIndex: Int?, - createNotificationCallback: (CreateNotificationMetadata) -> Unit - ): Int = withContext(Dispatchers.IO) { - val extension = "mp4" - - val m3u8 = M3u8Helper.M3u8Stream( - link.url, link.quality, mapOf("referer" to link.referer) - ) - - var realIndex = startIndex ?: 0 - val basePath = context.getBasePath() - - val relativePath = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && basePath.first.isDownloadDir()) getRelativePath( - folder - ) else folder - - val stream = setupStream(context, name, relativePath, extension, realIndex > 0) - if (stream.errorCode != SUCCESS_STREAM) return@withContext stream.errorCode - - if (stream.resume != true) realIndex = 0 - val fileLengthAdd = stream.fileLength ?: 0 - val items = M3u8Helper2.hslLazy(listOf(m3u8)) - - val displayName = getDisplayName(name, extension) - - val fileStream = stream.fileStream!! - - var isDone = false - var isFailed = false - var isPaused = false - var bytesDownloaded = fileLengthAdd - var tsProgress: Long = realIndex.toLong() + 1 // we don't want div by zero - val totalTs: Long = items.size.toLong() - - fun deleteFile(): Int { - return delete(context, name, relativePath, extension, parentId, basePath.first) - } - - fun updateInfo() { - setKey( - KEY_DOWNLOAD_INFO, - (parentId ?: return).toString(), - DownloadedFileInfo( - // approx bytes - totalBytes = (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - relativePath = relativePath ?: "", - displayName = displayName, - extraInfo = tsProgress.toString(), - basePath = basePath.second - ) - ) - } - - updateInfo() - - fun updateNotification() { - val type = when { - isDone -> DownloadType.IsDone - isFailed -> DownloadType.IsFailed - isPaused -> DownloadType.IsPaused - else -> DownloadType.IsDownloading - } - - parentId?.let { id -> - try { - downloadStatus[id] = type - downloadStatusEvent.invoke(Pair(id, type)) - downloadProgressEvent.invoke( - Triple( - id, - bytesDownloaded, - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - ) - ) - } catch (_: Throwable) {} - } - - createNotificationCallback.invoke( - CreateNotificationMetadata( - type, - bytesDownloaded, - (bytesDownloaded * (totalTs / tsProgress.toFloat())).toLong(), - tsProgress, - totalTs - ) - ) - } - - val notificationCoroutine = main { - while (true) { - if (!isDone) { - updateNotification() - } - for (i in 1..10) { - delay(100) - } - } - } - - val downloadEventListener = { event: Pair -> - if (event.first == parentId) { - when (event.second) { - DownloadActionType.Stop -> { - isFailed = true - } - - DownloadActionType.Pause -> { - isPaused = true - } - - DownloadActionType.Resume -> { - isPaused = false - } - } - updateNotification() - } - } - - fun closeAll() { - try { - if (parentId != null) - downloadEvent -= downloadEventListener - } catch (t: Throwable) { - logError(t) - } - try { - parentId?.let { - downloadStatus.remove(it) - } - } catch (t: Throwable) { - logError(t) - } - notificationCoroutine.cancel() - } - - if (parentId != null) - downloadEvent += downloadEventListener - - fun onFailed() { - fileStream.close() - deleteFile() - updateNotification() - closeAll() - } - - for (idx in realIndex until items.size) { - while (isPaused) { - if (isFailed) { - onFailed() - return@withContext SUCCESS_STOPPED - } - delay(100) - } - - if (isFailed) { - onFailed() - return@withContext SUCCESS_STOPPED - } - - val bytes = items.resolveLinkSafe(idx) ?: run { - isFailed = true - onFailed() - return@withContext ERROR_CONNECTION_ERROR - } - - fileStream.write(bytes) - tsProgress = idx.toLong() + 1 - bytesDownloaded += bytes.size.toLong() - updateInfo() - } - isDone = true - fileStream.close() - updateNotification() - - closeAll() - updateInfo() - return@withContext SUCCESS_DOWNLOAD_DONE - } fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String { return getFileName(context, metadata.name, metadata.episode, metadata.season) @@ -1353,22 +1328,30 @@ object VideoDownloadManager { )?.extraInfo?.toIntOrNull() } else null return suspendSafeApiCall { - downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> - main { - createNotification( - context, - source, - link.name, - ep, - meta.type, - meta.bytesDownloaded, - meta.bytesTotal, - notificationCallback, - meta.hlsProgress, - meta.hlsTotal - ) + downloadHLS( + context, + link, + name, + folder, + ep.id, + startIndex, + createNotificationCallback = { meta -> + main { + createNotification( + context, + source, + link.name, + ep, + meta.type, + meta.bytesDownloaded, + meta.bytesTotal, + notificationCallback, + meta.hlsProgress, + meta.hlsTotal + ) + } } - } + ) }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN @@ -1392,7 +1375,7 @@ object VideoDownloadManager { }.also { extractorJob.cancel() } ?: ERROR_UNKNOWN } - fun downloadCheck( + suspend fun downloadCheck( context: Context, notificationCallback: (Int, Notification) -> Unit, ): Int? { if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) { @@ -1407,42 +1390,55 @@ object VideoDownloadManager { currentDownloads.add(id) - main { - try { - for (index in (pkg.linkIndex ?: 0) until item.links.size) { - val link = item.links[index] - val resume = pkg.linkIndex == index + try { + for (index in (pkg.linkIndex ?: 0) until item.links.size) { + val link = item.links[index] + val resume = pkg.linkIndex == index - setKey( - KEY_RESUME_PACKAGES, - id.toString(), - DownloadResumePackage(item, index) + setKey( + KEY_RESUME_PACKAGES, + id.toString(), + DownloadResumePackage(item, index) + ) + + var connectionResult = + downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + resume + ) + //.also { println("Single episode finished with return code: $it") } + + // retry every link at least once + if (connectionResult <= 0) { + connectionResult = downloadSingleEpisode( + context, + item.source, + item.folder, + item.ep, + link, + notificationCallback, + true ) - val connectionResult = withContext(Dispatchers.IO) { - downloadSingleEpisode( - context, - item.source, - item.folder, - item.ep, - link, - notificationCallback, - resume - ).also { println("Single episode finished with return code: $it") } - } - if (connectionResult != null && connectionResult > 0) { // SUCCESS - removeKey(KEY_RESUME_PACKAGES, id.toString()) - break - } else if (index == item.links.lastIndex) { - downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) - } } - } catch (e: Exception) { - logError(e) - } finally { - currentDownloads.remove(id) - // Because otherwise notifications will not get caught by the workmanager - downloadCheckUsingWorker(context) + + if (connectionResult > 0) { // SUCCESS + removeKey(KEY_RESUME_PACKAGES, id.toString()) + break + } else if (index == item.links.lastIndex) { + downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed)) + } } + } catch (e: Exception) { + logError(e) + } finally { + currentDownloads.remove(id) + // Because otherwise notifications will not get caught by the work manager + downloadCheckUsingWorker(context) } } return null @@ -1538,26 +1534,13 @@ object VideoDownloadManager { return context.getKey(KEY_RESUME_PACKAGES, id.toString()) } - fun downloadFromResume( + suspend fun downloadFromResume( context: Context, pkg: DownloadResumePackage, notificationCallback: (Int, Notification) -> Unit, setKey: Boolean = true ) { if (!currentDownloads.any { it == pkg.item.ep.id }) { -// if (currentDownloads.size == maxConcurrentDownloads) { -// main { -//// showToast( // can be replaced with regular Toast -//// context, -//// "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${ -//// context.getString( -//// R.string.queued -//// ) -//// }", -//// Toast.LENGTH_SHORT -//// ) -// } -// } downloadQueue.addLast(pkg) downloadCheck(context, notificationCallback) if (setKey) saveQueue() @@ -1590,7 +1573,7 @@ object VideoDownloadManager { return false }*/ - fun downloadEpisode( + suspend fun downloadEpisode( context: Context?, source: String?, folder: String?, @@ -1599,13 +1582,12 @@ object VideoDownloadManager { notificationCallback: (Int, Notification) -> Unit, ) { if (context == null) return - if (links.isNotEmpty()) { - downloadFromResume( - context, - DownloadResumePackage(DownloadItem(source, folder, ep, links), null), - notificationCallback - ) - } + if (links.isEmpty()) return + downloadFromResume( + context, + DownloadResumePackage(DownloadItem(source, folder, ep, links), null), + notificationCallback + ) } /** Worker stuff */ diff --git a/app/src/main/res/drawable/baseline_stop_24.xml b/app/src/main/res/drawable/baseline_stop_24.xml new file mode 100644 index 00000000..100cb1fc --- /dev/null +++ b/app/src/main/res/drawable/baseline_stop_24.xml @@ -0,0 +1,10 @@ + + +