forked from recloudstream/cloudstream
downloads not working!!
This commit is contained in:
parent
3210e71eca
commit
728874fc03
8 changed files with 340 additions and 179 deletions
|
@ -33,6 +33,12 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<service
|
||||
android:name=".services.VideoDownloadService"
|
||||
android:enabled="true"
|
||||
android:exported="false">
|
||||
</service>
|
||||
|
||||
<activity android:name=".ui.ControllerActivity">
|
||||
</activity>
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ExtractorLink>
|
||||
)
|
||||
|
||||
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<Pair<Int, DownloadActionType>>()
|
||||
private val downloadQueue = LinkedList<DownloadItem>()
|
||||
|
||||
|
||||
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<DownloadActionType> = 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<DownloadActionType> = 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<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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -37,4 +37,5 @@
|
|||
<string name="result_go_back">Go Back</string>
|
||||
<string name="episode_poster">Episode Poster</string>
|
||||
<string name="play_episode">Play Episode</string>
|
||||
<string name="need_storage">Allow to download episodes</string>
|
||||
</resources>
|
Loading…
Reference in a new issue