more stuff

Added crop thumbnail as a preference
Added ability to backup errored downloads
Format cards will show collected filesize when multiple audio formats are chosen
This commit is contained in:
deniscerri 2023-05-26 20:54:34 +02:00
parent 0b487e10e6
commit f259d31b7f
No known key found for this signature in database
GPG key ID: 95C43D517D830350
14 changed files with 117 additions and 38 deletions

View file

@ -37,6 +37,9 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Processing' ORDER BY id DESC")
fun getProcessingDownloads() : Flow<List<DownloadItem>>

View file

@ -60,6 +60,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getCancelledDownloadsList()
}
fun getErroredDownloads() : List<DownloadItem> {
return downloadDao.getErroredDownloadsList()
}
suspend fun deleteCancelled(){
downloadDao.deleteCancelled()
}

View file

@ -3,7 +3,11 @@ package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
import android.net.ConnectivityManager
import android.os.Environment
import android.util.DisplayMetrics
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
@ -37,8 +41,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Locale
import java.util.concurrent.TimeUnit
class DownloadViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository : DownloadRepository
private val sharedPreferences: SharedPreferences
@ -58,6 +65,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
private val videoQualityPreference: String
private val formatIDPreference: String
private val resources : Resources
enum class Type {
audio, video, command
}
@ -80,7 +88,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString()
formatIDPreference = sharedPreferences.getString("format_id", "").toString()
val videoFormat = App.instance.resources.getStringArray(R.array.video_formats)
val confTmp = Configuration(application.resources.configuration)
confTmp.locale = Locale(sharedPreferences.getString("app_language", "en")!!)
val metrics = DisplayMetrics()
resources = Resources(application.assets, metrics, confTmp)
val videoFormat = resources.getStringArray(R.array.video_formats)
var videoContainer = sharedPreferences.getString("video_format", "Default")
if (videoContainer == "Default") videoContainer = App.instance.getString(R.string.defaultValue)
@ -103,13 +117,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
var audioContainer = sharedPreferences.getString("audio_format", "mp3")
if (audioContainer == "Default") audioContainer = App.instance.getString(R.string.defaultValue)
bestAudioFormat = Format(
getApplication<App>().resources.getString(R.string.best_quality),
resources.getString(R.string.best_quality),
audioContainer!!,
"",
"",
"",
0,
getApplication<App>().resources.getString(R.string.best_quality)
resources.getString(R.string.best_quality)
)
}
@ -138,9 +152,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
val downloadPath = when(type){
Type.audio -> sharedPreferences.getString("music_path", getApplication<App>().resources.getString(R.string.music_path))
Type.video -> sharedPreferences.getString("video_path", getApplication<App>().resources.getString(R.string.video_path))
else -> sharedPreferences.getString("command_path", getApplication<App>().resources.getString(R.string.command_path))
Type.audio -> sharedPreferences.getString("music_path", File(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/Audio").absolutePath)
Type.video -> sharedPreferences.getString("video_path", File(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/Video").absolutePath)
else -> sharedPreferences.getString("command_path", File(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/Command").absolutePath)
}
val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
@ -233,9 +247,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
val downloadPath = when(historyItem.type){
Type.audio -> sharedPreferences.getString("music_path", getApplication<App>().resources.getString(R.string.music_path))
Type.video -> sharedPreferences.getString("video_path", getApplication<App>().resources.getString(R.string.video_path))
else -> sharedPreferences.getString("command_path", getApplication<App>().resources.getString(R.string.command_path))
Type.audio -> sharedPreferences.getString("music_path", File(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/Audio").absolutePath)
Type.video -> sharedPreferences.getString("video_path", File(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/Video").absolutePath)
else -> sharedPreferences.getString("command_path", File(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/Command").absolutePath)
}
val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
@ -283,10 +297,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
formats.first { !it.format_note.contains("audio", ignoreCase = true) && it.format_id == formatIDPreference }
}catch (e: Exception){
when (videoQualityPreference) {
getApplication<App>().resources.getString(R.string.worst_quality) -> {
"worst" -> {
theFormats.first {!it.format_note.contains("audio", ignoreCase = true) }
}
getApplication<App>().resources.getString(R.string.best_quality) -> {
"best" -> {
theFormats.last {!it.format_note.contains("audio", ignoreCase = true) }
}
else -> {
@ -323,20 +337,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun getGenericAudioFormats() : MutableList<Format>{
val audioFormats = App.instance.resources.getStringArray(R.array.audio_formats)
val audioFormats = resources.getStringArray(R.array.audio_formats)
val formats = mutableListOf<Format>()
var containerPreference = sharedPreferences.getString("audio_format", "Default")
if (containerPreference == "Default") containerPreference = App.instance.getString(R.string.defaultValue)
if (containerPreference == "Default") containerPreference = resources.getString(R.string.defaultValue)
audioFormats.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) }
return formats
}
fun getGenericVideoFormats() : MutableList<Format>{
val videoFormats = App.instance.resources.getStringArray(R.array.video_formats)
val videoFormats = resources.getStringArray(R.array.video_formats)
val formats = mutableListOf<Format>()
var containerPreference = sharedPreferences.getString("video_format", "Default")
if (containerPreference == "Default") containerPreference = application.getString(R.string.defaultValue)
videoFormats.forEach { formats.add(Format(it, containerPreference!!,application.getString(R.string.defaultValue),"", "",0, it)) }
if (containerPreference == "Default") containerPreference = resources.getString(R.string.defaultValue)
videoFormats.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.defaultValue),"", "",0, it)) }
return formats
}
@ -378,6 +392,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return repository.getCancelledDownloads()
}
fun getErrored() : List<DownloadItem> {
return repository.getErroredDownloads()
}
private fun cloneFormat(item: Format) : Format {
val string = Gson().toJson(item, Format::class.java)
return Gson().fromJson(string, Format::class.java)
@ -407,10 +425,15 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} != null) {
Toast.makeText(context, context.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
}else{
it.id = lastDownloadId
val insert = async {repository.insert(it)}
val id = insert.await()
it.id = id
if (it.id == 0L){
it.id = lastDownloadId
val insert = async {repository.insert(it)}
val id = insert.await()
it.id = id
}else{
repository.update(it)
}
queuedItems.add(it)
}
}

View file

@ -161,7 +161,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
}
}
formats = allFormats.first().toMutableList()
uiUtil.populateFormatCard(formatCard, item.first(), item.drop(1).map { it.format_note })
uiUtil.populateFormatCard(formatCard, item.first(), item.drop(1))
downloadItem.format.container = container.editText?.text.toString()
}
}

View file

@ -155,6 +155,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
"downloads" -> json.add("downloads", backupHistory())
"queued" -> json.add("queued", backupQueuedDownloads() )
"cancelled" -> json.add("cancelled", backupCancelledDownloads() )
"errored" -> json.add("errored", backupErroredDownloads() )
"cookies" -> json.add("cookies", backupCookies() )
"templates" -> json.add("templates", backupCommandTemplates() )
"shortcuts" -> json.add("shortcuts", backupShortcuts() )
@ -273,6 +274,20 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
return JsonArray()
}
private suspend fun backupErroredDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getErrored()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCookies() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
@ -436,6 +451,23 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
}
}
//erorred downloads restore
if(json.has("errored")){
val items = json.getAsJsonArray("errored")
val errored = mutableListOf<DownloadItem>()
items.forEach {
val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java)
item.id = 0L
errored.add(item)
withContext(Dispatchers.IO){
downloadViewModel.insert(item)
}
}
if(errored.isNotEmpty()){
finalMessage.append("${getString(R.string.errored)}: ${errored.count()}\n")
}
}
//cookies restore
if(json.has("cookies")){
val items = json.getAsJsonArray("cookies")

View file

@ -3,8 +3,10 @@ package com.deniscerri.ytdlnis.util
import android.content.Context
import android.content.SharedPreferences
import android.os.Looper
import android.text.Html
import android.util.Log
import android.widget.Toast
import androidx.core.text.HtmlCompat
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.Format
@ -282,12 +284,8 @@ class InfoUtil(private val context: Context) {
var video: ResultItem? = null
try {
val id = obj.getString("videoId")
val title = obj.getString("title").toString()
title.replace("&#39;s", "'")
title.replace("&amp", "&")
val author = obj.getString("author").toString()
author.replace("&#39;s", "'")
author.replace("&amp", "&")
val title = Html.fromHtml(obj.getString("title").toString()).toString()
val author = Html.fromHtml(obj.getString("author").toString()).toString()
if (author.isBlank()) throw Exception()

View file

@ -54,12 +54,12 @@ import java.util.Calendar
class UiUtil(private val fileUtil: FileUtil) {
@SuppressLint("SetTextI18n")
fun populateFormatCard(formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List<String>?){
fun populateFormatCard(formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List<Format>?){
formatCard.findViewById<TextView>(R.id.container).text = chosenFormat.container.uppercase()
if (audioFormats.isNullOrEmpty()){
formatCard.findViewById<TextView>(R.id.format_note).text = chosenFormat.format_note.uppercase()
}else{
val title = "${chosenFormat.format_note.uppercase()} + [${audioFormats.joinToString("/")}]"
val title = "${chosenFormat.format_note.uppercase()} + [${audioFormats.joinToString("/") { it.format_note }}]"
formatCard.findViewById<TextView>(R.id.format_note).text = title
}
formatCard.findViewById<TextView>(R.id.format_id).text = "id: ${chosenFormat.format_id}"
@ -77,7 +77,9 @@ class UiUtil(private val fileUtil: FileUtil) {
formatCard.findViewById<TextView>(R.id.codec).visibility = View.VISIBLE
formatCard.findViewById<TextView>(R.id.codec).text = codec
}
formatCard.findViewById<TextView>(R.id.file_size).text = fileUtil.convertFileSize(chosenFormat.filesize)
var filesize = chosenFormat.filesize
if (!audioFormats.isNullOrEmpty()) filesize += audioFormats.sumOf { it.filesize }
formatCard.findViewById<TextView>(R.id.file_size).text = fileUtil.convertFileSize(filesize)
}

View file

@ -184,19 +184,20 @@ class DownloadWorker(
}
}
request.addOption("--embed-metadata")
if (downloadItem.audioPreferences.embedThumb) {
request.addOption("--embed-thumbnail")
request.addOption("--convert-thumbnails", "jpg")
try {
val config = File(context.cacheDir.absolutePath + "/downloads/${downloadItem.id}/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt")
val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
config.writeText(configData)
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
request.addOption("--config", config.absolutePath)
} catch (ignored: Exception) {}
if (sharedPreferences.getBoolean("crop_thumbnail", true)){
try {
val config = File(context.cacheDir.absolutePath + "/downloads/${downloadItem.id}/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt")
val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
config.writeText(configData)
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
request.addOption("--config", config.absolutePath)
} catch (ignored: Exception) {}
}
}
request.addOption("--parse-metadata", "%(release_year,upload_date)s:%(meta_date)s")

View file

@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<TextView

View file

@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<TextView

View file

@ -4,6 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity">
<TextView

View file

@ -702,6 +702,7 @@
<item>@string/downloads</item>
<item>@string/in_queue</item>
<item>@string/cancelled</item>
<item>@string/errored</item>
<item>@string/cookies</item>
<item>@string/command_templates</item>
<item>@string/shortcuts</item>
@ -713,6 +714,7 @@
<item>downloads</item>
<item>queued</item>
<item>cancelled</item>
<item>errored</item>
<item>cookies</item>
<item>templates</item>
<item>shortcuts</item>

View file

@ -269,4 +269,6 @@
<string name="general">General</string>
<string name="swipe_gestures">Swipe Gestures</string>
<string name="swipe_gestures_summary">Swipe items for certain actions</string>
<string name="crop_thumb">Crop Thumbnail</string>
<string name="crop_thumb_summary">Crop the thumbnail into a square for audio downloads</string>
</resources>

View file

@ -74,6 +74,15 @@
app:summary="@string/embed_thumb_summary"
app:title="@string/embed_thumb" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
android:dependency="embed_thumbnail"
app:icon="@drawable/ic_cut"
app:key="crop_thumbnail"
app:summary="@string/crop_thumb_summary"
app:title="@string/crop_thumb" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"