This commit is contained in:
zaednasr 2023-11-26 17:09:08 +01:00
parent 3b3873de49
commit ef5b68ffc1
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
30 changed files with 980 additions and 165 deletions

View file

@ -0,0 +1,598 @@
{
"formatVersion": 1,
"database": {
"version": 13,
"identityHash": "5cea521e1a26473b2ec5298791f10ac4",
"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, `playlistURL` TEXT DEFAULT '', `playlistIndex` INTEGER, `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": "playlistURL",
"columnName": "playlistURL",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "playlistIndex",
"columnName": "playlistIndex",
"affinity": "INTEGER",
"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, `command` TEXT NOT NULL DEFAULT '')",
"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"
},
{
"fieldPath": "command",
"columnName": "command",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"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, `extraCommands` TEXT NOT NULL DEFAULT '', `customFileNameTemplate` TEXT NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `downloadStartTime` INTEGER NOT NULL DEFAULT 0, `logID` INTEGER, `playlistURL` TEXT DEFAULT '', `playlistIndex` 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": "extraCommands",
"columnName": "extraCommands",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"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
},
{
"fieldPath": "playlistURL",
"columnName": "playlistURL",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
},
{
"fieldPath": "playlistIndex",
"columnName": "playlistIndex",
"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, `useAsExtraCommand` INTEGER NOT NULL DEFAULT 0, `useAsExtraCommandAudio` INTEGER NOT NULL DEFAULT 1, `useAsExtraCommandVideo` INTEGER NOT NULL DEFAULT 1)",
"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
},
{
"fieldPath": "useAsExtraCommand",
"columnName": "useAsExtraCommand",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "useAsExtraCommandAudio",
"columnName": "useAsExtraCommandAudio",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
},
{
"fieldPath": "useAsExtraCommandVideo",
"columnName": "useAsExtraCommandVideo",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
}
],
"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, `format` TEXT NOT NULL, `downloadType` TEXT NOT NULL, `downloadTime` INTEGER 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
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadType",
"columnName": "downloadType",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "downloadTime",
"columnName": "downloadTime",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "terminalDownloads",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `command` TEXT NOT NULL, `log` TEXT DEFAULT '')",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "command",
"columnName": "command",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "log",
"columnName": "log",
"affinity": "TEXT",
"notNull": false,
"defaultValue": "''"
}
],
"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, '5cea521e1a26473b2ec5298791f10ac4')"
]
}
}

View file

@ -23,7 +23,7 @@ import com.deniscerri.ytdlnis.database.models.*
LogItem::class, LogItem::class,
TerminalItem::class TerminalItem::class
], ],
version = 12, version = 13,
autoMigrations = [ autoMigrations = [
AutoMigration (from = 1, to = 2), AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3), AutoMigration (from = 2, to = 3),
@ -35,7 +35,8 @@ import com.deniscerri.ytdlnis.database.models.*
AutoMigration (from = 8, to = 9), AutoMigration (from = 8, to = 9),
AutoMigration (from = 9, to = 10), AutoMigration (from = 9, to = 10),
AutoMigration (from = 10, to = 11), AutoMigration (from = 10, to = 11),
AutoMigration (from = 11, to = 12) AutoMigration (from = 11, to = 12),
AutoMigration (from = 12, to = 13)
] ]
) )
abstract class DBManager : RoomDatabase(){ abstract class DBManager : RoomDatabase(){

View file

@ -40,6 +40,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused','PausedReQueued')") @Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused','PausedReQueued')")
fun getActiveAndQueuedDownloads() : Flow<List<DownloadItem>> fun getActiveAndQueuedDownloads() : Flow<List<DownloadItem>>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") @Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id")
fun getQueuedDownloads() : PagingSource<Int, DownloadItemSimple> fun getQueuedDownloads() : PagingSource<Int, DownloadItemSimple>
@ -55,6 +56,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") @Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id")
fun getQueuedDownloadsList() : List<DownloadItem> fun getQueuedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloads() : PagingSource<Int, DownloadItemSimple> fun getCancelledDownloads() : PagingSource<Int, DownloadItemSimple>
@ -64,12 +66,14 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'") @Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'")
fun getPausedDownloadsList() : List<DownloadItem> fun getPausedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : PagingSource<Int, DownloadItemSimple> fun getErroredDownloads() : PagingSource<Int, DownloadItemSimple>
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloadsList() : List<DownloadItem> fun getErroredDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC")
fun getSavedDownloads() : PagingSource<Int, DownloadItemSimple> fun getSavedDownloads() : PagingSource<Int, DownloadItemSimple>

View file

@ -37,6 +37,6 @@ interface ResultDao {
suspend fun delete(id: Long) suspend fun delete(id: Long)
@Query("SELECT * FROM results WHERE url=:url LIMIT 1") @Query("SELECT * FROM results WHERE url=:url LIMIT 1")
fun getResultByURL(url: String) : ResultItem fun getResultByURL(url: String) : ResultItem?
} }

View file

@ -38,5 +38,9 @@ data class DownloadItem(
var status: String, var status: String,
@ColumnInfo(defaultValue = "0") @ColumnInfo(defaultValue = "0")
var downloadStartTime: Long, var downloadStartTime: Long,
var logID: Long? var logID: Long?,
@ColumnInfo(defaultValue = "")
var playlistURL: String? = "",
@ColumnInfo(defaultValue = "")
var playlistIndex: Int? = null
) : Parcelable ) : Parcelable

View file

@ -22,5 +22,9 @@ data class ResultItem(
@ColumnInfo(defaultValue = "") @ColumnInfo(defaultValue = "")
var urls: String, var urls: String,
var chapters: MutableList<ChapterItem>?, var chapters: MutableList<ChapterItem>?,
@ColumnInfo(defaultValue = "")
var playlistURL: String? = "",
@ColumnInfo(defaultValue = "")
var playlistIndex: Int? = null,
var creationTime: Long = System.currentTimeMillis() / 1000, var creationTime: Long = System.currentTimeMillis() / 1000,
) : Parcelable ) : Parcelable

View file

@ -52,7 +52,12 @@ class HistoryRepository(private val historyDao: HistoryDao) {
} }
} }
suspend fun deleteAll(){ suspend fun deleteAll(deleteFile: Boolean = false){
if (deleteFile){
historyDao.getAllHistoryList().forEach { item ->
FileUtil.deleteFile(item.downloadPath)
}
}
historyDao.deleteAll() historyDao.deleteAll()
} }

View file

@ -44,7 +44,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
deleteAll() deleteAll()
itemCount.value = v.size itemCount.value = v.size
}else{ }else{
v.forEach { it?.playlistTitle = "ytdlnis-Search" } v.filter { it?.playlistTitle.isNullOrBlank() }.forEach { it?.playlistTitle = "ytdlnis-Search" }
} }
resultDao.insertMultiple(v) resultDao.insertMultiple(v)
return ArrayList(v) return ArrayList(v)
@ -77,7 +77,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
deleteAll() deleteAll()
itemCount.value = items.size itemCount.value = items.size
}else{ }else{
items.forEach { it!!.playlistTitle = "ytdlnis-Search" } items.filter { it?.playlistTitle.isNullOrBlank() }.forEach { it!!.playlistTitle = "ytdlnis-Search" }
} }
resultDao.insertMultiple(items.toList()) resultDao.insertMultiple(items.toList())
return items return items
@ -96,7 +96,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
resultDao.update(item) resultDao.update(item)
} }
fun getItemByURL(url: String): ResultItem { fun getItemByURL(url: String): ResultItem? {
return resultDao.getResultByURL(url) return resultDao.getResultByURL(url)
} }
} }

View file

@ -152,15 +152,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
audioContainer = sharedPreferences.getString("audio_format", "mp3") audioContainer = sharedPreferences.getString("audio_format", "mp3")
bestAudioFormat = if (audioFormatIDPreference.isEmpty()){ bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
Format( infoUtil.getGenericAudioFormats(resources).last()
"best",
audioContainer!!,
"",
"",
"",
0,
"best"
)
}else{ }else{
Format( Format(
audioFormatIDPreference.first().split("+").first(), audioFormatIDPreference.first().split("+").first(),
@ -260,23 +252,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!)) val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!))
val preferredAudioFormats = arrayListOf<String>()
for (f in resultItem.formats.sortedBy { it.format_id }){
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
if (fId != null) {
if (fId.split("+").all { resultItem.formats.map { f-> f.format_id }.contains(it) }){
preferredAudioFormats.addAll(fId.split("+"))
break
}
}
}
if (preferredAudioFormats.isEmpty()){ val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats)
val audioF = getFormat(resultItem.formats, Type.audio)
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
preferredAudioFormats.add(audioF.format_id)
}
}
val videoPreferences = VideoPreferences( val videoPreferences = VideoPreferences(
embedSubs, embedSubs,
@ -303,7 +280,15 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
container!!, container!!,
"", "",
resultItem.formats, resultItem.formats,
downloadPath!!, resultItem.website, "", resultItem.playlistTitle, audioPreferences, videoPreferences, extraCommands, customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Queued.toString(), 0, null downloadPath!!, resultItem.website,
"",
resultItem.playlistTitle,
audioPreferences,
videoPreferences,
extraCommands,
customFileNameTemplate!!,
saveThumb,
DownloadRepository.Status.Queued.toString(), 0, null, playlistURL = resultItem.playlistURL, playlistIndex = resultItem.playlistIndex
) )
} }
@ -321,6 +306,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
downloadItem.allFormats, downloadItem.allFormats,
"", "",
arrayListOf(), arrayListOf(),
downloadItem.playlistURL,
downloadItem.playlistIndex,
System.currentTimeMillis() System.currentTimeMillis()
) )
@ -339,6 +326,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
arrayListOf(), arrayListOf(),
"", "",
arrayListOf(), arrayListOf(),
"",
null,
System.currentTimeMillis() System.currentTimeMillis()
) )
@ -357,6 +346,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
arrayListOf(), arrayListOf(),
"", "",
arrayListOf(), arrayListOf(),
"",
null,
System.currentTimeMillis() System.currentTimeMillis()
) )
@ -493,6 +484,26 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
} }
fun getPreferredAudioFormats(formats: List<Format>) : ArrayList<String>{
val preferredAudioFormats = arrayListOf<String>()
for (f in formats.sortedBy { it.format_id }){
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
if (fId != null) {
if (fId.split("+").all { formats.map { f-> f.format_id }.contains(it) }){
preferredAudioFormats.addAll(fId.split("+"))
break
}
}
}
if (preferredAudioFormats.isEmpty()){
val audioF = getFormat(formats, Type.audio)
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
preferredAudioFormats.add(audioF.format_id)
}
}
return preferredAudioFormats
}
fun generateCommandFormat(c: CommandTemplate) : Format { fun generateCommandFormat(c: CommandTemplate) : Format {
return Format( return Format(
c.title, c.title,

View file

@ -92,8 +92,8 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
repository.delete(item, deleteFile) repository.delete(item, deleteFile)
} }
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { fun deleteAll(deleteFile: Boolean = false) = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll() repository.deleteAll(deleteFile)
} }
fun deleteDuplicates() = viewModelScope.launch(Dispatchers.IO) { fun deleteDuplicates() = viewModelScope.launch(Dispatchers.IO) {

View file

@ -151,7 +151,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
repository.update(item) repository.update(item)
} }
fun getItemByURL(url: String) : ResultItem { fun getItemByURL(url: String) : ResultItem? {
return repository.getItemByURL(url) return repository.getItemByURL(url)
} }
@ -233,11 +233,12 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
val formats = infoUtil.getFormats(result.url) val formats = infoUtil.getFormats(result.url)
updatingFormats.emit(false) updatingFormats.emit(false)
if (formats.isNotEmpty() && isActive) { if (formats.isNotEmpty() && isActive) {
val res = getItemByURL(result.url) getItemByURL(result.url)?.apply {
res.formats = formats.toMutableList() this.formats = formats.toMutableList()
update(res) update(this)
}
updateFormatsResultData.emit(formats.toMutableList())
} }
updateFormatsResultData.emit(formats.toMutableList())
} }
updateFormatsResultDataJob?.start() updateFormatsResultDataJob?.start()
updateFormatsResultDataJob?.invokeOnCompletion { updateFormatsResultDataJob?.invokeOnCompletion {

View file

@ -74,7 +74,7 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity)
// TITLE ---------------------------------- // TITLE ----------------------------------
val videoTitle = card.findViewById<TextView>(R.id.result_title) val videoTitle = card.findViewById<TextView>(R.id.result_title)
var title = video!!.title var title = video!!.title.ifBlank { video.url }
if (title.length > 100) { if (title.length > 100) {
title = title.substring(0, 40) + "..." title = title.substring(0, 40) + "..."
} }

View file

@ -198,24 +198,27 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
val chosenFormat = downloadItem.format val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null) UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null)
val listener = object : OnFormatClickListener { val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>) { override fun onFormatClick(item: List<FormatTuple>) {
downloadItem.format = item.first().format downloadItem.format = item.first().format
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, null) UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, null)
lifecycleScope.launch { }
withContext(Dispatchers.IO){
resultItem?.formats?.removeAll(formats.toSet()) override fun onFormatsUpdated(allFormats: List<List<Format>>) {
resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) }) lifecycleScope.launch(Dispatchers.IO) {
if (resultItem != null){ resultItem?.formats?.removeAll(formats.toSet())
resultViewModel.update(resultItem!!) resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) })
kotlin.runCatching { if (resultItem != null){
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment resultViewModel.update(resultItem!!)
f1.updateUI(resultItem) kotlin.runCatching {
} val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f1.updateUI(resultItem)
} }
} }
} }
formats = allFormats.first().filter { !genericAudioFormats.contains(it) }.toMutableList() formats = allFormats.first().filter { !genericAudioFormats.contains(it) }.toMutableList()
formats.removeAll(genericAudioFormats) formats.removeAll(genericAudioFormats)
val preferredFormat = downloadViewModel.getFormat(formats, Type.audio)
UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat, null)
} }
} }
formatCard.setOnClickListener{ formatCard.setOnClickListener{

View file

@ -19,6 +19,7 @@ import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
@ -440,20 +441,42 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
delay(500) delay(500)
runCatching { runCatching {
val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.VISIBLE f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = true
isClickable = true
setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
resultViewModel.cancelUpdateFormatsItemData()
}
}
}
} }
runCatching { runCatching {
val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.VISIBLE f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = true
isClickable = true
setOnClickListener {
lifecycleScope.launch(Dispatchers.IO) {
resultViewModel.cancelUpdateFormatsItemData()
}
}
}
} }
}else{ }else{
runCatching { runCatching {
val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = false
isClickable = false
}
} }
runCatching { runCatching {
val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment
f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.visibility = View.GONE f1.view?.findViewById<LinearProgressIndicator>(R.id.format_loading_progress)?.apply {
isVisible = false
isClickable = false
}
} }
} }
} }
@ -569,12 +592,22 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
when(viewPager2.currentItem){ when(viewPager2.currentItem){
0 -> { 0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment kotlin.runCatching {
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author) val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
} }
1 -> { 1 -> {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment kotlin.runCatching {
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author) val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
}
2 -> {
kotlin.runCatching {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment
f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author)
}
} }
else -> {} else -> {}
} }

View file

@ -44,7 +44,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
class DownloadCommandFragment(private val resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "") : Fragment() { class DownloadCommandFragment(private val resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "") : Fragment(), GUISync {
private var fragmentView: View? = null private var fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
@ -296,4 +296,12 @@ class DownloadCommandFragment(private val resultItem: ResultItem? = null, privat
)) ))
} }
} }
override fun updateTitleAuthor(t: String, a: String) {
downloadItem.title = t
downloadItem.author = a
}
override fun updateUI(res: ResultItem?) {
}
} }

View file

@ -208,18 +208,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
val formatListener = object : OnFormatClickListener { val formatListener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, selectedFormats: List<FormatTuple>) { override fun onFormatClick(selectedFormats: List<FormatTuple>) {
val formatCollection = mutableListOf<List<Format>>()
allFormats.forEach {f ->
formatCollection.add(f.mapTo(mutableListOf()) {it.copy()})
}
items.forEachIndexed { index, i -> items.forEachIndexed { index, i ->
i.allFormats.clear()
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
runCatching {
i.allFormats.addAll(formatCollection[index])
}
}
i.format = selectedFormats[index].format i.format = selectedFormats[index].format
if (i.type == DownloadViewModel.Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) } if (i.type == DownloadViewModel.Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
} }
@ -229,6 +219,35 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
lifecycleScope.launch(Dispatchers.IO){
val formatCollection = mutableListOf<List<Format>>()
allFormats.forEach {f ->
formatCollection.add(f.mapTo(mutableListOf()) {it.copy()})
}
items.forEachIndexed { index, i ->
i.allFormats.clear()
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
runCatching {
i.allFormats.addAll(formatCollection[index])
}
}
i.format = downloadViewModel.getFormat(i.allFormats, i.type)
kotlin.runCatching {
val resultItem = resultViewModel.getItemByURL(i.url)!!
resultItem.formats = formatCollection[index].toMutableList()
resultViewModel.update(resultItem)
}
}
withContext(Dispatchers.Main){
listAdapter.submitList(items.toList())
listAdapter.notifyDataSetChanged()
updateFileSize(items.map { it.format.filesize })
}
}
}
override fun onContinueOnBackground() { override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch { requireActivity().lifecycleScope.launch {
val ids = mutableListOf<Long>() val ids = mutableListOf<Long>()
@ -637,7 +656,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
lifecycleScope.launch{ lifecycleScope.launch{
val downloadItem = items.find { it.url == itemURL } val downloadItem = items.find { it.url == itemURL }
val resultItem = withContext(Dispatchers.IO){ val resultItem = withContext(Dispatchers.IO){
resultViewModel.getItemByURL(downloadItem!!.url) resultViewModel.getItemByURL(downloadItem!!.url)!!
} }
if (parentFragmentManager.findFragmentByTag("configureDownloadSingleSheet") == null){ if (parentFragmentManager.findFragmentByTag("configureDownloadSingleSheet") == null){
val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem!!, this@DownloadMultipleBottomSheetDialog) val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem!!, this@DownloadMultipleBottomSheetDialog)

View file

@ -210,12 +210,17 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val chosenFormat = downloadItem.format val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) }) UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) })
val listener = object : OnFormatClickListener { val listener = object : OnFormatClickListener {
override fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>) { override fun onFormatClick(item: List<FormatTuple>) {
downloadItem.format = item.first().format downloadItem.format = item.first().format
item.first().audioFormats?.map { it.format_id }?.let { item.first().audioFormats?.map { it.format_id }?.let {
downloadItem.videoPreferences.audioFormatIDs.clear() downloadItem.videoPreferences.audioFormatIDs.clear()
downloadItem.videoPreferences.audioFormatIDs.addAll(it) downloadItem.videoPreferences.audioFormatIDs.addAll(it)
} }
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, item.first().audioFormats)
}
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
resultItem?.formats?.removeAll(formats) resultItem?.formats?.removeAll(formats)
@ -230,8 +235,11 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
} }
} }
formats = allFormats.first().filter { !genericVideoFormats.contains(it) }.toMutableList() formats = allFormats.first().filter { !genericVideoFormats.contains(it) }.toMutableList()
UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, item.first().audioFormats) val preferredFormat = downloadViewModel.getFormat(formats, Type.video)
val preferredAudioFormats = downloadViewModel.getPreferredAudioFormats(formats)
UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat, formats.filter { preferredAudioFormats.contains(it.format_id) })
} }
} }
formatCard.setOnClickListener{ formatCard.setOnClickListener{
if (parentFragmentManager.findFragmentByTag("formatSheet") == null){ if (parentFragmentManager.findFragmentByTag("formatSheet") == null){

View file

@ -24,6 +24,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.* import java.util.*
@ -48,6 +50,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private lateinit var continueInBackgroundSnackBar : Snackbar private lateinit var continueInBackgroundSnackBar : Snackbar
private lateinit var view: View private lateinit var view: View
private var updateFormatsJob: Job? = null
enum class FormatSorting { enum class FormatSorting {
filesize, container, codec, id filesize, container, codec, id
@ -88,7 +91,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
okBtn = view.findViewById(R.id.format_ok) okBtn = view.findViewById(R.id.format_ok)
shimmers.visibility = View.GONE shimmers.visibility = View.GONE
val hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" } val hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" || it.format_id == "ba" }
if (items.size > 1){ if (items.size > 1){
if (!hasGenericFormats){ if (!hasGenericFormats){
@ -141,56 +144,77 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
formatListLinearLayout.visibility = View.GONE formatListLinearLayout.visibility = View.GONE
shimmers.visibility = View.VISIBLE shimmers.visibility = View.VISIBLE
shimmers.startShimmer() shimmers.startShimmer()
updateFormatsJob = launch(Dispatchers.IO) {
//simple download
if (items.size == 1) {
val res = infoUtil.getFormats(items.first()!!.url)
res.filter { it.format_note != "storyboard" }
chosenFormats = if (items.first()?.type == Type.audio) {
res.filter { it.format_note.contains("audio", ignoreCase = true) }
} else {
res
}
if (chosenFormats.isEmpty()) throw Exception()
//simple download formats = listOf(res)
if (items.size == 1){ withContext(Dispatchers.Main){
val res = withContext(Dispatchers.IO){ listener.onFormatsUpdated(formats)
infoUtil.getFormats(items.first()!!.url) }
} //list format filtering
res.filter { it.format_note != "storyboard" }
chosenFormats = if(items.first()?.type == Type.audio){
res.filter { it.format_note.contains("audio", ignoreCase = true) }
}else{ }else{
res var progress = "0/${items.size}"
} formatCollection.clear()
if (chosenFormats.isEmpty()) throw Exception() withContext(Dispatchers.Main) {
refreshBtn.text = progress
formats = listOf(res) }
//list format filtering
}else{
var progress = "0/${items.size}"
formatCollection.clear()
refreshBtn.text = progress
withContext(Dispatchers.IO){
infoUtil.getFormatsMultiple(items.map { it!!.url }) { infoUtil.getFormatsMultiple(items.map { it!!.url }) {
lifecycleScope.launch(Dispatchers.Main){ if (isActive) {
progress = "${formatCollection.size}/${items.size}" lifecycleScope.launch(Dispatchers.Main) {
refreshBtn.text = progress formatCollection.add(it)
formatCollection.add(it) progress = "${formatCollection.size}/${items.size}"
refreshBtn.text = progress
}
} }
} }
formats = formatCollection
withContext(Dispatchers.Main){
listener.onFormatsUpdated(formats)
}
val flatFormatCollection = formatCollection.flatten()
val commonFormats =
flatFormatCollection.groupingBy { it.format_id }.eachCount()
.filter { it.value == items.size }
.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }
.map { it.value }
chosenFormats = commonFormats.filter { it.filesize != 0L }
.mapTo(mutableListOf()) { it.copy() }
chosenFormats = when (items.first()?.type) {
Type.audio -> chosenFormats.filter {
it.format_note.contains(
"audio",
ignoreCase = true
)
}
else -> chosenFormats
}
if (chosenFormats.isEmpty()) throw Exception()
chosenFormats.forEach {
it.filesize =
flatFormatCollection.filter { f -> f.format_id == it.format_id }
.sumOf { itt -> itt.filesize }
}
} }
formats = formatCollection withContext(Dispatchers.Main){
val flatFormatCollection = formatCollection.flatten() shimmers.visibility = View.GONE
val commonFormats = flatFormatCollection.groupingBy { it.format_id }.eachCount().filter { it.value == items.size }.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }.map { it.value } shimmers.stopShimmer()
chosenFormats = commonFormats.filter { it.filesize != 0L }.mapTo(mutableListOf()) {it.copy()} addFormatsToView()
chosenFormats = when(items.first()?.type){ refreshBtn.visibility = View.GONE
Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } formatListLinearLayout.visibility = View.VISIBLE
else -> chosenFormats
}
if (chosenFormats.isEmpty()) throw Exception()
chosenFormats.forEach {
it.filesize =
flatFormatCollection.filter { f -> f.format_id == it.format_id }
.sumOf { itt -> itt.filesize }
} }
} }
shimmers.visibility = View.GONE updateFormatsJob?.start()
shimmers.stopShimmer()
addFormatsToView()
refreshBtn.visibility = View.GONE
formatListLinearLayout.visibility = View.VISIBLE
}catch (e: Exception){ }catch (e: Exception){
runCatching { runCatching {
refreshBtn.isEnabled = true refreshBtn.isEnabled = true
@ -223,7 +247,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private fun returnFormats(){ private fun returnFormats(){
//simple video format selection //simple video format selection
if (items.size == 1){ if (items.size == 1){
listener.onFormatClick(formats, listOf(FormatTuple(selectedVideo, selectedAudios))) listener.onFormatClick(listOf(FormatTuple(selectedVideo, selectedAudios)))
}else{ }else{
//playlist format selection //playlist format selection
val selectedFormats = mutableListOf<Format>() val selectedFormats = mutableListOf<Format>()
@ -235,7 +259,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
selectedFormats.add(selectedVideo) selectedFormats.add(selectedVideo)
} }
} }
listener.onFormatClick(formats, selectedFormats.map { FormatTuple(it, selectedAudios) }) listener.onFormatClick(selectedFormats.map { FormatTuple(it, selectedAudios) })
} }
} }
@ -317,7 +341,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
} }
}else{ }else{
if (items.size == 1){ if (items.size == 1){
listener.onFormatClick(formats, listOf(FormatTuple(format, null))) listener.onFormatClick(listOf(FormatTuple(format, null)))
}else{ }else{
val selectedFormats = mutableListOf<Format>() val selectedFormats = mutableListOf<Format>()
formatCollection.forEach { formatCollection.forEach {
@ -328,7 +352,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
selectedFormats.add(format) selectedFormats.add(format)
} }
} }
listener.onFormatClick(formats, selectedFormats.map { FormatTuple(it, null) }) listener.onFormatClick(selectedFormats.map { FormatTuple(it, null) })
} }
dismiss() dismiss()
} }
@ -360,14 +384,16 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private fun cleanUp(){ private fun cleanUp(){
kotlin.runCatching { kotlin.runCatching {
updateFormatsJob?.cancel()
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("formatSheet")!!).commit() parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("formatSheet")!!).commit()
} }
} }
} }
interface OnFormatClickListener{ interface OnFormatClickListener{
fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>) fun onFormatClick(selectedFormats: List<FormatTuple>)
fun onContinueOnBackground() {} fun onContinueOnBackground() {}
fun onFormatsUpdated(allFormats: List<List<Format>>)
} }
class FormatTuple internal constructor( class FormatTuple internal constructor(

View file

@ -178,7 +178,7 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL
val checkedItems = listAdapter.getCheckedItems() val checkedItems = listAdapter.getCheckedItems()
val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) } val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) }
if (checkedResultItems.size == 1){ if (checkedResultItems.size == 1){
val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url) val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url)!!
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf( findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf(
Pair("result", resultItem), Pair("result", resultItem),

View file

@ -252,12 +252,16 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
if(allhistoryList!!.isEmpty()){ if(allhistoryList!!.isEmpty()){
Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show()
}else{ }else{
val deleteFile = booleanArrayOf(false)
val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
deleteDialog.setTitle(getString(R.string.confirm_delete_history)) deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setMessage(getString(R.string.confirm_delete_history_desc)) deleteDialog.setMultiChoiceItems(
arrayOf(getString(R.string.delete_files_too)),
booleanArrayOf(false)
) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
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 ->
historyViewModel.deleteAll() historyViewModel.deleteAll(deleteFile[0])
} }
deleteDialog.show() deleteDialog.show()
} }

View file

@ -1,10 +1,14 @@
package com.deniscerri.ytdlnis.util package com.deniscerri.ytdlnis.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Color import android.graphics.Color
import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape import android.graphics.drawable.shapes.OvalShape
import android.media.MediaMetadataRetriever
import android.media.MediaMetadataRetriever.METADATA_KEY_DURATION
import android.net.Uri
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@ -15,6 +19,7 @@ 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 me.zhanghai.android.fastscroll.FastScrollerBuilder import me.zhanghai.android.fastscroll.FastScrollerBuilder
import java.io.File
import java.util.regex.Pattern import java.util.regex.Pattern
@ -91,4 +96,14 @@ object Extensions {
.setTrackDrawable(drawable) .setTrackDrawable(drawable)
.build() .build()
} }
fun File.getMediaDuration(context: Context): Int {
if (!exists()) return 0
val retriever = MediaMetadataRetriever()
retriever.setDataSource(context, Uri.parse(absolutePath))
val duration = retriever.extractMetadata(METADATA_KEY_DURATION)
retriever.release()
return duration?.toIntOrNull()?.div(1000) ?: 0
}
} }

View file

@ -247,7 +247,7 @@ object FileUtil {
fun scanMedia(files: List<String>, context: Context) : List<String> { fun scanMedia(files: List<String>, context: Context) : List<String> {
try { try {
val paths = files.sortedByDescending { File(it).length() } val paths = files.distinct().sortedByDescending { File(it).length() }
runCatching { runCatching {
paths.forEach { paths.forEach {
MediaScannerConnection.scanFile(context, arrayOf(it), null, null) MediaScannerConnection.scanFile(context, arrayOf(it), null, null)

View file

@ -20,6 +20,7 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
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.CancellationException
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
@ -115,7 +116,9 @@ class InfoUtil(private val context: Context) {
kotlin.runCatching { kotlin.runCatching {
val obj = dataArray.getJSONObject(i) val obj = dataArray.getJSONObject(i)
val itm = createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url")) val itm = createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))
itm?.playlistTitle = res.getString("name") + "[${i+1}]" itm?.playlistTitle = res.getString("name")
itm?.playlistURL = "https://www.youtube.com/playlist?list=$id"
itm?.playlistIndex = (i+1)
items.add(itm) items.add(itm)
} }
} }
@ -432,6 +435,7 @@ class InfoUtil(private val context: Context) {
getFormatsFromYTDL(url) getFormatsFromYTDL(url)
} }
}catch (e: Exception){ }catch (e: Exception){
if (e is CancellationException) throw e
getFormatsFromYTDL(url) getFormatsFromYTDL(url)
} }
} }
@ -607,7 +611,9 @@ class InfoUtil(private val context: Context) {
youtubeDLResponse.out.split(lineSeparator!!) youtubeDLResponse.out.split(lineSeparator!!)
} catch (e: Exception) { } catch (e: Exception) {
listOf(youtubeDLResponse.out) listOf(youtubeDLResponse.out)
}.filter { it.isNotBlank() } }.filter { it.isNotBlank() }.apply {
if (this.isEmpty()) throw Exception("Command Used: \n yt-dlp ${parseYTDLRequestString(request)}")
}
for (result in results) { for (result in results) {
if (result.isNullOrBlank()) continue if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result) val jsonObject = JSONObject(result)
@ -641,12 +647,17 @@ class InfoUtil(private val context: Context) {
} }
} }
val website = jsonObject.getString(listOf("ie_key", "extractor_key", "extractor").first { jsonObject.has(it) }) val website = jsonObject.getString(listOf("ie_key", "extractor_key", "extractor").first { jsonObject.has(it) })
var playlistTitle: String? = "" var playlistTitle: String? = ""
var playlistURL: String? = ""
var playlistIndex: Int? = null
if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title") if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title")
if(playlistTitle?.removeSurrounding("\"").equals(query)) playlistTitle = "" if(playlistTitle?.removeSurrounding("\"").equals(query)) playlistTitle = ""
if (playlistTitle?.isNotBlank() == true){ if (playlistTitle?.isNotBlank() == true){
playlistTitle += "[${jsonObject.getString("playlist_index")}]" playlistURL = query
kotlin.runCatching { playlistIndex = jsonObject.getInt("playlist_index") }
} }
val formatsInJSON = if (jsonObject.has("formats") && jsonObject.get("formats") is JSONArray) jsonObject.getJSONArray("formats") else null val formatsInJSON = if (jsonObject.has("formats") && jsonObject.get("formats") is JSONArray) jsonObject.getJSONArray("formats") else null
@ -692,7 +703,9 @@ class InfoUtil(private val context: Context) {
playlistTitle!!, playlistTitle!!,
formats, formats,
urls, urls,
chapters chapters,
playlistURL,
playlistIndex
) )
) )
} }
@ -814,6 +827,8 @@ class InfoUtil(private val context: Context) {
arrayListOf(), arrayListOf(),
"", "",
arrayListOf(), arrayListOf(),
"",
null,
System.currentTimeMillis() System.currentTimeMillis()
) )
} }
@ -961,8 +976,15 @@ class InfoUtil(private val context: Context) {
} }
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val url = downloadItem.url val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank() || downloadItem.playlistIndex == null){
val request = YoutubeDLRequest(url) YoutubeDLRequest(downloadItem.url)
}else{
YoutubeDLRequest(downloadItem.playlistURL!!).apply {
addOption("--playlist-start", downloadItem.playlistIndex!!)
addOption("--playlist-end", downloadItem.playlistIndex!!)
}
}
val type = downloadItem.type val type = downloadItem.type
val downDir : File val downDir : File
@ -996,6 +1018,12 @@ class InfoUtil(private val context: Context) {
val limitRate = sharedPreferences.getString("limit_rate", "")!! val limitRate = sharedPreferences.getString("limit_rate", "")!!
if (limitRate.isNotBlank()) request.addOption("-r", limitRate) if (limitRate.isNotBlank()) request.addOption("-r", limitRate)
val bufferSize = sharedPreferences.getString("buffer_size", "")!!
if (bufferSize.isNotBlank()){
request.addOption("--buffer-size", bufferSize)
request.addOption("--no-resize-buffer")
}
val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!! val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!!
if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL) if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL)
@ -1019,6 +1047,8 @@ class InfoUtil(private val context: Context) {
val preferredAudioCodec = sharedPreferences.getString("audio_codec", "")!! val preferredAudioCodec = sharedPreferences.getString("audio_codec", "")!!
val aCodecPref = "ba[acodec~='^($preferredAudioCodec)']" val aCodecPref = "ba[acodec~='^($preferredAudioCodec)']"
val embedMetadata = sharedPreferences.getBoolean("embed_metadata", true)
var filenameTemplate = downloadItem.customFileNameTemplate
if(downloadItem.type != DownloadViewModel.Type.command){ if(downloadItem.type != DownloadViewModel.Type.command){
request.addOption("--trim-filenames", downDir.absolutePath.length + 117) request.addOption("--trim-filenames", downDir.absolutePath.length + 117)
@ -1075,9 +1105,9 @@ class InfoUtil(private val context: Context) {
request.addOption("--force-keyframes-at-cuts") request.addOption("--force-keyframes-at-cuts")
} }
} }
downloadItem.customFileNameTemplate += " %(section_title|)s " filenameTemplate += " %(section_title|)s "
if (downloadItem.downloadSections.split(";").size > 1){ if (downloadItem.downloadSections.split(";").size > 1){
downloadItem.customFileNameTemplate += "%(autonumber)s" filenameTemplate += "%(autonumber)s"
} }
request.addOption("--output-na-placeholder", " ") request.addOption("--output-na-placeholder", " ")
} }
@ -1102,18 +1132,14 @@ class InfoUtil(private val context: Context) {
request.addOption("--write-description") request.addOption("--write-description")
} }
downloadItem.customFileNameTemplate = downloadItem.customFileNameTemplate.replace("%(uploader)s", "%(uploader,channel)s") filenameTemplate = filenameTemplate.replace("%(uploader)s", "%(uploader,channel)s")
} }
if (downloadItem.playlistTitle.isNotBlank()){ if (downloadItem.playlistTitle.isNotBlank()){
request.addOption("--parse-metadata","${downloadItem.playlistTitle.split("[")[0]}:%(playlist)s") request.addOption("--parse-metadata","${downloadItem.playlistTitle}:%(playlist)s")
runCatching { runCatching {
request.addOption("--parse-metadata", request.addOption("--parse-metadata",
downloadItem.playlistTitle downloadItem.playlistIndex.toString() + ":%(playlist_index)s"
.substring(
downloadItem.playlistTitle.indexOf("[") + 1,
downloadItem.playlistTitle.indexOf("]"),
) + ":%(playlist_index)s"
) )
} }
} }
@ -1149,7 +1175,7 @@ class InfoUtil(private val context: Context) {
request.addOption("-o", "chapter:%(section_title)s.%(ext)s") request.addOption("-o", "chapter:%(section_title)s.%(ext)s")
}else{ }else{
if (sharedPreferences.getBoolean("embed_metadata", true)){ if (embedMetadata){
request.addOption("--embed-metadata") request.addOption("--embed-metadata")
request.addOption("--parse-metadata", "%(release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s") request.addOption("--parse-metadata", "%(release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s")
@ -1180,8 +1206,8 @@ class InfoUtil(private val context: Context) {
} }
} }
if (downloadItem.customFileNameTemplate.isNotBlank()){ if (filenameTemplate.isNotBlank()){
request.addOption("-o", "${downloadItem.customFileNameTemplate.removeSuffix(".%(ext)s")}.%(ext)s") request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
} }
} }
@ -1355,8 +1381,8 @@ class InfoUtil(private val context: Context) {
request.addOption("--split-chapters") request.addOption("--split-chapters")
request.addOption("-o", "chapter:%(section_title)s.%(ext)s") request.addOption("-o", "chapter:%(section_title)s.%(ext)s")
}else{ }else{
if (downloadItem.customFileNameTemplate.isNotBlank()){ if (filenameTemplate.isNotBlank()){
request.addOption("-o", "${downloadItem.customFileNameTemplate.removeSuffix(".%(ext)s")}.%(ext)s") request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
} }
} }
@ -1407,10 +1433,12 @@ class InfoUtil(private val context: Context) {
fun getGenericAudioFormats(resources: Resources) : MutableList<Format>{ fun getGenericAudioFormats(resources: Resources) : MutableList<Format>{
val audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() } val audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
val audioFormats = resources.getStringArray(R.array.audio_formats) val audioFormats = resources.getStringArray(R.array.audio_formats)
val audioFormatsValues = resources.getStringArray(R.array.audio_formats_values)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("audio_format", "") val containerPreference = sharedPreferences.getString("audio_format", "")
val acodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac") val acodecPreference = sharedPreferences.getString("audio_codec", "m4a|mp4a|aac")
audioFormats.reversed().forEach { formats.add(Format(it, containerPreference!!,"",acodecPreference!!, "",0, it)) } audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) }
audioFormats.reverse()
audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",acodecPreference!!, "",1, it)) } audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",acodecPreference!!, "",1, it)) }
return formats return formats
} }

View file

@ -32,6 +32,7 @@ import java.util.Locale
class NotificationUtil(var context: Context) { class NotificationUtil(var context: Context) {
private val downloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_SERVICE_CHANNEL_ID) private val downloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_SERVICE_CHANNEL_ID)
private val workerNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_WORKER_CHANNEL_ID)
private val commandDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID) private val commandDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID)
private val finishedDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_FINISHED_CHANNEL_ID) private val finishedDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_FINISHED_CHANNEL_ID)
private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java) private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java)
@ -97,6 +98,7 @@ class NotificationUtil(var context: Context) {
DOWNLOAD_SERVICE_CHANNEL_ID -> { return downloadNotificationBuilder} DOWNLOAD_SERVICE_CHANNEL_ID -> { return downloadNotificationBuilder}
COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID -> { return commandDownloadNotificationBuilder } COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID -> { return commandDownloadNotificationBuilder }
DOWNLOAD_FINISHED_CHANNEL_ID -> { return finishedDownloadNotificationBuilder } DOWNLOAD_FINISHED_CHANNEL_ID -> { return finishedDownloadNotificationBuilder }
DOWNLOAD_WORKER_CHANNEL_ID -> { return workerNotificationBuilder }
} }
return downloadNotificationBuilder return downloadNotificationBuilder
} }

View file

@ -27,6 +27,7 @@ import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.LogItem 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.repository.LogRepository
import com.deniscerri.ytdlnis.util.Extensions.getMediaDuration
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
@ -217,10 +218,36 @@ class DownloadWorker(
} }
}else{ }else{
val unixTime = System.currentTimeMillis() / 1000 val unixTime = System.currentTimeMillis() / 1000
val file = File(finalPaths?.first()!!) finalPaths?.forEachIndexed {idx, ff ->
downloadItem.format.filesize = file.length() val file = File(ff)
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, commandString) var index = ""
historyDao.insert(historyItem) var duration = downloadItem.duration
if (idx > 0) {
index = "[${idx + 1}] "
downloadItem.author = file.nameWithoutExtension
}
val d = file.getMediaDuration(context)
if (d > 0) duration = infoUtil.formatIntegerDuration(d, Locale.US)
downloadItem.format.filesize = file.length()
downloadItem.format.container = file.extension
downloadItem.duration = duration
val historyItem = HistoryItem(0,
downloadItem.url,
index + downloadItem.title,
downloadItem.author,
downloadItem.duration,
downloadItem.thumb,
downloadItem.type,
unixTime,
ff,
downloadItem.website,
downloadItem.format,
downloadItem.id,
commandString)
historyDao.insert(historyItem)
}
} }
} }

View file

@ -38,7 +38,7 @@ class UpdatePlaylistFormatsWorker(
ids.forEach { ids.forEach {
if (!isStopped){ if (!isStopped){
val d = dao.getDownloadById(it) val d = dao.getDownloadById(it)
val r = resDao.getResultByURL(d.url) val r = resDao.getResultByURL(d.url)!!
if (d.allFormats.isNotEmpty()){ if (d.allFormats.isNotEmpty()){
count++ count++

View file

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M5,2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v4L1,6v6h6L7,6L5,6L5,2zM9,16c0,1.3 0.84,2.4 2,2.82L11,23h2v-4.18c1.16,-0.41 2,-1.51 2,-2.82v-2L9,14v2zM1,16c0,1.3 0.84,2.4 2,2.82L3,23h2v-4.18C6.16,18.4 7,17.3 7,16v-2L1,14v2zM21,6L21,2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v4h-2v6h6L23,6h-2zM13,2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v4L9,6v6h6L15,6h-2L13,2zM17,16c0,1.3 0.84,2.4 2,2.82L19,23h2v-4.18c1.16,-0.41 2,-1.51 2,-2.82v-2h-6v2z"/>
</vector>

View file

@ -62,6 +62,11 @@
<item>best</item> <item>best</item>
</string-array> </string-array>
<string-array name="audio_formats_values">
<item>wa</item>
<item>ba</item>
</string-array>
<string-array name="format_ordering"> <string-array name="format_ordering">
<item>@string/file_size</item> <item>@string/file_size</item>
<item>@string/container</item> <item>@string/container</item>
@ -222,33 +227,29 @@
<array name="search_engines"> <array name="search_engines">
<item>YouTube</item> <item>YouTube</item>
<item>YouTube Music</item> <item>YouTube Music</item>
<item>Soundcloud</item>
<item>Bilibili</item>
<item>Nico video</item> <item>Nico video</item>
<item>PRX Series</item> <item>PRX Series</item>
<item>PRX Stories</item> <item>PRX Stories</item>
<item>Rokfin</item> <item>Rokfin</item>
<item>Yahoo screen</item> <item>Yahoo screen</item>
<item>Soundcloud</item>
<item>Google Video</item> <item>Google Video</item>
<item>Bilibili</item>
<item>Netverse</item> <item>Netverse</item>
<item>All Clips of a trovo.live channel</item>
<item>All VODs of a trovo.live channel</item>
</array> </array>
<array name="search_engines_values"> <array name="search_engines_values">
<item>ytsearch</item> <item>ytsearch</item>
<item>ytsearchmusic</item> <item>ytsearchmusic</item>
<item>scsearch</item>
<item>bilisearch</item>
<item>nicosearch</item> <item>nicosearch</item>
<item>PRX Series</item> <item>prxseries</item>
<item>PRX Stories</item> <item>prxstories</item>
<item>rkfnsearch</item> <item>rkfnsearch</item>
<item>yvsearch</item> <item>yvsearch</item>
<item>scsearch</item>
<item>gvsearch</item> <item>gvsearch</item>
<item>bilisearch</item>
<item>netsearch</item> <item>netsearch</item>
<item>trovoclip</item>
<item>trovovod</item>
</array> </array>
<array name="start_destination"> <array name="start_destination">

View file

@ -323,4 +323,6 @@
<string name="export_file">Export File</string> <string name="export_file">Export File</string>
<string name="ytdl_source">yt-dlp Source</string> <string name="ytdl_source">yt-dlp Source</string>
<string name="update_ytdl_master">Master Version of yt-dlp</string> <string name="update_ytdl_master">Master Version of yt-dlp</string>
<string name="buffer_size">Buffer Size</string>
<string name="buffer_size_summary">Size of download buffer, e.g. 1024 or 16K (default is 1024)</string>
</resources> </resources>

View file

@ -99,6 +99,12 @@
app:summary="@string/limit_rate_summary" app:summary="@string/limit_rate_summary"
app:title="@string/limit_rate" /> app:title="@string/limit_rate" />
<EditTextPreference
android:icon="@drawable/baseline_settings_input_component_24"
app:key="buffer_size"
app:summary="@string/buffer_size_summary"
app:title="@string/buffer_size" />
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false" app:defaultValue="false"