2021-06-29 23:14:48 +00:00
|
|
|
package com.lagradost.cloudstream3.utils
|
|
|
|
|
2021-09-19 22:46:05 +00:00
|
|
|
import android.app.Notification
|
2021-09-01 21:30:21 +00:00
|
|
|
import android.app.NotificationChannel
|
|
|
|
import android.app.NotificationManager
|
|
|
|
import android.app.PendingIntent
|
2021-07-05 18:09:37 +00:00
|
|
|
import android.content.*
|
2021-06-29 23:14:48 +00:00
|
|
|
import android.graphics.Bitmap
|
2021-07-05 18:09:37 +00:00
|
|
|
import android.net.Uri
|
2021-06-29 23:14:48 +00:00
|
|
|
import android.os.Build
|
|
|
|
import androidx.annotation.DrawableRes
|
|
|
|
import androidx.core.app.NotificationCompat
|
|
|
|
import androidx.core.app.NotificationManagerCompat
|
|
|
|
import androidx.core.net.toUri
|
2021-11-01 15:33:46 +00:00
|
|
|
import androidx.preference.PreferenceManager
|
2021-09-14 22:18:03 +00:00
|
|
|
import androidx.work.Data
|
|
|
|
import androidx.work.ExistingWorkPolicy
|
|
|
|
import androidx.work.OneTimeWorkRequest
|
|
|
|
import androidx.work.WorkManager
|
2023-02-19 18:27:40 +00:00
|
|
|
import com.bumptech.glide.load.model.GlideUrl
|
2021-09-14 22:18:03 +00:00
|
|
|
import com.fasterxml.jackson.annotation.JsonProperty
|
2022-02-25 23:04:00 +00:00
|
|
|
import com.lagradost.cloudstream3.APIHolder.getApiFromNameNull
|
2021-12-16 23:45:20 +00:00
|
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.removeKey
|
|
|
|
import com.lagradost.cloudstream3.AcraApplication.Companion.setKey
|
2023-08-19 02:46:47 +00:00
|
|
|
import com.lagradost.cloudstream3.BuildConfig
|
2021-06-29 23:14:48 +00:00
|
|
|
import com.lagradost.cloudstream3.MainActivity
|
|
|
|
import com.lagradost.cloudstream3.R
|
2022-04-03 20:14:51 +00:00
|
|
|
import com.lagradost.cloudstream3.TvType
|
2023-08-18 22:48:00 +00:00
|
|
|
import com.lagradost.cloudstream3.app
|
2021-07-04 17:00:04 +00:00
|
|
|
import com.lagradost.cloudstream3.mvvm.logError
|
2023-08-22 02:00:05 +00:00
|
|
|
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
|
2021-07-04 17:00:04 +00:00
|
|
|
import com.lagradost.cloudstream3.services.VideoDownloadService
|
2022-02-25 23:04:00 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
|
2021-07-04 00:59:51 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.Coroutines.main
|
2021-07-05 20:28:50 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
2021-07-05 18:09:37 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
2021-08-19 20:05:18 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
|
2023-08-23 04:25:06 +00:00
|
|
|
import com.lagradost.cloudstream3.utils.storage.MediaFileContentType
|
|
|
|
import com.lagradost.cloudstream3.utils.storage.SafeFile
|
2023-08-19 19:37:14 +00:00
|
|
|
import kotlinx.coroutines.CancellationException
|
|
|
|
import kotlinx.coroutines.CoroutineScope
|
2021-07-04 00:59:51 +00:00
|
|
|
import kotlinx.coroutines.Dispatchers
|
2023-08-19 23:29:50 +00:00
|
|
|
import kotlinx.coroutines.Job
|
2023-08-19 19:37:14 +00:00
|
|
|
import kotlinx.coroutines.cancel
|
2021-07-04 17:00:04 +00:00
|
|
|
import kotlinx.coroutines.delay
|
2023-08-19 19:37:14 +00:00
|
|
|
import kotlinx.coroutines.isActive
|
|
|
|
import kotlinx.coroutines.launch
|
|
|
|
import kotlinx.coroutines.sync.Mutex
|
|
|
|
import kotlinx.coroutines.sync.withLock
|
2021-07-04 00:59:51 +00:00
|
|
|
import kotlinx.coroutines.withContext
|
2021-11-02 14:25:12 +00:00
|
|
|
import okhttp3.internal.closeQuietly
|
2023-08-18 22:48:00 +00:00
|
|
|
import java.io.Closeable
|
2021-11-06 21:06:13 +00:00
|
|
|
import java.io.File
|
2023-08-18 22:48:00 +00:00
|
|
|
import java.io.IOException
|
2021-11-06 21:06:13 +00:00
|
|
|
import java.io.OutputStream
|
2021-07-03 20:59:46 +00:00
|
|
|
import java.net.URL
|
2021-07-04 17:00:04 +00:00
|
|
|
import java.util.*
|
2021-07-08 17:46:47 +00:00
|
|
|
|
2021-07-06 00:18:56 +00:00
|
|
|
const val DOWNLOAD_CHANNEL_ID = "cloudstream3.general"
|
|
|
|
const val DOWNLOAD_CHANNEL_NAME = "Downloads"
|
|
|
|
const val DOWNLOAD_CHANNEL_DESCRIPT = "The download notification channel"
|
2021-06-29 23:14:48 +00:00
|
|
|
|
|
|
|
object VideoDownloadManager {
|
2021-07-03 20:59:46 +00:00
|
|
|
var maxConcurrentDownloads = 3
|
2021-07-05 20:28:50 +00:00
|
|
|
private var currentDownloads = mutableListOf<Int>()
|
2021-07-03 20:59:46 +00:00
|
|
|
|
|
|
|
private const val USER_AGENT =
|
2023-08-18 22:48:00 +00:00
|
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
@get:DrawableRes
|
|
|
|
val imgDone get() = R.drawable.rddone
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
@get:DrawableRes
|
|
|
|
val imgDownloading get() = R.drawable.rdload
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
@get:DrawableRes
|
|
|
|
val imgPaused get() = R.drawable.rdpause
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
@get:DrawableRes
|
|
|
|
val imgStopped get() = R.drawable.rderror
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
@get:DrawableRes
|
|
|
|
val imgError get() = R.drawable.rderror
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
@get:DrawableRes
|
|
|
|
val pressToPauseIcon get() = R.drawable.ic_baseline_pause_24
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
@get:DrawableRes
|
|
|
|
val pressToResumeIcon get() = R.drawable.ic_baseline_play_arrow_24
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
@get:DrawableRes
|
|
|
|
val pressToStopIcon get() = R.drawable.baseline_stop_24
|
2023-07-15 21:43:09 +00:00
|
|
|
|
2021-06-29 23:14:48 +00:00
|
|
|
enum class DownloadType {
|
|
|
|
IsPaused,
|
|
|
|
IsDownloading,
|
|
|
|
IsDone,
|
|
|
|
IsFailed,
|
|
|
|
IsStopped,
|
2023-07-15 21:43:09 +00:00
|
|
|
IsPending
|
2021-06-29 23:14:48 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
enum class DownloadActionType {
|
|
|
|
Pause,
|
|
|
|
Resume,
|
|
|
|
Stop,
|
|
|
|
}
|
|
|
|
|
2021-08-22 17:14:48 +00:00
|
|
|
interface IDownloadableMinimum {
|
|
|
|
val url: String
|
|
|
|
val referer: String
|
2021-09-11 18:44:37 +00:00
|
|
|
val headers: Map<String, String>
|
2021-08-22 17:14:48 +00:00
|
|
|
}
|
|
|
|
|
2021-08-29 18:42:44 +00:00
|
|
|
fun IDownloadableMinimum.getId(): Int {
|
2021-08-22 17:14:48 +00:00
|
|
|
return url.hashCode()
|
|
|
|
}
|
|
|
|
|
2021-07-03 20:59:46 +00:00
|
|
|
data class DownloadEpisodeMetadata(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("id") val id: Int,
|
|
|
|
@JsonProperty("mainName") val mainName: String,
|
|
|
|
@JsonProperty("sourceApiName") val sourceApiName: String?,
|
|
|
|
@JsonProperty("poster") val poster: String?,
|
|
|
|
@JsonProperty("name") val name: String?,
|
|
|
|
@JsonProperty("season") val season: Int?,
|
2022-04-03 20:14:51 +00:00
|
|
|
@JsonProperty("episode") val episode: Int?,
|
|
|
|
@JsonProperty("type") val type: TvType?,
|
2021-07-03 20:59:46 +00:00
|
|
|
)
|
|
|
|
|
2021-07-04 17:00:04 +00:00
|
|
|
data class DownloadItem(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("source") val source: String?,
|
|
|
|
@JsonProperty("folder") val folder: String?,
|
|
|
|
@JsonProperty("ep") val ep: DownloadEpisodeMetadata,
|
|
|
|
@JsonProperty("links") val links: List<ExtractorLink>,
|
2021-07-04 17:00:04 +00:00
|
|
|
)
|
|
|
|
|
2021-07-05 18:09:37 +00:00
|
|
|
data class DownloadResumePackage(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("item") val item: DownloadItem,
|
|
|
|
@JsonProperty("linkIndex") val linkIndex: Int?,
|
2021-07-05 18:09:37 +00:00
|
|
|
)
|
|
|
|
|
2021-07-05 20:28:50 +00:00
|
|
|
data class DownloadedFileInfo(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("totalBytes") val totalBytes: Long,
|
|
|
|
@JsonProperty("relativePath") val relativePath: String,
|
|
|
|
@JsonProperty("displayName") val displayName: String,
|
|
|
|
@JsonProperty("extraInfo") val extraInfo: String? = null,
|
|
|
|
@JsonProperty("basePath") val basePath: String? = null // null is for legacy downloads. See getDefaultPath()
|
2021-07-05 20:28:50 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
data class DownloadedFileInfoResult(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("fileLength") val fileLength: Long,
|
|
|
|
@JsonProperty("totalBytes") val totalBytes: Long,
|
|
|
|
@JsonProperty("path") val path: Uri,
|
2021-07-05 20:28:50 +00:00
|
|
|
)
|
|
|
|
|
2021-07-08 17:46:47 +00:00
|
|
|
data class DownloadQueueResumePackage(
|
2022-02-05 22:21:45 +00:00
|
|
|
@JsonProperty("index") val index: Int,
|
|
|
|
@JsonProperty("pkg") val pkg: DownloadResumePackage,
|
2021-07-08 17:46:47 +00:00
|
|
|
)
|
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
data class DownloadStatus(
|
|
|
|
/** if you should retry with the same args and hope for a better result */
|
|
|
|
val retrySame: Boolean,
|
|
|
|
/** if you should try the next mirror */
|
|
|
|
val tryNext: Boolean,
|
|
|
|
/** if the result is what the user intended */
|
|
|
|
val success: Boolean,
|
|
|
|
)
|
|
|
|
|
|
|
|
/** Invalid input, just skip to the next one as the same args will give the same error */
|
|
|
|
private val DOWNLOAD_INVALID_INPUT =
|
|
|
|
DownloadStatus(retrySame = false, tryNext = true, success = false)
|
2021-12-24 16:09:01 +00:00
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
/** no need to try any other mirror as we have downloaded the file */
|
|
|
|
private val DOWNLOAD_SUCCESS =
|
|
|
|
DownloadStatus(retrySame = false, tryNext = false, success = true)
|
2021-12-24 16:09:01 +00:00
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
/** the user pressed stop, so no need to download anything else */
|
|
|
|
private val DOWNLOAD_STOPPED =
|
|
|
|
DownloadStatus(retrySame = false, tryNext = false, success = true)
|
2021-12-24 16:09:01 +00:00
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
/** the process failed due to some reason, so we retry and also try the next mirror */
|
|
|
|
private val DOWNLOAD_FAILED = DownloadStatus(retrySame = true, tryNext = true, success = false)
|
2021-12-24 16:09:01 +00:00
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
/** bad config, skip all mirrors as every call to download will have the same bad config */
|
|
|
|
private val DOWNLOAD_BAD_CONFIG =
|
|
|
|
DownloadStatus(retrySame = false, tryNext = false, success = false)
|
2021-07-04 17:00:04 +00:00
|
|
|
|
2021-08-29 18:42:44 +00:00
|
|
|
private const val KEY_RESUME_PACKAGES = "download_resume"
|
2021-07-08 17:46:47 +00:00
|
|
|
const val KEY_DOWNLOAD_INFO = "download_info"
|
2021-08-29 18:42:44 +00:00
|
|
|
private const val KEY_RESUME_QUEUE_PACKAGES = "download_q_resume"
|
2021-07-04 17:00:04 +00:00
|
|
|
|
2021-07-06 00:18:56 +00:00
|
|
|
val downloadStatus = HashMap<Int, DownloadType>()
|
|
|
|
val downloadStatusEvent = Event<Pair<Int, DownloadType>>()
|
2021-07-25 14:25:09 +00:00
|
|
|
val downloadDeleteEvent = Event<Int>()
|
2021-07-05 20:28:50 +00:00
|
|
|
val downloadEvent = Event<Pair<Int, DownloadActionType>>()
|
2021-07-24 20:50:57 +00:00
|
|
|
val downloadProgressEvent = Event<Triple<Int, Long, Long>>()
|
2021-07-08 19:39:49 +00:00
|
|
|
val downloadQueue = LinkedList<DownloadResumePackage>()
|
2021-07-04 17:00:04 +00:00
|
|
|
|
2021-06-29 23:14:48 +00:00
|
|
|
private var hasCreatedNotChanel = false
|
|
|
|
private fun Context.createNotificationChannel() {
|
|
|
|
hasCreatedNotChanel = true
|
|
|
|
// Create the NotificationChannel, but only on API 26+ because
|
|
|
|
// the NotificationChannel class is new and not in the support library
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
2021-07-06 00:18:56 +00:00
|
|
|
val name = DOWNLOAD_CHANNEL_NAME //getString(R.string.channel_name)
|
|
|
|
val descriptionText = DOWNLOAD_CHANNEL_DESCRIPT//getString(R.string.channel_description)
|
2021-06-29 23:14:48 +00:00
|
|
|
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
2021-07-06 00:18:56 +00:00
|
|
|
val channel = NotificationChannel(DOWNLOAD_CHANNEL_ID, name, importance).apply {
|
2021-06-29 23:14:48 +00:00
|
|
|
description = descriptionText
|
|
|
|
}
|
|
|
|
// Register the channel with the system
|
|
|
|
val notificationManager: NotificationManager =
|
|
|
|
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
|
|
notificationManager.createNotificationChannel(channel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
///** Will return IsDone if not found or error */
|
|
|
|
//fun getDownloadState(id: Int): DownloadType {
|
|
|
|
// return try {
|
|
|
|
// downloadStatus[id] ?: DownloadType.IsDone
|
|
|
|
// } catch (e: Exception) {
|
|
|
|
// logError(e)
|
|
|
|
// DownloadType.IsDone
|
|
|
|
// }
|
|
|
|
//}
|
2021-07-24 15:13:21 +00:00
|
|
|
|
2021-06-29 23:14:48 +00:00
|
|
|
private val cachedBitmaps = hashMapOf<String, Bitmap>()
|
2023-02-19 18:27:40 +00:00
|
|
|
fun Context.getImageBitmapFromUrl(url: String, headers: Map<String, String>? = null): Bitmap? {
|
2021-08-19 20:05:18 +00:00
|
|
|
try {
|
|
|
|
if (cachedBitmaps.containsKey(url)) {
|
|
|
|
return cachedBitmaps[url]
|
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-10-07 17:40:31 +00:00
|
|
|
val bitmap = GlideApp.with(this)
|
2021-08-19 20:05:18 +00:00
|
|
|
.asBitmap()
|
2023-02-19 18:27:40 +00:00
|
|
|
.load(GlideUrl(url) { headers ?: emptyMap() })
|
|
|
|
.into(720, 720)
|
2021-08-19 20:05:18 +00:00
|
|
|
.get()
|
2023-02-19 18:27:40 +00:00
|
|
|
|
2021-08-19 20:05:18 +00:00
|
|
|
if (bitmap != null) {
|
|
|
|
cachedBitmaps[url] = bitmap
|
|
|
|
}
|
2023-02-19 18:27:40 +00:00
|
|
|
return bitmap
|
2021-08-22 17:14:48 +00:00
|
|
|
} catch (e: Exception) {
|
2021-11-04 15:11:28 +00:00
|
|
|
logError(e)
|
2021-08-19 20:05:18 +00:00
|
|
|
return null
|
2021-06-29 23:14:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-25 18:04:40 +00:00
|
|
|
/**
|
|
|
|
* @param hlsProgress will together with hlsTotal display another notification if used, to lessen the confusion about estimated size.
|
|
|
|
* */
|
2021-10-14 16:13:34 +00:00
|
|
|
private suspend fun createNotification(
|
2021-06-29 23:14:48 +00:00
|
|
|
context: Context,
|
2021-07-03 20:59:46 +00:00
|
|
|
source: String?,
|
|
|
|
linkName: String?,
|
|
|
|
ep: DownloadEpisodeMetadata,
|
2021-06-29 23:14:48 +00:00
|
|
|
state: DownloadType,
|
|
|
|
progress: Long,
|
|
|
|
total: Long,
|
2021-12-25 18:04:40 +00:00
|
|
|
notificationCallback: (Int, Notification) -> Unit,
|
|
|
|
hlsProgress: Long? = null,
|
2023-08-20 01:58:31 +00:00
|
|
|
hlsTotal: Long? = null,
|
|
|
|
bytesPerSecond: Long
|
2023-08-18 22:48:00 +00:00
|
|
|
): Notification? {
|
2021-12-24 16:09:01 +00:00
|
|
|
try {
|
|
|
|
if (total <= 0) return null// crash, invalid data
|
2021-09-14 22:18:03 +00:00
|
|
|
|
|
|
|
// main { // DON'T WANT TO SLOW IT DOWN
|
2021-12-24 16:09:01 +00:00
|
|
|
val builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID)
|
|
|
|
.setAutoCancel(true)
|
|
|
|
.setColorized(true)
|
|
|
|
.setOnlyAlertOnce(true)
|
|
|
|
.setShowWhen(false)
|
|
|
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
|
|
|
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
|
|
|
|
.setContentTitle(ep.mainName)
|
|
|
|
.setSmallIcon(
|
|
|
|
when (state) {
|
|
|
|
DownloadType.IsDone -> imgDone
|
|
|
|
DownloadType.IsDownloading -> imgDownloading
|
|
|
|
DownloadType.IsPaused -> imgPaused
|
|
|
|
DownloadType.IsFailed -> imgError
|
|
|
|
DownloadType.IsStopped -> imgStopped
|
2023-07-15 21:43:09 +00:00
|
|
|
DownloadType.IsPending -> imgDownloading
|
2021-12-24 16:09:01 +00:00
|
|
|
}
|
|
|
|
)
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
if (ep.sourceApiName != null) {
|
|
|
|
builder.setSubText(ep.sourceApiName)
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2021-12-24 16:09:01 +00:00
|
|
|
|
|
|
|
if (source != null) {
|
|
|
|
val intent = Intent(context, MainActivity::class.java).apply {
|
|
|
|
data = source.toUri()
|
|
|
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
|
|
}
|
2021-12-25 18:04:40 +00:00
|
|
|
val pendingIntent: PendingIntent =
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
|
|
PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
|
|
|
|
} else {
|
|
|
|
PendingIntent.getActivity(context, 0, intent, 0)
|
|
|
|
}
|
2021-12-24 16:09:01 +00:00
|
|
|
builder.setContentIntent(pendingIntent)
|
2021-10-14 16:13:34 +00:00
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
|
|
|
|
builder.setProgress((total / 1000).toInt(), (progress / 1000).toInt(), false)
|
2023-08-22 02:00:05 +00:00
|
|
|
} else if (state == DownloadType.IsPending) {
|
2023-08-23 04:25:06 +00:00
|
|
|
builder.setProgress(0, 0, true)
|
2021-12-24 16:09:01 +00:00
|
|
|
}
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else ""
|
|
|
|
val rowTwo = if (ep.season != null && ep.episode != null) {
|
|
|
|
"${context.getString(R.string.season_short)}${ep.season}:${context.getString(R.string.episode_short)}${ep.episode}" + rowTwoExtra
|
|
|
|
} else if (ep.episode != null) {
|
|
|
|
"${context.getString(R.string.episode)} ${ep.episode}" + rowTwoExtra
|
|
|
|
} else {
|
|
|
|
(ep.name ?: "") + ""
|
|
|
|
}
|
|
|
|
val downloadFormat = context.getString(R.string.download_format)
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
|
|
if (ep.poster != null) {
|
|
|
|
val poster = withContext(Dispatchers.IO) {
|
|
|
|
context.getImageBitmapFromUrl(ep.poster)
|
|
|
|
}
|
|
|
|
if (poster != null)
|
|
|
|
builder.setLargeIcon(poster)
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2021-12-25 18:04:40 +00:00
|
|
|
val progressPercentage: Long
|
|
|
|
val progressMbString: String
|
|
|
|
val totalMbString: String
|
|
|
|
val suffix: String
|
|
|
|
|
2023-08-20 01:58:31 +00:00
|
|
|
val mbFormat = "%.1f MB"
|
|
|
|
|
2021-12-25 18:04:40 +00:00
|
|
|
if (hlsProgress != null && hlsTotal != null) {
|
|
|
|
progressPercentage = hlsProgress.toLong() * 100 / hlsTotal
|
|
|
|
progressMbString = hlsProgress.toString()
|
|
|
|
totalMbString = hlsTotal.toString()
|
2023-08-20 01:58:31 +00:00
|
|
|
suffix = " - $mbFormat".format(progress / 1000000f)
|
2021-12-25 18:04:40 +00:00
|
|
|
} else {
|
|
|
|
progressPercentage = progress * 100 / total
|
2023-08-20 01:58:31 +00:00
|
|
|
progressMbString = mbFormat.format(progress / 1000000f)
|
|
|
|
totalMbString = mbFormat.format(total / 1000000f)
|
2021-12-25 18:04:40 +00:00
|
|
|
suffix = ""
|
|
|
|
}
|
|
|
|
|
2023-08-20 01:58:31 +00:00
|
|
|
val mbPerSecondString =
|
|
|
|
if (state == DownloadType.IsDownloading) {
|
|
|
|
" ($mbFormat/s)".format(bytesPerSecond.toFloat() / 1000000f)
|
|
|
|
} else ""
|
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
val bigText =
|
2023-08-18 22:48:00 +00:00
|
|
|
when (state) {
|
|
|
|
DownloadType.IsDownloading, DownloadType.IsPaused -> {
|
2023-08-20 01:58:31 +00:00
|
|
|
(if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString/$totalMbString)$suffix$mbPerSecondString"
|
2023-08-18 22:48:00 +00:00
|
|
|
}
|
|
|
|
|
2023-08-22 02:00:05 +00:00
|
|
|
DownloadType.IsPending -> {
|
|
|
|
(if (linkName == null) "" else "$linkName\n") + rowTwo
|
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
DownloadType.IsFailed -> {
|
|
|
|
downloadFormat.format(
|
|
|
|
context.getString(R.string.download_failed),
|
|
|
|
rowTwo
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
DownloadType.IsDone -> {
|
|
|
|
downloadFormat.format(context.getString(R.string.download_done), rowTwo)
|
|
|
|
}
|
|
|
|
|
2023-08-22 02:00:05 +00:00
|
|
|
DownloadType.IsStopped -> {
|
2023-08-18 22:48:00 +00:00
|
|
|
downloadFormat.format(
|
|
|
|
context.getString(R.string.download_canceled),
|
|
|
|
rowTwo
|
|
|
|
)
|
|
|
|
}
|
2021-12-24 16:09:01 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
val bodyStyle = NotificationCompat.BigTextStyle()
|
|
|
|
bodyStyle.bigText(bigText)
|
|
|
|
builder.setStyle(bodyStyle)
|
|
|
|
} else {
|
2021-12-25 18:04:40 +00:00
|
|
|
val txt =
|
2023-08-18 22:48:00 +00:00
|
|
|
when (state) {
|
2023-08-22 02:00:05 +00:00
|
|
|
DownloadType.IsDownloading, DownloadType.IsPaused, DownloadType.IsPending -> {
|
2023-08-18 22:48:00 +00:00
|
|
|
rowTwo
|
|
|
|
}
|
|
|
|
|
|
|
|
DownloadType.IsFailed -> {
|
|
|
|
downloadFormat.format(
|
|
|
|
context.getString(R.string.download_failed),
|
|
|
|
rowTwo
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
DownloadType.IsDone -> {
|
|
|
|
downloadFormat.format(context.getString(R.string.download_done), rowTwo)
|
|
|
|
}
|
|
|
|
|
2023-08-22 02:00:05 +00:00
|
|
|
DownloadType.IsStopped -> {
|
2023-08-18 22:48:00 +00:00
|
|
|
downloadFormat.format(
|
|
|
|
context.getString(R.string.download_canceled),
|
|
|
|
rowTwo
|
|
|
|
)
|
|
|
|
}
|
2021-12-25 18:04:40 +00:00
|
|
|
}
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
builder.setContentText(txt)
|
2021-07-03 20:59:46 +00:00
|
|
|
}
|
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
if ((state == DownloadType.IsDownloading || state == DownloadType.IsPaused) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
|
|
val actionTypes: MutableList<DownloadActionType> = ArrayList()
|
|
|
|
// INIT
|
|
|
|
if (state == DownloadType.IsDownloading) {
|
|
|
|
actionTypes.add(DownloadActionType.Pause)
|
|
|
|
actionTypes.add(DownloadActionType.Stop)
|
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
if (state == DownloadType.IsPaused) {
|
|
|
|
actionTypes.add(DownloadActionType.Resume)
|
|
|
|
actionTypes.add(DownloadActionType.Stop)
|
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
// ADD ACTIONS
|
|
|
|
for ((index, i) in actionTypes.withIndex()) {
|
|
|
|
val actionResultIntent = Intent(context, VideoDownloadService::class.java)
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
actionResultIntent.putExtra(
|
|
|
|
"type", when (i) {
|
|
|
|
DownloadActionType.Resume -> "resume"
|
|
|
|
DownloadActionType.Pause -> "pause"
|
|
|
|
DownloadActionType.Stop -> "stop"
|
|
|
|
}
|
|
|
|
)
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
actionResultIntent.putExtra("id", ep.id)
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
val pending: PendingIntent = PendingIntent.getService(
|
|
|
|
// BECAUSE episodes lying near will have the same id +1, index will give the same requested as the previous episode, *100000 fixes this
|
|
|
|
context, (4337 + index * 1000000 + ep.id),
|
|
|
|
actionResultIntent,
|
|
|
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
|
|
|
)
|
2021-09-14 22:18:03 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
builder.addAction(
|
|
|
|
NotificationCompat.Action(
|
|
|
|
when (i) {
|
|
|
|
DownloadActionType.Resume -> pressToResumeIcon
|
|
|
|
DownloadActionType.Pause -> pressToPauseIcon
|
|
|
|
DownloadActionType.Stop -> pressToStopIcon
|
|
|
|
}, when (i) {
|
|
|
|
DownloadActionType.Resume -> context.getString(R.string.resume)
|
|
|
|
DownloadActionType.Pause -> context.getString(R.string.pause)
|
|
|
|
DownloadActionType.Stop -> context.getString(R.string.cancel)
|
|
|
|
}, pending
|
|
|
|
)
|
2021-06-29 23:14:48 +00:00
|
|
|
)
|
2021-12-24 16:09:01 +00:00
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
}
|
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
if (!hasCreatedNotChanel) {
|
|
|
|
context.createNotificationChannel()
|
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
|
2021-12-24 16:09:01 +00:00
|
|
|
val notification = builder.build()
|
|
|
|
notificationCallback(ep.id, notification)
|
|
|
|
with(NotificationManagerCompat.from(context)) {
|
|
|
|
// notificationId is a unique int for each notification that you must define
|
|
|
|
notify(ep.id, notification)
|
|
|
|
}
|
|
|
|
return notification
|
|
|
|
} catch (e: Exception) {
|
|
|
|
logError(e)
|
|
|
|
return null
|
2021-06-29 23:14:48 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-04 17:00:04 +00:00
|
|
|
private const val reservedChars = "|\\?*<\":>+[]/\'"
|
2023-02-19 18:27:40 +00:00
|
|
|
fun sanitizeFilename(name: String, removeSpaces: Boolean = false): String {
|
2021-07-04 00:59:51 +00:00
|
|
|
var tempName = name
|
|
|
|
for (c in reservedChars) {
|
|
|
|
tempName = tempName.replace(c, ' ')
|
|
|
|
}
|
2022-08-07 16:01:32 +00:00
|
|
|
if (removeSpaces) tempName = tempName.replace(" ", "")
|
2021-07-04 17:00:04 +00:00
|
|
|
return tempName.replace(" ", " ").trim(' ')
|
|
|
|
}
|
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
/**
|
|
|
|
* Used for getting video player subs.
|
|
|
|
* @return List of pairs for the files in this format: <Name, Uri>
|
|
|
|
* */
|
2021-12-03 22:48:30 +00:00
|
|
|
fun getFolder(
|
|
|
|
context: Context,
|
|
|
|
relativePath: String,
|
|
|
|
basePath: String?
|
|
|
|
): List<Pair<String, Uri>>? {
|
2021-11-06 21:06:13 +00:00
|
|
|
val base = basePathToFile(context, basePath)
|
2023-08-23 04:25:06 +00:00
|
|
|
val folder = base?.gotoDirectory(relativePath, false) ?: return null
|
|
|
|
if (folder.isDirectory() != false) return null
|
2021-11-01 15:33:46 +00:00
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
return folder.listFiles()
|
|
|
|
?.mapNotNull { (it.name() ?: "") to (it.uri() ?: return@mapNotNull null) }
|
2021-08-22 17:14:48 +00:00
|
|
|
}
|
|
|
|
|
2021-07-05 20:28:50 +00:00
|
|
|
|
2021-08-22 17:14:48 +00:00
|
|
|
data class CreateNotificationMetadata(
|
|
|
|
val type: DownloadType,
|
|
|
|
val bytesDownloaded: Long,
|
|
|
|
val bytesTotal: Long,
|
2021-12-25 18:04:40 +00:00
|
|
|
val hlsProgress: Long? = null,
|
|
|
|
val hlsTotal: Long? = null,
|
2023-08-20 01:58:31 +00:00
|
|
|
val bytesPerSecond: Long
|
2021-08-22 17:14:48 +00:00
|
|
|
)
|
|
|
|
|
2021-09-01 21:30:21 +00:00
|
|
|
data class StreamData(
|
2023-08-22 02:00:05 +00:00
|
|
|
private val fileLength: Long,
|
2023-08-23 04:25:06 +00:00
|
|
|
val file: SafeFile,
|
2023-08-22 02:00:05 +00:00
|
|
|
//val fileStream: OutputStream,
|
|
|
|
) {
|
2023-08-23 04:25:06 +00:00
|
|
|
@Throws(IOException::class)
|
|
|
|
fun open(): OutputStream {
|
|
|
|
return file.openOutputStreamOrThrow(resume)
|
2023-08-22 02:00:05 +00:00
|
|
|
}
|
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
@Throws(IOException::class)
|
|
|
|
fun openNew(): OutputStream {
|
|
|
|
return file.openOutputStreamOrThrow(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun delete(): Boolean {
|
|
|
|
return file.delete()
|
2023-08-22 02:00:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
val resume: Boolean get() = fileLength > 0L
|
|
|
|
val startAt: Long get() = if (resume) fileLength else 0L
|
2023-08-23 04:25:06 +00:00
|
|
|
val exists: Boolean get() = file.exists() == true
|
2023-08-22 02:00:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
@Throws(IOException::class)
|
|
|
|
fun setupStream(
|
|
|
|
context: Context,
|
|
|
|
name: String,
|
|
|
|
folder: String?,
|
|
|
|
extension: String,
|
|
|
|
tryResume: Boolean,
|
|
|
|
): StreamData {
|
|
|
|
val (base, _) = context.getBasePath()
|
|
|
|
return setupStream(
|
|
|
|
base ?: throw IOException("Bad config"),
|
|
|
|
name,
|
|
|
|
folder,
|
|
|
|
extension,
|
|
|
|
tryResume
|
|
|
|
)
|
2023-08-22 02:00:05 +00:00
|
|
|
}
|
2021-09-01 21:30:21 +00:00
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
/**
|
|
|
|
* Sets up the appropriate file and creates a data stream from the file.
|
|
|
|
* Used for initializing downloads.
|
|
|
|
* */
|
2023-08-22 02:00:05 +00:00
|
|
|
@Throws(IOException::class)
|
2022-04-10 22:00:03 +00:00
|
|
|
fun setupStream(
|
2023-08-23 04:25:06 +00:00
|
|
|
baseFile: SafeFile,
|
2021-08-30 17:11:04 +00:00
|
|
|
name: String,
|
|
|
|
folder: String?,
|
|
|
|
extension: String,
|
2021-09-01 21:30:21 +00:00
|
|
|
tryResume: Boolean,
|
|
|
|
): StreamData {
|
|
|
|
val displayName = getDisplayName(name, extension)
|
2021-08-30 17:11:04 +00:00
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
val subDir = baseFile.gotoDirectoryOrThrow(folder)
|
2023-08-22 02:00:05 +00:00
|
|
|
val foundFile = subDir.findFile(displayName)
|
2021-08-30 17:11:04 +00:00
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
val (file, fileLength) = if (foundFile == null || foundFile.exists() != true) {
|
2023-08-22 02:00:05 +00:00
|
|
|
subDir.createFileOrThrow(displayName) to 0L
|
2021-08-30 17:11:04 +00:00
|
|
|
} else {
|
2023-08-22 02:00:05 +00:00
|
|
|
if (tryResume) {
|
2023-08-23 04:25:06 +00:00
|
|
|
foundFile to foundFile.lengthOrThrow()
|
2021-08-30 17:11:04 +00:00
|
|
|
} else {
|
2023-08-22 02:00:05 +00:00
|
|
|
foundFile.deleteOrThrow()
|
|
|
|
subDir.createFileOrThrow(displayName) to 0L
|
2021-08-30 17:11:04 +00:00
|
|
|
}
|
|
|
|
}
|
2023-08-22 02:00:05 +00:00
|
|
|
|
|
|
|
return StreamData(fileLength, file)
|
2021-09-01 21:30:21 +00:00
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
/** This class handles the notifications, as well as the relevant key */
|
|
|
|
data class DownloadMetaData(
|
|
|
|
private val id: Int?,
|
|
|
|
var bytesDownloaded: Long = 0,
|
2023-08-19 19:37:14 +00:00
|
|
|
var bytesWritten: Long = 0,
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
var totalBytes: Long? = null,
|
|
|
|
|
|
|
|
// notification metadata
|
|
|
|
private var lastUpdatedMs: Long = 0,
|
2023-08-20 01:58:31 +00:00
|
|
|
private var lastDownloadedBytes: Long = 0,
|
2023-08-18 22:48:00 +00:00
|
|
|
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
|
2023-08-19 02:46:47 +00:00
|
|
|
var hlsWrittenProgress: Int = 0,
|
2023-08-18 22:48:00 +00:00
|
|
|
|
|
|
|
// this is used for copy with metadata on how much we have downloaded for setKey
|
|
|
|
private var downloadFileInfoTemplate: DownloadedFileInfo? = null
|
|
|
|
) : Closeable {
|
2023-08-20 01:58:31 +00:00
|
|
|
fun setResumeLength(length: Long) {
|
|
|
|
bytesDownloaded = length
|
|
|
|
bytesWritten = length
|
|
|
|
lastDownloadedBytes = length
|
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
val approxTotalBytes: Long
|
|
|
|
get() = totalBytes ?: hlsTotal?.let { total ->
|
|
|
|
(bytesDownloaded * (total / hlsProgress.toFloat())).toLong()
|
2023-08-19 19:37:14 +00:00
|
|
|
} ?: bytesDownloaded
|
2023-08-18 22:48:00 +00:00
|
|
|
|
|
|
|
private val isHLS get() = hlsTotal != null
|
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
private var stopListener: (() -> Unit)? = null
|
|
|
|
|
|
|
|
/** on cancel button pressed or failed invoke this once and only once */
|
|
|
|
fun setOnStop(callback: (() -> Unit)) {
|
|
|
|
stopListener = callback
|
|
|
|
}
|
|
|
|
|
|
|
|
fun removeStopListener() {
|
|
|
|
stopListener = null
|
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
private val downloadEventListener = { event: Pair<Int, DownloadActionType> ->
|
|
|
|
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()
|
2023-08-19 19:37:14 +00:00
|
|
|
stopListener?.invoke()
|
|
|
|
stopListener = null
|
2023-08-18 22:48:00 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
DownloadActionType.Resume -> {
|
|
|
|
type = DownloadType.IsDownloading
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-08-30 17:11:04 +00:00
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
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
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2021-11-01 15:33:46 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
fun setDownloadFileInfoTemplate(template: DownloadedFileInfo) {
|
|
|
|
downloadFileInfoTemplate = template
|
|
|
|
updateFileInfo()
|
|
|
|
}
|
2021-07-05 00:55:07 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
init {
|
|
|
|
if (id != null) {
|
|
|
|
downloadEvent += downloadEventListener
|
|
|
|
}
|
2021-07-03 20:59:46 +00:00
|
|
|
}
|
2021-07-04 00:59:51 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
override fun close() {
|
|
|
|
// as we may need to resume hls downloads, we save the current written index
|
2023-08-19 19:37:14 +00:00
|
|
|
if (isHLS || totalBytes == null) {
|
2023-08-18 22:48:00 +00:00
|
|
|
updateFileInfo()
|
|
|
|
}
|
|
|
|
if (id != null) {
|
|
|
|
downloadEvent -= downloadEventListener
|
|
|
|
downloadStatus -= id
|
|
|
|
}
|
2023-08-19 19:37:14 +00:00
|
|
|
stopListener = null
|
2023-08-18 22:48:00 +00:00
|
|
|
}
|
2021-07-04 00:59:51 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
var type
|
|
|
|
get() = internalType
|
|
|
|
set(value) {
|
|
|
|
internalType = value
|
|
|
|
notify()
|
|
|
|
}
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2023-08-19 02:46:47 +00:00
|
|
|
fun onDelete() {
|
|
|
|
bytesDownloaded = 0
|
|
|
|
hlsWrittenProgress = 0
|
|
|
|
hlsProgress = 0
|
2023-08-19 15:03:27 +00:00
|
|
|
if (id != null)
|
|
|
|
downloadDeleteEvent(id)
|
2023-08-19 02:46:47 +00:00
|
|
|
|
|
|
|
//internalType = DownloadType.IsStopped
|
|
|
|
notify()
|
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
companion object {
|
|
|
|
const val UPDATE_RATE_MS: Long = 1000L
|
|
|
|
}
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
@JvmName("DownloadMetaDataNotify")
|
|
|
|
private fun notify() {
|
2023-08-20 01:58:31 +00:00
|
|
|
// max 10 sec between notifications, min 0.1s, this is to stop div by zero
|
|
|
|
val dt = (System.currentTimeMillis() - lastUpdatedMs).coerceIn(100, 10000)
|
|
|
|
|
|
|
|
val bytesPerSecond =
|
|
|
|
((bytesDownloaded - lastDownloadedBytes) * 1000L) / dt
|
|
|
|
|
|
|
|
lastDownloadedBytes = bytesDownloaded
|
2023-08-18 22:48:00 +00:00
|
|
|
lastUpdatedMs = System.currentTimeMillis()
|
|
|
|
try {
|
|
|
|
val bytes = approxTotalBytes
|
2021-08-14 17:31:27 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
// notification creation
|
|
|
|
if (isHLS) {
|
|
|
|
createNotificationCallback(
|
|
|
|
CreateNotificationMetadata(
|
|
|
|
internalType,
|
|
|
|
bytesDownloaded,
|
|
|
|
bytes,
|
|
|
|
hlsTotal = hlsTotal?.toLong(),
|
2023-08-20 01:58:31 +00:00
|
|
|
hlsProgress = hlsProgress.toLong(),
|
|
|
|
bytesPerSecond = bytesPerSecond
|
2023-08-18 22:48:00 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
createNotificationCallback(
|
|
|
|
CreateNotificationMetadata(
|
|
|
|
internalType,
|
|
|
|
bytesDownloaded,
|
|
|
|
bytes,
|
2023-08-20 01:58:31 +00:00
|
|
|
bytesPerSecond = bytesPerSecond
|
2023-08-18 22:48:00 +00:00
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2021-08-29 19:46:25 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
// as hls has an approx file size we want to update this metadata
|
|
|
|
if (isHLS) {
|
|
|
|
updateFileInfo()
|
|
|
|
}
|
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
if (internalType == DownloadType.IsStopped || internalType == DownloadType.IsFailed) {
|
|
|
|
stopListener?.invoke()
|
|
|
|
stopListener = null
|
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
// 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)
|
2023-08-19 02:46:47 +00:00
|
|
|
if (BuildConfig.DEBUG) {
|
|
|
|
throw t
|
|
|
|
}
|
2023-08-18 22:48:00 +00:00
|
|
|
}
|
2021-09-11 18:44:37 +00:00
|
|
|
}
|
2021-08-14 17:31:27 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
private fun checkNotification() {
|
|
|
|
if (lastUpdatedMs + UPDATE_RATE_MS > System.currentTimeMillis()) return
|
|
|
|
notify()
|
|
|
|
}
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2021-08-11 17:40:23 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
/** 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()
|
2021-08-11 17:40:23 +00:00
|
|
|
}
|
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
fun addBytesWritten(length: Long) {
|
|
|
|
bytesWritten += length
|
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
/** adds the length + hsl progress and pushes a notification if necessary */
|
|
|
|
fun addSegment(length: Long) {
|
|
|
|
hlsProgress += 1
|
|
|
|
addBytes(length)
|
|
|
|
}
|
2021-07-04 00:59:51 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
fun setWrittenSegment(segmentIndex: Int) {
|
2023-08-19 02:46:47 +00:00
|
|
|
hlsWrittenProgress = segmentIndex + 1
|
2023-08-22 02:00:05 +00:00
|
|
|
// in case of abort we need to save every written progress
|
|
|
|
updateFileInfo()
|
2021-08-22 17:14:48 +00:00
|
|
|
}
|
2023-08-18 22:48:00 +00:00
|
|
|
}
|
2021-07-05 20:28:50 +00:00
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
/** bytes have the size end-start where the byte range is [start,end)
|
|
|
|
* note that ByteArray is a pointer and therefore cant be stored without cloning it */
|
|
|
|
data class LazyStreamDownloadResponse(
|
|
|
|
val bytes: ByteArray,
|
|
|
|
val startByte: Long,
|
|
|
|
val endByte: Long,
|
|
|
|
) {
|
|
|
|
val size get() = endByte - startByte
|
|
|
|
|
|
|
|
override fun toString(): String {
|
|
|
|
return "$startByte->$endByte"
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun equals(other: Any?): Boolean {
|
|
|
|
if (other !is LazyStreamDownloadResponse) return false
|
|
|
|
return other.startByte == startByte && other.endByte == endByte
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun hashCode(): Int {
|
|
|
|
return Objects.hash(startByte, endByte)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
data class LazyStreamDownloadData(
|
|
|
|
private val url: String,
|
|
|
|
private val headers: Map<String, String>,
|
|
|
|
private val referer: String,
|
|
|
|
/** This specifies where chunck i starts and ends,
|
|
|
|
* bytes=${chuckStartByte[ i ]}-${chuckStartByte[ i+1 ] -1}
|
|
|
|
* where out of bounds => bytes=${chuckStartByte[ i ]}- */
|
|
|
|
private val chuckStartByte: LongArray,
|
|
|
|
val totalLength: Long?,
|
|
|
|
val downloadLength: Long?,
|
|
|
|
val chuckSize: Long,
|
|
|
|
val bufferSize: Int,
|
|
|
|
) {
|
|
|
|
val size get() = chuckStartByte.size
|
|
|
|
|
|
|
|
/** returns what byte it has downloaded,
|
|
|
|
* so start at 10 and download 4 bytes = return 14
|
|
|
|
*
|
|
|
|
* the range is [startByte, endByte) to be able to do [a, b) [b, c) ect
|
|
|
|
*
|
|
|
|
* [a, null) will return inclusive to eof = [a, eof]
|
|
|
|
*
|
|
|
|
* throws an error if initial get request fails, can be specified as return startByte
|
|
|
|
* */
|
|
|
|
@Throws
|
|
|
|
private suspend fun resolve(
|
|
|
|
startByte: Long,
|
|
|
|
endByte: Long?,
|
|
|
|
callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit)
|
|
|
|
): Long = withContext(Dispatchers.IO) {
|
|
|
|
var currentByte: Long = startByte
|
|
|
|
val stopAt = endByte ?: Long.MAX_VALUE
|
|
|
|
if (currentByte >= stopAt) return@withContext currentByte
|
|
|
|
|
|
|
|
val request = app.get(
|
|
|
|
url,
|
|
|
|
headers = headers + mapOf(
|
|
|
|
// range header is inclusive so [startByte, endByte-1] = [startByte, endByte)
|
|
|
|
// if nothing at end the server will continue until eof
|
|
|
|
"Range" to "bytes=$startByte-" // ${endByte?.minus(1)?.toString() ?: "" }
|
|
|
|
),
|
|
|
|
referer = referer,
|
|
|
|
verify = false
|
|
|
|
)
|
|
|
|
val requestStream = request.body.byteStream()
|
|
|
|
|
|
|
|
val buffer = ByteArray(bufferSize)
|
|
|
|
var read: Int
|
|
|
|
|
|
|
|
try {
|
|
|
|
while (requestStream.read(buffer, 0, bufferSize).also { read = it } >= 0) {
|
|
|
|
val start = currentByte
|
|
|
|
currentByte += read.toLong()
|
|
|
|
|
|
|
|
// this stops overflow
|
|
|
|
if (currentByte >= stopAt) {
|
|
|
|
callback(LazyStreamDownloadResponse(buffer, start, stopAt))
|
|
|
|
break
|
|
|
|
} else {
|
|
|
|
callback(LazyStreamDownloadResponse(buffer, start, currentByte))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (e: CancellationException) {
|
|
|
|
throw e
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
logError(t)
|
|
|
|
} finally {
|
|
|
|
requestStream.closeQuietly()
|
|
|
|
}
|
|
|
|
|
|
|
|
return@withContext currentByte
|
|
|
|
}
|
|
|
|
|
|
|
|
/** retries the resolve n times and returns true if successful */
|
|
|
|
suspend fun resolveSafe(
|
|
|
|
index: Int,
|
|
|
|
retries: Int = 3,
|
|
|
|
callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit)
|
|
|
|
): Boolean {
|
|
|
|
var start = chuckStartByte.getOrNull(index) ?: return false
|
|
|
|
val end = chuckStartByte.getOrNull(index + 1)
|
|
|
|
|
|
|
|
for (i in 0 until retries) {
|
|
|
|
try {
|
|
|
|
// in case
|
|
|
|
start = resolve(start, end, callback)
|
|
|
|
// no end defined, so we don't care exactly where it ended
|
|
|
|
if (end == null) return true
|
|
|
|
// we have download more or exactly what we needed
|
|
|
|
if (start >= end) return true
|
|
|
|
} catch (e: IllegalStateException) {
|
|
|
|
return false
|
|
|
|
} catch (e: CancellationException) {
|
|
|
|
return false
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
@Throws
|
|
|
|
suspend fun streamLazy(
|
|
|
|
url: String,
|
|
|
|
headers: Map<String, String>,
|
|
|
|
referer: String,
|
|
|
|
startByte: Long,
|
|
|
|
/** how many bytes every connection should be, by default it is 10 MiB */
|
|
|
|
chuckSize: Long = (1 shl 20) * 10,
|
|
|
|
/** maximum bytes in the buffer that responds */
|
|
|
|
bufferSize: Int = DEFAULT_BUFFER_SIZE
|
|
|
|
): LazyStreamDownloadData {
|
|
|
|
// we don't want to make a separate connection for every 1kb
|
|
|
|
require(chuckSize > 1000)
|
|
|
|
|
2023-08-20 01:58:31 +00:00
|
|
|
var contentLength =
|
2023-08-19 19:37:14 +00:00
|
|
|
app.head(url = url, headers = headers, referer = referer, verify = false).size
|
2023-08-20 01:58:31 +00:00
|
|
|
if (contentLength != null && contentLength <= 0) contentLength = null
|
2023-08-19 19:37:14 +00:00
|
|
|
|
|
|
|
var downloadLength: Long? = null
|
|
|
|
var totalLength: Long? = null
|
|
|
|
|
|
|
|
val ranges = if (contentLength == null) {
|
2023-08-20 01:58:31 +00:00
|
|
|
// is the equivalent of [startByte..EOF] as we don't know the size we can only do one
|
|
|
|
// connection
|
2023-08-19 19:37:14 +00:00
|
|
|
LongArray(1) { startByte }
|
|
|
|
} else {
|
|
|
|
downloadLength = contentLength - startByte
|
|
|
|
totalLength = contentLength
|
2023-08-20 01:58:31 +00:00
|
|
|
// div with ceiling as
|
|
|
|
// this makes the last part "unknown ending" and it will break at EOF
|
|
|
|
// so eg startByte = 0, downloadLength = 13, chuckSize = 10
|
|
|
|
// = LongArray(2) { 0, 10 } = [0,10) + [10..EOF]
|
|
|
|
LongArray(((downloadLength + chuckSize - 1) / chuckSize).toInt()) { idx ->
|
2023-08-19 19:37:14 +00:00
|
|
|
startByte + idx * chuckSize
|
|
|
|
}
|
|
|
|
}
|
2023-08-20 01:58:31 +00:00
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
return LazyStreamDownloadData(
|
|
|
|
url = url,
|
|
|
|
headers = headers,
|
|
|
|
referer = referer,
|
|
|
|
chuckStartByte = ranges,
|
|
|
|
downloadLength = downloadLength,
|
|
|
|
totalLength = totalLength,
|
|
|
|
chuckSize = chuckSize,
|
|
|
|
bufferSize = bufferSize
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-08-19 23:29:50 +00:00
|
|
|
/** Helper function to make sure duplicate attributes don't get overriden or inserted without lowercase cmp
|
|
|
|
* example: map("a" to 1) appendAndDontOverride map("A" to 2, "a" to 3, "c" to 4) = map("a" to 1, "c" to 4)
|
|
|
|
* */
|
|
|
|
private fun <V> Map<String, V>.appendAndDontOverride(rhs: Map<String, V>): Map<String, V> {
|
|
|
|
val out = this.toMutableMap()
|
|
|
|
val current = this.keys.map { it.lowercase() }
|
|
|
|
for ((key, value) in rhs) {
|
|
|
|
if (current.contains(key.lowercase())) continue
|
|
|
|
out[key] = value
|
|
|
|
}
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun List<Job>.cancel() {
|
|
|
|
forEach { job ->
|
|
|
|
try {
|
|
|
|
job.cancel()
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
logError(t)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private suspend fun List<Job>.join() {
|
|
|
|
forEach { job ->
|
|
|
|
try {
|
|
|
|
job.join()
|
|
|
|
} catch (t: Throwable) {
|
|
|
|
logError(t)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
suspend fun downloadThing(
|
|
|
|
context: Context,
|
|
|
|
link: IDownloadableMinimum,
|
|
|
|
name: String,
|
2023-08-23 04:25:06 +00:00
|
|
|
folder: String,
|
2023-08-18 22:48:00 +00:00
|
|
|
extension: String,
|
|
|
|
tryResume: Boolean,
|
|
|
|
parentId: Int?,
|
|
|
|
createNotificationCallback: (CreateNotificationMetadata) -> Unit,
|
2023-08-19 19:37:14 +00:00
|
|
|
parallelConnections: Int = 3
|
2023-08-23 04:25:06 +00:00
|
|
|
): DownloadStatus = withContext(Dispatchers.IO) {
|
2023-08-18 22:48:00 +00:00
|
|
|
// we cant download torrents with this implementation, aria2c might be used in the future
|
2023-08-23 04:25:06 +00:00
|
|
|
if (link.url.startsWith("magnet") || link.url.endsWith(".torrent") || parallelConnections < 1) {
|
|
|
|
return@withContext DOWNLOAD_INVALID_INPUT
|
2023-08-18 22:48:00 +00:00
|
|
|
}
|
2021-07-06 00:18:56 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
var fileStream: OutputStream? = null
|
2023-08-19 19:37:14 +00:00
|
|
|
//var requestStream: InputStream? = null
|
2023-08-18 22:48:00 +00:00
|
|
|
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)
|
2023-08-23 04:25:06 +00:00
|
|
|
if (baseFile == null) return@withContext DOWNLOAD_BAD_CONFIG
|
2023-08-18 22:48:00 +00:00
|
|
|
|
|
|
|
// set up the download file
|
2023-08-23 04:25:06 +00:00
|
|
|
val stream = setupStream(baseFile, name, folder, extension, tryResume)
|
2023-08-22 02:00:05 +00:00
|
|
|
|
|
|
|
fileStream = stream.open()
|
|
|
|
|
|
|
|
metadata.setResumeLength(stream.startAt)
|
2023-08-18 22:48:00 +00:00
|
|
|
metadata.type = DownloadType.IsPending
|
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
val items = streamLazy(
|
|
|
|
url = link.url.replace(" ", "%20"),
|
|
|
|
referer = link.referer,
|
2023-08-22 02:00:05 +00:00
|
|
|
startByte = stream.startAt,
|
2023-08-19 02:46:47 +00:00
|
|
|
headers = link.headers.appendAndDontOverride(
|
|
|
|
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",
|
2023-08-19 19:37:14 +00:00
|
|
|
)
|
|
|
|
)
|
2023-08-18 22:48:00 +00:00
|
|
|
)
|
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
metadata.totalBytes = items.totalLength
|
|
|
|
metadata.type = DownloadType.IsDownloading
|
2023-08-18 22:48:00 +00:00
|
|
|
metadata.setDownloadFileInfoTemplate(
|
|
|
|
DownloadedFileInfo(
|
|
|
|
totalBytes = metadata.approxTotalBytes,
|
2023-08-23 04:25:06 +00:00
|
|
|
relativePath = folder,
|
2023-08-18 22:48:00 +00:00
|
|
|
displayName = displayName,
|
|
|
|
basePath = basePath
|
2021-12-03 22:48:30 +00:00
|
|
|
)
|
|
|
|
)
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
val currentMutex = Mutex()
|
|
|
|
val current = (0 until items.size).iterator()
|
|
|
|
|
|
|
|
val fileMutex = Mutex()
|
|
|
|
// start to data
|
|
|
|
val pendingData: HashMap<Long, LazyStreamDownloadResponse> =
|
|
|
|
hashMapOf()
|
|
|
|
|
2023-08-22 02:00:05 +00:00
|
|
|
val fileChecker = launch(Dispatchers.IO) {
|
|
|
|
while (isActive) {
|
|
|
|
if (stream.exists) {
|
|
|
|
delay(5000)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
fileMutex.withLock {
|
|
|
|
metadata.type = DownloadType.IsStopped
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
val jobs = (0 until parallelConnections).map {
|
2023-08-19 23:29:50 +00:00
|
|
|
launch(Dispatchers.IO) {
|
2023-08-19 19:37:14 +00:00
|
|
|
|
2023-08-19 23:29:50 +00:00
|
|
|
// @downloadexplanation
|
2023-08-19 19:37:14 +00:00
|
|
|
// this may seem a bit complex but it more or less acts as a queue system
|
|
|
|
// imagine we do the downloading [0,3] and it response in the order 0,2,3,1
|
|
|
|
// file: [_,_,_,_] queue: [_,_,_,_] Initial condition
|
|
|
|
// file: [X,_,_,_] queue: [_,_,_,_] + added 0 directly to file
|
|
|
|
// file: [X,_,_,_] queue: [_,_,X,_] + added 2 to queue
|
|
|
|
// file: [X,_,_,_] queue: [_,_,X,X] + added 3 to queue
|
|
|
|
// file: [X,X,_,_] queue: [_,_,X,X] + added 1 directly to file
|
|
|
|
// file: [X,X,X,X] queue: [_,_,_,_] write the queue and remove from it
|
|
|
|
|
2023-08-19 23:29:50 +00:00
|
|
|
// note that this is a bit more complex compared to hsl as ever segment
|
|
|
|
// will return several bytearrays, and is therefore chained by the byte
|
|
|
|
// so every request has a front and back byte instead of an index
|
|
|
|
// this *requires* that no gap exist due because of resolve
|
2023-08-19 19:37:14 +00:00
|
|
|
val callback: (suspend CoroutineScope.(LazyStreamDownloadResponse) -> Unit) =
|
|
|
|
callback@{ response ->
|
|
|
|
if (!isActive) return@callback
|
|
|
|
fileMutex.withLock {
|
|
|
|
// wait until not paused
|
|
|
|
while (metadata.type == DownloadType.IsPaused) delay(100)
|
|
|
|
// if stopped then throw
|
|
|
|
if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed) {
|
|
|
|
this.cancel()
|
|
|
|
return@callback
|
|
|
|
}
|
|
|
|
|
|
|
|
val responseSize = response.size
|
|
|
|
metadata.addBytes(response.size)
|
|
|
|
|
|
|
|
if (response.startByte == metadata.bytesWritten) {
|
|
|
|
// if we are first in the queue then write it directly
|
|
|
|
fileStream.write(
|
|
|
|
response.bytes,
|
|
|
|
0,
|
|
|
|
responseSize.toInt()
|
|
|
|
)
|
|
|
|
metadata.addBytesWritten(responseSize)
|
|
|
|
} else {
|
|
|
|
// otherwise append to queue, we need to clone the bytes as they will be overridden otherwise
|
|
|
|
pendingData[response.startByte] =
|
|
|
|
response.copy(bytes = response.bytes.clone())
|
|
|
|
}
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
// remove the current queue start, so no possibility of
|
|
|
|
// while(true) { continue } in case size = 0, and removed extra
|
|
|
|
// garbage
|
|
|
|
val pending = pendingData.remove(metadata.bytesWritten) ?: break
|
|
|
|
|
|
|
|
val size = pending.size
|
|
|
|
|
|
|
|
fileStream.write(
|
|
|
|
pending.bytes,
|
|
|
|
0,
|
|
|
|
size.toInt()
|
|
|
|
)
|
|
|
|
metadata.addBytesWritten(size)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-08-17 21:10:21 +00:00
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
// this will take up the first available job and resolve
|
|
|
|
while (true) {
|
|
|
|
if (!isActive) return@launch
|
|
|
|
fileMutex.withLock {
|
2023-08-19 23:29:50 +00:00
|
|
|
if (metadata.type == DownloadType.IsStopped
|
2023-08-20 01:58:31 +00:00
|
|
|
|| metadata.type == DownloadType.IsFailed
|
|
|
|
) return@launch
|
2023-08-19 19:37:14 +00:00
|
|
|
}
|
2021-07-04 17:00:04 +00:00
|
|
|
|
2023-08-19 23:29:50 +00:00
|
|
|
// mutex just in case, we never want this to fail due to multithreading
|
2023-08-19 19:37:14 +00:00
|
|
|
val index = currentMutex.withLock {
|
|
|
|
if (!current.hasNext()) return@launch
|
|
|
|
current.nextInt()
|
|
|
|
}
|
2021-07-28 01:04:32 +00:00
|
|
|
|
2023-08-19 19:37:14 +00:00
|
|
|
// in case something has gone wrong set to failed if the fail is not caused by
|
|
|
|
// user cancellation
|
|
|
|
if (!items.resolveSafe(index, callback = callback)) {
|
|
|
|
fileMutex.withLock {
|
|
|
|
if (metadata.type != DownloadType.IsStopped) {
|
|
|
|
metadata.type = DownloadType.IsFailed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return@launch
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// fast stop as the jobs may be in a slow request
|
|
|
|
metadata.setOnStop {
|
2023-08-19 23:29:50 +00:00
|
|
|
jobs.cancel()
|
2023-08-19 19:37:14 +00:00
|
|
|
}
|
|
|
|
|
2023-08-19 23:29:50 +00:00
|
|
|
jobs.join()
|
2023-08-22 02:00:05 +00:00
|
|
|
fileChecker.cancel()
|
2023-08-19 19:37:14 +00:00
|
|
|
|
|
|
|
// jobs are finished so we don't want to stop them anymore
|
|
|
|
metadata.removeStopListener()
|
2023-08-22 02:00:05 +00:00
|
|
|
if (!stream.exists) metadata.type = DownloadType.IsStopped
|
2023-08-19 19:37:14 +00:00
|
|
|
|
|
|
|
if (metadata.type == DownloadType.IsFailed) {
|
2023-08-23 04:25:06 +00:00
|
|
|
return@withContext DOWNLOAD_FAILED
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2021-07-03 20:59:46 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
if (metadata.type == DownloadType.IsStopped) {
|
2023-08-19 02:46:47 +00:00
|
|
|
// we need to close before delete
|
|
|
|
fileStream.closeQuietly()
|
|
|
|
metadata.onDelete()
|
2023-08-23 04:25:06 +00:00
|
|
|
stream.delete()
|
|
|
|
return@withContext DOWNLOAD_STOPPED
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2023-08-18 22:48:00 +00:00
|
|
|
|
|
|
|
metadata.type = DownloadType.IsDone
|
2023-08-23 04:25:06 +00:00
|
|
|
return@withContext DOWNLOAD_SUCCESS
|
2023-08-18 22:48:00 +00:00
|
|
|
} catch (e: IOException) {
|
|
|
|
// some sort of IO error, this should not happened
|
|
|
|
// we just rethrow it
|
2021-11-04 15:11:28 +00:00
|
|
|
logError(e)
|
2023-08-18 22:48:00 +00:00
|
|
|
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
|
2023-08-23 04:25:06 +00:00
|
|
|
return@withContext DOWNLOAD_FAILED
|
2023-08-18 22:48:00 +00:00
|
|
|
} finally {
|
|
|
|
fileStream?.closeQuietly()
|
2023-08-19 19:37:14 +00:00
|
|
|
//requestStream?.closeQuietly()
|
2023-08-18 22:48:00 +00:00
|
|
|
metadata.close()
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2023-08-18 22:48:00 +00:00
|
|
|
}
|
2021-07-04 17:00:04 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
private suspend fun downloadHLS(
|
|
|
|
context: Context,
|
|
|
|
link: ExtractorLink,
|
|
|
|
name: String,
|
2023-08-23 04:25:06 +00:00
|
|
|
folder: String,
|
2023-08-18 22:48:00 +00:00
|
|
|
parentId: Int?,
|
|
|
|
startIndex: Int?,
|
|
|
|
createNotificationCallback: (CreateNotificationMetadata) -> Unit,
|
|
|
|
parallelConnections: Int = 3
|
2023-08-23 04:25:06 +00:00
|
|
|
): DownloadStatus = withContext(Dispatchers.IO) {
|
|
|
|
if (parallelConnections < 1) return@withContext DOWNLOAD_INVALID_INPUT
|
2023-08-18 22:48:00 +00:00
|
|
|
|
|
|
|
val metadata = DownloadMetaData(
|
|
|
|
createNotificationCallback = createNotificationCallback,
|
|
|
|
id = parentId
|
|
|
|
)
|
|
|
|
var fileStream: OutputStream? = null
|
2021-07-06 00:18:56 +00:00
|
|
|
try {
|
2023-08-23 04:25:06 +00:00
|
|
|
val extension = "mp4"
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
// the start .ts index
|
|
|
|
var startAt = startIndex ?: 0
|
|
|
|
|
|
|
|
// set up the file data
|
|
|
|
val (baseFile, basePath) = context.getBasePath()
|
2023-08-23 04:25:06 +00:00
|
|
|
if (baseFile == null) return@withContext DOWNLOAD_BAD_CONFIG
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
val displayName = getDisplayName(name, extension)
|
2023-08-22 02:00:05 +00:00
|
|
|
val stream =
|
2023-08-23 04:25:06 +00:00
|
|
|
setupStream(baseFile, name, folder, extension, startAt > 0)
|
2023-08-22 02:00:05 +00:00
|
|
|
if (!stream.resume) startAt = 0
|
|
|
|
fileStream = stream.open()
|
2023-08-18 22:48:00 +00:00
|
|
|
|
|
|
|
// push the metadata
|
2023-08-22 02:00:05 +00:00
|
|
|
metadata.setResumeLength(stream.startAt)
|
2023-08-18 22:48:00 +00:00
|
|
|
metadata.hlsProgress = startAt
|
|
|
|
metadata.type = DownloadType.IsPending
|
|
|
|
metadata.setDownloadFileInfoTemplate(
|
|
|
|
DownloadedFileInfo(
|
|
|
|
totalBytes = 0,
|
2023-08-23 04:25:06 +00:00
|
|
|
relativePath = folder,
|
2023-08-18 22:48:00 +00:00
|
|
|
displayName = displayName,
|
|
|
|
basePath = basePath
|
|
|
|
)
|
|
|
|
)
|
2021-07-06 00:18:56 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
// do the initial get request to fetch the segments
|
|
|
|
val m3u8 = M3u8Helper.M3u8Stream(
|
2023-08-19 02:46:47 +00:00
|
|
|
link.url, link.quality, link.headers.appendAndDontOverride(
|
|
|
|
mapOf(
|
|
|
|
"Accept-Encoding" to "identity",
|
|
|
|
"accept" to "*/*",
|
|
|
|
"user-agent" to USER_AGENT,
|
|
|
|
) + if (link.referer.isNotBlank()) mapOf("referer" to link.referer) else emptyMap()
|
|
|
|
)
|
2023-08-18 22:48:00 +00:00
|
|
|
)
|
|
|
|
val items = M3u8Helper2.hslLazy(listOf(m3u8))
|
|
|
|
|
|
|
|
metadata.hlsTotal = items.size
|
|
|
|
metadata.type = DownloadType.IsDownloading
|
|
|
|
|
2023-08-19 23:29:50 +00:00
|
|
|
val currentMutex = Mutex()
|
2023-08-22 02:00:05 +00:00
|
|
|
val current = (startAt until items.size).iterator()
|
2023-08-19 23:29:50 +00:00
|
|
|
|
|
|
|
val fileMutex = Mutex()
|
|
|
|
val pendingData: HashMap<Int, ByteArray> = hashMapOf()
|
|
|
|
|
2023-08-22 02:00:05 +00:00
|
|
|
val fileChecker = launch(Dispatchers.IO) {
|
|
|
|
while (isActive) {
|
|
|
|
if (stream.exists) {
|
|
|
|
delay(5000)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
fileMutex.withLock {
|
|
|
|
metadata.type = DownloadType.IsStopped
|
|
|
|
}
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-19 23:29:50 +00:00
|
|
|
// see @downloadexplanation for explanation of this download strategy,
|
|
|
|
// this keeps all jobs working at all times,
|
2023-08-18 22:48:00 +00:00
|
|
|
// does several connections in parallel instead of a regular for loop to improve
|
|
|
|
// download speed
|
2023-08-19 23:29:50 +00:00
|
|
|
val jobs = (0 until parallelConnections).map {
|
|
|
|
launch(Dispatchers.IO) {
|
|
|
|
while (true) {
|
|
|
|
if (!isActive) return@launch
|
|
|
|
fileMutex.withLock {
|
|
|
|
if (metadata.type == DownloadType.IsStopped
|
|
|
|
|| metadata.type == DownloadType.IsFailed
|
|
|
|
) return@launch
|
|
|
|
}
|
|
|
|
|
|
|
|
// mutex just in case, we never want this to fail due to multithreading
|
|
|
|
val index = currentMutex.withLock {
|
|
|
|
if (!current.hasNext()) return@launch
|
|
|
|
current.nextInt()
|
|
|
|
}
|
|
|
|
|
|
|
|
// in case something has gone wrong set to failed if the fail is not caused by
|
|
|
|
// user cancellation
|
|
|
|
val bytes = items.resolveLinkSafe(index) ?: run {
|
|
|
|
fileMutex.withLock {
|
|
|
|
if (metadata.type != DownloadType.IsStopped) {
|
|
|
|
metadata.type = DownloadType.IsFailed
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return@launch
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
fileMutex.lock()
|
|
|
|
// user pause
|
|
|
|
while (metadata.type == DownloadType.IsPaused) delay(100)
|
|
|
|
// if stopped then break to delete
|
2023-08-22 02:00:05 +00:00
|
|
|
if (metadata.type == DownloadType.IsStopped || metadata.type == DownloadType.IsFailed || !isActive) return@launch
|
2023-08-19 23:29:50 +00:00
|
|
|
|
2023-08-20 01:58:31 +00:00
|
|
|
val segmentLength = bytes.size.toLong()
|
2023-08-19 23:29:50 +00:00
|
|
|
// send notification, no matter the actual write order
|
2023-08-20 01:58:31 +00:00
|
|
|
metadata.addSegment(segmentLength)
|
2023-08-19 23:29:50 +00:00
|
|
|
|
|
|
|
// directly write the bytes if you are first
|
|
|
|
if (metadata.hlsWrittenProgress == index) {
|
|
|
|
fileStream.write(bytes)
|
2023-08-20 01:58:31 +00:00
|
|
|
|
|
|
|
metadata.addBytesWritten(segmentLength)
|
2023-08-19 23:29:50 +00:00
|
|
|
metadata.setWrittenSegment(index)
|
|
|
|
} else {
|
|
|
|
// no need to clone as there will be no modification of this bytearray
|
|
|
|
pendingData[index] = bytes
|
|
|
|
}
|
|
|
|
|
|
|
|
// write the cached bytes submitted by other threads
|
|
|
|
while (true) {
|
2023-08-20 01:58:31 +00:00
|
|
|
val cache = pendingData.remove(metadata.hlsWrittenProgress) ?: break
|
|
|
|
val cacheLength = cache.size.toLong()
|
|
|
|
|
|
|
|
fileStream.write(cache)
|
2023-08-22 02:00:05 +00:00
|
|
|
|
2023-08-20 01:58:31 +00:00
|
|
|
metadata.addBytesWritten(cacheLength)
|
2023-08-19 23:29:50 +00:00
|
|
|
metadata.setWrittenSegment(metadata.hlsWrittenProgress)
|
|
|
|
}
|
2023-08-20 01:58:31 +00:00
|
|
|
} catch (t: Throwable) {
|
2023-08-19 23:29:50 +00:00
|
|
|
// this is in case of write fail
|
2023-08-22 02:00:05 +00:00
|
|
|
logError(t)
|
2023-08-19 23:29:50 +00:00
|
|
|
if (metadata.type != DownloadType.IsStopped) {
|
|
|
|
metadata.type = DownloadType.IsFailed
|
|
|
|
}
|
|
|
|
} finally {
|
|
|
|
fileMutex.unlock()
|
|
|
|
}
|
2023-08-18 22:48:00 +00:00
|
|
|
}
|
|
|
|
}
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2023-08-17 21:10:21 +00:00
|
|
|
|
2023-08-19 23:29:50 +00:00
|
|
|
// fast stop as the jobs may be in a slow request
|
|
|
|
metadata.setOnStop {
|
|
|
|
jobs.cancel()
|
|
|
|
}
|
|
|
|
|
|
|
|
jobs.join()
|
2023-08-22 02:00:05 +00:00
|
|
|
fileChecker.cancel()
|
2023-08-19 23:29:50 +00:00
|
|
|
|
|
|
|
metadata.removeStopListener()
|
|
|
|
|
2023-08-22 02:00:05 +00:00
|
|
|
if (!stream.exists) metadata.type = DownloadType.IsStopped
|
|
|
|
|
2023-08-19 23:29:50 +00:00
|
|
|
if (metadata.type == DownloadType.IsFailed) {
|
2023-08-23 04:25:06 +00:00
|
|
|
return@withContext DOWNLOAD_FAILED
|
2023-08-19 23:29:50 +00:00
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
if (metadata.type == DownloadType.IsStopped) {
|
2023-08-19 02:46:47 +00:00
|
|
|
// we need to close before delete
|
|
|
|
fileStream.closeQuietly()
|
|
|
|
metadata.onDelete()
|
2023-08-23 04:25:06 +00:00
|
|
|
stream.delete()
|
|
|
|
return@withContext DOWNLOAD_STOPPED
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2023-08-17 21:10:21 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
metadata.type = DownloadType.IsDone
|
2023-08-23 04:25:06 +00:00
|
|
|
return@withContext DOWNLOAD_SUCCESS
|
2023-08-18 22:48:00 +00:00
|
|
|
} catch (t: Throwable) {
|
|
|
|
logError(t)
|
|
|
|
metadata.type = DownloadType.IsFailed
|
2023-08-23 04:25:06 +00:00
|
|
|
return@withContext DOWNLOAD_FAILED
|
2023-08-18 22:48:00 +00:00
|
|
|
} finally {
|
|
|
|
fileStream?.closeQuietly()
|
|
|
|
metadata.close()
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2021-06-29 23:14:48 +00:00
|
|
|
}
|
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
private fun getDisplayName(name: String, extension: String): String {
|
|
|
|
return "$name.$extension"
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the default download path as an UniFile.
|
|
|
|
* Vital for legacy downloads, be careful about changing anything here.
|
|
|
|
*
|
|
|
|
* As of writing UniFile is used for everything but download directory on scoped storage.
|
|
|
|
* Special ContentResolver fuckery is needed for that as UniFile doesn't work.
|
|
|
|
* */
|
2023-08-23 04:25:06 +00:00
|
|
|
fun getDefaultDir(context: Context): SafeFile? {
|
2021-11-01 15:33:46 +00:00
|
|
|
// See https://www.py4u.net/discuss/614761
|
2023-08-23 04:25:06 +00:00
|
|
|
return SafeFile.fromMedia(
|
|
|
|
context, MediaFileContentType.Downloads
|
2021-11-01 15:33:46 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Turns a string to an UniFile. Used for stored string paths such as settings.
|
|
|
|
* Should only be used to get a download path.
|
|
|
|
* */
|
2023-08-23 04:25:06 +00:00
|
|
|
private fun basePathToFile(context: Context, path: String?): SafeFile? {
|
2021-11-01 15:33:46 +00:00
|
|
|
return when {
|
2023-08-23 04:25:06 +00:00
|
|
|
path.isNullOrBlank() -> getDefaultDir(context)
|
|
|
|
path.startsWith("content://") -> SafeFile.fromUri(context, path.toUri())
|
|
|
|
else -> SafeFile.fromFile(context, File(path))
|
2021-11-01 15:33:46 +00:00
|
|
|
}
|
2021-09-01 21:30:21 +00:00
|
|
|
}
|
|
|
|
|
2021-11-01 15:33:46 +00:00
|
|
|
/**
|
|
|
|
* Base path where downloaded things should be stored, changes depending on settings.
|
|
|
|
* Returns the file and a string to be stored for future file retrieval.
|
|
|
|
* UniFile.filePath is not sufficient for storage.
|
|
|
|
* */
|
2023-08-23 04:25:06 +00:00
|
|
|
fun Context.getBasePath(): Pair<SafeFile?, String?> {
|
2021-11-01 15:33:46 +00:00
|
|
|
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
|
|
|
val basePathSetting = settingsManager.getString(getString(R.string.download_path_key), null)
|
|
|
|
return basePathToFile(this, basePathSetting) to basePathSetting
|
|
|
|
}
|
|
|
|
|
2022-04-03 20:14:51 +00:00
|
|
|
fun getFileName(context: Context, metadata: DownloadEpisodeMetadata): String {
|
|
|
|
return getFileName(context, metadata.name, metadata.episode, metadata.season)
|
|
|
|
}
|
|
|
|
|
2022-04-06 15:16:08 +00:00
|
|
|
private fun getFileName(
|
|
|
|
context: Context,
|
|
|
|
epName: String?,
|
|
|
|
episode: Int?,
|
|
|
|
season: Int?
|
|
|
|
): String {
|
2022-04-03 20:14:51 +00:00
|
|
|
// kinda ugly ik
|
|
|
|
return sanitizeFilename(
|
|
|
|
if (epName == null) {
|
|
|
|
if (season != null) {
|
|
|
|
"${context.getString(R.string.season)} $season ${context.getString(R.string.episode)} $episode"
|
|
|
|
} else {
|
|
|
|
"${context.getString(R.string.episode)} $episode"
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (episode != null) {
|
|
|
|
if (season != null) {
|
|
|
|
"${context.getString(R.string.season)} $season ${context.getString(R.string.episode)} $episode - $epName"
|
|
|
|
} else {
|
|
|
|
"${context.getString(R.string.episode)} $episode - $epName"
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
epName
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-08-17 21:10:21 +00:00
|
|
|
private suspend fun downloadSingleEpisode(
|
2021-08-22 17:14:48 +00:00
|
|
|
context: Context,
|
|
|
|
source: String?,
|
|
|
|
folder: String?,
|
|
|
|
ep: DownloadEpisodeMetadata,
|
|
|
|
link: ExtractorLink,
|
2021-09-14 22:18:03 +00:00
|
|
|
notificationCallback: (Int, Notification) -> Unit,
|
2021-08-22 17:14:48 +00:00
|
|
|
tryResume: Boolean = false,
|
2023-08-23 04:25:06 +00:00
|
|
|
): DownloadStatus {
|
2022-04-03 20:14:51 +00:00
|
|
|
val name = getFileName(context, ep)
|
2021-08-22 17:14:48 +00:00
|
|
|
|
2022-02-25 23:04:00 +00:00
|
|
|
// Make sure this is cancelled when download is done or cancelled.
|
|
|
|
val extractorJob = ioSafe {
|
|
|
|
if (link.extractorData != null) {
|
|
|
|
getApiFromNameNull(link.source)?.extractorVerifierJob(link.extractorData)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-22 02:00:05 +00:00
|
|
|
val callback: (CreateNotificationMetadata) -> Unit = { meta ->
|
|
|
|
main {
|
|
|
|
createNotification(
|
|
|
|
context,
|
|
|
|
source,
|
|
|
|
link.name,
|
|
|
|
ep,
|
|
|
|
meta.type,
|
|
|
|
meta.bytesDownloaded,
|
|
|
|
meta.bytesTotal,
|
|
|
|
notificationCallback,
|
|
|
|
meta.hlsProgress,
|
|
|
|
meta.hlsTotal,
|
|
|
|
meta.bytesPerSecond
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
|
|
if (link.isM3u8 || normalSafeApiCall { URL(link.url).path.endsWith(".m3u8") } == true) {
|
|
|
|
val startIndex = if (tryResume) {
|
|
|
|
context.getKey<DownloadedFileInfo>(
|
|
|
|
KEY_DOWNLOAD_INFO,
|
|
|
|
ep.id.toString(),
|
|
|
|
null
|
|
|
|
)?.extraInfo?.toIntOrNull()
|
|
|
|
} else null
|
|
|
|
|
|
|
|
return downloadHLS(
|
2023-08-18 22:48:00 +00:00
|
|
|
context,
|
|
|
|
link,
|
|
|
|
name,
|
2023-08-23 04:25:06 +00:00
|
|
|
folder ?: "",
|
2023-08-18 22:48:00 +00:00
|
|
|
ep.id,
|
|
|
|
startIndex,
|
2023-08-22 02:00:05 +00:00
|
|
|
callback
|
|
|
|
)
|
|
|
|
} else {
|
|
|
|
return downloadThing(
|
|
|
|
context,
|
|
|
|
link,
|
|
|
|
name,
|
2023-08-23 04:25:06 +00:00
|
|
|
folder ?: "",
|
2023-08-22 02:00:05 +00:00
|
|
|
"mp4",
|
|
|
|
tryResume,
|
|
|
|
ep.id,
|
|
|
|
callback
|
2023-08-18 22:48:00 +00:00
|
|
|
)
|
2023-08-22 02:00:05 +00:00
|
|
|
}
|
|
|
|
} catch (t: Throwable) {
|
2023-08-23 04:25:06 +00:00
|
|
|
return DOWNLOAD_FAILED
|
2023-08-22 02:00:05 +00:00
|
|
|
} finally {
|
|
|
|
extractorJob.cancel()
|
2021-09-01 12:21:03 +00:00
|
|
|
}
|
2021-08-22 17:14:48 +00:00
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
suspend fun downloadCheck(
|
2021-09-14 22:18:03 +00:00
|
|
|
context: Context, notificationCallback: (Int, Notification) -> Unit,
|
2023-08-19 02:46:47 +00:00
|
|
|
) {
|
|
|
|
if (!(currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0)) return
|
|
|
|
|
|
|
|
val pkg = downloadQueue.removeFirst()
|
|
|
|
val item = pkg.item
|
|
|
|
val id = item.ep.id
|
|
|
|
if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT
|
2023-08-23 04:36:43 +00:00
|
|
|
downloadEvent.invoke(id to DownloadActionType.Resume)
|
|
|
|
return
|
2023-08-19 02:46:47 +00:00
|
|
|
}
|
2021-07-08 17:46:47 +00:00
|
|
|
|
2023-08-19 02:46:47 +00:00
|
|
|
currentDownloads.add(id)
|
|
|
|
try {
|
|
|
|
for (index in (pkg.linkIndex ?: 0) until item.links.size) {
|
|
|
|
val link = item.links[index]
|
|
|
|
val resume = pkg.linkIndex == index
|
2021-07-05 18:43:28 +00:00
|
|
|
|
2023-08-19 02:46:47 +00:00
|
|
|
setKey(
|
|
|
|
KEY_RESUME_PACKAGES,
|
|
|
|
id.toString(),
|
|
|
|
DownloadResumePackage(item, index)
|
|
|
|
)
|
|
|
|
|
|
|
|
var connectionResult =
|
|
|
|
downloadSingleEpisode(
|
|
|
|
context,
|
|
|
|
item.source,
|
|
|
|
item.folder,
|
|
|
|
item.ep,
|
|
|
|
link,
|
|
|
|
notificationCallback,
|
|
|
|
resume
|
2023-08-18 22:48:00 +00:00
|
|
|
)
|
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
if (connectionResult.retrySame) {
|
2023-08-19 02:46:47 +00:00
|
|
|
connectionResult = downloadSingleEpisode(
|
|
|
|
context,
|
|
|
|
item.source,
|
|
|
|
item.folder,
|
|
|
|
item.ep,
|
|
|
|
link,
|
|
|
|
notificationCallback,
|
|
|
|
true
|
|
|
|
)
|
|
|
|
}
|
2023-08-18 22:48:00 +00:00
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
if (connectionResult.success) { // SUCCESS
|
2023-08-19 02:46:47 +00:00
|
|
|
removeKey(KEY_RESUME_PACKAGES, id.toString())
|
|
|
|
break
|
2023-08-23 04:25:06 +00:00
|
|
|
} else if (!connectionResult.tryNext || index >= item.links.lastIndex) {
|
2023-08-19 02:46:47 +00:00
|
|
|
downloadStatusEvent.invoke(Pair(id, DownloadType.IsFailed))
|
2023-08-23 04:25:06 +00:00
|
|
|
break
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
|
|
|
}
|
2023-08-19 02:46:47 +00:00
|
|
|
} catch (e: Exception) {
|
|
|
|
logError(e)
|
|
|
|
} finally {
|
|
|
|
currentDownloads.remove(id)
|
|
|
|
// Because otherwise notifications will not get caught by the work manager
|
|
|
|
downloadCheckUsingWorker(context)
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
2023-08-19 02:46:47 +00:00
|
|
|
|
|
|
|
// return id
|
2021-07-04 17:00:04 +00:00
|
|
|
}
|
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
/* fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? {
|
|
|
|
val res = getDownloadFileInfo(context, id)
|
|
|
|
if (res == null) context.removeKey(KEY_DOWNLOAD_INFO, id.toString())
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
*/
|
|
|
|
fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? =
|
|
|
|
getDownloadFileInfo(context, id, removeKeys = true)
|
|
|
|
|
|
|
|
private fun DownloadedFileInfo.toFile(context: Context): SafeFile? {
|
|
|
|
return basePathToFile(context, this.basePath)?.gotoDirectory(relativePath)
|
|
|
|
?.findFile(displayName)
|
2021-07-05 20:28:50 +00:00
|
|
|
}
|
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
private fun getDownloadFileInfo(
|
|
|
|
context: Context,
|
|
|
|
id: Int,
|
|
|
|
removeKeys: Boolean = false
|
|
|
|
): DownloadedFileInfoResult? {
|
2022-04-06 15:16:08 +00:00
|
|
|
try {
|
|
|
|
val info =
|
|
|
|
context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return null
|
2023-08-23 04:25:06 +00:00
|
|
|
val file = info.toFile(context)
|
|
|
|
|
|
|
|
// only delete the key if the file is not found
|
|
|
|
if (file == null || !file.existsOrThrow()) {
|
2023-08-24 16:14:54 +00:00
|
|
|
//if (removeKeys) context.removeKey(KEY_DOWNLOAD_INFO, id.toString()) // TODO READD
|
2023-08-23 04:25:06 +00:00
|
|
|
return null
|
|
|
|
}
|
2022-04-06 15:16:08 +00:00
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
return DownloadedFileInfoResult(
|
|
|
|
file.lengthOrThrow(),
|
|
|
|
info.totalBytes,
|
|
|
|
file.uriOrThrow()
|
|
|
|
)
|
2022-04-06 15:16:08 +00:00
|
|
|
} catch (e: Exception) {
|
|
|
|
logError(e)
|
|
|
|
return null
|
2021-11-02 14:25:12 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-07-05 20:28:50 +00:00
|
|
|
fun deleteFileAndUpdateSettings(context: Context, id: Int): Boolean {
|
|
|
|
val success = deleteFile(context, id)
|
|
|
|
if (success) context.removeKey(KEY_DOWNLOAD_INFO, id.toString())
|
|
|
|
return success
|
|
|
|
}
|
|
|
|
|
2023-08-23 04:25:06 +00:00
|
|
|
/*private fun deleteFile(
|
2023-08-19 02:46:47 +00:00
|
|
|
context: Context,
|
2023-08-23 04:25:06 +00:00
|
|
|
folder: SafeFile?,
|
2023-08-19 02:46:47 +00:00
|
|
|
relativePath: String,
|
|
|
|
displayName: String
|
|
|
|
): Boolean {
|
2023-08-23 04:25:06 +00:00
|
|
|
val file = folder?.gotoDirectory(relativePath)?.findFile(displayName) ?: return false
|
|
|
|
if (file.exists() == false) return true
|
2023-08-22 02:00:05 +00:00
|
|
|
return try {
|
|
|
|
file.delete()
|
|
|
|
} catch (e: Exception) {
|
|
|
|
logError(e)
|
2023-08-23 04:25:06 +00:00
|
|
|
(context.contentResolver?.delete(file.uri() ?: return true, null, null)
|
|
|
|
?: return false) > 0
|
2021-07-05 20:28:50 +00:00
|
|
|
}
|
2023-08-23 04:25:06 +00:00
|
|
|
}*/
|
2021-07-05 20:28:50 +00:00
|
|
|
|
2023-08-19 02:46:47 +00:00
|
|
|
private fun deleteFile(context: Context, id: Int): Boolean {
|
|
|
|
val info =
|
|
|
|
context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, id.toString()) ?: return false
|
2023-08-22 02:00:05 +00:00
|
|
|
downloadEvent.invoke(id to DownloadActionType.Stop)
|
2023-08-19 02:46:47 +00:00
|
|
|
downloadProgressEvent.invoke(Triple(id, 0, 0))
|
|
|
|
downloadStatusEvent.invoke(id to DownloadType.IsStopped)
|
|
|
|
downloadDeleteEvent.invoke(id)
|
2023-08-23 04:25:06 +00:00
|
|
|
return info.toFile(context)?.delete() ?: false
|
2023-08-19 02:46:47 +00:00
|
|
|
}
|
|
|
|
|
2021-07-05 20:28:50 +00:00
|
|
|
fun getDownloadResumePackage(context: Context, id: Int): DownloadResumePackage? {
|
|
|
|
return context.getKey(KEY_RESUME_PACKAGES, id.toString())
|
|
|
|
}
|
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
suspend fun downloadFromResume(
|
2021-09-14 22:18:03 +00:00
|
|
|
context: Context,
|
|
|
|
pkg: DownloadResumePackage,
|
|
|
|
notificationCallback: (Int, Notification) -> Unit,
|
|
|
|
setKey: Boolean = true
|
|
|
|
) {
|
2023-08-23 04:36:43 +00:00
|
|
|
if (!currentDownloads.any { it == pkg.item.ep.id } && !downloadQueue.any { it.item.ep.id == pkg.item.ep.id }) {
|
2021-07-17 15:56:26 +00:00
|
|
|
downloadQueue.addLast(pkg)
|
2021-09-14 22:18:03 +00:00
|
|
|
downloadCheck(context, notificationCallback)
|
2021-12-16 23:45:20 +00:00
|
|
|
if (setKey) saveQueue()
|
2023-08-19 02:46:47 +00:00
|
|
|
//ret
|
2021-07-24 15:13:21 +00:00
|
|
|
} else {
|
2023-08-19 02:46:47 +00:00
|
|
|
downloadEvent(
|
2023-08-23 04:36:43 +00:00
|
|
|
pkg.item.ep.id to DownloadActionType.Resume
|
2021-07-24 15:13:21 +00:00
|
|
|
)
|
2023-08-19 02:46:47 +00:00
|
|
|
//null
|
2021-07-17 15:56:26 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-16 23:45:20 +00:00
|
|
|
private fun saveQueue() {
|
2022-07-20 23:48:40 +00:00
|
|
|
try {
|
|
|
|
val dQueue =
|
|
|
|
downloadQueue.toList()
|
|
|
|
.mapIndexed { index, any -> DownloadQueueResumePackage(index, any) }
|
|
|
|
.toTypedArray()
|
|
|
|
setKey(KEY_RESUME_QUEUE_PACKAGES, dQueue)
|
2023-02-19 18:27:40 +00:00
|
|
|
} catch (t: Throwable) {
|
2022-08-25 01:59:20 +00:00
|
|
|
logError(t)
|
2022-07-20 23:48:40 +00:00
|
|
|
}
|
2021-07-05 18:09:37 +00:00
|
|
|
}
|
|
|
|
|
2021-08-29 18:42:44 +00:00
|
|
|
/*fun isMyServiceRunning(context: Context, serviceClass: Class<*>): Boolean {
|
2021-07-08 17:46:47 +00:00
|
|
|
val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager?
|
|
|
|
for (service in manager!!.getRunningServices(Int.MAX_VALUE)) {
|
|
|
|
if (serviceClass.name == service.service.className) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
2021-08-29 18:42:44 +00:00
|
|
|
}*/
|
2021-07-08 17:46:47 +00:00
|
|
|
|
2023-08-18 22:48:00 +00:00
|
|
|
suspend fun downloadEpisode(
|
2021-09-14 22:18:03 +00:00
|
|
|
context: Context?,
|
2021-07-06 00:18:56 +00:00
|
|
|
source: String?,
|
2021-07-04 00:59:51 +00:00
|
|
|
folder: String?,
|
|
|
|
ep: DownloadEpisodeMetadata,
|
2021-09-14 22:18:03 +00:00
|
|
|
links: List<ExtractorLink>,
|
|
|
|
notificationCallback: (Int, Notification) -> Unit,
|
2021-07-04 00:59:51 +00:00
|
|
|
) {
|
2021-08-29 19:46:25 +00:00
|
|
|
if (context == null) return
|
2023-08-18 22:48:00 +00:00
|
|
|
if (links.isEmpty()) return
|
|
|
|
downloadFromResume(
|
|
|
|
context,
|
|
|
|
DownloadResumePackage(DownloadItem(source, folder, ep, links), null),
|
|
|
|
notificationCallback
|
|
|
|
)
|
2021-06-29 23:14:48 +00:00
|
|
|
}
|
2021-09-14 22:18:03 +00:00
|
|
|
|
|
|
|
/** Worker stuff */
|
|
|
|
private fun startWork(context: Context, key: String) {
|
|
|
|
val req = OneTimeWorkRequest.Builder(DownloadFileWorkManager::class.java)
|
|
|
|
.setInputData(
|
|
|
|
Data.Builder()
|
|
|
|
.putString("key", key)
|
|
|
|
.build()
|
|
|
|
)
|
|
|
|
.build()
|
|
|
|
(WorkManager.getInstance(context)).enqueueUniqueWork(
|
|
|
|
key,
|
|
|
|
ExistingWorkPolicy.KEEP,
|
|
|
|
req
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun downloadCheckUsingWorker(
|
|
|
|
context: Context,
|
|
|
|
) {
|
|
|
|
startWork(context, DOWNLOAD_CHECK)
|
|
|
|
}
|
|
|
|
|
|
|
|
fun downloadFromResumeUsingWorker(
|
|
|
|
context: Context,
|
|
|
|
pkg: DownloadResumePackage,
|
|
|
|
) {
|
|
|
|
val key = pkg.item.ep.id.toString()
|
2021-12-16 23:45:20 +00:00
|
|
|
setKey(WORK_KEY_PACKAGE, key, pkg)
|
2021-09-14 22:18:03 +00:00
|
|
|
startWork(context, key)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Keys are needed to transfer the data to the worker reliably and without exceeding the data limit
|
|
|
|
const val WORK_KEY_PACKAGE = "work_key_package"
|
|
|
|
const val WORK_KEY_INFO = "work_key_info"
|
|
|
|
|
|
|
|
fun downloadEpisodeUsingWorker(
|
|
|
|
context: Context,
|
|
|
|
source: String?,
|
|
|
|
folder: String?,
|
|
|
|
ep: DownloadEpisodeMetadata,
|
|
|
|
links: List<ExtractorLink>,
|
|
|
|
) {
|
|
|
|
val info = DownloadInfo(
|
|
|
|
source, folder, ep, links
|
|
|
|
)
|
|
|
|
|
|
|
|
val key = info.ep.id.toString()
|
2021-12-16 23:45:20 +00:00
|
|
|
setKey(WORK_KEY_INFO, key, info)
|
2021-09-14 22:18:03 +00:00
|
|
|
startWork(context, key)
|
|
|
|
}
|
|
|
|
|
|
|
|
data class DownloadInfo(
|
|
|
|
@JsonProperty("source") val source: String?,
|
|
|
|
@JsonProperty("folder") val folder: String?,
|
|
|
|
@JsonProperty("ep") val ep: DownloadEpisodeMetadata,
|
|
|
|
@JsonProperty("links") val links: List<ExtractorLink>
|
|
|
|
)
|
2021-09-01 12:21:03 +00:00
|
|
|
}
|