fix downloads not continuing in the queue

This commit is contained in:
deniscerri 2025-03-22 21:37:08 +01:00
parent 78fd2427eb
commit 3b40ba7e02
No known key found for this signature in database
GPG key ID: 95C43D517D830350
19 changed files with 446 additions and 415 deletions

View file

@ -503,7 +503,7 @@
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="specialUse"/>
android:foregroundServiceType="dataSync"/>
</application>
</manifest>

View file

@ -107,19 +107,24 @@ interface DownloadDao {
@Query("""
SELECT * FROM downloads
WHERE status in ('Queued', 'Scheduled') AND downloadStartTime <= :currentTime
WHERE status in ('Active', 'Queued', 'Scheduled') AND downloadStartTime <= :currentTime
ORDER BY downloadStartTime, id
LIMIT 20
LIMIT 10
""")
fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>>
fun getActiveQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>>
@Query("""
SELECT * FROM downloads
WHERE id in (:priorityItems) AND status in ('Queued', 'Scheduled') AND downloadStartTime <= :currentTime
ORDER BY downloadStartTime, id
LIMIT 20
WHERE status in ('Active','Queued', 'Scheduled') AND downloadStartTime <= :currentTime
ORDER BY
CASE
WHEN id in (:priorityItems) THEN 0
ELSE 1
END,
downloadStartTime, id
LIMIT 10
""")
fun getQueuedScheduledDownloadsUntilWithPriority(currentTime: Long, priorityItems: List<Long>) : Flow<List<DownloadItem>>
fun getActiveQueuedScheduledDownloadsUntilWithPriority(currentTime: Long, priorityItems: List<Long>) : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id")
fun getQueuedDownloadsList() : List<DownloadItem>

View file

@ -3,11 +3,7 @@ package com.deniscerri.ytdl.database.repository
import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import android.os.Handler
import android.os.Looper
import android.text.format.DateFormat
import android.widget.Toast
import androidx.core.os.postDelayed
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.preference.PreferenceManager
@ -16,9 +12,7 @@ import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.await
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.dao.DownloadDao
@ -31,7 +25,6 @@ import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.DownloadWorker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import java.io.File
import java.text.SimpleDateFormat
import java.util.Locale

View file

@ -3,14 +3,11 @@ package com.deniscerri.ytdl.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.work.CancelScheduledDownloadWorker
import com.deniscerri.ytdl.work.DownloadWorker
import java.util.concurrent.TimeUnit
class CancelScheduleAlarmReceiver : BroadcastReceiver() {

View file

@ -12,7 +12,6 @@ import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
@ -20,8 +19,6 @@ import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider

View file

@ -1,80 +1,42 @@
package com.deniscerri.ytdl.ui.downloads
import android.animation.AnimatorSet
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.LinearLayout
import android.widget.ListView
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.setMargins
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.room.util.getColumnIndexOrThrow
import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdl.ui.adapter.QueuedDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.DownloadWorker
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.snackbar.Snackbar
import com.yausername.youtubedl_android.YoutubeDL
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe

View file

@ -6,8 +6,6 @@ import android.os.Build
import android.os.Bundle
import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.interaction.DragInteraction
import androidx.core.content.edit
import androidx.navigation.fragment.findNavController
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference

View file

@ -15,7 +15,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.TerminalItem
import com.deniscerri.ytdl.database.viewmodel.TerminalViewModel

View file

@ -40,20 +40,15 @@ import com.deniscerri.ytdl.util.Extensions.setCustomTextSize
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil
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
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.slider.Slider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import kotlin.properties.Delegates

View file

@ -1280,22 +1280,24 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
val poTokens = mutableListOf<String>()
val configuredPlayerClientsRaw = sharedPreferences.getString("youtube_player_clients", "[]")!!
val configuredPlayerClients = Gson().fromJson(configuredPlayerClientsRaw, Array<YoutubePlayerClientItem>::class.java).toMutableList()
kotlin.runCatching {
val configuredPlayerClients = Gson().fromJson(configuredPlayerClientsRaw, Array<YoutubePlayerClientItem>::class.java).toMutableList()
for (value in configuredPlayerClients) {
if (value.enabled) {
if (!value.useOnlyPoToken) {
playerClients.add(value.playerClient)
}
for (value in configuredPlayerClients) {
if (value.enabled) {
if (!value.useOnlyPoToken) {
playerClients.add(value.playerClient)
}
var canUsePoToken = true
if (value.urlRegex.isNotEmpty() && url != null) {
canUsePoToken = value.urlRegex.any { url.matches(it.toRegex()) }
}
var canUsePoToken = true
if (value.urlRegex.isNotEmpty() && url != null) {
canUsePoToken = value.urlRegex.any { url.matches(it.toRegex()) }
}
if (canUsePoToken) {
value.poTokens.forEach { pt ->
poTokens.add("${value.playerClient}.${pt.context}+${pt.token}")
if (canUsePoToken) {
value.poTokens.forEach { pt ->
poTokens.add("${value.playerClient}.${pt.context}+${pt.token}")
}
}
}
}
@ -1308,24 +1310,26 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
}
val generatedPoTokensRaw = sharedPreferences.getString("youtube_generated_po_tokens", "[]")
val generatedPoTokens = Gson().fromJson(generatedPoTokensRaw,Array<YoutubeGeneratePoTokenItem>::class.java).toMutableList()
if (generatedPoTokens.isNotEmpty()) {
for (value in generatedPoTokens) {
if (value.enabled) {
for (cl in value.clients) {
playerClients.add(cl)
for (pt in value.poTokens) {
if (pt.token.isNotBlank()) {
poTokens.add("${cl}.${pt.context}+${pt.token}")
kotlin.runCatching {
val generatedPoTokens = Gson().fromJson(generatedPoTokensRaw,Array<YoutubeGeneratePoTokenItem>::class.java).toMutableList()
if (generatedPoTokens.isNotEmpty()) {
for (value in generatedPoTokens) {
if (value.enabled) {
for (cl in value.clients) {
playerClients.add(cl)
for (pt in value.poTokens) {
if (pt.token.isNotBlank()) {
poTokens.add("${cl}.${pt.context}+${pt.token}")
}
}
}
}
if (dataSyncID.isBlank() && value.useVisitorData) {
extractorArgs.add("player_skip=webpage,configs")
extractorArgs.add("visitor_data=${value.visitorData}")
}
if (dataSyncID.isBlank() && value.useVisitorData) {
extractorArgs.add("player_skip=webpage,configs")
extractorArgs.add("visitor_data=${value.visitorData}")
}
}
}
}
}

View file

@ -3,7 +3,7 @@ package com.deniscerri.ytdl.work
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import android.os.Environment
import android.os.Handler

View file

@ -1,7 +1,7 @@
package com.deniscerri.ytdl.work
import android.content.Context
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
@ -26,7 +26,7 @@ class CleanUpLeftoverDownloads(
val notification = notificationUtil.createDeletingLeftoverDownloadsNotification()
if (Build.VERSION.SDK_INT > 33) {
setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE))
setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC))
}else{
setForegroundAsync(ForegroundInfo(id, notification))
}

View file

@ -4,7 +4,8 @@ import android.annotation.SuppressLint
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.SharedPreferences
import android.content.pm.ServiceInfo
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
@ -16,7 +17,6 @@ import android.widget.Toast
import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
@ -24,6 +24,10 @@ 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
@ -36,11 +40,23 @@ 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.CoroutineStart
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.flow.transformLatest
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
@ -48,45 +64,68 @@ import java.security.MessageDigest
import java.util.Locale
class DownloadWorker(
private val context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
@OptIn(ExperimentalStdlibApi::class)
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 historyDao: HistoryDao
private lateinit var commandTemplateDao: CommandTemplateDao
private lateinit var logRepo: LogRepository
private lateinit var resultRepo: ResultRepository
private lateinit var ytdlpUtil: YTDLPUtil
private lateinit var alarmScheduler : AlarmScheduler
private lateinit var sharedPreferences : SharedPreferences
private lateinit var resources: Resources
private lateinit var workManager: WorkManager
private lateinit var eventBus: EventBus
private lateinit var queueState : Flow<List<DownloadItem>>
private val handler = Handler(Looper.getMainLooper())
private val openDownloadQueue: PendingIntent = PendingIntent.getActivity(
context,
1000000000,
Intent(context, MainActivity::class.java).run {
action = Intent.ACTION_VIEW
putExtra("destination", "Queue")
},
PendingIntent.FLAG_IMMUTABLE
)
override suspend fun getForegroundInfo(): ForegroundInfo {
val workNotif = NotificationUtil(App.instance).createDefaultWorkerNotification()
return ForegroundInfo(
1000000000,
workNotif,
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
} else {
0
},
)
}
private fun cancelSelf() {
workManager.cancelWorkById(this@DownloadWorker.id)
}
@SuppressLint("RestrictedApi")
override suspend fun doWork(): Result {
if (isStopped) return Result.success()
val workManager = WorkManager.getInstance(context)
val currentWork = withContext(Dispatchers.IO) {
workManager.getWorkInfosByTag("download").get()
}
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) {
return Result.success()
}
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)
//init
notificationUtil = NotificationUtil(App.instance)
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()
val confTmp = Configuration(context.resources.configuration)
val locale = if (Build.VERSION.SDK_INT < 33) {
@ -100,240 +139,265 @@ class DownloadWorker(
}
confTmp.setLocale(locale)
val metrics = DisplayMetrics()
val resources = Resources(context.assets, metrics, confTmp)
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
)
workManager = WorkManager.getInstance(context)
if (workManager.isRunning("download")) return Result.Failure()
val workNotif = notificationUtil.createDefaultWorkerNotification()
val notificationID = 1000000000
if (Build.VERSION.SDK_INT > 33) {
setForegroundAsync(ForegroundInfo(notificationID, workNotif, FOREGROUND_SERVICE_TYPE_SPECIAL_USE))
}else{
setForegroundAsync(ForegroundInfo(notificationID, workNotif))
setForegroundSafely()
val priorityItemIDs = (inputData.getLongArray("priority_item_ids") ?: longArrayOf()).toMutableList()
val continueAfterPriorityIds = inputData.getBoolean("continue_after_priority_ids", true)
val time = System.currentTimeMillis() + 6000
queueState = if (priorityItemIDs.isEmpty()) {
dao.getActiveQueuedScheduledDownloadsUntil(time)
}else {
dao.getActiveQueuedScheduledDownloadsUntilWithPriority(time, priorityItemIDs)
}
queuedItems.collectLatest { items ->
if (this@DownloadWorker.isStopped) return@collectLatest
val downloadJobs = mutableMapOf<Long, Job>()
runningYTDLInstances.clear()
val activeDownloads = dao.getActiveDownloadsList()
activeDownloads.forEach {
runningYTDLInstances.add(it.id)
}
queueState.collectLatest { queue ->
val runningCount = queue.asSequence()
.filter { it.status == DownloadRepository.Status.Active.toString() }
.count()
val running = ArrayList(runningYTDLInstances)
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (items.isEmpty() && running.isEmpty()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
val queueItems = queue.asSequence()
.filter { it.status != DownloadRepository.Status.Active.toString() }
val queueCount = queueItems.count()
if (queueCount == 0 && runningCount == 0) {
cancelSelf()
return@collectLatest
}
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler){
if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
if (queueItems.none{ it.downloadStartTime > 0L } && runningCount == 0 && !alarmScheduler.isDuringTheScheduledTime()) {
cancelSelf()
return@collectLatest
}
}
if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collectLatest
}
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - runningCount
if (concurrentDownloads > 0) {
//free spots are open
if (priorityItemIDs.isNotEmpty()) {
if (!continueAfterPriorityIds && queueItems.none { priorityItemIDs.contains(it.id) }) {
//dont queue any more items if only required priority items
cancelSelf()
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 }
}
// Use supervisorScope to cancel child jobs when the downloader job is cancelled
supervisorScope {
val queuedDownloads = queueItems
.toList()
.take(concurrentDownloads)
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 queuedIDs = queuedDownloads.map { it.id }
val downloadJobsToStop = downloadJobs.filter { it.key !in queuedIDs }
downloadJobsToStop.forEach { (download, job) ->
job.cancel()
downloadJobs.remove(download)
}
val cacheDir = FileUtil.getCachePath(context)
val tempFileDir = File(cacheDir, downloadItem.id.toString())
tempFileDir.delete()
tempFileDir.mkdirs()
val downloadsToStart = queuedDownloads.filter { it.id !in downloadJobs }
downloadsToStart.forEach { download ->
downloadJobs[download.id] = launchDownloadJob(download)
}
}
}
val downloadLocation = downloadItem.downloadPath
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !downloadItem.incognito
}
return Result.Success()
}
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(),
@OptIn(DelicateCoroutinesApi::class)
private fun launchIO(block: suspend CoroutineScope.() -> Unit): Job =
GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT, block)
@OptIn(ExperimentalStdlibApi::class)
private fun launchDownloadJob(downloadItem: DownloadItem) = launchIO {
val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url })
notificationUtil.notify(downloadItem.id.toInt(), notification)
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(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()
runBlocking {
if (logDownloads) logItem.id = logRepo.insert(logItem)
downloadItem.logID = logItem.id
dao.update(downloadItem)
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 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")
}
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)
}
}.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
)
}
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 {
@ -348,80 +412,63 @@ class DownloadWorker(
// }
// }
dao.delete(downloadItem.id)
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 (logDownloads){
logRepo.update(initialLogDetails + it.out, logItem.id, true)
}
}
if (eligibleDownloads.isNotEmpty()){
eligibleDownloads.forEach {
it.status = DownloadRepository.Status.Active.toString()
priorityItemIDs.remove(it.id)
}
dao.updateMultiple(eligibleDownloads)
}.onFailure {
FileUtil.deleteConfigFiles(request)
withContext(Dispatchers.Main){
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
}
if (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))
}
return Result.success()
}
companion object {
val runningYTDLInstances: MutableList<Long> = mutableListOf()
const val TAG = "DownloadWorker"
}

View file

@ -3,7 +3,7 @@ package com.deniscerri.ytdl.work
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import android.os.Environment
import android.os.Handler
@ -42,7 +42,7 @@ class MoveCacheFilesWorker(
val notification = notificationUtil.createMoveCacheFilesNotification(pendingIntent, NotificationUtil.DOWNLOAD_MISC_CHANNEL_ID)
if (Build.VERSION.SDK_INT > 33) {
setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE))
setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC))
}else{
setForegroundAsync(ForegroundInfo(id, notification))
}

View file

@ -1,7 +1,7 @@
package com.deniscerri.ytdl.work
import android.content.Context
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import android.util.Log
import androidx.preference.PreferenceManager
@ -57,7 +57,7 @@ class ObserveSourceWorker(
val workerID = System.currentTimeMillis().toInt()
val notification = notificationUtil.createObserveSourcesNotification(item.name)
if (Build.VERSION.SDK_INT > 33) {
setForegroundAsync(ForegroundInfo(workerID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE))
setForegroundAsync(ForegroundInfo(workerID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC))
}else{
setForegroundAsync(ForegroundInfo(workerID, notification))
}

View file

@ -3,7 +3,7 @@ package com.deniscerri.ytdl.work
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import android.os.Handler
import android.os.Looper
@ -53,7 +53,7 @@ class TerminalDownloadWorker(
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE)
val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(65), NotificationUtil.DOWNLOAD_TERMINAL_RUNNING_NOTIFICATION_ID)
if (Build.VERSION.SDK_INT > 33) {
setForegroundAsync(ForegroundInfo(itemId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE))
setForegroundAsync(ForegroundInfo(itemId, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC))
}else{
setForegroundAsync(ForegroundInfo(itemId, notification))
}

View file

@ -1,7 +1,7 @@
package com.deniscerri.ytdl.work
import android.content.Context
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import androidx.work.ForegroundInfo
import androidx.work.Worker
@ -32,7 +32,7 @@ class UpdateMultipleDownloadsDataWorker(
val notification = notificationUtil.createDataUpdateNotification()
if (Build.VERSION.SDK_INT > 33) {
setForegroundAsync(ForegroundInfo(workID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE))
setForegroundAsync(ForegroundInfo(workID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC))
}else{
setForegroundAsync(ForegroundInfo(workID, notification))
}

View file

@ -1,7 +1,7 @@
package com.deniscerri.ytdl.work
import android.content.Context
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.os.Build
import androidx.work.ForegroundInfo
import androidx.work.Worker
@ -33,7 +33,7 @@ class UpdateMultipleDownloadsFormatsWorker(
val notification = notificationUtil.createFormatsUpdateNotification()
if (Build.VERSION.SDK_INT > 33) {
setForegroundAsync(ForegroundInfo(workID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE))
setForegroundAsync(ForegroundInfo(workID, notification, FOREGROUND_SERVICE_TYPE_DATA_SYNC))
}else{
setForegroundAsync(ForegroundInfo(workID, notification))
}

View file

@ -0,0 +1,34 @@
package com.deniscerri.ytdl.work
import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkInfo
import androidx.work.WorkManager
import kotlinx.coroutines.delay
val Context.workManager: WorkManager
get() = WorkManager.getInstance(this)
fun WorkManager.isRunning(tag: String): Boolean {
val list = this.getWorkInfosByTag(tag).get()
return list.count { it.state == WorkInfo.State.RUNNING } > 1
}
/**
* Makes this worker run in the context of a foreground service.
*
* Note that this function is a no-op if the process is subject to foreground
* service restrictions.
*
* Moving to foreground service context requires the worker to run a bit longer,
* allowing Service.startForeground() to be called and avoiding system crash.
*/
suspend fun CoroutineWorker.setForegroundSafely() {
try {
setForeground(getForegroundInfo())
delay(500)
} catch (e: IllegalStateException) {
Log.e("ERROR", "Not allowed to set foreground job")
}
}