downloader fixes

This commit is contained in:
deniscerri 2025-03-23 13:21:09 +01:00
parent 7626f7f62e
commit e1db3fa5e5
No known key found for this signature in database
GPG key ID: 95C43D517D830350
15 changed files with 264 additions and 172 deletions

View file

@ -26,7 +26,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Active' or status = 'Paused' ORDER BY CASE WHEN status = 'Active' THEN 0 ELSE 1 END")
@Query("SELECT * FROM downloads WHERE status='Active' or status = 'Paused' ORDER BY id")
fun getActiveAndPausedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Paused'")

View file

@ -22,7 +22,7 @@ import com.deniscerri.ytdl.database.models.DownloadItemSimple
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.DownloadWorker
import com.deniscerri.ytdl.work.downloader.DownloadWorker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import java.io.File

View file

@ -49,6 +49,7 @@ import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdateMultipleDownloadsDataWorker
import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker
import com.deniscerri.ytdl.work.downloader.DownloadManager
import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CancellationException
@ -99,6 +100,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val savedDownloadsCount : Flow<Int>
val scheduledDownloadsCount : Flow<Int>
val downloadManager: DownloadManager
val pausedAllDownloads = MediatorLiveData(PausedAllDownloadsState.HIDDEN)
private val pausedAllDownloadsFlow : Flow<PausedAllDownloadsState>
private var isPausingResuming = false
@ -145,6 +148,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
resultRepository = ResultRepository(dbManager.resultDao, commandTemplateDao, application)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
ytdlpUtil = YTDLPUtil(application, commandTemplateDao)
downloadManager = DownloadManager.getInstance()
activeDownloadsCount = repository.activeDownloadsCount
activePausedDownloadsCount = repository.activePausedDownloadsCount
@ -1291,28 +1295,45 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dao.getProcessingAsIncognitoCount() > 0
}
fun cancelDownloadOnly(id : Long) {
downloadManager.cancelDownload(id)
}
fun pauseAllDownloads() = viewModelScope.launch {
suspend fun cancelDownload(id: Long) {
downloadManager.cancelDownload(id)
val item = getItemByID(id)
item.status = DownloadRepository.Status.Cancelled.toString()
updateDownload(item)
}
suspend fun pauseDownload(id: Long) {
downloadManager.cancelDownload(id)
updateToStatus(id, DownloadRepository.Status.Paused)
}
suspend fun pauseAllDownloads() {
pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING
isPausingResuming = true
WorkManager.getInstance(application).cancelAllWorkByTag("download")
downloadManager.cancelAll()
val activeDownloadsList = withContext(Dispatchers.IO){
getActiveDownloads()
}
activeDownloadsList.forEach {
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
}
delay(1000)
isPausingResuming = false
repository.setDownloadStatusMultiple(activeDownloadsList.map { it.id }, DownloadRepository.Status.Paused)
pausedAllDownloads.value = PausedAllDownloadsState.RESUME
withContext(Dispatchers.IO) {
repository.setDownloadStatusMultiple(activeDownloadsList.map { it.id }, DownloadRepository.Status.Paused)
}
withContext(Dispatchers.Main) {
pausedAllDownloads.value = PausedAllDownloadsState.RESUME
}
}
fun resumeAllDownloads() = viewModelScope.launch {
suspend fun resumeAllDownloads() {
pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING
isPausingResuming = true
WorkManager.getInstance(application).cancelAllWorkByTag("download")
downloadManager.cancelAll()
val paused = withContext(Dispatchers.IO) {
dao.getPausedDownloadsList()
}

View file

@ -0,0 +1,5 @@
package com.deniscerri.ytdl.di
class AppModule {
}

View file

@ -9,7 +9,7 @@ import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.work.DownloadWorker
import com.deniscerri.ytdl.work.downloader.DownloadWorker
import java.util.concurrent.TimeUnit
class ScheduleAlarmReceiver : BroadcastReceiver() {

View file

@ -48,7 +48,7 @@ import com.deniscerri.ytdl.util.Extensions.setFullScreen
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.VideoPlayerUtil
import com.deniscerri.ytdl.work.DownloadWorker
import com.deniscerri.ytdl.work.downloader.DownloadWorker
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -474,25 +474,16 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
override fun onCancelClick(itemID: Long) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
item.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
downloadViewModel.cancelDownload(itemID)
}
}
}
override fun onPauseClick(itemID: Long, position: Int) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.updateToStatus(itemID, DownloadRepository.Status.Paused)
downloadViewModel.pauseDownload(itemID)
}
activeAdapter.notifyItemChanged(position)
}
@ -511,7 +502,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
//dont remove
@Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
fun onDownloadProgressEvent(event: com.deniscerri.ytdl.work.downloader.DownloadManager.DownloadProgress) {
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output")

View file

@ -24,16 +24,14 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.work.DownloadWorker
import com.deniscerri.ytdl.work.downloader.DownloadManager
import com.google.android.material.badge.ExperimentalBadgeUtils
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
@ -151,7 +149,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
//dont remove
@Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) {
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output")
@ -176,15 +174,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
override fun onCancelClick(itemID: Long) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
item.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
downloadViewModel.cancelDownload(itemID)
}
}
}
@ -202,10 +193,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
override fun onPauseClick(itemID: Long) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.updateToStatus(itemID, DownloadRepository.Status.Paused)
downloadViewModel.pauseDownload(itemID)
}
}
}

View file

@ -216,8 +216,7 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
val selectedObjects = getSelectedIDs()
adapter.clearCheckedItems()
for (id in selectedObjects){
YoutubeDL.getInstance().destroyProcessById(id.toInt().toString())
notificationUtil.cancelDownloadNotification(id.toInt())
downloadViewModel.cancelDownloadOnly(id)
}
downloadViewModel.deleteAllWithID(selectedObjects)
actionMode?.finish()
@ -524,20 +523,10 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
private fun cancelDownload(itemID: Long){
lifecycleScope.launch {
cancelItem(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}.let {
it.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(it)
}
downloadViewModel.cancelDownload(itemID)
}
}
}
private fun cancelItem(id: Int){
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id)
}
}

View file

@ -123,36 +123,14 @@ class MoreFragment : Fragment() {
val checkbox = dialogView.findViewById<CheckBox>(R.id.doNotShowAgain)
terminateDialog.setView(dialogView)
checkbox.setOnCheckedChangeListener { compoundButton, b ->
doNotShowAgain = compoundButton.isChecked
}
val workManager = WorkManager.getInstance(requireContext())
val notificationUtil = NotificationUtil(requireContext())
terminateDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
terminateDialog.setPositiveButton(getString(R.string.ok)) { diag: DialogInterface?, _: Int ->
lifecycleScope.launch {
val activeDownloads = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloadsCount()
}
if (activeDownloads > 0) {
workManager.cancelAllWorkByTag("download")
val activeDownloadsList = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}
activeDownloadsList.forEach {
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
it.status = DownloadRepository.Status.Paused.toString()
withContext(Dispatchers.IO) {
downloadViewModel.updateDownload(it)
}
}
}
downloadViewModel.pauseAllDownloads()
if (doNotShowAgain){
mainSharedPreferencesEditor.putBoolean("ask_terminate_app", false).apply()

View file

@ -30,7 +30,8 @@ import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.enableTextHighlight
import com.deniscerri.ytdl.util.Extensions.setCustomTextSize
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.work.DownloadWorker
import com.deniscerri.ytdl.work.downloader.DownloadManager
import com.deniscerri.ytdl.work.downloader.DownloadWorker
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -265,7 +266,7 @@ class DownloadLogFragment : Fragment() {
//dont remove
@Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) {
val progressBar = requireView().findViewById<LinearProgressIndicator>(R.id.progress)
if (event.logItemID == logID) {
progressBar.isVisible = event.progress < 100

View file

@ -22,7 +22,7 @@ import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.CleanUpLeftoverDownloads
import com.deniscerri.ytdl.work.DownloadWorker
import com.deniscerri.ytdl.work.downloader.DownloadWorker
import java.util.Calendar
import java.util.concurrent.TimeUnit

View file

@ -20,7 +20,8 @@ import com.deniscerri.ytdl.database.models.TerminalItem
import com.deniscerri.ytdl.database.viewmodel.TerminalViewModel
import com.deniscerri.ytdl.ui.adapter.TerminalDownloadsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.work.DownloadWorker
import com.deniscerri.ytdl.work.downloader.DownloadManager
import com.deniscerri.ytdl.work.downloader.DownloadWorker
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.flow.collectLatest
@ -84,7 +85,7 @@ class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnIte
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) {
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output")

View file

@ -22,6 +22,8 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.more.terminal.TerminalActivity
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.work.downloader.DownloadManager
import com.deniscerri.ytdl.work.downloader.DownloadWorker
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CoroutineScope
@ -122,7 +124,7 @@ class TerminalDownloadWorker(
YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line ->
runBlocking {
eventBus.post(DownloadWorker.WorkerProgress(progress.toInt(), line, itemId.toLong(), logItem.id))
eventBus.post(DownloadManager.DownloadProgress(progress.toInt(), line, itemId.toLong(), logItem.id))
}
val title: String = command.take(65)
@ -142,7 +144,7 @@ class TerminalDownloadWorker(
//move file from internal to set download directory
try {
FileUtil.moveFile(File(FileUtil.getCachePath(context) + "/TERMINAL/" + itemId),context, downloadLocation!!, false){ p ->
eventBus.post(DownloadWorker.WorkerProgress(p, "", itemId.toLong(), logItem.id))
eventBus.post(DownloadManager.DownloadProgress(p, "", itemId.toLong(), logItem.id))
}
}catch (e: Exception){
e.printStackTrace()

View file

@ -1,11 +1,9 @@
package com.deniscerri.ytdl.work
package com.deniscerri.ytdl.work.downloader
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ServiceInfo
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
@ -15,10 +13,6 @@ import android.util.DisplayMetrics
import android.util.Log
import android.widget.Toast
import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.MainActivity
@ -38,17 +32,17 @@ import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
@ -58,56 +52,72 @@ import java.io.File
import java.security.MessageDigest
import java.util.Locale
class DownloadManager private constructor() {
private var notificationUtil: NotificationUtil = NotificationUtil(App.instance)
private var sharedPreferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(App.instance)
class DownloadWorker(private val context: Context,workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
private lateinit var dao : DownloadDao
private lateinit var notificationUtil: NotificationUtil
private lateinit var dbManager: DBManager
private lateinit var dao : DownloadDao
private lateinit var historyDao: HistoryDao
private lateinit var commandTemplateDao: CommandTemplateDao
private lateinit var logRepo: LogRepository
private lateinit var resultRepo: ResultRepository
private lateinit var ytdlpUtil: YTDLPUtil
private lateinit var alarmScheduler : AlarmScheduler
private lateinit var sharedPreferences : SharedPreferences
private lateinit var resources: Resources
private lateinit var workManager: WorkManager
private lateinit var eventBus: EventBus
private lateinit var resources: Resources
private lateinit var openDownloadQueue: PendingIntent
private lateinit var queueState : Flow<List<DownloadItem>>
lateinit var observeJob : Job
private var observeJob : Job? = null
private val handler = Handler(Looper.getMainLooper())
private val openDownloadQueue: PendingIntent = PendingIntent.getActivity(
context,
1000000000,
Intent(context, MainActivity::class.java).run {
action = Intent.ACTION_VIEW
putExtra("destination", "Queue")
},
PendingIntent.FLAG_IMMUTABLE
)
val isRunning get() = observeJob?.isActive == true
override suspend fun getForegroundInfo(): ForegroundInfo {
val workNotif = NotificationUtil(App.instance).createDefaultWorkerNotification()
return ForegroundInfo(
1000000000,
workNotif,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
)
private fun getActiveDownloadsStore() : Set<String> {
return sharedPreferences.getStringSet("ytdlp_active_downloads", setOf())!!
}
@SuppressLint("RestrictedApi")
override suspend fun doWork(): Result {
private fun addIDToActiveDownloadsStore(id: Long) {
val new = mutableSetOf<String>()
new.addAll(getActiveDownloadsStore())
new.add(id.toString())
sharedPreferences.edit().putStringSet("ytdlp_active_downloads", new).apply()
}
private fun removeIDFromActiveDownloadsStore(id: Long) {
val new = mutableSetOf<String>()
new.addAll(getActiveDownloadsStore())
new.remove(id.toString())
sharedPreferences.edit().putStringSet("ytdlp_active_downloads", new).apply()
}
fun cancelDownload(id: Long) {
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id.toInt())
removeIDFromActiveDownloadsStore(id)
}
fun cancelAll() {
getActiveDownloadsStore().map { it.toLong() }.forEach {
cancelDownload(it)
}
observeJob?.cancel()
}
//only call from download worker
fun startDownload(
context: Context, //worker context
priorityItems: List<Long> = listOf(),
continueAfterPriorityIds : Boolean = true
) {
val priorityItemIDs = priorityItems.toMutableList()
//init
notificationUtil = NotificationUtil(App.instance)
notificationUtil = NotificationUtil(context)
dbManager = DBManager.getInstance(context)
dao = dbManager.downloadDao
historyDao = dbManager.historyDao
@ -119,6 +129,8 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
eventBus = EventBus.getDefault()
sharedPreferences.edit().putStringSet("ytdlp_active_downloads", setOf()).apply()
val confTmp = Configuration(context.resources.configuration)
val locale = if (Build.VERSION.SDK_INT < 33) {
sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language }
@ -133,13 +145,16 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
val metrics = DisplayMetrics()
resources = Resources(context.assets, metrics, confTmp)
workManager = WorkManager.getInstance(context)
if (workManager.isRunning("download")) return Result.Failure()
openDownloadQueue = PendingIntent.getActivity(
context,
1000000000,
Intent(context, MainActivity::class.java).run {
action = Intent.ACTION_VIEW
putExtra("destination", "Queue")
},
PendingIntent.FLAG_IMMUTABLE
)
setForegroundSafely()
val priorityItemIDs = (inputData.getLongArray("priority_item_ids") ?: longArrayOf()).toMutableList()
val continueAfterPriorityIds = inputData.getBoolean("continue_after_priority_ids", true)
val time = System.currentTimeMillis() + 6000
queueState = if (priorityItemIDs.isEmpty()) {
dao.getActiveQueuedScheduledDownloadsUntil(time)
@ -147,10 +162,8 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
dao.getActiveQueuedScheduledDownloadsUntilWithPriority(time, priorityItemIDs)
}
val downloadJobs = mutableMapOf<Long, Job>()
observeJob = CoroutineScope(SupervisorJob()).launch {
queueState.collectLatest { queue ->
observeJob = CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
queueState.distinctUntilChanged().collectLatest { queue ->
val runningCount = queue.asSequence()
.filter { it.status == DownloadRepository.Status.Active.toString() }
.count()
@ -160,66 +173,53 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
val queueCount = queueItems.count()
if (queueCount == 0 && runningCount == 0) {
observeJob.cancel()
observeJob?.cancel()
return@collectLatest
}
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler){
if (queueItems.none{ it.downloadStartTime > 0L } && runningCount == 0 && !alarmScheduler.isDuringTheScheduledTime()) {
observeJob.cancel()
observeJob?.cancel()
return@collectLatest
}
}
if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) {
observeJob?.cancel()
return@collectLatest
}
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - runningCount
if (concurrentDownloads > 0) {
//free spots are open
if (priorityItemIDs.isNotEmpty()) {
if (!continueAfterPriorityIds && queueItems.none { priorityItemIDs.contains(it.id) }) {
//dont queue any more items if only required priority items
observeJob.cancel()
return@collectLatest
}
}
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
supervisorScope {
val queuedDownloads = queueItems
.toList()
.take(concurrentDownloads)
val queuedIDs = queuedDownloads.map { it.id }
val downloadJobsToStop = downloadJobs.filter { it.key !in queuedIDs }
downloadJobsToStop.forEach { (download, job) ->
job.cancel()
downloadJobs.remove(download)
}
val downloadsToStart = queuedDownloads.filter { it.id !in downloadJobs }
priorityItemIDs.removeAll(queuedDownloads.map { it.id })
val downloadsToStart = queuedDownloads.filter { it.id.toString() !in getActiveDownloadsStore() }
downloadsToStart.forEach { downloadItem ->
val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url })
notificationUtil.notify(downloadItem.id.toInt(), notification)
downloadJobs[downloadItem.id] = launchDownloadJob(downloadItem)
launchDownloadJob(context, downloadItem)
addIDToActiveDownloadsStore(downloadItem.id)
}
}
}
}
}
while (observeJob.isActive) {
//keep alive
}
return Result.Success()
}
@OptIn(ExperimentalStdlibApi::class)
private fun launchDownloadJob(downloadItem: DownloadItem) = CoroutineScope(Dispatchers.IO).launch {
private fun launchDownloadJob(context: Context, downloadItem: DownloadItem) = CoroutineScope(Dispatchers.IO).launch {
val writtenPath = downloadItem.format.format_note.contains("-P ")
val noCache = writtenPath || (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite())
val noCache = writtenPath || (!sharedPreferences.getBoolean("cache_downloads", true) && File(
FileUtil.formatPath(downloadItem.downloadPath)).canWrite())
val request = ytdlpUtil.buildYoutubeDLRequest(downloadItem)
@ -270,7 +270,7 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
runCatching {
YoutubeDL.getInstance().destroyProcessById(downloadItem.id.toString())
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
eventBus.post(WorkerProgress(progress.toInt(), line, downloadItem.id, downloadItem.logID))
eventBus.post(DownloadProgress(progress.toInt(), line, downloadItem.id, downloadItem.logID))
val title: String = downloadItem.title.ifEmpty { downloadItem.url }
notificationUtil.updateDownloadNotification(
downloadItem.id.toInt(),
@ -288,12 +288,13 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
resultRepo.updateDownloadItem(downloadItem)?.apply {
dao.updateWithoutUpsert(this)
}
removeIDFromActiveDownloadsStore(downloadItem.id)
//val wasQuickDownloaded = resultDao.getCountInt() == 0
runBlocking {
var finalPaths = mutableListOf<String>()
if (noCache){
eventBus.post(WorkerProgress(100, "Scanning Files", downloadItem.id, downloadItem.logID))
eventBus.post(DownloadProgress(100, "Scanning Files", downloadItem.id, downloadItem.logID))
val outputSequence = it.out.split("\n")
finalPaths =
outputSequence.asSequence()
@ -315,17 +316,17 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
FileUtil.scanMedia(finalPaths, context)
}else{
//move file from internal to set download directory
eventBus.post(WorkerProgress(100, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID))
eventBus.post(DownloadProgress(100, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID))
try {
finalPaths = withContext(Dispatchers.IO){
FileUtil.moveFile(tempFileDir.absoluteFile,
context, downloadLocation, keepCache){ p ->
eventBus.post(WorkerProgress(p, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID))
eventBus.post(DownloadProgress(p, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID))
}
}.filter { !it.matches("\\.(description)|(txt)\$".toRegex()) }.toMutableList()
if (finalPaths.isNotEmpty()){
eventBus.post(WorkerProgress(100, "Moved file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID))
eventBus.post(DownloadProgress(100, "Moved file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID))
}
}catch (e: Exception){
e.printStackTrace()
@ -413,10 +414,11 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
}.onFailure {
FileUtil.deleteConfigFiles(request)
removeIDFromActiveDownloadsStore(downloadItem.id)
withContext(Dispatchers.Main){
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
}
if (isStopped) return@onFailure
if (!this.isActive) return@onFailure
if (it is YoutubeDL.CanceledException) return@onFailure
if (it.message?.contains("JSONDecodeError") == true) {
val cachePath = "${FileUtil.getCachePath(context)}infojsons"
@ -456,19 +458,28 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
resources
)
eventBus.post(WorkerProgress(100, it.toString(), downloadItem.id, downloadItem.logID))
eventBus.post(DownloadProgress(100, it.toString(), downloadItem.id, downloadItem.logID))
}
}
companion object {
const val TAG = "DownloadWorker"
const val TAG = "DownloadManager"
@Volatile
private var instance: DownloadManager? = null
fun getInstance() : DownloadManager {
return instance ?: synchronized(this) {
instance ?: DownloadManager().also {
instance = it
}
}
}
}
class WorkerProgress(
class DownloadProgress(
val progress: Int,
val output: String,
val downloadItemID: Long,
val logItemID: Long?
)
}

View file

@ -0,0 +1,104 @@
package com.deniscerri.ytdl.work.downloader
import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.ServiceInfo
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.DisplayMetrics
import android.util.Log
import android.widget.Toast
import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.dao.CommandTemplateDao
import com.deniscerri.ytdl.database.dao.DownloadDao
import com.deniscerri.ytdl.database.dao.HistoryDao
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.models.LogItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.LogRepository
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.util.Extensions.getMediaDuration
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.isRunning
import com.deniscerri.ytdl.work.setForegroundSafely
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import java.io.File
import java.security.MessageDigest
import java.util.Locale
class DownloadWorker(private val context: Context,workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
private lateinit var workManager: WorkManager
private lateinit var downloadManager: DownloadManager
override suspend fun getForegroundInfo(): ForegroundInfo {
val workNotif = NotificationUtil(App.instance).createDefaultWorkerNotification()
return ForegroundInfo(
1000000000,
workNotif,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
)
}
@SuppressLint("RestrictedApi")
override suspend fun doWork(): Result {
workManager = WorkManager.getInstance(context)
if (workManager.isRunning("download")) return Result.Failure()
setForegroundSafely()
downloadManager = DownloadManager.getInstance()
val priorityItemIDs = (inputData.getLongArray("priority_item_ids") ?: longArrayOf()).toMutableList()
val continueAfterPriorityIds = inputData.getBoolean("continue_after_priority_ids", true)
downloadManager.startDownload(context, priorityItemIDs, continueAfterPriorityIds)
var active = downloadManager.isRunning
while (active) {
active = !isStopped && downloadManager.isRunning
}
return Result.Success()
}
companion object {
const val TAG = "DownloadWorker"
}
}