From 6c96e390a6411ca2dd94dcb4c3b11be88837f282 Mon Sep 17 00:00:00 2001 From: zaednasr <75589932+zaednasr@users.noreply.github.com> Date: Sun, 9 Jun 2024 19:24:39 +0200 Subject: [PATCH] 1.7.6 --- app/build.gradle | 4 +- .../17.json | 725 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 1 + .../com/deniscerri/ytdl/database/DBManager.kt | 5 +- .../ytdl/database/dao/DownloadDao.kt | 12 + .../deniscerri/ytdl/database/dao/ResultDao.kt | 3 + .../database/models/DownloadItemSimple.kt | 1 + .../observeSources/ObserveSourcesItem.kt | 2 + .../database/repository/DownloadRepository.kt | 21 +- .../database/repository/ResultRepository.kt | 5 + .../database/viewmodel/CookieViewModel.kt | 3 + .../database/viewmodel/DownloadViewModel.kt | 114 +-- .../viewmodel/SharedDownloadViewModel.kt | 6 +- .../deniscerri/ytdl/receiver/ShareActivity.kt | 2 +- .../ProcessDownloadsInBackgroundService.kt | 26 +- .../com/deniscerri/ytdl/ui/BaseActivity.kt | 38 +- .../com/deniscerri/ytdl/ui/HomeFragment.kt | 9 +- .../ytdl/ui/adapter/ActiveDownloadAdapter.kt | 6 +- .../adapter/ActiveDownloadMinifiedAdapter.kt | 4 +- .../ytdl/ui/adapter/ChangelogAdapter.kt | 107 +++ .../ytdl/ui/adapter/GenericDownloadAdapter.kt | 6 +- .../ytdl/ui/adapter/QueuedDownloadAdapter.kt | 6 +- .../ui/adapter/ScheduledDownloadAdapter.kt | 6 +- .../downloadcard/CutVideoBottomSheetDialog.kt | 11 +- .../DownloadMultipleBottomSheetDialog.kt | 169 ++-- .../ObserveSourcesBottomSheetDialog.kt | 5 + .../downloadcard/SelectPlaylistItemsDialog.kt | 3 + .../downloads/CancelledDownloadsFragment.kt | 32 +- .../ui/downloads/DownloadQueueMainFragment.kt | 4 +- .../ui/downloads/ErroredDownloadsFragment.kt | 2 +- .../ytdl/ui/downloads/HistoryFragment.kt | 2 +- .../ui/downloads/SavedDownloadsFragment.kt | 2 +- .../ytdl/ui/more/CookiesFragment.kt | 39 +- .../downloadLogs/DownloadLogListFragment.kt | 1 - .../more/settings/FolderSettingsFragment.kt | 4 +- .../ui/more/settings/MainSettingsFragment.kt | 34 + .../settings/ProcessingSettingsFragment.kt | 6 +- .../com/deniscerri/ytdl/util/CrashListener.kt | 2 +- .../java/com/deniscerri/ytdl/util/FileUtil.kt | 10 +- .../java/com/deniscerri/ytdl/util/InfoUtil.kt | 63 +- .../deniscerri/ytdl/util/NotificationUtil.kt | 21 +- .../java/com/deniscerri/ytdl/util/UiUtil.kt | 6 +- .../com/deniscerri/ytdl/util/UpdateUtil.kt | 69 +- .../deniscerri/ytdl/work/DownloadWorker.kt | 29 +- .../ytdl/work/ObserveSourceWorker.kt | 5 +- .../main/res/layout-land/fragment_home.xml | 5 +- .../res/layout-sw600dp-land/fragment_home.xml | 5 +- .../main/res/layout-sw600dp/fragment_home.xml | 5 +- .../res/layout-sw950dp-land/fragment_home.xml | 5 +- .../main/res/layout/active_download_card.xml | 3 +- .../main/res/layout/command_template_item.xml | 2 +- app/src/main/res/layout/cookie_details.xml | 67 ++ app/src/main/res/layout/cut_video_sheet.xml | 2 +- app/src/main/res/layout/fragment_active.xml | 15 +- .../res/layout/fragment_download_video.xml | 1 + app/src/main/res/layout/fragment_home.xml | 5 +- .../main/res/layout/observe_sources_item.xml | 2 +- app/src/main/res/values-cs/strings.xml | 3 +- app/src/main/res/values-es/strings.xml | 3 +- app/src/main/res/values-hi/strings.xml | 3 +- app/src/main/res/values-ja/strings.xml | 3 +- app/src/main/res/values-nl/strings.xml | 3 +- app/src/main/res/values-pt-rBR/strings.xml | 3 +- app/src/main/res/values-pt/strings.xml | 3 +- app/src/main/res/values-ru/strings.xml | 3 +- app/src/main/res/values-sk/strings.xml | 3 +- app/src/main/res/values-sq-rAL/strings.xml | 3 +- app/src/main/res/values-sr/strings.xml | 3 +- app/src/main/res/values-uk/strings.xml | 3 +- app/src/main/res/values-zh-rCN/strings.xml | 3 +- app/src/main/res/values-zh-rTW/strings.xml | 3 +- app/src/main/res/values/arrays.xml | 16 +- app/src/main/res/values/strings.xml | 3 +- app/src/main/res/xml/folders_preference.xml | 4 +- .../main/res/xml/processing_preferences.xml | 8 +- .../android/en-US/changelogs/10760.txt | 71 ++ 76 files changed, 1493 insertions(+), 399 deletions(-) create mode 100644 app/schemas/com.deniscerri.ytdl.database.DBManager/17.json create mode 100644 app/src/main/java/com/deniscerri/ytdl/ui/adapter/ChangelogAdapter.kt create mode 100644 app/src/main/res/layout/cookie_details.xml create mode 100644 fastlane/metadata/android/en-US/changelogs/10760.txt diff --git a/app/build.gradle b/app/build.gradle index 789e537e..fb36918f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ def properties = new Properties() def versionMajor = 1 def versionMinor = 7 def versionPatch = 6 -def versionBuild = 1 // bump for dogfood builds, public betas, etc. -def isBeta = true +def versionBuild = 0 // bump for dogfood builds, public betas, etc. +def isBeta = false def versionExt = isBeta ? ".${versionBuild}-beta" : "" android { diff --git a/app/schemas/com.deniscerri.ytdl.database.DBManager/17.json b/app/schemas/com.deniscerri.ytdl.database.DBManager/17.json new file mode 100644 index 00000000..908e8b8e --- /dev/null +++ b/app/schemas/com.deniscerri.ytdl.database.DBManager/17.json @@ -0,0 +1,725 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "ae22d3024fcae377c9bd9b8ba6f1f299", + "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": [] + }, + { + "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)", + "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", + "notNull": false + }, + { + "fieldPath": "monthlyConfig", + "columnName": "monthlyConfig", + "affinity": "TEXT", + "notNull": false + }, + { + "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 + } + ], + "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, 'ae22d3024fcae377c9bd9b8ba6f1f299')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3e875b9c..a3592432 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,6 +63,7 @@ android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" android:excludeFromRecents="true" android:exported="true" + android:taskAffinity="" android:launchMode="singleInstance" android:theme="@style/Theme.BottomSheet"> 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 58d491b5..4ebb5b85 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/DBManager.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/DBManager.kt @@ -41,7 +41,7 @@ import com.deniscerri.ytdl.database.models.TerminalItem TerminalItem::class, ObserveSourcesItem::class ], - version = 16, + version = 17, autoMigrations = [ AutoMigration (from = 1, to = 2), AutoMigration (from = 2, to = 3), @@ -57,7 +57,8 @@ import com.deniscerri.ytdl.database.models.TerminalItem AutoMigration (from = 12, to = 13), // AutoMigration (from = 13, to = 14) MANUALLY HANDLED AutoMigration (from = 14, to = 15), - AutoMigration (from = 15, to = 16, spec = Migrations.resetObserveSources::class) + AutoMigration (from = 15, to = 16, spec = Migrations.resetObserveSources::class), + AutoMigration (from = 16, to = 17) ] ) 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 6d9a7394..81aab1b2 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 @@ -83,6 +83,9 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id") fun getQueuedDownloadsList() : List + @Query("SELECT * FROM downloads WHERE status='Scheduled' ORDER BY downloadStartTime, id") + fun getScheduledDownloadsList() : List + @Query("SELECT id FROM downloads WHERE status='Queued' ORDER BY id") fun getQueuedDownloadsListIDs() : List @@ -123,6 +126,15 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE id IN (:ids)") fun getDownloadsByIds(ids: List) : List + @Query("SELECT * FROM downloads WHERE status='Processing' AND id >=:id") + fun getProcessingItemsFromID(id: Long) : Flow> + + @Query("SELECT * FROM downloads WHERE status='Processing' AND id >=:id and id<=:id2") + fun getProcessingItemsBetweenIDs(id: Long, id2: Long) : List + + @Query("SELECT * FROM downloads WHERE id IN (:ids)") + fun getDownloadsByIdsFlow(ids: List) : Flow> + @Query("SELECT id FROM downloads ORDER BY id DESC LIMIT 1") fun getLastDownloadId(): Long diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/ResultDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/ResultDao.kt index 5da9d4cf..56f4effa 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/ResultDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/ResultDao.kt @@ -52,6 +52,9 @@ interface ResultDao { @Query("SELECT * FROM results WHERE url=:url LIMIT 1") fun getResultByURL(url: String) : ResultItem? + @Query("SELECT * FROM results where id=:id LIMIT 1") + fun getResultByID(id: Long): ResultItem? + @Query("SELECT * from results WHERE id > :item1 AND id < :item2 ORDER BY id") fun getResultsBetweenTwoItems(item1: Long, item2: Long) : List 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 bdd819dd..1eecf203 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 @@ -11,6 +11,7 @@ data class DownloadItemSimple( var id: Long, var url: String, var title: String, + var playlistTitle: String, var author: String, var thumb: String, var duration: String, diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/observeSources/ObserveSourcesItem.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/observeSources/ObserveSourcesItem.kt index 1153f58f..0ba6f44e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/models/observeSources/ObserveSourcesItem.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/models/observeSources/ObserveSourcesItem.kt @@ -35,5 +35,7 @@ data class ObserveSourcesItem( @ColumnInfo(defaultValue = "0") var getOnlyNewUploads: Boolean, var retryMissingDownloads: Boolean, + @ColumnInfo(defaultValue = "[]") + var ignoredLinks: MutableList, var alreadyProcessedLinks : MutableList ) : Parcelable diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt index ed96ef10..dca6a71e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt @@ -109,10 +109,22 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.getDownloadById(id) } + fun getProcessingItemsFromID(id: Long) : Flow> { + return downloadDao.getProcessingItemsFromID(id) + } + + fun getProcessingItemsBetweenIDs(first: Long, last:Long) : List { + return downloadDao.getProcessingItemsBetweenIDs(first, last) + } + fun getAllItemsByIDs(ids : List) : List{ return downloadDao.getDownloadsByIds(ids) } + fun getAllItemsByIDsFlow(ids: List) : Flow> { + return downloadDao.getDownloadsByIdsFlow(ids) + } + fun getActiveDownloads() : List { return downloadDao.getActiveAndPausedDownloadsList() } @@ -133,6 +145,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.getQueuedDownloadsList() } + fun getScheduledDownloads() : List { + return downloadDao.getScheduledDownloadsList() + } + fun getCancelledDownloads() : List { return downloadDao.getCancelledDownloadsList() } @@ -210,8 +226,9 @@ class DownloadRepository(private val downloadDao: DownloadDao) { val currentTime = System.currentTimeMillis() var delay = 0L if (queuedItems.isNotEmpty()){ - delay = if (queuedItems[0].downloadStartTime != 0L){ - queuedItems[0].downloadStartTime.minus(currentTime) + val earliestStart = queuedItems.minBy { it.downloadStartTime } + delay = if (earliestStart.downloadStartTime != 0L){ + earliestStart.downloadStartTime.minus(currentTime) } else 0 if (delay <= 60000L) delay = 0L } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt index 31a854f7..a28bc12e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt @@ -125,6 +125,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co resultDao.update(item) } + fun getItemByID(id: Long) : ResultItem? { + return resultDao.getResultByID(id) + } + fun getItemByURL(url: String): ResultItem? { return resultDao.getResultByURL(url) } @@ -178,6 +182,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co val info = getResultsFromSource(downloadItem.url, resetResults = false, addToResults = false, singleItem = true).first() if (downloadItem.title.isEmpty()) downloadItem.title = info.title if (downloadItem.author.isEmpty()) downloadItem.author = info.author + if (downloadItem.playlistTitle.isEmpty() && downloadItem.playlistTitle != "ytdlnis-Search") downloadItem.playlistTitle = info.playlistTitle downloadItem.duration = info.duration downloadItem.website = info.website if (downloadItem.thumb.isEmpty()) downloadItem.thumb = info.thumb diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/CookieViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/CookieViewModel.kt index 728a0dba..a26164b7 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/CookieViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/CookieViewModel.kt @@ -125,6 +125,9 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a close() } db.close() + + "# $url\n" + + "# Generated by YTDLnis\n" + cookieList.fold(StringBuilder("")) { acc, cookie -> acc.append(cookie.toNetscapeFormat()).append("\n") }.toString() 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 990c3055..b3d6a71e 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 @@ -8,9 +8,12 @@ import android.content.res.Resources import android.util.DisplayMetrics import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData +import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asFlow import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope +import androidx.media3.exoplayer.offline.Download import androidx.paging.PagingData import androidx.preference.PreferenceManager import androidx.work.Data @@ -43,14 +46,21 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.last import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File import java.util.Locale @@ -65,7 +75,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val allDownloads : Flow> val queuedDownloads : Flow> val activeDownloads : Flow> - val processingDownloads : Flow> + val allProcessingDownloads : Flow> + var mostRecentProcessingDownloads: Flow> val cancelledDownloads : Flow> val erroredDownloads : Flow> val savedDownloads : Flow> @@ -104,11 +115,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel auto, audio, video, command } - private val urlsForAudioType = listOf( - "music", - "audio", - "soundcloud" - ) + + val processingDownloads = MediatorLiveData>(listOf()) + var processingItemsFlow : ProcessingItemsJob? = null + var processingItems = MutableStateFlow(false) init { dbManager = DBManager.getInstance(application) @@ -133,7 +143,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel allDownloads = repository.allDownloads.flow queuedDownloads = repository.queuedDownloads.flow activeDownloads = repository.activeDownloads - processingDownloads = repository.processingDownloads + allProcessingDownloads = repository.processingDownloads + mostRecentProcessingDownloads = repository.getProcessingItemsFromID(0) savedDownloads = repository.savedDownloads.flow scheduledDownloads = repository.scheduledDownloads.flow cancelledDownloads = repository.cancelledDownloads.flow @@ -285,8 +296,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false) val customFileNameTemplate = when(historyItem.type) { - Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)30B - %(title)120B") - Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)30B - %(title)120B") + Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader).30B - %(title).170B") + Type.video -> sharedPreferences.getString("file_name_template", "%(uploader).30B - %(title).170B") else -> "" } @@ -367,88 +378,83 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel var originItemIDs: List, var processingDownloadItemIDs: MutableList = mutableListOf() ) - - var processingItemsJob: MutableLiveData = MutableLiveData(null) - var _filteredProcessingItems = MutableStateFlow>(listOf()) - var filteredProcessingItems: StateFlow> = _filteredProcessingItems - private fun updateProcessingJobData(item: ProcessingItemsJob) = viewModelScope.launch { - processingItemsJob.postValue(item) + private fun updateProcessingJobData(item: ProcessingItemsJob?) { + processingItemsFlow = item } fun turnDownloadItemsToProcessingDownloads(itemIDs: List) = viewModelScope.launch(Dispatchers.IO){ - processingItemsJob.postValue(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs)) + updateProcessingJobData(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs)) val job = viewModelScope.launch(Dispatchers.IO) { + processingItems.emit(true) try { - itemIDs.chunked(100).forEach { ids -> - val items = repository.getAllItemsByIDs(ids) - if (!isActive) { - throw CancellationException() - } - items.forEach { - it.id = 0 - it.status = DownloadRepository.Status.Processing.toString() - } + itemIDs.forEachIndexed { idx, it -> + val item = repository.getItemByID(it) + if (!isActive) throw CancellationException() - processingItemsJob.value?.apply { - processingDownloadItemIDs.addAll(repository.insertAll(items)) + item.id = 0 + item.status = DownloadRepository.Status.Processing.toString() + processingItemsFlow?.apply { + val id = repository.insert(item) + if (idx == 0) mostRecentProcessingDownloads = repository.getProcessingItemsFromID(id) + processingDownloadItemIDs.add(id) updateProcessingJobData(this) } } - + processingItems.emit(false) } catch (e: Exception) { - processingItemsJob.value?.apply { + processingItemsFlow?.apply { this.processingDownloadItemIDs.chunked(100).forEach { repository.deleteAllWithIDs(it) } } - processingItemsJob.postValue(null) + updateProcessingJobData(null) + processingItems.emit(false) } } - processingItemsJob.postValue(ProcessingItemsJob(job, DownloadItem::class.java.toString(), itemIDs)) + updateProcessingJobData(ProcessingItemsJob(job, DownloadItem::class.java.toString(), itemIDs)) } fun turnResultItemsToProcessingDownloads(itemIDs: List, downloadNow: Boolean = false) = viewModelScope.launch(Dispatchers.IO) { - processingItemsJob.postValue(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs)) + updateProcessingJobData(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs)) val job = viewModelScope.launch(Dispatchers.IO) { + processingItems.emit(true) try { - itemIDs.chunked(100).forEach { ids -> - val items = resultRepository.getAllByIDs(ids) - val downloadItems = items.map { - val preferredType = getDownloadType(url = it.url).toString() - val tmp = createDownloadItemFromResult(result = it, givenType = Type.valueOf( - preferredType - )) - tmp.status = DownloadRepository.Status.Processing.toString() - tmp - } + itemIDs.forEach { id -> + val item = resultRepository.getItemByID(id) ?: return@forEach + val preferredType = getDownloadType(url = item.url).toString() + val downloadItem = createDownloadItemFromResult(result = item, givenType = Type.valueOf( + preferredType + )) + downloadItem.status = DownloadRepository.Status.Processing.toString() + if (!isActive) { throw CancellationException() } if (downloadNow) { - downloadItems.forEach { - it.status = DownloadRepository.Status.Queued.toString() - } - queueDownloads(downloadItems) + downloadItem.status = DownloadRepository.Status.Queued.toString() + queueDownloads(listOf(downloadItem)) }else{ - processingItemsJob.value?.apply { - processingDownloadItemIDs.addAll(repository.insertAll(downloadItems)) + processingItemsFlow?.apply { + processingDownloadItemIDs.add(repository.insert(downloadItem)) updateProcessingJobData(this) } } } + processingItems.emit(false) }catch (e: Exception) { - processingItemsJob.value?.apply { + processingItemsFlow?.apply { this.processingDownloadItemIDs.chunked(100).forEach { repository.deleteAllWithIDs(it) } } - processingItemsJob.postValue(null) + updateProcessingJobData(null) + processingItems.emit(false) } } - processingItemsJob.postValue(ProcessingItemsJob(job, ResultItem::class.java.toString(), itemIDs)) + updateProcessingJobData(ProcessingItemsJob(job, ResultItem::class.java.toString(), itemIDs)) } fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){ @@ -498,7 +504,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) { - processingItemsJob.value?.apply { this.job?.cancel(CancellationException()) } + processingItemsFlow?.apply { this.job?.cancel(CancellationException()) } repository.cancelActiveQueued() } @@ -506,6 +512,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel return repository.getQueuedDownloads() } + fun getScheduled() : List { + return repository.getScheduledDownloads() + } + fun getCancelled() : List { return repository.getCancelledDownloads() } diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt index 68d546f5..2e71698a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt @@ -178,8 +178,8 @@ class SharedDownloadViewModel(private val context: Context) { if(type == Type.command && commandTemplateDao.getTotalNumber() == 0) type = Type.video val customFileNameTemplate = when(type) { - Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)s - %(title)s") - Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s") + Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader).30B - %(title).170B") + Type.video -> sharedPreferences.getString("file_name_template", "%(uploader).30B - %(title).170B") else -> "" } @@ -288,7 +288,7 @@ class SharedDownloadViewModel(private val context: Context) { val prefAudio = sharedPreferences.getString("format_importance_audio", itemValues.joinToString(","))!! prefAudio.split(",").forEachIndexed { idx, s -> - var importance = (itemValues.size - idx) * 10 + val importance = (itemValues.size - idx) * 10 when(s) { "id" -> { requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) importance else 0} diff --git a/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt b/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt index 911d8252..36200238 100644 --- a/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt @@ -98,7 +98,7 @@ class ShareActivity : BaseActivity() { // val params = window.attributes // params.alpha = 0f // window.attributes = params -// setContentView(R.layout.activity_share) + setContentView(R.layout.activity_share) }else{ window.run { diff --git a/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt b/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt index da190925..23d8816c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt +++ b/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt @@ -3,8 +3,10 @@ package com.deniscerri.ytdl.services import android.app.Service import android.content.Intent import android.os.Binder +import android.os.Build import android.os.IBinder import androidx.core.content.IntentCompat +import androidx.lifecycle.lifecycleScope import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.ResultItem @@ -18,8 +20,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class ProcessDownloadsInBackgroundService : Service() { @@ -46,7 +50,25 @@ class ProcessDownloadsInBackgroundService : Service() { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { val binding = intent.getBooleanExtra("binding", false) - if (binding) return super.onStartCommand(intent, flags, startId) + if (binding) { + val cancel = intent.getBooleanExtra("cancel", false) + if (cancel) { + cancelAllProcessingJobs() + CoroutineScope(SupervisorJob()).launch(Dispatchers.IO){ + repository.deleteProcessing() + withContext(Dispatchers.Main){ + if (Build.VERSION.SDK_INT > 23){ + stopForeground(STOP_FOREGROUND_REMOVE) + }else{ + stopForeground(true) + } + stopSelf() + } + } + }else{ + return super.onStartCommand(intent, flags, startId) + } + } val notificationUtil = NotificationUtil(this) startForeground(System.currentTimeMillis().toInt(), notificationUtil.createProcessingDownloads()) @@ -98,7 +120,7 @@ class ProcessDownloadsInBackgroundService : Service() { } } }else { - repository.getAllItemsByIDs(processingItemIDs).apply { + repository.getProcessingItemsBetweenIDs(processingItemIDs.first(), processingItemIDs.last()).apply { this.chunked(100).map { if (timeInMillis > 0){ this.forEach { d -> diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/BaseActivity.kt b/app/src/main/java/com/deniscerri/ytdl/ui/BaseActivity.kt index 6a5f9bcd..4fa5a8da 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/BaseActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/BaseActivity.kt @@ -26,18 +26,20 @@ open class BaseActivity : AppCompatActivity() { fun askPermissions() { val permissions = arrayListOf() - if (!checkFilePermission()) { - if (Build.VERSION.SDK_INT >= 33){ - permissions.add(Manifest.permission.READ_MEDIA_AUDIO) - permissions.add(Manifest.permission.READ_MEDIA_VIDEO) - if (Build.VERSION.SDK_INT >= 34){ - permissions.add(Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED) - } - }else{ - permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) - permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE) - } + val hasFilePerm = if (Build.VERSION.SDK_INT >= 30) { + true + }else { + (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED) && + (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED) } + + if (!hasFilePerm){ + permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE) + permissions.add(Manifest.permission.READ_EXTERNAL_STORAGE) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (!checkNotificationPermission()){ permissions.add(Manifest.permission.POST_NOTIFICATIONS) @@ -67,20 +69,6 @@ open class BaseActivity : AppCompatActivity() { } } - private fun checkFilePermission(): Boolean { - return if(Build.VERSION.SDK_INT >= 33){ - (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_AUDIO) - == PackageManager.PERMISSION_GRANTED) && - (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_MEDIA_VIDEO) - == PackageManager.PERMISSION_GRANTED) - }else{ - (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED) && - (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) - == PackageManager.PERMISSION_GRANTED) - } - } - @RequiresApi(Build.VERSION_CODES.TIRAMISU) private fun checkNotificationPermission(): Boolean { return (ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt index b1bda6b3..02e43a65 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt @@ -915,10 +915,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti queriesConstraint?.isVisible = queriesChipGroup?.childCount!! > 0 searchSuggestionsAdapter?.getList()?.apply { - if (this.first().type == SearchSuggestionType.CLIPBOARD){ - val newList = this.toMutableList().drop(1) - searchSuggestionsAdapter?.submitList(newList) + if (this.isNotEmpty()) { + if (this.first().type == SearchSuggestionType.CLIPBOARD){ + val newList = this.toMutableList().drop(1) + searchSuggestionsAdapter?.submitList(newList) + } } + } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt index d1ea6eee..4c0791a1 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadAdapter.kt @@ -76,11 +76,11 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: // TITLE ---------------------------------- val itemTitle = card.findViewById(R.id.title) - var title = item.title + var title = item.title.ifEmpty { item.playlistTitle.ifEmpty { item.url } } if (title.length > 100) { title = title.substring(0, 40) + "..." } - itemTitle.text = title.ifEmpty { item.url } + itemTitle.text = title // Author ---------------------------------- val author = card.findViewById(R.id.author) @@ -107,7 +107,7 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: val fileSize = FileUtil.convertFileSize(item.format.filesize) if (fileSize != "?") sideDetails.add(fileSize) - formatDetailsChip.text = sideDetails.joinToString(" · ") + formatDetailsChip.text = sideDetails.filter { it.isNotBlank() }.joinToString(" · ") //OUTPUT val output = card.findViewById(R.id.output) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt index 68eb1e23..a31362cd 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ActiveDownloadMinifiedAdapter.kt @@ -78,11 +78,11 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac // TITLE ---------------------------------- val itemTitle = card.findViewById(R.id.title) - var title = item.title + var title = item.title.ifEmpty { item.playlistTitle.ifEmpty { item.url } } if (title.length > 100) { title = title.substring(0, 40) + "..." } - itemTitle.text = title.ifEmpty { item.url } + itemTitle.text = title //DOWNLOAD TYPE ----------------------------- val type = card.findViewById(R.id.download_type) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ChangelogAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ChangelogAdapter.kt new file mode 100644 index 00000000..98fc085e --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ChangelogAdapter.kt @@ -0,0 +1,107 @@ +package com.deniscerri.ytdl.ui.adapter + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.core.view.isVisible +import androidx.recyclerview.widget.AsyncDifferConfig +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.deniscerri.ytdl.R +import com.deniscerri.ytdl.database.models.CommandTemplate +import com.deniscerri.ytdl.database.models.GithubRelease +import com.deniscerri.ytdl.util.Extensions.popup +import com.google.android.material.card.MaterialCardView +import com.google.android.material.chip.Chip +import com.google.android.material.chip.ChipGroup +import io.noties.markwon.AbstractMarkwonPlugin +import io.noties.markwon.Markwon +import io.noties.markwon.MarkwonConfiguration +import java.text.SimpleDateFormat +import java.util.Locale + +class ChangelogAdapter(activity: Activity) : ListAdapter(AsyncDifferConfig.Builder( + DIFF_CALLBACK +).build()) { + private val activity: Activity + + init { + this.activity = activity + } + + class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var layoutParams: LinearLayout.LayoutParams + init { + layoutParams = LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT + ) + layoutParams.setMargins(10, 10, 10, 0) + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val cardView = LayoutInflater.from(parent.context) + .inflate(R.layout.changelog_item, parent, false) + return ViewHolder(cardView) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val it = getItem(position) ?: return + val card = holder.itemView + + val layoutParams = holder.layoutParams + + card.findViewById(R.id.version).text = it.tag_name + card.findViewById(R.id.date).text = SimpleDateFormat( + DateFormat.getBestDateTimePattern( + Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(it.published_at.time) + + val mdText = card.findViewById(R.id.content) + val mw = Markwon.builder(activity).usePlugin(object: AbstractMarkwonPlugin() { + override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { + builder.linkResolver { view, link -> + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) + ContextCompat.startActivity(activity, browserIntent, Bundle()) + } + } + }).build() + mw.setMarkdown(mdText, it.body) + + val assetGroup = card.findViewById(R.id.assets) + assetGroup.removeAllViews() + it.assets.forEachIndexed { idx, c -> + val tmp = activity.layoutInflater.inflate(R.layout.filter_chip, assetGroup, false) as Chip + tmp.isCheckable = false + tmp.layoutParams = layoutParams + tmp.text = c.name + tmp.id = idx + tmp.setOnClickListener { + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(c.browser_download_url)) + ContextCompat.startActivity(activity, browserIntent, Bundle()) + } + assetGroup!!.addView(tmp) + } + } + + companion object { + private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: GithubRelease, newItem: GithubRelease): Boolean { + return oldItem.html_url == newItem.html_url + } + + override fun areContentsTheSame(oldItem: GithubRelease, newItem: GithubRelease): Boolean { + return oldItem.tag_name == newItem.tag_name + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/GenericDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/GenericDownloadAdapter.kt index 5594b0cd..62506d53 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/GenericDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/GenericDownloadAdapter.kt @@ -10,6 +10,7 @@ import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isVisible import androidx.paging.PagingDataAdapter import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil @@ -71,14 +72,15 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: val duration = card.findViewById(R.id.duration) duration.text = item.duration + duration.isVisible = item.duration != "-1" // TITLE ---------------------------------- val itemTitle = card.findViewById(R.id.title) - var title = item.title + var title = item.title.ifEmpty { item.playlistTitle.ifEmpty { item.url } } if (title.length > 100) { title = title.substring(0, 40) + "..." } - itemTitle.text = title.ifEmpty { item.url } + itemTitle.text = title //DOWNLOAD TYPE ----------------------------- val type = card.findViewById(R.id.download_type) 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 d7c725d7..20228443 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 @@ -87,14 +87,16 @@ class QueuedDownloadAdapter(onItemClickListener: OnItemClickListener, activity: val duration = card.findViewById(R.id.duration) duration.text = item.duration + duration.isVisible = item.duration != "-1" + // TITLE ---------------------------------- val itemTitle = card.findViewById(R.id.title) - var title = item.title + var title = item.title.ifEmpty { item.playlistTitle.ifEmpty { item.url } } if (title.length > 100) { title = title.substring(0, 40) + "..." } - itemTitle.text = title.ifEmpty { item.url } + itemTitle.text = title //DOWNLOAD TYPE ----------------------------- val type = card.findViewById(R.id.download_type) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ScheduledDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ScheduledDownloadAdapter.kt index ee99ef1a..0ab9474a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ScheduledDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ScheduledDownloadAdapter.kt @@ -12,6 +12,7 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView +import androidx.core.view.isVisible import androidx.paging.PagingDataAdapter import androidx.preference.PreferenceManager import androidx.recyclerview.widget.DiffUtil @@ -83,14 +84,15 @@ class ScheduledDownloadAdapter(onItemClickListener: OnItemClickListener, activit val duration = card.findViewById(R.id.duration) duration.text = item.duration + duration.isVisible = item.duration != "-1" // TITLE ---------------------------------- val itemTitle = card.findViewById(R.id.title) - var title = item.title + var title = item.title.ifEmpty { item.playlistTitle.ifEmpty { item.url } } if (title.length > 100) { title = title.substring(0, 40) + "..." } - itemTitle.text = title.ifEmpty { item.url } + itemTitle.text = title //DOWNLOAD TYPE ----------------------------- val type = card.findViewById(R.id.download_type) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/CutVideoBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/CutVideoBottomSheetDialog.kt index 4f40f349..e35aaa52 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/CutVideoBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/CutVideoBottomSheetDialog.kt @@ -498,7 +498,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private suggestedChapters.visibility = View.VISIBLE suggestedChips.removeAllViews() chapters!!.forEach { - val chip = layoutInflater.inflate(R.layout.suggestion_chip, chipGroup, false) as Chip + val chip = layoutInflater.inflate(R.layout.suggestion_chip, suggestedChips, false) as Chip chip.text = it.title chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK)) chip.isCheckedIconVisible = false @@ -514,9 +514,12 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private //replace existing chip to enable click events val idx = selectedCuts.indexOfFirst { c -> c.contains(it.title) } if (idx > -1){ - val chipForDeletion = chipGroup.children.firstOrNull { cc -> (cc as Chip).text == it.title } - chipGroup.removeView(chipForDeletion) - createChapterChip(it, idx) + val chipForDeletion = chipGroup.children.firstOrNull { cc -> (cc as Chip).text.contains(it.title) } + chipForDeletion?.apply { + chipGroup.removeView(chipForDeletion) + createChapterChip(it, idx) + } + } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt index d85c5428..c09bc448 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt @@ -100,8 +100,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure private lateinit var currentDownloadIDs: List private var processingItemsCount : Int = 0 - private lateinit var jobData: DownloadViewModel.ProcessingItemsJob - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] @@ -174,58 +172,57 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure shimmerTitle = view.findViewById(R.id.shimmer_loading_title) shimmerSubtitle = view.findViewById(R.id.shimmer_loading_subtitle) - downloadViewModel.processingItemsJob.observe(this) { - it?.apply { - lifecycleScope.launch { - jobData = it - processingItemsCount = it.processingDownloadItemIDs.size - count.text = "${processingItemsCount} ${getString(R.string.selected)}" - - val loading = it.originItemIDs.size != processingItemsCount - toggleLoadingShimmerTitle(loading) - bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading } - if (!loading){ - continueInBackgroundSnackBar.dismiss() - } - - downloadViewModel.processingDownloads.map { p -> p.filter { pd -> jobData.processingDownloadItemIDs.contains(pd.id) } }.collectLatest { items -> - listAdapter.submitList(items) - updateFileSize(items.map { it2 -> it2.format.filesize }) - - if (items.isNotEmpty()){ - if (items.all { it2 -> it2.type == items[0].type }) { - bottomAppBar.menu[1].icon?.alpha = 255 - if (items[0].type != DownloadViewModel.Type.command) { - bottomAppBar.menu[3].icon?.alpha = 255 - } - } else { - bottomAppBar.menu[1].icon?.alpha = 30 - bottomAppBar.menu[3].icon?.alpha = 30 - } - - val type = items.first().type - - when(type){ - DownloadViewModel.Type.audio -> { - preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24) - } - DownloadViewModel.Type.video -> { - preferredDownloadType.setIcon(R.drawable.baseline_video_file_24) - - } - DownloadViewModel.Type.command -> { - preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24) - } - - else -> {} - } - - } - } + lifecycleScope.launch { + downloadViewModel.processingItems.collectLatest { + val loading = it + toggleLoadingShimmerTitle(loading) + bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading } + if (!loading){ + continueInBackgroundSnackBar.dismiss() } } } + lifecycleScope.launch { + downloadViewModel.mostRecentProcessingDownloads.collectLatest { items -> + processingItemsCount = items.size + count.text = "${processingItemsCount} ${getString(R.string.selected)}" + listAdapter.submitList(items) + updateFileSize(items.map { it2 -> it2.format.filesize }) + + if (items.isNotEmpty()){ + if (items.all { it2 -> it2.type == items[0].type }) { + bottomAppBar.menu[1].icon?.alpha = 255 + if (items[0].type != DownloadViewModel.Type.command) { + bottomAppBar.menu[3].icon?.alpha = 255 + } + } else { + bottomAppBar.menu[1].icon?.alpha = 30 + bottomAppBar.menu[3].icon?.alpha = 30 + } + + val type = items.first().type + + when(type){ + DownloadViewModel.Type.audio -> { + preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24) + } + DownloadViewModel.Type.video -> { + preferredDownloadType.setIcon(R.drawable.baseline_video_file_24) + + } + DownloadViewModel.Type.command -> { + preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24) + } + + else -> {} + } + + } + } + } + + scheduleBtn.setOnClickListener{ fun initSchedule(processingFinished: Boolean) { UiUtil.showDatePicker(parentFragmentManager) { cal -> @@ -237,9 +234,11 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure downloadViewModel.deleteAllWithID(currentDownloadIDs) } - jobData.job?.cancel(CancellationException()) - jobData.job = null - downloadProcessDownloadsInBackground(jobData, processingFinished) + getProcessingItemsData()?.apply { + this.job?.cancel(CancellationException()) + this.job = null + downloadProcessDownloadsInBackground(this, processingFinished) + } val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(cal.timeInMillis) Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show() @@ -268,9 +267,11 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure downloadViewModel.deleteAllWithID(currentDownloadIDs) } - jobData.job?.cancel(CancellationException()) - jobData.job = null - downloadProcessDownloadsInBackground(jobData, processingFinished) + getProcessingItemsData()?.apply { + this.job?.cancel(CancellationException()) + this.job = null + downloadProcessDownloadsInBackground(this, processingFinished) + } dismiss() } } @@ -295,10 +296,12 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure lifecycleScope.launch{ withContext(Dispatchers.IO){ downloadViewModel.deleteAllWithID(currentDownloadIDs) - downloadViewModel.moveProcessingToSavedCategory(jobData.processingDownloadItemIDs) + downloadViewModel.moveProcessingToSavedCategory(getProcessingDownloadItemIDs()) + } + getProcessingItemsData()?.apply { + this.job?.cancel(CancellationException()) + this.job = null } - jobData.job?.cancel(CancellationException()) - jobData.job = null dismiss() } } @@ -309,13 +312,13 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure val formatListener = object : OnFormatClickListener { override fun onFormatClick(selectedFormats: List) { CoroutineScope(Dispatchers.IO).launch { - downloadViewModel.updateProcessingFormat(jobData.processingDownloadItemIDs, selectedFormats) + downloadViewModel.updateProcessingFormat(getProcessingDownloadItemIDs(), selectedFormats) } } override fun onFormatsUpdated(allFormats: List>) { CoroutineScope(Dispatchers.IO).launch { - downloadViewModel.updateProcessingAllFormats(jobData.processingDownloadItemIDs, allFormats) + downloadViewModel.updateProcessingAllFormats(getProcessingDownloadItemIDs(), allFormats) } } @@ -323,10 +326,12 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure override fun onContinueOnBackground() { requireActivity().lifecycleScope.launch { withContext(Dispatchers.IO){ - downloadViewModel.continueUpdatingFormatsOnBackground(jobData.processingDownloadItemIDs) + downloadViewModel.continueUpdatingFormatsOnBackground(getProcessingDownloadItemIDs()) + } + getProcessingItemsData()?.apply { + this.job?.cancel(CancellationException()) + this.job = null } - jobData.job?.cancel(CancellationException()) - jobData.job = null dismiss() } } @@ -364,7 +369,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure audio!!.setOnClickListener { CoroutineScope(Dispatchers.IO).launch { - downloadViewModel.updateProcessingType(jobData.processingDownloadItemIDs, DownloadViewModel.Type.audio) + downloadViewModel.updateProcessingType(getProcessingDownloadItemIDs(), DownloadViewModel.Type.audio) withContext(Dispatchers.Main){ preferredDownloadType.setIcon(R.drawable.baseline_audio_file_24) bottomAppBar.menu[1].icon?.alpha = 255 @@ -376,7 +381,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure video!!.setOnClickListener { CoroutineScope(Dispatchers.IO).launch{ - downloadViewModel.updateProcessingType(jobData.processingDownloadItemIDs, DownloadViewModel.Type.video) + downloadViewModel.updateProcessingType(getProcessingDownloadItemIDs(), DownloadViewModel.Type.video) withContext(Dispatchers.Main){ preferredDownloadType.setIcon(R.drawable.baseline_video_file_24) bottomAppBar.menu[1].icon?.alpha = 255 @@ -388,7 +393,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure command!!.setOnClickListener { CoroutineScope(Dispatchers.IO).launch{ - downloadViewModel.updateProcessingType(jobData.processingDownloadItemIDs, DownloadViewModel.Type.command) + downloadViewModel.updateProcessingType(getProcessingDownloadItemIDs(), DownloadViewModel.Type.command) withContext(Dispatchers.Main){ preferredDownloadType.setIcon(R.drawable.baseline_insert_drive_file_24) bottomAppBar.menu[1].icon?.alpha = 255 @@ -412,7 +417,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure R.id.format -> { lifecycleScope.launch { val res = withContext(Dispatchers.IO){ - downloadViewModel.checkIfAllProcessingItemsHaveSameType(jobData.processingDownloadItemIDs,) + downloadViewModel.checkIfAllProcessingItemsHaveSameType(getProcessingDownloadItemIDs()) } if (!res.first){ Toast.makeText(requireContext(), getString(R.string.format_filtering_hint), Toast.LENGTH_SHORT).show() @@ -431,12 +436,12 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure ) CoroutineScope(Dispatchers.IO).launch { - downloadViewModel.updateProcessingCommandFormat(jobData.processingDownloadItemIDs, format) + downloadViewModel.updateProcessingCommandFormat(getProcessingDownloadItemIDs(), format) } } }else{ val items = withContext(Dispatchers.IO){ - downloadViewModel.getAllByIDs(jobData.processingDownloadItemIDs) + downloadViewModel.getAllByIDs(getProcessingDownloadItemIDs()) } val flatFormatCollection = items.map { it.allFormats }.flatten() val commonFormats = withContext(Dispatchers.IO){ @@ -460,7 +465,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure R.id.more -> { lifecycleScope.launch { val res = withContext(Dispatchers.IO){ - downloadViewModel.checkIfAllProcessingItemsHaveSameType(jobData.processingDownloadItemIDs) + downloadViewModel.checkIfAllProcessingItemsHaveSameType(getProcessingDownloadItemIDs()) } if (!res.first) { Toast.makeText( @@ -481,7 +486,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure sheetView.findViewById(R.id.adjust).setPadding(padding,padding,padding,padding) val items = withContext(Dispatchers.IO){ - downloadViewModel.getAllByIDs(jobData.processingDownloadItemIDs) + downloadViewModel.getAllByIDs(getProcessingDownloadItemIDs()) } UiUtil.configureAudio( @@ -562,7 +567,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure sheetView.findViewById(R.id.adjust).setPadding(padding,padding,padding,padding) val items = withContext(Dispatchers.IO){ - downloadViewModel.getAllByIDs(jobData.processingDownloadItemIDs) + downloadViewModel.getAllByIDs(getProcessingDownloadItemIDs()) } UiUtil.configureVideo( @@ -700,7 +705,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure } CoroutineScope(Dispatchers.IO).launch { - downloadViewModel.updateProcessingDownloadPath(jobData.processingDownloadItemIDs, result.data?.data.toString()) + downloadViewModel.updateProcessingDownloadPath(getProcessingDownloadItemIDs(), result.data?.data.toString()) } val path = FileUtil.formatPath(result.data!!.data.toString()) @@ -832,9 +837,11 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure } override fun onDismiss(dialog: DialogInterface) { - if (jobData.job != null){ - jobData.job?.cancel(CancellationException()) - downloadViewModel.deleteAllWithID(jobData.processingDownloadItemIDs) + getProcessingItemsData()?.apply { + if (this.job != null){ + this.job?.cancel(CancellationException()) + downloadViewModel.deleteAllWithID(this.processingDownloadItemIDs) + } } super.onDismiss(dialog) } @@ -916,6 +923,14 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure } } + private fun getProcessingDownloadItemIDs() : List { + return downloadViewModel.processingItemsFlow?.processingDownloadItemIDs ?: listOf() + } + + private fun getProcessingItemsData() : DownloadViewModel.ProcessingItemsJob? { + return downloadViewModel.processingItemsFlow + } + private fun downloadProcessDownloadsInBackground(jobData: DownloadViewModel.ProcessingItemsJob, processingFinished: Boolean, timeInMillis: Long = 0){ Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent -> intent.putExtra("itemType", jobData.originItemType) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ObserveSourcesBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ObserveSourcesBottomSheetDialog.kt index 4291ee3d..9ee89c52 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ObserveSourcesBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ObserveSourcesBottomSheetDialog.kt @@ -523,6 +523,11 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() { mutableListOf() }else{ currentItem?.alreadyProcessedLinks ?: mutableListOf() + }, + ignoredLinks = if (resetProcessedLinks.isChecked || !getOnlyNewUploads.isChecked){ + mutableListOf() + }else{ + currentItem?.ignoredLinks ?: mutableListOf() } ) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/SelectPlaylistItemsDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/SelectPlaylistItemsDialog.kt index 10c553b1..ad577f97 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/SelectPlaylistItemsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/SelectPlaylistItemsDialog.kt @@ -5,6 +5,7 @@ import android.app.Dialog import android.content.res.Configuration import android.os.Build import android.os.Bundle +import android.text.method.DigitsKeyListener import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem @@ -106,7 +107,9 @@ class SelectPlaylistItemsDialog : BottomSheetDialogFragment(), PlaylistAdapter.O count.text = "0 ${resources.getString(R.string.selected)}" fromTextInput = view.findViewById(R.id.from_textinput) + fromTextInput.editText!!.keyListener = DigitsKeyListener.getInstance("0123456789") toTextInput = view.findViewById(R.id.to_textinput) + toTextInput.editText!!.keyListener = DigitsKeyListener.getInstance("0123456789") fromTextInput.editText!!.doAfterTextChanged { _text -> diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/CancelledDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/CancelledDownloadsFragment.kt index d32e6ac2..c91a4364 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/CancelledDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/CancelledDownloadsFragment.kt @@ -67,7 +67,7 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic ): View? { fragmentView = inflater.inflate(R.layout.generic_list, container, false) activity = getActivity() - downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] + downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) return fragmentView } @@ -251,24 +251,20 @@ class CancelledDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClic true } R.id.redownload -> { - lifecycleScope.launch { - val selectedObjects = getSelectedIDs() - val showDownloadCard = preferences.getBoolean("download_card", true) - if (showDownloadCard) { - CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { - downloadViewModel.turnDownloadItemsToProcessingDownloads(selectedObjects) - } - withContext(Dispatchers.Main){ - val bundle = Bundle() - bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray()) - findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundle) - } - }else { - downloadViewModel.reQueueDownloadItems(selectedObjects) - } - - adapter.clearCheckedItems() + val selectedObjects = runBlocking { + getSelectedIDs() } + val showDownloadCard = preferences.getBoolean("download_card", true) + if (showDownloadCard) { + downloadViewModel.turnDownloadItemsToProcessingDownloads(selectedObjects) + val bundle = Bundle() + bundle.putLongArray("currentDownloadIDs", selectedObjects.toLongArray()) + findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundle) + }else { + downloadViewModel.reQueueDownloadItems(selectedObjects) + } + + adapter.clearCheckedItems() runBlocking { actionMode?.finish() } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt index dc2465ab..4af8324a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/DownloadQueueMainFragment.kt @@ -39,6 +39,7 @@ import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator import com.yausername.youtubedl_android.YoutubeDL import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -288,8 +289,6 @@ class DownloadQueueMainFragment : Fragment(){ val binder = service as ProcessDownloadsInBackgroundService.LocalBinder mService = binder.service mBound = true - - mService.cancelAllProcessingJobs() } override fun onServiceDisconnected(arg0: ComponentName) { @@ -299,6 +298,7 @@ class DownloadQueueMainFragment : Fragment(){ Intent(requireActivity(), ProcessDownloadsInBackgroundService::class.java).also { intent -> intent.putExtra("binding", true) + intent.putExtra("cancel", true) requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE) } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ErroredDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ErroredDownloadsFragment.kt index 851f4b9d..dbdfed6a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ErroredDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/ErroredDownloadsFragment.kt @@ -67,7 +67,7 @@ class ErroredDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickL ): View? { fragmentView = inflater.inflate(R.layout.generic_list, container, false) activity = getActivity() - downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] + downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) return fragmentView } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt index 0e6da363..9b43350d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/HistoryFragment.kt @@ -414,7 +414,7 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{ val w = websites[i] if (w == "null" || w.isEmpty()) continue val tmp = layoutinflater!!.inflate(R.layout.filter_chip, websiteGroup, false) as Chip - tmp.text = w.replaceFirstChar { if (it.isLowerCase()) it.titlecase(java.util.Locale.getDefault()) else it.toString() } + tmp.text = w tmp.id = i tmp.setOnClickListener { Log.e(TAG, tmp.isChecked.toString()) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/SavedDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/SavedDownloadsFragment.kt index b322fc4a..d11ad0b3 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloads/SavedDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloads/SavedDownloadsFragment.kt @@ -66,7 +66,7 @@ class SavedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLis ): View? { fragmentView = inflater.inflate(R.layout.generic_list, container, false) activity = getActivity() - downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] + downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) return fragmentView } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/CookiesFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/CookiesFragment.kt index f217107c..1816833f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/CookiesFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/CookiesFragment.kt @@ -15,7 +15,9 @@ import android.view.ViewGroup import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.RelativeLayout +import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider @@ -30,9 +32,11 @@ import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.CookieItem import com.deniscerri.ytdl.database.viewmodel.CookieViewModel import com.deniscerri.ytdl.ui.adapter.CookieAdapter +import com.deniscerri.ytdl.util.Extensions.enableTextHighlight import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.UiUtil import com.google.android.material.appbar.MaterialToolbar +import com.google.android.material.card.MaterialCardView import com.google.android.material.chip.Chip import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.materialswitch.MaterialSwitch @@ -114,7 +118,7 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener { private fun initChips() { val new = view?.findViewById(R.id.newCookie) new?.setOnClickListener { - showDialog("") + showDialog(null) } } @@ -168,22 +172,25 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener { } } - private fun showDialog(url: String){ + private fun showDialog(item: CookieItem?){ lifecycleScope.launch { - val item = withContext(Dispatchers.IO){ - cookiesViewModel.getByURL(url) - } - - val builder = MaterialAlertDialogBuilder(requireContext()) builder.setTitle(getString(R.string.cookies)) - val inputLayout = layoutInflater.inflate(R.layout.textinput, null) - val editText = inputLayout.findViewById(R.id.url_edittext) - inputLayout.findViewById(R.id.url_textinput).hint = "URL" - val text = if (url.isNullOrBlank()) "https://" else url + val layout = layoutInflater.inflate(R.layout.cookie_details, null) + val editText = layout.findViewById(R.id.url_edittext) + layout.findViewById(R.id.url_textinput).hint = "URL" + val text = item?.url ?: "https://" editText.setText(text) editText.setSelection(editText.text.length) - builder.setView(inputLayout) + val current = layout.findViewById(R.id.current) + current.isVisible = item != null + item?.apply { + current.findViewById(R.id.currentText).apply { + setText(item.content) + enableTextHighlight() + } + } + builder.setView(layout) item?.apply { builder.setNeutralButton( @@ -194,7 +201,7 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener { } builder.setPositiveButton( - getString(R.string.get_cookies) + if (item == null) getString(R.string.get_cookies) else getString(R.string.update) ) { dialog: DialogInterface?, which: Int -> val myIntent = Intent(requireContext(), WebViewActivity::class.java) myIntent.putExtra("url", editText.text.toString()) @@ -220,12 +227,8 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener { } - companion object { - private const val TAG = "CookiesActivity" - } - override fun onItemClick(cookie: CookieItem) { - showDialog(cookie.url) + showDialog(cookie) } override fun onSelected(cookie: CookieItem) { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogListFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogListFragment.kt index d3b11538..dc20d44f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogListFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/downloadLogs/DownloadLogListFragment.kt @@ -99,7 +99,6 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe topAppBar.setOnMenuItemClickListener { m: MenuItem -> val itemId = m.itemId if (itemId == R.id.remove_logs) { - throw Exception("asffffffffff") try{ val deleteDialog = MaterialAlertDialogBuilder(requireContext()) deleteDialog.setTitle(getString(R.string.confirm_delete_history)) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt index 232e85cd..2863555a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/FolderSettingsFragment.kt @@ -142,9 +142,9 @@ class FolderSettingsFragment : BaseSettingsFragment() { } videoFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]" - videoFilenameTemplate?.summary = preferences.getString("file_name_template", "%(uploader)s - %(title)s") + videoFilenameTemplate?.summary = preferences.getString("file_name_template", "%(uploader).30B - %(title).170B") audioFilenameTemplate?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]" - audioFilenameTemplate?.summary = preferences.getString("file_name_template_audio", "%(uploader)s - %(title)s") + audioFilenameTemplate?.summary = preferences.getString("file_name_template_audio", "%(uploader).30B - %(title).170B") videoFilenameTemplate?.setOnPreferenceClickListener { UiUtil.showFilenameTemplateDialog(requireActivity(), videoFilenameTemplate?.summary.toString() ?: "", "${getString(R.string.file_name_template)} [${getString(R.string.video)}]") { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt index 3c7d89ee..288eb389 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/MainSettingsFragment.kt @@ -171,6 +171,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() { "settings" -> json.add("settings", backupSettings(preferences)) "downloads" -> json.add("downloads", backupHistory()) "queued" -> json.add("queued", backupQueuedDownloads() ) + "scheduled" -> json.add("scheduled", backupScheduledDownloads() ) "cancelled" -> json.add("cancelled", backupCancelledDownloads() ) "errored" -> json.add("errored", backupErroredDownloads() ) "saved" -> json.add("saved", backupSavedDownloads() ) @@ -279,6 +280,20 @@ class MainSettingsFragment : PreferenceFragmentCompat() { return JsonArray() } + private suspend fun backupScheduledDownloads() : JsonArray { + runCatching { + val items = withContext(Dispatchers.IO) { + downloadViewModel.getScheduled() + } + val arr = JsonArray() + items.forEach { + arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject) + } + return arr + } + return JsonArray() + } + private suspend fun backupCancelledDownloads() : JsonArray { runCatching { val items = withContext(Dispatchers.IO) { @@ -493,6 +508,25 @@ class MainSettingsFragment : PreferenceFragmentCompat() { errorMessage.append("\n QUEUED DOWNLOADS RESTORE FAILED: \n ${it.message}") } + //scheduled downloads restore + kotlin.runCatching { + if(json.has("scheduled")){ + val items = json.getAsJsonArray("scheduled") + val scheduled = mutableListOf() + items.forEach { + val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java) + item.id = 0L + scheduled.add(item) + } + if (scheduled.isNotEmpty()) { + finalMessage.append("${getString(R.string.scheduled)}: ${scheduled.count()}\n") + withContext(Dispatchers.IO){downloadViewModel.queueDownloads(scheduled)} + } + } + }.onFailure { + errorMessage.append("\n SCHEDULED DOWNLOADS RESTORE FAILED: \n ${it.message}") + } + //cancelled downloads restore kotlin.runCatching { if(json.has("cancelled")){ diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt index 8fbbec74..cf83298a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt @@ -51,6 +51,7 @@ class ProcessingSettingsFragment : BaseSettingsFragment() { formatImportanceAudio?.apply { + title = "${getString(R.string.format_importance)} [${getString(R.string.audio)}]" val items = requireContext().getStringArray(R.array.format_importance_audio) val itemValues = requireContext().getStringArray(R.array.format_importance_audio_values).toSet() val prefVideo = prefs.getString("format_importance_audio", itemValues.joinToString(","))!! @@ -62,7 +63,7 @@ class ProcessingSettingsFragment : BaseSettingsFragment() { Pair(it, items[itemValues.indexOf(it)]) }.toMutableList() - showFormatImportanceDialog(getString(R.string.format_importance_audio), itms) { new -> + showFormatImportanceDialog(title.toString(), itms) { new -> editor.putString("format_importance_audio", new.joinToString(",") { it.first }).apply() formatImportanceAudio.summary = new.map { it.second }.mapIndexed { index, s -> "${index + 1}. $s" }.joinToString("\n") } @@ -71,6 +72,7 @@ class ProcessingSettingsFragment : BaseSettingsFragment() { } formatImportanceVideo?.apply { + title = "${getString(R.string.format_importance)} [${getString(R.string.video)}]" val items = requireContext().getStringArray(R.array.format_importance_video) val itemValues = requireContext().getStringArray(R.array.format_importance_video_values).toSet() val prefVideo = prefs.getString("format_importance_video", itemValues.joinToString(","))!! @@ -82,7 +84,7 @@ class ProcessingSettingsFragment : BaseSettingsFragment() { Pair(it, items[itemValues.indexOf(it)]) }.toMutableList() - showFormatImportanceDialog(getString(R.string.format_importance_video), itms) {new -> + showFormatImportanceDialog(title.toString(), itms) {new -> editor.putString("format_importance_video", new.joinToString(",") { it.first }).apply() formatImportanceVideo.summary = new.map { it.second }.mapIndexed { index, s -> "${index + 1}. $s" }.joinToString("\n") } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/CrashListener.kt b/app/src/main/java/com/deniscerri/ytdl/util/CrashListener.kt index 43b0579c..1501f108 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/CrashListener.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/CrashListener.kt @@ -16,7 +16,7 @@ class CrashListener(private val context: Context) : Thread.UncaughtExceptionHand override fun uncaughtException(p0: Thread, p1: Throwable) { CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { - createLog("${p1.message}\n${p1.stackTrace}") + createLog("${p1.message}\n\n${p1.stackTrace.joinToString("\n")}") } } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt index cf6ef912..bc679e3a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt @@ -296,7 +296,15 @@ object FileUtil { } fun getDownloadArchivePath(context: Context) : String { - val folder = PreferenceManager.getDefaultSharedPreferences(context).getString("download_archive_path", context.filesDir.absolutePath + "/")!! + var folder = PreferenceManager.getDefaultSharedPreferences(context).getString("download_archive_path", "")!! + if (folder == "") { + val externalPath = context.getExternalFilesDir(null) + folder = if (externalPath == null){ + context.cacheDir.absolutePath + File.separator + }else{ + externalPath.absolutePath + File.separator + } + } return "${formatPath(folder)}download_archive.txt" } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt index 9a09c387..1a9c95bf 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt @@ -648,7 +648,7 @@ class InfoUtil(private val context: Context) { for (result in results) { if (result.isNullOrBlank()) continue val jsonObject = JSONObject(result) - val title = jsonObject.getStringByAny("track", "alt_title", "title", "webpage_url_basename") + var title = jsonObject.getStringByAny("alt_title", "title", "webpage_url_basename") if (title == "[Private video]" || title == "[Deleted video]") continue var author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id") @@ -678,7 +678,8 @@ class InfoUtil(private val context: Context) { } } - val website = jsonObject.getStringByAny("ie_key", "extractor_key", "extractor") + var website = jsonObject.getStringByAny("ie_key", "extractor_key", "extractor") + if (website == "Generic" || website == "HTML5MediaEmbed") website = jsonObject.getStringByAny("webpage_url_domain") var playlistTitle = jsonObject.getStringByAny("playlist_title") var playlistURL: String? = "" var playlistIndex: Int? = null @@ -724,6 +725,13 @@ class InfoUtil(private val context: Context) { } } + val type = jsonObject.getStringByAny("_type") + if (type == "playlist" && playlistTitle.isEmpty()) { + playlistTitle = title + title = "" + author = "" + } + val res = ResultItem(0, url, title, @@ -977,8 +985,8 @@ class InfoUtil(private val context: Context) { val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1) if (concurrentFragments > 1) request.addOption("-N", concurrentFragments) - val retries = sharedPreferences.getString("--retries", "")!! - val fragmentRetries = sharedPreferences.getString("--fragment_retries", "")!! + val retries = sharedPreferences.getString("retries", "")!! + val fragmentRetries = sharedPreferences.getString("fragment_retries", "")!! if(retries.isNotEmpty()) request.addOption("--retries", retries) if(fragmentRetries.isNotEmpty()) request.addOption("--fragment-retries", fragmentRetries) @@ -992,8 +1000,6 @@ class InfoUtil(private val context: Context) { request.addOption("--no-resize-buffer") } - request.addOption("--socket-timeout", 60) - val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!! if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL) @@ -1017,15 +1023,15 @@ class InfoUtil(private val context: Context) { request.addOption("--keep-fragments") } - if (sharedPreferences.getBoolean("no_part", false)){ - request.addOption("--no-part") - } - val embedMetadata = sharedPreferences.getBoolean("embed_metadata", true) val thumbnailFormat = sharedPreferences.getString("thumbnail_format", "jpg") var filenameTemplate = downloadItem.customFileNameTemplate if(downloadItem.type != DownloadViewModel.Type.command){ + if (sharedPreferences.getBoolean("no_part", false)){ + request.addOption("--no-part") + } + request.addOption("--trim-filenames", 254 - downDir.absolutePath.length) if (downloadItem.SaveThumb) { @@ -1068,7 +1074,7 @@ class InfoUtil(private val context: Context) { request.addCommands(listOf("--replace-in-metadata", "video:uploader", ".+", downloadItem.author.take(30))) } - request.addOption("--parse-metadata", "uploader:(?P.+)(?: - Topic)$") + request.addOption("--parse-metadata", "uploader:^(?P.*?)(?:(?= - Topic)|$)") if (embedMetadata){ request.addOption("--parse-metadata", "%(uploader,channel,creator,artist|null)s:%(uploader)s") @@ -1084,7 +1090,7 @@ class InfoUtil(private val context: Context) { } } filenameTemplate = if (filenameTemplate.isBlank()){ - "%(section_title&{} |)s%(title)s" + "%(section_title&{} |)s%(title).170B" }else{ "%(section_title&{} |)s$filenameTemplate" } @@ -1119,6 +1125,9 @@ class InfoUtil(private val context: Context) { audioQualityId = "ba/b" }else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){ audioQualityId = "wa/w" + }else if(audioQualityId.contains("kbps_ytdlnisgeneric")){ + request.addOption("--match-filter", "abr<=${audioQualityId.split("kbps")[0]}") + audioQualityId = "" } @@ -1150,7 +1159,7 @@ class InfoUtil(private val context: Context) { if(ext.isNotBlank()){ if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){ request.addOption("--audio-format", ext) - formatSorting.append(",ext::$ext") + formatSorting.append(",aext:$ext") } } @@ -1164,14 +1173,14 @@ class InfoUtil(private val context: Context) { if (embedMetadata){ request.addOption("--embed-metadata") - request.addOption("--parse-metadata", "artist:(?P[^,]+)") - request.addOption("--parse-metadata", "%(album_artist,meta_album_artist,uploader|)s:%(album_artist)s") + request.addOption("--parse-metadata", "%(artist,uploader)s:^(?P[^,]*)") + request.addOption("--parse-metadata", "%(album_artist,meta_album_artist|)s:%(album_artist)s") request.addOption("--parse-metadata", "description:(?:Released on: )(?P\\d{4})") request.addOption("--parse-metadata", "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s") if (downloadItem.playlistTitle.isNotEmpty()) { - request.addOption("--parse-metadata", "%(album,playlist,title)s:%(meta_album)s") + request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s") request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(track_number)s") } else { request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s") @@ -1239,6 +1248,8 @@ class InfoUtil(private val context: Context) { } } } + var acont = sharedPreferences.getString("audio_format", "")!! + if (acont == "Default") acont = "" //format logic var videoF = downloadItem.format.format_id @@ -1259,6 +1270,11 @@ class InfoUtil(private val context: Context) { val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!! if (downloadItem.videoPreferences.removeAudio) audioF = "" + if(audioF.contains("kbps_ytdlnisgeneric")){ + request.addOption("--match-filter", "abr<=${audioF.split("kbps")[0]}") + audioF = "" + } + val f = StringBuilder() val preferredCodec = sharedPreferences.getString("video_codec", "") @@ -1371,7 +1387,8 @@ class InfoUtil(private val context: Context) { if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size") if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref") if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref") - if (cont.isNotBlank()) append(",ext:$cont:") + if (cont.isNotBlank()) append(",vext:$cont") + if (acont.isNotBlank()) append(",aext:$acont") if (this.isNotBlank()){ request.addOption("-S", "+hasaud$this") } @@ -1381,14 +1398,7 @@ class InfoUtil(private val context: Context) { request.addOption("-f", f.toString().replace("/$".toRegex(), "")) if (downloadItem.videoPreferences.writeSubs){ - val subFormat = sharedPreferences.getString("sub_format", "srt") - request.addOption("--write-subs") - if(subFormat!!.isNotBlank()){ - request.addOption("--sub-format", "${subFormat}/best") - request.addOption("--convert-subtitles", subFormat ?: "srt") - } - } if (downloadItem.videoPreferences.embedSubs) { @@ -1400,6 +1410,11 @@ class InfoUtil(private val context: Context) { } if (downloadItem.videoPreferences.embedSubs || downloadItem.videoPreferences.writeSubs || downloadItem.videoPreferences.writeAutoSubs){ + val subFormat = sharedPreferences.getString("sub_format", "srt") + if(subFormat!!.isNotBlank()){ + request.addOption("--sub-format", "${subFormat}/best") + request.addOption("--convert-subtitles", subFormat) + } request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" }) } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt index 5321b7ac..81d2d65e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt @@ -26,6 +26,7 @@ import com.deniscerri.ytdl.receiver.CancelDownloadNotificationReceiver import com.deniscerri.ytdl.receiver.CancelWorkReceiver import com.deniscerri.ytdl.receiver.PauseDownloadNotificationReceiver import com.deniscerri.ytdl.receiver.ResumeActivity +import com.deniscerri.ytdl.services.ProcessDownloadsInBackgroundService import com.deniscerri.ytdl.util.Extensions.toBitmap import java.io.File @@ -247,6 +248,8 @@ class NotificationUtil(var context: Context) { else -> R.drawable.ic_launcher_foreground_large } + val contentText = StringBuilder("$title") + val bitmap = iconType.toBitmap(context) notificationBuilder .setContentTitle("${res.getString(R.string.downloaded)} $title") @@ -255,15 +258,11 @@ class NotificationUtil(var context: Context) { .setGroup(DOWNLOAD_FINISHED_NOTIFICATION_ID.toString()) .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN) .setContentText(title) - .setStyle(NotificationCompat.BigTextStyle() - .bigText(""" - $title - ${filepath?.joinToString("\n")} - """.trimIndent())) .setPriority(NotificationCompat.PRIORITY_MAX) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .clearActions() if (filepath != null){ + contentText.append("\n\n"+ filepath.joinToString("\n")) try{ val uris = filepath.mapNotNull { runCatching { @@ -317,6 +316,7 @@ class NotificationUtil(var context: Context) { notificationBuilder.addAction(0, res.getString(R.string.share), shareNotificationPendingIntent) }catch (_: Exception){} } + notificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(contentText.toString().trimIndent())) notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID + id.toInt(), notificationBuilder.build()) } @@ -646,6 +646,16 @@ class NotificationUtil(var context: Context) { fun createProcessingDownloads() : Notification{ val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) + val intent = Intent(context, ProcessDownloadsInBackgroundService::class.java) + intent.putExtra("binding", true) + intent.putExtra("cancel", true) + val cancelNotificationPendingIntent = PendingIntent.getService( + context, + System.currentTimeMillis().toInt(), + intent, + PendingIntent.FLAG_IMMUTABLE + ) + notificationBuilder .setContentTitle(resources.getString(R.string.processing)) .setSmallIcon(R.drawable.ic_app_icon) @@ -658,6 +668,7 @@ class NotificationUtil(var context: Context) { .setPriority(NotificationCompat.PRIORITY_DEFAULT) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .clearActions() + .addAction(0, resources.getString(R.string.cancel), cancelNotificationPendingIntent) return notificationBuilder.build() } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index 0eed303e..eb165e09 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -112,7 +112,7 @@ object UiUtil { else if (formatNote == "worst") formatNote = context.getString(R.string.worst_quality) var container = chosenFormat.container - if (container == "Default") container = context.getString(R.string.defaultValue) + if (container == "Default" || container.isBlank()) container = context.getString(R.string.defaultValue) formatCard.findViewById(R.id.container).text = container.uppercase() formatCard.findViewById(R.id.format_note).text = formatNote.uppercase() @@ -580,14 +580,14 @@ object UiUtil { bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet) bottomSheet.findViewById(R.id.bottom_sheet_title)?.apply { - text = item.title.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } + text = item.title.ifEmpty { item.playlistTitle.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } } setOnLongClickListener{ showFullTextDialog(context, text.toString(), context.getString(R.string.title)) true } } bottomSheet.findViewById(R.id.bottom_sheet_author)?.apply { - text = item.author.ifEmpty { "`${context.getString(R.string.defaultValue)}`" } + text = item.author setOnLongClickListener{ showFullTextDialog(context, text.toString(), context.getString(R.string.author)) true 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 518b0ef5..88eb8d22 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt @@ -23,9 +23,14 @@ import android.widget.TextView import android.widget.Toast import androidx.core.content.ContextCompat.startActivity import androidx.preference.PreferenceManager +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.LayoutManager import com.deniscerri.ytdl.BuildConfig import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.models.GithubRelease +import com.deniscerri.ytdl.ui.adapter.ChangelogAdapter +import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.google.android.material.card.MaterialCardView import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup @@ -166,71 +171,25 @@ class UpdateUtil(var context: Context) { fun showChangeLog(activity: Activity){ runCatching { - val scrollView = ScrollView(activity) - - val linearLayout = LinearLayout(activity) - linearLayout.orientation = LinearLayout.VERTICAL - - val layoutParams = LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT - ) - - layoutParams.setMargins(10, 10, 10, 0) - scrollView.layoutParams = layoutParams - scrollView.addView(linearLayout) val releases = getGithubReleases() + val view = activity.layoutInflater.inflate(R.layout.generic_list, null) + val adapter = ChangelogAdapter(activity) + val recycler = view.findViewById(R.id.download_recyclerview) + recycler.layoutManager = LinearLayoutManager(activity) + recycler.adapter = adapter + adapter.submitList(releases) + recycler.enableFastScroll() + val changeLogDialog = MaterialAlertDialogBuilder(context) .setTitle(activity.getString(R.string.changelog)) - .setView(scrollView) + .setView(view) .setIcon(R.drawable.ic_chapters) .setNegativeButton(context.resources.getString(R.string.dismiss)) { _: DialogInterface?, _: Int -> } Handler(Looper.getMainLooper()).post { changeLogDialog.show() } - CoroutineScope(Dispatchers.IO).launch { - releases.forEach { - (activity.layoutInflater.inflate(R.layout.changelog_item, null) as MaterialCardView).apply { - this.layoutParams = layoutParams - findViewById(R.id.version).text = it.tag_name - findViewById(R.id.date).text = SimpleDateFormat( - DateFormat.getBestDateTimePattern( - Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(it.published_at.time) - - val mdText = findViewById(R.id.content) - val mw = Markwon.builder(context).usePlugin(object: AbstractMarkwonPlugin() { - override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { - builder.linkResolver { view, link -> - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) - startActivity(context, browserIntent, Bundle()) - } - } - }).build() - mw.setMarkdown(mdText, it.body) - - - val assetGroup = findViewById(R.id.assets) - it.assets.forEachIndexed { idx, c -> - val tmp = activity.layoutInflater.inflate(R.layout.filter_chip, assetGroup, false) as Chip - tmp.isCheckable = false - tmp.layoutParams = layoutParams - tmp.text = c.name - tmp.id = idx - tmp.setOnClickListener { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(c.browser_download_url)) - startActivity(context, browserIntent, Bundle()) - } - assetGroup!!.addView(tmp) - } - - withContext(Dispatchers.Main){ - linearLayout.addView(this@apply) - } - } - } - } - }.onFailure { if (it.message != null){ Handler(Looper.getMainLooper()).post { diff --git a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt index dfa0b5ef..684baedd 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/DownloadWorker.kt @@ -84,8 +84,7 @@ class DownloadWorker( .createPendingIntent() val workNotif = notificationUtil.createDefaultWorkerNotification() - val workNotifID = Random.nextInt(900000000, 1000000000) - val foregroundInfo = ForegroundInfo(workNotifID, workNotif) + val foregroundInfo = ForegroundInfo(1000000000, workNotif) setForegroundAsync(foregroundInfo) queuedItems.collect { items -> @@ -265,7 +264,7 @@ class DownloadWorker( val historyItem = HistoryItem(0, downloadItem.url, - downloadItem.title, + downloadItem.title.ifEmpty { downloadItem.playlistTitle }, downloadItem.author, downloadItem.duration, downloadItem.thumb, @@ -287,18 +286,18 @@ class DownloadWorker( downloadItem.id, downloadItem.title, downloadItem.type, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths, resources ) - if (wasQuickDownloaded && createResultItem){ - runCatching { - eventBus.post(WorkerProgress(100, "Creating Result Items", downloadItem.id)) - runBlocking { - infoUtil.getFromYTDL(downloadItem.url).forEach { res -> - if (res != null) { - resultDao.insert(res) - } - } - } - } - } +// if (wasQuickDownloaded && createResultItem){ +// runCatching { +// eventBus.post(WorkerProgress(100, "Creating Result Items", downloadItem.id)) +// runBlocking { +// infoUtil.getFromYTDL(downloadItem.url).forEach { res -> +// if (res != null) { +// resultDao.insert(res) +// } +// } +// } +// } +// } dao.delete(downloadItem.id) diff --git a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt index ef8cce89..a8a15d2a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt @@ -58,7 +58,7 @@ class ObserveSourceWorker( .filter { result -> //if first run and get new items only is preferred then dont get anything on first run if (item.getOnlyNewUploads && item.runCount == 0){ - item.alreadyProcessedLinks.add(result.url) + item.ignoredLinks.add(result.url) false }else{ true @@ -66,6 +66,9 @@ class ObserveSourceWorker( } .filter { result -> val history = historyRepo.getAllByURL(result.url) + + !item.ignoredLinks.contains(result.url) && + if (item.retryMissingDownloads){ //all items that are not present in history history.none { hi -> hi.downloadPath.any { path -> FileUtil.exists(path) } } diff --git a/app/src/main/res/layout-land/fragment_home.xml b/app/src/main/res/layout-land/fragment_home.xml index b031dc7c..218d92a4 100644 --- a/app/src/main/res/layout-land/fragment_home.xml +++ b/app/src/main/res/layout-land/fragment_home.xml @@ -20,15 +20,14 @@ diff --git a/app/src/main/res/layout-sw600dp-land/fragment_home.xml b/app/src/main/res/layout-sw600dp-land/fragment_home.xml index 40ca20ac..8b1992d4 100644 --- a/app/src/main/res/layout-sw600dp-land/fragment_home.xml +++ b/app/src/main/res/layout-sw600dp-land/fragment_home.xml @@ -20,15 +20,14 @@ diff --git a/app/src/main/res/layout-sw600dp/fragment_home.xml b/app/src/main/res/layout-sw600dp/fragment_home.xml index 39c5e615..b63a255b 100644 --- a/app/src/main/res/layout-sw600dp/fragment_home.xml +++ b/app/src/main/res/layout-sw600dp/fragment_home.xml @@ -20,15 +20,14 @@ diff --git a/app/src/main/res/layout-sw950dp-land/fragment_home.xml b/app/src/main/res/layout-sw950dp-land/fragment_home.xml index 82ffbfd9..1f0606f3 100644 --- a/app/src/main/res/layout-sw950dp-land/fragment_home.xml +++ b/app/src/main/res/layout-sw950dp-land/fragment_home.xml @@ -20,15 +20,14 @@ diff --git a/app/src/main/res/layout/active_download_card.xml b/app/src/main/res/layout/active_download_card.xml index 49944fc3..0dac0897 100644 --- a/app/src/main/res/layout/active_download_card.xml +++ b/app/src/main/res/layout/active_download_card.xml @@ -18,8 +18,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" app:cardCornerRadius="20dp" - app:cardElevation="10dp" - app:cardMaxElevation="12dp" + app:cardElevation="5dp" android:checkable="true" app:strokeWidth="0dp" app:cardPreventCornerOverlap="true" diff --git a/app/src/main/res/layout/command_template_item.xml b/app/src/main/res/layout/command_template_item.xml index 0922c459..13add0f8 100644 --- a/app/src/main/res/layout/command_template_item.xml +++ b/app/src/main/res/layout/command_template_item.xml @@ -34,7 +34,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginEnd="15dp" - android:maxLines="2" + android:maxLines="3" android:ellipsize="end" android:textSize="12sp" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/cookie_details.xml b/app/src/main/res/layout/cookie_details.xml new file mode 100644 index 00000000..9377dd94 --- /dev/null +++ b/app/src/main/res/layout/cookie_details.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cut_video_sheet.xml b/app/src/main/res/layout/cut_video_sheet.xml index 1989d642..84cb89dd 100644 --- a/app/src/main/res/layout/cut_video_sheet.xml +++ b/app/src/main/res/layout/cut_video_sheet.xml @@ -191,7 +191,7 @@ android:layout_height="wrap_content" android:layout_gravity="center_vertical" android:paddingHorizontal="10dp" - android:text=":" + android:text="-" android:textSize="20sp" android:textStyle="bold" /> diff --git a/app/src/main/res/layout/fragment_active.xml b/app/src/main/res/layout/fragment_active.xml index add67a97..879edaf7 100644 --- a/app/src/main/res/layout/fragment_active.xml +++ b/app/src/main/res/layout/fragment_active.xml @@ -2,12 +2,12 @@ + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools"> - + android:layout_height="wrap_content"> - + + diff --git a/app/src/main/res/layout/fragment_download_video.xml b/app/src/main/res/layout/fragment_download_video.xml index 7b50d2b4..78395786 100644 --- a/app/src/main/res/layout/fragment_download_video.xml +++ b/app/src/main/res/layout/fragment_download_video.xml @@ -66,6 +66,7 @@ diff --git a/app/src/main/res/layout/fragment_home.xml b/app/src/main/res/layout/fragment_home.xml index 0a5a962d..f8f86631 100644 --- a/app/src/main/res/layout/fragment_home.xml +++ b/app/src/main/res/layout/fragment_home.xml @@ -20,15 +20,14 @@ diff --git a/app/src/main/res/layout/observe_sources_item.xml b/app/src/main/res/layout/observe_sources_item.xml index 5456a5d9..ae20f21c 100644 --- a/app/src/main/res/layout/observe_sources_item.xml +++ b/app/src/main/res/layout/observe_sources_item.xml @@ -117,7 +117,7 @@ V nastavení aplikace je třeba povolit oprávnění SCHEDULE_EXACT_ALARM. Stahované položky se stále načítají. Pokračovat v jejich zpracování na pozadí a zahájit stahování? Minuta - Pořadí důležitosti formátu (Video) + Pořadí důležitosti formátu Sekunda Milisekundy - Pořadí důležitosti formátu (Audio) Umístění Upravte důležitost prvku formátu. Toto pořadí se použije pouze tehdy, když aplikace načte formáty na kartě stahování a automaticky vybere formát! Video bez zvuku diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index aa09067a..dfbe3344 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -390,8 +390,7 @@ Borrar programado Segundo Milisegundos - Orden de importancia del formato (audio) - Orden de importancia del formato (vídeo) + Orden de importancia del formato Ajustar la importancia de los elementos de formato. ¡Este orden solo se utilizará cuando la aplicación busque los formatos en la tarjeta de descarga y los seleccione automáticamente! Vídeo sin sonido Descargar carpeta de archivos diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 6e604153..1fe066da 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -391,8 +391,7 @@ सेकेंड मिनट मिलीसेकेंड - प्रारूप महत्व क्रम (ऑडियो) - प्रारूप महत्व क्रम (वीडियो) + प्रारूप महत्व क्रम प्रारूप तत्व महत्व को समायोजित करें। इस आदेश का उपयोग केवल तभी किया जाएगा जब ऐप डाउनलोड कार्ड में प्रारूप लाएगा और प्रारूप का स्वतः चयन करेगा! बिना ऑडियो वाला वीडियो संग्रह फ़ोल्डर डाउनलोड करें diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 5c7a9918..e71925c6 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -390,13 +390,12 @@ 上に移動 ミリ秒 ダウンロード記録のフォルダ - 形式の重要度の順序 (動画) + 形式の重要度の順序 ダウンロードはまだ読み込み中です。処理をバックグラウンドで続け、ダウンロードを開始しますか? 音声なし動画 場所 - 形式の重要度の順序 (音声) アプリの設定から SCHEDULE_EXACT_ALARM の権限を有効にする必要があります。 形式の要素の重要度を調整します。この順序はアプリがダウンロードカード内で形式を取得し、形式を自動選択する際にみ使われます! \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index db9bc4e1..d4883909 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -331,8 +331,7 @@ Stabiele versie van yt-dlp Minuut Seconde - Prioriteit van audio­formaten - Prioriteit van video­formaten + Prioriteit formaten Video zonder audio Locatie Alleen nieuwe uploads ophalen diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 3bfb9bf0..cd4e0eb2 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -388,14 +388,13 @@ Limpar downloads agendados Copiar URL Mover para cima - Ordem de importância do formato (vídeo) + Ordem de importância do formato Minuto(s) Segundo(s) Milissegundo(s) Localização Pasta para arquivamento de downloads Vídeo sem áudio - Ordem de importância do formato (áudio) Você precisa habilitar a permissão SCHEDULE_EXACT_ALARM nas configurações do aplicativo. Ajuste a importância do elemento de formato. Essa ordem só será usada quando o aplicativo buscar os formatos no cartão de download e selecionar automaticamente o formato! Os downloads ainda estão sendo carregados. Continuar processando-os em segundo plano e iniciar o download? diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 2d687d81..d18d1819 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -388,11 +388,10 @@ Mover para baixo Agendado Limpar agendamento - Ordem preferencial do formato (vídeo) + Ordem preferencial do formato Minuto Segundo Milissegundos - Ordem preferencial do formato (áudio) Vídeo sem áudio Localização \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 653028b8..8368509e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -388,13 +388,12 @@ Двигайтесь вниз Запланированное Очистить Запланированное - Порядок важности формата (видео) Скачать папку архива Расположение Минута Секунда Миллисекунды - Порядок важности формата (аудио) + Порядок важности формата Отрегулируйте важность элемента формата. Этот порядок будет использоваться только в том случае, если приложение извлекает форматы из карты загрузки и автоматически выбирает формат! Видео без звука Вам необходимо включить разрешение SCHEDULE_EXACT_ALARM в настройках приложения. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 2fce1aa1..3640c489 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -394,8 +394,7 @@ Položky na stiahnutie sa stále načítavajú. Pokračovať v ich spracovaní na pozadí a začať sťahovanie? Minúta Sekunda - Poradie dôležitosti formátu (Audio) - Poradie dôležitosti formátu (Video) + Poradie dôležitosti formátu Video bez zvuku Umiestnenie V nastaveniach aplikácie musíte povoliť oprávnenie SCHEDULE_EXACT_ALARM. diff --git a/app/src/main/res/values-sq-rAL/strings.xml b/app/src/main/res/values-sq-rAL/strings.xml index 754073c4..8ac2564f 100644 --- a/app/src/main/res/values-sq-rAL/strings.xml +++ b/app/src/main/res/values-sq-rAL/strings.xml @@ -390,8 +390,7 @@ Minuta Sekonda Milisekonda - Rradha e rëndësisë së Formateve (Audio) - Rradha e rëndësisë së Formateve (Video) + Rradha e rëndësisë së Formateve Rregullo rëndësinë e elementit të formatit. Kjo porosi do të përdoret vetëm kur aplikacioni merr formatet në kartën e shkarkimit dhe zgjedh automatikisht formatin! Video pa audio Skedari i arkivës së shkarkimeve diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index dc1e1604..7aa1318e 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -391,8 +391,7 @@ Секунд Милисекунде Минут - Редослед важности формата (аудио) - Редослед важности формата (видео) + Редослед важности формата Подесите важност елемента формата. Овај налог ће се користити само када апликација преузме формате на картици за преузимање и аутоматски изабере формат! Видео без звука Преузимања се и даље учитавају. Наставити с обрађивањем преузимања у позадини и започети преузимање? diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index daa26ae5..cc305e31 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -390,12 +390,11 @@ Очистити Заплановане Хвилина Секунда - Порядок важливості формату (аудіо) Налаштуйте важливість елемента формату. Цей порядок буде використовуватися лише в тому випадку, якщо програма витягує формати з карти завантаження та автоматично вибирає формат! Відео без звуку Завантаження все ще завантажуються. Продовжити їх обробку у фоновому режимі та розпочати завантаження? Мілісекунди - Порядок важливості формату (відео) + Порядок важливості формату Завантажити папку архіву Розташування Вам потрібно включити роздільну здатність SCHEDULE_EXACT_ALARM в установках програми. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 44d9ebb1..f70a0e5b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -389,8 +389,7 @@ 字幕 移到底部 毫秒 - 格式重要性顺序(音频) - 格式重要性顺序(视频) + 格式重要性顺序 调整格式元素重要性。此顺序仅当应用在下载卡片中获取格式并自动选择格式时才会用到! 没有声音的视频 下载档案文件夹 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 7ae4ea9d..b062051b 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -390,10 +390,9 @@ 清除排程 分鐘 毫秒 - 格式重要性順序(影片) 下載仍在加載中。繼續在背景處理它們並開始下載嗎? - 格式重要性順序(音訊) + 格式重要性順序 下載存檔資料夾 您需要在應用程式設定中啟用 SCHEDULE_EXACT_ALARM 權限。 位置 diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 1ff15a05..ab4b49f8 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -11,7 +11,7 @@ - Default + mp3 m4a aac @@ -32,7 +32,7 @@ - Default + mp4 webm mkv @@ -67,11 +67,21 @@ best + 192kbps + 160kbps + 128kbps + 96kbps + 64kbps worst ba + 192kbps_ytdlnisgeneric + 160kbps_ytdlnisgeneric + 128kbps_ytdlnisgeneric + 96kbps_ytdlnisgeneric + 64kbps_ytdlnisgeneric wa @@ -799,6 +809,7 @@ @string/settings @string/downloads @string/in_queue + @string/scheduled @string/cancelled @string/errored @string/saved @@ -813,6 +824,7 @@ settings downloads queued + scheduled cancelled errored saved diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6609c6da..5859acd0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -394,8 +394,7 @@ Minute Second Milliseconds - Format Importance Order (Audio) - Format Importance Order (Video) + Format Importance Order Adjust format element importance. This order will only be used when the app fetches the formats in the download card and auto-selects the format! Video with no Audio Download Archive Folder diff --git a/app/src/main/res/xml/folders_preference.xml b/app/src/main/res/xml/folders_preference.xml index c5f396ba..0f658b44 100644 --- a/app/src/main/res/xml/folders_preference.xml +++ b/app/src/main/res/xml/folders_preference.xml @@ -59,14 +59,14 @@ android:icon="@drawable/ic_textformat" app:key="file_name_template" app:useSimpleSummaryProvider="true" - app:defaultValue="%(uploader)30B - %(title)120B" + app:defaultValue="%(uploader).30B - %(title).170B" app:title="@string/file_name_template" /> + app:title="@string/format_importance" /> + app:title="@string/format_importance" /> diff --git a/fastlane/metadata/android/en-US/changelogs/10760.txt b/fastlane/metadata/android/en-US/changelogs/10760.txt new file mode 100644 index 00000000..c99f5a2f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/10760.txt @@ -0,0 +1,71 @@ +## What's New + +- Made finished notification icon color to red +- Added download path in the history item details sheet +- Fixed Download On Schedule feature. Switched back to AlarmManager as its more accurate than WorkManager +- Removed move to top and to bottom in the scheduled tab. Code cleanup +- Fixed a huge issue when you press cancel to the quick download item before the data isn't fully fetched, it would go back to being queued after the data fetching is finally done +- moved force keyframes at cuts in the general category in processing settings +- showed the number when you select all items in the context menu bar +- fixed app going always to the search bar when you return to home if you launched the search shortcut from the app menu +- Fixed app not hiding update formats button after updating formats +- Fixed app not passing ba/b in format logic when using generic best audio format +- Fixed meta track parse metadata +- Fixed app not cancelling downloads in some cases +- Fixed huge issue when using multiple download card and it wouldnt keep user defined changes +- Fixed app not removing download notification when cancelling download +- Fixed some errors in arabic interface when selecting playlist items +- Limited characters in cut interface textboxes to just numbers colons and periods +- When user had a defined preferred format id and the user selects a generic 'best' format the app just accounted that format id and didnt consider the preferred video resolution as a fallback +- Fixed when trying to schedule a video download and setting it to download as audio as well, the audio is put in queued and not in scheduled +- Fixed app crashing when trying to tap adjust download in the errored notification, in case the user removed the download beforehand +- Added ability to modify the download item in the queue. But to prevent the app from starting the download prematurely, it is moved to the saved category and after you hit ok it will be requeued. If you cancel the edit card, the item will stay in saved +- Fixed app reseting user defined changes in the download card like the download path when it finally updates data +- Fixed app crashing when trying to open a download that has no duration in it. Now its disabled +- Previously the app would change the state of downloads to Paused, whether it is activepaused or queuedpaused. Since the app now simply pauses all downloads, there is no need to do such transformations especially when you could have many items in the queue. Now simply the app cancels the ytdlp instances and the download worker and stores a state whether the downloads are paused or not +- Fixed album playlist track not being saved #482 +- Fixed retries and fragment retries settings not applying +- Fixed container in RTL not showing correctly in video tab in the download card +- Fixed bad spacing in RTL for search engines +- Fixed app crashing when you multiple select cancelled downloads for redownload +- Removed the ability of the app to rename extractors in the history tab. Now they are raw from yt-dlp +- Changelog was too slow and buggy, so now its on a recyclerview ultra fast +- Fixed the case when you pasted multiple links in the home section and only one item would be registered +- Fixed observe sources not downloading new uploads when checked as such +- On select playlist items the end textbox was not purely numeric. fixed +- Fixed sub format conversion not applying when embedding subtitles +- added scheduled as a backup category in teh settings +- added ability to show cookie details +- fixed app crash in some cases when u copied multiple url and tapped the FAB in home screen +- removed excessive file permissions for API 30 and above + +## Format aspect importance + +Now in the settings you can order around which element based on your preferences like codec, container, quality, preferred format id should be prioritised when the app automatically chooses a format for you. The app uses a weigh system to sort formats based on preference. + +- fixed app crashing when swipe to delete a scheduled item +- fixed app not modifying time in a scheduled item +- added network constraint for observe sources worker +- added reconfigure button in errored notification +- fixed app not hiding the "Link you copied" after clicking on it +- fixed app crashing when trying to toast in the end (#469) +- added alternate urls for instagram,facebook,and reddit as supported sites +- fixed app not escaping double quotes in filenames +- fixed container text alignment for arabic interface +- rearranged video quality order in descending order in the settings + + +## Background processing of items for download + +Someone tested to download a 4000 long playlist in the app. First of all it couldnt even load them in the multiple download card. In those extreme cases the usual recommendation was to just turn quick download on, and consider the whole playlist as a single item and download. + +But still, if a madman wants the ability to modify the list, now the app can handle it and will show the download card. Since it will take a while until all items all processed and converted into download items, a progress bar is added. +Still you are able to hit download and schedule button early and let the app continue on its own by using the default configurations of the download items. So its fast like quick download but still separates all items into separate downloads. + +- fixed app using -a "" on observe sources command type downloads +- added milliseconds when trying to cut a download +- added ability to set the folder location of your download archive path. The file has to be named as download_archive.txt +- added feature to number each chapter when using split by chapters +- slight changes to downloads already exist dialog +- fixed app not considering m.youtube links +- other small fixes \ No newline at end of file