diff --git a/app/build.gradle b/app/build.gradle index 0360c55b..76376731 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ plugins { def properties = new Properties() def versionMajor = 1 def versionMinor = 6 -def versionPatch = 7 -def versionBuild = 0 // bump for dogfood builds, public betas, etc. +def versionPatch = 8 +def versionBuild = 2 // bump for dogfood builds, public betas, etc. def versionExt = "" if (versionBuild > 0){ @@ -136,6 +136,7 @@ dependencies { implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.test.ext:junit-ktx:1.1.5' + implementation 'androidx.compose.ui:ui-android:1.5.3' testImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" @@ -186,4 +187,6 @@ dependencies { implementation "com.anggrayudi:storage:1.5.5" implementation 'me.zhanghai.android.fastscroll:library:1.3.0' + implementation 'com.google.accompanist:accompanist-webview:0.30.1' + implementation 'androidx.compose.material3:material3-android:1.2.0-alpha08' } diff --git a/app/schemas/com.deniscerri.ytdlnis.database.DBManager/10.json b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/10.json index 385a6ccb..d182a497 100644 --- a/app/schemas/com.deniscerri.ytdlnis.database.DBManager/10.json +++ b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/10.json @@ -2,11 +2,11 @@ "formatVersion": 1, "database": { "version": 10, - "identityHash": "6b5efd73749a7138035e1f00349715c8", + "identityHash": "0647c4b527efea884e1dc26157cd4189", "entities": [ { "tableName": "results", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `creationTime` INTEGER NOT NULL)", + "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 DEFAULT '', `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `creationTime` INTEGER NOT NULL)", "fields": [ { "fieldPath": "id", @@ -42,7 +42,8 @@ "fieldPath": "thumb", "columnName": "thumb", "affinity": "TEXT", - "notNull": true + "notNull": true, + "defaultValue": "''" }, { "fieldPath": "website", @@ -93,7 +94,7 @@ }, { "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)", + "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", @@ -167,6 +168,13 @@ "affinity": "INTEGER", "notNull": true, "defaultValue": "0" + }, + { + "fieldPath": "command", + "columnName": "command", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" } ], "primaryKey": { @@ -538,7 +546,7 @@ "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, '6b5efd73749a7138035e1f00349715c8')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0647c4b527efea884e1dc26157cd4189')" ] } } \ No newline at end of file diff --git a/app/schemas/com.deniscerri.ytdlnis.database.DBManager/11.json b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/11.json new file mode 100644 index 00000000..ea52b871 --- /dev/null +++ b/app/schemas/com.deniscerri.ytdlnis.database.DBManager/11.json @@ -0,0 +1,558 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "09798517d2b7ce96999c879bcd381c7f", + "entities": [ + { + "tableName": "results", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `creationTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "author", + "columnName": "author", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "duration", + "columnName": "duration", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumb", + "columnName": "thumb", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "website", + "columnName": "website", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistTitle", + "columnName": "playlistTitle", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "formats", + "columnName": "formats", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "''" + }, + { + "fieldPath": "chapters", + "columnName": "chapters", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "creationTime", + "columnName": "creationTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "history", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadId` INTEGER NOT NULL DEFAULT 0, `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)", + "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 + } + ], + "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)", + "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" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "searchHistory", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "templateShortcuts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "cookies", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `content` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "logs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `content` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadType` TEXT NOT NULL, `downloadTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "format", + "columnName": "format", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadType", + "columnName": "downloadType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "downloadTime", + "columnName": "downloadTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "terminalDownloads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `command` TEXT NOT NULL, `log` TEXT DEFAULT '')", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "command", + "columnName": "command", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "log", + "columnName": "log", + "affinity": "TEXT", + "notNull": false, + "defaultValue": "''" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '09798517d2b7ce96999c879bcd381c7f')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8c04e1a9..1daf5700 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -194,6 +194,7 @@ + diff --git a/app/src/main/java/com/deniscerri/ytdlnis/App.kt b/app/src/main/java/com/deniscerri/ytdlnis/App.kt index 9e2f3753..dc94b265 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/App.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/App.kt @@ -5,19 +5,7 @@ import android.os.Looper import android.widget.Toast import androidx.core.content.edit import androidx.preference.PreferenceManager -import androidx.work.Configuration -import androidx.work.Constraints -import androidx.work.ExistingPeriodicWorkPolicy -import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequestBuilder -import androidx.work.WorkManager -import com.deniscerri.ytdlnis.database.DBManager -import com.deniscerri.ytdlnis.database.repository.DownloadRepository -import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.util.NotificationUtil -import com.deniscerri.ytdlnis.util.UpdateUtil -import com.deniscerri.ytdlnis.work.UpdateYTDLWorker -import com.google.android.material.snackbar.Snackbar import com.yausername.aria2c.Aria2c import com.yausername.ffmpeg.FFmpeg import com.yausername.youtubedl_android.YoutubeDL @@ -27,8 +15,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import java.util.* -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit class App : Application() { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/ConfigureMultipleDownloadsAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/ConfigureMultipleDownloadsAdapter.kt index 22c98177..326bda3c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/ConfigureMultipleDownloadsAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/ConfigureMultipleDownloadsAdapter.kt @@ -58,8 +58,8 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener // THUMBNAIL ---------------------------------- if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){ - val imageURL = item!!.thumb - if (imageURL.isNotEmpty()) { + val imageURL = item?.thumb + if (!imageURL.isNullOrBlank()) { uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) } } else { uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt index 7b936abc..a5c38ee5 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.Flow LogItem::class, TerminalItem::class ], - version = 10, + version = 11, autoMigrations = [ AutoMigration (from = 1, to = 2), AutoMigration (from = 2, to = 3), @@ -33,7 +33,8 @@ import kotlinx.coroutines.flow.Flow AutoMigration (from = 6, to = 7), AutoMigration (from = 7, to = 8), AutoMigration (from = 8, to = 9), - AutoMigration (from = 9, to = 10) + AutoMigration (from = 9, to = 10), + AutoMigration (from = 10, to = 11) ] ) abstract class DBManager : RoomDatabase(){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt index 3fedfefe..55539251 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt @@ -34,6 +34,9 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") fun getActiveAndQueuedDownloadsList() : List + @Query("SELECT id FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") + fun getActiveAndQueuedDownloadIDs() : List + @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") fun getActiveAndQueuedDownloads() : Flow> @@ -122,8 +125,8 @@ interface DownloadDao { @Query("DELETE FROM downloads WHERE id in (:list)") suspend fun deleteAllWithIDs(list: List) - @Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued' or status='QueuedPaused'") - suspend fun cancelQueued() + @Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued' or status='QueuedPaused' or status='Active' or status='Paused'") + suspend fun cancelActiveQueued() @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") suspend fun deleteSingleProcessing(id: Long) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/HistoryDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/HistoryDao.kt index cf21c4f2..e6bb8e6a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/HistoryDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/HistoryDao.kt @@ -35,6 +35,9 @@ interface HistoryDao { @Query("SELECT * FROM history WHERE id=:id LIMIT 1") suspend fun getHistoryItem(id: Int) : HistoryItem + @Query("SELECT * FROM history WHERE url=:url") + fun getAllHistoryByURL(url: String) : List + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(item: HistoryItem) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt index f9ccf873..548771ff 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/TerminalDao.kt @@ -11,15 +11,38 @@ import kotlinx.coroutines.flow.Flow @Dao interface TerminalDao { - @Query("SELECT * FROM terminalDownloads ORDER BY id") + @Query("SELECT id,command FROM terminalDownloads ORDER BY id") fun getActiveTerminalDownloads() : List - @Query("SELECT * FROM terminalDownloads ORDER BY id") + @Query("SELECT id,command FROM terminalDownloads ORDER BY id") fun getActiveTerminalDownloadsFlow() : Flow> + @Query("SELECT * from terminalDownloads where id=:id") + fun getActiveTerminalFlow(id: Long) : Flow + @Query("SELECT COUNT(*) FROM terminalDownloads") fun getActiveTerminalsCount() : Int + @Query("UPDATE terminalDownloads set log=:l where id=:id") + fun updateTerminalLog(l: String, id: Long) + + @Query("SELECT * FROM terminalDownloads WHERE id=:id LIMIT 1") + fun getTerminalById(id: Long) : TerminalItem? + @Transaction + fun updateLog(line: String, id: Long){ + val t = getTerminalById(id) ?: return + val log = t.log ?: "" + val lines = log.split("\n") + //clean dublicate progress + add newline + val l = if (listOf(line, lines.last()).all { it.contains("[download")}){ + lines.dropLast(1).joinToString("\n") + }else{ + log + } + "\n${line}" + + updateTerminalLog(l, id) + } + @Insert suspend fun insert(item: TerminalItem) : Long diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/HistoryItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/HistoryItem.kt index 5f9385ed..23f67300 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/HistoryItem.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/HistoryItem.kt @@ -20,5 +20,7 @@ data class HistoryItem( val website: String, val format: Format, @ColumnInfo(defaultValue = "0") - val downloadId: Long + val downloadId: Long, + @ColumnInfo(defaultValue = "") + val command: String = "" ) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/TerminalItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/TerminalItem.kt index fc5433a4..4744d312 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/TerminalItem.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/TerminalItem.kt @@ -9,5 +9,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel data class TerminalItem( @PrimaryKey(autoGenerate = true) var id: Long = 0, - var command: String + var command: String, + @ColumnInfo(defaultValue = "") + var log: String? = "", ) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt index 1a536f01..f8dd8f15 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt @@ -83,6 +83,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.getActiveAndQueuedDownloadsList() } + fun getActiveAndQueuedDownloadIDs() : List { + return downloadDao.getActiveAndQueuedDownloadIDs() + } + fun getQueuedDownloads() : List { return downloadDao.getQueuedDownloadsList() } @@ -120,8 +124,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) { } - suspend fun cancelQueued(){ - downloadDao.cancelQueued() + suspend fun cancelActiveQueued(){ + downloadDao.cancelActiveQueued() } fun pauseDownloads(){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt index 694fa1af..cc4125d7 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt @@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.database.viewmodel import android.app.Application import android.content.Context +import android.content.Intent import android.content.SharedPreferences import android.content.res.Configuration import android.content.res.Resources @@ -28,6 +29,7 @@ import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao import com.deniscerri.ytdlnis.database.dao.DownloadDao +import com.deniscerri.ytdlnis.database.dao.HistoryDao import com.deniscerri.ytdlnis.database.dao.ResultDao import com.deniscerri.ytdlnis.database.models.AudioPreferences import com.deniscerri.ytdlnis.database.models.CommandTemplate @@ -38,6 +40,7 @@ import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.VideoPreferences import com.deniscerri.ytdlnis.database.repository.DownloadRepository +import com.deniscerri.ytdlnis.ui.ErrorDialogActivity import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.work.AlarmScheduler @@ -82,6 +85,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application private var videoCodec: String? private var audioCodec: String? private val dao: DownloadDao + private val historyDao: HistoryDao enum class Type { auto, audio, video, command @@ -96,6 +100,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application init { dbManager = DBManager.getInstance(application) dao = dbManager.downloadDao + historyDao = dbManager.historyDao repository = DownloadRepository(dao) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) commandTemplateDao = DBManager.getInstance(application).commandTemplateDao @@ -173,8 +178,20 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return repository.getItemByID(id) } - fun getDownloadType(t: Type, url: String) : Type { - return when(t){ + fun getDownloadType(t: Type? = null, url: String) : Type { + var type = t + + if (type == null){ + val preferredDownloadType = sharedPreferences.getString("preferred_download_type", Type.auto.toString()) + type = if (sharedPreferences.getBoolean("remember_download_type", false)){ + Type.valueOf(sharedPreferences.getString("last_used_download_type", + preferredDownloadType)!!) + }else{ + Type.valueOf(preferredDownloadType!!) + } + } + + return when(type){ Type.auto -> { if (urlsForAudioType.any { url.contains(it) }){ Type.audio @@ -182,7 +199,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application Type.video } } - else -> t + + else -> type } } @@ -448,10 +466,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application fun turnResultItemsToDownloadItems(items: List) : List { val list : MutableList = mutableListOf() - var preferredType = sharedPreferences.getString("preferred_download_type", "video") items.forEach { - preferredType = getDownloadType(Type.valueOf(preferredType!!), it!!.url).toString() - list.add(createDownloadItemFromResult(result = it, givenType = Type.valueOf(preferredType!!))) + val preferredType = getDownloadType(url = it!!.url).toString() + list.add(createDownloadItemFromResult(result = it, givenType = Type.valueOf( + preferredType + ))) } return list } @@ -487,12 +506,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } fun deleteAllWithID(ids: List) = viewModelScope.launch(Dispatchers.IO){ - Log.e("Aa", ids.size.toString()) repository.deleteAllWithIDs(ids) } - fun cancelQueued() = viewModelScope.launch(Dispatchers.IO) { - repository.cancelQueued() + fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) { + repository.cancelActiveQueued() } fun getQueued() : List { @@ -528,6 +546,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return repository.getActiveAndQueuedDownloads() } + fun getActiveAndQueuedDownloadIDs() : List{ + return repository.getActiveAndQueuedDownloadIDs() + } + suspend fun resetScheduleTimeForItemsAndStartDownload(items: List) = CoroutineScope(Dispatchers.IO).launch { dbManager.downloadDao.resetScheduleTimeForItems(items) startDownloadWorker(emptyList()) @@ -548,7 +570,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val alarmScheduler = AlarmScheduler(context) val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads() val queuedItems = mutableListOf() - var exists = false + val existing = mutableListOf() //if scheduler is on val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) @@ -556,8 +578,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application alarmScheduler.schedule() } + items.forEach { if (it.status != DownloadRepository.Status.Paused.toString()) it.status = DownloadRepository.Status.Queued.toString() + val currentCommand = infoUtil.buildYoutubeDLRequest(it) + val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand) if (activeAndQueuedDownloads.firstOrNull{d -> d.id = 0 d.logID = null @@ -565,23 +590,33 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application d.status = DownloadRepository.Status.Queued.toString() d.toString() == it.toString() } != null) { - exists = true + existing.add(it) }else{ - if (it.id == 0L){ - val id = runBlocking {repository.insert(it)} - it.id = id - }else if (it.status == DownloadRepository.Status.Queued.toString()){ - repository.update(it) - } + //check if downloaded and file exists + val history = historyDao.getAllHistoryByURL(it.url).filter { item -> FileUtil.exists(item.downloadPath) } + if (history.any { h -> h.command.replace("-P \"(.*?)\"".toRegex(), "") == parsedCurrentCommand.replace("-P \"(.*?)\"".toRegex(), "") }){ + existing.add(it) + }else{ + if (it.id == 0L){ + val id = runBlocking {repository.insert(it)} + it.id = id + }else if (it.status == DownloadRepository.Status.Queued.toString()){ + repository.update(it) + } - queuedItems.add(it) + queuedItems.add(it) + } } } - if (exists){ - Looper.prepare().run { - Toast.makeText(context, context.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show() - } + if (existing.isNotEmpty()){ + val intent = Intent(context, ErrorDialogActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + intent.putExtra("title", context.resources.getString(R.string.download_already_exists)) + intent.putExtra("message", + "${existing.size}/${items.size}\n" + + existing.mapIndexed { index, downloadItem -> "${index+1}. ${downloadItem.title}" }.joinToString("\n")) + context.startActivity(intent) } if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || items.any { it.downloadStartTime > 0L } ){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/TerminalViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/TerminalViewModel.kt index 736ca623..35aae7c6 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/TerminalViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/TerminalViewModel.kt @@ -71,6 +71,10 @@ class TerminalViewModel(private val application: Application) : AndroidViewModel return dao.getActiveTerminalDownloadsFlow() } + fun getTerminal(id: Long) : Flow { + return dao.getActiveTerminalFlow(id) + } + suspend fun insert(item: TerminalItem) : Long { return dao.insert(item) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/AlarmStopReceiver.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/AlarmStopReceiver.kt index d3074039..fcdbe3ec 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/AlarmStopReceiver.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/AlarmStopReceiver.kt @@ -27,7 +27,6 @@ class AlarmStopReceiver : BroadcastReceiver() { CoroutineScope(Dispatchers.IO).launch { runCatching { - Log.e("assa", "assssssssssssssssssssssssssssss") val active = dbManager.downloadDao.getActiveDownloadsList() val startingTime = preferences.getString("schedule_start", "00:00")!! diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelWorkReceiver.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelWorkReceiver.kt index 3632fde5..ae2c415b 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelWorkReceiver.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelWorkReceiver.kt @@ -16,12 +16,9 @@ import kotlinx.coroutines.runBlocking class CancelWorkReceiver : BroadcastReceiver() { override fun onReceive(c: Context, intent: Intent) { - val id = intent.getStringExtra("workID") - if (!id.isNullOrBlank()) { - runCatching { - WorkManager.getInstance(c).cancelAllWorkByTag(id) - } - + val id = intent.getStringExtra("workTag") + if (!id.isNullOrBlank()){ + WorkManager.getInstance(c).cancelAllWorkByTag(id) } } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt index a51e66d8..f583aab8 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt @@ -108,65 +108,56 @@ class ShareActivity : BaseActivity() { runCatching { supportFragmentManager.popBackStack() } quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false) || sharedPreferences.getString("preferred_download_type", "video") == "command") - val intentData = when(action){ + val url = when(action){ Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!! else -> intent.dataString!! } - val matcher = Patterns.WEB_URL.matcher(intentData) + val matcher = Patterns.WEB_URL.matcher(url) val inputQuery = if (matcher.find()){ matcher.group() }else{ - intentData + url } - lifecycleScope.launch { - try { - val result = withContext(Dispatchers.IO){ - resultViewModel.getItemByURL(inputQuery) - } - if (result == null) throw Exception() - showDownloadSheet(result) - }catch (e: Exception){ - resultViewModel.deleteAll() - showDownloadSheet(downloadViewModel.createEmptyResultItem(inputQuery)) - if (matcher.matches()){ + val type = intent.getStringExtra("TYPE") + val background = intent.getBooleanExtra("BACKGROUND", false) + val command = intent.getStringExtra("COMMAND") ?: "" - }else{ - val res = withContext(Dispatchers.IO){ - resultViewModel.parseQuery(inputQuery, true) - } - if (res.isEmpty()) { - Toast.makeText(this@ShareActivity, "No Results Found!", Toast.LENGTH_SHORT).show() - exit() + lifecycleScope.launch { + var result = withContext(Dispatchers.IO){ + resultViewModel.getItemByURL(inputQuery) + } + if (result == null) { + resultViewModel.deleteAll() + result = downloadViewModel.createEmptyResultItem(inputQuery) + } + val downloadType = DownloadViewModel.Type.valueOf(type ?: downloadViewModel.getDownloadType(url = result.url).toString()) + if (sharedPreferences.getBoolean("download_card", true) && !background){ + val bottomSheet = DownloadBottomSheetDialog( + result = result, + type = downloadType) + bottomSheet.show(supportFragmentManager, "downloadSingleSheet") + }else{ + lifecycleScope.launch(Dispatchers.IO){ + val downloadItem = downloadViewModel.createDownloadItemFromResult( + result = result, + givenType = downloadType) + + if (downloadType == DownloadViewModel.Type.command && command.isNotBlank()){ + downloadItem.format.format_note = command }else{ - if (res.size == 1){ - showDownloadSheet(res[0]!!) - }else{ - showSelectPlaylistItems(res.toList()) - } + downloadItem.extraCommands = downloadItem.extraCommands + " $command" } + downloadViewModel.queueDownloads(listOf(downloadItem)) } + this@ShareActivity.finish() } } } } private fun showDownloadSheet(it: ResultItem){ - val downloadType = downloadViewModel.getDownloadType(DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!), it.url) - if (sharedPreferences.getBoolean("download_card", true)){ - val bottomSheet = DownloadBottomSheetDialog( - result = it, - type = downloadType) - bottomSheet.show(supportFragmentManager, "downloadSingleSheet") - }else{ - lifecycleScope.launch(Dispatchers.IO){ - val downloadItem = downloadViewModel.createDownloadItemFromResult( - result = it, - givenType = downloadType) - downloadViewModel.queueDownloads(listOf(downloadItem)) - } - this.finish() - } + } private fun showSelectPlaylistItems(it: List){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/ErrorDialogActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/ErrorDialogActivity.kt index baaaa980..57db0860 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/ErrorDialogActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/ErrorDialogActivity.kt @@ -51,7 +51,9 @@ open class ErrorDialogActivity : BaseActivity() { }.setOnDismissListener { this.finish() } - errDialog.show() + this.runOnUiThread { + errDialog.show() + } super.onCreate(savedInstanceState) } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt index c71affa0..b67af116 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt @@ -706,7 +706,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects!!.size == 1) { showSingleDownloadSheet( selectedObjects!![0], - DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!) + downloadViewModel.getDownloadType(url = selectedObjects!![0].url) ) }else{ val downloadList = withContext(Dispatchers.IO){ @@ -714,15 +714,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene } if (sharedPreferences!!.getBoolean("download_card", true)) { - if (selectedObjects!!.size == 1){ - showSingleDownloadSheet( - selectedObjects!![0], - DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!) - ) - }else{ - val bottomSheet = DownloadMultipleBottomSheetDialog(downloadList.toMutableList()) - bottomSheet.show(parentFragmentManager, "downloadMultipleSheet") - } + val bottomSheet = DownloadMultipleBottomSheetDialog(downloadList.toMutableList()) + bottomSheet.show(parentFragmentManager, "downloadMultipleSheet") } else { downloadViewModel.queueDownloads(downloadList) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt index baef007a..9164d506 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt @@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.ui.downloadcard import android.annotation.SuppressLint import android.app.Dialog import android.content.DialogInterface +import android.content.SharedPreferences import android.os.Bundle import android.util.DisplayMetrics import android.view.LayoutInflater @@ -10,9 +11,11 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.Toast +import androidx.core.content.edit import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import androidx.viewpager2.widget.ViewPager2 import com.deniscerri.ytdlnis.R @@ -41,6 +44,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private private lateinit var commandTemplateDao: CommandTemplateDao private lateinit var behavior: BottomSheetBehavior private lateinit var onDownloadItemUpdateListener: OnDownloadItemUpdateListener + private lateinit var sharedPreferences : SharedPreferences override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -48,6 +52,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao onDownloadItemUpdateListener = listener + sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -153,6 +158,10 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private override fun onPageSelected(position: Int) { tabLayout.selectTab(tabLayout.getTabAt(position)) runCatching { + sharedPreferences.edit(commit = true) { + putString("last_used_download_type", + listOf(Type.audio, Type.video, Type.command)[position].toString()) + } updateTitleAuthorWhenSwitching() } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt index e6979f7d..196422d2 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt @@ -12,6 +12,8 @@ import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.AutoCompleteTextView import android.widget.TextView +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.content.res.AppCompatResources import androidx.core.view.isEmpty import androidx.fragment.app.Fragment @@ -140,17 +142,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - val callback = FileUtil.getURIFromResult(this@DownloadAudioFragment, requireActivity()){result -> - downloadItem.downloadPath = result - //downloadViewModel.updateDownload(downloadItem) - saveDir.editText?.setText(FileUtil.formatPath(result), TextView.BufferType.EDITABLE) - - val free = FileUtil.convertFileSize( - File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace) - freeSpace.text = String.format( getString(R.string.freespace) + ": " + free) - if (free == "?") freeSpace.visibility = View.GONE - } - callback.launch(intent) + pathResultLauncher.launch(intent) } freeSpace = view.findViewById(R.id.freespace) val free = FileUtil.convertFileSize( @@ -296,4 +288,28 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private state.putBoolean("updated", true) onViewCreated(requireView(),savedInstanceState = state) } + + private var pathResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { + activity?.contentResolver?.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + + downloadItem.downloadPath = result.data?.data.toString() + //downloadViewModel.updateDownload(downloadItem) + saveDir.editText?.setText(FileUtil.formatPath(result.data?.data.toString()), TextView.BufferType.EDITABLE) + + val free = FileUtil.convertFileSize( + File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace) + freeSpace.text = String.format( getString(R.string.freespace) + ": " + free) + if (free == "?") freeSpace.visibility = View.GONE + + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt index 6e3afd86..ed0d8a82 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt @@ -1,12 +1,15 @@ package com.deniscerri.ytdlnis.ui.downloadcard import android.annotation.SuppressLint +import android.app.ActivityManager import android.app.Dialog +import android.content.Context import android.content.DialogInterface import android.content.Intent import android.content.SharedPreferences import android.content.res.Configuration import android.os.Bundle +import android.os.Looper import android.text.format.DateFormat import android.util.DisplayMetrics import android.util.Patterns @@ -16,6 +19,7 @@ import android.view.ViewGroup import android.widget.Button import android.widget.LinearLayout import android.widget.Toast +import androidx.core.content.edit import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider @@ -158,6 +162,11 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type } } + sharedPreferences.edit(commit = true) { + putString("last_used_download_type", + type.toString()) + } + tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { override fun onTabSelected(tab: TabLayout.Tab?) { @@ -184,6 +193,10 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type override fun onPageSelected(position: Int) { tabLayout.selectTab(tabLayout.getTabAt(position)) runCatching { + sharedPreferences.edit(commit = true) { + putString("last_used_download_type", + listOf(Type.audio, Type.video, Type.command)[position].toString()) + } updateTitleAuthorWhenSwitching() } } @@ -326,13 +339,18 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type if (result.url.isBlank()){ withContext(Dispatchers.Main){dismiss()} + return@launch } - val result = resultViewModel.parseQueries(listOf(result.url)) + val result = resultViewModel.parseQuery(result.url, true) if (result.isEmpty()){ withContext(Dispatchers.Main){ + Looper.prepare().run { + Toast.makeText(requireContext(), "No Results Found!", Toast.LENGTH_SHORT).show() + } dismiss() } + return@launch } if (result.size == 1 && result[0] != null){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt index f8b57796..8b543e08 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt @@ -55,7 +55,9 @@ import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -132,7 +134,10 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList) { + override fun onContinueOnBackground() { requireActivity().lifecycleScope.launch { val ids = mutableListOf() runBlocking { - items.map { it!!.status = DownloadRepository.Status.Saved.toString() } + items.map { it.status = DownloadRepository.Status.Saved.toString() } items.forEach { - ids.add(downloadViewModel.repository.insert(it!!)) + ids.add(downloadViewModel.repository.insert(it)) } } val id = System.currentTimeMillis().toInt() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt index 0bf86ea9..79be8d3c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt @@ -14,6 +14,7 @@ import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.AutoCompleteTextView import android.widget.TextView +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.content.res.AppCompatResources import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider @@ -130,16 +131,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - val callback = FileUtil.getURIFromResult(this@DownloadVideoFragment, requireActivity()){ result -> - downloadItem.downloadPath = result - saveDir.editText?.setText(FileUtil.formatPath(result), TextView.BufferType.EDITABLE) - - val free = FileUtil.convertFileSize( - File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace) - freeSpace.text = String.format( getString(R.string.freespace) + ": " + free) - if (free == "?") freeSpace.visibility = View.GONE - } - callback.launch(intent) + pathResultLauncher.launch(intent) } freeSpace = view.findViewById(R.id.freespace) @@ -311,5 +303,27 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private onViewCreated(requireView(),savedInstanceState = state) } + private var pathResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { + activity?.contentResolver?.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + + downloadItem.downloadPath = result.data?.data.toString() + saveDir.editText?.setText(FileUtil.formatPath(result.data?.data.toString()), TextView.BufferType.EDITABLE) + + val free = FileUtil.convertFileSize( + File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace) + freeSpace.text = String.format( getString(R.string.freespace) + ": " + free) + if (free == "?") freeSpace.visibility = View.GONE + + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index 95e23133..164e0bf0 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -130,7 +130,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p if (items.size > 10){ continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG) continueInBackgroundSnackBar.setAction(R.string.ok) { - listener.onContinueOnBackground(items) + listener.onContinueOnBackground() this@FormatSelectionBottomSheetDialog.dismiss() } continueInBackgroundSnackBar.show() @@ -369,7 +369,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p interface OnFormatClickListener{ fun onFormatClick(allFormats: List>, item: List) - fun onContinueOnBackground(items : List) {} + fun onContinueOnBackground() {} } class FormatTuple internal constructor( diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ResultCardDetailsDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ResultCardDetailsDialog.kt index fd89e166..e48d5fed 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ResultCardDetailsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ResultCardDetailsDialog.kt @@ -89,7 +89,6 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) infoUtil = InfoUtil(requireActivity()) - notificationUtil = NotificationUtil(requireActivity().applicationContext) downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/DownloadQueueMainFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/DownloadQueueMainFragment.kt index 8a8e91f4..4ab277f3 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/DownloadQueueMainFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/DownloadQueueMainFragment.kt @@ -160,14 +160,12 @@ class DownloadQueueMainFragment : Fragment(){ lifecycleScope.launch { val notificationUtil = NotificationUtil(requireContext()) val activeAndQueued = withContext(Dispatchers.IO){ - downloadViewModel.getActiveAndQueuedDownloads() + downloadViewModel.getActiveAndQueuedDownloadIDs() } - activeAndQueued.forEach { - it.status = DownloadRepository.Status.Cancelled.toString() - downloadViewModel.updateDownload(it) - val id = it.id.toInt() + downloadViewModel.cancelActiveQueued() + activeAndQueued.forEach { id -> YoutubeDL.getInstance().destroyProcessById(id.toString()) - notificationUtil.cancelDownloadNotification(id) + notificationUtil.cancelDownloadNotification(id.toInt()) } } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesFragment.kt index 6107b2b8..256ab035 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesFragment.kt @@ -106,7 +106,15 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener { newCookie.isEnabled = useCookiesPref } + private fun initChips() { + val new = view?.findViewById(R.id.newCookie) + new?.setOnClickListener { + showDialog("") + } + } + private fun initMenu() { + topAppBar.menu.getItem(1).isChecked = preferences.getBoolean("use_header", false) topAppBar.setOnMenuItemClickListener { m: MenuItem -> when (m.itemId) { R.id.delete_cookies -> { @@ -131,18 +139,15 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener { cookiesViewModel.exportToClipboard() Snackbar.make(recyclerView, getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show() } + R.id.use_header -> { + m.isChecked = !m.isChecked + preferences.edit().putBoolean("use_header", m.isChecked).apply() + } } true } } - private fun initChips() { - val new = view?.findViewById(R.id.newCookie) - new?.setOnClickListener { - showDialog("") - } - } - private fun showDialog(url: String){ val builder = MaterialAlertDialogBuilder(requireContext()) builder.setTitle(getString(R.string.cookies)) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/WebViewActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/WebViewActivity.kt index a8cf98f0..5cc4af0d 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/WebViewActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/WebViewActivity.kt @@ -1,18 +1,29 @@ package com.deniscerri.ytdlnis.ui.more import android.annotation.SuppressLint +import android.content.SharedPreferences import android.os.Bundle -import android.util.Log import android.webkit.CookieManager import android.webkit.WebView -import android.webkit.WebViewClient import android.widget.Toast +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceManager import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel import com.deniscerri.ytdlnis.ui.BaseActivity +import com.google.accompanist.web.AccompanistWebChromeClient +import com.google.accompanist.web.AccompanistWebViewClient +import com.google.accompanist.web.WebView +import com.google.accompanist.web.rememberWebViewState import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.button.MaterialButton @@ -24,12 +35,14 @@ import kotlinx.coroutines.withContext class WebViewActivity : BaseActivity() { private lateinit var cookiesViewModel: CookieViewModel private lateinit var webView: WebView + private lateinit var webViewCompose: ComposeView private lateinit var toolbar: MaterialToolbar private lateinit var generateBtn: MaterialButton private lateinit var cookieManager: CookieManager private lateinit var url: String private lateinit var cookies: String - + private lateinit var webViewClient: AccompanistWebViewClient + private lateinit var preferences: SharedPreferences @SuppressLint("SetJavaScriptEnabled") public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -40,13 +53,20 @@ class WebViewActivity : BaseActivity() { val appbar = findViewById(R.id.webview_appbarlayout) toolbar = appbar.findViewById(R.id.webviewToolbar) generateBtn = toolbar.findViewById(R.id.generate) + webViewCompose = findViewById(R.id.webview_compose) cookieManager = CookieManager.getInstance() cookieManager.removeAllCookies(null) + preferences = PreferenceManager.getDefaultSharedPreferences(this) - webView = findViewById(R.id.webview) - webView.settings.javaScriptEnabled = true - webView.settings.domStorageEnabled = true - webView.settings.javaScriptCanOpenWindowsAutomatically = true + webViewClient = object : AccompanistWebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + kotlin.runCatching { + toolbar.title = view?.title ?: "" + cookies = CookieManager.getInstance().getCookie(view?.url) + } + } + } cookieManager = CookieManager.getInstance() @@ -55,21 +75,8 @@ class WebViewActivity : BaseActivity() { onBackPressedDispatcher.onBackPressed() } - val webViewClient: WebViewClient = object : WebViewClient() { - - override fun onPageFinished(view: WebView, url: String) { - super.onPageFinished(view, url) - kotlin.runCatching { - toolbar.title = view.title - cookies = CookieManager.getInstance().getCookie(view.url) - } - } - } - webView.webViewClient = webViewClient - generateBtn.setOnClickListener { cookiesViewModel.getCookiesFromDB().getOrNull()?.let { - Log.e("asas", it) kotlin.runCatching { lifecycleScope.launch { withContext(Dispatchers.IO){ @@ -91,8 +98,40 @@ class WebViewActivity : BaseActivity() { onBackPressedDispatcher.onBackPressed() } - webView.loadUrl(url) + webViewCompose.apply { + setContent { WebViewView() } + } + } + @SuppressLint("SetJavaScriptEnabled") + @Composable + fun WebViewView() { + val webViewChromeClient = remember { + object : AccompanistWebChromeClient() { + } + } + + Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues -> + WebView( + state = rememberWebViewState(url), client = webViewClient, chromeClient = webViewChromeClient, + modifier = Modifier + .padding(paddingValues) + .fillMaxSize(), + captureBackPresses = false, factory = { context -> + WebView(context).apply { + settings.run { + javaScriptEnabled = true + domStorageEnabled = true + javaScriptCanOpenWindowsAutomatically = true + preferences.edit().putString("useragent_header", userAgentString).apply() + } + cookieManager.setAcceptThirdPartyCookies(this, true) + } + } + ) + + + } } companion object { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/DownloadSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/DownloadSettingsFragment.kt index d00d8d58..cc1e1ed8 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/DownloadSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/DownloadSettingsFragment.kt @@ -1,14 +1,13 @@ package com.deniscerri.ytdlnis.ui.more.settings -import android.app.PendingIntent import android.content.Intent import android.os.Bundle +import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceManager import androidx.preference.SwitchPreferenceCompat import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.receiver.AlarmStartReceiver -import com.deniscerri.ytdlnis.receiver.AlarmStopReceiver import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.work.AlarmScheduler import java.util.Calendar @@ -21,6 +20,14 @@ class DownloadSettingsFragment : BaseSettingsFragment() { setPreferencesFromResource(R.xml.downloading_preferences, rootKey) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val rememberDownloadType = findPreference("remember_download_type") + val downloadType = findPreference("preferred_download_type") + downloadType?.isEnabled = rememberDownloadType?.isChecked == false + rememberDownloadType?.setOnPreferenceClickListener { + downloadType?.isEnabled = !rememberDownloadType.isChecked + true + } + val useScheduler = findPreference("use_scheduler") val scheduleStart = findPreference("schedule_start") scheduleStart?.summary = preferences.getString("schedule_start", "00:00") diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalDownloadsListFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalDownloadsListFragment.kt index aefa168d..af174caf 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalDownloadsListFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalDownloadsListFragment.kt @@ -11,6 +11,7 @@ import android.view.View import android.view.ViewGroup import android.widget.HorizontalScrollView import android.widget.LinearLayout +import android.widget.RelativeLayout import android.widget.TextView import androidx.activity.addCallback import androidx.constraintlayout.widget.ConstraintLayout @@ -39,6 +40,7 @@ import kotlinx.coroutines.launch class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnItemClickListener { private var topAppBar: MaterialToolbar? = null + private lateinit var noResults: RelativeLayout private lateinit var terminalViewModel: TerminalViewModel override fun onCreateView( @@ -52,6 +54,7 @@ class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnIte override fun onViewCreated(view: View, savedInstanceState: Bundle?) { lifecycleScope.launch { + noResults = view.findViewById(R.id.no_results) val recycler = view.findViewById(R.id.terminal_recycler) val adapter = TerminalDownloadsAdapter(this@TerminalDownloadsListFragment, requireActivity()) recycler.adapter = adapter @@ -73,9 +76,7 @@ class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnIte terminalViewModel.getTerminals().collectLatest { adapter.submitList(it) - if (it.isEmpty()){ - findNavController().navigate(R.id.terminalFragment) - } + noResults.visibility = if (it.isEmpty()) View.VISIBLE else View.GONE } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalFragment.kt index 32d50783..c349d81c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/terminal/TerminalFragment.kt @@ -48,9 +48,12 @@ import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.yausername.youtubedl_android.YoutubeDL +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.lang.StringBuilder import kotlin.properties.Delegates @@ -102,11 +105,13 @@ class TerminalFragment : Fragment() { topAppBar!!.setNavigationOnClickListener { requireActivity().finish() } input = view.findViewById(R.id.command_edittext) + fab = view.findViewById(R.id.command_fab) if (arguments?.getLong("id") != null){ downloadID = requireArguments().getLong("id") if(downloadID != 0L){ input!!.visibility = View.GONE + showCancelFab() } } @@ -145,6 +150,7 @@ class TerminalFragment : Fragment() { }else{ bottomAppBar.menu.getItem(1).icon?.alpha = 255 } + } bottomAppBar.setOnMenuItemClickListener { when(it.itemId){ @@ -195,7 +201,6 @@ class TerminalFragment : Fragment() { output!!.setTextIsSelectable(true) output!!.layoutParams!!.width = LayoutParams.WRAP_CONTENT input!!.requestFocus() - fab = view.findViewById(R.id.command_fab) fab!!.setOnClickListener { if (fab!!.text == getString(R.string.run_command)){ input!!.visibility = View.GONE @@ -205,7 +210,7 @@ class TerminalFragment : Fragment() { lifecycleScope.launch { val command = input!!.text.toString().replace("yt-dlp", "") downloadID = withContext(Dispatchers.IO){ - terminalViewModel.insert(TerminalItem(command = command)) + terminalViewModel.insert(TerminalItem(command = command, log = output!!.text.toString())) } terminalViewModel.startTerminalDownloadWorker(TerminalItem(downloadID, command)) runWorkerListener() @@ -293,6 +298,22 @@ class TerminalFragment : Fragment() { } private fun runWorkerListener(){ + CoroutineScope(Dispatchers.IO).launch { + terminalViewModel.getTerminal(downloadID).collectLatest { + kotlin.runCatching { + requireActivity().runOnUiThread{ + if (it != null){ + if (!it.log.isNullOrBlank()) { + output?.text = it.log + } + output?.scrollTo(0, output!!.height) + scrollView?.fullScroll(View.FOCUS_DOWN) + } + } + } + + } + } WorkManager.getInstance(requireContext()) .getWorkInfosByTagLiveData(downloadID.toString()) @@ -310,25 +331,9 @@ class TerminalFragment : Fragment() { } return@forEach } - val outputData = work.progress.getString("output") - if (!outputData.isNullOrBlank()){ - val pieces = output!!.text.split("\n").toMutableList() - val lastLine = pieces.last() - requireActivity().runOnUiThread{ - kotlin.runCatching { - showCancelFab() - if (listOf(lastLine, outputData).all { it.contains("[download]") }){ - output!!.text = pieces.dropLast(1).joinToString("\n") - } - output!!.append("\n$outputData") - output!!.scrollTo(0, output!!.height) - scrollView!!.fullScroll(View.FOCUS_DOWN) - } - } - } - } } + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt index d8f926e5..ffefe18b 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt @@ -309,19 +309,4 @@ object FileUtil { return "${DecimalFormat("#,##0.#", symbols).format(s / 1024.0.pow(digitGroups.toDouble()))} ${units[digitGroups]}" } - fun getURIFromResult(f: Fragment, a: FragmentActivity?, callback: (data: String) -> Unit) = f.registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.let { - a?.contentResolver?.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - } - - callback(result.data?.data.toString()) - } - } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt index 1664c5e8..d8bd2f28 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt @@ -1,49 +1,26 @@ package com.deniscerri.ytdlnis.util -import android.app.Activity -import android.app.DownloadManager -import android.content.ClipboardManager import android.content.Context -import android.content.DialogInterface import android.content.Intent import android.content.SharedPreferences import android.content.res.Resources -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.os.Environment import android.os.Looper import android.text.Html import android.util.Log import android.util.Patterns -import android.util.TypedValue -import android.view.WindowManager import android.widget.Toast -import androidx.navigation.findNavController -import androidx.preference.AndroidResources import androidx.preference.PreferenceManager -import com.deniscerri.ytdlnis.App -import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.R -import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.models.ChapterItem import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.Format -import com.deniscerri.ytdlnis.database.models.LogItem import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.ui.ErrorDialogActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar import com.google.gson.Gson import com.google.gson.reflect.TypeToken import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDLRequest -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -475,6 +452,12 @@ class InfoUtil(private val context: Context) { FileUtil.getCookieFile(context){ request.addOption("--cookies", it) } + + val useHeader = sharedPreferences.getBoolean("use_header", false) + val header = sharedPreferences.getString("useragent_header", "") + if (useHeader && !header.isNullOrBlank()){ + request.addOption("--add-header","User-Agent:${header}") + } } val proxy = sharedPreferences.getString("proxy", "") @@ -482,6 +465,8 @@ class InfoUtil(private val context: Context) { request.addOption("--proxy", proxy) } + + val res = YoutubeDL.getInstance().execute(request) val results: Array = try { val lineSeparator = System.getProperty("line.separator") @@ -528,6 +513,12 @@ class InfoUtil(private val context: Context) { FileUtil.getCookieFile(context){ request.addOption("--cookies", it) } + + val useHeader = sharedPreferences.getBoolean("use_header", false) + val header = sharedPreferences.getString("useragent_header", "") + if (useHeader && !header.isNullOrBlank()){ + request.addOption("--add-header","User-Agent:${header}") + } } @@ -536,6 +527,8 @@ class InfoUtil(private val context: Context) { request.addOption("--proxy", proxy) } + + YoutubeDL.getInstance().execute(request){ progress, _, line -> try{ if (line.isNotBlank()){ @@ -592,6 +585,12 @@ class InfoUtil(private val context: Context) { FileUtil.getCookieFile(context){ request.addOption("--cookies", it) } + + val useHeader = sharedPreferences.getBoolean("use_header", false) + val header = sharedPreferences.getString("useragent_header", "") + if (useHeader && !header.isNullOrBlank()){ + request.addOption("--add-header","User-Agent:${header}") + } } val proxy = sharedPreferences.getString("proxy", "") @@ -599,6 +598,8 @@ class InfoUtil(private val context: Context) { request.addOption("--proxy", proxy) } + + val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val results: List = try { val lineSeparator = System.getProperty("line.separator") @@ -759,6 +760,12 @@ class InfoUtil(private val context: Context) { FileUtil.getCookieFile(context){ request.addOption("--cookies", it) } + + val useHeader = sharedPreferences.getBoolean("use_header", false) + val header = sharedPreferences.getString("useragent_header", "") + if (useHeader && !header.isNullOrBlank()){ + request.addOption("--add-header","User-Agent:${header}") + } } @@ -768,6 +775,8 @@ class InfoUtil(private val context: Context) { request.addOption("--proxy", proxy) } + + val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val jsonObject = JSONObject(youtubeDLResponse.out) @@ -921,6 +930,12 @@ class InfoUtil(private val context: Context) { FileUtil.getCookieFile(context){ request.addOption("--cookies", it) } + + val useHeader = sharedPreferences.getBoolean("use_header", false) + val header = sharedPreferences.getString("useragent_header", "") + if (useHeader && !header.isNullOrBlank()){ + request.addOption("--add-header","User-Agent:${header}") + } } @@ -930,6 +945,8 @@ class InfoUtil(private val context: Context) { request.addOption("--proxy", proxy) } + + val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val results: Array = try { val lineSeparator = System.getProperty("line.separator") @@ -958,22 +975,6 @@ class InfoUtil(private val context: Context) { } } - fun getFilePaths(request: YoutubeDLRequest) : List{ - return try{ - request.addOption("--print", "filename") - val res = YoutubeDL.getInstance().execute(request) - val results: Array = try { - val lineSeparator = System.getProperty("line.separator") - res.out.split(lineSeparator!!).toTypedArray() - } catch (e: Exception) { - arrayOf(res.out) - } - results.filter { !it.isNullOrBlank() }.map { it!!.substring(0, it.lastIndexOf(".")) }.map { it.substring(it.lastIndexOf("/") + 1, it.length) } - }catch (e: Exception){ - listOf() - } - } - fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ val url = downloadItem.url val request = YoutubeDLRequest(url) @@ -982,6 +983,9 @@ class InfoUtil(private val context: Context) { val downDir : File if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){ downDir = File(FileUtil.formatPath(downloadItem.downloadPath)) + request.addOption("--print", "after_video:'%(filename)s'") + request.addOption("--progress") + request.addOption("-v") }else{ val cacheDir = FileUtil.getCachePath(context) downDir = File(cacheDir, downloadItem.id.toString()) @@ -1010,6 +1014,27 @@ class InfoUtil(private val context: Context) { val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!! if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL) + if (sharedPreferences.getBoolean("restrict_filenames", true)) request.addOption("--restrict-filenames") + if (sharedPreferences.getBoolean("use_cookies", false)){ + FileUtil.getCookieFile(context){ + request.addOption("--cookies", it) + } + } + + val proxy = sharedPreferences.getString("proxy", "") + if (proxy!!.isNotBlank()){ + request.addOption("--proxy", proxy) + } + + val keepCache = sharedPreferences.getBoolean("keep_cache", false) + if(keepCache){ + request.addOption("--part") + request.addOption("--keep-fragments") + } + + val preferredAudioCodec = sharedPreferences.getString("audio_codec", "")!! + val aCodecPref = "ba[acodec~='^($preferredAudioCodec)']" + if(downloadItem.type != DownloadViewModel.Type.command){ request.addOption("--trim-filenames", downDir.absolutePath.length + 120) @@ -1106,34 +1131,19 @@ class InfoUtil(private val context: Context) { downloadItem.customFileNameTemplate = downloadItem.customFileNameTemplate.replace("%(uploader)s", "%(uploader,channel)s") } - if (sharedPreferences.getBoolean("restrict_filenames", true)) { - request.addOption("--restrict-filenames") - } - - if (sharedPreferences.getBoolean("use_cookies", false)){ - FileUtil.getCookieFile(context){ - request.addOption("--cookies", it) - } - } - - val proxy = sharedPreferences.getString("proxy", "") - if (proxy!!.isNotBlank()){ - request.addOption("--proxy", proxy) - } - - val keepCache = sharedPreferences.getBoolean("keep_cache", false) - if(keepCache){ - request.addOption("--part") - request.addOption("--keep-fragments") - } - when(type){ DownloadViewModel.Type.audio -> { val supportedContainers = context.resources.getStringArray(R.array.audio_containers) var audioQualityId : String = downloadItem.format.format_id - if (audioQualityId.isBlank() || audioQualityId == "0" || audioQualityId == context.getString(R.string.best_quality) || audioQualityId == "best") audioQualityId = "" - else if (audioQualityId == context.getString(R.string.worst_quality) || audioQualityId == "worst") audioQualityId = "worstaudio" + if (audioQualityId.isBlank() || listOf("0", context.getString(R.string.best_quality), "best", "").contains(audioQualityId)){ + audioQualityId = "" + if (preferredAudioCodec.isNotBlank()){ + audioQualityId = "${aCodecPref}/bestaudio" + } + }else if (listOf(context.getString(R.string.worst_quality), "worst").contains(audioQualityId)){ + audioQualityId = "worstaudio" + } val ext = downloadItem.container if (audioQualityId.isNotBlank()) request.addOption("-f", audioQualityId) @@ -1230,6 +1240,9 @@ class InfoUtil(private val context: Context) { val f = StringBuilder() if(!usingGenericFormat){ + if (preferredAudioCodec.isNotBlank()){ + f.append("$videof+$aCodecPref/") + } val aa = if (audiof.isNotBlank()) "+$audiof" else "" f.append("$videof$aa/$videof/best") @@ -1257,7 +1270,12 @@ class InfoUtil(private val context: Context) { .toString() .split(",") .filter { it.isNotEmpty() } - .ifEmpty { listOf(audiof) } + .ifEmpty { + val list = mutableListOf() + if (preferredAudioCodec.isNotBlank()) list.add("ba[acodec~='^($preferredAudioCodec)']") + list.add(audiof) + list + } val preferredAudioFormats = if (downloadItem.videoPreferences.audioFormatIDs.isEmpty()) { preferredAudioFormatIDs diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt index 2876a6ab..d638b96a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt @@ -7,19 +7,26 @@ import android.app.PendingIntent import android.app.TaskStackBuilder import android.content.Context import android.content.Intent +import android.content.SharedPreferences +import android.content.res.Configuration +import android.content.res.Resources import android.graphics.BitmapFactory import android.os.Build import android.os.Bundle +import android.util.DisplayMetrics import androidx.core.app.NotificationCompat import androidx.core.content.FileProvider import androidx.navigation.NavDeepLinkBuilder +import androidx.preference.PreferenceManager import com.deniscerri.ytdlnis.R +import com.deniscerri.ytdlnis.App.Companion.instance import com.deniscerri.ytdlnis.receiver.CancelDownloadNotificationReceiver import com.deniscerri.ytdlnis.receiver.CancelWorkReceiver import com.deniscerri.ytdlnis.receiver.PauseDownloadNotificationReceiver import com.deniscerri.ytdlnis.receiver.ResumeActivity import com.deniscerri.ytdlnis.receiver.ShareFileActivity import java.io.File +import java.util.Locale class NotificationUtil(var context: Context) { @@ -27,6 +34,15 @@ class NotificationUtil(var context: Context) { private val commandDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID) private val finishedDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_FINISHED_CHANNEL_ID) private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java) + private val resources: Resources + private var sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + + init { + val confTmp = Configuration(context.resources.configuration) + confTmp.setLocale(Locale(sharedPreferences.getString("app_language", "en")!!)) + val metrics = DisplayMetrics() + resources = Resources(context.assets, metrics, confTmp) + } fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -35,7 +51,7 @@ class NotificationUtil(var context: Context) { ) //downloading worker notification - var name: CharSequence = context.getString(R.string.downloading) + var name: CharSequence = resources.getString(R.string.downloading) var description = "WorkManager Default Notification" val importance = NotificationManager.IMPORTANCE_LOW var channel = NotificationChannel(DOWNLOAD_WORKER_CHANNEL_ID, name, importance) @@ -43,30 +59,30 @@ class NotificationUtil(var context: Context) { notificationManager.createNotificationChannel(channel) //gui downloads - name = context.getString(R.string.download_notification_channel_name) - description = context.getString(R.string.download_notification_channel_description) + name = resources.getString(R.string.download_notification_channel_name) + description = resources.getString(R.string.download_notification_channel_description) channel = NotificationChannel(DOWNLOAD_SERVICE_CHANNEL_ID, name, importance) channel.description = description notificationManager.createNotificationChannel(channel) //command downloads - name = context.getString(R.string.command_download_notification_channel_name) + name = resources.getString(R.string.command_download_notification_channel_name) description = - context.getString(R.string.command_download_notification_channel_description) + resources.getString(R.string.command_download_notification_channel_description) channel = NotificationChannel(COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID, name, importance) channel.description = description notificationManager.createNotificationChannel(channel) //finished or errored downloads - name = context.getString(R.string.finished_download_notification_channel_name) + name = resources.getString(R.string.finished_download_notification_channel_name) description = - context.getString(R.string.finished_download_notification_channel_description) + resources.getString(R.string.finished_download_notification_channel_description) channel = NotificationChannel(DOWNLOAD_FINISHED_CHANNEL_ID, name, NotificationManager.IMPORTANCE_HIGH) channel.description = description notificationManager.createNotificationChannel(channel) //misc - name = context.getString(R.string.misc) + name = resources.getString(R.string.misc) description = "" channel = NotificationChannel(DOWNLOAD_MISC_CHANNEL_ID, name, NotificationManager.IMPORTANCE_HIGH) channel.description = description @@ -87,12 +103,12 @@ class NotificationUtil(var context: Context) { val notificationBuilder = getBuilder(DOWNLOAD_WORKER_CHANNEL_ID) return notificationBuilder - .setContentTitle(context.getString(R.string.downloading)) + .setContentTitle(resources.getString(R.string.downloading)) .setOngoing(true) .setSmallIcon(R.drawable.ic_launcher_foreground_large) .setLargeIcon( BitmapFactory.decodeResource( - context.resources, + resources, R.drawable.ic_launcher_foreground_large ) ) @@ -118,7 +134,7 @@ class NotificationUtil(var context: Context) { .setSmallIcon(android.R.drawable.stat_sys_download) .setLargeIcon( BitmapFactory.decodeResource( - context.resources, + resources, android.R.drawable.stat_sys_download ) ) @@ -140,7 +156,7 @@ class NotificationUtil(var context: Context) { .setSmallIcon(R.drawable.ic_launcher_foreground_large) .setLargeIcon( BitmapFactory.decodeResource( - context.resources, + resources, R.drawable.ic_launcher_foreground_large ) ) @@ -157,7 +173,7 @@ class NotificationUtil(var context: Context) { PendingIntent.FLAG_IMMUTABLE ) - notificationBuilder.addAction(0, context.getString(R.string.resume), resumeNotificationPendingIntent) + notificationBuilder.addAction(0, resources.getString(R.string.resume), resumeNotificationPendingIntent) notificationManager.notify(DOWNLOAD_RESUME_NOTIFICATION_ID + itemID, notificationBuilder.build()) } @@ -165,11 +181,11 @@ class NotificationUtil(var context: Context) { val notificationBuilder = getBuilder(channel) notificationBuilder - .setContentTitle(context.getString(R.string.updating_download_data)) + .setContentTitle(resources.getString(R.string.updating_download_data)) .setSmallIcon(android.R.drawable.stat_sys_download) .setLargeIcon( BitmapFactory.decodeResource( - context.resources, + resources, android.R.drawable.stat_sys_download ) ) @@ -185,11 +201,11 @@ class NotificationUtil(var context: Context) { val notificationBuilder = getBuilder(DOWNLOAD_FINISHED_CHANNEL_ID) notificationBuilder - .setContentTitle("${context.getString(R.string.downloaded)} $title") + .setContentTitle("${resources.getString(R.string.downloaded)} $title") .setSmallIcon(R.drawable.ic_launcher_foreground_large) .setLargeIcon( BitmapFactory.decodeResource( - context.resources, + resources, R.drawable.ic_launcher_foreground_large ) ) @@ -228,8 +244,8 @@ class NotificationUtil(var context: Context) { PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE ) - notificationBuilder.addAction(0, context.getString(R.string.Open_File), openNotificationPendingIntent) - notificationBuilder.addAction(0, context.getString(R.string.share), shareNotificationPendingIntent) + notificationBuilder.addAction(0, resources.getString(R.string.Open_File), openNotificationPendingIntent) + notificationBuilder.addAction(0, resources.getString(R.string.share), shareNotificationPendingIntent) }catch (_: Exception){} } notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID, notificationBuilder.build()) @@ -263,12 +279,12 @@ class NotificationUtil(var context: Context) { .createPendingIntent() notificationBuilder - .setContentTitle("${context.getString(R.string.failed_download)}: $title") + .setContentTitle("${resources.getString(R.string.failed_download)}: $title") .setContentText(error) .setSmallIcon(R.drawable.ic_launcher_foreground_large) .setLargeIcon( BitmapFactory.decodeResource( - context.resources, + resources, R.drawable.ic_launcher_foreground_large ) ) @@ -278,7 +294,7 @@ class NotificationUtil(var context: Context) { .clearActions() if (logID != null){ notificationBuilder.setContentIntent(errorPendingIntent) - notificationBuilder.addAction(0, context.getString(R.string.logs), errorPendingIntent) + notificationBuilder.addAction(0, resources.getString(R.string.logs), errorPendingIntent) } notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID, notificationBuilder.build()) } @@ -299,7 +315,7 @@ class NotificationUtil(var context: Context) { val notificationBuilder = getBuilder(channel) var contentText = "" - if (queue > 1) contentText += """${queue - 1} ${context.getString(R.string.items_left)}""" + "\n" + if (queue > 1) contentText += """${queue - 1} ${resources.getString(R.string.items_left)}""" + "\n" contentText += desc.replace("\\[.*?\\] ".toRegex(), "") val pauseIntent = Intent(context, PauseDownloadNotificationReceiver::class.java) @@ -326,8 +342,8 @@ class NotificationUtil(var context: Context) { .setContentTitle(title) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) .clearActions() - .addAction(0, context.getString(R.string.pause), pauseNotificationPendingIntent) - .addAction(0, context.getString(R.string.cancel), cancelNotificationPendingIntent) + .addAction(0, resources.getString(R.string.pause), pauseNotificationPendingIntent) + .addAction(0, resources.getString(R.string.cancel), cancelNotificationPendingIntent) notificationManager.notify(id, notificationBuilder.build()) } catch (e: Exception) { e.printStackTrace() @@ -342,13 +358,13 @@ class NotificationUtil(var context: Context) { val notificationBuilder = getBuilder(downloadMiscChannelId) return notificationBuilder - .setContentTitle(context.getString(R.string.move_temporary_files)) + .setContentTitle(resources.getString(R.string.move_temporary_files)) .setOngoing(true) .setCategory(Notification.CATEGORY_PROGRESS) .setSmallIcon(android.R.drawable.stat_sys_download) .setLargeIcon( BitmapFactory.decodeResource( - context.resources, + resources, android.R.drawable.stat_sys_download ) ) @@ -367,7 +383,7 @@ class NotificationUtil(var context: Context) { val contentText = "${progress}/${totalFiles}" try { notificationBuilder.setProgress(100, progress, false) - .setContentTitle(context.getString(R.string.move_temporary_files)) + .setContentTitle(resources.getString(R.string.move_temporary_files)) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) notificationManager.notify(id, notificationBuilder.build()) } catch (e: Exception) { @@ -383,7 +399,7 @@ class NotificationUtil(var context: Context) { .setSmallIcon(android.R.drawable.stat_sys_download) .setLargeIcon( BitmapFactory.decodeResource( - context.resources, + resources, android.R.drawable.stat_sys_download ) ) @@ -394,26 +410,16 @@ class NotificationUtil(var context: Context) { return notificationBuilder.build() } - fun createFormatsUpdateNotification(workID: Int): Notification { + fun createFormatsUpdateNotification(): Notification { val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) - - val cancelIntent = Intent(context, CancelWorkReceiver::class.java) - cancelIntent.putExtra("workID", workID) - val cancelNotificationPendingIntent = PendingIntent.getBroadcast( - context, - workID, - cancelIntent, - PendingIntent.FLAG_IMMUTABLE - ) - return notificationBuilder - .setContentTitle(context.getString(R.string.update_formats_background)) + .setContentTitle(resources.getString(R.string.update_formats_background)) .setOngoing(true) .setCategory(Notification.CATEGORY_PROGRESS) .setSmallIcon(android.R.drawable.stat_sys_download) .setLargeIcon( BitmapFactory.decodeResource( - context.resources, + resources, android.R.drawable.stat_sys_download ) ) @@ -423,23 +429,37 @@ class NotificationUtil(var context: Context) { .setProgress(PROGRESS_MAX, PROGRESS_CURR, false) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .clearActions() - .addAction(0, context.getString(R.string.cancel), cancelNotificationPendingIntent) .build() } fun updateFormatUpdateNotification( - id: Int, + workID: Int, + workTag: String, progress: Int, queue: Int, ) { val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) - val contentText = """${queue - progress} ${context.getString(R.string.items_left)}""" + val contentText = """${queue - progress} ${resources.getString(R.string.items_left)}""" + + + val cancelIntent = Intent(context, CancelWorkReceiver::class.java) + cancelIntent.putExtra("workTag", workTag) + val cancelNotificationPendingIntent = PendingIntent.getBroadcast( + context, + workID, + cancelIntent, + PendingIntent.FLAG_IMMUTABLE + ) + + try { notificationBuilder.setProgress(queue, progress, false) - .setContentTitle(context.getString(R.string.update_formats_background)) + .setContentTitle(resources.getString(R.string.update_formats_background)) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) - notificationManager.notify(id, notificationBuilder.build()) + .clearActions() + .addAction(0, resources.getString(R.string.cancel), cancelNotificationPendingIntent) + notificationManager.notify(workID, notificationBuilder.build()) } catch (e: Exception) { e.printStackTrace() } @@ -460,11 +480,11 @@ class NotificationUtil(var context: Context) { .createPendingIntent() notificationBuilder - .setContentTitle(context.getString(R.string.finished_download_notification_channel_name)) + .setContentTitle(resources.getString(R.string.finished_download_notification_channel_name)) .setSmallIcon(R.drawable.ic_launcher_foreground_large) .setLargeIcon( BitmapFactory.decodeResource( - context.resources, + resources, R.drawable.ic_launcher_foreground_large ) ) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt index 84a39a53..d820c363 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt @@ -17,6 +17,7 @@ import androidx.work.WorkManager import androidx.work.WorkerParameters import androidx.work.await import androidx.work.workDataOf +import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.dao.DownloadDao @@ -49,7 +50,7 @@ class DownloadWorker( override suspend fun doWork(): Result { if (isStopped) return Result.success() - val notificationUtil = NotificationUtil(context) + val notificationUtil = NotificationUtil(App.instance) val infoUtil = InfoUtil(context) val dbManager = DBManager.getInstance(context) val dao = dbManager.downloadDao @@ -119,6 +120,8 @@ class DownloadWorker( val keepCache = sharedPreferences.getBoolean("keep_cache", false) val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false) + + val commandString = infoUtil.parseYTDLRequestString(request) val logItem = LogItem( 0, downloadItem.title.ifEmpty { downloadItem.url }, @@ -126,7 +129,7 @@ class DownloadWorker( "Title: ${downloadItem.title}\n" + "URL: ${downloadItem.url}\n" + "Type: ${downloadItem.type}\n" + - "Command:\n ${infoUtil.parseYTDLRequestString(request)}\n\n", + "Command:\n ${commandString}\n\n", downloadItem.format, downloadItem.type, System.currentTimeMillis(), @@ -142,6 +145,8 @@ class DownloadWorker( } + val noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite() + runCatching { YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line.chunked(5000).first().toString(), "id" to downloadItem.id)) @@ -162,16 +167,18 @@ class DownloadWorker( runBlocking { var finalPaths : List? - //if there was no cache used - if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){ + if (noCache){ setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id)) - val p = infoUtil.getFilePaths(request) + val p = it.out.split("\r") + .filter { it.startsWith("'/storage") } + .map { it.removePrefix("'") } + .map { it.removeSuffix("'\n") } finalPaths = File(FileUtil.formatPath(downloadLocation)) - .walkTopDown() - .filter { it.isFile && p.contains(it.nameWithoutExtension) } - .sortedByDescending { it.length() } - .map { it.absolutePath } - .toList() + .walkTopDown() + .filter { it.isFile && p.any { f -> f.contains(it.nameWithoutExtension) }} + .sortedByDescending { it.length() } + .map { it.absolutePath } + .toList() FileUtil.scanMedia(finalPaths, context) if (finalPaths.isEmpty()){ finalPaths = listOf(context.getString(R.string.unfound_file)) @@ -208,7 +215,7 @@ class DownloadWorker( val unixtime = System.currentTimeMillis() / 1000 val file = File(finalPaths?.first()!!) downloadItem.format.filesize = file.length() - val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id) + val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id, commandString) historyDao.insert(historyItem) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/MoveCacheFilesWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/MoveCacheFilesWorker.kt index 9ddba70f..6c35cf02 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/MoveCacheFilesWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/MoveCacheFilesWorker.kt @@ -15,6 +15,7 @@ import androidx.work.ForegroundInfo import androidx.work.Worker import androidx.work.WorkerParameters import androidx.work.workDataOf +import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.DBManager @@ -43,7 +44,7 @@ class MoveCacheFilesWorker( workerParams: WorkerParameters ) : Worker(context, workerParams) { override fun doWork(): Result { - val notificationUtil = NotificationUtil(context) + val notificationUtil = NotificationUtil(App.instance) val id = System.currentTimeMillis().toInt() val downloadFolders = File(context.cacheDir.absolutePath + "/downloads") diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt index 1d32f8fc..f4e75faa 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt @@ -72,6 +72,12 @@ class TerminalDownloadWorker( if (cookiesFile.exists()){ request.addOption("--cookies", cookiesFile.absolutePath) } + + val useHeader = sharedPreferences.getBoolean("use_header", false) + val header = sharedPreferences.getString("useragent_header", "") + if (useHeader && !header.isNullOrBlank()){ + request.addOption("--add-header","User-Agent:${header}") + } } request.addOption("-P", FileUtil.getDefaultCommandPath() + "/" + itemId) @@ -83,7 +89,7 @@ class TerminalDownloadWorker( "Terminal Download", "Downloading:\n" + "Terminal Download\n" + - "Command: ${infoUtil.parseYTDLRequestString(request)}\n\n", + "Command: ${command}\n\n", Format(), DownloadViewModel.Type.command, System.currentTimeMillis(), @@ -110,10 +116,9 @@ class TerminalDownloadWorker( line, progress.toInt(), 0, title, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID ) - if (logDownloads){ - CoroutineScope(Dispatchers.IO).launch { - logRepo.update(line, logItem.id) - } + CoroutineScope(Dispatchers.IO).launch { + if (logDownloads) logRepo.update(line, logItem.id) + dao.updateLog(line, itemId.toLong()) } } }.onSuccess { @@ -132,39 +137,30 @@ class TerminalDownloadWorker( } return runBlocking { - it.out.split("\n").forEach { - setProgress(workDataOf("progress" to 100, "output" to it, "id" to itemId, "log" to logDownloads)) - Thread.sleep(50) - } - - if (logDownloads){ - CoroutineScope(Dispatchers.IO).launch { - logRepo.update(it.out, logItem.id) - } + CoroutineScope(Dispatchers.IO).launch { + if (logDownloads) logRepo.update(it.out, logItem.id) + dao.updateLog(it.out, itemId.toLong()) + Thread.sleep(1000) + dao.delete(itemId.toLong()) } notificationUtil.cancelDownloadNotification(itemId) - dao.delete(itemId.toLong()) - return@runBlocking Result.success() + + Result.success() } }.onFailure { return runBlocking { - it.message?.chunked(10000)?.forEach { - setProgress(workDataOf("progress" to 100, "output" to it, "id" to itemId, "log" to logDownloads)) - Thread.sleep(100) - } - - if (logDownloads){ - CoroutineScope(Dispatchers.IO).launch { - if (it.message != null){ - logRepo.update(it.message!!, logItem.id) - } + CoroutineScope(Dispatchers.IO).launch { + if (it.message != null){ + if (logDownloads) logRepo.update(it.message!!, logItem.id) + dao.updateLog(it.message!!, itemId.toLong()) + Thread.sleep(1000) + dao.delete(itemId.toLong()) } } + notificationUtil.cancelDownloadNotification(itemId) File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively() Log.e(TAG, context.getString(R.string.failed_download), it) - notificationUtil.cancelDownloadNotification(itemId) - dao.delete(itemId.toLong()) - return@runBlocking Result.failure() + Result.failure() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/UpdatePlaylistFormatsWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/UpdatePlaylistFormatsWorker.kt index f3c0c1a6..b3cb6648 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/UpdatePlaylistFormatsWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/UpdatePlaylistFormatsWorker.kt @@ -1,50 +1,23 @@ package com.deniscerri.ytdlnis.work import android.content.Context -import android.os.Handler -import android.os.Looper import android.util.Log -import android.widget.Toast -import androidx.lifecycle.ViewModelProvider -import androidx.navigation.NavDeepLinkBuilder -import androidx.preference.PreferenceManager -import androidx.work.CoroutineWorker -import androidx.work.Data import androidx.work.ForegroundInfo import androidx.work.Worker import androidx.work.WorkerParameters -import androidx.work.workDataOf import com.deniscerri.ytdlnis.App -import com.deniscerri.ytdlnis.R -import com.deniscerri.ytdlnis.database.Converters import com.deniscerri.ytdlnis.database.DBManager -import com.deniscerri.ytdlnis.database.dao.DownloadDao -import com.deniscerri.ytdlnis.database.dao.ResultDao -import com.deniscerri.ytdlnis.database.models.DownloadItem -import com.deniscerri.ytdlnis.database.models.HistoryItem -import com.deniscerri.ytdlnis.database.models.LogItem -import com.deniscerri.ytdlnis.database.repository.DownloadRepository -import com.deniscerri.ytdlnis.database.repository.LogRepository import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel -import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.NotificationUtil -import com.deniscerri.ytdlnis.util.NotificationUtil.Companion.FORMAT_UPDATING_NOTIFICATION_ID -import com.deniscerri.ytdlnis.util.UpdateUtil -import com.yausername.youtubedl_android.YoutubeDL -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import java.io.File class UpdatePlaylistFormatsWorker( private val context: Context, workerParams: WorkerParameters -) : CoroutineWorker(context, workerParams) { - override suspend fun doWork(): Result { +) : Worker(context, workerParams) { + override fun doWork(): Result { val dbManager = DBManager.getInstance(context) val dao = dbManager.downloadDao val resDao = dbManager.resultDao @@ -55,47 +28,59 @@ class UpdatePlaylistFormatsWorker( val workID = inputData.getInt("id", 0) if (workID == 0) return Result.failure() - val notification = notificationUtil.createFormatsUpdateNotification(workID) - val foregroundInfo = ForegroundInfo(FORMAT_UPDATING_NOTIFICATION_ID, notification) - setForeground(foregroundInfo) + val notification = notificationUtil.createFormatsUpdateNotification() + val foregroundInfo = ForegroundInfo(workID, notification) + setForegroundAsync(foregroundInfo) var count = 0 - ids.forEach { - if (!isStopped){ - val d = dao.getDownloadById(it) - val r = resDao.getResultByURL(d.url) - if (d.allFormats.isNotEmpty()){ + return try{ + ids.forEach { + if (!isStopped){ + val d = dao.getDownloadById(it) + val r = resDao.getResultByURL(d.url) + + if (d.allFormats.isNotEmpty()){ + count++ + return@forEach + } + + runCatching { + d.allFormats.clear() + d.allFormats.addAll(infoUtil.getFormats(d.url)) + d.format = vm.getFormat(d.allFormats,d.type) + + r.formats.clear() + r.formats.addAll(d.allFormats) + + runBlocking { + resDao.update(r) + dao.delete(d.id) + } + } + + count++ - return@forEach + notificationUtil.updateFormatUpdateNotification(workID, UpdatePlaylistFormatsWorker::class.java.name, count, ids.size) + }else{ + throw Exception() } + } - runCatching { - d.allFormats.clear() - d.allFormats.addAll(infoUtil.getFormats(d.url)) - d.format = vm.getFormat(d.allFormats,d.type) - - r.formats.clear() - r.formats.addAll(d.allFormats) - - dao.update(d) - resDao.update(r) - } - - - count++ - notificationUtil.updateFormatUpdateNotification(FORMAT_UPDATING_NOTIFICATION_ID, count, ids.size) - }else{ - notificationUtil.cancelDownloadNotification(FORMAT_UPDATING_NOTIFICATION_ID) + Result.success() + }catch (e: Exception){ + notificationUtil.cancelDownloadNotification(workID) + ids.clear() + Result.failure() + }finally { + if (ids.isNotEmpty()){ + notificationUtil.showFormatsUpdatedNotification(ids) } } + } - - - runBlocking { - notificationUtil.showFormatsUpdatedNotification(ids) - } - - return Result.success() + override fun onStopped() { + Log.e("asd", "Asd") + super.onStopped() } } \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_terminal_download_list.xml b/app/src/main/res/layout/fragment_terminal_download_list.xml index ff257ec7..6f9ac26c 100644 --- a/app/src/main/res/layout/fragment_terminal_download_list.xml +++ b/app/src/main/res/layout/fragment_terminal_download_list.xml @@ -18,4 +18,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/webview_activity.xml b/app/src/main/res/layout/webview_activity.xml index 36c0430d..7cab57b7 100644 --- a/app/src/main/res/layout/webview_activity.xml +++ b/app/src/main/res/layout/webview_activity.xml @@ -43,14 +43,16 @@ - + diff --git a/app/src/main/res/menu/cookies_menu.xml b/app/src/main/res/menu/cookies_menu.xml index c1c1ac35..1806fce9 100644 --- a/app/src/main/res/menu/cookies_menu.xml +++ b/app/src/main/res/menu/cookies_menu.xml @@ -6,6 +6,12 @@ android:title="@string/delete_all_cookies" app:showAsAction="never" /> + + Preferred Audio Codec Loading Sponsorblock API URL + Remember Download Type + Select the last opened download type for the current item + User-Agent header \ No newline at end of file diff --git a/app/src/main/res/xml/downloading_preferences.xml b/app/src/main/res/xml/downloading_preferences.xml index d9d1f6db..48d3a404 100644 --- a/app/src/main/res/xml/downloading_preferences.xml +++ b/app/src/main/res/xml/downloading_preferences.xml @@ -53,6 +53,19 @@ app:summary="@string/preferred_download_type_summary" app:title="@string/preferred_download_type" /> + + + + + + + + \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index c740938e..fda45d67 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -1,38 +1,49 @@ -download audio/video files from more than 1000 websites -process playlists -edit every playlist item separately just like in a normal download item. -select a common format for all items and/or select multiple audio formats in case you are downloading them as a video -select a download path for all items -select a filename template for all items -batch update download type to audio/video/custom command in one click -queue downloads and schedule them by date and time -you can also schedule multiple items at the same time -download multiple items at the same time -use custom commands and templates or go full yt-dlp mode with a built in terminal -you can backup and restore templates, so you can share with your buddies -COOKIES support. Log in with your accounts and download private/unavailable videos, unlock premium formats etc. -cut videos based on timestamps and video chapters (This yt-dlp feature is experimental on the original project) -you can make unlimited cuts -remove sponsorblock elements from the item -embed them as a chapters in your video -embed subtitles/metadata/chapters etc -modify metadata such as title and author -split item into separate files depending on its chapters -select different download formats -bottom card right from the share menu, no need to open the app -you can create a txt file and fill it with links/playlists/search queries separate by a new line and the app will process them -search or insert a link from the app -you can stack searches so you can process them at the same time -log downloads in case of problems -re-download cancelled or failed downloads -you can use gestures to swipe left to redownload and right to delete -you can long click the redownload button in the details sheet to show the download card for more functionality -incognito mode when you don't want to save a download history or logs -quick download mode -download immediately without having to wait for data to process. Turn off the bottom card and it will instantly start -open / share downloaded files right from the finished notification -most yt-dlp features are implemented, suggestions are welcome -Material You interface -Theming options -Backup and restore features. (Almost everything can be backed up) -MVVM Architecture w/ WorkManager \ No newline at end of file +YTDLnis lets you download audio/video files from more than 1000 websites, and offers a lot of features. For example: + +process playlists + +- edit every playlist item separately just like in a normal download item. +- select a common format for all items and/or select multiple audio formats in case you are downloading them as a video +- select a download path for all items +- select a filename template for all items +- batch update download type to audio/video/custom command in one click +- queue downloads and schedule them by date and time +- you can also schedule multiple items at the same time +- download multiple items at the same time +- use custom commands and templates or go full yt-dlp mode with a built in terminal +- you can backup and restore templates, so you can share with your buddies + +COOKIES support + +- Log in with your accounts and download private/unavailable videos, unlock premium formats etc. + +Different ways of modifying the file + +- cut videos based on timestamps and video chapters (This yt-dlp feature is experimental on the original project) +- you can make unlimited cuts +- remove sponsorblock elements from the item +- embed them as a chapters in your video +- embed subtitles/metadata/chapters etc +- modify metadata such as title and author +- split item into separate files depending on its chapters +- select different download formats + +Other features + +- bottom card right from the share menu, no need to open the app +- you can create a txt file and fill it with links/playlists/search queries separate by a new line and the app will process them +- search or insert a link from the app +- you can stack searches so you can process them at the same time +- log downloads in case of problems +- re-download cancelled or failed downloads +- you can use gestures to swipe left to redownload and right to delete +- you can long click the redownload button in the details sheet to show the download card for more functionality +- incognito mode when you don't want to save a download history or logs +- quick download mode +- download immediately without having to wait for data to process. Turn off the bottom card and it will instantly start +- open / share downloaded files right from the finished notification +- most yt-dlp features are implemented, suggestions are welcome +- Material You interface +- Theming options +- Backup and restore features. (Almost everything can be backed up) +- MVVM Architecture w/ WorkManager \ No newline at end of file