mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
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>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.VideoDownloadService"
|
||||||
|
android:enabled="true"
|
||||||
|
android:exported="false">
|
||||||
|
</service>
|
||||||
|
|
||||||
<activity android:name=".ui.ControllerActivity">
|
<activity android:name=".ui.ControllerActivity">
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,24 @@
|
||||||
package com.lagradost.cloudstream3
|
package com.lagradost.cloudstream3
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
import android.app.PictureInPictureParams
|
import android.app.PictureInPictureParams
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.navigation.findNavController
|
import androidx.navigation.findNavController
|
||||||
import androidx.navigation.ui.AppBarConfiguration
|
import androidx.navigation.ui.AppBarConfiguration
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
import com.anggrayudi.storage.SimpleStorage
|
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.ApplicationMetadata
|
||||||
import com.google.android.gms.cast.Cast
|
import com.google.android.gms.cast.Cast
|
||||||
import com.google.android.gms.cast.LaunchOptions
|
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.CastSession
|
||||||
import com.google.android.gms.cast.framework.SessionManagerListener
|
import com.google.android.gms.cast.framework.SessionManagerListener
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
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.checkWrite
|
||||||
import com.lagradost.cloudstream3.UIHelper.hasPIPPermission
|
import com.lagradost.cloudstream3.UIHelper.hasPIPPermission
|
||||||
import com.lagradost.cloudstream3.UIHelper.requestRW
|
import com.lagradost.cloudstream3.UIHelper.requestRW
|
||||||
|
@ -118,7 +129,10 @@ class MainActivity : AppCompatActivity() {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
mainContext = this
|
mainContext = this
|
||||||
setupSimpleStorage()
|
setupSimpleStorage()
|
||||||
storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS)
|
|
||||||
|
if(!storage.isStorageAccessGranted(StorageId.PRIMARY)) {
|
||||||
|
storage.requestStorageAccess(REQUEST_CODE_STORAGE_ACCESS)
|
||||||
|
}
|
||||||
|
|
||||||
setContentView(R.layout.activity_main)
|
setContentView(R.layout.activity_main)
|
||||||
val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
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.ExtractorLink
|
||||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager
|
||||||
|
import com.lagradost.cloudstream3.utils.VideoDownloadManager.sanitizeFilename
|
||||||
import jp.wasabeef.glide.transformations.BlurTransformation
|
import jp.wasabeef.glide.transformations.BlurTransformation
|
||||||
import kotlinx.android.synthetic.main.fragment_result.*
|
import kotlinx.android.synthetic.main.fragment_result.*
|
||||||
|
|
||||||
|
@ -343,10 +344,10 @@ class ResultFragment : Fragment() {
|
||||||
viewModel.loadEpisode(episodeClick.data, true) { data ->
|
viewModel.loadEpisode(episodeClick.data, true) { data ->
|
||||||
if (data is Resource.Success) {
|
if (data is Resource.Success) {
|
||||||
val isMovie = currentIsMovie ?: return@loadEpisode
|
val isMovie = currentIsMovie ?: return@loadEpisode
|
||||||
val titleName = currentHeaderName?: return@loadEpisode
|
val titleName = sanitizeFilename(currentHeaderName ?: return@loadEpisode)
|
||||||
val meta = VideoDownloadManager.DownloadEpisodeMetadata(
|
val meta = VideoDownloadManager.DownloadEpisodeMetadata(
|
||||||
episodeClick.data.id,
|
episodeClick.data.id,
|
||||||
titleName ,
|
titleName,
|
||||||
apiName ?: return@loadEpisode,
|
apiName ?: return@loadEpisode,
|
||||||
episodeClick.data.poster ?: currentPoster,
|
episodeClick.data.poster ?: currentPoster,
|
||||||
episodeClick.data.name,
|
episodeClick.data.name,
|
||||||
|
@ -362,7 +363,7 @@ class ResultFragment : Fragment() {
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
VideoDownloadManager.DownloadEpisode(
|
VideoDownloadManager.downloadEpisode(
|
||||||
requireContext(),
|
requireContext(),
|
||||||
tempUrl,
|
tempUrl,
|
||||||
folder,
|
folder,
|
||||||
|
|
|
@ -2,11 +2,12 @@ package com.lagradost.cloudstream3.utils
|
||||||
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
object Coroutines {
|
object Coroutines {
|
||||||
fun main(work: suspend (() -> Unit)) {
|
fun main(work: suspend (() -> Unit)) : Job {
|
||||||
CoroutineScope(Dispatchers.Main).launch {
|
return CoroutineScope(Dispatchers.Main).launch {
|
||||||
work()
|
work()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,25 +7,33 @@ import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.anggrayudi.storage.extension.closeStream
|
import com.anggrayudi.storage.extension.closeStream
|
||||||
import com.anggrayudi.storage.file.DocumentFileCompat
|
import com.anggrayudi.storage.file.DocumentFileCompat
|
||||||
|
import com.anggrayudi.storage.file.forceDelete
|
||||||
import com.anggrayudi.storage.file.openOutputStream
|
import com.anggrayudi.storage.file.openOutputStream
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.google.android.exoplayer2.offline.DownloadService
|
|
||||||
import com.lagradost.cloudstream3.MainActivity
|
import com.lagradost.cloudstream3.MainActivity
|
||||||
import com.lagradost.cloudstream3.R
|
import com.lagradost.cloudstream3.R
|
||||||
import com.lagradost.cloudstream3.UIHelper.colorFromAttribute
|
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 com.lagradost.cloudstream3.utils.Coroutines.main
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import java.lang.Thread.sleep
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.net.URLConnection
|
import java.net.URLConnection
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
const val CHANNEL_ID = "cloudstream3.general"
|
const val CHANNEL_ID = "cloudstream3.general"
|
||||||
const val CHANNEL_NAME = "Downloads"
|
const val CHANNEL_NAME = "Downloads"
|
||||||
|
@ -33,6 +41,7 @@ const val CHANNEL_DESCRIPT = "The download notification channel"
|
||||||
|
|
||||||
object VideoDownloadManager {
|
object VideoDownloadManager {
|
||||||
var maxConcurrentDownloads = 3
|
var maxConcurrentDownloads = 3
|
||||||
|
var currentDownloads = 0
|
||||||
|
|
||||||
private const val USER_AGENT =
|
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"
|
"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?
|
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 var hasCreatedNotChanel = false
|
||||||
private fun Context.createNotificationChannel() {
|
private fun Context.createNotificationChannel() {
|
||||||
hasCreatedNotChanel = true
|
hasCreatedNotChanel = true
|
||||||
|
@ -129,63 +158,80 @@ object VideoDownloadManager {
|
||||||
progress: Long,
|
progress: Long,
|
||||||
total: Long,
|
total: Long,
|
||||||
) {
|
) {
|
||||||
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
main { // DON'T WANT TO SLOW IT DOWN
|
||||||
.setAutoCancel(true)
|
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
.setColorized(true)
|
.setAutoCancel(true)
|
||||||
.setOnlyAlertOnce(true)
|
.setColorized(true)
|
||||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
.setOnlyAlertOnce(true)
|
||||||
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
|
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||||
.setContentTitle(ep.mainName)
|
.setColor(context.colorFromAttribute(R.attr.colorPrimary))
|
||||||
.setSmallIcon(
|
.setContentTitle(ep.mainName)
|
||||||
when (state) {
|
.setSmallIcon(
|
||||||
DownloadType.IsDone -> imgDone
|
when (state) {
|
||||||
DownloadType.IsDownloading -> imgDownloading
|
DownloadType.IsDone -> imgDone
|
||||||
DownloadType.IsPaused -> imgPaused
|
DownloadType.IsDownloading -> imgDownloading
|
||||||
DownloadType.IsFailed -> imgError
|
DownloadType.IsPaused -> imgPaused
|
||||||
DownloadType.IsStopped -> imgStopped
|
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
|
||||||
}
|
}
|
||||||
)
|
val pendingIntent: PendingIntent = PendingIntent.getActivity(context, 0, intent, 0)
|
||||||
|
builder.setContentIntent(pendingIntent)
|
||||||
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 progressPercentage = progress * 100 / total
|
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
|
||||||
val progressMbString = "%.1f".format(progress / 1000000f)
|
builder.setProgress(total.toInt(), progress.toInt(), false)
|
||||||
val totalMbString = "%.1f".format(total / 1000000f)
|
}
|
||||||
|
|
||||||
val bigText =
|
val rowTwoExtra = if (ep.name != null) " - ${ep.name}\n" else ""
|
||||||
if (state == DownloadType.IsDownloading || state == DownloadType.IsPaused) {
|
val rowTwo = if (ep.season != null && ep.episode != null) {
|
||||||
(if (linkName == null) "" else "$linkName\n") + "$rowTwo\n$progressPercentage % ($progressMbString MB/$totalMbString MB)"
|
"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) {
|
} else if (state == DownloadType.IsFailed) {
|
||||||
"Download Failed - $rowTwo"
|
"Download Failed - $rowTwo"
|
||||||
} else if (state == DownloadType.IsDone) {
|
} else if (state == DownloadType.IsDone) {
|
||||||
|
@ -194,103 +240,99 @@ object VideoDownloadManager {
|
||||||
"Download Stopped - $rowTwo"
|
"Download Stopped - $rowTwo"
|
||||||
}
|
}
|
||||||
|
|
||||||
val bodyStyle = NotificationCompat.BigTextStyle()
|
builder.setContentText(txt)
|
||||||
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)
|
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) {
|
if (state == DownloadType.IsPaused) {
|
||||||
val actionTypes: MutableList<DownloadActionType> = ArrayList()
|
actionTypes.add(DownloadActionType.Resume)
|
||||||
// INIT
|
actionTypes.add(DownloadActionType.Stop)
|
||||||
if (state == DownloadType.IsDownloading) {
|
}
|
||||||
actionTypes.add(DownloadActionType.Pause)
|
|
||||||
actionTypes.add(DownloadActionType.Stop)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state == DownloadType.IsPaused) {
|
// ADD ACTIONS
|
||||||
actionTypes.add(DownloadActionType.Resume)
|
for ((index, i) in actionTypes.withIndex()) {
|
||||||
actionTypes.add(DownloadActionType.Stop)
|
val actionResultIntent = Intent(context, VideoDownloadService::class.java)
|
||||||
}
|
|
||||||
|
|
||||||
// ADD ACTIONS
|
actionResultIntent.putExtra(
|
||||||
for ((index, i) in actionTypes.withIndex()) {
|
"type", when (i) {
|
||||||
val actionResultIntent = Intent(context, DownloadService::class.java)
|
DownloadActionType.Resume -> "resume"
|
||||||
|
DownloadActionType.Pause -> "pause"
|
||||||
actionResultIntent.putExtra(
|
DownloadActionType.Stop -> "stop"
|
||||||
"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("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) {
|
if (!hasCreatedNotChanel) {
|
||||||
context.createNotificationChannel()
|
context.createNotificationChannel()
|
||||||
}
|
}
|
||||||
|
|
||||||
with(NotificationManagerCompat.from(context)) {
|
with(NotificationManagerCompat.from(context)) {
|
||||||
// notificationId is a unique int for each notification that you must define
|
// notificationId is a unique int for each notification that you must define
|
||||||
notify(ep.id, builder.build())
|
notify(ep.id, builder.build())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private const val reservedChars = "|\\?*<\":>+[]/'"
|
private const val reservedChars = "|\\?*<\":>+[]/\'"
|
||||||
private fun sanitizeFilename(name: String): String {
|
fun sanitizeFilename(name: String): String {
|
||||||
var tempName = name
|
var tempName = name
|
||||||
for (c in reservedChars) {
|
for (c in reservedChars) {
|
||||||
tempName = tempName.replace(c, ' ')
|
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,
|
context: Context,
|
||||||
source: String?,
|
source: String?,
|
||||||
folder: String?,
|
folder: String?,
|
||||||
ep: DownloadEpisodeMetadata,
|
ep: DownloadEpisodeMetadata,
|
||||||
link: ExtractorLink
|
link: ExtractorLink
|
||||||
): Boolean {
|
): Int {
|
||||||
val name = (ep.name ?: "Episode ${ep.episode}")
|
val name = sanitizeFilename(ep.name ?: "Episode ${ep.episode}")
|
||||||
val path = sanitizeFilename("Download/${if (folder == null) "" else "$folder/"}$name")
|
val path = "${Environment.DIRECTORY_DOWNLOADS}/${if (folder == null) "" else "$folder/"}$name.mp4"
|
||||||
var resume = false
|
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
|
// IF NOT RESUME CREATE FILE
|
||||||
val tempFile = DocumentFileCompat.fromSimplePath(context, basePath = path)
|
val tempFile = DocumentFileCompat.fromSimplePath(context, basePath = path)
|
||||||
val fileExists = tempFile?.exists() ?: false
|
val fileExists = tempFile?.exists() ?: false
|
||||||
|
@ -298,7 +340,7 @@ object VideoDownloadManager {
|
||||||
if (!fileExists) resume = false
|
if (!fileExists) resume = false
|
||||||
if (fileExists && !resume) {
|
if (fileExists && !resume) {
|
||||||
if (tempFile?.delete() == false) { // DELETE FAILED ON RESUME FILE
|
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
|
// END OF FILE CREATION
|
||||||
|
|
||||||
if (dFile == null) {
|
if (dFile == null || !dFile.exists()) {
|
||||||
println("FUCK YOU")
|
return ERROR_FILE_NOT_FOUND
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OPEN FILE
|
// OPEN FILE
|
||||||
val fileStream = dFile.openOutputStream(context, resume) ?: return false
|
val fileStream = dFile.openOutputStream(context, resume) ?: return ERROR_OPEN_FILE
|
||||||
|
|
||||||
// CONNECT
|
// CONNECT
|
||||||
val connection: URLConnection = URL(link.url).openConnection()
|
val connection: URLConnection = URL(link.url).openConnection()
|
||||||
|
@ -331,15 +372,16 @@ object VideoDownloadManager {
|
||||||
connection.connect()
|
connection.connect()
|
||||||
val contentLength = connection.contentLength
|
val contentLength = connection.contentLength
|
||||||
val bytesTotal = contentLength + resumeLength
|
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,
|
// Could use connection.contentType for mime types when creating the file,
|
||||||
// however file is already created and players don't go of file type
|
// 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
|
// https://stackoverflow.com/questions/23714383/what-are-all-the-possible-values-for-http-content-type-header
|
||||||
if(!connection.contentType.isNullOrEmpty() && !connection.contentType.startsWith("video")) {
|
// might receive application/octet-stream
|
||||||
return false // CONTENT IS NOT VIDEO, SHOULD NEVER HAPPENED, BUT JUST IN CASE
|
/*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
|
// READ DATA FROM CONNECTION
|
||||||
val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream)
|
val connectionInputStream: InputStream = BufferedInputStream(connection.inputStream)
|
||||||
|
@ -347,8 +389,21 @@ object VideoDownloadManager {
|
||||||
var count: Int
|
var count: Int
|
||||||
var bytesDownloaded = resumeLength
|
var bytesDownloaded = resumeLength
|
||||||
|
|
||||||
|
|
||||||
|
var isPaused = false
|
||||||
|
var isStopped = false
|
||||||
|
var isDone = false
|
||||||
|
var isFailed = false
|
||||||
|
|
||||||
// TO NOT REUSE CODE
|
// 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(
|
createNotification(
|
||||||
context,
|
context,
|
||||||
source,
|
source,
|
||||||
|
@ -360,24 +415,108 @@ object VideoDownloadManager {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) { // TODO PAUSE
|
fun onEvent(event: Pair<Int, DownloadActionType>) {
|
||||||
count = connectionInputStream.read(buffer)
|
if (event.first == ep.id) {
|
||||||
if (count < 0) break
|
when (event.second) {
|
||||||
bytesDownloaded += count
|
DownloadActionType.Pause -> {
|
||||||
|
isPaused = true; updateNotification()
|
||||||
updateNotification(DownloadType.IsDownloading)
|
}
|
||||||
fileStream.write(buffer, 0, count)
|
DownloadActionType.Stop -> {
|
||||||
|
isStopped = true; updateNotification()
|
||||||
|
}
|
||||||
|
DownloadActionType.Resume -> {
|
||||||
|
isPaused = false; updateNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DOWNLOAD EXITED CORRECTLY
|
events += ::onEvent
|
||||||
updateNotification(DownloadType.IsDone)
|
|
||||||
|
// 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()
|
fileStream.closeStream()
|
||||||
connectionInputStream.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,
|
context: Context,
|
||||||
source: String,
|
source: String,
|
||||||
folder: String?,
|
folder: String?,
|
||||||
|
@ -386,16 +525,8 @@ object VideoDownloadManager {
|
||||||
) {
|
) {
|
||||||
val validLinks = links.filter { !it.isM3u8 }
|
val validLinks = links.filter { !it.isM3u8 }
|
||||||
if (validLinks.isNotEmpty()) {
|
if (validLinks.isNotEmpty()) {
|
||||||
try {
|
downloadQueue.addLast(DownloadItem(source, folder, ep, validLinks))
|
||||||
main {
|
downloadCheck(context)
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
DownloadSingleEpisode(context, source, folder, ep, validLinks.first())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
println(e)
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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="result_go_back">Go Back</string>
|
||||||
<string name="episode_poster">Episode Poster</string>
|
<string name="episode_poster">Episode Poster</string>
|
||||||
<string name="play_episode">Play Episode</string>
|
<string name="play_episode">Play Episode</string>
|
||||||
|
<string name="need_storage">Allow to download episodes</string>
|
||||||
</resources>
|
</resources>
|
Loading…
Reference in a new issue