more fixes and features

This commit is contained in:
zaednasr 2024-07-27 20:32:21 +02:00
parent f4f527721c
commit b45ddbeffc
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
33 changed files with 437 additions and 116 deletions

View file

@ -23,6 +23,7 @@ import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.os.bundleOf
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -63,9 +64,11 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.BufferedReader import java.io.BufferedReader
import java.io.File import java.io.File
import java.io.InputStreamReader import java.io.InputStreamReader

View file

@ -170,6 +170,9 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE status='Processing'") @Query("DELETE FROM downloads WHERE status='Processing'")
suspend fun deleteProcessing() suspend fun deleteProcessing()
@Query("DELETE FROM downloads WHERE status='Duplicate'")
suspend fun deleteWithDuplicateStatus()
@Query("DELETE FROM downloads WHERE status='Scheduled'") @Query("DELETE FROM downloads WHERE status='Scheduled'")
suspend fun deleteScheduled() suspend fun deleteScheduled()

View file

@ -174,6 +174,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
deleteCache(cancelled) deleteCache(cancelled)
} }
fun getActiveDownloadsCount() : Int {
return downloadDao.getDownloadsCountByStatus(listOf(Status.Active).toListString())
}
suspend fun deleteScheduled() { suspend fun deleteScheduled() {
downloadDao.deleteScheduled() downloadDao.deleteScheduled()
} }
@ -196,6 +200,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.deleteProcessing() downloadDao.deleteProcessing()
} }
suspend fun deleteWithDuplicateStatus() {
downloadDao.deleteWithDuplicateStatus()
}
suspend fun deleteAllWithIDs(ids: List<Long>){ suspend fun deleteAllWithIDs(ids: List<Long>){
downloadDao.deleteAllWithIDs(ids) downloadDao.deleteAllWithIDs(ids)
@ -214,7 +222,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) { suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) : Result<String> {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workManager = WorkManager.getInstance(context) val workManager = WorkManager.getInstance(context)
@ -236,7 +244,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
if (delay > 0L && useAlarmForScheduling) { if (delay > 0L && useAlarmForScheduling) {
AlarmScheduler(context).scheduleAt(queuedItems.minBy { it.downloadStartTime }.downloadStartTime) AlarmScheduler(context).scheduleAt(queuedItems.minBy { it.downloadStartTime }.downloadStartTime)
return return Result.success("")
} }
@ -256,26 +264,22 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
) )
} }
val message = StringBuilder()
val handler = Handler(Looper.getMainLooper())
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered
if (!allowMeteredNetworks && isCurrentNetworkMetered){ if (!allowMeteredNetworks && isCurrentNetworkMetered){
handler.postDelayed({ message.appendLine(context.getString(R.string.metered_network_download_start_info))
Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show()
}, 0)
} }
if (queuedItems.isNotEmpty()) { if (queuedItems.isNotEmpty()) {
val first = queuedItems.first() val first = queuedItems.first()
if (first.downloadStartTime > 0L) { if (first.downloadStartTime > 0L) {
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(queuedItems.first().downloadStartTime) val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(queuedItems.first().downloadStartTime)
handler.postDelayed({ message.appendLine(context.getString(R.string.download_rescheduled_to) + " " + date)
Toast.makeText(context, context.getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
}, 0)
} }
} }
return Result.success(message.toString())
} }
} }

View file

@ -638,6 +638,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.deleteProcessing() repository.deleteProcessing()
} }
fun deleteWithDuplicateStatus() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteWithDuplicateStatus()
}
suspend fun deleteAllWithID(ids: List<Long>) { suspend fun deleteAllWithID(ids: List<Long>) {
repository.deleteAllWithIDs(ids) repository.deleteAllWithIDs(ids)
} }
@ -671,7 +675,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun getActiveDownloadsCount() : Int { fun getActiveDownloadsCount() : Int {
return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Active).toListString()) return repository.getActiveDownloadsCount()
} }
fun getActiveQueuedDownloadsCount() : Int { fun getActiveQueuedDownloadsCount() : Int {
@ -761,12 +765,17 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.startDownloadWorker(emptyList(), application) repository.startDownloadWorker(emptyList(), application)
} }
suspend fun queueProcessingDownloads(){ suspend fun queueProcessingDownloads() : QueueDownloadsResult {
val processingItems = repository.getProcessingDownloads() val processingItems = repository.getProcessingDownloads()
queueDownloads(processingItems) return queueDownloads(processingItems)
} }
suspend fun queueDownloads(items: List<DownloadItem>, ignoreDuplicates : Boolean = false) { data class QueueDownloadsResult(
var message: String,
var duplicateDownloadIDs : List<AlreadyExistsIDs>
)
suspend fun queueDownloads(items: List<DownloadItem>, ignoreDuplicates : Boolean = false) : QueueDownloadsResult {
val context = App.instance val context = App.instance
val alarmScheduler = AlarmScheduler(context) val alarmScheduler = AlarmScheduler(context)
val queuedItems = mutableListOf<DownloadItem>() val queuedItems = mutableListOf<DownloadItem>()
@ -788,10 +797,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
items.forEach { items.forEach {
if (it.status != DownloadRepository.Status.Scheduled.toString()) { if (it.downloadStartTime > 0) {
it.status = DownloadRepository.Status.Scheduled.toString()
}else{
it.status = DownloadRepository.Status.Queued.toString() it.status = DownloadRepository.Status.Queued.toString()
} }
//CHECK DUPLICATES //CHECK DUPLICATES
var isDuplicate = false var isDuplicate = false
if (checkDuplicate.isNotEmpty() && !ignoreDuplicates){ if (checkDuplicate.isNotEmpty() && !ignoreDuplicates){
@ -903,22 +915,23 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
val queued = repository.updateAll(queuedItems) val result = QueueDownloadsResult("", listOf())
//if scheduler is on //if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){ if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
if (alarmScheduler.canSchedule()){ if (alarmScheduler.canSchedule()){
repository.updateAll(queuedItems)
alarmScheduler.schedule() alarmScheduler.schedule()
}else{ }else{
sharedPreferences.edit().putBoolean("use_scheduler", false).apply() sharedPreferences.edit().putBoolean("use_scheduler", false).apply()
Handler(Looper.getMainLooper()).post { result.message = context.getString(R.string.enable_alarm_permission)
Toast.makeText(context, context.getString(R.string.enable_alarm_permission), Toast.LENGTH_LONG).show()
}
} }
}else{ }else{
val queued = repository.updateAll(queuedItems)
if (!sharedPreferences.getBoolean("paused_downloads", false)) { if (!sharedPreferences.getBoolean("paused_downloads", false)) {
repository.startDownloadWorker(queued, context) result.message = repository.startDownloadWorker(queued, context).getOrElse { "" }
} }
if(!useScheduler){ if(!useScheduler){
@ -947,7 +960,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
if (existingItemIDs.isNotEmpty()){ if (existingItemIDs.isNotEmpty()){
alreadyExistsUiState.value = existingItemIDs.toList() alreadyExistsUiState.value = existingItemIDs.toList()
result.duplicateDownloadIDs = existingItemIDs.toList()
} }
return result
} }
fun getQueuedCollectedFileSize() : Long { fun getQueuedCollectedFileSize() : Long {
@ -1077,13 +1093,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
} }
suspend fun updateProcessingDownloadTimeAndQueueScheduled(time: Long) { suspend fun updateProcessingDownloadTimeAndQueueScheduled(time: Long) : QueueDownloadsResult {
val processing = repository.getProcessingDownloads() val processing = repository.getProcessingDownloads()
processing.forEach { processing.forEach {
it.downloadStartTime = time it.downloadStartTime = time
it.status = DownloadRepository.Status.Scheduled.toString() it.status = DownloadRepository.Status.Scheduled.toString()
} }
queueDownloads(processing) return queueDownloads(processing)
} }
fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> { fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> {

View file

@ -213,18 +213,6 @@ class ShareActivity : BaseActivity() {
} }
this@ShareActivity.finish() this@ShareActivity.finish()
} }
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.isNotEmpty()){
withContext(Dispatchers.Main){
val bundle = bundleOf(
Pair("duplicates", res)
)
navController.navigate(R.id.downloadsAlreadyExistDialog2, bundle)
}
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
}
}
} }
} }
} }

View file

@ -11,10 +11,19 @@ import android.provider.Settings
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.findNavController
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.ThemeUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.system.exitProcess import kotlin.system.exitProcess
open class BaseActivity : AppCompatActivity() { open class BaseActivity : AppCompatActivity() {

View file

@ -221,7 +221,7 @@ class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: Downlo
incognito = !incognito incognito = !incognito
fragmentAdapter.isIncognito = incognito fragmentAdapter.isIncognito = incognito
val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled) val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled)
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: $onOff", Toast.LENGTH_SHORT).show() Snackbar.make(incognitoBtn, "${getString(R.string.incognito)}: $onOff", Snackbar.LENGTH_SHORT).show()
} }

View file

@ -39,6 +39,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.receiver.ShareActivity import com.deniscerri.ytdl.receiver.ShareActivity
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
@ -80,6 +81,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var title : View private lateinit var title : View
private lateinit var shimmerLoadingSubtitle : ShimmerFrameLayout private lateinit var shimmerLoadingSubtitle : ShimmerFrameLayout
private lateinit var subtitle : View private lateinit var subtitle : View
private lateinit var parentActivity: BaseActivity
private lateinit var result: ResultItem private lateinit var result: ResultItem
@ -95,7 +97,6 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java] commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
infoUtil = InfoUtil(requireContext()) infoUtil = InfoUtil(requireContext())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val res: ResultItem? val res: ResultItem?
val dwl: DownloadItem? val dwl: DownloadItem?
@ -131,6 +132,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
view = LayoutInflater.from(context).inflate(R.layout.download_bottom_sheet, null) view = LayoutInflater.from(context).inflate(R.layout.download_bottom_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity()) dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
parentActivity = activity as BaseActivity
dialog.setOnShowListener { dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)
@ -331,21 +333,34 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
audioDownloadItem.status = DownloadRepository.Status.Scheduled.toString() audioDownloadItem.status = DownloadRepository.Status.Scheduled.toString()
itemsToQueue.add(audioDownloadItem) itemsToQueue.add(audioDownloadItem)
runBlocking { lifecycleScope.launch {
downloadViewModel.queueDownloads(itemsToQueue) val result = withContext(Dispatchers.IO){
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(item.downloadStartTime) downloadViewModel.queueDownloads(itemsToQueue)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show() }
if (result.message.isNotBlank()){
Toast.makeText(requireContext(), result.message, Toast.LENGTH_LONG).show()
}
withContext(Dispatchers.Main){
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
}
} }
dismiss()
} }
}else{ }else{
runBlocking { lifecycleScope.launch {
downloadViewModel.queueDownloads(listOf(item)) val result = withContext(Dispatchers.IO){
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(item.downloadStartTime) downloadViewModel.queueDownloads(listOf(item))
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show() }
}
dismiss()
if (result.message.isNotBlank()){
Toast.makeText(requireContext(), result.message, Toast.LENGTH_LONG).show()
}
withContext(Dispatchers.Main){
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
}
}
} }
} }
@ -365,15 +380,17 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
itemsToQueue.add(it) itemsToQueue.add(it)
runBlocking { runBlocking {
downloadViewModel.queueDownloads(itemsToQueue) val result = downloadViewModel.queueDownloads(itemsToQueue)
withContext(Dispatchers.Main){
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
}
} }
dismiss()
} }
}else{ }else{
runBlocking { val result = withContext(Dispatchers.IO) {
downloadViewModel.queueDownloads(listOf(item)) downloadViewModel.queueDownloads(listOf(item))
} }
dismiss() handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
} }
} }
} }
@ -441,7 +458,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
incognito = !incognito incognito = !incognito
fragmentAdapter.isIncognito = incognito fragmentAdapter.isIncognito = incognito
val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled) val onOff = if (incognito) getString(R.string.ok) else getString(R.string.disabled)
Toast.makeText(requireContext(), "${getString(R.string.incognito)}: $onOff", Toast.LENGTH_SHORT).show() Snackbar.make(incognitoBtn, "${getString(R.string.incognito)}: $onOff", Snackbar.LENGTH_SHORT).show()
} }
@ -584,6 +601,23 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
} }
} }
lifecycleScope.launch {
launch{
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.isNotEmpty() && activity is ShareActivity){
withContext(Dispatchers.Main){
val bundle = bundleOf(
Pair("duplicates", ArrayList(res))
)
delay(500)
findNavController().navigate(R.id.action_downloadBottomSheetDialog_to_downloadsAlreadyExistDialog2, bundle)
}
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
}
}
}
}
lifecycleScope.launch { lifecycleScope.launch {
resultViewModel.updateFormatsResultData.collectLatest { formats -> resultViewModel.updateFormatsResultData.collectLatest { formats ->
if (formats == null) return@collectLatest if (formats == null) return@collectLatest
@ -686,5 +720,13 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
super.onDismiss(dialog) super.onDismiss(dialog)
} }
} }
private fun handleDuplicatesAndDismiss(res: List<DownloadViewModel.AlreadyExistsIDs>) {
if (activity is ShareActivity && res.isNotEmpty()) {
//let the lifecycle listener handle it
}else{
dismiss()
}
}
} }

View file

@ -19,11 +19,13 @@ import android.view.Window
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.os.bundleOf
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -37,6 +39,8 @@ import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.receiver.ShareActivity
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
@ -56,6 +60,7 @@ import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -80,6 +85,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var shimmerTitle: ShimmerFrameLayout private lateinit var shimmerTitle: ShimmerFrameLayout
private lateinit var shimmerSubtitle: ShimmerFrameLayout private lateinit var shimmerSubtitle: ShimmerFrameLayout
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var parentActivity: BaseActivity
private lateinit var currentDownloadIDs: List<Long> private lateinit var currentDownloadIDs: List<Long>
private var processingItemsCount : Int = 0 private var processingItemsCount : Int = 0
@ -103,6 +109,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val view = LayoutInflater.from(context).inflate(R.layout.download_multiple_bottom_sheet, null) val view = LayoutInflater.from(context).inflate(R.layout.download_multiple_bottom_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity()) dialog.window?.navigationBarColor = SurfaceColors.SURFACE_1.getColor(requireActivity())
parentActivity = activity as BaseActivity
dialog.setOnShowListener { dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)
@ -204,9 +211,20 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs) downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.updateProcessingDownloadTimeAndQueueScheduled(cal.timeInMillis) val result = downloadViewModel.updateProcessingDownloadTimeAndQueueScheduled(cal.timeInMillis)
if (result.message.isNotBlank()){
lifecycleScope.launch {
withContext(Dispatchers.Main) {
Toast.makeText(requireContext(), result.message, Toast.LENGTH_LONG).show()
}
}
}
withContext(Dispatchers.Main){
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
dismiss()
}
} }
dismiss()
} }
} }
} }
@ -216,9 +234,20 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
downloadViewModel.deleteAllWithID(currentDownloadIDs) downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.queueProcessingDownloads() val result = downloadViewModel.queueProcessingDownloads()
if (result.message.isNotBlank()){
lifecycleScope.launch {
withContext(Dispatchers.Main) {
Toast.makeText(requireContext(), result.message, Toast.LENGTH_LONG).show()
}
}
}
withContext(Dispatchers.Main){
handleDuplicatesAndDismiss(result.duplicateDownloadIDs)
dismiss()
}
} }
dismiss()
} }
} }
@ -284,6 +313,23 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
lifecycleScope.launch {
launch{
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
if (res.isNotEmpty() && activity is ShareActivity){
withContext(Dispatchers.Main){
val bundle = bundleOf(
Pair("duplicates", ArrayList(res))
)
delay(500)
findNavController().navigate(R.id.action_downloadMultipleBottomSheetDialog_to_downloadsAlreadyExistDialog2, bundle)
}
downloadViewModel.alreadyExistsUiState.value = mutableListOf()
}
}
}
}
bottomAppBar.setOnMenuItemClickListener { m: MenuItem -> bottomAppBar.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) { when (m.itemId) {
@ -910,5 +956,13 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
} }
private fun handleDuplicatesAndDismiss(res: List<DownloadViewModel.AlreadyExistsIDs>) {
if (activity is ShareActivity && res.isNotEmpty()) {
//let the lifecycle listener handle it
}else{
dismiss()
}
}
} }

View file

@ -10,6 +10,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.View import android.view.View
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -28,6 +29,7 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -102,10 +104,17 @@ class DownloadsAlreadyExistDialog : BottomSheetDialogFragment(), AlreadyExistsAd
view.findViewById<MaterialButton>(R.id.bottomsheet_download_button).setOnClickListener { view.findViewById<MaterialButton>(R.id.bottomsheet_download_button).setOnClickListener {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing() downloadViewModel.deleteWithDuplicateStatus()
val items = duplicates.map { it.downloadItem } val items = duplicates.map { it.downloadItem }
items.forEach { it.id = 0 } items.forEach { it.id = 0 }
downloadViewModel.queueDownloads(items, true) val result = downloadViewModel.queueDownloads(items, true)
if (result.message.isNotBlank()){
lifecycleScope.launch {
withContext(Dispatchers.Main) {
Toast.makeText(requireContext(), result.message, Toast.LENGTH_LONG).show()
}
}
}
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
dismiss() dismiss()
} }
@ -117,8 +126,7 @@ class DownloadsAlreadyExistDialog : BottomSheetDialogFragment(), AlreadyExistsAd
override fun onDismiss(dialog: DialogInterface) { override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog) super.onDismiss(dialog)
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.deleteProcessing() downloadViewModel.deleteWithDuplicateStatus()
downloadViewModel.deleteAllWithID(duplicates.map { it.downloadItem.id })
} }
} }

View file

@ -504,7 +504,10 @@ class FormatSelectionBottomSheetDialog(
finalFormats = if (items.first()?.type == Type.audio){ finalFormats = if (items.first()?.type == Type.audio){
formatSorter.sortAudioFormats(finalFormats) formatSorter.sortAudioFormats(finalFormats)
}else{ }else{
formatSorter.sortVideoFormats(finalFormats) val audioFormats = finalFormats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
val videoFormats = finalFormats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }
formatSorter.sortVideoFormats(videoFormats) + formatSorter.sortAudioFormats(audioFormats)
} }
} }
FormatCategory.SMALLEST -> { FormatCategory.SMALLEST -> {

View file

@ -179,9 +179,9 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O
ok.isEnabled = false ok.isEnabled = false
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch(Dispatchers.IO) {
val checkedItems = listAdapter.getCheckedItems() val checkedItems = listAdapter.getCheckedItems()
val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) } val checkedResultItems = items.filter { item -> checkedItems.contains(item.url) }
if (checkedResultItems.size == 1){ if (checkedResultItems.size == 1){
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url)!! val resultItem = resultViewModel.getItemByURL(checkedResultItems[0].url)!!
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf( findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf(
Pair("result", resultItem), Pair("result", resultItem),

View file

@ -1,23 +1,15 @@
package com.deniscerri.ytdl.ui.downloads package com.deniscerri.ytdl.ui.downloads
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent
import android.content.ServiceConnection
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.os.IBinder
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.get
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -26,12 +18,10 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import androidx.work.WorkManager import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.inflate
import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.downloadcard.DownloadAudioFragment
import com.deniscerri.ytdl.util.Extensions.createBadge import com.deniscerri.ytdl.util.Extensions.createBadge
import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
@ -42,7 +32,6 @@ import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext

View file

@ -164,10 +164,11 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyViewModel.getFilteredList().observe(viewLifecycleOwner) { historyViewModel.getFilteredList().observe(viewLifecycleOwner) {
historyAdapter!!.submitList(it) historyAdapter!!.submitList(it)
historyList = it historyList = it
scrollToTop()
if (recyclerView.adapter == null){ if (recyclerView.adapter == null){
recyclerView.adapter = historyAdapter recyclerView.adapter = historyAdapter
}else{
scrollToTop()
} }
} }

View file

@ -6,6 +6,7 @@ import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.interaction.DragInteraction
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
@ -13,12 +14,14 @@ import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import androidx.work.Constraints import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.CleanUpLeftoverDownloads
import com.deniscerri.ytdl.work.DownloadWorker import com.deniscerri.ytdl.work.DownloadWorker
import java.util.Calendar import java.util.Calendar
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -69,6 +72,42 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
archivePathResultLauncher.launch(intent) archivePathResultLauncher.launch(intent)
true true
} }
val workManager = WorkManager.getInstance(requireContext())
val cleanupLeftoverDownloads = findPreference<Preference>("cleanup_leftover_downloads")
cleanupLeftoverDownloads?.setOnPreferenceChangeListener { preference, newValue ->
var nextTime : Calendar? = Calendar.getInstance()
when(newValue) {
"daily" -> nextTime?.add(Calendar.DAY_OF_WEEK, 1)
"weekly" -> nextTime?.add(Calendar.DAY_OF_WEEK, 7)
"monthly" -> nextTime?.add(Calendar.MONTH, 1)
else -> nextTime = null
}
if (nextTime == null) workManager.cancelAllWorkByTag("cleanup_leftover_downloads")
else {
val workConstraints = Constraints.Builder()
val allowMeteredNetworks = preferences.getBoolean("metered_networks", true)
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
val delay = nextTime.timeInMillis.minus(System.currentTimeMillis())
val workRequest = OneTimeWorkRequestBuilder<CleanUpLeftoverDownloads>()
.addTag("cleanup_leftover_downloads")
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
true
}
val scheduler = AlarmScheduler(requireContext()) val scheduler = AlarmScheduler(requireContext())
val useAlarmManagerInsteadOfWorkManager = findPreference<SwitchPreferenceCompat>("use_alarm_for_scheduling") val useAlarmManagerInsteadOfWorkManager = findPreference<SwitchPreferenceCompat>("use_alarm_for_scheduling")

View file

@ -11,6 +11,8 @@ import android.os.Environment
import android.provider.Settings import android.provider.Settings
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
@ -19,10 +21,14 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.MoveCacheFilesWorker import com.deniscerri.ytdl.work.MoveCacheFilesWorker
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -43,6 +49,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
private lateinit var editor: SharedPreferences.Editor private lateinit var editor: SharedPreferences.Editor
private lateinit var downloadViewModel: DownloadViewModel
private var activeDownloadCount = 0 private var activeDownloadCount = 0
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -50,6 +57,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
editor = preferences.edit() editor = preferences.edit()
downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java]
musicPath = findPreference("music_path") musicPath = findPreference("music_path")
videoPath = findPreference("video_path") videoPath = findPreference("video_path")
@ -166,13 +174,18 @@ class FolderSettingsFragment : BaseSettingsFragment() {
clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}" clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
clearCache!!.onPreferenceClickListener = clearCache!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
if (activeDownloadCount == 0){ lifecycleScope.launch {
File(FileUtil.getCachePath(requireContext())).deleteRecursively() activeDownloadCount = withContext(Dispatchers.IO){
Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show() downloadViewModel.getActiveDownloadsCount()
cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() } }
clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}" if (activeDownloadCount == 0){
}else{ File(FileUtil.getCachePath(requireContext())).deleteRecursively()
Snackbar.make(requireView(), getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show() Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show()
cacheSize = File(FileUtil.getCachePath(requireContext())).walkBottomUp().fold(0L) { acc, file -> acc + file.length() }
clearCache!!.summary = "(${FileUtil.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}"
}else{
Snackbar.make(requireView(), getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show()
}
} }
true true
} }

View file

@ -142,11 +142,10 @@ class FormatSorter(private var context: Context) {
val aIndex = videoResolutionOrder.indexOfFirst { a.format_note.contains(it, ignoreCase = true) } val aIndex = videoResolutionOrder.indexOfFirst { a.format_note.contains(it, ignoreCase = true) }
val bIndex = videoResolutionOrder.indexOfFirst { b.format_note.contains(it, ignoreCase = true) } val bIndex = videoResolutionOrder.indexOfFirst { b.format_note.contains(it, ignoreCase = true) }
if (aIndex > preferenceIndex || bIndex > preferenceIndex) { if (aIndex > preferenceIndex || bIndex > preferenceIndex) {
-1 -1
}else if(aIndex == -1 && bIndex == -1){ }else if(aIndex == -1 && bIndex == -1){
-1 0
}else{ }else{
bIndex.compareTo(aIndex) bIndex.compareTo(aIndex)
} }
@ -168,7 +167,6 @@ class FormatSorter(private var context: Context) {
return 0 return 0
} }
} }
return formats.sortedWith(fieldSorter) return formats.sortedWith(fieldSorter)
} }

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
import android.content.res.Resources.NotFoundException
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.Html import android.text.Html
@ -26,6 +27,7 @@ import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLException
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -112,23 +114,37 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class) @Throws(JSONException::class)
fun getPlaylist(id: String, nextPageToken: String, playlistName: String): PlaylistTuple { fun getPlaylist(id: String, nextPageToken: String, playlistName: String): PlaylistTuple {
try{ try {
val items = arrayListOf<ResultItem>() val items = arrayListOf<ResultItem>()
// -------------- PIPED API FUNCTION ------------------- // -------------- PIPED API FUNCTION -------------------
var url = "" var url = ""
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id" url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
else """$pipedURL/nextpage/playlists/$id?nextpage=${nextPageToken.replace("&prettyPrint", "%26prettyPrint")}""" else """$pipedURL/nextpage/playlists/$id?nextpage=${
nextPageToken.replace(
"&prettyPrint",
"%26prettyPrint"
)
}"""
println(url)
val res = genericRequest(url) val res = genericRequest(url)
if (!res.has("relatedStreams")) throw Exception() if (!res.has("relatedStreams")) throw Exception()
val dataArray = res.getJSONArray("relatedStreams") val dataArray = res.getJSONArray("relatedStreams")
var nextpage = res.getString("nextpage") var nextpage = res.getString("nextpage")
for (i in 0 until dataArray.length()){ val isMixPlaylist = nextPageToken.isBlank() && res.getInt("videos") < 0
if (isMixPlaylist) throw YoutubeDLException("This playlist type is unviewable.")
for (i in 0 until dataArray.length()) {
kotlin.runCatching { kotlin.runCatching {
val obj = dataArray.getJSONObject(i) val obj = dataArray.getJSONObject(i)
createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))?.apply { createVideoFromPipedJSON(
playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName } obj,
"https://youtube.com" + obj.getString("url")
)?.apply {
playlistTitle =
runCatching { res.getString("name") }.getOrElse { playlistName }
playlistURL = "https://www.youtube.com/playlist?list=$id" playlistURL = "https://www.youtube.com/playlist?list=$id"
items.add(this) items.add(this)
} }
@ -137,11 +153,14 @@ class InfoUtil(private val context: Context) {
if (nextpage == "null") nextpage = "" if (nextpage == "null") nextpage = ""
return PlaylistTuple(nextpage, items) return PlaylistTuple(nextpage, items)
}catch (e: Exception){ }catch (e: Exception){
Log.e("PLST", e.toString()) if (e is YoutubeDLException) {
return PlaylistTuple( throw(e)
"", }else{
getFromYTDL("https://www.youtube.com/playlist?list=$id") return PlaylistTuple(
) "",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
)
}
} }
} }
@ -202,7 +221,7 @@ class InfoUtil(private val context: Context) {
Html.fromHtml(obj.getString("uploader").toString()).toString() Html.fromHtml(obj.getString("uploader").toString()).toString()
}catch (e: Exception){ }catch (e: Exception){
Html.fromHtml(obj.getString("uploaderName").toString()).toString() Html.fromHtml(obj.getString("uploaderName").toString()).toString()
}.replace(" - Topic", "") }.removeSuffix(" - Topic")
val duration = obj.getInt("duration").toStringDuration(Locale.US) val duration = obj.getInt("duration").toStringDuration(Locale.US)
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg" val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
@ -1222,18 +1241,20 @@ class InfoUtil(private val context: Context) {
val ext = downloadItem.container val ext = downloadItem.container
val preferredLanguage = sharedPreferences.getString("audio_language","")!!
println(audioQualityId)
if (audioQualityId.isNotBlank()) { if (audioQualityId.isNotBlank()) {
if(audioQualityId.contains("-")){ if (audioQualityId.matches(".*-[0-9]+.*".toRegex())) {
audioQualityId = if(!downloadItem.format.lang.isNullOrBlank() && downloadItem.format.lang != "None"){ audioQualityId = if(!downloadItem.format.lang.isNullOrBlank() && downloadItem.format.lang != "None"){
"ba[format_id~='^(${audioQualityId.split("-")[0]})'][language^=${downloadItem.format.lang}]/ba/b" "ba[format_id~='^(${audioQualityId.split("-")[0]})'][language^=${downloadItem.format.lang}]/ba/b"
}else{ }else{
"$audioQualityId/${audioQualityId.split("-")[0]}" "$audioQualityId/${audioQualityId.split("-")[0]}"
} }
} }
request.addOption("-f", audioQualityId) request.addOption("-f", audioQualityId)
}else{ }else{
//enters here if generic or quick downloaded with ba format //enters here if generic or quick downloaded with ba format
val preferredLanguage = sharedPreferences.getString("audio_language","")!!
if (preferredLanguage.isNotBlank()){ if (preferredLanguage.isNotBlank()){
request.addOption("-f", "ba[language^=$preferredLanguage]/ba/b") request.addOption("-f", "ba[language^=$preferredLanguage]/ba/b")
} }
@ -1257,6 +1278,10 @@ class InfoUtil(private val context: Context) {
} }
} }
if (preferredLanguage.isNotBlank()) {
formatSorting.append(",lang:${preferredLanguage}")
}
request.addOption("-P", downDir.absolutePath) request.addOption("-P", downDir.absolutePath)
request.addOption("-S", formatSorting.toString()) request.addOption("-S", formatSorting.toString())
@ -1357,7 +1382,8 @@ class InfoUtil(private val context: Context) {
var audioF = downloadItem.videoPreferences.audioFormatIDs.map { f -> var audioF = downloadItem.videoPreferences.audioFormatIDs.map { f ->
val format = downloadItem.allFormats.find { it.format_id == f } val format = downloadItem.allFormats.find { it.format_id == f }
format?.run { format?.run {
if (this.format_id.contains("-")) { println(format_id)
if (this.format_id.matches(".*-[0-9]+".toRegex())) {
if (!this.lang.isNullOrBlank() && this.lang != "None") { if (!this.lang.isNullOrBlank() && this.lang != "None") {
"ba[format_id~='^(${this.format_id.split("-")[0]})'][language^=${this.lang}]" "ba[format_id~='^(${this.format_id.split("-")[0]})'][language^=${this.lang}]"
} else { } else {
@ -1477,13 +1503,11 @@ class InfoUtil(private val context: Context) {
} }
val genericFormats = getGenericVideoFormats(context.resources).map { it.format_id } val preferredLanguage = sharedPreferences.getString("audio_language","")!!
StringBuilder().apply { StringBuilder().apply {
if (hasGenericResulutionFormat.isNotBlank()) { if (hasGenericResulutionFormat.isNotBlank()) {
append(",res:${hasGenericResulutionFormat}") append(",res:${hasGenericResulutionFormat}")
}else if (genericFormats.contains(videoF) && preferredQuality!!.contains("p_")){
append(",res:${preferredQuality.split("_")[0]}")
} }
if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size") if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size")
if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref") if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref")
@ -1491,6 +1515,7 @@ class InfoUtil(private val context: Context) {
if (cont.isNotBlank()) append(",vext:$cont") if (cont.isNotBlank()) append(",vext:$cont")
if (acont.isNotBlank()) append(",aext:$acont") if (acont.isNotBlank()) append(",aext:$acont")
if (abrSort.isNotBlank()) append(",abr~${abrSort}") if (abrSort.isNotBlank()) append(",abr~${abrSort}")
if (preferredLanguage.isNotBlank()) append(",lang:${preferredLanguage}")
if (this.isNotBlank()){ if (this.isNotBlank()){
request.addOption("-S", "+hasaud$this") request.addOption("-S", "+hasaud$this")
} }

View file

@ -474,6 +474,27 @@ class NotificationUtil(var context: Context) {
notificationManager.cancel(DOWNLOAD_ERRORED_NOTIFICATION_ID + id) notificationManager.cancel(DOWNLOAD_ERRORED_NOTIFICATION_ID + id)
} }
fun createDeletingLeftoverDownloadsNotification() : Notification {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
return notificationBuilder
.setContentTitle(resources.getString(R.string.cleanup_leftover_downloads))
.setCategory(Notification.CATEGORY_EVENT)
.setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon(
BitmapFactory.decodeResource(
resources,
R.drawable.ic_launcher_foreground_large
)
)
.setContentText("")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.clearActions()
.build()
}
fun createMoveCacheFilesNotification(pendingIntent: PendingIntent?, downloadMiscChannelId: String): Notification { fun createMoveCacheFilesNotification(pendingIntent: PendingIntent?, downloadMiscChannelId: String): Notification {
val notificationBuilder = getBuilder(downloadMiscChannelId) val notificationBuilder = getBuilder(downloadMiscChannelId)

View file

@ -0,0 +1,42 @@
package com.deniscerri.ytdl.work
import android.content.Context
import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkerParameters
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.google.android.material.snackbar.Snackbar
import java.io.File
class CleanUpLeftoverDownloads(
private val context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val notificationUtil = NotificationUtil(App.instance)
val id = System.currentTimeMillis().toInt()
val notification = notificationUtil.createDeletingLeftoverDownloadsNotification()
val foregroundInfo = ForegroundInfo(id, notification)
setForegroundAsync(foregroundInfo)
val dbManager = DBManager.getInstance(context)
val downloadRepo = DownloadRepository(dbManager.downloadDao)
downloadRepo.deleteCancelled()
downloadRepo.deleteErrored()
val activeDownloadCount = downloadRepo.getActiveDownloadsCount()
if (activeDownloadCount == 0){
File(FileUtil.getCachePath(context)).deleteRecursively()
}
return Result.success()
}
}

View file

@ -159,7 +159,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="100dp" android:paddingBottom="200dp"
app:spanCount="2" app:spanCount="2"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/> />

View file

@ -149,7 +149,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="100dp" android:paddingBottom="200dp"
app:spanCount="3" app:spanCount="3"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/> />

View file

@ -157,7 +157,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="100dp" android:paddingBottom="200dp"
app:spanCount="2" app:spanCount="2"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/> />

View file

@ -149,7 +149,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="100dp" android:paddingBottom="200dp"
app:spanCount="4" app:spanCount="4"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/> />

View file

@ -105,25 +105,25 @@
<TextView <TextView
android:id="@+id/audio_formats" android:id="@+id/audio_formats"
style="@style/Widget.Material3.FloatingActionButton.Large.Primary"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="5dp" android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner" android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorPrimaryContainer" android:backgroundTint="?attr/colorPrimary"
android:clickable="false" android:clickable="false"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center" android:gravity="center"
android:maxLength="17"
android:minWidth="30dp" android:minWidth="30dp"
android:paddingHorizontal="5dp" android:paddingHorizontal="5dp"
android:textColor="@color/white"
android:textCursorDrawable="@string/audio_format"
android:textStyle="bold" android:textStyle="bold"
app:cornerRadius="10dp" app:cornerRadius="10dp"
app:drawableStartCompat="@drawable/ic_music_formatcard" app:drawableStartCompat="@drawable/ic_music_formatcard"
app:drawableTint="@color/white" app:drawableTint="@color/white"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<TextView <TextView
@ -180,8 +180,7 @@
android:ellipsize="end" android:ellipsize="end"
android:gravity="bottom|end" android:gravity="bottom|end"
android:maxWidth="70dp" android:maxWidth="70dp"
android:maxLines="2" android:maxLines="1"
app:layout_constraintBottom_toBottomOf="@+id/format_note"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />

View file

@ -92,8 +92,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="100dp" android:paddingBottom="200dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/> />

View file

@ -31,6 +31,7 @@
<TextView <TextView
android:id="@+id/drag" android:id="@+id/drag"
android:visibility="gone"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingVertical="10dp" android:paddingVertical="10dp"

View file

@ -12,6 +12,12 @@
app:launchSingleTop="true" app:launchSingleTop="true"
app:popUpTo="@id/downloadBottomSheetDialog" app:popUpTo="@id/downloadBottomSheetDialog"
app:popUpToInclusive="true" /> app:popUpToInclusive="true" />
<action
android:id="@+id/action_downloadBottomSheetDialog_to_downloadsAlreadyExistDialog2"
app:destination="@id/downloadsAlreadyExistDialog2"
app:launchSingleTop="true"
app:popUpTo="@id/downloadBottomSheetDialog"
app:popUpToInclusive="true" />
</dialog> </dialog>
<dialog <dialog
android:id="@+id/selectPlaylistItemsDialog" android:id="@+id/selectPlaylistItemsDialog"
@ -33,7 +39,14 @@
<dialog <dialog
android:id="@+id/downloadMultipleBottomSheetDialog" android:id="@+id/downloadMultipleBottomSheetDialog"
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadMultipleBottomSheetDialog" android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadMultipleBottomSheetDialog"
android:label="DownloadMultipleBottomSheetDialog" /> android:label="DownloadMultipleBottomSheetDialog" >
<action
android:id="@+id/action_downloadMultipleBottomSheetDialog_to_downloadsAlreadyExistDialog2"
app:destination="@id/downloadsAlreadyExistDialog2"
app:launchSingleTop="true"
app:popUpTo="@id/downloadMultipleBottomSheetDialog"
app:popUpToInclusive="true" />
</dialog>
<dialog <dialog
android:id="@+id/downloadsAlreadyExistDialog2" android:id="@+id/downloadsAlreadyExistDialog2"
android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadsAlreadyExistDialog" android:name="com.deniscerri.ytdl.ui.downloadcard.DownloadsAlreadyExistDialog"

View file

@ -411,4 +411,7 @@
<string name="recode_video">Rikodo Videon</string> <string name="recode_video">Rikodo Videon</string>
<string name="recode_video_summary">Rikodon skedarin e videos sipas formatit të cilësuar nga përdoruesi</string> <string name="recode_video_summary">Rikodon skedarin e videos sipas formatit të cilësuar nga përdoruesi</string>
<string name="restore_info">Shtyp \'Rikthe\' për të kombinuar të dhënat e ruajtura me të dhënat aktuale. Shtyp \'Rivendos\' për të fshirë gjithçka dhe vetëm përdor të dhënat e ruajtura në skedar.</string> <string name="restore_info">Shtyp \'Rikthe\' për të kombinuar të dhënat e ruajtura me të dhënat aktuale. Shtyp \'Rivendos\' për të fshirë gjithçka dhe vetëm përdor të dhënat e ruajtura në skedar.</string>
<string name="daily">Çdo ditë</string>
<string name="weekly">Çdo javë</string>
<string name="monthly">Çdo muaj</string>
</resources> </resources>

View file

@ -1475,4 +1475,18 @@
<item>never</item> <item>never</item>
</string-array> </string-array>
<string-array name="cleanup_leftover_downloads">
<item>@string/disabled</item>
<item>@string/daily</item>
<item>@string/weekly</item>
<item>@string/monthly</item>
</string-array>
<string-array name="cleanup_leftover_downloads_values">
<item></item>
<item>daily</item>
<item>weekly</item>
<item>monthly</item>
</string-array>
</resources> </resources>

View file

@ -414,4 +414,8 @@
<string name="use_item_url_not_playlist_summary">Useful for Libretube offline playlists that are not recognised by yt-dlp. Playlist metadata wont be embedded with this enabled</string> <string name="use_item_url_not_playlist_summary">Useful for Libretube offline playlists that are not recognised by yt-dlp. Playlist metadata wont be embedded with this enabled</string>
<string name="recode_video">Recode Video</string> <string name="recode_video">Recode Video</string>
<string name="recode_video_summary">Recodes the video file to the specified video format</string> <string name="recode_video_summary">Recodes the video file to the specified video format</string>
<string name="daily">Daily</string>
<string name="weekly">Weekly</string>
<string name="monthly">Monthly</string>
<string name="cleanup_leftover_downloads">Clean-up leftover downloads (cancelled, errored)</string>
</resources> </resources>

View file

@ -145,6 +145,15 @@
app:summary="@string/log_downloads_summary" app:summary="@string/log_downloads_summary"
app:title="@string/log_downloads" /> app:title="@string/log_downloads" />
<ListPreference
android:defaultValue=""
android:entries="@array/cleanup_leftover_downloads"
android:entryValues="@array/cleanup_leftover_downloads_values"
android:icon="@drawable/baseline_delete_24"
app:key="cleanup_leftover_downloads"
app:useSimpleSummaryProvider="true"
app:title="@string/cleanup_leftover_downloads" />
<EditTextPreference <EditTextPreference
app:key="retries" app:key="retries"
android:icon="@drawable/ic_retries" android:icon="@drawable/ic_retries"

View file

@ -63,4 +63,25 @@ Add --recode-video toggle in the settings to use instead of --merge-output-forma
adjusted the "Adjust video" section in the download card and put all subtitle items together adjusted the "Adjust video" section in the download card and put all subtitle items together
Fixed app not selecting an audio format to show in the format view in the download card after updating formats Fixed app not selecting an audio format to show in the format view in the download card after updating formats
download_rescheduled_to shows up twice, one in original language and one in english
It's very difficult to tap on video icon for the video at the bottom
#529
https://www.instagram.com/reel/C9zTRlONU6G/?igsh=MWJ1djBhYjdscG5wMw== chooses smallest format
Limit audio id length
Remember scroll position in history tab
Make piped not fetch mix playlists
turn incognito toast to snackbar
duplicate dialog crash app when in share activity because the activity closes too early
4. In videos with multiple languages, previously, when I entered the suggested tab, all audios in my preferred language were displayed at the top, with the lowest quality. However, this no longer happens, and now I need to search among multiple languages (again, I'm not entirely sure about this, but it's challenging to compare with a previous version since I no longer have access to older app versions)