From ef5b68ffc12ca003941f83a8d6445ba0e03cab46 Mon Sep 17 00:00:00 2001 From: zaednasr <75589932+zaednasr@users.noreply.github.com> Date: Sun, 26 Nov 2023 17:09:08 +0100 Subject: [PATCH] . --- .../13.json | 598 ++++++++++++++++++ .../deniscerri/ytdlnis/database/DBManager.kt | 5 +- .../ytdlnis/database/dao/DownloadDao.kt | 4 + .../ytdlnis/database/dao/ResultDao.kt | 2 +- .../ytdlnis/database/models/DownloadItem.kt | 6 +- .../ytdlnis/database/models/ResultItem.kt | 4 + .../database/repository/HistoryRepository.kt | 7 +- .../database/repository/ResultRepository.kt | 6 +- .../database/viewmodel/DownloadViewModel.kt | 63 +- .../database/viewmodel/HistoryViewModel.kt | 4 +- .../database/viewmodel/ResultViewModel.kt | 11 +- .../ytdlnis/ui/adapter/HomeAdapter.kt | 2 +- .../ui/downloadcard/DownloadAudioFragment.kt | 25 +- .../downloadcard/DownloadBottomSheetDialog.kt | 49 +- .../downloadcard/DownloadCommandFragment.kt | 10 +- .../DownloadMultipleBottomSheetDialog.kt | 43 +- .../ui/downloadcard/DownloadVideoFragment.kt | 12 +- .../FormatSelectionBottomSheetDialog.kt | 122 ++-- .../downloadcard/SelectPlaylistItemsDialog.kt | 2 +- .../ytdlnis/ui/downloads/HistoryFragment.kt | 10 +- .../com/deniscerri/ytdlnis/util/Extensions.kt | 15 + .../com/deniscerri/ytdlnis/util/FileUtil.kt | 2 +- .../com/deniscerri/ytdlnis/util/InfoUtil.kt | 70 +- .../ytdlnis/util/NotificationUtil.kt | 2 + .../deniscerri/ytdlnis/work/DownloadWorker.kt | 35 +- .../work/UpdatePlaylistFormatsWorker.kt | 2 +- .../baseline_settings_input_component_24.xml | 5 + app/src/main/res/values/arrays.xml | 21 +- app/src/main/res/values/strings.xml | 2 + .../main/res/xml/downloading_preferences.xml | 6 + 30 files changed, 980 insertions(+), 165 deletions(-) create mode 100644 app/schemas/com.deniscerri.ytdlnis.database.DBManager/13.json create mode 100644 app/src/main/res/drawable/baseline_settings_input_component_24.xml diff --git a/app/schemas/com.deniscerri.ytdlnis.database.DBManager/13.json b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/13.json new file mode 100644 index 00000000..5d7b601f --- /dev/null +++ b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/13.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt index 0b36f4db..d3e6cebd 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt @@ -23,7 +23,7 @@ import com.deniscerri.ytdlnis.database.models.* LogItem::class, TerminalItem::class ], - version = 12, + version = 13, autoMigrations = [ AutoMigration (from = 1, to = 2), AutoMigration (from = 2, to = 3), @@ -35,7 +35,8 @@ import com.deniscerri.ytdlnis.database.models.* AutoMigration (from = 8, to = 9), AutoMigration (from = 9, to = 10), AutoMigration (from = 10, to = 11), - AutoMigration (from = 11, to = 12) + AutoMigration (from = 11, to = 12), + AutoMigration (from = 12, to = 13) ] ) abstract class DBManager : RoomDatabase(){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt index f39b0e81..fc9206b9 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt @@ -40,6 +40,7 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status in('Active','Queued','QueuedPaused','ActivePaused','PausedReQueued')") fun getActiveAndQueuedDownloads() : Flow> + @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") fun getQueuedDownloads() : PagingSource @@ -55,6 +56,7 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status in('Queued','QueuedPaused') ORDER BY downloadStartTime, id") fun getQueuedDownloadsList() : List + @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") fun getCancelledDownloads() : PagingSource @@ -64,12 +66,14 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'") fun getPausedDownloadsList() : List + @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") fun getErroredDownloads() : PagingSource @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") fun getErroredDownloadsList() : List + @RewriteQueriesToDropUnusedColumns @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") fun getSavedDownloads() : PagingSource diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt index e4796154..d28d30ff 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt @@ -37,6 +37,6 @@ interface ResultDao { suspend fun delete(id: Long) @Query("SELECT * FROM results WHERE url=:url LIMIT 1") - fun getResultByURL(url: String) : ResultItem + fun getResultByURL(url: String) : ResultItem? } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt index 6fde4cbc..dd6d90c1 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt @@ -38,5 +38,9 @@ data class DownloadItem( var status: String, @ColumnInfo(defaultValue = "0") var downloadStartTime: Long, - var logID: Long? + var logID: Long?, + @ColumnInfo(defaultValue = "") + var playlistURL: String? = "", + @ColumnInfo(defaultValue = "") + var playlistIndex: Int? = null ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt index 35d0ed95..914b7e53 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt @@ -22,5 +22,9 @@ data class ResultItem( @ColumnInfo(defaultValue = "") var urls: String, var chapters: MutableList?, + @ColumnInfo(defaultValue = "") + var playlistURL: String? = "", + @ColumnInfo(defaultValue = "") + var playlistIndex: Int? = null, var creationTime: Long = System.currentTimeMillis() / 1000, ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt index 19362428..ffb8eca7 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt @@ -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() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt index 82a65f74..eaad76d0 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt @@ -44,7 +44,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co deleteAll() itemCount.value = v.size }else{ - v.forEach { it?.playlistTitle = "ytdlnis-Search" } + v.filter { it?.playlistTitle.isNullOrBlank() }.forEach { it?.playlistTitle = "ytdlnis-Search" } } resultDao.insertMultiple(v) return ArrayList(v) @@ -77,7 +77,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co deleteAll() itemCount.value = items.size }else{ - items.forEach { it!!.playlistTitle = "ytdlnis-Search" } + items.filter { it?.playlistTitle.isNullOrBlank() }.forEach { it!!.playlistTitle = "ytdlnis-Search" } } resultDao.insertMultiple(items.toList()) return items @@ -96,7 +96,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co resultDao.update(item) } - fun getItemByURL(url: String): ResultItem { + fun getItemByURL(url: String): ResultItem? { return resultDao.getResultByURL(url) } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt index fe2b53c1..9cf38281 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt @@ -152,15 +152,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application audioContainer = sharedPreferences.getString("audio_format", "mp3") bestAudioFormat = if (audioFormatIDPreference.isEmpty()){ - Format( - "best", - audioContainer!!, - "", - "", - "", - 0, - "best" - ) + infoUtil.getGenericAudioFormats(resources).last() }else{ Format( audioFormatIDPreference.first().split("+").first(), @@ -260,23 +252,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!)) - val preferredAudioFormats = arrayListOf() - 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 audioF = getFormat(resultItem.formats, Type.audio) - if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){ - preferredAudioFormats.add(audioF.format_id) - } - } + val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats) val videoPreferences = VideoPreferences( embedSubs, @@ -303,7 +280,15 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application container!!, "", 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, "", arrayListOf(), + downloadItem.playlistURL, + downloadItem.playlistIndex, System.currentTimeMillis() ) @@ -339,6 +326,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application arrayListOf(), "", arrayListOf(), + "", + null, System.currentTimeMillis() ) @@ -357,6 +346,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application arrayListOf(), "", arrayListOf(), + "", + null, System.currentTimeMillis() ) @@ -493,6 +484,26 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } } + fun getPreferredAudioFormats(formats: List) : ArrayList{ + val preferredAudioFormats = arrayListOf() + 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 { return Format( c.title, diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt index 91be95f5..c38a6b97 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt @@ -92,8 +92,8 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application) repository.delete(item, deleteFile) } - fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { - repository.deleteAll() + fun deleteAll(deleteFile: Boolean = false) = viewModelScope.launch(Dispatchers.IO) { + repository.deleteAll(deleteFile) } fun deleteDuplicates() = viewModelScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt index e3df1d87..c31c0ad4 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt @@ -151,7 +151,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) repository.update(item) } - fun getItemByURL(url: String) : ResultItem { + fun getItemByURL(url: String) : ResultItem? { return repository.getItemByURL(url) } @@ -233,11 +233,12 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) val formats = infoUtil.getFormats(result.url) updatingFormats.emit(false) if (formats.isNotEmpty() && isActive) { - val res = getItemByURL(result.url) - res.formats = formats.toMutableList() - update(res) + getItemByURL(result.url)?.apply { + this.formats = formats.toMutableList() + update(this) + } + updateFormatsResultData.emit(formats.toMutableList()) } - updateFormatsResultData.emit(formats.toMutableList()) } updateFormatsResultDataJob?.start() updateFormatsResultDataJob?.invokeOnCompletion { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HomeAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HomeAdapter.kt index aa1c9e88..0a121eef 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HomeAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HomeAdapter.kt @@ -74,7 +74,7 @@ class HomeAdapter(onItemClickListener: OnItemClickListener, activity: Activity) // TITLE ---------------------------------- val videoTitle = card.findViewById(R.id.result_title) - var title = video!!.title + var title = video!!.title.ifBlank { video.url } if (title.length > 100) { title = title.substring(0, 40) + "..." } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt index 848b74d6..2b79af61 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt @@ -198,24 +198,27 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private val chosenFormat = downloadItem.format UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null) val listener = object : OnFormatClickListener { - override fun onFormatClick(allFormats: List>, item: List) { + override fun onFormatClick(item: List) { downloadItem.format = item.first().format UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, null) - lifecycleScope.launch { - withContext(Dispatchers.IO){ - resultItem?.formats?.removeAll(formats.toSet()) - resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) }) - if (resultItem != null){ - resultViewModel.update(resultItem!!) - kotlin.runCatching { - val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment - f1.updateUI(resultItem) - } + } + + override fun onFormatsUpdated(allFormats: List>) { + lifecycleScope.launch(Dispatchers.IO) { + resultItem?.formats?.removeAll(formats.toSet()) + resultItem?.formats?.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) }) + if (resultItem != null){ + resultViewModel.update(resultItem!!) + kotlin.runCatching { + val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment + f1.updateUI(resultItem) } } } formats = allFormats.first().filter { !genericAudioFormats.contains(it) }.toMutableList() formats.removeAll(genericAudioFormats) + val preferredFormat = downloadViewModel.getFormat(formats, Type.audio) + UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat, null) } } formatCard.setOnClickListener{ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt index b0f4c73c..873fd930 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt @@ -19,6 +19,7 @@ import android.widget.LinearLayout import android.widget.Toast import androidx.core.content.edit import androidx.core.os.bundleOf +import androidx.core.view.isVisible import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController @@ -440,20 +441,42 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { delay(500) runCatching { val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment - f1.view?.findViewById(R.id.format_loading_progress)?.visibility = View.VISIBLE + f1.view?.findViewById(R.id.format_loading_progress)?.apply { + isVisible = true + isClickable = true + setOnClickListener { + lifecycleScope.launch(Dispatchers.IO) { + resultViewModel.cancelUpdateFormatsItemData() + } + } + } } runCatching { val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment - f1.view?.findViewById(R.id.format_loading_progress)?.visibility = View.VISIBLE + f1.view?.findViewById(R.id.format_loading_progress)?.apply { + isVisible = true + isClickable = true + setOnClickListener { + lifecycleScope.launch(Dispatchers.IO) { + resultViewModel.cancelUpdateFormatsItemData() + } + } + } } }else{ runCatching { val f1 = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment - f1.view?.findViewById(R.id.format_loading_progress)?.visibility = View.GONE + f1.view?.findViewById(R.id.format_loading_progress)?.apply { + isVisible = false + isClickable = false + } } runCatching { val f1 = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment - f1.view?.findViewById(R.id.format_loading_progress)?.visibility = View.GONE + f1.view?.findViewById(R.id.format_loading_progress)?.apply { + isVisible = false + isClickable = false + } } } } @@ -569,12 +592,22 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { when(viewPager2.currentItem){ 0 -> { - val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment - f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author) + kotlin.runCatching { + val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment + f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author) + } } 1 -> { - val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment - f.updateTitleAuthor(prevDownloadItem.title, prevDownloadItem.author) + kotlin.runCatching { + 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 -> {} } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt index 5496708a..ae3f1cfc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt @@ -44,7 +44,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext 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 activity: Activity? = null 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?) { + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt index 807955d6..3a43fca2 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt @@ -208,18 +208,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure } val formatListener = object : OnFormatClickListener { - override fun onFormatClick(allFormats: List>, selectedFormats: List) { - val formatCollection = mutableListOf>() - allFormats.forEach {f -> - formatCollection.add(f.mapTo(mutableListOf()) {it.copy()}) - } + override fun onFormatClick(selectedFormats: List) { 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 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>) { + lifecycleScope.launch(Dispatchers.IO){ + val formatCollection = mutableListOf>() + 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() { requireActivity().lifecycleScope.launch { val ids = mutableListOf() @@ -637,7 +656,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure lifecycleScope.launch{ val downloadItem = items.find { it.url == itemURL } val resultItem = withContext(Dispatchers.IO){ - resultViewModel.getItemByURL(downloadItem!!.url) + resultViewModel.getItemByURL(downloadItem!!.url)!! } if (parentFragmentManager.findFragmentByTag("configureDownloadSingleSheet") == null){ val bottomSheet = ConfigureDownloadBottomSheetDialog(resultItem, downloadItem!!, this@DownloadMultipleBottomSheetDialog) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt index cc958c87..ad3fd511 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt @@ -210,12 +210,17 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private val chosenFormat = downloadItem.format UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) }) val listener = object : OnFormatClickListener { - override fun onFormatClick(allFormats: List>, item: List) { + override fun onFormatClick(item: List) { downloadItem.format = item.first().format item.first().audioFormats?.map { it.format_id }?.let { downloadItem.videoPreferences.audioFormatIDs.clear() downloadItem.videoPreferences.audioFormatIDs.addAll(it) } + UiUtil.populateFormatCard(requireContext(), formatCard, item.first().format, item.first().audioFormats) + + } + + override fun onFormatsUpdated(allFormats: List>) { lifecycleScope.launch { withContext(Dispatchers.IO){ resultItem?.formats?.removeAll(formats) @@ -230,8 +235,11 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private } } 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{ if (parentFragmentManager.findFragmentByTag("formatSheet") == null){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index 5681a242..3f6792c7 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -24,6 +24,8 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.card.MaterialCardView import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.* @@ -48,6 +50,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p private lateinit var continueInBackgroundSnackBar : Snackbar private lateinit var view: View + private var updateFormatsJob: Job? = null enum class FormatSorting { filesize, container, codec, id @@ -88,7 +91,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p okBtn = view.findViewById(R.id.format_ok) 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 (!hasGenericFormats){ @@ -141,56 +144,77 @@ class FormatSelectionBottomSheetDialog(private val items: List, p formatListLinearLayout.visibility = View.GONE shimmers.visibility = View.VISIBLE 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 - if (items.size == 1){ - val res = withContext(Dispatchers.IO){ - 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) } + formats = listOf(res) + withContext(Dispatchers.Main){ + listener.onFormatsUpdated(formats) + } + //list format filtering }else{ - res - } - if (chosenFormats.isEmpty()) throw Exception() - - formats = listOf(res) - - //list format filtering - }else{ - var progress = "0/${items.size}" - formatCollection.clear() - refreshBtn.text = progress - withContext(Dispatchers.IO){ + var progress = "0/${items.size}" + formatCollection.clear() + withContext(Dispatchers.Main) { + refreshBtn.text = progress + } infoUtil.getFormatsMultiple(items.map { it!!.url }) { - lifecycleScope.launch(Dispatchers.Main){ - progress = "${formatCollection.size}/${items.size}" - refreshBtn.text = progress - formatCollection.add(it) + if (isActive) { + lifecycleScope.launch(Dispatchers.Main) { + 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 - 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 } + withContext(Dispatchers.Main){ + shimmers.visibility = View.GONE + shimmers.stopShimmer() + addFormatsToView() + refreshBtn.visibility = View.GONE + formatListLinearLayout.visibility = View.VISIBLE } } - shimmers.visibility = View.GONE - shimmers.stopShimmer() - addFormatsToView() - refreshBtn.visibility = View.GONE - formatListLinearLayout.visibility = View.VISIBLE + updateFormatsJob?.start() }catch (e: Exception){ runCatching { refreshBtn.isEnabled = true @@ -223,7 +247,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p private fun returnFormats(){ //simple video format selection if (items.size == 1){ - listener.onFormatClick(formats, listOf(FormatTuple(selectedVideo, selectedAudios))) + listener.onFormatClick(listOf(FormatTuple(selectedVideo, selectedAudios))) }else{ //playlist format selection val selectedFormats = mutableListOf() @@ -235,7 +259,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p 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, p } }else{ if (items.size == 1){ - listener.onFormatClick(formats, listOf(FormatTuple(format, null))) + listener.onFormatClick(listOf(FormatTuple(format, null))) }else{ val selectedFormats = mutableListOf() formatCollection.forEach { @@ -328,7 +352,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p selectedFormats.add(format) } } - listener.onFormatClick(formats, selectedFormats.map { FormatTuple(it, null) }) + listener.onFormatClick(selectedFormats.map { FormatTuple(it, null) }) } dismiss() } @@ -360,14 +384,16 @@ class FormatSelectionBottomSheetDialog(private val items: List, p private fun cleanUp(){ kotlin.runCatching { + updateFormatsJob?.cancel() parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("formatSheet")!!).commit() } } } interface OnFormatClickListener{ - fun onFormatClick(allFormats: List>, item: List) + fun onFormatClick(selectedFormats: List) fun onContinueOnBackground() {} + fun onFormatsUpdated(allFormats: List>) } class FormatTuple internal constructor( diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/SelectPlaylistItemsDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/SelectPlaylistItemsDialog.kt index 8181e370..d5f9c77b 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/SelectPlaylistItemsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/SelectPlaylistItemsDialog.kt @@ -178,7 +178,7 @@ class SelectPlaylistItemsDialog : DialogFragment(), PlaylistAdapter.OnItemClickL val checkedItems = listAdapter.getCheckedItems() val checkedResultItems = items.filter { item -> checkedItems.contains(item!!.url) } if (checkedResultItems.size == 1){ - val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url) + val resultItem = resultViewModel.getItemByURL(checkedResultItems[0]!!.url)!! withContext(Dispatchers.Main){ findNavController().navigate(R.id.action_selectPlaylistItemsDialog_to_downloadBottomSheetDialog, bundleOf( Pair("result", resultItem), diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt index 317c7aad..8f991b46 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/HistoryFragment.kt @@ -252,12 +252,16 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ if(allhistoryList!!.isEmpty()){ Toast.makeText(context, R.string.history_is_empty, Toast.LENGTH_SHORT).show() }else{ + val deleteFile = booleanArrayOf(false) val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) - deleteDialog.setTitle(getString(R.string.confirm_delete_history)) - deleteDialog.setMessage(getString(R.string.confirm_delete_history_desc)) + deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items)) + 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.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> - historyViewModel.deleteAll() + historyViewModel.deleteAll(deleteFile[0]) } deleteDialog.show() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt index 1b8a5efa..14e672fd 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt @@ -1,10 +1,14 @@ package com.deniscerri.ytdlnis.util import android.annotation.SuppressLint +import android.content.Context import android.content.res.Resources import android.graphics.Color import android.graphics.drawable.ShapeDrawable 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.view.MotionEvent 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.scheme.ColorScheme import me.zhanghai.android.fastscroll.FastScrollerBuilder +import java.io.File import java.util.regex.Pattern @@ -91,4 +96,14 @@ object Extensions { .setTrackDrawable(drawable) .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 + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt index 5f4309e0..957a0345 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt @@ -247,7 +247,7 @@ object FileUtil { fun scanMedia(files: List, context: Context) : List { try { - val paths = files.sortedByDescending { File(it).length() } + val paths = files.distinct().sortedByDescending { File(it).length() } runCatching { paths.forEach { MediaScannerConnection.scanFile(context, arrayOf(it), null, null) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt index 778de788..ce8576b2 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt @@ -20,6 +20,7 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDLRequest +import kotlinx.coroutines.CancellationException import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -115,7 +116,9 @@ class InfoUtil(private val context: Context) { kotlin.runCatching { val obj = dataArray.getJSONObject(i) 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) } } @@ -432,6 +435,7 @@ class InfoUtil(private val context: Context) { getFormatsFromYTDL(url) } }catch (e: Exception){ + if (e is CancellationException) throw e getFormatsFromYTDL(url) } } @@ -607,7 +611,9 @@ class InfoUtil(private val context: Context) { youtubeDLResponse.out.split(lineSeparator!!) } catch (e: Exception) { 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) { if (result.isNullOrBlank()) continue 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) }) + var playlistTitle: String? = "" + var playlistURL: String? = "" + var playlistIndex: Int? = null + if (jsonObject.has("playlist_title")) playlistTitle = jsonObject.getString("playlist_title") if(playlistTitle?.removeSurrounding("\"").equals(query)) playlistTitle = "" 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 @@ -692,7 +703,9 @@ class InfoUtil(private val context: Context) { playlistTitle!!, formats, urls, - chapters + chapters, + playlistURL, + playlistIndex ) ) } @@ -814,6 +827,8 @@ class InfoUtil(private val context: Context) { arrayListOf(), "", arrayListOf(), + "", + null, System.currentTimeMillis() ) } @@ -961,8 +976,15 @@ class InfoUtil(private val context: Context) { } fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ - val url = downloadItem.url - val request = YoutubeDLRequest(url) + val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank() || downloadItem.playlistIndex == null){ + YoutubeDLRequest(downloadItem.url) + }else{ + YoutubeDLRequest(downloadItem.playlistURL!!).apply { + addOption("--playlist-start", downloadItem.playlistIndex!!) + addOption("--playlist-end", downloadItem.playlistIndex!!) + } + } + val type = downloadItem.type val downDir : File @@ -996,6 +1018,12 @@ class InfoUtil(private val context: Context) { val limitRate = sharedPreferences.getString("limit_rate", "")!! 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", "")!! 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 aCodecPref = "ba[acodec~='^($preferredAudioCodec)']" + val embedMetadata = sharedPreferences.getBoolean("embed_metadata", true) + var filenameTemplate = downloadItem.customFileNameTemplate if(downloadItem.type != DownloadViewModel.Type.command){ request.addOption("--trim-filenames", downDir.absolutePath.length + 117) @@ -1075,9 +1105,9 @@ class InfoUtil(private val context: Context) { request.addOption("--force-keyframes-at-cuts") } } - downloadItem.customFileNameTemplate += " %(section_title|)s " + filenameTemplate += " %(section_title|)s " if (downloadItem.downloadSections.split(";").size > 1){ - downloadItem.customFileNameTemplate += "%(autonumber)s" + filenameTemplate += "%(autonumber)s" } request.addOption("--output-na-placeholder", " ") } @@ -1102,18 +1132,14 @@ class InfoUtil(private val context: Context) { 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()){ - request.addOption("--parse-metadata","${downloadItem.playlistTitle.split("[")[0]}:%(playlist)s") + request.addOption("--parse-metadata","${downloadItem.playlistTitle}:%(playlist)s") runCatching { request.addOption("--parse-metadata", - downloadItem.playlistTitle - .substring( - downloadItem.playlistTitle.indexOf("[") + 1, - downloadItem.playlistTitle.indexOf("]"), - ) + ":%(playlist_index)s" + downloadItem.playlistIndex.toString() + ":%(playlist_index)s" ) } } @@ -1149,7 +1175,7 @@ class InfoUtil(private val context: Context) { request.addOption("-o", "chapter:%(section_title)s.%(ext)s") }else{ - if (sharedPreferences.getBoolean("embed_metadata", true)){ + if (embedMetadata){ request.addOption("--embed-metadata") 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()){ - request.addOption("-o", "${downloadItem.customFileNameTemplate.removeSuffix(".%(ext)s")}.%(ext)s") + if (filenameTemplate.isNotBlank()){ + 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("-o", "chapter:%(section_title)s.%(ext)s") }else{ - if (downloadItem.customFileNameTemplate.isNotBlank()){ - request.addOption("-o", "${downloadItem.customFileNameTemplate.removeSuffix(".%(ext)s")}.%(ext)s") + if (filenameTemplate.isNotBlank()){ + request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s") } } @@ -1407,10 +1433,12 @@ class InfoUtil(private val context: Context) { fun getGenericAudioFormats(resources: Resources) : MutableList{ val audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() } val audioFormats = resources.getStringArray(R.array.audio_formats) + val audioFormatsValues = resources.getStringArray(R.array.audio_formats_values) val formats = mutableListOf() val containerPreference = sharedPreferences.getString("audio_format", "") 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)) } return formats } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt index 933a25f4..81d2be89 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt @@ -32,6 +32,7 @@ import java.util.Locale class NotificationUtil(var context: Context) { 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 finishedDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_FINISHED_CHANNEL_ID) private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java) @@ -97,6 +98,7 @@ class NotificationUtil(var context: Context) { DOWNLOAD_SERVICE_CHANNEL_ID -> { return downloadNotificationBuilder} COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID -> { return commandDownloadNotificationBuilder } DOWNLOAD_FINISHED_CHANNEL_ID -> { return finishedDownloadNotificationBuilder } + DOWNLOAD_WORKER_CHANNEL_ID -> { return workerNotificationBuilder } } return downloadNotificationBuilder } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt index 7cad603c..95d75ef0 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt @@ -27,6 +27,7 @@ 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.LogRepository +import com.deniscerri.ytdlnis.util.Extensions.getMediaDuration import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.NotificationUtil @@ -217,10 +218,36 @@ class DownloadWorker( } }else{ val unixTime = System.currentTimeMillis() / 1000 - val file = File(finalPaths?.first()!!) - 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, commandString) - historyDao.insert(historyItem) + finalPaths?.forEachIndexed {idx, ff -> + val file = File(ff) + var index = "" + 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) + } + } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/UpdatePlaylistFormatsWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/UpdatePlaylistFormatsWorker.kt index b3cb6648..ebcab815 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/UpdatePlaylistFormatsWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/UpdatePlaylistFormatsWorker.kt @@ -38,7 +38,7 @@ class UpdatePlaylistFormatsWorker( ids.forEach { if (!isStopped){ val d = dao.getDownloadById(it) - val r = resDao.getResultByURL(d.url) + val r = resDao.getResultByURL(d.url)!! if (d.allFormats.isNotEmpty()){ count++ diff --git a/app/src/main/res/drawable/baseline_settings_input_component_24.xml b/app/src/main/res/drawable/baseline_settings_input_component_24.xml new file mode 100644 index 00000000..f85a6381 --- /dev/null +++ b/app/src/main/res/drawable/baseline_settings_input_component_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index fb0ebfaa..1dbdb8dc 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -62,6 +62,11 @@ best + + wa + ba + + @string/file_size @string/container @@ -222,33 +227,29 @@ YouTube YouTube Music + Soundcloud + Bilibili Nico video PRX Series PRX Stories Rokfin Yahoo screen - Soundcloud Google Video - Bilibili Netverse - All Clips of a trovo.live channel - All VODs of a trovo.live channel ytsearch ytsearchmusic + scsearch + bilisearch nicosearch - PRX Series - PRX Stories + prxseries + prxstories rkfnsearch yvsearch - scsearch gvsearch - bilisearch netsearch - trovoclip - trovovod diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0d7f32d2..cab773c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -323,4 +323,6 @@ Export File yt-dlp Source Master Version of yt-dlp + Buffer Size + Size of download buffer, e.g. 1024 or 16K (default is 1024) \ No newline at end of file diff --git a/app/src/main/res/xml/downloading_preferences.xml b/app/src/main/res/xml/downloading_preferences.xml index 935edb3c..b83fda68 100644 --- a/app/src/main/res/xml/downloading_preferences.xml +++ b/app/src/main/res/xml/downloading_preferences.xml @@ -99,6 +99,12 @@ app:summary="@string/limit_rate_summary" app:title="@string/limit_rate" /> + +