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") @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : Flow<List<DownloadItem>> 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") @Query("SELECT * FROM downloads WHERE status='Processing' ORDER BY id DESC")
fun getProcessingDownloads() : Flow<List<DownloadItem>> fun getProcessingDownloads() : Flow<List<DownloadItem>>

View file

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

View file

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

View file

@ -161,7 +161,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
} }
} }
formats = allFormats.first().toMutableList() 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() downloadItem.format.container = container.editText?.text.toString()
} }
} }

View file

@ -155,6 +155,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
"downloads" -> json.add("downloads", backupHistory()) "downloads" -> json.add("downloads", backupHistory())
"queued" -> json.add("queued", backupQueuedDownloads() ) "queued" -> json.add("queued", backupQueuedDownloads() )
"cancelled" -> json.add("cancelled", backupCancelledDownloads() ) "cancelled" -> json.add("cancelled", backupCancelledDownloads() )
"errored" -> json.add("errored", backupErroredDownloads() )
"cookies" -> json.add("cookies", backupCookies() ) "cookies" -> json.add("cookies", backupCookies() )
"templates" -> json.add("templates", backupCommandTemplates() ) "templates" -> json.add("templates", backupCommandTemplates() )
"shortcuts" -> json.add("shortcuts", backupShortcuts() ) "shortcuts" -> json.add("shortcuts", backupShortcuts() )
@ -273,6 +274,20 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
return JsonArray() 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 { private suspend fun backupCookies() : JsonArray {
runCatching { runCatching {
val items = withContext(Dispatchers.IO) { 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 //cookies restore
if(json.has("cookies")){ if(json.has("cookies")){
val items = json.getAsJsonArray("cookies") val items = json.getAsJsonArray("cookies")

View file

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

View file

@ -54,12 +54,12 @@ import java.util.Calendar
class UiUtil(private val fileUtil: FileUtil) { class UiUtil(private val fileUtil: FileUtil) {
@SuppressLint("SetTextI18n") @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() formatCard.findViewById<TextView>(R.id.container).text = chosenFormat.container.uppercase()
if (audioFormats.isNullOrEmpty()){ if (audioFormats.isNullOrEmpty()){
formatCard.findViewById<TextView>(R.id.format_note).text = chosenFormat.format_note.uppercase() formatCard.findViewById<TextView>(R.id.format_note).text = chosenFormat.format_note.uppercase()
}else{ }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_note).text = title
} }
formatCard.findViewById<TextView>(R.id.format_id).text = "id: ${chosenFormat.format_id}" 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).visibility = View.VISIBLE
formatCard.findViewById<TextView>(R.id.codec).text = codec 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") request.addOption("--embed-metadata")
if (downloadItem.audioPreferences.embedThumb) { if (downloadItem.audioPreferences.embedThumb) {
request.addOption("--embed-thumbnail") request.addOption("--embed-thumbnail")
request.addOption("--convert-thumbnails", "jpg") request.addOption("--convert-thumbnails", "jpg")
try { if (sharedPreferences.getBoolean("crop_thumbnail", true)){
val config = File(context.cacheDir.absolutePath + "/downloads/${downloadItem.id}/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt") try {
val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\"" val config = File(context.cacheDir.absolutePath + "/downloads/${downloadItem.id}/config" + downloadItem.title + "##" + downloadItem.format.format_id + ".txt")
config.writeText(configData) val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\""
request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1") config.writeText(configData)
request.addOption("--config", config.absolutePath) request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1")
} catch (ignored: Exception) {} request.addOption("--config", config.absolutePath)
} catch (ignored: Exception) {}
}
} }
request.addOption("--parse-metadata", "%(release_year,upload_date)s:%(meta_date)s") 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" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fitsSystemWindows="true"
tools:context=".MainActivity"> tools:context=".MainActivity">
<TextView <TextView

View file

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

View file

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

View file

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

View file

@ -269,4 +269,6 @@
<string name="general">General</string> <string name="general">General</string>
<string name="swipe_gestures">Swipe Gestures</string> <string name="swipe_gestures">Swipe Gestures</string>
<string name="swipe_gestures_summary">Swipe items for certain actions</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> </resources>

View file

@ -74,6 +74,15 @@
app:summary="@string/embed_thumb_summary" app:summary="@string/embed_thumb_summary"
app:title="@string/embed_thumb" /> 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 <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true" app:defaultValue="true"