From e640cd0ba6cc391bde200513a7885795c4352681 Mon Sep 17 00:00:00 2001
From: CranberrySoup <142951702+CranberrySoup@users.noreply.github.com>
Date: Sun, 19 Oct 2025 19:30:24 +0000
Subject: [PATCH 1/4] download
---
app/src/main/AndroidManifest.xml | 6 +
.../lagradost/cloudstream3/MainActivity.kt | 11 +-
.../cloudstream3/plugins/PluginManager.kt | 2 +-
.../services/DownloadQueueService.kt | 153 +++
.../services/SubscriptionWorkManager.kt | 2 +-
.../services/VideoDownloadService.kt | 19 +-
.../ui/download/DownloadAdapter.kt | 14 +-
.../ui/download/DownloadButtonSetup.kt | 23 +-
.../ui/download/DownloadFragment.kt | 4 +
.../ui/download/DownloadViewModel.kt | 36 +-
.../ui/download/button/BaseFetchButton.kt | 6 +-
.../ui/download/button/DownloadButton.kt | 4 +-
.../ui/download/button/PieFetchButton.kt | 12 +-
.../download/queue/DownloadQueueFragment.kt | 42 +
.../cloudstream3/ui/home/HomeViewModel.kt | 4 +-
.../ui/player/DownloadFileGenerator.kt | 6 +-
.../cloudstream3/ui/player/GeneratorPlayer.kt | 2 +-
.../cloudstream3/ui/result/EpisodeAdapter.kt | 14 +-
.../ui/result/ResultFragmentPhone.kt | 6 +-
.../ui/result/ResultViewModel2.kt | 315 ++----
.../cloudstream3/ui/search/SearchHelper.kt | 4 +-
.../ui/settings/SettingsGeneral.kt | 9 +-
.../ui/settings/SettingsUpdates.kt | 3 +-
.../cloudstream3/utils/AppContextUtils.kt | 10 +-
.../cloudstream3/utils/BackupUtils.kt | 9 +-
.../cloudstream3/utils/DataStoreHelper.kt | 7 +-
.../utils/DownloadFileWorkManager.kt | 104 --
.../cloudstream3/utils/SubtitleUtils.kt | 6 +-
.../cloudstream3/utils/VideoDownloadHelper.kt | 55 --
.../utils/downloader/DownloadQueueManager.kt | 140 +++
.../downloader/VideoDownloadFileManagement.kt | 132 +++
.../{ => downloader}/VideoDownloadManager.kt | 910 +++++++-----------
.../utils/downloader/VideoDownloadObjects.kt | 211 ++++
.../utils/downloader/VideoDownloadUtils.kt | 168 ++++
.../drawable/round_keyboard_arrow_up_24.xml | 13 +
.../res/layout/fragment_download_queue.xml | 95 ++
.../main/res/layout/fragment_downloads.xml | 80 +-
.../main/res/navigation/mobile_navigation.xml | 25 +-
app/src/main/res/values/strings.xml | 10 +
39 files changed, 1562 insertions(+), 1110 deletions(-)
create mode 100644 app/src/main/java/com/lagradost/cloudstream3/services/DownloadQueueService.kt
create mode 100644 app/src/main/java/com/lagradost/cloudstream3/ui/download/queue/DownloadQueueFragment.kt
create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/downloader/DownloadQueueManager.kt
create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/downloader/VideoDownloadFileManagement.kt
rename app/src/main/java/com/lagradost/cloudstream3/utils/{ => downloader}/VideoDownloadManager.kt (74%)
create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/downloader/VideoDownloadObjects.kt
create mode 100644 app/src/main/java/com/lagradost/cloudstream3/utils/downloader/VideoDownloadUtils.kt
create mode 100644 app/src/main/res/drawable/round_keyboard_arrow_up_24.xml
create mode 100644 app/src/main/res/layout/fragment_download_queue.xml
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 9e1bc9ac97..56622aab91 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -216,6 +216,12 @@
android:foregroundServiceType="dataSync"
android:exported="false" />
+
+
=
+ mutableListOf()
+
+ private val baseNotification by lazy {
+ val intent = Intent(this, MainActivity::class.java)
+ val pendingIntent =
+ PendingIntentCompat.getActivity(this, 0, intent, 0, false)
+
+ val activeDownloads = resources.getQuantityString(R.plurals.downloads_active, 0).format(0)
+ val activeQueue = resources.getQuantityString(R.plurals.downloads_queued, 0).format(0)
+
+ NotificationCompat.Builder(this, DOWNLOAD_QUEUE_CHANNEL_ID)
+ .setOngoing(true) // Make it persistent
+ .setAutoCancel(false)
+ .setColorized(false)
+ .setOnlyAlertOnce(true)
+ .setSilent(true)
+ // If low priority then the notification might not show :(
+ .setPriority(NotificationCompat.PRIORITY_DEFAULT)
+ .setColor(this.colorFromAttribute(R.attr.colorPrimary))
+ .setContentText(activeDownloads)
+ .setSubText(activeQueue)
+ .setContentIntent(pendingIntent)
+ .setSmallIcon(R.drawable.download_icon_load)
+ }
+
+
+ private fun updateNotification(context: Context, downloads: Int, queued: Int) {
+ val activeDownloads = resources.getQuantityString(R.plurals.downloads_active, downloads).format(downloads)
+ val activeQueue = resources.getQuantityString(R.plurals.downloads_queued, queued).format(queued)
+
+ val newNotification = baseNotification
+ .setContentText(activeDownloads)
+ .setSubText(activeQueue)
+ .build()
+
+ NotificationManagerCompat.from(context).notify(DOWNLOAD_QUEUE_NOTIFICATION_ID, newNotification)
+ }
+
+ override fun onCreate() {
+ isRunning = true
+ Log.d(TAG, "Download queue service started.")
+ this.createNotificationChannel(
+ DOWNLOAD_QUEUE_CHANNEL_ID,
+ DOWNLOAD_QUEUE_CHANNEL_NAME,
+ DOWNLOAD_QUEUE_CHANNEL_DESCRIPTION
+ )
+ if (SDK_INT >= 29) {
+ startForeground(
+ DOWNLOAD_QUEUE_NOTIFICATION_ID,
+ baseNotification.build(),
+ FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ )
+ } else {
+ startForeground(DOWNLOAD_QUEUE_NOTIFICATION_ID, baseNotification.build())
+ }
+
+ val context = this.applicationContext
+
+ ioSafe {
+ while (isRunning && (DownloadQueueManager.queue.isNotEmpty() || downloadInstances.isNotEmpty())) {
+ // Remove any completed or failed works
+ downloadInstances =
+ downloadInstances.filterNot { it.isCompleted || it.isFailed }.toMutableList()
+
+ val maxDownloads = VideoDownloadManager.maxConcurrentDownloads(context)
+ val currentDownloads = downloadInstances.size
+
+ val newDownloads = minOf(
+ // Cannot exceed the max downloads
+ maxOf(0, maxDownloads - currentDownloads),
+ // Cannot start more downloads than the queue size
+ DownloadQueueManager.queue.size
+ )
+
+ repeat(newDownloads) {
+ val downloadInstance = DownloadQueueManager.popQueue(context) ?: return@repeat
+ downloadInstance.startDownload()
+ downloadInstances.add(downloadInstance)
+ }
+
+ // TODO Notification for acquiring links
+ // The downloads actually displayed to the user with a notification
+ val currentVisualDownloads = VideoDownloadManager.currentDownloads.size
+ // The queue + the download instances not actively downloading
+ val currentVisualQueue = DownloadQueueManager.queue.size + downloadInstances.count {
+ VideoDownloadManager.currentDownloads.contains(it.downloadQueueWrapper.id).not()
+ }
+
+ updateNotification(context, currentVisualDownloads, currentVisualQueue)
+
+ // Arbitrary delay to prevent hogging the CPU, decrease to make the queue feel slightly more responsive
+ delay(500)
+ }
+ stopSelf()
+ }
+ }
+
+ override fun onDestroy() {
+ Log.d(TAG, "Download queue service stopped.")
+ isRunning = false
+ super.onDestroy()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ return START_STICKY // We want the service restarted if its killed
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ override fun onTimeout(reason: Int) {
+ stopSelf()
+ Log.e(TAG, "Service stopped due to timeout: $reason")
+ }
+
+}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
index fc31c1f3e0..b809120d40 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/services/SubscriptionWorkManager.kt
@@ -22,7 +22,7 @@ import com.lagradost.cloudstream3.utils.DataStoreHelper
import com.lagradost.cloudstream3.utils.DataStoreHelper.getAllSubscriptions
import com.lagradost.cloudstream3.utils.DataStoreHelper.getDub
import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute
-import com.lagradost.cloudstream3.utils.VideoDownloadManager.getImageBitmapFromUrl
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadUtils.getImageBitmapFromUrl
import kotlinx.coroutines.withTimeoutOrNull
import java.util.concurrent.TimeUnit
diff --git a/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt b/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt
index 6151a0edd2..d63b18cdc9 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/services/VideoDownloadService.kt
@@ -2,12 +2,13 @@ package com.lagradost.cloudstream3.services
import android.app.Service
import android.content.Intent
import android.os.IBinder
-import com.lagradost.cloudstream3.utils.VideoDownloadManager
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
+/** Handle notification actions such as pause/resume downloads */
class VideoDownloadService : Service() {
private val downloadScope = CoroutineScope(Dispatchers.Default)
@@ -42,19 +43,3 @@ class VideoDownloadService : Service() {
super.onDestroy()
}
}
-// 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.downloadEvent.invoke(Pair(id, state))
-// }
-// }
-// }
-//}
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt
index d0740f66a8..061d26c701 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadAdapter.kt
@@ -19,7 +19,7 @@ import com.lagradost.cloudstream3.ui.download.button.DownloadStatusTell
import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
import com.lagradost.cloudstream3.utils.DataStoreHelper.getViewPos
import com.lagradost.cloudstream3.utils.ImageLoader.loadImage
-import com.lagradost.cloudstream3.utils.VideoDownloadHelper
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadObjects
const val DOWNLOAD_ACTION_PLAY_FILE = 0
const val DOWNLOAD_ACTION_DELETE_FILE = 1
@@ -34,22 +34,22 @@ const val DOWNLOAD_ACTION_LOAD_RESULT = 1
sealed class VisualDownloadCached {
abstract val currentBytes: Long
abstract val totalBytes: Long
- abstract val data: VideoDownloadHelper.DownloadCached
+ abstract val data: VideoDownloadObjects.DownloadCached
abstract var isSelected: Boolean
data class Child(
override val currentBytes: Long,
override val totalBytes: Long,
- override val data: VideoDownloadHelper.DownloadEpisodeCached,
+ override val data: VideoDownloadObjects.DownloadEpisodeCached,
override var isSelected: Boolean,
) : VisualDownloadCached()
data class Header(
override val currentBytes: Long,
override val totalBytes: Long,
- override val data: VideoDownloadHelper.DownloadHeaderCached,
+ override val data: VideoDownloadObjects.DownloadHeaderCached,
override var isSelected: Boolean,
- val child: VideoDownloadHelper.DownloadEpisodeCached?,
+ val child: VideoDownloadObjects.DownloadEpisodeCached?,
val currentOngoingDownloads: Int,
val totalDownloads: Int,
) : VisualDownloadCached()
@@ -57,12 +57,12 @@ sealed class VisualDownloadCached {
data class DownloadClickEvent(
val action: Int,
- val data: VideoDownloadHelper.DownloadEpisodeCached
+ val data: VideoDownloadObjects.DownloadEpisodeCached
)
data class DownloadHeaderClickEvent(
val action: Int,
- val data: VideoDownloadHelper.DownloadHeaderCached
+ val data: VideoDownloadObjects.DownloadHeaderCached
)
class DownloadAdapter(
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt
index e9855ef3a3..9216a504bd 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadButtonSetup.kt
@@ -14,12 +14,13 @@ import com.lagradost.cloudstream3.ui.player.ExtractorUri
import com.lagradost.cloudstream3.ui.player.GeneratorPlayer
import com.lagradost.cloudstream3.utils.AppContextUtils.getNameFull
import com.lagradost.cloudstream3.utils.AppContextUtils.setDefaultFocus
+import com.lagradost.cloudstream3.utils.Coroutines.ioSafe
import com.lagradost.cloudstream3.utils.DOWNLOAD_EPISODE_CACHE
import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.SnackbarHelper.showSnackbar
import com.lagradost.cloudstream3.utils.UIHelper.navigate
-import com.lagradost.cloudstream3.utils.VideoDownloadHelper
-import com.lagradost.cloudstream3.utils.VideoDownloadManager
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadObjects
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager
import kotlinx.coroutines.MainScope
object DownloadButtonSetup {
@@ -82,7 +83,10 @@ object DownloadButtonSetup {
} else {
val pkg = VideoDownloadManager.getDownloadResumePackage(ctx, id)
if (pkg != null) {
- VideoDownloadManager.downloadFromResumeUsingWorker(ctx, pkg)
+ ioSafe {
+ // TODO FIX THIS WITH PROPER QUEUE
+// VideoDownloadManager.downloadFromResume(ctx, pkg)
+ }
} else {
VideoDownloadManager.downloadEvent.invoke(
Pair(click.data.id, VideoDownloadManager.DownloadActionType.Resume)
@@ -95,7 +99,7 @@ object DownloadButtonSetup {
DOWNLOAD_ACTION_LONG_CLICK -> {
activity?.let { act ->
val length =
- VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
+ VideoDownloadManager.getDownloadFileInfo(
act,
click.data.id
)?.fileLength
@@ -112,22 +116,25 @@ object DownloadButtonSetup {
DOWNLOAD_ACTION_PLAY_FILE -> {
activity?.let { act ->
- val parent = getKey(
+ val parent = getKey(
DOWNLOAD_HEADER_CACHE,
click.data.parentId.toString()
) ?: return
val episodes = getKeys(DOWNLOAD_EPISODE_CACHE)
?.mapNotNull {
- getKey(it)
+ getKey(it)
}
?.filter { it.parentId == click.data.parentId }
val items = mutableListOf()
- val allRelevantEpisodes = episodes?.sortedWith(compareBy { it.season ?: 0 }.thenBy { it.episode })
+ val allRelevantEpisodes =
+ episodes?.sortedWith(compareBy {
+ it.season ?: 0
+ }.thenBy { it.episode })
allRelevantEpisodes?.forEach {
- val keyInfo = getKey(
+ val keyInfo = getKey(
VideoDownloadManager.KEY_DOWNLOAD_INFO,
it.id.toString()
) ?: return@forEach
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
index e3d77abac9..0882b118a7 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadFragment.kt
@@ -225,6 +225,10 @@ class DownloadFragment : BaseFragment(
setOnClickListener { showStreamInputDialog(it.context) }
}
+ downloadQueueButton.setOnClickListener {
+ findNavController().navigate(R.id.action_navigation_global_to_navigation_download_queue)
+ }
+
downloadStreamButtonTv.isFocusableInTouchMode = isLayout(TV)
downloadAppbar.isFocusableInTouchMode = isLayout(TV)
diff --git a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt
index bf81e60698..c812d801b2 100644
--- a/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt
+++ b/app/src/main/java/com/lagradost/cloudstream3/ui/download/DownloadViewModel.kt
@@ -21,10 +21,9 @@ import com.lagradost.cloudstream3.utils.DOWNLOAD_HEADER_CACHE
import com.lagradost.cloudstream3.utils.DataStore.getFolderName
import com.lagradost.cloudstream3.utils.DataStore.getKey
import com.lagradost.cloudstream3.utils.DataStore.getKeys
-import com.lagradost.cloudstream3.utils.ResourceLiveData
-import com.lagradost.cloudstream3.utils.VideoDownloadHelper
-import com.lagradost.cloudstream3.utils.VideoDownloadManager.deleteFilesAndUpdateSettings
-import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadObjects
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.deleteFilesAndUpdateSettings
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager.getDownloadFileInfo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -119,14 +118,14 @@ class DownloadViewModel : ViewModel() {
val visual = withContext(Dispatchers.IO) {
val children = context.getKeys(DOWNLOAD_EPISODE_CACHE)
- .mapNotNull { context.getKey(it) }
+ .mapNotNull { context.getKey(it) }
.distinctBy { it.id } // Remove duplicates
val (totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads) =
calculateDownloadStats(context, children)
val cached = context.getKeys(DOWNLOAD_HEADER_CACHE)
- .mapNotNull { context.getKey(it) }
+ .mapNotNull { context.getKey(it) }
createVisualDownloadList(
context, cached, totalBytesUsedByChild, currentBytesUsedByChild, totalDownloads
@@ -163,7 +162,7 @@ class DownloadViewModel : ViewModel() {
private fun calculateDownloadStats(
context: Context,
- children: List
+ children: List
): Triple