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