diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fae7bb08..6d504cc1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -33,6 +33,12 @@ + + + diff --git a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt index 1cf5388e..912bcc1c 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/MainActivity.kt @@ -1,16 +1,24 @@ package com.lagradost.cloudstream3 +import android.Manifest import android.app.PictureInPictureParams import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Build import android.os.Bundle +import android.widget.Toast import androidx.appcompat.app.AppCompatActivity +import androidx.documentfile.provider.DocumentFile import androidx.navigation.findNavController import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.setupWithNavController import com.anggrayudi.storage.SimpleStorage +import com.anggrayudi.storage.callback.StorageAccessCallback +import com.anggrayudi.storage.file.StorageId +import com.anggrayudi.storage.file.StorageType +import com.anggrayudi.storage.file.getStorageId import com.google.android.gms.cast.ApplicationMetadata import com.google.android.gms.cast.Cast import com.google.android.gms.cast.LaunchOptions @@ -19,6 +27,9 @@ import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.cast.framework.CastSession import com.google.android.gms.cast.framework.SessionManagerListener import com.google.android.material.bottomnavigation.BottomNavigationView +import com.karumi.dexter.Dexter +import com.karumi.dexter.MultiplePermissionsReport +import com.karumi.dexter.listener.multi.BaseMultiplePermissionsListener import com.lagradost.cloudstream3.UIHelper.checkWrite import com.lagradost.cloudstream3.UIHelper.hasPIPPermission import com.lagradost.cloudstream3.UIHelper.requestRW @@ -118,7 +129,10 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) mainContext = this setupSimpleStorage() - storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS) + + if(!storage.isStorageAccessGranted(StorageId.PRIMARY)) { + storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS) + } setContentView(R.layout.activity_main) val navView: BottomNavigationView = findViewById(R.id.nav_view) diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt b/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt new file mode 100644 index 00000000..84ad304c --- /dev/null +++ b/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt @@ -0,0 +1,23 @@ +package com.lagradost.cloudstream3.services + +import android.app.IntentService +import android.content.Intent +import com.lagradost.cloudstream3.utils.VideoDownloadManager + +class VideoDownloadService : IntentService("VideoDownloadService") { + override fun onHandleIntent(intent: Intent?) { + if (intent != null) { + val id = intent.getIntExtra("id", -1) + val type = intent.getStringExtra("type") + if (id != -1 && type != null) { + val state = when (type) { + "resume" -> VideoDownloadManager.DownloadActionType.Resume + "pause" -> VideoDownloadManager.DownloadActionType.Pause + "stop" -> VideoDownloadManager.DownloadActionType.Stop + else -> return + } + VideoDownloadManager.events.invoke(Pair(id, state)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt index 7867c6b7..525f18f9 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/ui/result/ResultFragment.kt @@ -48,6 +48,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos import com.lagradost.cloudstream3.utils.ExtractorLink import com.lagradost.cloudstream3.utils.VideoDownloadManager +import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename import jp.wasabeef.glide.transformations.BlurTransformation import kotlinx.android.synthetic.main.fragment_result.* @@ -343,10 +344,10 @@ class ResultFragment : Fragment() { viewModel.loadEpisode(episodeClick.data, true) { data -> if (data is Resource.Success) { val isMovie = currentIsMovie ?: return@loadEpisode - val titleName = currentHeaderName?: return@loadEpisode + val titleName = sanitizeFilename(currentHeaderName ?: return@loadEpisode) val meta = VideoDownloadManager.DownloadEpisodeMetadata( episodeClick.data.id, - titleName , + titleName, apiName ?: return@loadEpisode, episodeClick.data.poster ?: currentPoster, episodeClick.data.name, @@ -362,7 +363,7 @@ class ResultFragment : Fragment() { else -> null } - VideoDownloadManager.DownloadEpisode( + VideoDownloadManager.downloadEpisode( requireContext(), tempUrl, folder, diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt index 6bfbc1a3..e1b055e8 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/Coroutines.kt @@ -2,11 +2,12 @@ package com.lagradost.cloudstream3.utils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.launch object Coroutines { - fun main(work: suspend (() -> Unit)) { - CoroutineScope(Dispatchers.Main).launch { + fun main(work: suspend (() -> Unit)) : Job { + return CoroutineScope(Dispatchers.Main).launch { work() } } diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt index 8651ca9d..80acec78 100644 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt +++ b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadManager.kt @@ -7,25 +7,33 @@ import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.os.Build +import android.os.Environment import androidx.annotation.DrawableRes import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.net.toUri import com.anggrayudi.storage.extension.closeStream import com.anggrayudi.storage.file.DocumentFileCompat +import com.anggrayudi.storage.file.forceDelete import com.anggrayudi.storage.file.openOutputStream import com.bumptech.glide.Glide -import com.google.android.exoplayer2.offline.DownloadService import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.UIHelper.colorFromAttribute +import com.lagradost.cloudstream3.mvvm.logError +import com.lagradost.cloudstream3.mvvm.normalSafeApiCall +import com.lagradost.cloudstream3.services.VideoDownloadService import com.lagradost.cloudstream3.utils.Coroutines.main import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import java.io.BufferedInputStream import java.io.InputStream +import java.lang.Thread.sleep import java.net.URL import java.net.URLConnection +import java.util.* +import kotlin.collections.ArrayList const val CHANNEL_ID = "cloudstream3.general" const val CHANNEL_NAME = "Downloads" @@ -33,6 +41,7 @@ const val CHANNEL_DESCRIPT = "The download notification channel" object VideoDownloadManager { var maxConcurrentDownloads = 3 + var currentDownloads = 0 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" @@ -85,6 +94,26 @@ object VideoDownloadManager { val episode: Int? ) + data class DownloadItem( + val source: String, + val folder: String?, + val ep: DownloadEpisodeMetadata, + val links: List + ) + + private const val SUCCESS_DOWNLOAD_DONE = 1 + private const val SUCCESS_STOPPED = 2 + private const val ERROR_DELETING_FILE = -1 + private const val ERROR_FILE_NOT_FOUND = -2 + 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 + + val events = Event>() + private val downloadQueue = LinkedList() + + private var hasCreatedNotChanel = false private fun Context.createNotificationChannel() { hasCreatedNotChanel = true @@ -129,63 +158,80 @@ object VideoDownloadManager { progress: Long, total: Long, ) { - 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 + 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 + } + ) + + if (ep.sourceApiName != null) { + builder.setSubText(ep.sourceApiName) + } + + 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 } - ) - - if (ep.sourceApiName != null) { - builder.setSubText(ep.sourceApiName) - } - - 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) - } - - if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - builder.setProgress(total.toInt(), progress.toInt(), false) - } - - 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 ?: "") + "" - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (ep.poster != null) { - val poster = context.getImageBitmapFromUrl(ep.poster) - if (poster != null) - builder.setLargeIcon(poster) + val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0) + builder.setContentIntent(pendingIntent) } - val progressPercentage = progress * 100 / total - val progressMbString = "%.1f".format(progress / 1000000f) - val totalMbString = "%.1f".format(total / 1000000f) + if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { + builder.setProgress(total.toInt(), progress.toInt(), false) + } - val bigText = - if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - (if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString MB/$totalMbString MB)" + 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 ?: "") + "" + } + + 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) + } + + 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 } else if (state == DownloadType.IsFailed) { "Download Failed - $rowTwo" } else if (state == DownloadType.IsDone) { @@ -194,103 +240,99 @@ object VideoDownloadManager { "Download Stopped - $rowTwo" } - val bodyStyle = NotificationCompat.BigTextStyle() - bodyStyle.bigText(bigText) - builder.setStyle(bodyStyle) - } else { - val txt = if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) { - rowTwo - } else if (state == DownloadType.IsFailed) { - "Download Failed - $rowTwo" - } else if (state == DownloadType.IsDone) { - "Download Done - $rowTwo" - } else { - "Download Stopped - $rowTwo" + builder.setContentText(txt) } - builder.setContentText(txt) - } + if ((state == DownloadType.IsDownloading || state == DownloadType.IsPaused) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val actionTypes: MutableList = ArrayList() + // INIT + if (state == DownloadType.IsDownloading) { + actionTypes.add(DownloadActionType.Pause) + actionTypes.add(DownloadActionType.Stop) + } - if ((state == DownloadType.IsDownloading || state == DownloadType.IsPaused) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val actionTypes: MutableList = ArrayList() - // INIT - if (state == DownloadType.IsDownloading) { - actionTypes.add(DownloadActionType.Pause) - actionTypes.add(DownloadActionType.Stop) - } + if (state == DownloadType.IsPaused) { + actionTypes.add(DownloadActionType.Resume) + actionTypes.add(DownloadActionType.Stop) + } - if (state == DownloadType.IsPaused) { - actionTypes.add(DownloadActionType.Resume) - actionTypes.add(DownloadActionType.Stop) - } + // ADD ACTIONS + for ((index, i) in actionTypes.withIndex()) { + val actionResultIntent = Intent(context, VideoDownloadService::class.java) - // ADD ACTIONS - for ((index, i) in actionTypes.withIndex()) { - val actionResultIntent = Intent(context, DownloadService::class.java) - - actionResultIntent.putExtra( - "type", when (i) { - DownloadActionType.Resume -> "resume" - DownloadActionType.Pause -> "pause" - DownloadActionType.Stop -> "stop" - } - ) - - actionResultIntent.putExtra("id", ep.id) - - val pending: PendingIntent = PendingIntent.getService( - context, 4337 + index + ep.id, - actionResultIntent, - PendingIntent.FLAG_UPDATE_CURRENT - ) - - 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 + actionResultIntent.putExtra( + "type", when (i) { + DownloadActionType.Resume -> "resume" + DownloadActionType.Pause -> "pause" + DownloadActionType.Stop -> "stop" + } ) - ) + + actionResultIntent.putExtra("id", ep.id) + + val pending: PendingIntent = PendingIntent.getService( + context, 4337 + index + ep.id, + actionResultIntent, + PendingIntent.FLAG_UPDATE_CURRENT + ) + + 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 + ) + ) + } } - } - if (!hasCreatedNotChanel) { - context.createNotificationChannel() - } + if (!hasCreatedNotChanel) { + context.createNotificationChannel() + } - with(NotificationManagerCompat.from(context)) { - // notificationId is a unique int for each notification that you must define - notify(ep.id, builder.build()) + with(NotificationManagerCompat.from(context)) { + // notificationId is a unique int for each notification that you must define + notify(ep.id, builder.build()) + } } } - private const val reservedChars = "|\\?*<\":>+[]/'" - private fun sanitizeFilename(name: String): String { + private const val reservedChars = "|\\?*<\":>+[]/\'" + fun sanitizeFilename(name: String): String { var tempName = name for (c in reservedChars) { tempName = tempName.replace(c, ' ') } - return tempName.replace(" ", " ") + return tempName.replace(" ", " ").trim(' ') } - fun DownloadSingleEpisode( + private const val reservedCharsPath = "|\\?*<\":>+[]\'" + fun sanitizePath(name: String): String { + var tempName = name + for (c in reservedCharsPath) { + tempName = tempName.replace(c, ' ') + } + return tempName.replace(" ", " ").trim(' ') + } + + private fun downloadSingleEpisode( context: Context, source: String?, folder: String?, ep: DownloadEpisodeMetadata, link: ExtractorLink - ): Boolean { - val name = (ep.name ?: "Episode ${ep.episode}") - val path = sanitizeFilename("Download/${if (folder == null) "" else "$folder/"}$name") + ): Int { + val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}") + val path = "${Environment.DIRECTORY_DOWNLOADS}/${if (folder == null) "" else "$folder/"}$name.mp4" var resume = false - // IF RESUME, DELETE FILE IF FOUND AND RECREATE + // IF RESUME, DON'T DELETE FILE, CONTINUE, RECREATE IF NOT FOUND // IF NOT RESUME CREATE FILE val tempFile = DocumentFileCompat.fromSimplePath(context, basePath = path) val fileExists = tempFile?.exists() ?: false @@ -298,7 +340,7 @@ object VideoDownloadManager { if (!fileExists) resume = false if (fileExists && !resume) { if (tempFile?.delete() == false) { // DELETE FAILED ON RESUME FILE - return false + return ERROR_DELETING_FILE } } @@ -308,13 +350,12 @@ object VideoDownloadManager { // END OF FILE CREATION - if (dFile == null) { - println("FUCK YOU") - return false + if (dFile == null || !dFile.exists()) { + return ERROR_FILE_NOT_FOUND } // OPEN FILE - val fileStream = dFile.openOutputStream(context, resume) ?: return false + val fileStream = dFile.openOutputStream(context, resume) ?: return ERROR_OPEN_FILE // CONNECT val connection: URLConnection = URL(link.url).openConnection() @@ -331,15 +372,16 @@ object VideoDownloadManager { connection.connect() val contentLength = connection.contentLength val bytesTotal = contentLength + resumeLength - if (bytesTotal < 5000000) return false // DATA IS LESS THAN 5MB, SOMETHING IS WRONG + if (bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG // 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 - if(!connection.contentType.isNullOrEmpty() && !connection.contentType.startsWith("video")) { - return false // CONTENT IS NOT VIDEO, SHOULD NEVER HAPPENED, BUT JUST IN CASE - } + // 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 + }*/ // READ DATA FROM CONNECTION val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream) @@ -347,8 +389,21 @@ object VideoDownloadManager { var count: Int var bytesDownloaded = resumeLength + + var isPaused = false + var isStopped = false + var isDone = false + var isFailed = false + // TO NOT REUSE CODE - fun updateNotification(type: DownloadType) { + fun updateNotification() { + val type = when { + isDone -> DownloadType.IsDone + isStopped -> DownloadType.IsStopped + isFailed -> DownloadType.IsFailed + isPaused -> DownloadType.IsPaused + else -> DownloadType.IsDownloading + } createNotification( context, source, @@ -360,24 +415,108 @@ object VideoDownloadManager { ) } - while (true) { // TODO PAUSE - count = connectionInputStream.read(buffer) - if (count < 0) break - bytesDownloaded += count - - updateNotification(DownloadType.IsDownloading) - fileStream.write(buffer, 0, count) + fun onEvent(event: Pair) { + if (event.first == ep.id) { + when (event.second) { + DownloadActionType.Pause -> { + isPaused = true; updateNotification() + } + DownloadActionType.Stop -> { + isStopped = true; updateNotification() + } + DownloadActionType.Resume -> { + isPaused = false; updateNotification() + } + } + } } - // DOWNLOAD EXITED CORRECTLY - updateNotification(DownloadType.IsDone) + events += ::onEvent + + // UPDATE DOWNLOAD NOTIFICATION + val notificationCoroutine = main { + while (true) { + if (!isPaused) { + updateNotification() + } + for (i in 1..10) { + delay(100) + } + } + } + + // THE REAL READ + try { + while (true) { + count = connectionInputStream.read(buffer) + if (count < 0) break + bytesDownloaded += count + 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 fileStream.closeStream() connectionInputStream.closeStream() + notificationCoroutine.cancel() - return true + // RETURN MESSAGE + return when { + isFailed -> { + ERROR_CONNECTION_ERROR + } + isStopped -> { + dFile.delete() + SUCCESS_STOPPED + } + else -> { + isDone = true + updateNotification() + SUCCESS_DOWNLOAD_DONE + } + } } - public fun DownloadEpisode( + private fun downloadCheck(context: Context) { + if (currentDownloads < maxConcurrentDownloads && downloadQueue.size > 0) { + val item = downloadQueue.removeFirst() + currentDownloads++ + try { + main { + for (link in item.links) { + val connectionResult = withContext(Dispatchers.IO) { + normalSafeApiCall { + downloadSingleEpisode(context, item.source, item.folder, item.ep, link) + } + } + if (connectionResult != null && connectionResult > 0) { // SUCCESS + break + } + } + } + } catch (e: Exception) { + logError(e) + } finally { + currentDownloads-- + downloadCheck(context) + } + } + } + + fun downloadEpisode( context: Context, source: String, folder: String?, @@ -386,16 +525,8 @@ object VideoDownloadManager { ) { val validLinks = links.filter { !it.isM3u8 } if (validLinks.isNotEmpty()) { - try { - main { - withContext(Dispatchers.IO) { - DownloadSingleEpisode(context, source, folder, ep, validLinks.first()) - } - } - } catch (e: Exception) { - println(e) - e.printStackTrace() - } + downloadQueue.addLast(DownloadItem(source, folder, ep, validLinks)) + downloadCheck(context) } } } \ No newline at end of file diff --git a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadService.kt b/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadService.kt deleted file mode 100644 index a365313f..00000000 --- a/app/src/main/java/com/lagradost/cloudstream3/utils/VideoDownloadService.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.lagradost.cloudstream3.utils - -import android.app.IntentService -import android.content.Intent - -class VideoDownloadService : IntentService("DownloadService") { - override fun onHandleIntent(intent: Intent?) { - if (intent != null) { - val id = intent.getIntExtra("id", -1) - val type = intent.getStringExtra("type") - if (id != -1 && type != null) { - - } - } - } -} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a8d0f6eb..43bc938b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,4 +37,5 @@ Go Back Episode Poster Play Episode + Allow to download episodes \ No newline at end of file