From 087ba0781a6262b73af9286e08461f307fe517dc Mon Sep 17 00:00:00 2001 From: zaednasr <75589932+zaednasr@users.noreply.github.com> Date: Sun, 11 Aug 2024 17:28:02 +0200 Subject: [PATCH] 1.7.9.1 prep --- app/build.gradle | 1 + app/proguard-rules.pro | 6 + app/src/main/AndroidManifest.xml | 125 ++++++++++++------ .../java/com/deniscerri/ytdl/MainActivity.kt | 7 +- .../deniscerri/ytdl/database/dao/LogDao.kt | 7 +- .../ytdl/database/dao/TerminalDao.kt | 7 +- .../database/viewmodel/DownloadViewModel.kt | 34 +++-- .../database/viewmodel/ResultViewModel.kt | 61 +++++---- .../com/deniscerri/ytdl/ui/HomeFragment.kt | 37 +++++- .../adapter/ActiveDownloadMinifiedAdapter.kt | 8 +- .../downloadcard/DownloadBottomSheetDialog.kt | 34 ++--- .../DownloadMultipleBottomSheetDialog.kt | 3 + .../FormatSelectionBottomSheetDialog.kt | 22 ++- .../downloadcard/ResultCardDetailsDialog.kt | 88 +++++++----- .../more/downloadLogs/DownloadLogFragment.kt | 41 ++++-- .../more/settings/FolderSettingsFragment.kt | 5 +- .../com/deniscerri/ytdl/util/Extensions.kt | 35 +++-- .../java/com/deniscerri/ytdl/util/FileUtil.kt | 5 + .../java/com/deniscerri/ytdl/util/InfoUtil.kt | 94 ++++++++----- .../com/deniscerri/ytdl/util/NavbarUtil.kt | 1 + .../deniscerri/ytdl/util/NotificationUtil.kt | 3 +- .../java/com/deniscerri/ytdl/util/UiUtil.kt | 61 +++++---- .../deniscerri/ytdl/work/DownloadWorker.kt | 96 ++++++++------ .../ytdl/work/TerminalDownloadWorker.kt | 2 +- .../main/res/drawable/baseline_album_24.xml | 5 + app/src/main/res/drawable/rounded_corner.xml | 2 +- .../layout/active_download_card_minified.xml | 44 +++--- app/src/main/res/layout/download_card.xml | 2 +- app/src/main/res/layout/format_item.xml | 2 +- .../main/res/layout/fragment_download_log.xml | 117 +++++++++------- .../main/res/layout/queued_download_card.xml | 2 +- .../main/res/layout/result_card_details.xml | 8 +- .../res/menu/active_downloads_minified.xml | 24 ++-- app/src/main/res/menu/log_detail_menu.xml | 1 - app/src/main/res/values/arrays.xml | 5 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 5 + .../main/res/xml/processing_preferences.xml | 8 ++ 38 files changed, 642 insertions(+), 369 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_album_24.xml diff --git a/app/build.gradle b/app/build.gradle index 2ee78c91..e0aec3d9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -201,4 +201,5 @@ dependencies { implementation 'androidx.compose.material3:material3-android:1.2.1' implementation "io.noties.markwon:core:4.6.2" implementation("org.greenrobot:eventbus:3.3.1") + //implementation("com.github.TeamNewPipe:NewPipeExtractor:0.24.2") } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index cf608782..e4c07011 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -88,5 +88,11 @@ static <1>$Companion Companion; } +### Rules for NewPipeExtractor +#-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; } +#-keep class org.mozilla.javascript.** { *; } +#-keep class org.mozilla.classfile.ClassFileWriter +#-dontwarn org.mozilla.javascript.tools.** + -keepattributes RuntimeVisibleAnnotations,AnnotationDefault -dontobfuscate diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 017d5f45..93060c51 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -80,24 +80,46 @@ - - - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -114,7 +136,7 @@ - + @@ -122,162 +144,177 @@ + + + + + + + + + + + + + + - - - - - - - - - - - - + + + + + + + + + + + + + + - - - + - - - - - + - - - - - - - - - - + + + + + + + - - - - - - + + + + + + + + + + + + + + { navController.navigate(R.id.historyFragment) diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/LogDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/LogDao.kt index 3a99eebe..8abcfb76 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/LogDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/LogDao.kt @@ -7,6 +7,7 @@ import androidx.room.Query import androidx.room.Transaction import androidx.room.Update import com.deniscerri.ytdl.database.models.LogItem +import com.deniscerri.ytdl.util.Extensions.appendLineToLog import kotlinx.coroutines.flow.Flow @Dao @@ -43,10 +44,10 @@ interface LogDao { suspend fun updateLog(line: String, id: Long, resetLog: Boolean = false) { val l = getByID(id) ?: return var log = l.content ?: "" - if (resetLog) { - log = line.lines().joinToString("\n") + log = if (resetLog) { + line.appendLineToLog() }else{ - log += "\n${line}" + log.appendLineToLog(line) } updateLogContent(log, id) } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/TerminalDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/TerminalDao.kt index a3ee24c7..d10d0da3 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/TerminalDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/TerminalDao.kt @@ -5,6 +5,7 @@ import androidx.room.Insert import androidx.room.Query import androidx.room.Transaction import com.deniscerri.ytdl.database.models.TerminalItem +import com.deniscerri.ytdl.util.Extensions.appendLineToLog import kotlinx.coroutines.flow.Flow @Dao @@ -31,10 +32,10 @@ interface TerminalDao { suspend fun updateLog(line: String, id: Long, resetLog : Boolean = false){ val t = getTerminalById(id) ?: return var log = t.log ?: "" - if (resetLog) { - log = line.lines().joinToString("\n") + log = if (resetLog) { + line.lines().joinToString("\n") }else{ - log += "\n${line}" + log.appendLineToLog(line) } updateTerminalLog(log, id) } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt index d756e7be..56a846ff 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.SharedPreferences import android.content.res.Configuration import android.content.res.Resources +import android.os.Build import android.os.Handler import android.os.Looper import android.os.Parcelable @@ -148,7 +149,16 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val confTmp = Configuration(application.resources.configuration) - confTmp.setLocale(Locale(sharedPreferences.getString("app_language", "en")!!)) + val locale = if (Build.VERSION.SDK_INT < 33) { + sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language } + }else{ + Locale.getDefault().language + }.run { + split("-") + }.run { + if (this.size == 1) Locale(this[0]) else Locale(this[0], this[1]) + } + confTmp.setLocale(locale) val metrics = DisplayMetrics() resources = Resources(application.assets, metrics, confTmp) } @@ -245,6 +255,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats) + val subsLanguages = sharedPreferences.getString("subs_lang", "en.*,.*-orig")!! val videoPreferences = VideoPreferences( embedSubs, @@ -252,6 +263,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel ArrayList(sponsorblock), saveSubs, saveAutoSubs, + subsLanguages, audioFormatIDs = preferredAudioFormats, recodeVideo = recodeVideo ) @@ -354,8 +366,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val format = getFormat(it.allFormats, type) it.format = format - var updatedDownloadPath: String = "" - var container: String = "" + var updatedDownloadPath = "" + var container = "" when(type){ Type.audio -> { @@ -363,11 +375,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel container = sharedPreferences.getString("audio_format", "")!! } Type.video -> { - updatedDownloadPath = sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())!! - container = sharedPreferences.getString("audio_format", "")!! + updatedDownloadPath = sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())!! + container = sharedPreferences.getString("video_format", "")!! } Type.command -> { - updatedDownloadPath = sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())!! + updatedDownloadPath = sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())!! container = "" } else -> { @@ -391,6 +403,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false) val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false) + val subsLanguages = sharedPreferences.getString("subs_lang", "en.*,.*-orig")!! val customFileNameTemplate = when(historyItem.type) { Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader).30B - %(title).170B") @@ -420,7 +433,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!)) - val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs, saveAutoSubs, recodeVideo = recodeVideo) + val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs, saveAutoSubs, subsLanguages, recodeVideo = recodeVideo) var path = defaultPath historyItem.downloadPath.first().apply { File(this).parent?.apply { @@ -930,10 +943,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel }else{ val queued = repository.updateAll(queuedItems) - if (!sharedPreferences.getBoolean("paused_downloads", false)) { - result.message = repository.startDownloadWorker(queued, context).getOrElse { "" } + if (sharedPreferences.getBoolean("paused_downloads", true)) { + sharedPreferences.edit().putBoolean("paused_downloads", false).apply() + resetActiveToQueued() } + 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 { diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt index 3fbc2b07..c0b074a1 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt @@ -22,11 +22,16 @@ import com.deniscerri.ytdl.database.repository.SearchHistoryRepository import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.NotificationUtil import com.yausername.youtubedl_android.YoutubeDLException +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit import java.util.concurrent.CancellationException @@ -40,18 +45,13 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a private val dao: ResultDao data class ResultsUiState( var processing: Boolean, - var errorMessage: Pair?, - var actions: MutableList>? + var errorMessage: String?, + var canContinueOnError: Boolean = false ) - enum class ResultAction { - COPY_LOG - } - val uiState: MutableStateFlow = MutableStateFlow(ResultsUiState( processing = false, errorMessage = null, - actions = null )) @@ -63,7 +63,8 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a var updateFormatsResultData: MutableStateFlow?> = MutableStateFlow(null) private var updateFormatsResultDataJob: Job? = null - var parsingQueries: Job? = null + private var parsingQueries: Job? = null + private var parsingQueriesJobList : MutableList = mutableListOf() private val sharedPreferences: SharedPreferences @@ -101,6 +102,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a fun cancelParsingQueries(){ parsingQueries?.cancel(CancellationException()) + parsingQueriesJobList.forEach { it.cancel(CancellationException()) } uiState.update {it.copy(processing = false)} } @@ -110,23 +112,29 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a } val resetResults = inputQueries.size == 1 - uiState.update {it.copy(processing = true, errorMessage = null, actions = null)} + uiState.update {it.copy(processing = true, errorMessage = null)} val res = mutableListOf() - inputQueries.forEach { inputQuery -> - try { - res.addAll(repository.getResultsFromSource(inputQuery, resetResults)) - } catch (e: Exception) { - if (updateResultDataJob?.isCancelled == false || e is YoutubeDLException){ - uiState.update {it.copy( - processing = false, - errorMessage = Pair(R.string.no_results, e.message.toString()), - actions = mutableListOf(Pair(R.string.copy_log, ResultAction.COPY_LOG)) - )} - Log.e(tag, e.toString()) - } - } + val requestSemaphore = Semaphore(10) + inputQueries.forEach { inputQuery -> + parsingQueriesJobList.add(viewModelScope.launch(Dispatchers.IO){ + requestSemaphore.withPermit { + try { + res.addAll(repository.getResultsFromSource(inputQuery, resetResults)) + } catch (e: Exception) { + if (updateResultDataJob?.isCancelled == false || e is YoutubeDLException){ + uiState.update {it.copy( + processing = false, + errorMessage = e.message.toString(), + )} + Log.e(tag, e.toString()) + } + + } + } + }) } + parsingQueriesJobList.joinAll() if (!isForegrounded() && inputQueries.size > 1){ notificationUtil.showQueriesFinished() } @@ -279,17 +287,12 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a uiState.update { it.copy( processing = false, - errorMessage = Pair(R.string.no_results, e.message.toString()), - actions = mutableListOf( - Pair( - R.string.copy_log, - ResultAction.COPY_LOG - ) - ) + errorMessage = e.message.toString(), ) } Log.e(tag, e.toString()) } + updatingFormats.emit(false) } } updateFormatsResultDataJob?.start() diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt index 64c96b24..e296ceaa 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt @@ -114,6 +114,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti private var appBarLayout: AppBarLayout? = null private var materialToolbar: MaterialToolbar? = null private var loadingItems: Boolean = false + private var queryList = mutableListOf() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? @@ -255,8 +256,28 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti launch{ resultViewModel.uiState.collectLatest { res -> if (res.errorMessage != null){ - kotlin.runCatching { UiUtil.handleResultResponse(requireActivity(), res, closed ={}) } - resultViewModel.uiState.update {it.copy(errorMessage = null, actions = 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 + ) + } + } 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 = {}) } + resultViewModel.uiState.update {it.copy(errorMessage = null) } } loadingItems = res.processing @@ -270,9 +291,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti shimmerCards!!.stopShimmer() shimmerCards!!.visibility = GONE if (resultsList!!.size > 1 && resultsList!![0]!!.playlistTitle.isNotEmpty()){ - this@HomeFragment.downloadAllFab!!.visibility = VISIBLE + downloadAllFab!!.visibility = VISIBLE }else{ - this@HomeFragment.downloadAllFab!!.visibility = GONE + downloadAllFab!!.visibility = GONE } } } @@ -602,7 +623,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti private fun initSearch(searchView: SearchView){ - val queryList = mutableListOf() + queryList = mutableListOf() if (queriesChipGroup!!.childCount > 0){ queriesChipGroup!!.children.forEach { val query = (it as Chip).text.toString().trim {it2 -> it2 <= ' '} @@ -689,7 +710,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti @SuppressLint("RestrictedApi") private fun showSingleDownloadSheet( resultItem: ResultItem, - type: DownloadViewModel.Type + type: DownloadViewModel.Type, + disableUpdateData : Boolean = false ){ if(findNavController().currentBackStack.value.firstOrNull {it.destination.id == R.id.downloadBottomSheetDialog} == null && findNavController().currentDestination?.id == R.id.homeFragment @@ -698,6 +720,9 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti val bundle = Bundle() bundle.putParcelable("result", resultItem) bundle.putSerializable("type", downloadViewModel.getDownloadType(type, resultItem.url)) + if (disableUpdateData) { + bundle.putBoolean("disableUpdateData", true) + } findNavController().navigate(R.id.downloadBottomSheetDialog, bundle) } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt index 540a7fc9..1a4ba055 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt @@ -76,6 +76,10 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac progressBar.progress = 0 progressBar.isIndeterminate = true + //OUTPUT ------------------------------------------------------------ + val output = card.findViewById(R.id.output) + output.tag = "${item.id}##output" + // TITLE ---------------------------------- val itemTitle = card.findViewById(R.id.title) var title = item.title.ifEmpty { item.playlistTitle.ifEmpty { item.url } } @@ -131,8 +135,8 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac popup.menuInflater.inflate(R.menu.active_downloads_minified, popup.menu) if (Build.VERSION.SDK_INT > 27) popup.menu.setGroupDividerEnabled(true) - val pause = popup.menu[0] - val resume = popup.menu[1] +// val pause = popup.menu[0] +// val resume = popup.menu[1] // // if (paused){ // pause.isVisible = false diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt index 477c4cdc..65dd89f9 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt @@ -86,6 +86,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { private lateinit var result: ResultItem private lateinit var type: Type + private var disableUpdateData : Boolean = false private var currentDownloadItem: DownloadItem? = null private var incognito: Boolean = false @@ -108,6 +109,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { dwl = arguments?.getParcelable("downloadItem") } type = arguments?.getSerializable("type") as Type + disableUpdateData = arguments?.getBoolean("disableUpdateData") == true if (res == null){ dismiss() @@ -429,13 +431,11 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { } //if auto-update after the card is open is off - if (result.title.isEmpty() && currentDownloadItem == null) { - if(sharedPreferences.getBoolean("quick_download", false)) { - (updateItem.parent as LinearLayout).visibility = View.VISIBLE - updateItem.setOnClickListener { - (updateItem.parent as LinearLayout).visibility = View.GONE - initUpdateData() - } + if (result.title.isEmpty() && currentDownloadItem == null && sharedPreferences.getBoolean("quick_download", false)) { + (updateItem.parent as LinearLayout).visibility = View.VISIBLE + updateItem.setOnClickListener { + (updateItem.parent as LinearLayout).visibility = View.GONE + initUpdateData() } }else{ (updateItem.parent as LinearLayout).visibility = View.GONE @@ -463,24 +463,28 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { //update in the background if there is no data - if(result.title.isEmpty() && currentDownloadItem == null && !sharedPreferences.getBoolean("quick_download", false) && type != Type.command){ - initUpdateData() - }else { - val usingGenericFormatsOrEmpty = result.formats.isEmpty() || result.formats.any { it.format_note.contains("ytdlnisgeneric") } - if (usingGenericFormatsOrEmpty && sharedPreferences.getBoolean("update_formats", false) && !sharedPreferences.getBoolean("quick_download", false)){ - initUpdateFormats(result) + if (!disableUpdateData) { + if(result.title.isEmpty() && currentDownloadItem == null && !sharedPreferences.getBoolean("quick_download", false) && type != Type.command){ + initUpdateData() + }else { + val usingGenericFormatsOrEmpty = result.formats.isEmpty() || result.formats.any { it.format_note.contains("ytdlnisgeneric") } + if (usingGenericFormatsOrEmpty && sharedPreferences.getBoolean("update_formats", false) && !sharedPreferences.getBoolean("quick_download", false)){ + initUpdateFormats(result) + } } } + + lifecycleScope.launch { resultViewModel.uiState.collectLatest { res -> if (res.errorMessage != null){ - kotlin.runCatching { UiUtil.handleResultResponse(requireActivity(), res, closed = { + kotlin.runCatching { UiUtil.handleNoResults(requireActivity(), res.errorMessage!!, true, continued = {}, closed = { dismiss() }) } - resultViewModel.uiState.update {it.copy(errorMessage = null, actions = null) } + resultViewModel.uiState.update {it.copy(errorMessage = null) } } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt index 5474307e..a0a9c1ef 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt @@ -774,6 +774,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure val fileSizes = mutableListOf() items.forEach { if (it.type == DownloadViewModel.Type.video){ + println(it.format.filesize) if (it.format.filesize <= 5L) { fileSizes.add(0) }else{ @@ -787,6 +788,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure } fileSizes.add(it.format.filesize + audioFormatSize) } + }else if (it.type == DownloadViewModel.Type.audio){ + fileSizes.add(it.format.filesize) } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index 0a6e3d7a..53a60945 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -114,7 +114,7 @@ class FormatSelectionBottomSheetDialog( return } - items = _items.distinctBy { it!!.url }.toMutableList() + items = _items.toMutableList() if (items.size == 1) { formats = items.first()!!.allFormats }else{ @@ -192,8 +192,10 @@ class FormatSelectionBottomSheetDialog( refreshBtn.setOnClickListener { lifecycleScope.launch { - val itemsThatHaveFormats = items.filter { it!!.allFormats.isNotEmpty() } - val itemsWithMissingFormats = items.filter { it!!.allFormats.isEmpty() }.ifEmpty { items } + val distinctItems = items.distinctBy { it!!.url } + + val itemsThatHaveFormats = distinctItems.filter { it!!.allFormats.isNotEmpty() } + val itemsWithMissingFormats = distinctItems.filter { it!!.allFormats.isEmpty() }.ifEmpty { distinctItems } if (itemsWithMissingFormats.size > 10){ continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG) @@ -238,11 +240,7 @@ class FormatSelectionBottomSheetDialog( } }.onFailure { err -> withContext(Dispatchers.Main){ - UiUtil.handleResultResponse(requireActivity(), ResultViewModel.ResultsUiState( - false, - Pair(R.string.no_results, err.message.toString()), - mutableListOf(Pair(R.string.copy_log, ResultViewModel.ResultAction.COPY_LOG)) - ), closed = {}) + UiUtil.handleNoResults(requireActivity(), err.message.toString(), false, continued = {}, closed = {}) } } @@ -270,9 +268,9 @@ class FormatSelectionBottomSheetDialog( } }else{ multipleFormatsListener.onFormatUpdated(it.url, it.formats) - items.firstOrNull { item -> item!!.url == it.url }?.apply { - allFormats.clear() - allFormats.addAll(it.formats) + items.filter { item -> item!!.url == it.url }.forEach { d -> + d?.allFormats?.clear() + d?.allFormats?.addAll(it.formats) } progressInt++ lifecycleScope.launch(Dispatchers.Main) { @@ -290,7 +288,7 @@ class FormatSelectionBottomSheetDialog( val flatFormatCollection = formatCollection.flatten() val commonFormats = flatFormatCollection.groupingBy { it.format_id }.eachCount() - .filter { it.value == items.size } + .filter { it.value == distinctItems.size } .mapValues { flatFormatCollection.first { f -> f.format_id == it.key } } .map { it.value } formats.addAll(commonFormats) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt index 4dab5c32..904508a5 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ResultCardDetailsDialog.kt @@ -53,6 +53,7 @@ import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.VideoPlayerUtil +import com.deniscerri.ytdl.work.DownloadWorker import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment @@ -72,6 +73,9 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -86,8 +90,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap private lateinit var activeAdapter: ActiveDownloadMinifiedAdapter private lateinit var queuedAdapter: GenericDownloadAdapter - private var activeCount: Int = 0 - + private lateinit var downloadManager: DownloadManager private lateinit var sharedPreferences: SharedPreferences @@ -97,6 +100,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) infoUtil = InfoUtil(requireActivity()) + notificationUtil = NotificationUtil(requireActivity()) downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager @@ -237,12 +241,6 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap } } - lifecycleScope.launch { - downloadViewModel.activeDownloadsCount.collectLatest { - activeCount = it - } - } - bottomSheetLink.text = item.url bottomSheetLink.setOnClickListener{ @@ -482,21 +480,42 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap override fun onCancelClick(itemID: Long) { lifecycleScope.launch { - if (activeCount == 1){ - val queue = withContext(Dispatchers.IO){ - val list = downloadViewModel.getQueued().toMutableList() - list.map { it.status = DownloadRepository.Status.Queued.toString() } - list - } + YoutubeDL.getInstance().destroyProcessById(itemID.toString()) + notificationUtil.cancelDownloadNotification(itemID.toInt()) - runBlocking { - downloadViewModel.queueDownloads(queue) + val item = withContext(Dispatchers.IO){ + downloadViewModel.getItemByID(itemID) + } + item.status = DownloadRepository.Status.Cancelled.toString() + withContext(Dispatchers.IO){ + downloadViewModel.updateDownload(item) + } + + val activeCount = withContext(Dispatchers.IO){ + downloadViewModel.getActiveDownloadsCount() + } + + if (activeCount == 0){ + val queuedCount = withContext(Dispatchers.IO){ + downloadViewModel.getQueuedDownloadsCount() + } + if (queuedCount == 0) { + sharedPreferences.edit().putBoolean("paused_downloads", false).apply() } } - } + if (activeCount == 1){ + val queue = withContext(Dispatchers.IO){ + downloadViewModel.getQueued() + } - cancelActiveDownload(itemID) + if (!sharedPreferences.getBoolean("paused_downloads", false)){ + runBlocking { + downloadViewModel.queueDownloads(queue) + } + } + } + } } override fun onCardClick() { this.dismiss() @@ -505,22 +524,29 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap ) } - private fun cancelActiveDownload(itemID: Long){ - lifecycleScope.launch { - val id = itemID.toInt() - YoutubeDL.getInstance().destroyProcessById(id.toString()) - notificationUtil.cancelDownloadNotification(id) + //dont remove + @Subscribe(threadMode = ThreadMode.MAIN) + fun onDownloadProgressEvent(event: DownloadWorker.WorkerProgress) { + val progressBar = requireView().findViewWithTag("${event.downloadItemID}##progress") + val outputText = requireView().findViewWithTag("${event.downloadItemID}##output") - withContext(Dispatchers.IO){ - downloadViewModel.getItemByID(itemID) - }.let { - it.status = DownloadRepository.Status.Cancelled.toString() - lifecycleScope.launch(Dispatchers.IO){ - downloadViewModel.updateDownload(it) - } - } + requireActivity().runOnUiThread { + try { + progressBar?.setProgressCompat(event.progress, true) + outputText?.text = event.output + }catch (ignored: Exception) {} } + } + + override fun onStart() { + super.onStart() + EventBus.getDefault().register(this) + } + + override fun onStop() { + super.onStop() + EventBus.getDefault().unregister(this) } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt index 229be4d9..daa0cd48 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogFragment.kt @@ -48,6 +48,9 @@ class DownloadLogFragment : Fragment() { private lateinit var logViewModel: LogViewModel private lateinit var sharedPreferences: SharedPreferences + private var autoScroll : Boolean = true + private var scrollDownBtn : MenuItem? = null + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -113,6 +116,7 @@ class DownloadLogFragment : Fragment() { contentScrollView.enableFastScroll() + scrollDownBtn = bottomAppBar?.menu?.children?.first { it.itemId == R.id.scroll_down } val slider = view.findViewById(R.id.textsize_seekbar) bottomAppBar?.setOnMenuItemClickListener { m: MenuItem -> @@ -124,16 +128,17 @@ class DownloadLogFragment : Fragment() { scrollView.removeAllViews() parent.removeView(scrollView) parent.addView(content, 0) - contentScrollView.setPadding(0,0,0, - (requireContext().resources.displayMetrics.density * 150).toInt() - ) +// contentScrollView.setPadding(0,0,0, +// (requireContext().resources.displayMetrics.density * 150).toInt() +// ) + updateAutoScrollState() }else{ val parent = content.parent as ViewGroup parent.removeView(content) scrollView = HorizontalScrollView(requireContext()) - scrollView.setPadding(0,0,0, - (requireContext().resources.displayMetrics.density * 150).toInt() - ) +// scrollView.setPadding(0,0,0, +// (requireContext().resources.displayMetrics.density * 150).toInt() +// ) contentScrollView.setPadding(0,0,0,0) scrollView.layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -142,11 +147,13 @@ class DownloadLogFragment : Fragment() { scrollView.addView(content) scrollView.id = R.id.horizontalscroll_output parent.addView(scrollView, 0) + updateAutoScrollState() } } R.id.scroll_down -> { m.isVisible = false + autoScroll = true contentScrollView.fullScroll(View.FOCUS_DOWN) } @@ -171,9 +178,12 @@ class DownloadLogFragment : Fragment() { } contentScrollView.setOnScrollChangeListener { view, sx, sy, osx, osy -> - if (sy < osy){ - bottomAppBar?.menu?.get(1)?.isVisible = contentScrollView.canScrollVertically(1) - } + updateAutoScrollState() + } + + contentScrollView.setOnTouchListener { view, motionEvent -> + autoScroll = false + false } @@ -185,9 +195,11 @@ class DownloadLogFragment : Fragment() { content.setText(logItem.content, TextView.BufferType.SPANNABLE) bottomAppBar?.menu?.get(1)?.isVisible = contentScrollView.canScrollVertically(1) } - if (!bottomAppBar.menu.children.first { it.itemId == R.id.scroll_down }.isVisible){ - content.scrollTo(0, content.height) - contentScrollView.fullScroll(View.FOCUS_DOWN) + if (autoScroll){ + //content.scrollTo(0, content.height) + content.post { + contentScrollView.fullScroll(View.FOCUS_DOWN) + } } } } @@ -195,6 +207,11 @@ class DownloadLogFragment : Fragment() { } } + private fun updateAutoScrollState() { + val canVerticallyScroll = contentScrollView.canScrollVertically(1) + scrollDownBtn?.isVisible = canVerticallyScroll + } + companion object { private const val TAG = "DownloadLogActivity" } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt index 30dd34bd..ce7b172c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt @@ -81,8 +81,7 @@ class FolderSettingsFragment : BaseSettingsFragment() { editor.putString("command_path", FileUtil.getDefaultCommandPath()) } - if((VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) || - VERSION.SDK_INT < 30) { + if (FileUtil.hasAllFilesAccess()) { accessAllFiles!!.isVisible = false cacheDownloads!!.isEnabled = true }else{ @@ -90,8 +89,6 @@ class FolderSettingsFragment : BaseSettingsFragment() { cacheDownloads!!.isEnabled = false } - editor.apply() - musicPath!!.summary = FileUtil.formatPath(preferences.getString("music_path", "")!!) musicPath!!.onPreferenceClickListener = Preference.OnPreferenceClickListener { diff --git a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt index 2d655873..2ed10798 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt @@ -261,20 +261,27 @@ object Extensions { } } -// fun String.appendLineToLog(line: String): String { -// val lines = this.split("\n") -// if (!lines.takeLast(3).contains(line)){ -// //clean dublicate progress + add newline -// var newLine = line -// if (newLine.contains("[download")) { -// newLine = "[download]" + line.split("[download]").last() -// } -// -// return lines.dropLastWhile { it.contains("[download") }.joinToString("\n") + "\n${newLine}" -// } -// -// return this -// } + fun String.appendLineToLog(line: String = ""): String { + val lines = this.lines().toMutableList() + val finishingProgressLinesRegex = Pattern.compile("\\[download]\\h+(100%|[a-zA-Z])") + + if (line.isNotBlank()) { + var newLine = "" + val newLines = line.lines() + if (!lines.takeLast(3).any { newLines.contains(it) }) { + lines.addAll(newLines.dropLast(1)) + newLine = "\n${newLines.last()}" + } + + return lines.dropLastWhile { + it.contains("[download") && !finishingProgressLinesRegex.matcher(it).find() + }.joinToString("\n") + newLine + } + + return lines.filterNot { + it.contains("[download") && !finishingProgressLinesRegex.matcher(it).find() + }.joinToString("\n") + } fun ImageView.loadThumbnail(hideThumb: Boolean, imageURL: String){ if(!hideThumb){ diff --git a/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt index 2d56345c..14fe4710 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt @@ -400,4 +400,9 @@ object FileUtil { } } + fun hasAllFilesAccess() : Boolean { + if (Build.VERSION.SDK_INT < 30) return true + return Environment.isExternalStorageManager() + } + } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt index 780cdd58..97969898 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt @@ -4,7 +4,6 @@ import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.content.res.Resources -import android.content.res.Resources.NotFoundException import android.os.Handler import android.os.Looper import android.text.Html @@ -43,7 +42,6 @@ import java.lang.reflect.Type import java.net.HttpURLConnection import java.net.URL import java.util.* -import java.util.regex.Pattern class InfoUtil(private val context: Context) { @@ -168,6 +166,13 @@ class InfoUtil(private val context: Context) { fun getYoutubeVideo(url: String): List? { val theURL = url.replace("\\?list.*".toRegex(), "") try{ + //TODO NEWPIPE TEST +// val extractor = ServiceList.YouTube.getStreamExtractor(url) +// extractor.fetchPage() +// val extractor2 = ServiceList.YouTube.getPlaylistExtractor(url) +// extractor2.fetchPage() +// var info = extractor2.getPage(extractor2.initialPage.nextPage) + val id = getIDFromYoutubeURL(theURL) val res = genericRequest("$pipedURL/streams/$id") if (res.length() == 0) { @@ -678,7 +683,7 @@ class InfoUtil(private val context: Context) { } val lang = sharedPreferences.getString("app_language", "en") if (searchEngine == "ytsearch" || query.isYoutubeURL()) { - var extractorArgs = "player_client=default,mediaconnect,android" + var extractorArgs = "player_client=default,mediaconnect" if (context.getStringArray(R.array.subtitle_langs).contains(lang)) { extractorArgs += ";lang=$lang" } @@ -1034,6 +1039,12 @@ class InfoUtil(private val context: Context) { } } + private fun MutableList.addOption(vararg options: String) { + options.forEach { + this.add(it) + } + } + @SuppressLint("RestrictedApi") fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest { @@ -1052,14 +1063,16 @@ class InfoUtil(private val context: Context) { } } + val metadataCommands = mutableListOf() + if (downloadItem.playlistIndex != null && useItemURL) { - request.addOption("--parse-metadata", " ${downloadItem.playlistIndex}: %(playlist_index)s") + metadataCommands.addOption("--parse-metadata", " ${downloadItem.playlistIndex}: %(playlist_index)s") } val type = downloadItem.type val downDir : File - val canWrite = File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite() || sharedPreferences.getBoolean("access_all_files", false) + val canWrite = File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite() if (!sharedPreferences.getBoolean("cache_downloads", true) && canWrite){ downDir = File(FileUtil.formatPath(downloadItem.downloadPath)) request.addOption("--no-quiet") @@ -1162,18 +1175,14 @@ class InfoUtil(private val context: Context) { } if(downloadItem.title.isNotBlank()){ - request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.take(180))) + metadataCommands.addOption("--replace-in-metadata", "title", ".+", downloadItem.title.take(180)) + metadataCommands.addOption("--parse-metadata", "%(title)s:%(meta_title)s") } if (downloadItem.author.isNotBlank()){ - request.addCommands(listOf("--replace-in-metadata", "video:uploader", ".+", downloadItem.author.take(30))) - } - - request.addOption("--parse-metadata", "uploader:^(?P.*?)(?:(?= - Topic)|$)") - - if (embedMetadata){ - request.addOption("--parse-metadata", "%(uploader,channel,creator,artist|null)s:%(uploader)s") + metadataCommands.addOption("--replace-in-metadata", "uploader", ".+", downloadItem.author.take(30)) + metadataCommands.addOption("--parse-metadata", "%(uploader)s:%(artist)s") } if (downloadItem.downloadSections.isNotBlank()){ @@ -1196,7 +1205,7 @@ class InfoUtil(private val context: Context) { } if (sharedPreferences.getBoolean("use_audio_quality", false)){ - request.addOption("--audio-quality", sharedPreferences.getInt("audio_quality", 0)) + request.addOption("--audio-quality", sharedPreferences.getInt("audio_quality", 0).toString()) } if (sharedPreferences.getBoolean("write_description", false)){ @@ -1204,7 +1213,7 @@ class InfoUtil(private val context: Context) { } if (downloadItem.url.isYoutubeURL()) { - var extractorArgs = "player_client=default,mediaconnect,android" + var extractorArgs = "player_client=default,mediaconnect" val lang = sharedPreferences.getString("app_language", "en") if (context.getStringArray(R.array.subtitle_langs).contains(lang)) { extractorArgs += ";lang=$lang" @@ -1261,7 +1270,7 @@ class InfoUtil(private val context: Context) { } request.addOption("-x") - val formatSorting = StringBuilder("hasaud") + val formatSorting = StringBuilder("hasaud,size") if (aCodecPref.isNotBlank()){ formatSorting.append(",acodec:$aCodecPref") @@ -1285,31 +1294,46 @@ class InfoUtil(private val context: Context) { request.addOption("-P", downDir.absolutePath) request.addOption("-S", formatSorting.toString()) + metadataCommands.addOption("--parse-metadata", """%(uploader,artist,channel,creator|null)s:^(?P.*?)(?:(?= - Topic)|$)""") + if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){ request.addOption("--split-chapters") request.addOption("-o", "chapter:%(section_title)s.%(ext)s") }else{ if (embedMetadata){ - request.addOption("--embed-metadata") - request.addOption("--parse-metadata", "%(artist,uploader|)s:^(?P[^,]*)") - request.addOption("--parse-metadata", "%(album_artist,meta_album_artist|)s:%(album_artist)s") + metadataCommands.addOption("--embed-metadata") - request.addOption("--parse-metadata", "description:(?:Released on: )(?P\\d{4})") - request.addOption("--parse-metadata", "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:(?P\\d+)") + val emptyAuthor = downloadItem.author.isEmpty() + val usePlaylistMetadata = sharedPreferences.getBoolean("playlist_as_album", true) - if (downloadItem.playlistTitle.isNotEmpty()) { - request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s") - request.addOption("--parse-metadata", "%(track_number,playlist_index)d:(?P\\d+)") - } else { - request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s") + if (emptyAuthor) { + if (usePlaylistMetadata) { + metadataCommands.addOption("--parse-metadata", "%(playlist_uploader,artist,uploader|)s:^(?P.*?)(?:(?=,\\s+)|$)") + }else{ + metadataCommands.addOption("--parse-metadata", "%(artist,uploader|)s:^(?P.*?)(?:(?=,\\s+)|$)") + } + }else{ + if (usePlaylistMetadata) { + metadataCommands.addOption("--parse-metadata", "%(playlist_uploader,artist|)s:^(?P.*?)(?:(?=,\\s+)|$)") + }else{ + metadataCommands.addOption("--parse-metadata", "%(artist|)s:^(?P.*?)(?:(?=,\\s+)|$)") + } } + + + metadataCommands.addOption("--parse-metadata", "description:(?:Released on: )(?P\\d{4})") + metadataCommands.addOption("--parse-metadata", "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:(?P\\d+)") + metadataCommands.addOption("--parse-metadata", "%(album_artist,first_artist|)s:%(album_artist)s") + metadataCommands.addOption("--parse-metadata", "%(track_number,playlist_index)d:(?P\\d+)") + + } val cropThumb = downloadItem.audioPreferences.cropThumb ?: sharedPreferences.getBoolean("crop_thumbnail", true) if (downloadItem.audioPreferences.embedThumb){ - request.addOption("--embed-thumbnail") - if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!) + metadataCommands.addOption("--embed-thumbnail") + if (!request.hasOption("--convert-thumbnails")) metadataCommands.addOption("--convert-thumbnails", thumbnailFormat!!) val thumbnailConfig = StringBuilder("") val cropConfig = """-vf crop=\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\""""" @@ -1326,7 +1350,7 @@ class InfoUtil(private val context: Context) { runCatching { val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.id + "##ffmpegCrop.txt") config.writeText(thumbnailConfig.toString()) - request.addOption("--config", config.absolutePath) + metadataCommands.addOption("--config", config.absolutePath) } } @@ -1339,6 +1363,8 @@ class InfoUtil(private val context: Context) { } DownloadViewModel.Type.video -> { + metadataCommands.addOption("--parse-metadata", """%(uploader,channel,creator,artist|null)s:^(?P.*?)(?:(?= - Topic)|$)""") + val supportedContainers = context.resources.getStringArray(R.array.video_containers) if (downloadItem.videoPreferences.addChapters) { @@ -1359,7 +1385,8 @@ class InfoUtil(private val context: Context) { ){ cont = outputContainer - if (downloadItem.videoPreferences.recodeVideo) { + val cantRecode = listOf("avi") + if (downloadItem.videoPreferences.recodeVideo && !cantRecode.contains(cont)) { request.addOption("--recode-video", outputContainer.lowercase()) }else{ request.addOption("--merge-output-format", outputContainer.lowercase()) @@ -1368,7 +1395,7 @@ class InfoUtil(private val context: Context) { if (!listOf("webm", "avi", "flv").contains(outputContainer.lowercase())) { val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) if (embedThumb) { - request.addOption("--embed-thumbnail") + metadataCommands.addOption("--embed-thumbnail") if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!) } } @@ -1382,7 +1409,6 @@ class InfoUtil(private val context: Context) { var audioF = downloadItem.videoPreferences.audioFormatIDs.map { f -> val format = downloadItem.allFormats.find { it.format_id == f } format?.run { - println(format_id) if (this.format_id.matches(".*-[0-9]+".toRegex())) { if (!this.lang.isNullOrBlank() && this.lang != "None") { "ba[format_id~='^(${this.format_id.split("-")[0]})'][language^=${this.lang}]" @@ -1591,7 +1617,9 @@ class InfoUtil(private val context: Context) { ) } - + if (metadataCommands.isNotEmpty()){ + request.addCommands(metadataCommands) + } return request } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/NavbarUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/NavbarUtil.kt index 5010f2ac..8e2d1088 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/NavbarUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/NavbarUtil.kt @@ -35,6 +35,7 @@ object NavbarUtil { val pref = settings.getString("start_destination", "")!! val items = getDefaultNavBarItems(context) val navBarHasPref = getNavBarPrefs().contains(items.indexOfFirst { it.itemId == navItems[pref] }.toString()) + println("START FRAGMENT: $pref") return if (pref == "") { R.id.homeFragment }else { diff --git a/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt index ada4893b..232adddc 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt @@ -20,6 +20,7 @@ import androidx.core.content.FileProvider import androidx.core.os.bundleOf import androidx.documentfile.provider.DocumentFile import androidx.navigation.NavDeepLinkBuilder +import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.receiver.CancelDownloadNotificationReceiver @@ -148,11 +149,9 @@ class NotificationUtil(var context: Context) { fun createDownloadServiceNotification( pendingIntent: PendingIntent?, title: String?, - itemID: Int, ): Notification { val notificationBuilder = getBuilder(DOWNLOAD_SERVICE_CHANNEL_ID) - return notificationBuilder .setContentTitle(title) .setOngoing(true) diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index 05ab1c08..61d90220 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -103,6 +103,7 @@ import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale import java.util.Queue +import java.util.function.Predicate object UiUtil { @@ -862,14 +863,16 @@ object UiUtil { true } - if (format.format_id.isBlank()) formatIdParent?.visibility = View.GONE + fun String?.doesntExist() : Boolean { return this.isNullOrBlank() || this == "None"} + + if (format.format_id.doesntExist()) formatIdParent?.visibility = View.GONE else { formatIdParent?.findViewById(R.id.format_id_value)?.text = format.format_id formatIdParent?.setOnClickListener(clicker) formatIdParent?.setOnLongClickListener(longClicker) } - if (format.url.isNullOrBlank()) formatURLParent?.visibility = View.GONE + if (format.url.doesntExist()) formatURLParent?.visibility = View.GONE else { formatURLParent?.findViewById(R.id.format_url_value)?.text = format.url formatURLParent?.setOnClickListener(clicker) @@ -877,7 +880,7 @@ object UiUtil { } - if (format.container.isBlank()) containerParent?.visibility = View.GONE + if (format.container.doesntExist()) containerParent?.visibility = View.GONE else { containerParent?.findViewById(R.id.container_value)?.text = format.container containerParent?.setOnClickListener(clicker) @@ -893,7 +896,7 @@ object UiUtil { format.acodec.uppercase() } - if (codecField.isBlank()) codecParent?.visibility = View.GONE + if (codecField.doesntExist()) codecParent?.visibility = View.GONE else { codecParent?.findViewById(R.id.codec_value)?.text = codecField codecParent?.setOnClickListener(clicker) @@ -907,28 +910,28 @@ object UiUtil { filesizeParent?.setOnLongClickListener(longClicker) } - if (format.format_note.isBlank()) formatnoteParent?.visibility = View.GONE + if (format.format_note.doesntExist()) formatnoteParent?.visibility = View.GONE else { formatnoteParent?.findViewById(R.id.format_note_value)?.text = format.format_note formatnoteParent?.setOnClickListener(clicker) formatnoteParent?.setOnLongClickListener(longClicker) } - if (format.fps.isNullOrBlank() || format.fps == "0") fpsParent?.visibility = View.GONE + if (format.fps.doesntExist() || format.fps == "0") fpsParent?.visibility = View.GONE else { fpsParent?.findViewById(R.id.fps_value)?.text = format.fps fpsParent?.setOnClickListener(clicker) fpsParent?.setOnLongClickListener(longClicker) } - if (format.asr.isNullOrBlank()) asrParent?.visibility = View.GONE + if (format.asr.doesntExist()) asrParent?.visibility = View.GONE else { asrParent?.findViewById(R.id.asr_value)?.text = format.asr asrParent?.setOnClickListener(clicker) asrParent?.setOnLongClickListener(longClicker) } - if (format.tbr.isNullOrBlank()) bitrateParent?.visibility = View.GONE + if (format.tbr.doesntExist()) bitrateParent?.visibility = View.GONE else{ bitrateParent?.findViewById(R.id.bitrate_value)?.text = format.tbr bitrateParent?.setOnClickListener(clicker) @@ -990,7 +993,21 @@ object UiUtil { val chips = mutableListOf() context.getStringArray(R.array.subtitle_langs).forEachIndexed { index, s -> val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip - tmp.text = Locale(s).displayLanguage + s.split("-", ".*").run { + if (s.endsWith(".*")) { + val name = "${Locale(this[0]).displayName} (${context.getString(R.string.all)})" + tmp.text = name + }else{ + if (this.size == 1) { + Locale(this[0]) + } else { + Locale(this[0], this[1]) + }.apply { + tmp.text = this.displayName + } + } + } + tmp.tag = s tmp.id = index @@ -1564,24 +1581,20 @@ object UiUtil { } - - fun handleResultResponse(context: Activity, it: ResultViewModel.ResultsUiState, closed: () -> Unit){ - val title = context.getString(it.errorMessage!!.first) - val message = it.errorMessage!!.second - + fun handleNoResults(context: Activity, message: String, continueAnyway: Boolean = false, continued: () -> Unit, closed: () -> Unit) { val errDialog = MaterialAlertDialogBuilder(context) - .setTitle(title) + .setTitle(R.string.no_results) .setMessage(message) - for (a in it.actions!!){ - when(a.second){ - ResultViewModel.ResultAction.COPY_LOG -> { - errDialog.setPositiveButton(a.first) { d:DialogInterface?, _:Int -> - val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.setText(message) - d?.dismiss() - } - } + errDialog.setPositiveButton(R.string.copy_log) { d:DialogInterface?, _:Int -> + val clipboard: ClipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setText(message) + d?.dismiss() + } + + if (continueAnyway) { + errDialog.setNeutralButton(R.string.continue_anyway) {d: DialogInterface?, _:Int -> + continued() } } diff --git a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt index d285036d..2c18f12b 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt @@ -1,9 +1,12 @@ package com.deniscerri.ytdl.work import android.annotation.SuppressLint +import android.app.PendingIntent import android.content.Context +import android.content.Intent import android.content.res.Configuration import android.content.res.Resources +import android.os.Build import android.os.Handler import android.os.Looper import android.util.DisplayMetrics @@ -20,6 +23,7 @@ import androidx.work.await import androidx.work.workDataOf import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.deniscerri.ytdl.App +import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.models.HistoryItem @@ -73,15 +77,28 @@ class DownloadWorker( //val createResultItem = inputData.getBoolean("createResultItem", true) val confTmp = Configuration(context.resources.configuration) - val currLang = sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language }.split("-") - confTmp.setLocale(if (currLang.size == 1) Locale(currLang[0]) else Locale(currLang[0], currLang[1])) + val locale = if (Build.VERSION.SDK_INT < 33) { + sharedPreferences.getString("app_language", "")!!.ifEmpty { Locale.getDefault().language } + }else{ + Locale.getDefault().language + }.run { + split("-") + }.run { + if (this.size == 1) Locale(this[0]) else Locale(this[0], this[1]) + } + confTmp.setLocale(locale) val metrics = DisplayMetrics() val resources = Resources(context.assets, metrics, confTmp) - val pendingIntent = NavDeepLinkBuilder(context) - .setGraph(R.navigation.nav_graph) - .setDestination(R.id.downloadQueueMainFragment) - .createPendingIntent() + val openQueueIntent = Intent(context, MainActivity::class.java) + openQueueIntent.setAction(Intent.ACTION_VIEW) + openQueueIntent.putExtra("destination", "Queue") + val openDownloadQueue = PendingIntent.getActivity( + context, + 1000000000, + openQueueIntent, + PendingIntent.FLAG_IMMUTABLE + ) val workNotif = notificationUtil.createDefaultWorkerNotification() val foregroundInfo = ForegroundInfo(1000000000, workNotif) @@ -112,7 +129,7 @@ class DownloadWorker( val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running } eligibleDownloads.forEach{downloadItem -> - val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, downloadItem.title.ifEmpty { downloadItem.url }, downloadItem.id.toInt()) + val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url }) notificationUtil.notify(downloadItem.id.toInt(), notification) CoroutineScope(Dispatchers.IO).launch { @@ -189,7 +206,7 @@ class DownloadWorker( } //val wasQuickDownloaded = resultDao.getCountInt() == 0 runBlocking { - var finalPaths : MutableList? + var finalPaths = mutableListOf() if (noCache){ eventBus.post(WorkerProgress(100, "Scanning Files", downloadItem.id)) @@ -212,9 +229,6 @@ class DownloadWorker( finalPaths.sortBy { File(it).lastModified() } finalPaths = finalPaths.distinct().toMutableList() FileUtil.scanMedia(finalPaths, context) - if (finalPaths.isEmpty()){ - finalPaths = mutableListOf(context.getString(R.string.unfound_file)) - } }else{ //move file from internal to set download directory eventBus.post(WorkerProgress(100, "Moving file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id)) @@ -227,11 +241,8 @@ class DownloadWorker( if (finalPaths.isNotEmpty()){ eventBus.post(WorkerProgress(100, "Moved file to ${FileUtil.formatPath(downloadLocation)}", downloadItem.id)) - }else{ - finalPaths = mutableListOf(context.getString(R.string.unfound_file)) } }catch (e: Exception){ - finalPaths = mutableListOf(context.getString(R.string.unfound_file)) e.printStackTrace() handler.postDelayed({ Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() @@ -246,51 +257,52 @@ class DownloadWorker( add("description") add("txt") } - finalPaths = finalPaths?.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }?.toMutableList() + finalPaths = finalPaths.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }.toMutableList() + if (finalPaths.isEmpty()){ + finalPaths = mutableListOf(context.getString(R.string.unfound_file)) + } FileUtil.deleteConfigFiles(request) //put download in history if (!downloadItem.incognito) { - if (request.hasOption("--download-archive") && finalPaths == listOf(context.getString(R.string.unfound_file))) { + if (request.hasOption("--download-archive") && finalPaths.isEmpty()) { handler.postDelayed({ Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show() }, 100) }else{ val unixTime = System.currentTimeMillis() / 1000 - finalPaths?.apply { - this.first().apply { - val file = File(this) - var duration = downloadItem.duration - val d = file.getMediaDuration(context) - if (d > 0) duration = d.toStringDuration(Locale.US) + finalPaths.first().apply { + val file = File(this) + var duration = downloadItem.duration + val d = file.getMediaDuration(context) + if (d > 0) duration = d.toStringDuration(Locale.US) - downloadItem.format.filesize = file.length() - downloadItem.format.container = file.extension - downloadItem.duration = duration - } - - val historyItem = HistoryItem(0, - downloadItem.url, - downloadItem.title, - downloadItem.author, - downloadItem.duration, - downloadItem.thumb, - downloadItem.type, - unixTime, - this, - downloadItem.website, - downloadItem.format, - downloadItem.id, - commandString) - historyDao.insert(historyItem) + downloadItem.format.filesize = file.length() + downloadItem.format.container = file.extension + downloadItem.duration = duration } + val historyItem = HistoryItem(0, + downloadItem.url, + downloadItem.title, + downloadItem.author, + downloadItem.duration, + downloadItem.thumb, + downloadItem.type, + unixTime, + finalPaths, + downloadItem.website, + downloadItem.format, + downloadItem.id, + commandString) + historyDao.insert(historyItem) + } } notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) notificationUtil.createDownloadFinished( - downloadItem.id, downloadItem.title, downloadItem.type, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths, resources + downloadItem.id, downloadItem.title, downloadItem.type, if (finalPaths.first().equals(context.getString(R.string.unfound_file))) null else finalPaths, resources ) // if (wasQuickDownloaded && createResultItem){ diff --git a/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt index d90a273b..31d9ed83 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/TerminalDownloadWorker.kt @@ -49,7 +49,7 @@ class TerminalDownloadWorker( val intent = Intent(context, TerminalActivity::class.java) val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) - val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(65), itemId) + val notification = notificationUtil.createDownloadServiceNotification(pendingIntent, command.take(65)) val foregroundInfo = ForegroundInfo(itemId, notification) setForegroundAsync(foregroundInfo) diff --git a/app/src/main/res/drawable/baseline_album_24.xml b/app/src/main/res/drawable/baseline_album_24.xml new file mode 100644 index 00000000..30fded47 --- /dev/null +++ b/app/src/main/res/drawable/baseline_album_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/rounded_corner.xml b/app/src/main/res/drawable/rounded_corner.xml index c9f2a687..01227d24 100644 --- a/app/src/main/res/drawable/rounded_corner.xml +++ b/app/src/main/res/drawable/rounded_corner.xml @@ -8,5 +8,5 @@ android:bottom="1dp" android:top="1dp" /> - + \ No newline at end of file diff --git a/app/src/main/res/layout/active_download_card_minified.xml b/app/src/main/res/layout/active_download_card_minified.xml index 361d5da0..9215d993 100644 --- a/app/src/main/res/layout/active_download_card_minified.xml +++ b/app/src/main/res/layout/active_download_card_minified.xml @@ -34,7 +34,7 @@ + android:layout_height="wrap_content"> + app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar2" /> + - - + + diff --git a/app/src/main/res/layout/download_card.xml b/app/src/main/res/layout/download_card.xml index 6fbe9802..9c3d8f43 100644 --- a/app/src/main/res/layout/download_card.xml +++ b/app/src/main/res/layout/download_card.xml @@ -38,7 +38,7 @@ app:layout_constraintHorizontal_weight="0.3" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" /> + app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar2" /> @@ -173,6 +172,7 @@ - + android:layout_height="match_parent"> + android:fitsSystemWindows="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:liftOnScroll="false"> + app:navigationIcon="@drawable/ic_back" /> @@ -34,34 +36,45 @@ + android:id="@+id/frameLayout" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@+id/coordinatorLayout3" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/appbarlayout"> + android:clipToPadding="false"> - + android:id="@+id/horizontalscroll_output" + android:layout_height="wrap_content"> + + + + + + @@ -69,27 +82,41 @@ - - - + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent"> - + + + + - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/queued_download_card.xml b/app/src/main/res/layout/queued_download_card.xml index 0e9cc689..93b0ce35 100644 --- a/app/src/main/res/layout/queued_download_card.xml +++ b/app/src/main/res/layout/queued_download_card.xml @@ -67,7 +67,7 @@ app:layout_constraintHorizontal_weight="0.3" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" - app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" /> + app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar2" /> @@ -113,8 +116,9 @@ style="@style/Widget.Material3.ExtendedFloatingActionButton.Icon.Secondary" android:layout_width="55dp" android:layout_height="55dp" + app:elevation="0dp" android:contentDescription="@string/video" - android:layout_marginHorizontal="10dp" + android:layout_marginHorizontal="5dp" app:icon="@drawable/ic_video" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" /> diff --git a/app/src/main/res/menu/active_downloads_minified.xml b/app/src/main/res/menu/active_downloads_minified.xml index bf150d99..427dedcb 100644 --- a/app/src/main/res/menu/active_downloads_minified.xml +++ b/app/src/main/res/menu/active_downloads_minified.xml @@ -3,19 +3,19 @@ xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity" > - - + + + + + + - - + + + + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 815c602c..532f1b07 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1029,13 +1029,16 @@ en-IN, en-GB, en, + en.*, es, es-419, es-US, + es.*, eu, fil, fr, fr-CA, + fr.*, gl, hr, zu, @@ -1051,6 +1054,7 @@ pl, pt-PT, pt, + pt.*, ro, sq, sk, @@ -1097,6 +1101,7 @@ zh-CN, zh-TW, zh-HK, + zh.*, ja, ko diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c6d70a7..92c7cc34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -418,4 +418,7 @@ Weekly Monthly Clean-up leftover downloads (cancelled, errored) + Continue Anyway + Use Playlist Name as Album Metadata + If Album Metadata is not present, use playlist name instead \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b5b1f7b2..b26dcd49 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -32,6 +32,11 @@ 5dp + +