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'") @Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloads() : Flow<List<DownloadItem>> 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>> fun getActiveAndPausedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Paused'") @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.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.work.AlarmScheduler 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.Flow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import java.io.File 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.AlarmScheduler
import com.deniscerri.ytdl.work.UpdateMultipleDownloadsDataWorker import com.deniscerri.ytdl.work.UpdateMultipleDownloadsDataWorker
import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker
import com.deniscerri.ytdl.work.downloader.DownloadManager
import com.google.gson.Gson import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@ -99,6 +100,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val savedDownloadsCount : Flow<Int> val savedDownloadsCount : Flow<Int>
val scheduledDownloadsCount : Flow<Int> val scheduledDownloadsCount : Flow<Int>
val downloadManager: DownloadManager
val pausedAllDownloads = MediatorLiveData(PausedAllDownloadsState.HIDDEN) val pausedAllDownloads = MediatorLiveData(PausedAllDownloadsState.HIDDEN)
private val pausedAllDownloadsFlow : Flow<PausedAllDownloadsState> private val pausedAllDownloadsFlow : Flow<PausedAllDownloadsState>
private var isPausingResuming = false private var isPausingResuming = false
@ -145,6 +148,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
resultRepository = ResultRepository(dbManager.resultDao, commandTemplateDao, application) resultRepository = ResultRepository(dbManager.resultDao, commandTemplateDao, application)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
ytdlpUtil = YTDLPUtil(application, commandTemplateDao) ytdlpUtil = YTDLPUtil(application, commandTemplateDao)
downloadManager = DownloadManager.getInstance()
activeDownloadsCount = repository.activeDownloadsCount activeDownloadsCount = repository.activeDownloadsCount
activePausedDownloadsCount = repository.activePausedDownloadsCount activePausedDownloadsCount = repository.activePausedDownloadsCount
@ -1291,28 +1295,45 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dao.getProcessingAsIncognitoCount() > 0 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 pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING
isPausingResuming = true isPausingResuming = true
WorkManager.getInstance(application).cancelAllWorkByTag("download") WorkManager.getInstance(application).cancelAllWorkByTag("download")
downloadManager.cancelAll()
val activeDownloadsList = withContext(Dispatchers.IO){ val activeDownloadsList = withContext(Dispatchers.IO){
getActiveDownloads() getActiveDownloads()
} }
activeDownloadsList.forEach {
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
}
delay(1000) delay(1000)
isPausingResuming = false isPausingResuming = false
repository.setDownloadStatusMultiple(activeDownloadsList.map { it.id }, DownloadRepository.Status.Paused) withContext(Dispatchers.IO) {
pausedAllDownloads.value = PausedAllDownloadsState.RESUME 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 pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING
isPausingResuming = true isPausingResuming = true
WorkManager.getInstance(application).cancelAllWorkByTag("download") WorkManager.getInstance(application).cancelAllWorkByTag("download")
downloadManager.cancelAll()
val paused = withContext(Dispatchers.IO) { val paused = withContext(Dispatchers.IO) {
dao.getPausedDownloadsList() 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.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdl.work.DownloadWorker import com.deniscerri.ytdl.work.downloader.DownloadWorker
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class ScheduleAlarmReceiver : BroadcastReceiver() { 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.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.VideoPlayerUtil 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.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -474,25 +474,16 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
override fun onCancelClick(itemID: Long) { override fun onCancelClick(itemID: Long) {
lifecycleScope.launch { 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){ withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item) downloadViewModel.cancelDownload(itemID)
} }
} }
} }
override fun onPauseClick(itemID: Long, position: Int) { override fun onPauseClick(itemID: Long, position: Int) {
lifecycleScope.launch { lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.updateToStatus(itemID, DownloadRepository.Status.Paused) downloadViewModel.pauseDownload(itemID)
} }
activeAdapter.notifyItemChanged(position) activeAdapter.notifyItemChanged(position)
} }
@ -511,7 +502,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
//dont remove //dont remove
@Subscribe(threadMode = ThreadMode.MAIN) @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 progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output") val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output")

View file

@ -24,16 +24,14 @@ import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem 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.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.NotificationUtil 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.badge.ExperimentalBadgeUtils
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -151,7 +149,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
//dont remove //dont remove
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) { fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) {
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress") val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output") val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output")
@ -176,15 +174,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
override fun onCancelClick(itemID: Long) { override fun onCancelClick(itemID: Long) {
lifecycleScope.launch { 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){ withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item) downloadViewModel.cancelDownload(itemID)
} }
} }
} }
@ -202,10 +193,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
override fun onPauseClick(itemID: Long) { override fun onPauseClick(itemID: Long) {
lifecycleScope.launch { lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){ 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() val selectedObjects = getSelectedIDs()
adapter.clearCheckedItems() adapter.clearCheckedItems()
for (id in selectedObjects){ for (id in selectedObjects){
YoutubeDL.getInstance().destroyProcessById(id.toInt().toString()) downloadViewModel.cancelDownloadOnly(id)
notificationUtil.cancelDownloadNotification(id.toInt())
} }
downloadViewModel.deleteAllWithID(selectedObjects) downloadViewModel.deleteAllWithID(selectedObjects)
actionMode?.finish() actionMode?.finish()
@ -524,20 +523,10 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
private fun cancelDownload(itemID: Long){ private fun cancelDownload(itemID: Long){
lifecycleScope.launch { lifecycleScope.launch {
cancelItem(itemID.toInt())
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID) downloadViewModel.cancelDownload(itemID)
}.let {
it.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(it)
}
} }
} }
} }
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) val checkbox = dialogView.findViewById<CheckBox>(R.id.doNotShowAgain)
terminateDialog.setView(dialogView) terminateDialog.setView(dialogView)
checkbox.setOnCheckedChangeListener { compoundButton, b -> checkbox.setOnCheckedChangeListener { compoundButton, b ->
doNotShowAgain = compoundButton.isChecked doNotShowAgain = compoundButton.isChecked
} }
val workManager = WorkManager.getInstance(requireContext())
val notificationUtil = NotificationUtil(requireContext())
terminateDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } terminateDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
terminateDialog.setPositiveButton(getString(R.string.ok)) { diag: DialogInterface?, _: Int -> terminateDialog.setPositiveButton(getString(R.string.ok)) { diag: DialogInterface?, _: Int ->
lifecycleScope.launch { lifecycleScope.launch {
val activeDownloads = withContext(Dispatchers.IO){ downloadViewModel.pauseAllDownloads()
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)
}
}
}
if (doNotShowAgain){ if (doNotShowAgain){
mainSharedPreferencesEditor.putBoolean("ask_terminate_app", false).apply() 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.enableTextHighlight
import com.deniscerri.ytdl.util.Extensions.setCustomTextSize import com.deniscerri.ytdl.util.Extensions.setCustomTextSize
import com.deniscerri.ytdl.util.FileUtil 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.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
@ -265,7 +266,7 @@ class DownloadLogFragment : Fragment() {
//dont remove //dont remove
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) { fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) {
val progressBar = requireView().findViewById<LinearProgressIndicator>(R.id.progress) val progressBar = requireView().findViewById<LinearProgressIndicator>(R.id.progress)
if (event.logItemID == logID) { if (event.logItemID == logID) {
progressBar.isVisible = event.progress < 100 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.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.CleanUpLeftoverDownloads 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.Calendar
import java.util.concurrent.TimeUnit 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.database.viewmodel.TerminalViewModel
import com.deniscerri.ytdl.ui.adapter.TerminalDownloadsAdapter import com.deniscerri.ytdl.ui.adapter.TerminalDownloadsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll 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.appbar.MaterialToolbar
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -84,7 +85,7 @@ class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnIte
} }
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) { fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) {
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress") val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output") 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.ui.more.terminal.TerminalActivity
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil 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.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -122,7 +124,7 @@ class TerminalDownloadWorker(
YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line -> YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line ->
runBlocking { 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) val title: String = command.take(65)
@ -142,7 +144,7 @@ class TerminalDownloadWorker(
//move file from internal to set download directory //move file from internal to set download directory
try { try {
FileUtil.moveFile(File(FileUtil.getCachePath(context) + "/TERMINAL/" + itemId),context, downloadLocation!!, false){ p -> 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){ }catch (e: Exception){
e.printStackTrace() 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.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.pm.ServiceInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
@ -15,10 +13,6 @@ import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.preference.PreferenceManager 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.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.App import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.MainActivity 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.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
@ -58,56 +52,72 @@ import java.io.File
import java.security.MessageDigest import java.security.MessageDigest
import java.util.Locale 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 dbManager: DBManager
private lateinit var dao : DownloadDao
private lateinit var historyDao: HistoryDao private lateinit var historyDao: HistoryDao
private lateinit var commandTemplateDao: CommandTemplateDao private lateinit var commandTemplateDao: CommandTemplateDao
private lateinit var logRepo: LogRepository private lateinit var logRepo: LogRepository
private lateinit var resultRepo: ResultRepository private lateinit var resultRepo: ResultRepository
private lateinit var ytdlpUtil: YTDLPUtil private lateinit var ytdlpUtil: YTDLPUtil
private lateinit var alarmScheduler : AlarmScheduler 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 eventBus: EventBus
private lateinit var resources: Resources
private lateinit var openDownloadQueue: PendingIntent
private lateinit var queueState : Flow<List<DownloadItem>> private lateinit var queueState : Flow<List<DownloadItem>>
lateinit var observeJob : Job private var observeJob : Job? = null
private val handler = Handler(Looper.getMainLooper()) private val handler = Handler(Looper.getMainLooper())
private val openDownloadQueue: PendingIntent = PendingIntent.getActivity( val isRunning get() = observeJob?.isActive == true
context,
1000000000,
Intent(context, MainActivity::class.java).run {
action = Intent.ACTION_VIEW
putExtra("destination", "Queue")
},
PendingIntent.FLAG_IMMUTABLE
)
override suspend fun getForegroundInfo(): ForegroundInfo { private fun getActiveDownloadsStore() : Set<String> {
val workNotif = NotificationUtil(App.instance).createDefaultWorkerNotification() return sharedPreferences.getStringSet("ytdlp_active_downloads", setOf())!!
return ForegroundInfo(
1000000000,
workNotif,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
)
} }
@SuppressLint("RestrictedApi") private fun addIDToActiveDownloadsStore(id: Long) {
override suspend fun doWork(): Result { 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 //init
notificationUtil = NotificationUtil(App.instance) notificationUtil = NotificationUtil(context)
dbManager = DBManager.getInstance(context) dbManager = DBManager.getInstance(context)
dao = dbManager.downloadDao dao = dbManager.downloadDao
historyDao = dbManager.historyDao historyDao = dbManager.historyDao
@ -119,6 +129,8 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
eventBus = EventBus.getDefault() eventBus = EventBus.getDefault()
sharedPreferences.edit().putStringSet("ytdlp_active_downloads", setOf()).apply()
val confTmp = Configuration(context.resources.configuration) val confTmp = Configuration(context.resources.configuration)
val locale = if (Build.VERSION.SDK_INT < 33) { val locale = if (Build.VERSION.SDK_INT < 33) {
sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language } sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language }
@ -133,13 +145,16 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
val metrics = DisplayMetrics() val metrics = DisplayMetrics()
resources = Resources(context.assets, metrics, confTmp) resources = Resources(context.assets, metrics, confTmp)
workManager = WorkManager.getInstance(context) openDownloadQueue = PendingIntent.getActivity(
if (workManager.isRunning("download")) return Result.Failure() 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 val time = System.currentTimeMillis() + 6000
queueState = if (priorityItemIDs.isEmpty()) { queueState = if (priorityItemIDs.isEmpty()) {
dao.getActiveQueuedScheduledDownloadsUntil(time) dao.getActiveQueuedScheduledDownloadsUntil(time)
@ -147,10 +162,8 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
dao.getActiveQueuedScheduledDownloadsUntilWithPriority(time, priorityItemIDs) dao.getActiveQueuedScheduledDownloadsUntilWithPriority(time, priorityItemIDs)
} }
val downloadJobs = mutableMapOf<Long, Job>() observeJob = CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
queueState.distinctUntilChanged().collectLatest { queue ->
observeJob = CoroutineScope(SupervisorJob()).launch {
queueState.collectLatest { queue ->
val runningCount = queue.asSequence() val runningCount = queue.asSequence()
.filter { it.status == DownloadRepository.Status.Active.toString() } .filter { it.status == DownloadRepository.Status.Active.toString() }
.count() .count()
@ -160,66 +173,53 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
val queueCount = queueItems.count() val queueCount = queueItems.count()
if (queueCount == 0 && runningCount == 0) { if (queueCount == 0 && runningCount == 0) {
observeJob.cancel() observeJob?.cancel()
return@collectLatest return@collectLatest
} }
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler){ if (useScheduler){
if (queueItems.none{ it.downloadStartTime > 0L } && runningCount == 0 && !alarmScheduler.isDuringTheScheduledTime()) { if (queueItems.none{ it.downloadStartTime > 0L } && runningCount == 0 && !alarmScheduler.isDuringTheScheduledTime()) {
observeJob.cancel() observeJob?.cancel()
return@collectLatest return@collectLatest
} }
} }
if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) {
observeJob?.cancel()
return@collectLatest
}
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - runningCount val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - runningCount
if (concurrentDownloads > 0) { if (concurrentDownloads > 0) {
//free spots are open //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 // Use supervisorScope to cancel child jobs when the downloader job is cancelled
supervisorScope { supervisorScope {
val queuedDownloads = queueItems val queuedDownloads = queueItems
.toList() .toList()
.take(concurrentDownloads) .take(concurrentDownloads)
val queuedIDs = queuedDownloads.map { it.id } priorityItemIDs.removeAll(queuedDownloads.map { it.id })
val downloadJobsToStop = downloadJobs.filter { it.key !in queuedIDs } val downloadsToStart = queuedDownloads.filter { it.id.toString() !in getActiveDownloadsStore() }
downloadJobsToStop.forEach { (download, job) ->
job.cancel()
downloadJobs.remove(download)
}
val downloadsToStart = queuedDownloads.filter { it.id !in downloadJobs }
downloadsToStart.forEach { downloadItem -> downloadsToStart.forEach { downloadItem ->
val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url }) val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url })
notificationUtil.notify(downloadItem.id.toInt(), notification) 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) @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 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) val request = ytdlpUtil.buildYoutubeDLRequest(downloadItem)
@ -270,7 +270,7 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
runCatching { runCatching {
YoutubeDL.getInstance().destroyProcessById(downloadItem.id.toString()) YoutubeDL.getInstance().destroyProcessById(downloadItem.id.toString())
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> 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 } val title: String = downloadItem.title.ifEmpty { downloadItem.url }
notificationUtil.updateDownloadNotification( notificationUtil.updateDownloadNotification(
downloadItem.id.toInt(), downloadItem.id.toInt(),
@ -288,12 +288,13 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
resultRepo.updateDownloadItem(downloadItem)?.apply { resultRepo.updateDownloadItem(downloadItem)?.apply {
dao.updateWithoutUpsert(this) dao.updateWithoutUpsert(this)
} }
removeIDFromActiveDownloadsStore(downloadItem.id)
//val wasQuickDownloaded = resultDao.getCountInt() == 0 //val wasQuickDownloaded = resultDao.getCountInt() == 0
runBlocking { runBlocking {
var finalPaths = mutableListOf<String>() var finalPaths = mutableListOf<String>()
if (noCache){ 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") val outputSequence = it.out.split("\n")
finalPaths = finalPaths =
outputSequence.asSequence() outputSequence.asSequence()
@ -315,17 +316,17 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
FileUtil.scanMedia(finalPaths, context) FileUtil.scanMedia(finalPaths, context)
}else{ }else{
//move file from internal to set download directory //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 { try {
finalPaths = withContext(Dispatchers.IO){ finalPaths = withContext(Dispatchers.IO){
FileUtil.moveFile(tempFileDir.absoluteFile, FileUtil.moveFile(tempFileDir.absoluteFile,
context, downloadLocation, keepCache){ p -> 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() }.filter { !it.matches("\\.(description)|(txt)\$".toRegex()) }.toMutableList()
if (finalPaths.isNotEmpty()){ 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){ }catch (e: Exception){
e.printStackTrace() e.printStackTrace()
@ -413,10 +414,11 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
}.onFailure { }.onFailure {
FileUtil.deleteConfigFiles(request) FileUtil.deleteConfigFiles(request)
removeIDFromActiveDownloadsStore(downloadItem.id)
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
} }
if (isStopped) return@onFailure if (!this.isActive) return@onFailure
if (it is YoutubeDL.CanceledException) return@onFailure if (it is YoutubeDL.CanceledException) return@onFailure
if (it.message?.contains("JSONDecodeError") == true) { if (it.message?.contains("JSONDecodeError") == true) {
val cachePath = "${FileUtil.getCachePath(context)}infojsons" val cachePath = "${FileUtil.getCachePath(context)}infojsons"
@ -456,19 +458,28 @@ class DownloadWorker(private val context: Context,workerParams: WorkerParameters
resources resources
) )
eventBus.post(WorkerProgress(100, it.toString(), downloadItem.id, downloadItem.logID)) eventBus.post(DownloadProgress(100, it.toString(), downloadItem.id, downloadItem.logID))
} }
} }
companion object { 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 progress: Int,
val output: String, val output: String,
val downloadItemID: Long, val downloadItemID: Long,
val logItemID: 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"
}
}