fixed background downloads

merging this fucking shit directly to master because git has been assfucking for an hour now
This commit is contained in:
Blatzar 2021-09-15 00:18:03 +02:00
parent 0adb01e1cf
commit 77cd2cbc8e
5 changed files with 386 additions and 170 deletions

View file

@ -24,13 +24,13 @@ android {
} }
} }
} }
compileSdkVersion 30 compileSdkVersion 31
buildToolsVersion "30.0.3" buildToolsVersion "30.0.3"
defaultConfig { defaultConfig {
applicationId "com.lagradost.cloudstream3" applicationId "com.lagradost.cloudstream3"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 30 targetSdkVersion 31
versionCode 25 versionCode 25
versionName "1.9.9" versionName "1.9.9"
@ -131,4 +131,8 @@ dependencies {
// TorrentStream // TorrentStream
implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0' implementation 'com.github.TorrentStream:TorrentStream-Android:2.7.0'
// Downloading
implementation "androidx.work:work-runtime:2.7.0-beta01"
implementation "androidx.work:work-runtime-ktx:2.7.0-beta01"
} }

View file

@ -56,7 +56,7 @@ object DownloadButtonSetup {
activity?.let { ctx -> activity?.let { ctx ->
val pkg = VideoDownloadManager.getDownloadResumePackage(ctx, id) val pkg = VideoDownloadManager.getDownloadResumePackage(ctx, id)
if (pkg != null) { if (pkg != null) {
VideoDownloadManager.downloadFromResume(ctx, pkg) VideoDownloadManager.downloadFromResumeUsingWorker(ctx, pkg)
} else { } else {
VideoDownloadManager.downloadEvent.invoke( VideoDownloadManager.downloadEvent.invoke(
Pair(click.data.id, VideoDownloadManager.DownloadActionType.Resume) Pair(click.data.id, VideoDownloadManager.DownloadActionType.Resume)

View file

@ -492,8 +492,8 @@ class ResultFragment : Fragment() {
) )
// DOWNLOAD VIDEO // DOWNLOAD VIDEO
VideoDownloadManager.downloadEpisode( VideoDownloadManager.downloadEpisodeUsingWorker(
activity, ctx,
src,//url ?: return, src,//url ?: return,
folder, folder,
meta, meta,

View file

@ -0,0 +1,93 @@
package com.lagradost.cloudstream3.utils
import android.app.Notification
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.lagradost.cloudstream3.utils.Coroutines.main
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.removeKey
import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_INFO
import com.lagradost.cloudstream3.utils.VideoDownloadManager.WORK_KEY_PACKAGE
import com.lagradost.cloudstream3.utils.VideoDownloadManager.createNotification
import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadCheck
import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadEpisode
import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadFromResume
import com.lagradost.cloudstream3.utils.VideoDownloadManager.downloadStatusEvent
import kotlinx.coroutines.delay
const val DOWNLOAD_CHECK = "DownloadCheck"
class DownloadFileWorkManager(val context: Context, val workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val key = workerParams.inputData.getString("key")
try {
println("KEY $key")
if (key == DOWNLOAD_CHECK) {
downloadCheck(applicationContext, ::handleNotification)?.let {
awaitDownload(it)
}
} else if (key != null) {
val info = applicationContext.getKey<VideoDownloadManager.DownloadInfo>(WORK_KEY_INFO, key)
val pkg =
applicationContext.getKey<VideoDownloadManager.DownloadResumePackage>(WORK_KEY_PACKAGE, key)
if (info != null) {
downloadEpisode(
applicationContext,
info.source,
info.folder,
info.ep,
info.links,
::handleNotification
)
awaitDownload(info.ep.id)
} else if (pkg != null) {
downloadFromResume(applicationContext, pkg, ::handleNotification)
awaitDownload(pkg.item.ep.id)
}
removeKeys(key)
}
return Result.success()
} catch (e: Exception) {
if (key != null) {
removeKeys(key)
}
return Result.failure()
}
}
private fun removeKeys(key: String) {
applicationContext.removeKey(WORK_KEY_INFO, key)
applicationContext.removeKey(WORK_KEY_PACKAGE, key)
}
private suspend fun awaitDownload(id: Int) {
var isDone = false
val listener = { data: Pair<Int, VideoDownloadManager.DownloadType> ->
if (id == data.first) {
when (data.second) {
VideoDownloadManager.DownloadType.IsDone, VideoDownloadManager.DownloadType.IsFailed, VideoDownloadManager.DownloadType.IsStopped -> {
isDone = true
}
else -> {
}
}
}
}
downloadStatusEvent += listener
while (!isDone) {
delay(1000)
}
downloadStatusEvent -= listener
}
private fun handleNotification(id: Int, notification: Notification) {
main {
setForegroundAsync(ForegroundInfo(id, notification))
}
}
}

View file

@ -1,6 +1,7 @@
package com.lagradost.cloudstream3.utils package com.lagradost.cloudstream3.utils
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.*
import android.app.Activity import android.app.Activity
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
@ -17,12 +18,18 @@ import androidx.annotation.RequiresApi
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 androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.fasterxml.jackson.annotation.JsonProperty
import com.github.se_bastiaan.torrentstream.StreamStatus import com.github.se_bastiaan.torrentstream.StreamStatus
import com.github.se_bastiaan.torrentstream.Torrent import com.github.se_bastiaan.torrentstream.Torrent
import com.github.se_bastiaan.torrentstream.TorrentOptions import com.github.se_bastiaan.torrentstream.TorrentOptions
import com.github.se_bastiaan.torrentstream.TorrentStream import com.github.se_bastiaan.torrentstream.TorrentStream
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener import com.github.se_bastiaan.torrentstream.listeners.TorrentListener
import com.lagradost.cloudstream3.AnimeLoadResponse
import com.lagradost.cloudstream3.MainActivity import com.lagradost.cloudstream3.MainActivity
import com.lagradost.cloudstream3.MainActivity.Companion.showToast import com.lagradost.cloudstream3.MainActivity.Companion.showToast
import com.lagradost.cloudstream3.R import com.lagradost.cloudstream3.R
@ -217,7 +224,7 @@ object VideoDownloadManager {
} }
} }
private fun createNotification( suspend fun createNotification(
context: Context, context: Context,
source: String?, source: String?,
linkName: String?, linkName: String?,
@ -225,10 +232,12 @@ object VideoDownloadManager {
state: DownloadType, state: DownloadType,
progress: Long, progress: Long,
total: Long, total: Long,
) { notificationCallback: (Int, Notification) -> Unit
if(total <= 0) return // crash, invalid data
main { // DON'T WANT TO SLOW IT DOWN ): Notification? {
if (total <= 0) return null// crash, invalid data
// main { // DON'T WANT TO SLOW IT DOWN
val builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID) val builder = NotificationCompat.Builder(context, DOWNLOAD_CHANNEL_ID)
.setAutoCancel(true) .setAutoCancel(true)
.setColorized(true) .setColorized(true)
@ -368,11 +377,15 @@ object VideoDownloadManager {
context.createNotificationChannel() context.createNotificationChannel()
} }
val notification = builder.build()
notificationCallback(ep.id, notification)
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, notification)
}
} }
return notification
// }
} }
private const val reservedChars = "|\\?*<\":>+[]/\'" private const val reservedChars = "|\\?*<\":>+[]/\'"
@ -678,7 +691,8 @@ object VideoDownloadManager {
createNotificationCallback.invoke( createNotificationCallback.invoke(
CreateNotificationMetadata( CreateNotificationMetadata(
type, type,
lengthSize.first, lengthSize.second lengthSize.first,
lengthSize.second
) )
) )
} }
@ -816,7 +830,8 @@ object VideoDownloadManager {
val fileLength = stream.fileLength!! val fileLength = stream.fileLength!!
// CONNECT // CONNECT
val connection: URLConnection = URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK val connection: URLConnection =
URL(link.url.replace(" ", "%20")).openConnection() // IDK OLD PHONES BE WACK
// SET CONNECTION SETTINGS // SET CONNECTION SETTINGS
connection.connectTimeout = 10000 connection.connectTimeout = 10000
@ -851,7 +866,8 @@ object VideoDownloadManager {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // fuck android if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { // fuck android
connection.contentLengthLong ?: 0L connection.contentLengthLong ?: 0L
} else { } else {
connection.getHeaderField("content-length").toLongOrNull() ?: connection.contentLength?.toLong() ?: 0L connection.getHeaderField("content-length").toLongOrNull() ?: connection.contentLength?.toLong()
?: 0L
} }
} catch (e: Exception) { } catch (e: Exception) {
logError(e) logError(e)
@ -862,7 +878,11 @@ object VideoDownloadManager {
if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG if (extension == "mp4" && bytesTotal < 5000000) return ERROR_TOO_SMALL_CONNECTION // DATA IS LESS THAN 5MB, SOMETHING IS WRONG
parentId?.let { parentId?.let {
context.setKey(KEY_DOWNLOAD_INFO, it.toString(), DownloadedFileInfo(bytesTotal, relativePath, displayName)) context.setKey(
KEY_DOWNLOAD_INFO,
it.toString(),
DownloadedFileInfo(bytesTotal, relativePath, displayName)
)
} }
// Could use connection.contentType for mime types when creating the file, // Could use connection.contentType for mime types when creating the file,
@ -1140,7 +1160,13 @@ object VideoDownloadManager {
try { try {
downloadStatus[id] = type downloadStatus[id] = type
downloadStatusEvent.invoke(Pair(id, type)) downloadStatusEvent.invoke(Pair(id, type))
downloadProgressEvent.invoke(Triple(id, bytesDownloaded, (bytesDownloaded / tsProgress) * totalTs)) downloadProgressEvent.invoke(
Triple(
id,
bytesDownloaded,
(bytesDownloaded / tsProgress) * totalTs
)
)
} catch (e: Exception) { } catch (e: Exception) {
// IDK MIGHT ERROR // IDK MIGHT ERROR
} }
@ -1283,15 +1309,21 @@ object VideoDownloadManager {
folder: String?, folder: String?,
ep: DownloadEpisodeMetadata, ep: DownloadEpisodeMetadata,
link: ExtractorLink, link: ExtractorLink,
notificationCallback: (Int, Notification) -> Unit,
tryResume: Boolean = false, tryResume: Boolean = false,
): Int { ): Int {
val name = sanitizeFilename(ep.name ?: "${context.getString(R.string.episode)} ${ep.episode}") val name = sanitizeFilename(ep.name ?: "${context.getString(R.string.episode)} ${ep.episode}")
if (link.isM3u8 || link.url.endsWith(".m3u8")) { if (link.isM3u8 || link.url.endsWith(".m3u8")) {
val startIndex = if (tryResume) { val startIndex = if (tryResume) {
context.getKey<DownloadedFileInfo>(KEY_DOWNLOAD_INFO, ep.id.toString(), null)?.extraInfo?.toIntOrNull() context.getKey<DownloadedFileInfo>(
KEY_DOWNLOAD_INFO,
ep.id.toString(),
null
)?.extraInfo?.toIntOrNull()
} else null } else null
return downloadHLS(context, link, name, folder, ep.id, startIndex) { meta -> return downloadHLS(context, link, name, folder, ep.id, startIndex) { meta ->
main {
createNotification( createNotification(
context, context,
source, source,
@ -1299,13 +1331,16 @@ object VideoDownloadManager {
ep, ep,
meta.type, meta.type,
meta.bytesDownloaded, meta.bytesDownloaded,
meta.bytesTotal meta.bytesTotal,
notificationCallback
) )
} }
} }
}
return normalSafeApiCall { return normalSafeApiCall {
downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta -> downloadThing(context, link, name, folder, "mp4", tryResume, ep.id) { meta ->
main {
createNotification( createNotification(
context, context,
source, source,
@ -1313,20 +1348,26 @@ object VideoDownloadManager {
ep, ep,
meta.type, meta.type,
meta.bytesDownloaded, meta.bytesDownloaded,
meta.bytesTotal meta.bytesTotal,
notificationCallback
) )
} }
}
} ?: ERROR_UNKNOWN } ?: ERROR_UNKNOWN
} }
private fun downloadCheck(context: Context) { fun downloadCheck(
context: Context, notificationCallback: (Int, Notification) -> Unit,
): Int? {
if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) { if (currentDownloads.size < maxConcurrentDownloads && downloadQueue.size > 0) {
val pkg = downloadQueue.removeFirst() val pkg = downloadQueue.removeFirst()
val item = pkg.item val item = pkg.item
val id = item.ep.id val id = item.ep.id
if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT if (currentDownloads.contains(id)) { // IF IT IS ALREADY DOWNLOADING, RESUME IT
downloadEvent.invoke(Pair(id, DownloadActionType.Resume)) downloadEvent.invoke(Pair(id, DownloadActionType.Resume))
return /** ID needs to be returned to the work-manager to properly await notification */
return id
} }
currentDownloads.add(id) currentDownloads.add(id)
@ -1340,7 +1381,15 @@ object VideoDownloadManager {
context.setKey(KEY_RESUME_PACKAGES, id.toString(), DownloadResumePackage(item, index)) context.setKey(KEY_RESUME_PACKAGES, id.toString(), DownloadResumePackage(item, index))
val connectionResult = withContext(Dispatchers.IO) { val connectionResult = withContext(Dispatchers.IO) {
normalSafeApiCall { normalSafeApiCall {
downloadSingleEpisode(context, item.source, item.folder, item.ep, link, resume) downloadSingleEpisode(
context,
item.source,
item.folder,
item.ep,
link,
notificationCallback,
resume
)
} }
} }
if (connectionResult != null && connectionResult > 0) { // SUCCESS if (connectionResult != null && connectionResult > 0) { // SUCCESS
@ -1352,10 +1401,12 @@ object VideoDownloadManager {
logError(e) logError(e)
} finally { } finally {
currentDownloads.remove(id) currentDownloads.remove(id)
downloadCheck(context) // Because otherwise notifications will not get caught by the workmanager
downloadCheckUsingWorker(context)
} }
} }
} }
return null
} }
fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? { fun getDownloadFileInfoAndUpdateSettings(context: Context, id: Int): DownloadedFileInfoResult? {
@ -1414,23 +1465,28 @@ object VideoDownloadManager {
return context.getKey(KEY_RESUME_PACKAGES, id.toString()) return context.getKey(KEY_RESUME_PACKAGES, id.toString())
} }
fun downloadFromResume(context: Activity, pkg: DownloadResumePackage, setKey: Boolean = true) { fun downloadFromResume(
context: Context,
pkg: DownloadResumePackage,
notificationCallback: (Int, Notification) -> Unit,
setKey: Boolean = true
) {
if (!currentDownloads.any { it == pkg.item.ep.id }) { if (!currentDownloads.any { it == pkg.item.ep.id }) {
if (currentDownloads.size == maxConcurrentDownloads) { if (currentDownloads.size == maxConcurrentDownloads) {
main { main {
showToast( // can be replaced with regular Toast // showToast( // can be replaced with regular Toast
context, // context,
"${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${ // "${pkg.item.ep.mainName}${pkg.item.ep.episode?.let { " ${context.getString(R.string.episode)} $it " } ?: " "}${
context.getString( // context.getString(
R.string.queued // R.string.queued
) // )
}", // }",
Toast.LENGTH_SHORT // Toast.LENGTH_SHORT
) // )
} }
} }
downloadQueue.addLast(pkg) downloadQueue.addLast(pkg)
downloadCheck(context) downloadCheck(context, notificationCallback)
if (setKey) saveQueue(context) if (setKey) saveQueue(context)
} else { } else {
downloadEvent.invoke( downloadEvent.invoke(
@ -1457,15 +1513,78 @@ object VideoDownloadManager {
}*/ }*/
fun downloadEpisode( fun downloadEpisode(
context: Activity?, context: Context?,
source: String?, source: String?,
folder: String?, folder: String?,
ep: DownloadEpisodeMetadata, ep: DownloadEpisodeMetadata,
links: List<ExtractorLink> links: List<ExtractorLink>,
notificationCallback: (Int, Notification) -> Unit,
) { ) {
if (context == null) return if (context == null) return
if (links.isNotEmpty()) { if (links.isNotEmpty()) {
downloadFromResume(context, DownloadResumePackage(DownloadItem(source, folder, ep, links), null)) downloadFromResume(
context,
DownloadResumePackage(DownloadItem(source, folder, ep, links), null),
notificationCallback
)
} }
} }
/** Worker stuff */
private fun startWork(context: Context, key: String) {
val req = OneTimeWorkRequest.Builder(DownloadFileWorkManager::class.java)
.setInputData(
Data.Builder()
.putString("key", key)
.build()
)
.build()
(WorkManager.getInstance(context)).enqueueUniqueWork(
key,
ExistingWorkPolicy.KEEP,
req
)
}
fun downloadCheckUsingWorker(
context: Context,
) {
startWork(context, DOWNLOAD_CHECK)
}
fun downloadFromResumeUsingWorker(
context: Context,
pkg: DownloadResumePackage,
) {
val key = pkg.item.ep.id.toString()
context.setKey(WORK_KEY_PACKAGE, key, pkg)
startWork(context, key)
}
// Keys are needed to transfer the data to the worker reliably and without exceeding the data limit
const val WORK_KEY_PACKAGE = "work_key_package"
const val WORK_KEY_INFO = "work_key_info"
fun downloadEpisodeUsingWorker(
context: Context,
source: String?,
folder: String?,
ep: DownloadEpisodeMetadata,
links: List<ExtractorLink>,
) {
val info = DownloadInfo(
source, folder, ep, links
)
val key = info.ep.id.toString()
context.setKey(WORK_KEY_INFO, key, info)
startWork(context, key)
}
data class DownloadInfo(
@JsonProperty("source") val source: String?,
@JsonProperty("folder") val folder: String?,
@JsonProperty("ep") val ep: DownloadEpisodeMetadata,
@JsonProperty("links") val links: List<ExtractorLink>
)
} }