AquaStream/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt

641 lines
24 KiB
Kotlin
Raw Normal View History

2021-06-29 23:14:48 +00:00
package com.lagradost.cloudstream3.utils
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
2021-07-04 17:00:04 +00:00
import android.os.Environment
2021-07-05 00:55:07 +00:00
import android.provider.MediaStore
2021-06-29 23:14:48 +00:00
import androidx.annotation.DrawableRes
2021-07-05 18:09:37 +00:00
import androidx.annotation.RequiresApi
2021-06-29 23:14:48 +00:00
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.net.toUri
2021-07-03 20:59:46 +00:00
import com.anggrayudi.storage.extension.closeStream
2021-06-29 23:14:48 +00:00
import com.bumptech.glide.Glide
import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.R
import com.lagradost.cloudstream3.UIHelper.colorFromAttribute
2021-07-04 17:00:04 +00:00
import com.lagradost.cloudstream3.mvvm.logError
import com.lagradost.cloudstream3.mvvm.normalSafeApiCall
import com.lagradost.cloudstream3.services.VideoDownloadService
2021-07-04 00:59:51 +00:00
import com.lagradost.cloudstream3.utils.Coroutines.main
2021-07-05 18:09:37 +00:00
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.DataStore.setKey
2021-07-04 00:59:51 +00:00
import kotlinx.coroutines.Dispatchers
2021-07-04 17:00:04 +00:00
import kotlinx.coroutines.delay
2021-07-04 00:59:51 +00:00
import kotlinx.coroutines.withContext
2021-07-05 18:09:37 +00:00
import java.io.*
2021-07-04 17:00:04 +00:00
import java.lang.Thread.sleep
2021-07-03 20:59:46 +00:00
import java.net.URL
import java.net.URLConnection
2021-07-04 17:00:04 +00:00
import java.util.*
import kotlin.collections.ArrayList
2021-06-29 23:14:48 +00:00
const val CHANNEL_ID = "cloudstream3.general"
const val CHANNEL_NAME = "Downloads"
const val CHANNEL_DESCRIPT = "The download notification channel"
object VideoDownloadManager {
2021-07-03 20:59:46 +00:00
var maxConcurrentDownloads = 3
2021-07-04 17:00:04 +00:00
var currentDownloads = 0
2021-07-03 20:59:46 +00:00
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"
2021-06-29 23:14:48 +00:00
@DrawableRes
const val imgDone = R.drawable.rddone
@DrawableRes
const val imgDownloading = R.drawable.rdload
@DrawableRes
const val imgPaused = R.drawable.rdpause
@DrawableRes
const val imgStopped = R.drawable.rderror
@DrawableRes
const val imgError = R.drawable.rderror
@DrawableRes
const val pressToPauseIcon = R.drawable.ic_baseline_pause_24
@DrawableRes
const val pressToResumeIcon = R.drawable.ic_baseline_play_arrow_24
@DrawableRes
const val pressToStopIcon = R.drawable.exo_icon_stop
enum class DownloadType {
IsPaused,
IsDownloading,
IsDone,
IsFailed,
IsStopped,
}
enum class DownloadActionType {
Pause,
Resume,
Stop,
}
2021-07-03 20:59:46 +00:00
data class DownloadEpisodeMetadata(
val id: Int,
val mainName: String,
val sourceApiName: String?,
val poster: String?,
val name: String?,
val season: Int?,
val episode: Int?
)
2021-07-04 17:00:04 +00:00
data class DownloadItem(
val source: String,
val folder: String?,
val ep: DownloadEpisodeMetadata,
val links: List<ExtractorLink>
)
2021-07-05 18:09:37 +00:00
data class DownloadResumePackage(
val item: DownloadItem,
val linkIndex: Int?,
)
2021-07-04 17:00:04 +00:00
private const val SUCCESS_DOWNLOAD_DONE = 1
private const val SUCCESS_STOPPED = 2
private const val ERROR_DELETING_FILE = -1
2021-07-05 18:09:37 +00:00
private const val ERROR_CREATE_FILE = -2
2021-07-04 17:00:04 +00:00
private const val ERROR_OPEN_FILE = -3
private const val ERROR_TOO_SMALL_CONNECTION = -4
private const val ERROR_WRONG_CONTENT = -5
private const val ERROR_CONNECTION_ERROR = -6
2021-07-05 18:09:37 +00:00
private const val ERROR_MEDIA_STORE_URI_CANT_BE_CREATED = -7
private const val ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM = -8
private const val ERROR_CONTENT_RESOLVER_NOT_FOUND = -9
2021-07-04 17:00:04 +00:00
2021-07-05 18:09:37 +00:00
private const val KEY_RESUME_STORAGE = "download_resume"
2021-07-04 17:00:04 +00:00
2021-07-05 18:09:37 +00:00
val events = Event<Pair<Int, DownloadActionType>>()
private 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) {
val name = CHANNEL_NAME //getString(R.string.channel_name)
val descriptionText = CHANNEL_DESCRIPT//getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText
}
// Register the channel with the system
val notificationManager: NotificationManager =
this.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
private val cachedBitmaps = hashMapOf<String, Bitmap>()
private fun Context.getImageBitmapFromUrl(url: String): Bitmap? {
if (cachedBitmaps.containsKey(url)) {
return cachedBitmaps[url]
}
val bitmap = Glide.with(this)
.asBitmap()
.load(url).into(720, 720)
.get()
if (bitmap != null) {
cachedBitmaps[url] = bitmap
}
return null
}
2021-07-03 20:59:46 +00:00
private 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-07-04 17:00:04 +00:00
main { // DON'T WANT TO SLOW IT DOWN
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setAutoCancel(true)
.setColorized(true)
.setOnlyAlertOnce(true)
.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
}
)
2021-07-03 20:59:46 +00:00
2021-07-04 17:00:04 +00:00
if (ep.sourceApiName != null) {
builder.setSubText(ep.sourceApiName)
2021-07-03 20:59:46 +00:00
}
2021-06-29 23:14:48 +00:00
2021-07-04 17:00:04 +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
}
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
builder.setContentIntent(pendingIntent)
}
2021-06-29 23:14:48 +00:00
2021-07-04 17:00:04 +00:00
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
builder.setProgress(total.toInt(), progress.toInt(), false)
}
2021-07-03 20:59:46 +00:00
2021-07-04 17:00:04 +00:00
val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else ""
val rowTwo = if (ep.season != null && ep.episode != null) {
"S${ep.season}:E${ep.episode}" + rowTwoExtra
} else if (ep.episode != null) {
"Episode ${ep.episode}" + rowTwoExtra
} else {
(ep.name ?: "") + ""
2021-06-29 23:14:48 +00:00
}
2021-07-03 20:59:46 +00:00
2021-07-04 17:00:04 +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-03 20:59:46 +00:00
2021-07-04 17:00:04 +00:00
val progressPercentage = progress * 100 / total
val progressMbString = "%.1f".format(progress / 1000000f)
val totalMbString = "%.1f".format(total / 1000000f)
val bigText =
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
(if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString MB/$totalMbString MB)"
} else if (state == DownloadType.IsFailed) {
"Download Failed - $rowTwo"
} else if (state == DownloadType.IsDone) {
"Download Done - $rowTwo"
} else {
"Download Stopped - $rowTwo"
}
val bodyStyle = NotificationCompat.BigTextStyle()
bodyStyle.bigText(bigText)
builder.setStyle(bodyStyle)
} else {
val txt = if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
rowTwo
2021-07-03 20:59:46 +00:00
} else if (state == DownloadType.IsFailed) {
"Download Failed - $rowTwo"
} else if (state == DownloadType.IsDone) {
"Download Done - $rowTwo"
} else {
"Download Stopped - $rowTwo"
}
2021-07-04 17:00:04 +00:00
builder.setContentText(txt)
2021-07-03 20:59:46 +00:00
}
2021-07-04 17:00:04 +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-07-04 17:00:04 +00:00
if (state == DownloadType.IsPaused) {
actionTypes.add(DownloadActionType.Resume)
actionTypes.add(DownloadActionType.Stop)
}
2021-06-29 23:14:48 +00:00
2021-07-04 17:00:04 +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-07-04 17:00:04 +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-07-04 17:00:04 +00:00
actionResultIntent.putExtra("id", ep.id)
2021-06-29 23:14:48 +00:00
2021-07-04 17:00:04 +00:00
val pending: PendingIntent = PendingIntent.getService(
context, 4337 + index + ep.id,
actionResultIntent,
PendingIntent.FLAG_UPDATE_CURRENT
)
2021-06-29 23:14:48 +00:00
2021-07-04 17:00:04 +00:00
builder.addAction(
NotificationCompat.Action(
when (i) {
DownloadActionType.Resume -> pressToResumeIcon
DownloadActionType.Pause -> pressToPauseIcon
DownloadActionType.Stop -> pressToStopIcon
}, when (i) {
DownloadActionType.Resume -> "Resume"
DownloadActionType.Pause -> "Pause"
DownloadActionType.Stop -> "Stop"
}, pending
)
2021-06-29 23:14:48 +00:00
)
2021-07-04 17:00:04 +00:00
}
2021-06-29 23:14:48 +00:00
}
2021-07-04 17:00:04 +00:00
if (!hasCreatedNotChanel) {
context.createNotificationChannel()
}
2021-06-29 23:14:48 +00:00
2021-07-04 17:00:04 +00:00
with(NotificationManagerCompat.from(context)) {
// notificationId is a unique int for each notification that you must define
notify(ep.id, builder.build())
}
2021-06-29 23:14:48 +00:00
}
}
2021-07-04 17:00:04 +00:00
private const val reservedChars = "|\\?*<\":>+[]/\'"
fun sanitizeFilename(name: String): String {
2021-07-04 00:59:51 +00:00
var tempName = name
for (c in reservedChars) {
tempName = tempName.replace(c, ' ')
}
2021-07-04 17:00:04 +00:00
return tempName.replace(" ", " ").trim(' ')
}
private const val reservedCharsPath = "|\\?*<\":>+[]\'"
fun sanitizePath(name: String): String {
var tempName = name
for (c in reservedCharsPath) {
tempName = tempName.replace(c, ' ')
}
return tempName.replace(" ", " ").trim(' ')
2021-07-04 00:59:51 +00:00
}
2021-07-05 18:09:37 +00:00
@RequiresApi(Build.VERSION_CODES.Q)
private fun ContentResolver.getExistingDownloadUriOrNullQ(relativePath: String, displayName: String): Uri? {
val projection = arrayOf(
MediaStore.MediaColumns._ID,
//MediaStore.MediaColumns.DISPLAY_NAME, // unused (for verification use only)
//MediaStore.MediaColumns.RELATIVE_PATH, // unused (for verification use only)
)
val selection =
"${MediaStore.MediaColumns.RELATIVE_PATH}='$relativePath' AND " + "${MediaStore.MediaColumns.DISPLAY_NAME}='$displayName'"
val result = this.query(
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY),
projection, selection, null, null
)
result.use { c ->
if (c != null && c.count >= 1) {
c.moveToFirst().let {
val id = c.getLong(c.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
/*
val cDisplayName = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME))
val cRelativePath = c.getString(c.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH))*/
return ContentUris.withAppendedId(
MediaStore.Downloads.EXTERNAL_CONTENT_URI, id
)
}
}
}
return null
}
private fun isScopedStorage(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
}
2021-07-04 17:00:04 +00:00
private fun downloadSingleEpisode(
2021-07-03 20:59:46 +00:00
context: Context,
source: String?,
2021-07-04 00:59:51 +00:00
folder: String?,
2021-07-03 20:59:46 +00:00
ep: DownloadEpisodeMetadata,
2021-07-05 18:09:37 +00:00
link: ExtractorLink,
tryResume: Boolean = false,
2021-07-04 17:00:04 +00:00
): Int {
val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}")
2021-07-03 20:59:46 +00:00
2021-07-05 18:09:37 +00:00
val relativePath = (Environment.DIRECTORY_DOWNLOADS + '/' + folder + '/').replace('/', File.separatorChar)
val displayName = "$name.mp4"
2021-07-05 00:55:07 +00:00
2021-07-05 18:09:37 +00:00
val normalPath = "${Environment.getExternalStorageDirectory()}${File.separatorChar}$relativePath$displayName"
var resume = tryResume
2021-07-05 00:55:07 +00:00
2021-07-05 18:09:37 +00:00
val fileStream: OutputStream
val fileLength: Long
2021-07-05 00:55:07 +00:00
2021-07-05 18:09:37 +00:00
fun deleteFile(): Int {
if (isScopedStorage()) {
val lastContent = context.contentResolver.getExistingDownloadUriOrNullQ(relativePath, displayName)
if (lastContent != null) {
context.contentResolver.delete(lastContent, null, null)
}
} else {
if (!File(normalPath).delete()) return ERROR_DELETING_FILE
2021-07-03 20:59:46 +00:00
}
2021-07-05 18:09:37 +00:00
return SUCCESS_STOPPED
2021-07-03 20:59:46 +00:00
}
2021-07-04 00:59:51 +00:00
2021-07-05 18:09:37 +00:00
if (isScopedStorage()) {
val cr = context.contentResolver ?: return ERROR_CONTENT_RESOLVER_NOT_FOUND
2021-07-04 00:59:51 +00:00
2021-07-05 18:09:37 +00:00
val currentExistingFile = cr.getExistingDownloadUriOrNullQ(relativePath, displayName) // CURRENT FILE WITH THE SAME PATH
2021-07-04 00:59:51 +00:00
2021-07-05 18:09:37 +00:00
fileLength =
if (currentExistingFile == null || !resume) 0 else cr.openFileDescriptor(currentExistingFile, "r")
.use { it?.statSize ?: 0 } // IF NOT RESUME THEN 0, OTHERWISE THE CURRENT FILE SIZE
if (!resume && currentExistingFile != null) { // DELETE FILE IF FILE EXITS AND NOT RESUME
val rowsDeleted = context.contentResolver.delete(currentExistingFile, null, null)
if (rowsDeleted < 1) {
println("ERROR DELETING FILE!!!")
}
}
val newFileUri = if (resume && currentExistingFile != null) currentExistingFile else {
val contentUri =
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
val newFile = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
put(MediaStore.MediaColumns.TITLE, name)
put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
put(
MediaStore.MediaColumns.RELATIVE_PATH,
relativePath
)
}
cr.insert(
contentUri,
newFile
) ?: return ERROR_MEDIA_STORE_URI_CANT_BE_CREATED
}
2021-07-03 20:59:46 +00:00
2021-07-05 18:09:37 +00:00
fileStream = cr.openOutputStream(newFileUri, "w")
?: return ERROR_CONTENT_RESOLVER_CANT_OPEN_STREAM
} else {
// NORMAL NON SCOPED STORAGE FILE CREATION
val rFile = File(normalPath)
if (!rFile.exists()) {
fileLength = 0
rFile.parentFile?.mkdirs()
if (!rFile.createNewFile()) return ERROR_CREATE_FILE
} else {
if (resume) {
fileLength = rFile.length()
} else {
fileLength = 0
rFile.parentFile?.mkdirs()
if (!rFile.delete()) return ERROR_DELETING_FILE
if (!rFile.createNewFile()) return ERROR_CREATE_FILE
}
}
fileStream = FileOutputStream(rFile, false)
}
if (fileLength == 0L) resume = false
2021-07-03 20:59:46 +00:00
// CONNECT
2021-07-05 18:09:37 +00:00
val connection: URLConnection = URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK
2021-07-03 20:59:46 +00:00
// 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)
2021-07-05 18:09:37 +00:00
if (resume) connection.setRequestProperty("Range", "bytes=${fileLength}-")
val resumeLength = (if (resume) fileLength else 0)
2021-07-03 20:59:46 +00:00
// ON CONNECTION
connection.connect()
val contentLength = connection.contentLength
val bytesTotal = contentLength + resumeLength
2021-07-04 17:00:04 +00:00
if (bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
2021-07-04 00:59:51 +00:00
// 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
2021-07-04 17:00:04 +00:00
// 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
}*/
2021-07-03 20:59:46 +00:00
// READ DATA FROM CONNECTION
val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream)
val buffer = ByteArray(1024)
var count: Int
var bytesDownloaded = resumeLength
2021-07-04 17:00:04 +00:00
var isPaused = false
var isStopped = false
var isDone = false
var isFailed = false
2021-07-04 00:59:51 +00:00
// TO NOT REUSE CODE
2021-07-04 17:00:04 +00:00
fun updateNotification() {
val type = when {
isDone -> DownloadType.IsDone
isStopped -> DownloadType.IsStopped
isFailed -> DownloadType.IsFailed
isPaused -> DownloadType.IsPaused
else -> DownloadType.IsDownloading
}
2021-07-03 20:59:46 +00:00
createNotification(
context,
source,
link.name,
ep,
type,
bytesDownloaded,
bytesTotal
)
}
2021-07-04 17:00:04 +00:00
fun onEvent(event: Pair<Int, DownloadActionType>) {
if (event.first == ep.id) {
when (event.second) {
DownloadActionType.Pause -> {
isPaused = true; updateNotification()
}
DownloadActionType.Stop -> {
isStopped = true; updateNotification()
}
DownloadActionType.Resume -> {
isPaused = false; updateNotification()
}
}
}
}
events += ::onEvent
2021-07-03 20:59:46 +00:00
2021-07-04 17:00:04 +00:00
// UPDATE DOWNLOAD NOTIFICATION
val notificationCoroutine = main {
while (true) {
if (!isPaused) {
updateNotification()
}
for (i in 1..10) {
delay(100)
}
}
2021-07-03 20:59:46 +00:00
}
2021-07-04 17:00:04 +00:00
// THE REAL READ
try {
while (true) {
count = connectionInputStream.read(buffer)
if (count < 0) break
bytesDownloaded += count
while (isPaused) {
sleep(100)
if (isStopped) {
break
}
}
if (isStopped) {
break
}
fileStream.write(buffer, 0, count)
}
} catch (e: Exception) {
isFailed = true
updateNotification()
}
// REMOVE AND EXIT ALL
events -= ::onEvent
2021-07-03 20:59:46 +00:00
fileStream.closeStream()
connectionInputStream.closeStream()
2021-07-04 17:00:04 +00:00
notificationCoroutine.cancel()
2021-07-03 20:59:46 +00:00
2021-07-04 17:00:04 +00:00
// RETURN MESSAGE
return when {
isFailed -> {
ERROR_CONNECTION_ERROR
}
isStopped -> {
2021-07-05 18:09:37 +00:00
deleteFile()
2021-07-04 17:00:04 +00:00
}
else -> {
isDone = true
updateNotification()
SUCCESS_DOWNLOAD_DONE
}
}
2021-06-29 23:14:48 +00:00
}
2021-07-04 17:00:04 +00:00
private fun downloadCheck(context: Context) {
if (currentDownloads < maxConcurrentDownloads && downloadQueue.size > 0) {
2021-07-05 18:09:37 +00:00
val pkg = downloadQueue.removeFirst()
val item = pkg.item
2021-07-04 17:00:04 +00:00
currentDownloads++
try {
main {
2021-07-05 18:09:37 +00:00
for (index in (pkg.linkIndex ?: 0) until item.links.size) {
val link = item.links[index]
val resume = pkg.linkIndex == index
context.setKey(KEY_RESUME_STORAGE, item.ep.id.toString(), DownloadResumePackage(item, index))
2021-07-04 17:00:04 +00:00
val connectionResult = withContext(Dispatchers.IO) {
normalSafeApiCall {
2021-07-05 18:09:37 +00:00
downloadSingleEpisode(context, item.source, item.folder, item.ep, link, resume)
2021-07-04 17:00:04 +00:00
}
}
if (connectionResult != null && connectionResult > 0) { // SUCCESS
2021-07-05 18:09:37 +00:00
context.removeKey(KEY_RESUME_STORAGE, item.ep.id.toString())
2021-07-04 17:00:04 +00:00
break
}
}
}
} catch (e: Exception) {
logError(e)
} finally {
currentDownloads--
downloadCheck(context)
}
}
}
2021-07-05 18:09:37 +00:00
fun downloadFromResume(context: Context, pkg: DownloadResumePackage) {
downloadQueue.addLast(pkg)
downloadCheck(context)
}
2021-07-04 17:00:04 +00:00
fun downloadEpisode(
2021-07-04 00:59:51 +00:00
context: Context,
source: String,
folder: String?,
ep: DownloadEpisodeMetadata,
links: List<ExtractorLink>
) {
2021-06-29 23:14:48 +00:00
val validLinks = links.filter { !it.isM3u8 }
if (validLinks.isNotEmpty()) {
2021-07-05 18:09:37 +00:00
downloadFromResume(context, DownloadResumePackage(DownloadItem(source, folder, ep, validLinks), null))
2021-06-29 23:14:48 +00:00
}
}
}