hmm
This commit is contained in:
parent
fd151105ef
commit
444e188549
14 changed files with 475 additions and 646 deletions
|
|
@ -107,15 +107,15 @@ interface DownloadDao {
|
|||
|
||||
@Query("""
|
||||
SELECT * FROM downloads
|
||||
WHERE status in ('Active', 'Queued', 'Scheduled') AND downloadStartTime <= :currentTime
|
||||
WHERE status in ('Queued', 'Scheduled') AND downloadStartTime <= :currentTime
|
||||
ORDER BY downloadStartTime, id
|
||||
LIMIT 10
|
||||
""")
|
||||
fun getActiveQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>>
|
||||
fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>>
|
||||
|
||||
@Query("""
|
||||
SELECT * FROM downloads
|
||||
WHERE status in ('Active', 'Queued', 'Scheduled') AND downloadStartTime <= :currentTime
|
||||
WHERE status in ('Queued', 'Scheduled') AND downloadStartTime <= :currentTime
|
||||
ORDER BY
|
||||
CASE
|
||||
WHEN id in (:priorityItems) THEN 0
|
||||
|
|
@ -124,7 +124,7 @@ interface DownloadDao {
|
|||
downloadStartTime, id
|
||||
LIMIT 10
|
||||
""")
|
||||
fun getActiveQueuedScheduledDownloadsUntilWithPriority(currentTime: Long, priorityItems: List<Long>) : Flow<List<DownloadItem>>
|
||||
fun getQueuedScheduledDownloadsUntilWithPriority(currentTime: Long, priorityItems: List<Long>) : Flow<List<DownloadItem>>
|
||||
|
||||
@Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id")
|
||||
fun getQueuedDownloadsList() : List<DownloadItem>
|
||||
|
|
|
|||
|
|
@ -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.downloader.DownloadWorker
|
||||
import com.deniscerri.ytdl.work.DownloadWorker
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import java.io.File
|
||||
|
|
|
|||
|
|
@ -10,13 +10,9 @@ import android.util.DisplayMetrics
|
|||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.media3.common.Player.Command
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.filter
|
||||
import androidx.paging.map
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
|
|
@ -49,7 +45,6 @@ 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
|
||||
|
|
@ -58,11 +53,8 @@ import kotlinx.coroutines.Dispatchers
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -99,9 +91,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
val erroredDownloadsCount : Flow<Int>
|
||||
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
|
||||
|
|
@ -148,7 +137,6 @@ 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
|
||||
|
|
@ -1296,38 +1284,39 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
}
|
||||
|
||||
fun cancelDownloadOnly(id : Long) {
|
||||
downloadManager.cancelDownload(id)
|
||||
YoutubeDL.getInstance().destroyProcessById(id.toString())
|
||||
notificationUtil.cancelDownloadNotification(id.toInt())
|
||||
}
|
||||
|
||||
suspend fun cancelDownload(id: Long) {
|
||||
downloadManager.cancelDownload(id)
|
||||
val item = getItemByID(id)
|
||||
item.status = DownloadRepository.Status.Cancelled.toString()
|
||||
updateDownload(item)
|
||||
cancelDownloadOnly(id)
|
||||
updateToStatus(id, DownloadRepository.Status.Cancelled)
|
||||
}
|
||||
|
||||
suspend fun pauseDownload(id: Long) {
|
||||
downloadManager.cancelDownload(id)
|
||||
cancelDownloadOnly(id)
|
||||
updateToStatus(id, DownloadRepository.Status.Paused)
|
||||
}
|
||||
|
||||
suspend fun pauseAllDownloads() {
|
||||
pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING
|
||||
isPausingResuming = true
|
||||
|
||||
withContext(Dispatchers.IO){
|
||||
downloadManager.cancelAll()
|
||||
}
|
||||
|
||||
WorkManager.getInstance(application).cancelAllWorkByTag("download")
|
||||
val activeDownloadsList = withContext(Dispatchers.IO){
|
||||
getActiveDownloads()
|
||||
}
|
||||
delay(1000)
|
||||
isPausingResuming = false
|
||||
|
||||
activeDownloadsList.forEach {
|
||||
cancelDownloadOnly(it.id)
|
||||
}
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
repository.setDownloadStatusMultiple(activeDownloadsList.map { it.id }, DownloadRepository.Status.Paused)
|
||||
}
|
||||
delay(1500)
|
||||
isPausingResuming = false
|
||||
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
pausedAllDownloads.value = PausedAllDownloadsState.RESUME
|
||||
}
|
||||
|
|
@ -1336,12 +1325,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
suspend fun resumeAllDownloads() {
|
||||
pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING
|
||||
isPausingResuming = true
|
||||
WorkManager.getInstance(application).cancelAllWorkByTag("download")
|
||||
|
||||
withContext(Dispatchers.IO){
|
||||
downloadManager.cancelAll()
|
||||
}
|
||||
|
||||
val paused = withContext(Dispatchers.IO) {
|
||||
dao.getPausedDownloadsList()
|
||||
}
|
||||
|
|
@ -1350,7 +1333,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
dbManager.downloadDao.resetPausedToQueued()
|
||||
repository.startDownloadWorker(paused, application)
|
||||
}
|
||||
delay(1000)
|
||||
delay(1500)
|
||||
isPausingResuming = false
|
||||
withContext(Dispatchers.Main) {
|
||||
pausedAllDownloads.value = PausedAllDownloadsState.PAUSE
|
||||
|
|
@ -1366,12 +1349,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
|
|||
cancelAllDownloadsImpl()
|
||||
}
|
||||
|
||||
private suspend fun cancelAllDownloadsImpl() {
|
||||
cancelActiveQueued()
|
||||
withContext(Dispatchers.IO) {
|
||||
downloadManager.cancelAll()
|
||||
}
|
||||
private fun cancelAllDownloadsImpl() {
|
||||
WorkManager.getInstance(application).cancelAllWorkByTag("download")
|
||||
cancelActiveQueued()
|
||||
}
|
||||
|
||||
fun resumeDownload(itemID: Long) = viewModelScope.launch {
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
package com.deniscerri.ytdl.di
|
||||
|
||||
class AppModule {
|
||||
|
||||
}
|
||||
|
|
@ -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.downloader.DownloadWorker
|
||||
import com.deniscerri.ytdl.work.DownloadWorker
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ScheduleAlarmReceiver : BroadcastReceiver() {
|
||||
|
|
|
|||
|
|
@ -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.downloader.DownloadWorker
|
||||
import com.deniscerri.ytdl.work.DownloadWorker
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
|
|
@ -502,7 +502,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
|
|||
|
||||
//dont remove
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onDownloadProgressEvent(event: com.deniscerri.ytdl.work.downloader.DownloadManager.DownloadProgress) {
|
||||
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
|
||||
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
|
||||
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output")
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ 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.downloader.DownloadManager
|
||||
import com.deniscerri.ytdl.work.DownloadWorker
|
||||
import com.google.android.material.badge.ExperimentalBadgeUtils
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
|
|
@ -149,7 +149,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
|
|||
|
||||
//dont remove
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) {
|
||||
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
|
||||
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
|
||||
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output")
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,7 @@ 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.downloader.DownloadManager
|
||||
import com.deniscerri.ytdl.work.downloader.DownloadWorker
|
||||
import com.deniscerri.ytdl.work.DownloadWorker
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.bottomappbar.BottomAppBar
|
||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
|
|
@ -266,7 +265,7 @@ class DownloadLogFragment : Fragment() {
|
|||
|
||||
//dont remove
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) {
|
||||
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
|
||||
val progressBar = requireView().findViewById<LinearProgressIndicator>(R.id.progress)
|
||||
if (event.logItemID == logID) {
|
||||
progressBar.isVisible = event.progress < 100
|
||||
|
|
|
|||
|
|
@ -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.downloader.DownloadWorker
|
||||
import com.deniscerri.ytdl.work.DownloadWorker
|
||||
import java.util.Calendar
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,7 @@ 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.downloader.DownloadManager
|
||||
import com.deniscerri.ytdl.work.downloader.DownloadWorker
|
||||
import com.deniscerri.ytdl.work.DownloadWorker
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
|
|
@ -85,7 +84,7 @@ class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnIte
|
|||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onDownloadProgressEvent(event: DownloadManager.DownloadProgress) {
|
||||
fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) {
|
||||
val progressBar = requireView().findViewWithTag<LinearProgressIndicator>("${event.downloadItemID}##progress")
|
||||
val outputText = requireView().findViewWithTag<TextView>("${event.downloadItemID}##output")
|
||||
|
||||
|
|
|
|||
441
app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt
Normal file
441
app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
package com.deniscerri.ytdl.work
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
|
||||
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.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.yausername.youtubedl_android.YoutubeDL
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
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) {
|
||||
|
||||
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
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
@SuppressLint("RestrictedApi")
|
||||
override suspend fun doWork(): Result {
|
||||
val workManager = WorkManager.getInstance(context)
|
||||
if (workManager.isRunning("download") || isStopped) return Result.Failure()
|
||||
|
||||
setForegroundSafely()
|
||||
|
||||
val notificationUtil = NotificationUtil(App.instance)
|
||||
val dbManager = DBManager.getInstance(context)
|
||||
val dao = dbManager.downloadDao
|
||||
val historyDao = dbManager.historyDao
|
||||
val commandTemplateDao = dbManager.commandTemplateDao
|
||||
val logRepo = LogRepository(dbManager.logDao)
|
||||
val resultRepo = ResultRepository(dbManager.resultDao, commandTemplateDao, context)
|
||||
val ytdlpUtil = YTDLPUtil(context, commandTemplateDao)
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
val alarmScheduler = AlarmScheduler(context)
|
||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
val time = System.currentTimeMillis() + 6000
|
||||
val priorityItemIDs = (inputData.getLongArray("priority_item_ids") ?: longArrayOf()).toMutableList()
|
||||
val continueAfterPriorityIds = inputData.getBoolean("continue_after_priority_ids", true)
|
||||
val queuedItems = if (priorityItemIDs.isEmpty()) {
|
||||
dao.getQueuedScheduledDownloadsUntil(time)
|
||||
}else {
|
||||
dao.getQueuedScheduledDownloadsUntilWithPriority(time, priorityItemIDs)
|
||||
}
|
||||
|
||||
// this is needed for observe sources call, so it wont create result items
|
||||
// [removed]
|
||||
//val createResultItem = inputData.getBoolean("createResultItem", true)
|
||||
|
||||
val confTmp = Configuration(context.resources.configuration)
|
||||
val locale = if (Build.VERSION.SDK_INT < 33) {
|
||||
sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language }
|
||||
}else{
|
||||
Locale.getDefault().language
|
||||
}.run {
|
||||
split("-")
|
||||
}.run {
|
||||
if (this.size == 1) Locale(this[0]) else Locale(this[0], this[1])
|
||||
}
|
||||
confTmp.setLocale(locale)
|
||||
val metrics = DisplayMetrics()
|
||||
val resources = Resources(context.assets, metrics, confTmp)
|
||||
|
||||
val openQueueIntent = Intent(context, MainActivity::class.java)
|
||||
openQueueIntent.setAction(Intent.ACTION_VIEW)
|
||||
openQueueIntent.putExtra("destination", "Queue")
|
||||
val openDownloadQueue = PendingIntent.getActivity(
|
||||
context,
|
||||
1000000000,
|
||||
openQueueIntent,
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
queuedItems.collectLatest { items ->
|
||||
if (this@DownloadWorker.isStopped) return@collectLatest
|
||||
|
||||
runningYTDLInstances.clear()
|
||||
val activeDownloads = dao.getActiveDownloadsList()
|
||||
activeDownloads.forEach {
|
||||
runningYTDLInstances.add(it.id)
|
||||
}
|
||||
|
||||
val running = ArrayList(runningYTDLInstances)
|
||||
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
|
||||
if (items.isEmpty() && running.isEmpty()) {
|
||||
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
if (useScheduler){
|
||||
if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) {
|
||||
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
|
||||
return@collectLatest
|
||||
}
|
||||
}
|
||||
|
||||
if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) {
|
||||
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
|
||||
val eligibleDownloads = if (priorityItemIDs.isNotEmpty()) {
|
||||
val tmp = priorityItemIDs.take(concurrentDownloads)
|
||||
items.filter { it.id !in running && tmp.contains(it.id) }
|
||||
}else{
|
||||
items.take(concurrentDownloads).filter { it.id !in running }
|
||||
}
|
||||
|
||||
eligibleDownloads.forEach{downloadItem ->
|
||||
val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url })
|
||||
notificationUtil.notify(downloadItem.id.toInt(), notification)
|
||||
|
||||
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 request = ytdlpUtil.buildYoutubeDLRequest(downloadItem)
|
||||
downloadItem.status = DownloadRepository.Status.Active.toString()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(1500)
|
||||
//update item if its incomplete
|
||||
resultRepo.updateDownloadItem(downloadItem)?.apply {
|
||||
val status = dao.checkStatus(this.id)
|
||||
if (status == DownloadRepository.Status.Active){
|
||||
dao.updateWithoutUpsert(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val cacheDir = FileUtil.getCachePath(context)
|
||||
val tempFileDir = File(cacheDir, downloadItem.id.toString())
|
||||
tempFileDir.delete()
|
||||
tempFileDir.mkdirs()
|
||||
|
||||
val downloadLocation = downloadItem.downloadPath
|
||||
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
|
||||
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !downloadItem.incognito
|
||||
|
||||
|
||||
val commandString = ytdlpUtil.parseYTDLRequestString(request)
|
||||
val initialLogDetails = "Downloading:\n" +
|
||||
"Title: ${downloadItem.title}\n" +
|
||||
"URL: ${downloadItem.url}\n" +
|
||||
"Type: ${downloadItem.type}\n" +
|
||||
"Command:\n$commandString \n\n"
|
||||
val logString = StringBuilder(initialLogDetails)
|
||||
val logItem = LogItem(
|
||||
0,
|
||||
downloadItem.title.ifBlank { downloadItem.url },
|
||||
logString.toString(),
|
||||
downloadItem.format,
|
||||
downloadItem.type,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
|
||||
runBlocking {
|
||||
if (logDownloads) logItem.id = logRepo.insert(logItem)
|
||||
downloadItem.logID = logItem.id
|
||||
dao.update(downloadItem)
|
||||
}
|
||||
|
||||
val eventBus = EventBus.getDefault()
|
||||
|
||||
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))
|
||||
val title: String = downloadItem.title.ifEmpty { downloadItem.url }
|
||||
notificationUtil.updateDownloadNotification(
|
||||
downloadItem.id.toInt(),
|
||||
line, progress.toInt(), 0, title,
|
||||
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
|
||||
)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (logDownloads) {
|
||||
logRepo.update(line, logItem.id)
|
||||
logString.append("$line\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
resultRepo.updateDownloadItem(downloadItem)?.apply {
|
||||
dao.updateWithoutUpsert(this)
|
||||
}
|
||||
//val wasQuickDownloaded = resultDao.getCountInt() == 0
|
||||
runBlocking {
|
||||
var finalPaths = mutableListOf<String>()
|
||||
|
||||
if (noCache){
|
||||
eventBus.post(WorkerProgress(100, "Scanning Files", downloadItem.id, downloadItem.logID))
|
||||
val outputSequence = it.out.split("\n")
|
||||
finalPaths =
|
||||
outputSequence.asSequence()
|
||||
.filter { it.startsWith("'/storage") }
|
||||
.map { it.removeSuffix("\n") }
|
||||
.map { it.removeSurrounding("'", "'") }
|
||||
.toMutableList()
|
||||
|
||||
finalPaths.addAll(
|
||||
outputSequence.asSequence()
|
||||
.filter { it.startsWith("[SplitChapters]") && it.contains("Destination: ") }
|
||||
.map { it.split("Destination: ")[1] }
|
||||
.map { it.removeSuffix("\n") }
|
||||
.toList()
|
||||
)
|
||||
|
||||
finalPaths.sortBy { File(it).lastModified() }
|
||||
finalPaths = finalPaths.distinct().toMutableList()
|
||||
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))
|
||||
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))
|
||||
}
|
||||
}.filter { !it.matches("\\.(description)|(txt)\$".toRegex()) }.toMutableList()
|
||||
|
||||
if (finalPaths.isNotEmpty()){
|
||||
eventBus.post(WorkerProgress(100, "Moved file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID))
|
||||
}
|
||||
}catch (e: Exception){
|
||||
e.printStackTrace()
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val nonMediaExtensions = mutableListOf<String>().apply {
|
||||
addAll(context.getStringArray(R.array.thumbnail_containers_values))
|
||||
addAll(context.getStringArray(R.array.sub_formats_values).filter { it.isNotBlank() })
|
||||
add("description")
|
||||
add("txt")
|
||||
}
|
||||
finalPaths = finalPaths.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }.toMutableList()
|
||||
FileUtil.deleteConfigFiles(request)
|
||||
|
||||
//put download in history
|
||||
if (!downloadItem.incognito) {
|
||||
if (request.hasOption("--download-archive") && finalPaths.isEmpty()) {
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
|
||||
}, 100)
|
||||
}else{
|
||||
if (finalPaths.isNotEmpty()) {
|
||||
val unixTime = System.currentTimeMillis() / 1000
|
||||
finalPaths.first().apply {
|
||||
val file = File(this)
|
||||
var duration = downloadItem.duration
|
||||
val d = file.getMediaDuration(context)
|
||||
if (d > 0) duration = d.toStringDuration(Locale.US)
|
||||
|
||||
downloadItem.format.filesize = file.length()
|
||||
downloadItem.format.container = file.extension
|
||||
downloadItem.duration = duration
|
||||
}
|
||||
|
||||
val historyItem = HistoryItem(0,
|
||||
downloadItem.url,
|
||||
downloadItem.title,
|
||||
downloadItem.author,
|
||||
downloadItem.duration,
|
||||
downloadItem.thumb,
|
||||
downloadItem.type,
|
||||
unixTime,
|
||||
finalPaths,
|
||||
downloadItem.website,
|
||||
downloadItem.format,
|
||||
downloadItem.format.filesize,
|
||||
downloadItem.id,
|
||||
commandString)
|
||||
historyDao.insert(historyItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
|
||||
notificationUtil.createDownloadFinished(
|
||||
downloadItem.id, downloadItem.title, downloadItem.type, if (finalPaths.isEmpty()) null else finalPaths, resources
|
||||
)
|
||||
}
|
||||
|
||||
// if (wasQuickDownloaded && createResultItem){
|
||||
// runCatching {
|
||||
// eventBus.post(WorkerProgress(100, "Creating Result Items", downloadItem.id))
|
||||
// runBlocking {
|
||||
// infoUtil.getFromYTDL(downloadItem.url).forEach { res ->
|
||||
// if (res != null) {
|
||||
// resultDao.insert(res)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
dao.delete(downloadItem.id)
|
||||
|
||||
if (logDownloads){
|
||||
logRepo.update(initialLogDetails + it.out, logItem.id, true)
|
||||
}
|
||||
}
|
||||
|
||||
}.onFailure {
|
||||
FileUtil.deleteConfigFiles(request)
|
||||
withContext(Dispatchers.Main){
|
||||
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
|
||||
}
|
||||
if (this@DownloadWorker.isStopped) return@onFailure
|
||||
if (it is YoutubeDL.CanceledException) return@onFailure
|
||||
if (it.message?.contains("JSONDecodeError") == true) {
|
||||
val cachePath = "${FileUtil.getCachePath(context)}infojsons"
|
||||
val infoJsonName = MessageDigest.getInstance("MD5").digest(downloadItem.url.toByteArray()).toHexString()
|
||||
FileUtil.deleteFile("${cachePath}/${infoJsonName}.info.json")
|
||||
}
|
||||
|
||||
if(it.message != null){
|
||||
if (logDownloads){
|
||||
logRepo.update(it.message!!, logItem.id)
|
||||
}else{
|
||||
logString.append("${it.message}\n")
|
||||
logItem.content = logString.toString()
|
||||
val logID = logRepo.insert(logItem)
|
||||
downloadItem.logID = logID
|
||||
}
|
||||
}
|
||||
|
||||
tempFileDir.delete()
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, it.message, Toast.LENGTH_LONG).show()
|
||||
}, 1000)
|
||||
|
||||
Log.e(TAG, context.getString(R.string.failed_download), it)
|
||||
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
|
||||
|
||||
downloadItem.status = DownloadRepository.Status.Error.toString()
|
||||
runBlocking {
|
||||
dao.update(downloadItem)
|
||||
}
|
||||
|
||||
notificationUtil.createDownloadErrored(
|
||||
downloadItem.id,
|
||||
downloadItem.title.ifEmpty { downloadItem.url },
|
||||
it.message,
|
||||
downloadItem.logID,
|
||||
resources
|
||||
)
|
||||
|
||||
eventBus.post(WorkerProgress(100, it.toString(), downloadItem.id, downloadItem.logID))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (eligibleDownloads.isNotEmpty()){
|
||||
eligibleDownloads.forEach {
|
||||
it.status = DownloadRepository.Status.Active.toString()
|
||||
priorityItemIDs.remove(it.id)
|
||||
}
|
||||
dao.updateMultiple(eligibleDownloads)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
|
||||
|
||||
companion object {
|
||||
val runningYTDLInstances: MutableList<Long> = mutableListOf()
|
||||
const val TAG = "DownloadWorker"
|
||||
}
|
||||
|
||||
class WorkerProgress(
|
||||
val progress: Int,
|
||||
val output: String,
|
||||
val downloadItemID: Long,
|
||||
val logItemID: Long?
|
||||
)
|
||||
|
||||
}
|
||||
|
|
@ -22,8 +22,6 @@ 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
|
||||
|
|
@ -124,7 +122,7 @@ class TerminalDownloadWorker(
|
|||
|
||||
YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line ->
|
||||
runBlocking {
|
||||
eventBus.post(DownloadManager.DownloadProgress(progress.toInt(), line, itemId.toLong(), logItem.id))
|
||||
eventBus.post(DownloadWorker.WorkerProgress(progress.toInt(), line, itemId.toLong(), logItem.id))
|
||||
}
|
||||
|
||||
val title: String = command.take(65)
|
||||
|
|
@ -144,7 +142,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(DownloadManager.DownloadProgress(p, "", itemId.toLong(), logItem.id))
|
||||
eventBus.post(DownloadWorker.WorkerProgress(p, "", itemId.toLong(), logItem.id))
|
||||
}
|
||||
}catch (e: Exception){
|
||||
e.printStackTrace()
|
||||
|
|
|
|||
|
|
@ -1,479 +0,0 @@
|
|||
package com.deniscerri.ytdl.work.downloader
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
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 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.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.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.isActive
|
||||
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 DownloadManager private constructor() {
|
||||
private var notificationUtil: NotificationUtil = NotificationUtil(App.instance)
|
||||
private var sharedPreferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(App.instance)
|
||||
private var dbManager: DBManager = DBManager.getInstance(App.instance)
|
||||
private var dao : DownloadDao = dbManager.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 eventBus: EventBus
|
||||
private lateinit var resources: Resources
|
||||
|
||||
private lateinit var openDownloadQueue: PendingIntent
|
||||
|
||||
private lateinit var queueState : Flow<List<DownloadItem>>
|
||||
private var observeJob : Job? = null
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
|
||||
val isRunning get() = observeJob?.isActive == true
|
||||
|
||||
fun cancelDownload(id: Long) {
|
||||
YoutubeDL.getInstance().destroyProcessById(id.toString())
|
||||
notificationUtil.cancelDownloadNotification(id.toInt())
|
||||
}
|
||||
|
||||
fun cancelAll() {
|
||||
observeJob?.cancel()
|
||||
observeJob = null
|
||||
val activeDownloads = dao.getActiveDownloadsList()
|
||||
activeDownloads.forEach {
|
||||
cancelDownload(it.id)
|
||||
}
|
||||
}
|
||||
|
||||
//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(context)
|
||||
dbManager = DBManager.getInstance(context)
|
||||
dao = dbManager.downloadDao
|
||||
historyDao = dbManager.historyDao
|
||||
commandTemplateDao = dbManager.commandTemplateDao
|
||||
logRepo = LogRepository(dbManager.logDao)
|
||||
resultRepo = ResultRepository(dbManager.resultDao, commandTemplateDao, context)
|
||||
ytdlpUtil = YTDLPUtil(context, commandTemplateDao)
|
||||
alarmScheduler = AlarmScheduler(context)
|
||||
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 }
|
||||
}else{
|
||||
Locale.getDefault().language
|
||||
}.run {
|
||||
split("-")
|
||||
}.run {
|
||||
if (this.size == 1) Locale(this[0]) else Locale(this[0], this[1])
|
||||
}
|
||||
confTmp.setLocale(locale)
|
||||
val metrics = DisplayMetrics()
|
||||
resources = Resources(context.assets, metrics, confTmp)
|
||||
|
||||
openDownloadQueue = PendingIntent.getActivity(
|
||||
context,
|
||||
1000000000,
|
||||
Intent(context, MainActivity::class.java).run {
|
||||
action = Intent.ACTION_VIEW
|
||||
putExtra("destination", "Queue")
|
||||
},
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val time = System.currentTimeMillis() + 6000
|
||||
queueState = if (priorityItemIDs.isEmpty()) {
|
||||
dao.getActiveQueuedScheduledDownloadsUntil(time)
|
||||
}else {
|
||||
dao.getActiveQueuedScheduledDownloadsUntilWithPriority(time, priorityItemIDs)
|
||||
}
|
||||
|
||||
observeJob = CoroutineScope(SupervisorJob() + Dispatchers.IO).launch {
|
||||
supervisorScope {
|
||||
queueState.distinctUntilChanged().collectLatest { queue ->
|
||||
val activeDownloads = queue
|
||||
.asSequence()
|
||||
.filter { it.status == DownloadRepository.Status.Active.toString() }
|
||||
.map { it.id }
|
||||
|
||||
val activeDownloadsCount = activeDownloads.count()
|
||||
|
||||
val queueItems = queue
|
||||
.asSequence()
|
||||
.filter { it.status != DownloadRepository.Status.Active.toString() }
|
||||
|
||||
val queueCount = queueItems.count()
|
||||
if (queueCount == 0 && activeDownloadsCount == 0) {
|
||||
observeJob?.cancel()
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
if (!isRunning) {
|
||||
observeJob?.cancel()
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
|
||||
if (useScheduler){
|
||||
if (queueItems.none{ it.downloadStartTime > 0L } && activeDownloadsCount == 0 && !alarmScheduler.isDuringTheScheduledTime()) {
|
||||
observeJob?.cancel()
|
||||
return@collectLatest
|
||||
}
|
||||
}
|
||||
|
||||
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - activeDownloadsCount
|
||||
if (concurrentDownloads > 0) {
|
||||
|
||||
supervisorScope {
|
||||
//free spots are open
|
||||
val queuedDownloads = if (priorityItemIDs.isNotEmpty() && !continueAfterPriorityIds) {
|
||||
queueItems
|
||||
.filter { priorityItemIDs.contains(it.id) }
|
||||
.toList()
|
||||
}else{
|
||||
queueItems.toList()
|
||||
}.take(concurrentDownloads)
|
||||
|
||||
|
||||
priorityItemIDs.removeAll(queuedDownloads.map { it.id })
|
||||
val downloadsToStart = queuedDownloads.filter { it.id !in activeDownloads }
|
||||
downloadsToStart.forEach { downloadItem ->
|
||||
val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url })
|
||||
notificationUtil.notify(downloadItem.id.toInt(), notification)
|
||||
launchDownloadJob(context, downloadItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) {
|
||||
observeJob?.cancel()
|
||||
return@collectLatest
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
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 request = ytdlpUtil.buildYoutubeDLRequest(downloadItem)
|
||||
|
||||
val cacheDir = FileUtil.getCachePath(context)
|
||||
val tempFileDir = File(cacheDir, downloadItem.id.toString())
|
||||
tempFileDir.delete()
|
||||
tempFileDir.mkdirs()
|
||||
|
||||
val downloadLocation = downloadItem.downloadPath
|
||||
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
|
||||
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !downloadItem.incognito
|
||||
|
||||
val commandString = ytdlpUtil.parseYTDLRequestString(request)
|
||||
val initialLogDetails = "Downloading:\n" +
|
||||
"Title: ${downloadItem.title}\n" +
|
||||
"URL: ${downloadItem.url}\n" +
|
||||
"Type: ${downloadItem.type}\n" +
|
||||
"Command:\n$commandString \n\n"
|
||||
val logString = StringBuilder(initialLogDetails)
|
||||
val logItem = LogItem(
|
||||
0,
|
||||
downloadItem.title.ifBlank { downloadItem.url },
|
||||
logString.toString(),
|
||||
downloadItem.format,
|
||||
downloadItem.type,
|
||||
System.currentTimeMillis(),
|
||||
)
|
||||
|
||||
|
||||
runBlocking {
|
||||
if (logDownloads) logItem.id = logRepo.insert(logItem)
|
||||
downloadItem.logID = logItem.id
|
||||
downloadItem.status = DownloadRepository.Status.Active.toString()
|
||||
dao.update(downloadItem)
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(1500)
|
||||
//update item if its incomplete
|
||||
resultRepo.updateDownloadItem(downloadItem)?.apply {
|
||||
val status = dao.checkStatus(id)
|
||||
if (status == DownloadRepository.Status.Active){
|
||||
dao.updateWithoutUpsert(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
runCatching {
|
||||
YoutubeDL.getInstance().destroyProcessById(downloadItem.id.toString())
|
||||
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
|
||||
eventBus.post(DownloadProgress(progress.toInt(), line, downloadItem.id, downloadItem.logID))
|
||||
val title: String = downloadItem.title.ifEmpty { downloadItem.url }
|
||||
notificationUtil.updateDownloadNotification(
|
||||
downloadItem.id.toInt(),
|
||||
line, progress.toInt(), 0, title,
|
||||
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
|
||||
)
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (logDownloads) {
|
||||
logRepo.update(line, logItem.id)
|
||||
logString.append("$line\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
resultRepo.updateDownloadItem(downloadItem)?.apply {
|
||||
dao.updateWithoutUpsert(this)
|
||||
}
|
||||
//val wasQuickDownloaded = resultDao.getCountInt() == 0
|
||||
runBlocking {
|
||||
var finalPaths = mutableListOf<String>()
|
||||
|
||||
if (noCache){
|
||||
eventBus.post(DownloadProgress(100, "Scanning Files", downloadItem.id, downloadItem.logID))
|
||||
val outputSequence = it.out.split("\n")
|
||||
finalPaths =
|
||||
outputSequence.asSequence()
|
||||
.filter { it.startsWith("'/storage") }
|
||||
.map { it.removeSuffix("\n") }
|
||||
.map { it.removeSurrounding("'", "'") }
|
||||
.toMutableList()
|
||||
|
||||
finalPaths.addAll(
|
||||
outputSequence.asSequence()
|
||||
.filter { it.startsWith("[SplitChapters]") && it.contains("Destination: ") }
|
||||
.map { it.split("Destination: ")[1] }
|
||||
.map { it.removeSuffix("\n") }
|
||||
.toList()
|
||||
)
|
||||
|
||||
finalPaths.sortBy { File(it).lastModified() }
|
||||
finalPaths = finalPaths.distinct().toMutableList()
|
||||
FileUtil.scanMedia(finalPaths, context)
|
||||
}else{
|
||||
//move file from internal to set download directory
|
||||
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(DownloadProgress(p, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID))
|
||||
}
|
||||
}.filter { !it.matches("\\.(description)|(txt)\$".toRegex()) }.toMutableList()
|
||||
|
||||
if (finalPaths.isNotEmpty()){
|
||||
eventBus.post(DownloadProgress(100, "Moved file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id, downloadItem.logID))
|
||||
}
|
||||
}catch (e: Exception){
|
||||
e.printStackTrace()
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val nonMediaExtensions = mutableListOf<String>().apply {
|
||||
addAll(context.getStringArray(R.array.thumbnail_containers_values))
|
||||
addAll(context.getStringArray(R.array.sub_formats_values).filter { it.isNotBlank() })
|
||||
add("description")
|
||||
add("txt")
|
||||
}
|
||||
finalPaths = finalPaths.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }.toMutableList()
|
||||
FileUtil.deleteConfigFiles(request)
|
||||
|
||||
//put download in history
|
||||
if (!downloadItem.incognito) {
|
||||
if (request.hasOption("--download-archive") && finalPaths.isEmpty()) {
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
|
||||
}, 100)
|
||||
}else{
|
||||
if (finalPaths.isNotEmpty()) {
|
||||
val unixTime = System.currentTimeMillis() / 1000
|
||||
finalPaths.first().apply {
|
||||
val file = File(this)
|
||||
var duration = downloadItem.duration
|
||||
val d = file.getMediaDuration(context)
|
||||
if (d > 0) duration = d.toStringDuration(Locale.US)
|
||||
|
||||
downloadItem.format.filesize = file.length()
|
||||
downloadItem.format.container = file.extension
|
||||
downloadItem.duration = duration
|
||||
}
|
||||
|
||||
val historyItem = HistoryItem(0,
|
||||
downloadItem.url,
|
||||
downloadItem.title,
|
||||
downloadItem.author,
|
||||
downloadItem.duration,
|
||||
downloadItem.thumb,
|
||||
downloadItem.type,
|
||||
unixTime,
|
||||
finalPaths,
|
||||
downloadItem.website,
|
||||
downloadItem.format,
|
||||
downloadItem.format.filesize,
|
||||
downloadItem.id,
|
||||
commandString)
|
||||
historyDao.insert(historyItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
|
||||
notificationUtil.createDownloadFinished(
|
||||
downloadItem.id, downloadItem.title, downloadItem.type, if (finalPaths.isEmpty()) null else finalPaths, resources
|
||||
)
|
||||
}
|
||||
|
||||
// if (wasQuickDownloaded && createResultItem){
|
||||
// runCatching {
|
||||
// eventBus.post(WorkerProgress(100, "Creating Result Items", downloadItem.id))
|
||||
// runBlocking {
|
||||
// infoUtil.getFromYTDL(downloadItem.url).forEach { res ->
|
||||
// if (res != null) {
|
||||
// resultDao.insert(res)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
dao.delete(downloadItem.id)
|
||||
|
||||
if (logDownloads){
|
||||
logRepo.update(initialLogDetails + it.out, logItem.id, true)
|
||||
}
|
||||
}
|
||||
|
||||
}.onFailure {
|
||||
FileUtil.deleteConfigFiles(request)
|
||||
withContext(Dispatchers.Main){
|
||||
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
|
||||
}
|
||||
if (!this.isActive) return@onFailure
|
||||
if (it is YoutubeDL.CanceledException) return@onFailure
|
||||
if (it.message?.contains("JSONDecodeError") == true) {
|
||||
val cachePath = "${FileUtil.getCachePath(context)}infojsons"
|
||||
val infoJsonName = MessageDigest.getInstance("MD5").digest(downloadItem.url.toByteArray()).toHexString()
|
||||
FileUtil.deleteFile("${cachePath}/${infoJsonName}.info.json")
|
||||
}
|
||||
|
||||
if(it.message != null){
|
||||
if (logDownloads){
|
||||
logRepo.update(it.message!!, logItem.id)
|
||||
}else{
|
||||
logString.append("${it.message}\n")
|
||||
logItem.content = logString.toString()
|
||||
val logID = logRepo.insert(logItem)
|
||||
downloadItem.logID = logID
|
||||
}
|
||||
}
|
||||
|
||||
tempFileDir.delete()
|
||||
handler.postDelayed({
|
||||
Toast.makeText(context, it.message, Toast.LENGTH_LONG).show()
|
||||
}, 1000)
|
||||
|
||||
Log.e(TAG, context.getString(R.string.failed_download), it)
|
||||
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
|
||||
|
||||
downloadItem.status = DownloadRepository.Status.Error.toString()
|
||||
runBlocking {
|
||||
dao.update(downloadItem)
|
||||
}
|
||||
|
||||
notificationUtil.createDownloadErrored(
|
||||
downloadItem.id,
|
||||
downloadItem.title.ifEmpty { downloadItem.url },
|
||||
it.message,
|
||||
downloadItem.logID,
|
||||
resources
|
||||
)
|
||||
|
||||
eventBus.post(DownloadProgress(100, it.toString(), downloadItem.id, downloadItem.logID))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG = "DownloadManager"
|
||||
@Volatile
|
||||
private var instance: DownloadManager? = null
|
||||
|
||||
fun getInstance() : DownloadManager {
|
||||
return instance ?: synchronized(this) {
|
||||
instance ?: DownloadManager().also {
|
||||
instance = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadProgress(
|
||||
val progress: Int,
|
||||
val output: String,
|
||||
val downloadItemID: Long,
|
||||
val logItemID: Long?
|
||||
)
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
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"
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue