diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 4dda33a7e9..03c7357090 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -218,6 +218,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)
+            .setShowWhen(false)
+            // 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)
+                }
+
+                // The downloads actually displayed to the user with a notification
+                val currentVisualDownloads =
+                    VideoDownloadManager.currentDownloads.size + downloadInstances.count {
+                        VideoDownloadManager.currentDownloads.contains(it.downloadQueueWrapper.id)
+                            .not()
+                    }
+                // Just the queue
+                val currentVisualQueue = DownloadQueueManager.queue.size
+
+                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..242f081296 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.DownloadUtils.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 a0e5cabc46..7321406e49 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.utils.AppContextUtils.getNameFull
 import com.lagradost.cloudstream3.utils.DataStoreHelper.fixVisual
 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.DownloadObjects
 
 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: DownloadObjects.DownloadCached
     abstract var isSelected: Boolean
 
     data class Child(
         override val currentBytes: Long,
         override val totalBytes: Long,
-        override val data: VideoDownloadHelper.DownloadEpisodeCached,
+        override val data: DownloadObjects.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: DownloadObjects.DownloadHeaderCached,
         override var isSelected: Boolean,
-        val child: VideoDownloadHelper.DownloadEpisodeCached?,
+        val child: DownloadObjects.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: DownloadObjects.DownloadEpisodeCached
 )
 
 data class DownloadHeaderClickEvent(
     val action: Int,
-    val data: VideoDownloadHelper.DownloadHeaderCached
+    val data: DownloadObjects.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 83e0d01678..682a691e85 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,14 @@ 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.DownloadObjects
+import com.lagradost.cloudstream3.utils.downloader.DownloadQueueManager
+import com.lagradost.cloudstream3.utils.downloader.VideoDownloadManager
 import kotlinx.coroutines.MainScope
 
 object DownloadButtonSetup {
@@ -82,7 +84,7 @@ object DownloadButtonSetup {
                     } else {
                         val pkg = VideoDownloadManager.getDownloadResumePackage(ctx, id)
                         if (pkg != null) {
-                            VideoDownloadManager.downloadFromResumeUsingWorker(ctx, pkg)
+                            DownloadQueueManager.addToQueue(pkg.toWrapper())
                         } else {
                             VideoDownloadManager.downloadEvent.invoke(
                                 Pair(click.data.id, VideoDownloadManager.DownloadActionType.Resume)
@@ -95,7 +97,7 @@ object DownloadButtonSetup {
             DOWNLOAD_ACTION_LONG_CLICK -> {
                 activity?.let { act ->
                     val length =
-                        VideoDownloadManager.getDownloadFileInfoAndUpdateSettings(
+                        VideoDownloadManager.getDownloadFileInfo(
                             act,
                             click.data.id
                         )?.fileLength
@@ -112,22 +114,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 2010fe7e36..7438c63413 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
@@ -24,6 +24,7 @@ import androidx.core.view.isVisible
 import androidx.core.widget.doOnTextChanged
 import androidx.fragment.app.Fragment
 import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
 import com.lagradost.cloudstream3.CommonActivity.showToast
 import com.lagradost.cloudstream3.R
 import com.lagradost.cloudstream3.databinding.FragmentDownloadsBinding
@@ -222,6 +223,10 @@ class DownloadFragment : Fragment() {
                 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 137f1355e2..a1f8c75e8d 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
@@ -20,9 +20,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.VideoDownloadHelper
-import com.lagradost.cloudstream3.utils.VideoDownloadManager.deleteFilesAndUpdateSettings
-import com.lagradost.cloudstream3.utils.VideoDownloadManager.getDownloadFileInfoAndUpdateSettings
+import com.lagradost.cloudstream3.utils.downloader.DownloadObjects
+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 +119,14 @@ class DownloadViewModel : ViewModel() {
     fun updateHeaderList(context: Context) = viewModelScope.launchSafe {
         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
@@ -142,7 +142,7 @@ class DownloadViewModel : ViewModel() {
 
     private fun calculateDownloadStats(
         context: Context,
-        children: List
+        children: List
     ): Triple