few changes

-made the download logs not freeze the app
-download logs will work on quick downloaded items that were later updated
-added app arch and build type in the about section
-added "force-keyframes-at-cuts" preference
This commit is contained in:
deniscerri 2023-07-10 22:30:59 +02:00
parent 1783b40608
commit d659686e01
No known key found for this signature in database
GPG key ID: 95C43D517D830350
26 changed files with 869 additions and 280 deletions

View file

@ -10,11 +10,11 @@ def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 6 def versionMinor = 6
def versionPatch = 3 def versionPatch = 3
def versionBuild = 2 // bump for dogfood builds, public betas, etc. def versionBuild = 4 // bump for dogfood builds, public betas, etc.
def versionExt = "" def versionExt = ""
if (versionBuild > 0){ if (versionBuild > 0){
versionExt = ".${versionBuild}-beta" versionExt = ".${versionBuild}"
} }
android { android {
@ -72,7 +72,8 @@ android {
beta { beta {
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false debuggable false
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }

View file

@ -0,0 +1,486 @@
{
"formatVersion": 1,
"database": {
"version": 8,
"identityHash": "b21f2aa737d29eef2c1bf6418b314726",
"entities": [
{
"tableName": "results",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `creationTime` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumb",
"columnName": "thumb",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "website",
"columnName": "website",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistTitle",
"columnName": "playlistTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "formats",
"columnName": "formats",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "urls",
"columnName": "urls",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "chapters",
"columnName": "chapters",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "creationTime",
"columnName": "creationTime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadId` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumb",
"columnName": "thumb",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "downloadPath",
"columnName": "downloadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "website",
"columnName": "website",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadId",
"columnName": "downloadId",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "downloads",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT NOT NULL, `container` TEXT NOT NULL DEFAULT 'Default', `downloadSections` TEXT NOT NULL DEFAULT '', `allFormats` TEXT NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `audioPreferences` TEXT NOT NULL, `videoPreferences` TEXT NOT NULL, `customFileNameTemplate` TEXT NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `downloadStartTime` INTEGER NOT NULL DEFAULT 0, `logID` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumb",
"columnName": "thumb",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "container",
"columnName": "container",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'Default'"
},
{
"fieldPath": "downloadSections",
"columnName": "downloadSections",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "allFormats",
"columnName": "allFormats",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadPath",
"columnName": "downloadPath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "website",
"columnName": "website",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadSize",
"columnName": "downloadSize",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistTitle",
"columnName": "playlistTitle",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "audioPreferences",
"columnName": "audioPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "videoPreferences",
"columnName": "videoPreferences",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "customFileNameTemplate",
"columnName": "customFileNameTemplate",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "SaveThumb",
"columnName": "SaveThumb",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'Queued'"
},
{
"fieldPath": "downloadStartTime",
"columnName": "downloadStartTime",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "logID",
"columnName": "logID",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "commandTemplates",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "searchHistory",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "templateShortcuts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `content` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "cookies",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `content` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "url",
"columnName": "url",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "logs",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "content",
"columnName": "content",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b21f2aa737d29eef2c1bf6418b314726')"
]
}
}

View file

@ -84,6 +84,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
if (item.author.isNotEmpty()) info += "" if (item.author.isNotEmpty()) info += ""
info += item.duration info += item.duration
} }
author.text = info
val type = card.findViewById<MaterialButton>(R.id.download_type) val type = card.findViewById<MaterialButton>(R.id.download_type)
when(item.type){ when(item.type){
@ -93,7 +94,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
} }
val formatDetailsChip = card.findViewById<Chip>(R.id.format_note) val formatDetailsChip = card.findViewById<Chip>(R.id.format_note)
val formatDetailsText = StringBuilder(item.format.format_note.uppercase()+"\t") val formatDetailsText = StringBuilder(item.format.format_note.uppercase())
val codecText = val codecText =
if (item.format.encoding != "") { if (item.format.encoding != "") {
@ -104,11 +105,11 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
item.format.acodec.uppercase() item.format.acodec.uppercase()
} }
if (codecText != "" && codecText != "none"){ if (codecText != "" && codecText != "none"){
formatDetailsText.append("\t|\t$codecText") formatDetailsText.append(" \t\t $codecText")
} }
val fileSize = FileUtil.convertFileSize(item.format.filesize) val fileSize = FileUtil.convertFileSize(item.format.filesize)
if (fileSize != "?") formatDetailsText.append("\t|\t$fileSize") if (fileSize != "?") formatDetailsText.append(" \t\t $fileSize")
formatDetailsChip.text = formatDetailsText formatDetailsChip.text = formatDetailsText

View file

@ -12,9 +12,10 @@ import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.LogItem
import java.io.File import java.io.File
class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<File?, DownloadLogsAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<LogItem?, DownloadLogsAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
private val onItemClickListener: OnItemClickListener private val onItemClickListener: OnItemClickListener
private val activity: Activity private val activity: Activity
@ -42,7 +43,7 @@ class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Ac
val card = holder.item val card = holder.item
val title = card.findViewById<TextView>(R.id.title) val title = card.findViewById<TextView>(R.id.title)
title.text = item?.name title.text = item?.title
val delete = card.findViewById<Button>(R.id.delete) val delete = card.findViewById<Button>(R.id.delete)
@ -56,18 +57,18 @@ class DownloadLogsAdapter(onItemClickListener: OnItemClickListener, activity: Ac
} }
interface OnItemClickListener { interface OnItemClickListener {
fun onItemClick(file: File) fun onItemClick(item: LogItem)
fun onDeleteClick(file: File) fun onDeleteClick(item: LogItem)
} }
companion object { companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<File> = object : DiffUtil.ItemCallback<File>() { private val DIFF_CALLBACK: DiffUtil.ItemCallback<LogItem> = object : DiffUtil.ItemCallback<LogItem>() {
override fun areItemsTheSame(oldItem: File, newItem: File): Boolean { override fun areItemsTheSame(oldItem: LogItem, newItem: LogItem): Boolean {
return oldItem == newItem return oldItem.id == newItem.id
} }
override fun areContentsTheSame(oldItem: File, newItem: File): Boolean { override fun areContentsTheSame(oldItem: LogItem, newItem: LogItem): Boolean {
return oldItem.absolutePath == newItem.absolutePath return oldItem.content == newItem.content
} }
} }
} }

View file

@ -105,8 +105,7 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
DownloadRepository.Status.Queued.toString() -> actionButton.setIconResource(R.drawable.ic_baseline_delete_outline_24) DownloadRepository.Status.Queued.toString() -> actionButton.setIconResource(R.drawable.ic_baseline_delete_outline_24)
else -> { else -> {
actionButton.setIconResource(R.drawable.ic_baseline_file_open_24) actionButton.setIconResource(R.drawable.ic_baseline_file_open_24)
val logFile = FileUtil.checkLogFileExists(activity, item) if (item.logID == null){
if (logFile == null){
actionButton.visibility = View.GONE actionButton.visibility = View.GONE
} }
} }

View file

@ -7,15 +7,16 @@ import com.deniscerri.ytdlnis.database.models.*
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
@Database( @Database(
entities = [ResultItem::class, HistoryItem::class, DownloadItem::class, CommandTemplate::class, SearchHistoryItem::class, TemplateShortcut::class, CookieItem::class], entities = [ResultItem::class, HistoryItem::class, DownloadItem::class, CommandTemplate::class, SearchHistoryItem::class, TemplateShortcut::class, CookieItem::class, LogItem::class],
version = 7, version = 8,
autoMigrations = [ autoMigrations = [
AutoMigration (from = 1, to = 2), AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3), AutoMigration (from = 2, to = 3),
AutoMigration (from = 3, to = 4), AutoMigration (from = 3, to = 4),
AutoMigration (from = 4, to = 5), AutoMigration (from = 4, to = 5),
AutoMigration (from = 5, to = 6), AutoMigration (from = 5, to = 6),
AutoMigration (from = 6, to = 7) AutoMigration (from = 6, to = 7),
AutoMigration (from = 7, to = 8)
] ]
) )
abstract class DBManager : RoomDatabase(){ abstract class DBManager : RoomDatabase(){
@ -25,6 +26,7 @@ abstract class DBManager : RoomDatabase(){
abstract val commandTemplateDao : CommandTemplateDao abstract val commandTemplateDao : CommandTemplateDao
abstract val searchHistoryDao: SearchHistoryDao abstract val searchHistoryDao: SearchHistoryDao
abstract val cookieDao: CookieDao abstract val cookieDao: CookieDao
abstract val logDao: LogDao
companion object { companion object {
//prevents multiple instances of db getting created at the same time //prevents multiple instances of db getting created at the same time

View file

@ -0,0 +1,30 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.models.LogItem
import kotlinx.coroutines.flow.Flow
@Dao
interface LogDao {
@Query("SELECT * FROM logs ORDER BY id DESC")
fun getAllLogs() : List<LogItem>
@Query("SELECT * FROM logs ORDER BY id DESC")
fun getAllLogsFlow() : Flow<List<LogItem>>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: LogItem) : Long
@Query("DELETE FROM logs")
suspend fun deleteAll()
@Query("DELETE FROM logs WHERE id=:itemId")
suspend fun delete(itemId: Long)
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: LogItem)
@Query("SELECT * FROM logs WHERE id=:id LIMIT 1")
fun getByID(id: Long) : LogItem
}

View file

@ -32,5 +32,6 @@ data class DownloadItem(
@ColumnInfo(defaultValue = "Queued") @ColumnInfo(defaultValue = "Queued")
var status: String, var status: String,
@ColumnInfo(defaultValue = "0") @ColumnInfo(defaultValue = "0")
var downloadStartTime: Long var downloadStartTime: Long,
var logID: Long?
) )

View file

@ -0,0 +1,12 @@
package com.deniscerri.ytdlnis.database.models
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "logs")
data class LogItem(
@PrimaryKey(autoGenerate = true)
var id: Long,
var title: String,
var content: String
)

View file

@ -0,0 +1,39 @@
package com.deniscerri.ytdlnis.database.repository
import com.deniscerri.ytdlnis.database.dao.LogDao
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.models.LogItem
import kotlinx.coroutines.flow.Flow
class LogRepository(private val logDao: LogDao) {
val items : Flow<List<LogItem>> = logDao.getAllLogsFlow()
fun getAll() : List<LogItem> {
return logDao.getAllLogs()
}
suspend fun insert(item: LogItem) : Long{
return logDao.insert(item)
}
suspend fun delete(item: LogItem){
logDao.delete(item.id)
}
suspend fun deleteAll(){
logDao.deleteAll()
}
fun getItem(id: Long) : LogItem{
return logDao.getByID(id);
}
suspend fun update(newLine: String, id: Long){
val item = getItem(id)
item.content += "${newLine}\n"
logDao.update(item)
}
}

View file

@ -180,7 +180,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
sharedPreferences.getString("video_format", "Default")!!, sharedPreferences.getString("video_format", "Default")!!,
"", "",
resultItem.formats, resultItem.formats,
downloadPath!!, resultItem.website, "", resultItem.playlistTitle, audioPreferences, videoPreferences,customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0 downloadPath!!, resultItem.website, "", resultItem.playlistTitle, audioPreferences, videoPreferences,customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0, null
) )
} }
@ -302,7 +302,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
container, container,
"", "",
ArrayList(), ArrayList(),
path, historyItem.website, "", "", audioPreferences, videoPreferences,customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0 path, historyItem.website, "", "", audioPreferences, videoPreferences,customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0, null
) )
} }

View file

@ -0,0 +1,50 @@
package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.LogRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class LogViewModel(private val application: Application) : AndroidViewModel(application) {
private val repository: LogRepository
val items: LiveData<List<LogItem>>
init {
val dao = DBManager.getInstance(application).logDao
repository = LogRepository(dao)
items = repository.items.asLiveData()
}
fun getItemById(id: Long): LogItem{
return repository.getItem(id)
}
fun getAll(): List<LogItem> {
return repository.getAll()
}
suspend fun insert(item: LogItem) : Long {
return repository.insert(item)
}
fun delete(item: LogItem) = viewModelScope.launch(Dispatchers.IO) {
repository.delete(item)
}
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
}
fun update(newLine: String, id: Long) = viewModelScope.launch(Dispatchers.IO) {
repository.update(newLine, id)
}
}

View file

@ -18,8 +18,6 @@ import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.progressindicator.LinearProgressIndicator
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
@ -98,10 +96,9 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
override fun onOutputClick(item: DownloadItem) { override fun onOutputClick(item: DownloadItem) {
val logFile = FileUtil.getLogFile(requireContext(), item) if (item.logID != null) {
if (logFile.exists()) {
val bundle = Bundle() val bundle = Bundle()
bundle.putString("logpath", logFile.absolutePath) bundle.putLong("logID", item.logID!!)
findNavController().navigate( findNavController().navigate(
R.id.downloadLogFragment, R.id.downloadLogFragment,
bundle bundle

View file

@ -30,7 +30,6 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter import com.deniscerri.ytdlnis.adapter.GenericDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
@ -42,7 +41,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.File
class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener, OnItemClickListener { class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickListener, OnItemClickListener {
@ -93,10 +91,9 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL
override fun onActionButtonClick(itemID: Long) { override fun onActionButtonClick(itemID: Long) {
val item = items.find { it.id == itemID } ?: return val item = items.find { it.id == itemID } ?: return
val logFile = FileUtil.getLogFile(requireContext(), item) if (item.logID != null) {
if (logFile.exists()) {
val bundle = Bundle() val bundle = Bundle()
bundle.putString("logpath", logFile.absolutePath) bundle.putLong("logID", item.logID!!)
findNavController().navigate( findNavController().navigate(
R.id.downloadLogFragment, R.id.downloadLogFragment,
bundle bundle

View file

@ -3,7 +3,6 @@ package com.deniscerri.ytdlnis.ui.more.downloadLogs
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE import android.content.Context.CLIPBOARD_SERVICE
import android.graphics.Color import android.graphics.Color
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.FileObserver import android.os.FileObserver
import android.view.LayoutInflater import android.view.LayoutInflater
@ -12,14 +11,21 @@ import android.view.ViewGroup
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.map
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.neo.highlight.core.Highlight import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme import com.neo.highlight.util.scheme.ColorScheme
import java.io.File import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.regex.Pattern import java.util.regex.Pattern
@ -27,9 +33,9 @@ class DownloadLogFragment : Fragment() {
private lateinit var content: TextView private lateinit var content: TextView
private lateinit var contentScrollView : ScrollView private lateinit var contentScrollView : ScrollView
private lateinit var topAppBar: MaterialToolbar private lateinit var topAppBar: MaterialToolbar
private lateinit var observer: FileObserver
private lateinit var copyLog : ExtendedFloatingActionButton private lateinit var copyLog : ExtendedFloatingActionButton
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
private lateinit var logViewModel: LogViewModel
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -61,41 +67,27 @@ class DownloadLogFragment : Fragment() {
clipboard.setText(content.text) clipboard.setText(content.text)
} }
val path = arguments?.getString("logpath") val id = arguments?.getLong("logID")
if (path == null) { if (id == null) {
mainActivity.onBackPressedDispatcher.onBackPressed() mainActivity.onBackPressedDispatcher.onBackPressed()
}else{ }else{
arguments?.remove("logpath") arguments?.remove("logID")
} }
val file = File(path!!)
topAppBar.title = file.name
content.text = file.readText()
if(Build.VERSION.SDK_INT < 29){ logViewModel = ViewModelProvider(this)[LogViewModel::class.java]
observer = object : FileObserver(file.absolutePath, MODIFY) { logViewModel.items.map { it.find { log -> log.id == id } }.observe(viewLifecycleOwner) { logItem ->
override fun onEvent(event: Int, p: String?) { mainActivity.runOnUiThread{
mainActivity.runOnUiThread{ content.text = logItem?.content
val newText = File(path).readText() content.scrollTo(0, content.height)
content.text = newText contentScrollView.fullScroll(View.FOCUS_DOWN)
content.scrollTo(0, content.height)
contentScrollView.fullScroll(View.FOCUS_DOWN)
}
}
} }
observer.startWatching(); }
}else{
observer = object : FileObserver(file, MODIFY) {
override fun onEvent(event: Int, p: String?) { CoroutineScope(Dispatchers.IO).launch {
mainActivity.runOnUiThread{ val logItem = logViewModel.getItemById(id!!)
val newText = File(path).readText() topAppBar.title = logItem.title
content.text = newText
content.scrollTo(0, content.height)
contentScrollView.fullScroll(View.FOCUS_DOWN)
}
}
}
observer.startWatching();
} }
//init syntax highlighter //init syntax highlighter
@ -119,11 +111,6 @@ class DownloadLogFragment : Fragment() {
content.addTextChangedListener(highlightWatcher) content.addTextChangedListener(highlightWatcher)
} }
override fun onDestroy() {
super.onDestroy()
observer.stopWatching()
}
companion object { companion object {
private const val TAG = "DownloadLogActivity" private const val TAG = "DownloadLogActivity"
} }

View file

@ -1,9 +1,7 @@
package com.deniscerri.ytdlnis.ui.more.downloadLogs package com.deniscerri.ytdlnis.ui.more.downloadLogs
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.FileObserver
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -11,12 +9,15 @@ import android.view.ViewGroup
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.DownloadLogsAdapter import com.deniscerri.ytdlnis.adapter.DownloadLogsAdapter
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import java.io.File import java.io.File
@ -28,8 +29,9 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
private lateinit var noResults: RelativeLayout private lateinit var noResults: RelativeLayout
private lateinit var fileList: MutableList<File> private lateinit var fileList: MutableList<File>
private lateinit var topAppBar: MaterialToolbar private lateinit var topAppBar: MaterialToolbar
private lateinit var logFolder : File
private lateinit var mainActivity: MainActivity private lateinit var mainActivity: MainActivity
private lateinit var logViewModel: LogViewModel
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -56,32 +58,17 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
noResults = view.findViewById(R.id.no_results) noResults = view.findViewById(R.id.no_results)
noResults.visibility = View.GONE noResults.visibility = View.GONE
logFolder = File(requireContext().filesDir.absolutePath + "/logs") logViewModel = ViewModelProvider(this)[LogViewModel::class.java]
updateList(logFolder) logViewModel.items.observe(viewLifecycleOwner) {
if (it.isEmpty()) noResults.visibility = View.VISIBLE
if(Build.VERSION.SDK_INT < 29){ else noResults.visibility = View.GONE
val observer: FileObserver = object : FileObserver(logFolder.absolutePath) { downloadLogAdapter.submitList(it)
override fun onEvent(event: Int, path: String?) {
when(event) {
CREATE, DELETE -> updateList(logFolder)
}
}
}
observer.startWatching()
}else{
val observer: FileObserver = object : FileObserver(logFolder) {
override fun onEvent(event: Int, path: String?) {
when(event) {
CREATE, DELETE -> updateList(logFolder)
}
}
}
observer.startWatching()
} }
initMenu(logFolder)
initMenu()
} }
private fun initMenu(logFolder: File) { private fun initMenu() {
topAppBar.setOnMenuItemClickListener { m: MenuItem -> topAppBar.setOnMenuItemClickListener { m: MenuItem ->
val itemId = m.itemId val itemId = m.itemId
if (itemId == R.id.remove_logs) { if (itemId == R.id.remove_logs) {
@ -91,11 +78,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
deleteDialog.setMessage(getString(R.string.confirm_delete_logs_desc)) deleteDialog.setMessage(getString(R.string.confirm_delete_logs_desc))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
logFolder.listFiles()!!.forEach { logViewModel.deleteAll()
it.delete()
}.run {
updateList(logFolder)
}
} }
deleteDialog.show() deleteDialog.show()
}catch (e: Exception){ }catch (e: Exception){
@ -106,43 +89,22 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
} }
} }
private fun updateList(logFolder: File){ override fun onItemClick(item: LogItem) {
fileList = mutableListOf()
try{
fileList.addAll(logFolder.listFiles()!!)
fileList.sortByDescending { it.lastModified()}
}catch (e: Exception){
e.printStackTrace()
}
downloadLogAdapter.submitList(fileList.toList())
mainActivity.runOnUiThread{
if (fileList.isNotEmpty()) {
noResults.visibility = View.GONE
topAppBar.menu.findItem(R.id.remove_logs).isVisible = true
}else{
topAppBar.menu.findItem(R.id.remove_logs).isVisible = false
noResults.visibility = View.VISIBLE
}
}
}
override fun onItemClick(file: File) {
val bundle = Bundle() val bundle = Bundle()
bundle.putString("logpath", file.absolutePath) bundle.putLong("logID", item.id)
findNavController().navigate( findNavController().navigate(
R.id.downloadLogFragment, R.id.downloadLogFragment,
bundle bundle
) )
} }
override fun onDeleteClick(file: File) { override fun onDeleteClick(item: LogItem) {
val deleteDialog = MaterialAlertDialogBuilder(requireContext()) val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + file.name + "\"!") deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
file.delete().run { logViewModel.delete(item)
updateList(logFolder)
}
} }
deleteDialog.show() deleteDialog.show()
} }

View file

@ -34,7 +34,6 @@ import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.util.UpdateUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -65,6 +64,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
private lateinit var cookieViewModel: CookieViewModel private lateinit var cookieViewModel: CookieViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private var arch: Preference? = null
private var version: Preference? = null private var version: Preference? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
@ -200,7 +200,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
} }
version = findPreference("version") version = findPreference("version")
version!!.summary = BuildConfig.VERSION_NAME version!!.summary = "${BuildConfig.VERSION_NAME} [${Build.SUPPORTED_ABIS[0]}] ${BuildConfig.BUILD_TYPE}"
version!!.onPreferenceClickListener = version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
@ -223,11 +223,13 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
val prefs = preferences.all val prefs = preferences.all
val arr = JsonArray() val arr = JsonArray()
prefs.forEach { prefs.forEach {
val obj = JsonObject() if (it.key != "app_language"){
obj.addProperty("key", it.key) val obj = JsonObject()
obj.addProperty("value", it.value.toString()) obj.addProperty("key", it.key)
obj.addProperty("type", it.value!!::class.simpleName) obj.addProperty("value", it.value.toString())
arr.add(obj) obj.addProperty("type", it.value!!::class.simpleName)
arr.add(obj)
}
} }
return arr return arr
} }

View file

@ -152,25 +152,6 @@ object FileUtil {
return listOf(context.getString(R.string.unfound_file)) return listOf(context.getString(R.string.unfound_file))
} }
fun getLogFile(context: Context, item: DownloadItem) : File {
val titleRegex = Regex("[^A-Za-z\\d ]")
val title = item.title.ifEmpty { item.url }
return File(context.filesDir.absolutePath + """/logs/${item.id} - ${titleRegex.replace(title, "").take(150)}##${item.type}##${item.format.format_id}.log""")
}
fun checkLogFileExists(context: Context, item: DownloadItem) : File? {
return try {
val dir = File(context.filesDir.absolutePath + "/logs/")
dir.listFiles()?.toList()?.first { it.name.startsWith(item.id.toString()) }
}catch (e: Exception){
null
}
}
fun getLogFileForTerminal(context: Context, command: String) : File {
val titleRegex = Regex("[^A-Za-z\\d ]")
return File(context.filesDir.absolutePath + """/logs/Terminal - ${titleRegex.replace(command.take(30), "")}##terminal.log""")
}
fun getDefaultAudioPath() : String{ fun getDefaultAudioPath() : String{
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis/Audio" return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis/Audio"

View file

@ -279,48 +279,51 @@ class InfoUtil(private val context: Context) {
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg" val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val url = "https://www.youtube.com/watch?v=$id" val url = "https://www.youtube.com/watch?v=$id"
val formats : ArrayList<Format> = ArrayList() val formats : ArrayList<Format> = ArrayList()
if (obj.has("audioStreams")){
val formatsInJSON = obj.getJSONArray("audioStreams") if(sharedPreferences.getString("formats_source", "yt-dlp") == "piped"){
for (f in 0 until formatsInJSON.length()){ if (obj.has("audioStreams")){
val format = formatsInJSON.getJSONObject(f) val formatsInJSON = obj.getJSONArray("audioStreams")
if (format.getInt("bitrate") == 0) continue for (f in 0 until formatsInJSON.length()){
val formatObj = Gson().fromJson(format.toString(), Format::class.java) val format = formatsInJSON.getJSONObject(f)
try{ if (format.getInt("bitrate") == 0) continue
formatObj.acodec = format.getString("codec") val formatObj = Gson().fromJson(format.toString(), Format::class.java)
formatObj.asr = format.getString("quality") try{
if (! format.getString("audioTrackName").equals("null", ignoreCase = true)){ formatObj.acodec = format.getString("codec")
formatObj.format_note = format.getString("audioTrackName") + " Audio, " + formatObj.format_note formatObj.asr = format.getString("quality")
}else{ if (! format.getString("audioTrackName").equals("null", ignoreCase = true)){
formatObj.format_note = formatObj.format_note + " Audio" formatObj.format_note = format.getString("audioTrackName") + " Audio, " + formatObj.format_note
}else{
formatObj.format_note = formatObj.format_note + " Audio"
}
}catch (e: Exception) {
e.printStackTrace()
} }
formats.add(formatObj)
}catch (e: Exception) {
e.printStackTrace()
} }
formats.add(formatObj)
} }
}
if (obj.has("videoStreams")){ if (obj.has("videoStreams")){
val formatsInJSON = obj.getJSONArray("videoStreams") val formatsInJSON = obj.getJSONArray("videoStreams")
for (f in 0 until formatsInJSON.length()){ for (f in 0 until formatsInJSON.length()){
val format = formatsInJSON.getJSONObject(f) val format = formatsInJSON.getJSONObject(f)
if (format.getInt("bitrate") == 0) continue if (format.getInt("bitrate") == 0) continue
val formatObj = Gson().fromJson(format.toString(), Format::class.java) val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{ try{
formatObj.vcodec = format.getString("codec") formatObj.vcodec = format.getString("codec")
}catch (e: Exception) { }catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
}
formats.add(formatObj)
} }
formats.add(formatObj)
} }
} formats.sortBy { it.filesize }
formats.sortBy { it.filesize } formats.groupBy { it.format_id }.forEach {
formats.groupBy { it.format_id }.forEach { if (it.value.count() > 1) {
if (it.value.count() > 1) { it.value.filter { f-> !f.format_note.contains("original", true) }.forEachIndexed { index, format -> format.format_id = format.format_id.split("-")[0] + "-${index}" }
it.value.filter { f-> !f.format_note.contains("original", true) }.forEachIndexed { index, format -> format.format_id = format.format_id.split("-")[0] + "-${index}" } val engDefault = it.value.find { f -> f.format_note.contains("original", true) }
val engDefault = it.value.find { f -> f.format_note.contains("original", true) } engDefault?.format_id = (engDefault?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}"
engDefault?.format_id = (engDefault?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}" }
} }
} }

View file

@ -19,6 +19,7 @@ import com.deniscerri.ytdlnis.receiver.PauseDownloadNotificationReceiver
import com.deniscerri.ytdlnis.receiver.ResumeActivity import com.deniscerri.ytdlnis.receiver.ResumeActivity
import com.deniscerri.ytdlnis.receiver.ShareFileActivity import com.deniscerri.ytdlnis.receiver.ShareFileActivity
import java.io.File import java.io.File
import kotlin.math.log
class NotificationUtil(var context: Context) { class NotificationUtil(var context: Context) {
@ -231,13 +232,15 @@ class NotificationUtil(var context: Context) {
fun createDownloadErrored(title: String?, fun createDownloadErrored(title: String?,
error: String?, error: String?,
logFile: File?, logID: Long?,
channel: String channel: String
) { ) {
val notificationBuilder = getBuilder(channel) val notificationBuilder = getBuilder(channel)
val bundle = Bundle() val bundle = Bundle()
bundle.putString("logpath", logFile?.absolutePath) if (logID != null){
bundle.putLong("logID", logID)
}
val errorPendingIntent = NavDeepLinkBuilder(context) val errorPendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph) .setGraph(R.navigation.nav_graph)
@ -268,7 +271,7 @@ class NotificationUtil(var context: Context) {
.setPriority(NotificationCompat.PRIORITY_MAX) .setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions() .clearActions()
if (logFile != null){ if (logID != null){
notificationBuilder.setContentIntent(errorPendingIntent) notificationBuilder.setContentIntent(errorPendingIntent)
notificationBuilder.addAction(0, context.getString(R.string.logs), errorPendingIntent) notificationBuilder.addAction(0, context.getString(R.string.logs), errorPendingIntent)
} }

View file

@ -1,7 +1,6 @@
package com.deniscerri.ytdlnis.work package com.deniscerri.ytdlnis.work
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
@ -19,7 +18,9 @@ import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.dao.ResultDao import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
@ -48,6 +49,7 @@ class DownloadWorker(
val repository = DownloadRepository(dao) val repository = DownloadRepository(dao)
val historyDao = dbManager.historyDao val historyDao = dbManager.historyDao
val resultDao = dbManager.resultDao val resultDao = dbManager.resultDao
val logRepo = LogRepository(dbManager.logDao)
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
@ -92,16 +94,6 @@ class DownloadWorker(
tempFileDir.delete() tempFileDir.delete()
tempFileDir.mkdirs() tempFileDir.mkdirs()
var useCacheFolder = true
val downloadLocationFile = File(downloadLocation)
if (downloadLocationFile.canWrite()) {
File(downloadLocation).mkdirs()
if(Build.VERSION.SDK_INT > 23) request.addOption("-P", "temp:" + tempFileDir.absolutePath)
useCacheFolder = false
}
val pathUsed = if (useCacheFolder) tempFileDir.absolutePath else downloadLocation
val aria2 = sharedPreferences.getBoolean("aria2", false) val aria2 = sharedPreferences.getBoolean("aria2", false)
if (aria2) { if (aria2) {
request.addOption("--downloader", "libaria2c.so") request.addOption("--downloader", "libaria2c.so")
@ -159,6 +151,10 @@ class DownloadWorker(
request.addOption("--download-sections", "*$it") request.addOption("--download-sections", "*$it")
else else
request.addOption("--download-sections", it) request.addOption("--download-sections", it)
if (sharedPreferences.getBoolean("force_keyframes", false)){
request.addOption("--force-keyframes-at-cuts")
}
} }
downloadItem.customFileNameTemplate += " %(section_title)s %(autonumber)s" downloadItem.customFileNameTemplate += " %(section_title)s %(autonumber)s"
request.addOption("--output-na-placeholder", " ") request.addOption("--output-na-placeholder", " ")
@ -233,9 +229,9 @@ class DownloadWorker(
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){ if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters") request.addOption("--split-chapters")
request.addOption("-P", pathUsed) request.addOption("-P", tempFileDir.absolutePath)
}else{ }else{
request.addOption("-o", pathUsed + "/${downloadItem.customFileNameTemplate}.%(ext)s") request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
} }
} }
@ -262,7 +258,7 @@ class DownloadWorker(
else if (videoFormatID == context.resources.getString(R.string.worst_quality) || videoFormatID == "worst") videoFormatID = "worst" else if (videoFormatID == context.resources.getString(R.string.worst_quality) || videoFormatID == "worst") videoFormatID = "worst"
else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]" else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]"
formatArgument = if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty()){ formatArgument = if (downloadItem.videoPreferences.audioFormatIDs.isNotEmpty() && ! downloadItem.videoPreferences.removeAudio){
val audioIds = downloadItem.videoPreferences.audioFormatIDs.joinToString("+") val audioIds = downloadItem.videoPreferences.audioFormatIDs.joinToString("+")
"$videoFormatID+$audioIds/best/$videoFormatID" "$videoFormatID+$audioIds/best/$videoFormatID"
}else{ }else{
@ -292,16 +288,15 @@ class DownloadWorker(
} }
} }
if (downloadItem.videoPreferences.removeAudio && if (downloadItem.videoPreferences.removeAudio){
(downloadItem.format.acodec.isNotEmpty() && downloadItem.format.acodec != "none")){
request.addOption("--ppa", "ffmpeg:-an") request.addOption("--ppa", "ffmpeg:-an")
} }
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){ if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters") request.addOption("--split-chapters")
request.addOption("-P", pathUsed) request.addOption("-P", tempFileDir.absolutePath)
}else{ }else{
request.addOption("-o", pathUsed + "/${downloadItem.customFileNameTemplate}.%(ext)s") request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s")
} }
} }
@ -312,24 +307,32 @@ class DownloadWorker(
writeText(downloadItem.format.format_note) writeText(downloadItem.format.format_note)
}.absolutePath }.absolutePath
) )
request.addOption("-P", pathUsed) request.addOption("-P", tempFileDir.absolutePath)
} }
} }
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false) val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
val logFolder = File(context.filesDir.absolutePath + "/logs")
val logFile = FileUtil.getLogFile(context, downloadItem)
if (logDownloads){ val logItem = LogItem(
logFolder.mkdirs() 0,
logFile.createNewFile() downloadItem.title,
logFile.writeText("Downloading:\n" + "Downloading:\n" +
"Title: ${downloadItem.title}\n" + "Title: ${downloadItem.title}\n" +
"URL: ${downloadItem.url}\n" + "URL: ${downloadItem.url}\n" +
"Type: ${downloadItem.type}\n" + "Type: ${downloadItem.type}\n" +
"Format: ${downloadItem.format}\n\n" + "Format: ${downloadItem.format}\n\n" +
"Command: ${java.lang.String.join(" ", request.buildCommand())}\n\n") "Command: ${java.lang.String.join(" ", request.buildCommand())}\n\n"
)
if (logDownloads){
runBlocking {
logItem.id = logRepo.insert(logItem)
downloadItem.logID = logItem.id
dao.update(downloadItem)
}
} }
runCatching { runCatching {
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line ->
@ -340,48 +343,41 @@ class DownloadWorker(
line, progress.toInt(), 0, title, line, progress.toInt(), 0, title,
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
) )
if (logDownloads && logFile.exists()){ if (logDownloads){
logFile.appendText("${line}\n") CoroutineScope(Dispatchers.IO).launch {
logRepo.update(line, logItem.id)
}
} }
} }
}.onSuccess { }.onSuccess {
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao) val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
var finalPaths: List<String>
if (useCacheFolder){
//move file from internal to set download directory
setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads))
try {
finalPaths = FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p ->
setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads))
}
if (finalPaths.isNotEmpty()){
setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id, "log" to logDownloads))
}else{
finalPaths = listOf(context.getString(R.string.unfound_file))
}
}catch (e: Exception){
finalPaths = listOf(context.getString(R.string.unfound_file))
e.printStackTrace()
handler.postDelayed({
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}, 1000)
}
}else{
val now = System.currentTimeMillis()
finalPaths = downloadLocationFile
.walkTopDown()
.filter { it.isFile && it.absolutePath.contains(downloadItem.title) && it.lastModified() > now - 10000000}
.map { it.absolutePath }
.toList()
if (finalPaths.isEmpty()) finalPaths = listOf(context.getString(R.string.unfound_file)) var finalPaths : List<String>?
//move file from internal to set download directory
setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads))
try {
finalPaths = FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p ->
setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads))
}
if (finalPaths.isNotEmpty()){
setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id, "log" to logDownloads))
}else{
finalPaths = listOf(context.getString(R.string.unfound_file))
}
}catch (e: Exception){
finalPaths = listOf(context.getString(R.string.unfound_file))
e.printStackTrace()
handler.postDelayed({
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}, 1000)
} }
//put download in history //put download in history
val incognito = sharedPreferences.getBoolean("incognito", false) val incognito = sharedPreferences.getBoolean("incognito", false)
if (!incognito) { if (!incognito) {
val unixtime = System.currentTimeMillis() / 1000 val unixtime = System.currentTimeMillis() / 1000
val file = File(finalPaths.first()!!) val file = File(finalPaths?.first()!!)
downloadItem.format.filesize = file.length() downloadItem.format.filesize = file.length()
val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id) val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id)
runBlocking { runBlocking {
@ -421,8 +417,12 @@ class DownloadWorker(
Data.Builder().putString("output", "Download has been cancelled!").build() Data.Builder().putString("output", "Download has been cancelled!").build()
) )
}else{ }else{
if (logDownloads && logFile.exists()){ if (logDownloads){
logFile.appendText("${it.message}\n") CoroutineScope(Dispatchers.IO).launch {
if(it.message != null){
logRepo.update(it.message!!, logItem.id)
}
}
} }
tempFileDir.delete() tempFileDir.delete()
@ -440,7 +440,7 @@ class DownloadWorker(
notificationUtil.createDownloadErrored( notificationUtil.createDownloadErrored(
downloadItem.title, it.message, downloadItem.title, it.message,
if (logDownloads) logFile else null, if (logDownloads) logItem.id else null,
NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID
) )

View file

@ -14,12 +14,22 @@ import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.ui.more.TerminalActivity import com.deniscerri.ytdlnis.ui.more.TerminalActivity
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import java.time.LocalDate
import java.time.LocalDateTime
import java.util.Calendar
class TerminalDownloadWorker( class TerminalDownloadWorker(
@ -32,6 +42,9 @@ class TerminalDownloadWorker(
if (itemId == 0) return Result.failure() if (itemId == 0) return Result.failure()
if (command!!.isEmpty()) return Result.failure() if (command!!.isEmpty()) return Result.failure()
val dbManager = DBManager.getInstance(context)
val logRepo = LogRepository(dbManager.logDao)
val notificationUtil = NotificationUtil(context) val notificationUtil = NotificationUtil(context)
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
@ -67,16 +80,20 @@ class TerminalDownloadWorker(
request.addOption("-P", tempFileDir.absolutePath) request.addOption("-P", tempFileDir.absolutePath)
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false) val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
val logFolder = File(context.filesDir.absolutePath + "/logs")
val logFile = FileUtil.getLogFileForTerminal(context, command) val logItem = LogItem(
0,
"Terminal Download at " + Calendar.getInstance().time,
"Downloading:\n" +
"Terminal Download\n" +
"Command: ${command}\n\n"
)
runCatching { runCatching {
if (logDownloads){ if (logDownloads){
logFolder.mkdirs() runBlocking {
logFile.createNewFile() logItem.id = logRepo.insert(logItem)
logFile.writeText("Downloading:\n" + }
"Terminal Download\n" +
"Command: ${command}\n\n")
} }
YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line -> YoutubeDL.getInstance().execute(request, itemId.toString()){ progress, _, line ->
@ -87,8 +104,10 @@ class TerminalDownloadWorker(
line, progress.toInt(), 0, title, line, progress.toInt(), 0, title,
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
) )
if (logDownloads && logFile.exists()){ if (logDownloads){
logFile.appendText("${line}\n") CoroutineScope(Dispatchers.IO).launch {
logRepo.update(line, logItem.id)
}
} }
} }
}.onSuccess { }.onSuccess {
@ -106,8 +125,10 @@ class TerminalDownloadWorker(
if (it.out.length > 200){ if (it.out.length > 200){
outputFile.appendText("${it.out}\n") outputFile.appendText("${it.out}\n")
if (logDownloads && logFile.exists()){ if (logDownloads){
logFile.appendText("${it.out}\n") CoroutineScope(Dispatchers.IO).launch {
logRepo.update(it.out, logItem.id)
}
} }
} }
notificationUtil.cancelDownloadNotification(itemId) notificationUtil.cancelDownloadNotification(itemId)
@ -117,8 +138,12 @@ class TerminalDownloadWorker(
return Result.failure() return Result.failure()
} }
outputFile.appendText("${it.message}\n") outputFile.appendText("${it.message}\n")
if (logDownloads && logFile.exists()){ if (logDownloads){
logFile.appendText("${it.message}\n") CoroutineScope(Dispatchers.IO).launch {
if (it.message != null){
logRepo.update(it.message!!, logItem.id)
}
}
} }
tempFileDir.delete() tempFileDir.delete()

View file

@ -282,4 +282,6 @@
<string name="retries">Retries</string> <string name="retries">Retries</string>
<string name="fragment_retries">Fragment Retries</string> <string name="fragment_retries">Fragment Retries</string>
<string name="format_order">Format Order</string> <string name="format_order">Format Order</string>
<string name="force_keyframes">Force Keyframes At Cuts</string>
<string name="force_keyframes_summary">Accurate Video Cuts. Slower</string>
</resources> </resources>

View file

@ -2,7 +2,7 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory android:title="@string/locale"> <PreferenceCategory android:title="@string/appearance">
<ListPreference <ListPreference
android:entries="@array/language_values" android:entries="@array/language_values"
android:entryValues="@array/language_values" android:entryValues="@array/language_values"
@ -11,18 +11,7 @@
app:summary="en" app:summary="en"
app:title="@string/language" /> app:title="@string/language" />
<ListPreference
android:defaultValue=""
android:entries="@array/countries"
android:entryValues="@array/countries_values"
app:icon="@drawable/ic_language"
app:useSimpleSummaryProvider="true"
app:dependency="home_recommendations"
app:key="locale"
app:title="@string/preferred_locale" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/appearance">
<ListPreference <ListPreference
android:entries="@array/themes" android:entries="@array/themes"
android:entryValues="@array/themes_values" android:entryValues="@array/themes_values"
@ -60,6 +49,17 @@
app:summary="@string/video_recommendations_summary" app:summary="@string/video_recommendations_summary"
app:title="@string/video_recommendations" /> app:title="@string/video_recommendations" />
<ListPreference
android:defaultValue=""
android:entries="@array/countries"
android:entryValues="@array/countries_values"
app:icon="@drawable/ic_language"
app:useSimpleSummaryProvider="true"
app:dependency="home_recommendations"
app:key="locale"
app:title="@string/preferred_locale" />
<ListPreference <ListPreference
android:defaultValue="Home" android:defaultValue="Home"
android:entries="@array/start_destination" android:entries="@array/start_destination"

View file

@ -60,6 +60,14 @@
app:summary="@string/crop_thumb_summary" app:summary="@string/crop_thumb_summary"
app:title="@string/crop_thumb" /> app:title="@string/crop_thumb" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_cut"
app:key="force_keyframes"
app:summary="@string/force_keyframes_summary"
app:title="@string/force_keyframes" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true" app:defaultValue="true"

View file

@ -30,7 +30,7 @@
app:title="@string/update_formats" /> app:title="@string/update_formats" />
<ListPreference <ListPreference
android:defaultValue="yt-dlp" android:defaultValue="piped"
android:entries="@array/formats_source" android:entries="@array/formats_source"
android:entryValues="@array/formats_source_values" android:entryValues="@array/formats_source_values"
android:icon="@drawable/baseline_manage_search_24" android:icon="@drawable/baseline_manage_search_24"
@ -49,7 +49,7 @@
<EditTextPreference <EditTextPreference
app:key="piped_instance" app:key="piped_instance"
app:defaultValue="https://pipedapi.kavin.rocks" app:defaultValue="https://pipedapi.syncpundit.io"
android:summary="@string/piped_instance_summary" android:summary="@string/piped_instance_summary"
app:title="@string/piped_instance" /> app:title="@string/piped_instance" />