diff --git a/app/build.gradle b/app/build.gradle index 5fc7d7b7..b3e9cbec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -237,7 +237,7 @@ dependencies { implementation 'com.google.accompanist:accompanist-webview:0.36.0' implementation 'androidx.compose.material3:material3-android:1.4.0' implementation "io.noties.markwon:core:4.6.2" - implementation("com.github.teamnewpipe:newpipeextractor:-SNAPSHOT") + implementation("com.github.teamnewpipe:newpipeextractor:v0.26.3") implementation("commons-io:commons-io:2.5") implementation("org.apache.commons:commons-compress:1.12") diff --git a/app/schemas/com.deniscerri.ytdl.database.DBManager/28.json b/app/schemas/com.deniscerri.ytdl.database.DBManager/28.json new file mode 100644 index 00000000..a2ad9982 --- /dev/null +++ b/app/schemas/com.deniscerri.ytdl.database.DBManager/28.json @@ -0,0 +1,775 @@ +{ + "formatVersion": 1, + "database": { + "version": 28, + "identityHash": "a72cd9c71e75598a05be1225e0e6b541", + "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, `availableSubtitles` 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": "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" + }, + { + "fieldPath": "playlistURL", + "columnName": "playlistURL", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "playlistIndex", + "columnName": "playlistIndex", + "affinity": "INTEGER" + }, + { + "fieldPath": "creationTime", + "columnName": "creationTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "availableSubtitles", + "columnName": "availableSubtitles", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "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, `filesize` INTEGER NOT NULL DEFAULT 0, `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": "filesize", + "columnName": "filesize", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "downloadId", + "columnName": "downloadId", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "command", + "columnName": "command", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "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, `incognito` INTEGER NOT NULL DEFAULT 0, `availableSubtitles` TEXT NOT NULL DEFAULT '[]', `rowNumber` INTEGER NOT NULL, `queueOrder` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "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" + }, + { + "fieldPath": "playlistURL", + "columnName": "playlistURL", + "affinity": "TEXT", + "defaultValue": "''" + }, + { + "fieldPath": "playlistIndex", + "columnName": "playlistIndex", + "affinity": "INTEGER" + }, + { + "fieldPath": "incognito", + "columnName": "incognito", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "availableSubtitles", + "columnName": "availableSubtitles", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "rowNumber", + "columnName": "rowNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "queueOrder", + "columnName": "queueOrder", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "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, `useAsExtraCommandDataFetching` INTEGER NOT NULL DEFAULT 0, `preferredCommandTemplate` INTEGER NOT NULL DEFAULT 0, `urlRegex` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "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" + }, + { + "fieldPath": "useAsExtraCommandDataFetching", + "columnName": "useAsExtraCommandDataFetching", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "preferredCommandTemplate", + "columnName": "preferredCommandTemplate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "urlRegex", + "columnName": "urlRegex", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "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" + ] + } + }, + { + "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" + ] + } + }, + { + "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, `description` TEXT NOT NULL, `enabled` INTEGER 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 + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "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" + ] + } + }, + { + "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", + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + }, + { + "tableName": "sources", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `downloadItemTemplate` TEXT NOT NULL, `everyNr` INTEGER NOT NULL, `everyCategory` TEXT NOT NULL, `everyTime` INTEGER NOT NULL, `weeklyConfig` TEXT, `monthlyConfig` TEXT, `status` TEXT NOT NULL, `startsTime` INTEGER NOT NULL, `endsDate` INTEGER NOT NULL DEFAULT 0, `endsAfterCount` INTEGER NOT NULL DEFAULT 0, `runCount` INTEGER NOT NULL DEFAULT 0, `getOnlyNewUploads` INTEGER NOT NULL DEFAULT 0, `retryMissingDownloads` INTEGER NOT NULL, `ignoredLinks` TEXT NOT NULL DEFAULT '[]', `alreadyProcessedLinks` TEXT NOT NULL, `syncWithSource` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadItemTemplate", + "columnName": "downloadItemTemplate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "everyNr", + "columnName": "everyNr", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "everyCategory", + "columnName": "everyCategory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "everyTime", + "columnName": "everyTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weeklyConfig", + "columnName": "weeklyConfig", + "affinity": "TEXT" + }, + { + "fieldPath": "monthlyConfig", + "columnName": "monthlyConfig", + "affinity": "TEXT" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startsTime", + "columnName": "startsTime", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endsDate", + "columnName": "endsDate", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "endsAfterCount", + "columnName": "endsAfterCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "runCount", + "columnName": "runCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "getOnlyNewUploads", + "columnName": "getOnlyNewUploads", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "retryMissingDownloads", + "columnName": "retryMissingDownloads", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignoredLinks", + "columnName": "ignoredLinks", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'[]'" + }, + { + "fieldPath": "alreadyProcessedLinks", + "columnName": "alreadyProcessedLinks", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "syncWithSource", + "columnName": "syncWithSource", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "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, 'a72cd9c71e75598a05be1225e0e6b541')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/DBManager.kt b/app/src/main/java/com/deniscerri/ytdl/database/DBManager.kt index 9091a798..008c2910 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/DBManager.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/DBManager.kt @@ -40,7 +40,7 @@ import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem TerminalItem::class, ObserveSourcesItem::class ], - version = 27, + version = 28, autoMigrations = [ AutoMigration (from = 1, to = 2), AutoMigration (from = 2, to = 3), @@ -67,7 +67,8 @@ import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem //AutoMigration(from = 23, to = 24) MANUALLY HANDLED //AutoMigration(from = 24, to = 25) MANUALLY HANDLED //AutoMigration(from = 25, to = 26) MANUALLY HANDLED - AutoMigration(from = 26, to = 27) + AutoMigration(from = 26, to = 27), + AutoMigration(from = 27, to = 28) ] ) abstract class DBManager : RoomDatabase(){ diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt index 014c5f39..be06b14d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt @@ -36,7 +36,7 @@ interface DownloadDao { fun getPausedDownloadsList() : List @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM downloads WHERE status = 'Processing'") + @Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY queueOrder, id") fun getProcessingDownloads() : PagingSource @Query("SELECT id FROM downloads WHERE status = 'Processing'") @@ -81,7 +81,7 @@ interface DownloadDao { fun getFirstProcessingDownloadFlow() : Flow - @Query("SELECT * FROM downloads WHERE status = 'Processing'") + @Query("SELECT * FROM downloads WHERE status = 'Processing' ORDER BY queueOrder, id") fun getProcessingDownloadsList() : List @Query("UPDATE downloads set downloadPath=:path WHERE status ='Processing'") @@ -118,7 +118,7 @@ interface DownloadDao { fun getActiveAndQueuedDownloads() : Flow> @RewriteQueriesToDropUnusedColumns - @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY id") + @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY queueOrder, id") fun getQueuedDownloads() : PagingSource @Query("SELECT format FROM downloads WHERE status='Queued'") @@ -127,7 +127,7 @@ interface DownloadDao { @Query(""" SELECT * FROM downloads WHERE status in ('Queued', 'Scheduled') AND downloadStartTime <= :currentTime - ORDER BY downloadStartTime, id + ORDER BY downloadStartTime, queueOrder, id LIMIT 10 """) fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow> @@ -140,7 +140,7 @@ interface DownloadDao { WHEN id in (:priorityItems) THEN 0 ELSE 1 END, - downloadStartTime, id + downloadStartTime, queueOrder, id LIMIT 10 """) fun getQueuedScheduledDownloadsUntilWithPriority(currentTime: Long, priorityItems: List) : Flow> @@ -151,7 +151,7 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Scheduled' ORDER BY downloadStartTime, id") fun getScheduledDownloadsList() : List - @Query("SELECT id FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id") + @Query("SELECT id FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, queueOrder, id") fun getQueuedDownloadsListIDs() : List @RewriteQueriesToDropUnusedColumns @@ -327,59 +327,80 @@ interface DownloadDao { @Query("Update downloads SET status='Saved' WHERE status='Processing'") suspend fun updateProcessingtoSavedStatus() + @Query("SELECT COALESCE(MAX(queueOrder), 0) FROM downloads where status='Queued'") + suspend fun getLastQueueOrder() : Int + + @Query("SELECT COALESCE(MIN(queueOrder), 0) FROM downloads where status='Queued'") + suspend fun getFirstQueueOrder() : Int + + @Query("SELECT queueOrder FROM downloads where id=:id") + suspend fun getQueueOrder(id: Long) : Int + + @Query("UPDATE downloads set queueOrder = queueOrder + 1 WHERE id=:id") + suspend fun shiftQueueOrderDownForDownload(id: Long) + + @Query("UPDATE downloads set queueOrder = queueOrder - 1 WHERE id=:id") + suspend fun shiftQueueOrderUpForDownload(id: Long) + + @Query("UPDATE downloads set queueOrder=:queueOrder WHERE id=:id") + suspend fun updateQueueOrder(queueOrder: Int, id: Long) + @Transaction - suspend fun putAtTopOfTheQueue(existingIDs: List){ - val downloads = getQueuedDownloadsListIDs() - val newIDs = downloads.sortedBy { it }.take(existingIDs.size) - - resetScheduleTimeForItems(existingIDs) - existingIDs.forEach { updateDownloadID(it, -it) } - downloads.filter { !existingIDs.contains(it) }.toMutableList().apply { - this.reverse() - this.forEach { - updateDownloadID(it, it + existingIDs.size) - } + suspend fun reverseProcessingDownloads() { + val items = getProcessingDownloadsList() + var idx = 0 + items.reversed().forEach { + updateQueueOrder(idx, it.id) + idx++ } + } + + @Transaction + suspend fun putAtTopOfQueue(ids: List) { + val firstQueueOrder = getFirstQueueOrder() + + val downloads = getQueuedDownloadsListIDs() + resetScheduleTimeForItems(ids) - existingIDs.forEachIndexed { idx, it -> - updateDownloadID(-it, newIDs[idx]) + downloads.filter { !ids.contains(it) }.forEach { + shiftQueueOrderDownForDownload(it) + } + ids.forEach { it -> + updateQueueOrder(firstQueueOrder, it) } } @Transaction - suspend fun putAtBottomOfTheQueue(existingIDs: List){ + suspend fun putAtBottomOfQueue(ids: List) { + val lastQueueOrder = getLastQueueOrder() + val downloads = getQueuedDownloadsListIDs() - val newIDs = downloads.sortedByDescending { it }.take(existingIDs.size) - - resetScheduleTimeForItems(existingIDs) - existingIDs.forEach { updateDownloadID(it, -it) } - downloads.filter { !existingIDs.contains(it) }.toMutableList().apply { - this.reverse() - this.forEach { - updateDownloadID(it, it + existingIDs.size) - } - } + resetScheduleTimeForItems(ids) - existingIDs.forEachIndexed { idx, it -> - updateDownloadID(-it, newIDs[idx]) + downloads.filter { !ids.contains(it) }.forEach { + shiftQueueOrderUpForDownload(it) + } + ids.forEach { it -> + updateQueueOrder(lastQueueOrder, it) } } @Transaction - suspend fun reverseProcessingDownloads() { - val items = getProcessingDownloadsList() - val newIDs = items.reversed().map { it.id } - items.forEach { updateDownloadID(it.id, -(it.id)) } + suspend fun putQueueDownloadAtPosition(mutableIds: MutableList, startIdx: Int, endIdx: Int) { + val baseOrder = if (startIdx == 0) { + 0 + } else { + val previousItemId = mutableIds[startIdx - 1] + getQueueOrder(previousItemId) + 1 + } - items.forEachIndexed { idx, it -> - updateDownloadID(-(it.id), newIDs[idx]) - updateDownloadRowNumber(newIDs[idx], items.size - idx) + for (i in startIdx..endIdx) { + val idToUpdate = mutableIds[i] + val newOrder = baseOrder + (i - startIdx) + updateQueueOrder(newOrder, idToUpdate) } } - @Query("Update downloads set id=:newId where id=:id") - suspend fun updateDownloadID(id: Long, newId: Long) - @Query("Update downloads set rowNumber=:newNr where id=:id") suspend fun updateDownloadRowNumber(id: Long, newNr: Int) diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItem.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItem.kt index 7c7dc593..5d5ccdbf 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItem.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItem.kt @@ -47,5 +47,7 @@ data class DownloadItem( var incognito: Boolean = false, @ColumnInfo(defaultValue = "[]") var availableSubtitles: List = listOf(), - var rowNumber: Int = 0 + var rowNumber: Int = 0, + @ColumnInfo(defaultValue = "0") + var queueOrder: Int = 0 ) : Parcelable \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItemSimple.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItemSimple.kt index 8c1dd5bf..f42a3bc3 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItemSimple.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/models/DownloadItemSimple.kt @@ -22,5 +22,6 @@ data class DownloadItemSimple( var type: DownloadType, @ColumnInfo(defaultValue = "0") var downloadStartTime: Long, - var incognito: Boolean = false + var incognito: Boolean = false, + var queueOrder: Int = 0 ) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt index cad0d488..d97bc46f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt @@ -7,6 +7,7 @@ import android.content.res.Resources import android.os.Build import android.os.Parcelable import android.util.DisplayMetrics +import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData @@ -859,69 +860,32 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel repository.startDownloadWorker(emptyList(), application) } - suspend fun putAtTopOfQueue(ids: List) = CoroutineScope(Dispatchers.IO).launch{ - val downloads = dao.getQueuedDownloadsListIDs() - val lastID = ids.maxOf { it } - ids.forEach { dao.updateDownloadID(it, -it) } - val newIDs = downloads.take(ids.size) - - //other ids that need to move around - val takenPositions = mutableListOf() - downloads.filter { !ids.contains(it) && it < lastID }.toMutableList().apply { - this.reverse() - this.forEach { dID -> - val newID = downloads.last { !newIDs.contains(it) && !takenPositions.contains(it) && it <= lastID } - takenPositions.add(newID) - dao.updateDownloadID(dID, newID) - } - } - ids.forEachIndexed { idx, it -> - dao.updateDownloadID(-it, newIDs[idx]) - } + suspend fun putAtTopOfQueue(ids: List) { + dao.putAtTopOfQueue(ids) } + suspend fun putAtBottomOfQueue(ids: List) { + dao.putAtBottomOfQueue(ids) + } - suspend fun putAtBottomOfQueue(ids: List) = CoroutineScope(Dispatchers.IO).launch{ - val downloads = dao.getQueuedDownloadsListIDs() - ids.forEach { dao.updateDownloadID(it, -it)} - val newIDs = downloads.takeLast(ids.size) - //other ids that need to move around - val takenPositions = mutableListOf() - for (dID in downloads.filter { !ids.contains(it) }){ - val newID = downloads.first { !newIDs.contains(it) && !takenPositions.contains(it) } - takenPositions.add(newID) - dao.updateDownloadID(dID, newID) - } + fun putAtPosition(currentId: Long, targetId: Long) = CoroutineScope(Dispatchers.IO).launch { + if (currentId == targetId) return@launch - ids.toMutableList().apply { - this.reverse() - this.forEachIndexed { idx, it -> - dao.updateDownloadID(-it, newIDs[idx]) - } - } - } + val orderedIds = dao.getQueuedDownloadsListIDs() + val currentIndex = orderedIds.indexOf(currentId) + val targetIndex = orderedIds.indexOf(targetId) + if (currentIndex == -1 || targetIndex == -1) return@launch + val mutableIds = orderedIds.toMutableList() + mutableIds.removeAt(currentIndex) + mutableIds.add(targetIndex, currentId) - fun putAtPosition(current: Long, id: Long) = CoroutineScope(Dispatchers.IO).launch { - val downloads = dao.getQueuedDownloadsListIDs() - dao.updateDownloadID(current, -current) + //Figure out the bounds of what actually moved to optimize DB writes + val startIdx = minOf(currentIndex, targetIndex) + val endIdx = maxOf(currentIndex, targetIndex) - if (current > id){ - downloads.filter { it in id until current }.toMutableList().apply { - this.reverse() - this.forEach { dID -> - val index = downloads.indexOf(dID) - dao.updateDownloadID(dID, downloads[index + 1]) - } - } - }else{ - for (dID in downloads.filter { it in (current + 1)..id }){ - val index = downloads.indexOf(dID) - dao.updateDownloadID(dID, downloads[index - 1]) - } - } - dao.updateDownloadID(-current, id) + dao.putQueueDownloadAtPosition(mutableIds, startIdx, endIdx) } fun reQueueDownloadItems(items: List) = viewModelScope.launch(Dispatchers.IO) { @@ -960,6 +924,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel repository.getActiveAndQueuedDownloads() } + var lastQueueOrder = withContext(Dispatchers.IO) { + dao.getLastQueueOrder() + } + items.forEachIndexed { idx, it -> if (it.downloadStartTime > 0) { it.status = DownloadRepository.Status.Scheduled.toString() @@ -969,6 +937,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel if (it.rowNumber == 0 && items.size > 1) { it.rowNumber = idx + 1 } + it.queueOrder = lastQueueOrder + 1 + lastQueueOrder++ //CHECK DUPLICATES var isDuplicate = false diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/QueuedDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/QueuedDownloadAdapter.kt index 3196cf4d..08961b3e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/QueuedDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/QueuedDownloadAdapter.kt @@ -232,8 +232,6 @@ class QueuedDownloadAdapter(onItemClickListener: OnItemClickListener, activity: notifyDataSetChanged() } - - private fun checkCard(card: MaterialCardView, itemID: Long, position: Int) { if (card.isChecked) { card.strokeWidth = 0 @@ -265,7 +263,7 @@ class QueuedDownloadAdapter(onItemClickListener: OnItemClickListener, activity: } override fun areContentsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean { - return oldItem.id == newItem.id && oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.thumb == newItem.thumb + return oldItem.id == newItem.id && oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.thumb == newItem.thumb && oldItem.queueOrder == newItem.queueOrder } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt index f569c3a8..90354393 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/QueuedDownloadsFragment.kt @@ -431,13 +431,17 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis override fun onMoveQueuedItemToTop(itemID: Long) { lifecycleScope.launch { - downloadViewModel.putAtTopOfQueue(listOf(itemID)) + withContext(Dispatchers.IO) { + downloadViewModel.putAtTopOfQueue(listOf(itemID)) + } } } override fun onMoveQueuedItemToBottom(itemID: Long) { lifecycleScope.launch { - downloadViewModel.putAtBottomOfQueue(listOf(itemID)) + withContext(Dispatchers.IO) { + downloadViewModel.putAtBottomOfQueue(listOf(itemID)) + } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/YoutubePlayerClientFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/YoutubePlayerClientFragment.kt index efd56f3a..9948b282 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/YoutubePlayerClientFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/advanced/YoutubePlayerClientFragment.kt @@ -213,17 +213,17 @@ class YoutubePlayerClientFragment : Fragment(), YoutubePlayerClientAdapter.OnIte okBtn.setOnClickListener { val titleVal = title.editText!!.text.toString() if (titleVal.isBlank()) { - title.error = "Player Client tag shouldn't be empty" + title.error = getString(R.string.player_client_not_empty) return@setOnClickListener } if(existingConfigs.any { it2 -> it2.playerClient == titleVal && it2.enabled } && ( currentValue == null || currentValue.playerClient != titleVal )) { - title.error = "Player Client is already created" + title.error = getString(R.string.player_client_already_created) return@setOnClickListener } if (useOnlyPOToken.isChecked && (poTokenInputs.filter { it.tag.toString().startsWith("potoken") }.all { it.editText!!.text.isBlank() })) { - poTokenInputs.first().error = "You need to write at least one PO Token" + poTokenInputs.first().error = getString(R.string.write_atleast_one_potoken) return@setOnClickListener } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt index 4e5be09e..1f2537d7 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt @@ -56,8 +56,8 @@ class UpdateUtil(var context: Context) { var v: GithubRelease if (useBeta) { - v = res.firstOrNull { it.tag_name.contains("beta", true) && it.tag_name.contains("github", true) } ?: res.first() - val stableV = res.first { !it.tag_name.contains("beta", true) && it.tag_name.contains("github", true) } + v = res.firstOrNull { it.tag_name.contains("beta", true) } ?: res.first() + val stableV = res.first { !it.tag_name.contains("beta", true) } val incomingVerNumber = v.tag_name.removePrefix("v").tagNameToVersionNumber() val incomingStableVerNumber = stableV.tag_name.removePrefix("v").tagNameToVersionNumber() @@ -70,7 +70,7 @@ class UpdateUtil(var context: Context) { isInLatest = currentVerNumber >= incomingVerNumber } }else { - v = res.first { !it.tag_name.contains("beta", true) && it.tag_name.contains("github", true) } + v = res.first { !it.tag_name.contains("beta", true) } val incomingVerNumber = v.tag_name.removePrefix("v").tagNameToVersionNumber() //if current version is beta but wants to downgrade to stable, allow it diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 18b7d480..0b5a0de9 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -157,6 +157,7 @@ 조선말 latviešu بهاس ملايو + Mara Reih Norsk Bokmål Norsk Nynorsk पंजाबी @@ -203,6 +204,7 @@ ko lv ms + mrh nb nn pa diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 450b5c4f..d97574cb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -534,4 +534,7 @@ Y Offset Width Height + Player Client tag shouldn\'t be empty + Player Client is already created + You need to write at least one PO Token