slight fixes

This commit is contained in:
deniscerri 2024-12-29 22:42:34 +01:00
parent 3c3ef92cbd
commit 2e5fc3206e
No known key found for this signature in database
GPG key ID: 95C43D517D830350
29 changed files with 381 additions and 136 deletions

View file

@ -18,7 +18,7 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- alarm scheduler-->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<!-- queueing processing downloads in the background-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

View file

@ -153,6 +153,9 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
fun getDownloadById(id: Long) : DownloadItem
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
fun getNullableDownloadById(id: Long) : DownloadItem?
@Query("SELECT * FROM downloads WHERE id IN (:ids)")
fun getDownloadsByIds(ids: List<Long>) : List<DownloadItem>

View file

@ -106,8 +106,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
}
}
suspend fun update(item: DownloadItem){
downloadDao.update(item)
suspend fun update(item: DownloadItem) : Long {
return downloadDao.update(item)
}
suspend fun updateAll(list: List<DownloadItem>) : List<DownloadItem> {

View file

@ -106,25 +106,37 @@ class HistoryRepository(private val historyDao: HistoryDao) {
suspend fun deleteAllWithIDs(ids: List<Long>, deleteFile: Boolean = false){
if (deleteFile){
historyDao.getAllHistoryByIDs(ids).forEach { item ->
item.downloadPath.forEach {
FileUtil.deleteFile(it)
ids.chunked(500).forEach { chunks ->
historyDao.getAllHistoryByIDs(chunks).forEach { item ->
item.downloadPath.forEach {
FileUtil.deleteFile(it)
}
}
}
}
historyDao.deleteAllByIDs(ids)
ids.chunked(500).forEach { chunks ->
historyDao.deleteAllByIDs(chunks)
}
}
suspend fun deleteAllWithIDsCheckFiles(ids: List<Long>){
val idsToDelete = mutableListOf<Long>()
historyDao.getAllHistoryByIDs(ids).forEach { item ->
val filesNotPresent = item.downloadPath.all { !File(it).exists() && it.isNotBlank()}
if (filesNotPresent) {
idsToDelete.add(item.id)
ids.chunked(500).forEach { chunks ->
historyDao.getAllHistoryByIDs(chunks).forEach { item ->
val filesNotPresent = item.downloadPath.all { !File(it).exists() && it.isNotBlank()}
if (filesNotPresent) {
idsToDelete.add(item.id)
}
}
}
if (idsToDelete.isNotEmpty()) {
historyDao.deleteAllByIDs(idsToDelete)
idsToDelete.chunked(500).forEach { chunked ->
historyDao.deleteAllByIDs(chunked)
}
}
}
@ -133,8 +145,12 @@ class HistoryRepository(private val historyDao: HistoryDao) {
)
fun getDownloadPathsFromIDs(ids: List<Long>) : List<List<String>> {
val res = historyDao.getDownloadPathsFromIDs(ids)
return res.map { it.downloadPath }
val res : MutableList<List<String>> = mutableListOf()
ids.chunked(500).forEach { chunks ->
val tmp = historyDao.getDownloadPathsFromIDs(chunks)
res.addAll(tmp.map { it.downloadPath })
}
return res
}
suspend fun deleteDuplicates(){

View file

@ -12,6 +12,7 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeWatchVideosURL
import com.deniscerri.ytdl.util.Extensions.needsDataUpdating
import com.deniscerri.ytdl.util.extractors.GoogleApiUtil
import com.deniscerri.ytdl.util.extractors.newpipe.NewPipeUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
@ -414,7 +415,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
suspend fun updateDownloadItem(
downloadItem: DownloadItem
) : DownloadItem? {
if (downloadItem.title.isEmpty() || downloadItem.author.isEmpty() || downloadItem.thumb.isEmpty()){
if (downloadItem.needsDataUpdating()){
runCatching {
val info = getResultsFromSource(downloadItem.url, resetResults = false, addToResults = false, singleItem = true).first()
if (downloadItem.title.isEmpty()) downloadItem.title = info.title

View file

@ -39,12 +39,14 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple
import com.deniscerri.ytdl.util.Extensions.needsDataUpdating
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdateMultipleDownloadsDataWorker
import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker
import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDL
@ -217,6 +219,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.update(item)
}
suspend fun putToSaved(item: DownloadItem) {
item.status = DownloadRepository.Status.Saved.toString()
val id = repository.update(item)
if (item.needsDataUpdating()) {
continueUpdatingDataInBackground(listOf(id))
}
}
fun getItemByID(id: Long) : DownloadItem {
return repository.getItemByID(id)
}
@ -641,7 +651,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
//repository.insert(downloadItem)
}
}
repository.insertAll(toInsert)
toInsert.chunked(500).forEach { chunked ->
repository.insertAll(chunked)
}
processingItems.emit(false)
} catch (e: Exception) {
deleteProcessing()
@ -678,7 +691,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
//repository.insert(downloadItem)
}
}
repository.insertAll(toInsert)
toInsert.chunked(500).forEach { chunked ->
repository.insertAll(chunked)
}
processingItems.emit(false)
}catch (e: Exception) {
deleteProcessing()
@ -1031,27 +1046,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
result.message = repository.startDownloadWorker(queued, context).getOrElse { "" }
if(!useScheduler){
CoroutineScope(Dispatchers.IO).launch {
queued.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
kotlin.runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}else{
CoroutineScope(Dispatchers.IO).launch {
queued.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
kotlin.runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}
val ids = queued.filter { it.needsDataUpdating() }.map { it.id }
continueUpdatingDataInBackground(ids)
}
@ -1188,6 +1184,25 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
private fun continueUpdatingDataInBackground(ids: List<Long>){
val id = System.currentTimeMillis().toInt()
val workRequest = OneTimeWorkRequestBuilder<UpdateMultipleDownloadsDataWorker>()
.setInputData(
Data.Builder()
.putLongArray("ids", ids.toLongArray())
.putInt("id", id)
.build())
.addTag("updateData")
.build()
val context = App.instance
WorkManager.getInstance(context).enqueueUniqueWork(
id.toString(),
ExistingWorkPolicy.REPLACE,
workRequest
)
}
suspend fun updateProcessingType(newType: Type) {
val processing = repository.getProcessingDownloads()
processing.apply {

View file

@ -15,6 +15,7 @@ import android.view.*
import android.view.View.*
import android.widget.*
import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.constraintlayout.widget.ConstraintLayout
@ -41,8 +42,10 @@ import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.adapter.HomeAdapter
import com.deniscerri.ytdl.ui.adapter.SearchSuggestionsAdapter
import com.deniscerri.ytdl.ui.more.WebViewActivity
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.isURL
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil
@ -64,6 +67,8 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.net.URL
import kotlin.collections.ArrayList
@ -263,25 +268,37 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
if (res.errorMessage != null){
val isSingleQueryAndURL = queryList.size == 1 && Patterns.WEB_URL.matcher(queryList.first()).matches()
kotlin.runCatching { UiUtil.handleNoResults(requireActivity(), res.errorMessage!!, continueAnyway = isSingleQueryAndURL, continued = {
lifecycleScope.launch {
if (sharedPreferences!!.getBoolean("download_card", true)) {
withContext(Dispatchers.Main){
showSingleDownloadSheet(
resultItem = downloadViewModel.createEmptyResultItem(queryList.first()),
type = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!),
disableUpdateData = true
kotlin.runCatching {
UiUtil.handleNoResults(requireActivity(), res.errorMessage!!,
url = if (isSingleQueryAndURL) queryList.first() else null,
continueAnyway = isSingleQueryAndURL,
continued = {
lifecycleScope.launch {
if (sharedPreferences!!.getBoolean("download_card", true)) {
withContext(Dispatchers.Main){
showSingleDownloadSheet(
resultItem = downloadViewModel.createEmptyResultItem(queryList.first()),
type = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!),
disableUpdateData = true
)
}
} else {
val downloadItem = downloadViewModel.createDownloadItemFromResult(
result = downloadViewModel.createEmptyResultItem(queryList.first()),
givenType = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!)
)
downloadViewModel.queueDownloads(listOf(downloadItem))
}
} else {
val downloadItem = downloadViewModel.createDownloadItemFromResult(
result = downloadViewModel.createEmptyResultItem(queryList.first()),
givenType = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!)
)
downloadViewModel.queueDownloads(listOf(downloadItem))
}
}
}, closed = {}) }
},
cookieFetch = {
val myIntent = Intent(requireContext(), WebViewActivity::class.java)
myIntent.putExtra("url", "https://${URL(queryList.first()).host}")
cookiesFetchedResultLauncher.launch(myIntent)
},
closed = {}
)
}
resultViewModel.uiState.update {it.copy(errorMessage = null) }
}
@ -324,6 +341,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
private var cookiesFetchedResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
sharedPreferences?.edit()?.putBoolean("use_cookies", true)?.apply()
startSearch()
}
}
override fun onResume() {
super.onResume()
if(arguments?.getString("url") == null){
@ -653,6 +679,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
resultViewModel.addSearchQueryToHistory(q)
}
}
startSearch()
}
private fun startSearch() {
lifecycleScope.launch(Dispatchers.IO){
resultViewModel.deleteAll()
if(sharedPreferences!!.getBoolean("quick_download", false) || sharedPreferences!!.getString("preferred_download_type", "video") == "command"){

View file

@ -161,7 +161,7 @@ class HistoryPaginatedAdapter(onItemClickListener: OnItemClickListener, activity
true
}
card.setOnClickListener {
if (checkedItems.size > 0) {
if (checkedItems.size > 0 || inverted) {
checkCard(card, item.id, position)
} else {
onItemClickListener.onCardClick(item.id, finalFilePresent)

View file

@ -1,8 +1,10 @@
package com.deniscerri.ytdl.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Build
@ -16,6 +18,7 @@ import android.widget.Button
import android.widget.LinearLayout
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.edit
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
@ -39,6 +42,7 @@ import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.receiver.ShareActivity
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.ui.more.WebViewActivity
import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -58,6 +62,7 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.net.URL
class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
@ -397,9 +402,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
dd.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
lifecycleScope.launch(Dispatchers.IO){
val item = getDownloadItem()
item.status = DownloadRepository.Status.Saved.toString()
downloadViewModel.updateDownload(item)
downloadViewModel.putToSaved(getDownloadItem())
dismiss()
}
}
@ -468,16 +471,25 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}
}
lifecycleScope.launch {
resultViewModel.uiState.collectLatest { res ->
if (res.errorMessage != null){
kotlin.runCatching { UiUtil.handleNoResults(requireActivity(), res.errorMessage!!, true, continued = {}, closed = {
dismiss()
}) }
kotlin.runCatching {
UiUtil.handleNoResults(requireActivity(), res.errorMessage!!,
url = result.url,
continueAnyway = true,
continued = {},
cookieFetch = {
val myIntent = Intent(requireContext(), WebViewActivity::class.java)
myIntent.putExtra("url", "https://${URL(result.url).host}")
cookiesFetchedResultLauncher.launch(myIntent)
},
closed = {
dismiss()
}
)
}
resultViewModel.uiState.update {it.copy(errorMessage = null) }
}
}
@ -657,6 +669,16 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
}
}
private var cookiesFetchedResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
sharedPreferences.edit().putBoolean("use_cookies", true).apply()
updateItem.isVisible = true
initUpdateData()
}
}
private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem {
return fragmentAdapter.getDownloadItem(selectedTabPosition)
}

View file

@ -236,7 +236,7 @@ class FormatSelectionBottomSheetDialog(
}
}.onFailure { err ->
withContext(Dispatchers.Main){
UiUtil.handleNoResults(requireActivity(), err.message.toString(), false, continued = {}, closed = {})
UiUtil.handleNoResults(requireActivity(), err.message.toString(), null, false, continued = {}, closed = {}, cookieFetch = {})
}
}

View file

@ -98,7 +98,8 @@ class WebViewActivity : BaseActivity() {
}
cookieManager.removeAllCookies(null)
withContext(Dispatchers.Main) {
onBackPressedDispatcher.onBackPressed()
this@WebViewActivity.setResult(RESULT_OK)
this@WebViewActivity.finish()
}
}
}

View file

@ -59,7 +59,8 @@ class ProcessingSettingsFragment : BaseSettingsFragment() {
setOnPreferenceClickListener {
val pref = prefs.getString("format_importance_audio", itemValues.joinToString(","))!!
val itms = pref.split(",").map {
val prefArr = pref.split(",")
val itms = itemValues.sortedBy { prefArr.indexOf(it) }.map {
Pair(it, items[itemValues.indexOf(it)])
}.toMutableList()
@ -80,7 +81,8 @@ class ProcessingSettingsFragment : BaseSettingsFragment() {
setOnPreferenceClickListener {
val pref = prefs.getString("format_importance_video", itemValues.joinToString(","))!!
val itms = pref.split(",").map {
val prefArr = pref.split(",")
val itms = itemValues.sortedBy { prefArr.indexOf(it) }.map {
Pair(it, items[itemValues.indexOf(it)])
}.toMutableList()

View file

@ -558,4 +558,9 @@ object Extensions {
t4.second
)
}
fun DownloadItem.needsDataUpdating() : Boolean {
return this.title.isBlank() || this.author.isBlank() || this.thumb.isBlank()
}
}

View file

@ -35,6 +35,7 @@ class FormatUtil(private var context: Context) {
val orderPreferences = sharedPreferences.getString("format_importance_audio", itemValues.joinToString(","))!!.split(",").toMutableSet()
if (preferSmallerFormats) {
orderPreferences.add("smallsize")
orderPreferences.remove("file_size")
}
return orderPreferences
@ -46,6 +47,7 @@ class FormatUtil(private var context: Context) {
val orderPreferences = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!!.split(",").toMutableSet()
if (preferSmallerFormats) {
orderPreferences.add("smallsize")
orderPreferences.remove("file_size")
}
return orderPreferences
@ -63,7 +65,10 @@ class FormatUtil(private var context: Context) {
"smallsize" -> {
(a.filesize).compareTo(b.filesize)
}
"file_size" -> {
val result = b.filesize.compareTo(a.filesize)
result
}
"id" -> {
(audioFormatIDPreference.contains(b.format_id)).compareTo(
audioFormatIDPreference.contains(a.format_id)
@ -119,6 +124,10 @@ class FormatUtil(private var context: Context) {
val result = a.filesize.compareTo(b.filesize)
result
}
"file_size" -> {
val result = b.filesize.compareTo(a.filesize)
result
}
"id" -> {
videoFormatIDPreference.contains(b.format_id).compareTo(videoFormatIDPreference.contains(a.format_id))
}

View file

@ -621,6 +621,63 @@ class NotificationUtil(var context: Context) {
}
fun createDataUpdateNotification(): Notification {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
return notificationBuilder
.setContentTitle(resources.getString(R.string.updating_download_data))
.setOngoing(true)
.setCategory(Notification.CATEGORY_PROGRESS)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setLargeIcon(
BitmapFactory.decodeResource(
resources,
R.drawable.ic_launcher_foreground_large
)
)
.setContentText("")
.setPriority(NotificationCompat.PRIORITY_LOW)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setProgress(PROGRESS_MAX, PROGRESS_CURR, false)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.clearActions()
.build()
}
@SuppressLint("MissingPermission")
fun updateDataUpdateNotification(
workID: Int,
workTag: String,
progress: Int,
queue: Int,
) {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID)
val contentText = """${queue - progress} ${resources.getString(R.string.items_left)}"""
val cancelIntent = Intent(context, CancelWorkReceiver::class.java)
cancelIntent.putExtra("workTag", workTag)
val cancelNotificationPendingIntent = PendingIntent.getBroadcast(
context,
workID,
cancelIntent,
PendingIntent.FLAG_IMMUTABLE
)
try {
notificationBuilder.setProgress(queue, progress, progress == 0)
.setContentTitle(resources.getString(R.string.updating_download_data))
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.clearActions()
.addAction(0, resources.getString(R.string.cancel), cancelNotificationPendingIntent)
notificationManager.notify(workID, notificationBuilder.build())
} catch (e: Exception) {
e.printStackTrace()
}
}
@SuppressLint("MissingPermission")
fun showFormatsUpdatedNotification(downloadIds: List<Long>) {
val notificationBuilder = getBuilder(DOWNLOAD_FINISHED_CHANNEL_ID)

View file

@ -5,6 +5,7 @@ import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
@ -33,6 +34,7 @@ import android.widget.PopupMenu
import android.widget.RadioButton
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DimenRes
import androidx.annotation.OptIn
import androidx.appcompat.app.AlertDialog
@ -62,6 +64,7 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.ui.downloadcard.VideoCutListener
import com.deniscerri.ytdl.ui.more.WebViewActivity
import com.deniscerri.ytdl.util.Extensions.enableTextHighlight
import com.deniscerri.ytdl.util.Extensions.getMediaDuration
import com.deniscerri.ytdl.util.Extensions.toStringDuration
@ -94,6 +97,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
@ -1633,9 +1637,7 @@ object UiUtil {
}
}
fun handleNoResults(context: Activity, message: String, continueAnyway: Boolean = false, continued: () -> Unit, closed: () -> Unit) {
fun handleNoResults(context: Activity, message: String, url: String? = null, continueAnyway: Boolean = false, continued: () -> Unit, closed: () -> Unit, cookieFetch: () -> Unit) {
val errDialog = MaterialAlertDialogBuilder(context)
.setTitle(R.string.no_results)
.setMessage(message)
@ -1646,12 +1648,19 @@ object UiUtil {
d?.dismiss()
}
if (continueAnyway) {
val cookieRelated = message.contains("cookie", true) || message.contains("sign in", true)
if (cookieRelated && !url.isNullOrBlank()) {
errDialog.setNeutralButton(context.getString(R.string.get_cookies)) { d: DialogInterface?, _ : Int ->
cookieFetch()
}
}else if (continueAnyway) {
errDialog.setNeutralButton(R.string.continue_anyway) {d: DialogInterface?, _:Int ->
continued()
}
}
errDialog.setOnCancelListener {
closed()
}

View file

@ -228,7 +228,7 @@ class UpdateUtil(var context: Context) {
updatingYTDL = true
val channel = if (c.isNullOrBlank()) sharedPreferences.getString("ytdlp_source", "nightly") else c
val channel = if (c.isNullOrBlank()) sharedPreferences.getString("ytdlp_source", "stable") else c
val request = YoutubeDLRequest(emptyList())
request.addOption("--update-to", "${channel}@latest")

View file

@ -110,7 +110,7 @@ class YTDLPUtil(private val context: Context) {
}
}
if (searchEngine == "ytsearch" || query.isYoutubeURL()) {
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
request.setYoutubeExtractorArgs()
}
request.addOption("--flat-playlist")
@ -250,7 +250,7 @@ class YTDLPUtil(private val context: Context) {
fun getYoutubeWatchLater() : ArrayList<ResultItem> {
val request = YoutubeDLRequest(listOf())
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
request.setYoutubeExtractorArgs()
request.addOption( "-j")
request.addOption("--flat-playlist")
request.applyDefaultOptionsForFetchingData()
@ -269,7 +269,7 @@ class YTDLPUtil(private val context: Context) {
fun getYoutubeRecommendations() : ArrayList<ResultItem> {
val request = YoutubeDLRequest(listOf())
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
request.setYoutubeExtractorArgs()
request.addOption( "-j")
request.addOption("--flat-playlist")
request.applyDefaultOptionsForFetchingData()
@ -288,7 +288,7 @@ class YTDLPUtil(private val context: Context) {
fun getYoutubeLikedVideos() : ArrayList<ResultItem> {
val request = YoutubeDLRequest(listOf())
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
request.setYoutubeExtractorArgs()
request.addOption( "-j")
request.addOption("--flat-playlist")
request.applyDefaultOptionsForFetchingData()
@ -307,7 +307,7 @@ class YTDLPUtil(private val context: Context) {
fun getYoutubeWatchHistory() : ArrayList<ResultItem> {
val request = YoutubeDLRequest(listOf())
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
request.setYoutubeExtractorArgs()
request.addOption( "-j")
request.addOption("--flat-playlist")
request.applyDefaultOptionsForFetchingData()
@ -574,9 +574,12 @@ class YTDLPUtil(private val context: Context) {
}else{
addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}")
}
addOption("-i")
}
}
request.addOption("--newline")
val metadataCommands = mutableListOf<String>()
if (downloadItem.playlistIndex != null && useItemURL) {
@ -732,21 +735,22 @@ class YTDLPUtil(private val context: Context) {
}
if (downloadItem.url.isYoutubeURL()) {
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
request.setYoutubeExtractorArgs()
}
if (!sharedPreferences.getBoolean("disable_write_info_json", false)) {
val cachePath = "${FileUtil.getCachePath(context)}infojsons"
val infoJsonName = MessageDigest.getInstance("MD5").digest(downloadItem.url.toByteArray()).toHexString()
val infoJsonFile = File(cachePath).walkTopDown().firstOrNull { it.name == "${infoJsonName}.info.json" }
//ignore info file if its older than 5 hours. puny measure to prevent expired formats in some cases
if (infoJsonFile == null || System.currentTimeMillis() - infoJsonFile.lastModified() > (1000 * 60 * 60 * 5)) {
request.addCommands(listOf("--print-to-file", "video:%()#j", "${cachePath}/${infoJsonName}.info.json"))
}else {
request.addOption("--load-info-json", infoJsonFile.absolutePath)
}
}
//TODO REVIEW TO ADD THIS AGAIN LATER?
// if (!sharedPreferences.getBoolean("disable_write_info_json", false)) {
// val cachePath = "${FileUtil.getCachePath(context)}infojsons"
// val infoJsonName = MessageDigest.getInstance("MD5").digest(downloadItem.url.toByteArray()).toHexString()
//
// val infoJsonFile = File(cachePath).walkTopDown().firstOrNull { it.name == "${infoJsonName}.info.json" }
// //ignore info file if its older than 5 hours. puny measure to prevent expired formats in some cases
// if (infoJsonFile == null || System.currentTimeMillis() - infoJsonFile.lastModified() > (1000 * 60 * 60 * 5)) {
// request.addCommands(listOf("--print-to-file", "video:%()#j", "${cachePath}/${infoJsonName}.info.json"))
// }else {
// request.addOption("--load-info-json", infoJsonFile.absolutePath)
// }
// }
}
if (sharedPreferences.getString("prevent_duplicate_downloads", "")!! == "download_archive"){
@ -856,7 +860,7 @@ class YTDLPUtil(private val context: Context) {
metadataCommands.addOption("--parse-metadata", "%(album_artist,first_artist|)s:%(album_artist)s")
metadataCommands.addOption("--parse-metadata", "description:(?:Released on: )(?P<dscrptn_year>\\d{4})")
metadataCommands.addOption("--parse-metadata", "description:(?:.+?Released\\ on\\s*(?P<dscrptn_year>\\d{4}))?")
metadataCommands.addOption("--parse-metadata", "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:(?P<meta_date>\\d+)")
if (isPlaylistItem) {
@ -1167,7 +1171,7 @@ class YTDLPUtil(private val context: Context) {
return YoutubeDL.getInstance().execute(req).out.trim()
}
private fun getYoutubeExtractorArgs() : String {
private fun YoutubeDLRequest.setYoutubeExtractorArgs() {
val playerClient = sharedPreferences.getString("yt_player_client", "")!!.split(",").filter { it.isNotBlank() }.toMutableList()
val extractorArgs = mutableListOf<String>()
@ -1196,7 +1200,10 @@ class YTDLPUtil(private val context: Context) {
extractorArgs.add(otherArgs)
}
return extractorArgs.joinToString(";")
val extArgs = extractorArgs.joinToString(";")
if (extractorArgs.isNotEmpty()) {
this.addOption("--extractor-args", "youtube:${extArgs}")
}
}
private fun YoutubeDLRequest.addConfig(commandString: String) {

View file

@ -15,6 +15,7 @@ import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
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.models.DownloadItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
@ -22,6 +23,7 @@ import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.util.Extensions.calculateNextTimeForObserving
import com.deniscerri.ytdl.util.Extensions.needsDataUpdating
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
@ -113,8 +115,6 @@ class ObserveSourceWorker(
val string = Gson().toJson(item.downloadItemTemplate, DownloadItem::class.java)
val downloadItem = Gson().fromJson(string, DownloadItem::class.java)
downloadItem.url = it.url
downloadItem.title = it.title
downloadItem.author = it.author
downloadItem.thumb = it.thumb
downloadItem.status = DownloadRepository.Status.Queued.toString()
downloadItem.playlistTitle = it.playlistTitle
@ -137,9 +137,6 @@ class ObserveSourceWorker(
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
alarmScheduler.schedule()
}
// if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
// items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
@ -184,33 +181,17 @@ class ObserveSourceWorker(
}
}
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime() && alarmScheduler.canSchedule()){
alarmScheduler.schedule()
}else {
downloadRepo.startDownloadWorker(queuedItems, context)
if(!useScheduler){
queuedItems.filter { it.downloadStartTime != 0L || (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
runCatching {
resultsRepo.updateDownloadItem(it)?.apply {
downloadRepo.updateWithoutUpsert(this)
}
}
}
}else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
runCatching {
resultsRepo.updateDownloadItem(it)?.apply {
downloadRepo.updateWithoutUpsert(this)
}
}
}
}
}
item.alreadyProcessedLinks.removeAll(items.map { it.url })
item.alreadyProcessedLinks.addAll(items.map { it.url })
}
item.runCount = item.runCount + 1
item.runCount += 1
if (item.runCount > item.endsAfterCount && item.endsAfterCount > 0){
item.status = ObserveSourcesRepository.SourceStatus.STOPPED

View file

@ -0,0 +1,77 @@
package com.deniscerri.ytdl.work
import android.content.Context
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.os.Build
import androidx.work.ForegroundInfo
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.NotificationUtil
import kotlinx.coroutines.runBlocking
class UpdateMultipleDownloadsDataWorker(
private val context: Context,
workerParams: WorkerParameters
) : Worker(context, workerParams) {
override fun doWork(): Result {
val dbManager = DBManager.getInstance(context)
val dao = dbManager.downloadDao
val resDao = dbManager.resultDao
val resultRepo = ResultRepository(resDao, context)
val notificationUtil = NotificationUtil(context)
val ids = inputData.getLongArray("ids")!!.toMutableList()
val workID = inputData.getInt("id", 0)
if (workID == 0) return Result.failure()
val notification = notificationUtil.createDataUpdateNotification()
if (Build.VERSION.SDK_INT > 33) {
setForegroundAsync(ForegroundInfo(workID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE))
}else{
setForegroundAsync(ForegroundInfo(workID, notification))
}
var count = 0
return try{
ids.forEach {
if (!isStopped){
val d = dao.getDownloadById(it)
if (d.title.isNotBlank() && d.author.isNotBlank() && d.thumb.isNotBlank()) {
count++
return@forEach
}
runCatching {
runBlocking {
resultRepo.updateDownloadItem(d)?.apply {
val dd = dao.getNullableDownloadById(it)
if (dd != null) {
d.status = dd.status
dao.updateWithoutUpsert(this)
}
}
}
}
count++
notificationUtil.updateDataUpdateNotification(workID, UpdateMultipleDownloadsDataWorker::class.java.name, count, ids.size)
}else{
throw Exception()
}
}
Result.success()
}catch (e: Exception){
notificationUtil.cancelDownloadNotification(workID)
ids.clear()
Result.failure()
}
}
}

View file

@ -10,7 +10,6 @@
android:paddingTop="10dp"
android:layout_width="wrap_content"
android:paddingBottom="5dp"
android:textSize="15sp"
android:layout_height="wrap_content"
android:text="@string/adjust_audio" />

View file

@ -9,7 +9,6 @@
<TextView
android:layout_width="wrap_content"
android:paddingBottom="5dp"
android:textSize="15sp"
android:layout_height="wrap_content"
android:text="@string/adjust_templates" />

View file

@ -12,8 +12,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="5dp"
android:text="@string/adjust_video"
android:textSize="15sp" />
android:text="@string/adjust_video"/>
<HorizontalScrollView
android:scrollbars="none"

View file

@ -19,7 +19,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="20dp"
android:layout_marginTop="15dp"
android:orientation="horizontal">
<com.facebook.shimmer.ShimmerFrameLayout
@ -104,6 +104,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:outlineProvider="none"
app:icon="@drawable/ic_clock"
android:contentDescription="@string/schedule"
app:layout_constraintBottom_toBottomOf="@id/bottomsheet_download_button"
@ -117,6 +118,7 @@
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/download"
android:outlineProvider="none"
app:icon="@drawable/ic_down"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -129,7 +131,8 @@
android:id="@+id/download_tablayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:layout_marginHorizontal="10dp"
android:layout_marginTop="10dp"
android:backgroundTint="@android:color/transparent"
app:tabGravity="start"
app:tabMinWidth="0dp"

View file

@ -19,7 +19,7 @@
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:paddingHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
@ -53,6 +53,7 @@
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/ok"
android:outlineProvider="none"
app:icon="@drawable/ic_check"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -102,7 +102,9 @@
style="@style/Widget.Material3.Chip.Assist.Elevated"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_margin="12dp"
android:layout_marginHorizontal="5dp"
android:layout_marginVertical="8dp"
android:outlineProvider="none"
app:chipIcon="@drawable/baseline_loop_24"
app:chipIconVisible="true"
app:chipMinTouchTargetSize="0dp"

View file

@ -21,6 +21,7 @@
android:layout_height="match_parent"
app:cardCornerRadius="20dp"
app:cardElevation="5dp"
app:cardBackgroundColor="?attr/colorSurfaceContainer"
app:strokeColor="?attr/colorPrimary"
app:checkedIconTint="?attr/colorPrimary"
app:cardMaxElevation="12dp"
@ -71,8 +72,8 @@
android:paddingEnd="10dp"
android:scrollbars="none"
android:shadowColor="@color/black"
android:shadowDx="2"
android:shadowDy="2"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="1"
android:textColor="#FFF"
android:textSize="17sp"
@ -89,8 +90,8 @@
android:paddingEnd="10dp"
android:scrollbars="none"
android:shadowColor="@color/black"
android:shadowDx="2"
android:shadowDy="2"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="1"
android:textColor="#FFF"
android:textSize="12sp"
@ -107,8 +108,8 @@
android:paddingEnd="10dp"
android:paddingBottom="20dp"
android:shadowColor="@color/black"
android:shadowDx="2"
android:shadowDy="2"
android:shadowDx="1"
android:shadowDy="1"
android:shadowRadius="1"
android:textColor="@color/white"
android:textSize="12sp"
@ -124,6 +125,7 @@
app:elevation="0dp"
android:elevation="0dp"
app:borderWidth="0dp"
android:outlineProvider="none"
android:contentDescription="@string/audio"
android:layout_height="55dp"
android:layout_marginVertical="10dp"
@ -142,6 +144,7 @@
android:elevation="0dp"
android:contentDescription="@string/video"
app:borderWidth="0dp"
android:outlineProvider="none"
app:icon="@drawable/ic_video"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />

View file

@ -1462,6 +1462,7 @@
<item>@string/language</item>
<item>@string/codec</item>
<item>@string/container</item>
<item>@string/file_size</item>
</string-array>
<string-array name="format_importance_audio_values">
@ -1469,6 +1470,7 @@
<item>language</item>
<item>codec</item>
<item>container</item>
<item>file_size</item>
</string-array>
<string-array name="format_importance_video">
@ -1477,6 +1479,7 @@
<item>@string/codec</item>
<item>@string/no_audio</item>
<item>@string/container</item>
<item>@string/file_size</item>
</string-array>
<string-array name="format_importance_video_values">
@ -1485,6 +1488,7 @@
<item>codec</item>
<item>no_audio</item>
<item>container</item>
<item>file_size</item>
</string-array>
<string-array name="label_visibility">

View file

@ -33,7 +33,7 @@
android:title="@string/data_fetching_extra_command" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/downloading">
<PreferenceCategory android:title="@string/downloading" android:enabled="false">
<SwitchPreferenceCompat
android:icon="@drawable/if_file_rename"
android:key="disable_write_info_json"