1.6.8.2-beta

This commit is contained in:
deniscerri 2023-10-16 09:02:16 +02:00
parent a922633567
commit 559efca908
No known key found for this signature in database
GPG key ID: 95C43D517D830350
46 changed files with 1269 additions and 474 deletions

View file

@ -9,8 +9,8 @@ plugins {
def properties = new Properties() def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 6 def versionMinor = 6
def versionPatch = 7 def versionPatch = 8
def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionBuild = 2 // bump for dogfood builds, public betas, etc.
def versionExt = "" def versionExt = ""
if (versionBuild > 0){ if (versionBuild > 0){
@ -136,6 +136,7 @@ dependencies {
implementation "androidx.navigation:navigation-ui-ktx:$navVer" implementation "androidx.navigation:navigation-ui-ktx:$navVer"
implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.test.ext:junit-ktx:1.1.5' implementation 'androidx.test.ext:junit-ktx:1.1.5'
implementation 'androidx.compose.ui:ui-android:1.5.3'
testImplementation "junit:junit:$junitVer" testImplementation "junit:junit:$junitVer"
androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer"
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
@ -186,4 +187,6 @@ dependencies {
implementation "com.anggrayudi:storage:1.5.5" implementation "com.anggrayudi:storage:1.5.5"
implementation 'me.zhanghai.android.fastscroll:library:1.3.0' 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'
} }

View file

@ -2,11 +2,11 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 10, "version": 10,
"identityHash": "6b5efd73749a7138035e1f00349715c8", "identityHash": "0647c4b527efea884e1dc26157cd4189",
"entities": [ "entities": [
{ {
"tableName": "results", "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": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -42,7 +42,8 @@
"fieldPath": "thumb", "fieldPath": "thumb",
"columnName": "thumb", "columnName": "thumb",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true,
"defaultValue": "''"
}, },
{ {
"fieldPath": "website", "fieldPath": "website",
@ -93,7 +94,7 @@
}, },
{ {
"tableName": "history", "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": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -167,6 +168,13 @@
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true, "notNull": true,
"defaultValue": "0" "defaultValue": "0"
},
{
"fieldPath": "command",
"columnName": "command",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
} }
], ],
"primaryKey": { "primaryKey": {
@ -538,7 +546,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "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')"
] ]
} }
} }

View file

@ -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')"
]
}
}

View file

@ -194,6 +194,7 @@
</activity> </activity>
<receiver android:name=".receiver.CancelDownloadNotificationReceiver" /> <receiver android:name=".receiver.CancelDownloadNotificationReceiver" />
<receiver android:name=".receiver.CancelWorkReceiver" />
<receiver android:name=".receiver.PauseDownloadNotificationReceiver" /> <receiver android:name=".receiver.PauseDownloadNotificationReceiver" />
<receiver android:name=".receiver.AlarmStartReceiver" /> <receiver android:name=".receiver.AlarmStartReceiver" />
<receiver android:name=".receiver.AlarmStopReceiver" /> <receiver android:name=".receiver.AlarmStopReceiver" />

View file

@ -5,19 +5,7 @@ import android.os.Looper
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit import androidx.core.content.edit
import androidx.preference.PreferenceManager 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.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.aria2c.Aria2c
import com.yausername.ffmpeg.FFmpeg import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
@ -27,8 +15,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.* import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class App : Application() { class App : Application() {

View file

@ -58,8 +58,8 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
// THUMBNAIL ---------------------------------- // THUMBNAIL ----------------------------------
if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){ if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("home")){
val imageURL = item!!.thumb val imageURL = item?.thumb
if (imageURL.isNotEmpty()) { if (!imageURL.isNullOrBlank()) {
uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) } uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) }
} else { } else {
uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) } uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) }

View file

@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.Flow
LogItem::class, LogItem::class,
TerminalItem::class TerminalItem::class
], ],
version = 10, version = 11,
autoMigrations = [ autoMigrations = [
AutoMigration (from = 1, to = 2), AutoMigration (from = 1, to = 2),
AutoMigration (from = 2, to = 3), AutoMigration (from = 2, to = 3),
@ -33,7 +33,8 @@ import kotlinx.coroutines.flow.Flow
AutoMigration (from = 6, to = 7), AutoMigration (from = 6, to = 7),
AutoMigration (from = 7, to = 8), AutoMigration (from = 7, to = 8),
AutoMigration (from = 8, to = 9), AutoMigration (from = 8, to = 9),
AutoMigration (from = 9, to = 10) AutoMigration (from = 9, to = 10),
AutoMigration (from = 10, to = 11)
] ]
) )
abstract class DBManager : RoomDatabase(){ abstract class DBManager : RoomDatabase(){

View file

@ -34,6 +34,9 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem> fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("SELECT id FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'")
fun getActiveAndQueuedDownloadIDs() : List<Long>
@Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'")
fun getActiveAndQueuedDownloads() : Flow<List<DownloadItem>> fun getActiveAndQueuedDownloads() : Flow<List<DownloadItem>>
@ -122,8 +125,8 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE id in (:list)") @Query("DELETE FROM downloads WHERE id in (:list)")
suspend fun deleteAllWithIDs(list: List<Long>) suspend fun deleteAllWithIDs(list: List<Long>)
@Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued' or status='QueuedPaused'") @Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued' or status='QueuedPaused' or status='Active' or status='Paused'")
suspend fun cancelQueued() suspend fun cancelActiveQueued()
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
suspend fun deleteSingleProcessing(id: Long) suspend fun deleteSingleProcessing(id: Long)

View file

@ -35,6 +35,9 @@ interface HistoryDao {
@Query("SELECT * FROM history WHERE id=:id LIMIT 1") @Query("SELECT * FROM history WHERE id=:id LIMIT 1")
suspend fun getHistoryItem(id: Int) : HistoryItem suspend fun getHistoryItem(id: Int) : HistoryItem
@Query("SELECT * FROM history WHERE url=:url")
fun getAllHistoryByURL(url: String) : List<HistoryItem>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(item: HistoryItem) suspend fun insert(item: HistoryItem)

View file

@ -11,15 +11,38 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface TerminalDao { interface TerminalDao {
@Query("SELECT * FROM terminalDownloads ORDER BY id") @Query("SELECT id,command FROM terminalDownloads ORDER BY id")
fun getActiveTerminalDownloads() : List<TerminalItem> fun getActiveTerminalDownloads() : List<TerminalItem>
@Query("SELECT * FROM terminalDownloads ORDER BY id") @Query("SELECT id,command FROM terminalDownloads ORDER BY id")
fun getActiveTerminalDownloadsFlow() : Flow<List<TerminalItem>> fun getActiveTerminalDownloadsFlow() : Flow<List<TerminalItem>>
@Query("SELECT * from terminalDownloads where id=:id")
fun getActiveTerminalFlow(id: Long) : Flow<TerminalItem>
@Query("SELECT COUNT(*) FROM terminalDownloads") @Query("SELECT COUNT(*) FROM terminalDownloads")
fun getActiveTerminalsCount() : Int 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 @Insert
suspend fun insert(item: TerminalItem) : Long suspend fun insert(item: TerminalItem) : Long

View file

@ -20,5 +20,7 @@ data class HistoryItem(
val website: String, val website: String,
val format: Format, val format: Format,
@ColumnInfo(defaultValue = "0") @ColumnInfo(defaultValue = "0")
val downloadId: Long val downloadId: Long,
@ColumnInfo(defaultValue = "")
val command: String = ""
) )

View file

@ -9,5 +9,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
data class TerminalItem( data class TerminalItem(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
var id: Long = 0, var id: Long = 0,
var command: String var command: String,
@ColumnInfo(defaultValue = "")
var log: String? = "",
) )

View file

@ -83,6 +83,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getActiveAndQueuedDownloadsList() return downloadDao.getActiveAndQueuedDownloadsList()
} }
fun getActiveAndQueuedDownloadIDs() : List<Long> {
return downloadDao.getActiveAndQueuedDownloadIDs()
}
fun getQueuedDownloads() : List<DownloadItem> { fun getQueuedDownloads() : List<DownloadItem> {
return downloadDao.getQueuedDownloadsList() return downloadDao.getQueuedDownloadsList()
} }
@ -120,8 +124,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
} }
suspend fun cancelQueued(){ suspend fun cancelActiveQueued(){
downloadDao.cancelQueued() downloadDao.cancelActiveQueued()
} }
fun pauseDownloads(){ fun pauseDownloads(){

View file

@ -2,6 +2,7 @@ package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources 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.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.dao.DownloadDao 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.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.AudioPreferences import com.deniscerri.ytdlnis.database.models.AudioPreferences
import com.deniscerri.ytdlnis.database.models.CommandTemplate 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.ResultItem
import com.deniscerri.ytdlnis.database.models.VideoPreferences import com.deniscerri.ytdlnis.database.models.VideoPreferences
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.ui.ErrorDialogActivity
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler import com.deniscerri.ytdlnis.work.AlarmScheduler
@ -82,6 +85,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
private var videoCodec: String? private var videoCodec: String?
private var audioCodec: String? private var audioCodec: String?
private val dao: DownloadDao private val dao: DownloadDao
private val historyDao: HistoryDao
enum class Type { enum class Type {
auto, audio, video, command auto, audio, video, command
@ -96,6 +100,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
init { init {
dbManager = DBManager.getInstance(application) dbManager = DBManager.getInstance(application)
dao = dbManager.downloadDao dao = dbManager.downloadDao
historyDao = dbManager.historyDao
repository = DownloadRepository(dao) repository = DownloadRepository(dao)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
@ -173,8 +178,20 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return repository.getItemByID(id) return repository.getItemByID(id)
} }
fun getDownloadType(t: Type, url: String) : Type { fun getDownloadType(t: Type? = null, url: String) : Type {
return when(t){ 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 -> { Type.auto -> {
if (urlsForAudioType.any { url.contains(it) }){ if (urlsForAudioType.any { url.contains(it) }){
Type.audio Type.audio
@ -182,7 +199,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Type.video Type.video
} }
} }
else -> t
else -> type
} }
} }
@ -448,10 +466,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
fun turnResultItemsToDownloadItems(items: List<ResultItem?>) : List<DownloadItem> { fun turnResultItemsToDownloadItems(items: List<ResultItem?>) : List<DownloadItem> {
val list : MutableList<DownloadItem> = mutableListOf() val list : MutableList<DownloadItem> = mutableListOf()
var preferredType = sharedPreferences.getString("preferred_download_type", "video")
items.forEach { items.forEach {
preferredType = getDownloadType(Type.valueOf(preferredType!!), it!!.url).toString() val preferredType = getDownloadType(url = it!!.url).toString()
list.add(createDownloadItemFromResult(result = it, givenType = Type.valueOf(preferredType!!))) list.add(createDownloadItemFromResult(result = it, givenType = Type.valueOf(
preferredType
)))
} }
return list return list
} }
@ -487,12 +506,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
fun deleteAllWithID(ids: List<Long>) = viewModelScope.launch(Dispatchers.IO){ fun deleteAllWithID(ids: List<Long>) = viewModelScope.launch(Dispatchers.IO){
Log.e("Aa", ids.size.toString())
repository.deleteAllWithIDs(ids) repository.deleteAllWithIDs(ids)
} }
fun cancelQueued() = viewModelScope.launch(Dispatchers.IO) { fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
repository.cancelQueued() repository.cancelActiveQueued()
} }
fun getQueued() : List<DownloadItem> { fun getQueued() : List<DownloadItem> {
@ -528,6 +546,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
return repository.getActiveAndQueuedDownloads() return repository.getActiveAndQueuedDownloads()
} }
fun getActiveAndQueuedDownloadIDs() : List<Long>{
return repository.getActiveAndQueuedDownloadIDs()
}
suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch { suspend fun resetScheduleTimeForItemsAndStartDownload(items: List<Long>) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.resetScheduleTimeForItems(items) dbManager.downloadDao.resetScheduleTimeForItems(items)
startDownloadWorker(emptyList()) startDownloadWorker(emptyList())
@ -548,7 +570,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val alarmScheduler = AlarmScheduler(context) val alarmScheduler = AlarmScheduler(context)
val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads() val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads()
val queuedItems = mutableListOf<DownloadItem>() val queuedItems = mutableListOf<DownloadItem>()
var exists = false val existing = mutableListOf<DownloadItem>()
//if scheduler is on //if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
@ -556,8 +578,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
alarmScheduler.schedule() alarmScheduler.schedule()
} }
items.forEach { items.forEach {
if (it.status != DownloadRepository.Status.Paused.toString()) it.status = DownloadRepository.Status.Queued.toString() 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 -> if (activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0 d.id = 0
d.logID = null d.logID = null
@ -565,23 +590,33 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
d.status = DownloadRepository.Status.Queued.toString() d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString() d.toString() == it.toString()
} != null) { } != null) {
exists = true existing.add(it)
}else{ }else{
if (it.id == 0L){ //check if downloaded and file exists
val id = runBlocking {repository.insert(it)} val history = historyDao.getAllHistoryByURL(it.url).filter { item -> FileUtil.exists(item.downloadPath) }
it.id = id if (history.any { h -> h.command.replace("-P \"(.*?)\"".toRegex(), "") == parsedCurrentCommand.replace("-P \"(.*?)\"".toRegex(), "") }){
}else if (it.status == DownloadRepository.Status.Queued.toString()){ existing.add(it)
repository.update(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){ if (existing.isNotEmpty()){
Looper.prepare().run { val intent = Intent(context, ErrorDialogActivity::class.java)
Toast.makeText(context, context.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show() 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 } ){ if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || items.any { it.downloadStartTime > 0L } ){

View file

@ -71,6 +71,10 @@ class TerminalViewModel(private val application: Application) : AndroidViewModel
return dao.getActiveTerminalDownloadsFlow() return dao.getActiveTerminalDownloadsFlow()
} }
fun getTerminal(id: Long) : Flow<TerminalItem> {
return dao.getActiveTerminalFlow(id)
}
suspend fun insert(item: TerminalItem) : Long { suspend fun insert(item: TerminalItem) : Long {
return dao.insert(item) return dao.insert(item)
} }

View file

@ -27,7 +27,6 @@ class AlarmStopReceiver : BroadcastReceiver() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
runCatching { runCatching {
Log.e("assa", "assssssssssssssssssssssssssssss")
val active = dbManager.downloadDao.getActiveDownloadsList() val active = dbManager.downloadDao.getActiveDownloadsList()
val startingTime = preferences.getString("schedule_start", "00:00")!! val startingTime = preferences.getString("schedule_start", "00:00")!!

View file

@ -16,12 +16,9 @@ import kotlinx.coroutines.runBlocking
class CancelWorkReceiver : BroadcastReceiver() { class CancelWorkReceiver : BroadcastReceiver() {
override fun onReceive(c: Context, intent: Intent) { override fun onReceive(c: Context, intent: Intent) {
val id = intent.getStringExtra("workID") val id = intent.getStringExtra("workTag")
if (!id.isNullOrBlank()) { if (!id.isNullOrBlank()){
runCatching { WorkManager.getInstance(c).cancelAllWorkByTag(id)
WorkManager.getInstance(c).cancelAllWorkByTag(id)
}
} }
} }
} }

View file

@ -108,65 +108,56 @@ class ShareActivity : BaseActivity() {
runCatching { supportFragmentManager.popBackStack() } runCatching { supportFragmentManager.popBackStack() }
quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false) || sharedPreferences.getString("preferred_download_type", "video") == "command") 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)!! Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!!
else -> intent.dataString!! else -> intent.dataString!!
} }
val matcher = Patterns.WEB_URL.matcher(intentData) val matcher = Patterns.WEB_URL.matcher(url)
val inputQuery = if (matcher.find()){ val inputQuery = if (matcher.find()){
matcher.group() matcher.group()
}else{ }else{
intentData url
} }
lifecycleScope.launch { val type = intent.getStringExtra("TYPE")
try { val background = intent.getBooleanExtra("BACKGROUND", false)
val result = withContext(Dispatchers.IO){ val command = intent.getStringExtra("COMMAND") ?: ""
resultViewModel.getItemByURL(inputQuery)
}
if (result == null) throw Exception()
showDownloadSheet(result)
}catch (e: Exception){
resultViewModel.deleteAll()
showDownloadSheet(downloadViewModel.createEmptyResultItem(inputQuery))
if (matcher.matches()){
}else{ lifecycleScope.launch {
val res = withContext(Dispatchers.IO){ var result = withContext(Dispatchers.IO){
resultViewModel.parseQuery(inputQuery, true) resultViewModel.getItemByURL(inputQuery)
} }
if (res.isEmpty()) { if (result == null) {
Toast.makeText(this@ShareActivity, "No Results Found!", Toast.LENGTH_SHORT).show() resultViewModel.deleteAll()
exit() 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{ }else{
if (res.size == 1){ downloadItem.extraCommands = downloadItem.extraCommands + " $command"
showDownloadSheet(res[0]!!)
}else{
showSelectPlaylistItems(res.toList())
}
} }
downloadViewModel.queueDownloads(listOf(downloadItem))
} }
this@ShareActivity.finish()
} }
} }
} }
} }
private fun showDownloadSheet(it: ResultItem){ 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<ResultItem?>){ private fun showSelectPlaylistItems(it: List<ResultItem?>){

View file

@ -51,7 +51,9 @@ open class ErrorDialogActivity : BaseActivity() {
}.setOnDismissListener { }.setOnDismissListener {
this.finish() this.finish()
} }
errDialog.show() this.runOnUiThread {
errDialog.show()
}
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
} }

View file

@ -706,7 +706,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects!!.size == 1) { if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects!!.size == 1) {
showSingleDownloadSheet( showSingleDownloadSheet(
selectedObjects!![0], selectedObjects!![0],
DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!) downloadViewModel.getDownloadType(url = selectedObjects!![0].url)
) )
}else{ }else{
val downloadList = withContext(Dispatchers.IO){ val downloadList = withContext(Dispatchers.IO){
@ -714,15 +714,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
} }
if (sharedPreferences!!.getBoolean("download_card", true)) { if (sharedPreferences!!.getBoolean("download_card", true)) {
if (selectedObjects!!.size == 1){ val bottomSheet = DownloadMultipleBottomSheetDialog(downloadList.toMutableList())
showSingleDownloadSheet( bottomSheet.show(parentFragmentManager, "downloadMultipleSheet")
selectedObjects!![0],
DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!)
)
}else{
val bottomSheet = DownloadMultipleBottomSheetDialog(downloadList.toMutableList())
bottomSheet.show(parentFragmentManager, "downloadMultipleSheet")
}
} else { } else {
downloadViewModel.queueDownloads(downloadList) downloadViewModel.queueDownloads(downloadList)
} }

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Dialog import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
@ -10,9 +11,11 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
@ -41,6 +44,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
private lateinit var commandTemplateDao: CommandTemplateDao private lateinit var commandTemplateDao: CommandTemplateDao
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var onDownloadItemUpdateListener: OnDownloadItemUpdateListener private lateinit var onDownloadItemUpdateListener: OnDownloadItemUpdateListener
private lateinit var sharedPreferences : SharedPreferences
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -48,6 +52,7 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao
onDownloadItemUpdateListener = listener onDownloadItemUpdateListener = listener
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -153,6 +158,10 @@ class ConfigureDownloadBottomSheetDialog(private var result: ResultItem, private
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
tabLayout.selectTab(tabLayout.getTabAt(position)) tabLayout.selectTab(tabLayout.getTabAt(position))
runCatching { runCatching {
sharedPreferences.edit(commit = true) {
putString("last_used_download_type",
listOf(Type.audio, Type.video, Type.command)[position].toString())
}
updateTitleAuthorWhenSwitching() updateTitleAuthorWhenSwitching()
} }
} }

View file

@ -12,6 +12,8 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isEmpty import androidx.core.view.isEmpty
import androidx.fragment.app.Fragment 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_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
val callback = FileUtil.getURIFromResult(this@DownloadAudioFragment, requireActivity()){result -> pathResultLauncher.launch(intent)
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)
} }
freeSpace = view.findViewById(R.id.freespace) freeSpace = view.findViewById(R.id.freespace)
val free = FileUtil.convertFileSize( val free = FileUtil.convertFileSize(
@ -296,4 +288,28 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
state.putBoolean("updated", true) state.putBoolean("updated", true)
onViewCreated(requireView(),savedInstanceState = state) 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
}
}
} }

View file

@ -1,12 +1,15 @@
package com.deniscerri.ytdlnis.ui.downloadcard package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.ActivityManager
import android.app.Dialog import android.app.Dialog
import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.os.Looper
import android.text.format.DateFormat import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Patterns import android.util.Patterns
@ -16,6 +19,7 @@ import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.Toast import android.widget.Toast
import androidx.core.content.edit
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider 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 { tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) { override fun onTabSelected(tab: TabLayout.Tab?) {
@ -184,6 +193,10 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
tabLayout.selectTab(tabLayout.getTabAt(position)) tabLayout.selectTab(tabLayout.getTabAt(position))
runCatching { runCatching {
sharedPreferences.edit(commit = true) {
putString("last_used_download_type",
listOf(Type.audio, Type.video, Type.command)[position].toString())
}
updateTitleAuthorWhenSwitching() updateTitleAuthorWhenSwitching()
} }
} }
@ -326,13 +339,18 @@ class DownloadBottomSheetDialog(private var result: ResultItem, private val type
if (result.url.isBlank()){ if (result.url.isBlank()){
withContext(Dispatchers.Main){dismiss()} withContext(Dispatchers.Main){dismiss()}
return@launch
} }
val result = resultViewModel.parseQueries(listOf(result.url)) val result = resultViewModel.parseQuery(result.url, true)
if (result.isEmpty()){ if (result.isEmpty()){
withContext(Dispatchers.Main){ withContext(Dispatchers.Main){
Looper.prepare().run {
Toast.makeText(requireContext(), "No Results Found!", Toast.LENGTH_SHORT).show()
}
dismiss() dismiss()
} }
return@launch
} }
if (result.size == 1 && result[0] != null){ if (result.size == 1 && result[0] != null){

View file

@ -55,7 +55,9 @@ import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -132,7 +134,10 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
item.downloadStartTime = it.timeInMillis item.downloadStartTime = it.timeInMillis
} }
runBlocking { runBlocking {
downloadViewModel.queueDownloads(items) val chunks = items.chunked(10)
for (c in chunks) {
downloadViewModel.queueDownloads(c)
}
val first = items.first() val first = items.first()
val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(first.downloadStartTime) val date = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(first.downloadStartTime)
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show() Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + date, Toast.LENGTH_LONG).show()
@ -145,7 +150,11 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
scheduleBtn.isEnabled = false scheduleBtn.isEnabled = false
download.isEnabled = false download.isEnabled = false
runBlocking { runBlocking {
downloadViewModel.queueDownloads(items) val chunks = items.chunked(10)
for (c in chunks) {
downloadViewModel.queueDownloads(c)
}
} }
dismiss() dismiss()
} }
@ -206,13 +215,13 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
} }
override fun onContinueOnBackground(items: List<DownloadItem?>) { override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch { requireActivity().lifecycleScope.launch {
val ids = mutableListOf<Long>() val ids = mutableListOf<Long>()
runBlocking { runBlocking {
items.map { it!!.status = DownloadRepository.Status.Saved.toString() } items.map { it.status = DownloadRepository.Status.Saved.toString() }
items.forEach { items.forEach {
ids.add(downloadViewModel.repository.insert(it!!)) ids.add(downloadViewModel.repository.insert(it))
} }
} }
val id = System.currentTimeMillis().toInt() val id = System.currentTimeMillis().toInt()

View file

@ -14,6 +14,7 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider 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_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
val callback = FileUtil.getURIFromResult(this@DownloadVideoFragment, requireActivity()){ result -> pathResultLauncher.launch(intent)
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)
} }
freeSpace = view.findViewById(R.id.freespace) freeSpace = view.findViewById(R.id.freespace)
@ -311,5 +303,27 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
onViewCreated(requireView(),savedInstanceState = state) 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
}
}
} }

View file

@ -130,7 +130,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
if (items.size > 10){ if (items.size > 10){
continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG) continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG)
continueInBackgroundSnackBar.setAction(R.string.ok) { continueInBackgroundSnackBar.setAction(R.string.ok) {
listener.onContinueOnBackground(items) listener.onContinueOnBackground()
this@FormatSelectionBottomSheetDialog.dismiss() this@FormatSelectionBottomSheetDialog.dismiss()
} }
continueInBackgroundSnackBar.show() continueInBackgroundSnackBar.show()
@ -369,7 +369,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
interface OnFormatClickListener{ interface OnFormatClickListener{
fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>) fun onFormatClick(allFormats: List<List<Format>>, item: List<FormatTuple>)
fun onContinueOnBackground(items : List<DownloadItem?>) {} fun onContinueOnBackground() {}
} }
class FormatTuple internal constructor( class FormatTuple internal constructor(

View file

@ -89,7 +89,6 @@ class ResultCardDetailsDialog(private val item: ResultItem) : BottomSheetDialogF
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity()) infoUtil = InfoUtil(requireActivity())
notificationUtil = NotificationUtil(requireActivity().applicationContext)
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager downloadManager = requireContext().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager

View file

@ -160,14 +160,12 @@ class DownloadQueueMainFragment : Fragment(){
lifecycleScope.launch { lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext()) val notificationUtil = NotificationUtil(requireContext())
val activeAndQueued = withContext(Dispatchers.IO){ val activeAndQueued = withContext(Dispatchers.IO){
downloadViewModel.getActiveAndQueuedDownloads() downloadViewModel.getActiveAndQueuedDownloadIDs()
} }
activeAndQueued.forEach { downloadViewModel.cancelActiveQueued()
it.status = DownloadRepository.Status.Cancelled.toString() activeAndQueued.forEach { id ->
downloadViewModel.updateDownload(it)
val id = it.id.toInt()
YoutubeDL.getInstance().destroyProcessById(id.toString()) YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id) notificationUtil.cancelDownloadNotification(id.toInt())
} }
} }
} }

View file

@ -106,7 +106,15 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
newCookie.isEnabled = useCookiesPref newCookie.isEnabled = useCookiesPref
} }
private fun initChips() {
val new = view?.findViewById<Chip>(R.id.newCookie)
new?.setOnClickListener {
showDialog("")
}
}
private fun initMenu() { private fun initMenu() {
topAppBar.menu.getItem(1).isChecked = preferences.getBoolean("use_header", false)
topAppBar.setOnMenuItemClickListener { m: MenuItem -> topAppBar.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) { when (m.itemId) {
R.id.delete_cookies -> { R.id.delete_cookies -> {
@ -131,18 +139,15 @@ class CookiesFragment : Fragment(), CookieAdapter.OnItemClickListener {
cookiesViewModel.exportToClipboard() cookiesViewModel.exportToClipboard()
Snackbar.make(recyclerView, getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show() 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 true
} }
} }
private fun initChips() {
val new = view?.findViewById<Chip>(R.id.newCookie)
new?.setOnClickListener {
showDialog("")
}
}
private fun showDialog(url: String){ private fun showDialog(url: String){
val builder = MaterialAlertDialogBuilder(requireContext()) val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(getString(R.string.cookies)) builder.setTitle(getString(R.string.cookies))

View file

@ -1,18 +1,29 @@
package com.deniscerri.ytdlnis.ui.more package com.deniscerri.ytdlnis.ui.more
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast 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.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.ui.BaseActivity 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.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -24,12 +35,14 @@ import kotlinx.coroutines.withContext
class WebViewActivity : BaseActivity() { class WebViewActivity : BaseActivity() {
private lateinit var cookiesViewModel: CookieViewModel private lateinit var cookiesViewModel: CookieViewModel
private lateinit var webView: WebView private lateinit var webView: WebView
private lateinit var webViewCompose: ComposeView
private lateinit var toolbar: MaterialToolbar private lateinit var toolbar: MaterialToolbar
private lateinit var generateBtn: MaterialButton private lateinit var generateBtn: MaterialButton
private lateinit var cookieManager: CookieManager private lateinit var cookieManager: CookieManager
private lateinit var url: String private lateinit var url: String
private lateinit var cookies: String private lateinit var cookies: String
private lateinit var webViewClient: AccompanistWebViewClient
private lateinit var preferences: SharedPreferences
@SuppressLint("SetJavaScriptEnabled") @SuppressLint("SetJavaScriptEnabled")
public override fun onCreate(savedInstanceState: Bundle?) { public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -40,13 +53,20 @@ class WebViewActivity : BaseActivity() {
val appbar = findViewById<AppBarLayout>(R.id.webview_appbarlayout) val appbar = findViewById<AppBarLayout>(R.id.webview_appbarlayout)
toolbar = appbar.findViewById(R.id.webviewToolbar) toolbar = appbar.findViewById(R.id.webviewToolbar)
generateBtn = toolbar.findViewById(R.id.generate) generateBtn = toolbar.findViewById(R.id.generate)
webViewCompose = findViewById(R.id.webview_compose)
cookieManager = CookieManager.getInstance() cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookies(null) cookieManager.removeAllCookies(null)
preferences = PreferenceManager.getDefaultSharedPreferences(this)
webView = findViewById(R.id.webview) webViewClient = object : AccompanistWebViewClient() {
webView.settings.javaScriptEnabled = true override fun onPageFinished(view: WebView?, url: String?) {
webView.settings.domStorageEnabled = true super.onPageFinished(view, url)
webView.settings.javaScriptCanOpenWindowsAutomatically = true kotlin.runCatching {
toolbar.title = view?.title ?: ""
cookies = CookieManager.getInstance().getCookie(view?.url)
}
}
}
cookieManager = CookieManager.getInstance() cookieManager = CookieManager.getInstance()
@ -55,21 +75,8 @@ class WebViewActivity : BaseActivity() {
onBackPressedDispatcher.onBackPressed() 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 { generateBtn.setOnClickListener {
cookiesViewModel.getCookiesFromDB().getOrNull()?.let { cookiesViewModel.getCookiesFromDB().getOrNull()?.let {
Log.e("asas", it)
kotlin.runCatching { kotlin.runCatching {
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
@ -91,8 +98,40 @@ class WebViewActivity : BaseActivity() {
onBackPressedDispatcher.onBackPressed() 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 { companion object {

View file

@ -1,14 +1,13 @@
package com.deniscerri.ytdlnis.ui.more.settings package com.deniscerri.ytdlnis.ui.more.settings
import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.preference.ListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat import androidx.preference.SwitchPreferenceCompat
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.receiver.AlarmStartReceiver import com.deniscerri.ytdlnis.receiver.AlarmStartReceiver
import com.deniscerri.ytdlnis.receiver.AlarmStopReceiver
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler import com.deniscerri.ytdlnis.work.AlarmScheduler
import java.util.Calendar import java.util.Calendar
@ -21,6 +20,14 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
setPreferencesFromResource(R.xml.downloading_preferences, rootKey) setPreferencesFromResource(R.xml.downloading_preferences, rootKey)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val rememberDownloadType = findPreference<SwitchPreferenceCompat>("remember_download_type")
val downloadType = findPreference<ListPreference>("preferred_download_type")
downloadType?.isEnabled = rememberDownloadType?.isChecked == false
rememberDownloadType?.setOnPreferenceClickListener {
downloadType?.isEnabled = !rememberDownloadType.isChecked
true
}
val useScheduler = findPreference<SwitchPreferenceCompat>("use_scheduler") val useScheduler = findPreference<SwitchPreferenceCompat>("use_scheduler")
val scheduleStart = findPreference<Preference>("schedule_start") val scheduleStart = findPreference<Preference>("schedule_start")
scheduleStart?.summary = preferences.getString("schedule_start", "00:00") scheduleStart?.summary = preferences.getString("schedule_start", "00:00")

View file

@ -11,6 +11,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.HorizontalScrollView import android.widget.HorizontalScrollView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.activity.addCallback import androidx.activity.addCallback
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
@ -39,6 +40,7 @@ import kotlinx.coroutines.launch
class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnItemClickListener { class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnItemClickListener {
private var topAppBar: MaterialToolbar? = null private var topAppBar: MaterialToolbar? = null
private lateinit var noResults: RelativeLayout
private lateinit var terminalViewModel: TerminalViewModel private lateinit var terminalViewModel: TerminalViewModel
override fun onCreateView( override fun onCreateView(
@ -52,6 +54,7 @@ class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnIte
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
lifecycleScope.launch { lifecycleScope.launch {
noResults = view.findViewById(R.id.no_results)
val recycler = view.findViewById<RecyclerView>(R.id.terminal_recycler) val recycler = view.findViewById<RecyclerView>(R.id.terminal_recycler)
val adapter = TerminalDownloadsAdapter(this@TerminalDownloadsListFragment, requireActivity()) val adapter = TerminalDownloadsAdapter(this@TerminalDownloadsListFragment, requireActivity())
recycler.adapter = adapter recycler.adapter = adapter
@ -73,9 +76,7 @@ class TerminalDownloadsListFragment : Fragment(), TerminalDownloadsAdapter.OnIte
terminalViewModel.getTerminals().collectLatest { terminalViewModel.getTerminals().collectLatest {
adapter.submitList(it) adapter.submitList(it)
if (it.isEmpty()){ noResults.visibility = if (it.isEmpty()) View.VISIBLE else View.GONE
findNavController().navigate(R.id.terminalFragment)
}
} }
} }

View file

@ -48,9 +48,12 @@ import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.StringBuilder
import kotlin.properties.Delegates import kotlin.properties.Delegates
@ -102,11 +105,13 @@ class TerminalFragment : Fragment() {
topAppBar!!.setNavigationOnClickListener { requireActivity().finish() } topAppBar!!.setNavigationOnClickListener { requireActivity().finish() }
input = view.findViewById(R.id.command_edittext) input = view.findViewById(R.id.command_edittext)
fab = view.findViewById(R.id.command_fab)
if (arguments?.getLong("id") != null){ if (arguments?.getLong("id") != null){
downloadID = requireArguments().getLong("id") downloadID = requireArguments().getLong("id")
if(downloadID != 0L){ if(downloadID != 0L){
input!!.visibility = View.GONE input!!.visibility = View.GONE
showCancelFab()
} }
} }
@ -145,6 +150,7 @@ class TerminalFragment : Fragment() {
}else{ }else{
bottomAppBar.menu.getItem(1).icon?.alpha = 255 bottomAppBar.menu.getItem(1).icon?.alpha = 255
} }
} }
bottomAppBar.setOnMenuItemClickListener { bottomAppBar.setOnMenuItemClickListener {
when(it.itemId){ when(it.itemId){
@ -195,7 +201,6 @@ class TerminalFragment : Fragment() {
output!!.setTextIsSelectable(true) output!!.setTextIsSelectable(true)
output!!.layoutParams!!.width = LayoutParams.WRAP_CONTENT output!!.layoutParams!!.width = LayoutParams.WRAP_CONTENT
input!!.requestFocus() input!!.requestFocus()
fab = view.findViewById(R.id.command_fab)
fab!!.setOnClickListener { fab!!.setOnClickListener {
if (fab!!.text == getString(R.string.run_command)){ if (fab!!.text == getString(R.string.run_command)){
input!!.visibility = View.GONE input!!.visibility = View.GONE
@ -205,7 +210,7 @@ class TerminalFragment : Fragment() {
lifecycleScope.launch { lifecycleScope.launch {
val command = input!!.text.toString().replace("yt-dlp", "") val command = input!!.text.toString().replace("yt-dlp", "")
downloadID = withContext(Dispatchers.IO){ downloadID = withContext(Dispatchers.IO){
terminalViewModel.insert(TerminalItem(command = command)) terminalViewModel.insert(TerminalItem(command = command, log = output!!.text.toString()))
} }
terminalViewModel.startTerminalDownloadWorker(TerminalItem(downloadID, command)) terminalViewModel.startTerminalDownloadWorker(TerminalItem(downloadID, command))
runWorkerListener() runWorkerListener()
@ -293,6 +298,22 @@ class TerminalFragment : Fragment() {
} }
private fun runWorkerListener(){ 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()) WorkManager.getInstance(requireContext())
.getWorkInfosByTagLiveData(downloadID.toString()) .getWorkInfosByTagLiveData(downloadID.toString())
@ -310,25 +331,9 @@ class TerminalFragment : Fragment() {
} }
return@forEach 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)
}
}
}
} }
} }
} }
} }

View file

@ -309,19 +309,4 @@ object FileUtil {
return "${DecimalFormat("#,##0.#", symbols).format(s / 1024.0.pow(digitGroups.toDouble()))} ${units[digitGroups]}" 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())
}
}
} }

View file

@ -1,49 +1,26 @@
package com.deniscerri.ytdlnis.util package com.deniscerri.ytdlnis.util
import android.app.Activity
import android.app.DownloadManager
import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources 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.os.Looper
import android.text.Html import android.text.Html
import android.util.Log import android.util.Log
import android.util.Patterns import android.util.Patterns
import android.util.TypedValue
import android.view.WindowManager
import android.widget.Toast import android.widget.Toast
import androidx.navigation.findNavController
import androidx.preference.AndroidResources
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.ChapterItem import com.deniscerri.ytdlnis.database.models.ChapterItem
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format 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.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.ui.ErrorDialogActivity 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.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
@ -475,6 +452,12 @@ class InfoUtil(private val context: Context) {
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) 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", "") val proxy = sharedPreferences.getString("proxy", "")
@ -482,6 +465,8 @@ class InfoUtil(private val context: Context) {
request.addOption("--proxy", proxy) request.addOption("--proxy", proxy)
} }
val res = YoutubeDL.getInstance().execute(request) val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try { val results: Array<String?> = try {
val lineSeparator = System.getProperty("line.separator") val lineSeparator = System.getProperty("line.separator")
@ -528,6 +513,12 @@ class InfoUtil(private val context: Context) {
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) 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) request.addOption("--proxy", proxy)
} }
YoutubeDL.getInstance().execute(request){ progress, _, line -> YoutubeDL.getInstance().execute(request){ progress, _, line ->
try{ try{
if (line.isNotBlank()){ if (line.isNotBlank()){
@ -592,6 +585,12 @@ class InfoUtil(private val context: Context) {
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) 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", "") val proxy = sharedPreferences.getString("proxy", "")
@ -599,6 +598,8 @@ class InfoUtil(private val context: Context) {
request.addOption("--proxy", proxy) request.addOption("--proxy", proxy)
} }
val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val results: List<String?> = try { val results: List<String?> = try {
val lineSeparator = System.getProperty("line.separator") val lineSeparator = System.getProperty("line.separator")
@ -759,6 +760,12 @@ class InfoUtil(private val context: Context) {
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) 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) request.addOption("--proxy", proxy)
} }
val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val jsonObject = JSONObject(youtubeDLResponse.out) val jsonObject = JSONObject(youtubeDLResponse.out)
@ -921,6 +930,12 @@ class InfoUtil(private val context: Context) {
FileUtil.getCookieFile(context){ FileUtil.getCookieFile(context){
request.addOption("--cookies", it) 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) request.addOption("--proxy", proxy)
} }
val youtubeDLResponse = YoutubeDL.getInstance().execute(request) val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try { val results: Array<String?> = try {
val lineSeparator = System.getProperty("line.separator") val lineSeparator = System.getProperty("line.separator")
@ -958,22 +975,6 @@ class InfoUtil(private val context: Context) {
} }
} }
fun getFilePaths(request: YoutubeDLRequest) : List<String>{
return try{
request.addOption("--print", "filename")
val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = 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{ fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val url = downloadItem.url val url = downloadItem.url
val request = YoutubeDLRequest(url) val request = YoutubeDLRequest(url)
@ -982,6 +983,9 @@ class InfoUtil(private val context: Context) {
val downDir : File val downDir : File
if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){ if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){
downDir = File(FileUtil.formatPath(downloadItem.downloadPath)) downDir = File(FileUtil.formatPath(downloadItem.downloadPath))
request.addOption("--print", "after_video:'%(filename)s'")
request.addOption("--progress")
request.addOption("-v")
}else{ }else{
val cacheDir = FileUtil.getCachePath(context) val cacheDir = FileUtil.getCachePath(context)
downDir = File(cacheDir, downloadItem.id.toString()) downDir = File(cacheDir, downloadItem.id.toString())
@ -1010,6 +1014,27 @@ class InfoUtil(private val context: Context) {
val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!! val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!!
if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL) if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL)
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){ if(downloadItem.type != DownloadViewModel.Type.command){
request.addOption("--trim-filenames", downDir.absolutePath.length + 120) 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") 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){ when(type){
DownloadViewModel.Type.audio -> { DownloadViewModel.Type.audio -> {
val supportedContainers = context.resources.getStringArray(R.array.audio_containers) val supportedContainers = context.resources.getStringArray(R.array.audio_containers)
var audioQualityId : String = downloadItem.format.format_id var audioQualityId : String = downloadItem.format.format_id
if (audioQualityId.isBlank() || audioQualityId == "0" || audioQualityId == context.getString(R.string.best_quality) || audioQualityId == "best") audioQualityId = "" if (audioQualityId.isBlank() || listOf("0", context.getString(R.string.best_quality), "best", "").contains(audioQualityId)){
else if (audioQualityId == context.getString(R.string.worst_quality) || audioQualityId == "worst") audioQualityId = "worstaudio" 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 val ext = downloadItem.container
if (audioQualityId.isNotBlank()) request.addOption("-f", audioQualityId) if (audioQualityId.isNotBlank()) request.addOption("-f", audioQualityId)
@ -1230,6 +1240,9 @@ class InfoUtil(private val context: Context) {
val f = StringBuilder() val f = StringBuilder()
if(!usingGenericFormat){ if(!usingGenericFormat){
if (preferredAudioCodec.isNotBlank()){
f.append("$videof+$aCodecPref/")
}
val aa = if (audiof.isNotBlank()) "+$audiof" else "" val aa = if (audiof.isNotBlank()) "+$audiof" else ""
f.append("$videof$aa/$videof/best") f.append("$videof$aa/$videof/best")
@ -1257,7 +1270,12 @@ class InfoUtil(private val context: Context) {
.toString() .toString()
.split(",") .split(",")
.filter { it.isNotEmpty() } .filter { it.isNotEmpty() }
.ifEmpty { listOf(audiof) } .ifEmpty {
val list = mutableListOf<String>()
if (preferredAudioCodec.isNotBlank()) list.add("ba[acodec~='^($preferredAudioCodec)']")
list.add(audiof)
list
}
val preferredAudioFormats = if (downloadItem.videoPreferences.audioFormatIDs.isEmpty()) { val preferredAudioFormats = if (downloadItem.videoPreferences.audioFormatIDs.isEmpty()) {
preferredAudioFormatIDs preferredAudioFormatIDs

View file

@ -7,19 +7,26 @@ import android.app.PendingIntent
import android.app.TaskStackBuilder import android.app.TaskStackBuilder
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.navigation.NavDeepLinkBuilder import androidx.navigation.NavDeepLinkBuilder
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.App.Companion.instance
import com.deniscerri.ytdlnis.receiver.CancelDownloadNotificationReceiver import com.deniscerri.ytdlnis.receiver.CancelDownloadNotificationReceiver
import com.deniscerri.ytdlnis.receiver.CancelWorkReceiver import com.deniscerri.ytdlnis.receiver.CancelWorkReceiver
import com.deniscerri.ytdlnis.receiver.PauseDownloadNotificationReceiver import com.deniscerri.ytdlnis.receiver.PauseDownloadNotificationReceiver
import com.deniscerri.ytdlnis.receiver.ResumeActivity import com.deniscerri.ytdlnis.receiver.ResumeActivity
import com.deniscerri.ytdlnis.receiver.ShareFileActivity import com.deniscerri.ytdlnis.receiver.ShareFileActivity
import java.io.File import java.io.File
import java.util.Locale
class NotificationUtil(var context: Context) { 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 commandDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID)
private val finishedDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_FINISHED_CHANNEL_ID) private val finishedDownloadNotificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(context, DOWNLOAD_FINISHED_CHANNEL_ID)
private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java) private val notificationManager: NotificationManager = context.getSystemService(NotificationManager::class.java)
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() { fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -35,7 +51,7 @@ class NotificationUtil(var context: Context) {
) )
//downloading worker notification //downloading worker notification
var name: CharSequence = context.getString(R.string.downloading) var name: CharSequence = resources.getString(R.string.downloading)
var description = "WorkManager Default Notification" var description = "WorkManager Default Notification"
val importance = NotificationManager.IMPORTANCE_LOW val importance = NotificationManager.IMPORTANCE_LOW
var channel = NotificationChannel(DOWNLOAD_WORKER_CHANNEL_ID, name, importance) var channel = NotificationChannel(DOWNLOAD_WORKER_CHANNEL_ID, name, importance)
@ -43,30 +59,30 @@ class NotificationUtil(var context: Context) {
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
//gui downloads //gui downloads
name = context.getString(R.string.download_notification_channel_name) name = resources.getString(R.string.download_notification_channel_name)
description = context.getString(R.string.download_notification_channel_description) description = resources.getString(R.string.download_notification_channel_description)
channel = NotificationChannel(DOWNLOAD_SERVICE_CHANNEL_ID, name, importance) channel = NotificationChannel(DOWNLOAD_SERVICE_CHANNEL_ID, name, importance)
channel.description = description channel.description = description
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
//command downloads //command downloads
name = context.getString(R.string.command_download_notification_channel_name) name = resources.getString(R.string.command_download_notification_channel_name)
description = 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 = NotificationChannel(COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID, name, importance)
channel.description = description channel.description = description
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
//finished or errored downloads //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 = 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 = NotificationChannel(DOWNLOAD_FINISHED_CHANNEL_ID, name, NotificationManager.IMPORTANCE_HIGH)
channel.description = description channel.description = description
notificationManager.createNotificationChannel(channel) notificationManager.createNotificationChannel(channel)
//misc //misc
name = context.getString(R.string.misc) name = resources.getString(R.string.misc)
description = "" description = ""
channel = NotificationChannel(DOWNLOAD_MISC_CHANNEL_ID, name, NotificationManager.IMPORTANCE_HIGH) channel = NotificationChannel(DOWNLOAD_MISC_CHANNEL_ID, name, NotificationManager.IMPORTANCE_HIGH)
channel.description = description channel.description = description
@ -87,12 +103,12 @@ class NotificationUtil(var context: Context) {
val notificationBuilder = getBuilder(DOWNLOAD_WORKER_CHANNEL_ID) val notificationBuilder = getBuilder(DOWNLOAD_WORKER_CHANNEL_ID)
return notificationBuilder return notificationBuilder
.setContentTitle(context.getString(R.string.downloading)) .setContentTitle(resources.getString(R.string.downloading))
.setOngoing(true) .setOngoing(true)
.setSmallIcon(R.drawable.ic_launcher_foreground_large) .setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, resources,
R.drawable.ic_launcher_foreground_large R.drawable.ic_launcher_foreground_large
) )
) )
@ -118,7 +134,7 @@ class NotificationUtil(var context: Context) {
.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, resources,
android.R.drawable.stat_sys_download android.R.drawable.stat_sys_download
) )
) )
@ -140,7 +156,7 @@ class NotificationUtil(var context: Context) {
.setSmallIcon(R.drawable.ic_launcher_foreground_large) .setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, resources,
R.drawable.ic_launcher_foreground_large R.drawable.ic_launcher_foreground_large
) )
) )
@ -157,7 +173,7 @@ class NotificationUtil(var context: Context) {
PendingIntent.FLAG_IMMUTABLE 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()) notificationManager.notify(DOWNLOAD_RESUME_NOTIFICATION_ID + itemID, notificationBuilder.build())
} }
@ -165,11 +181,11 @@ class NotificationUtil(var context: Context) {
val notificationBuilder = getBuilder(channel) val notificationBuilder = getBuilder(channel)
notificationBuilder notificationBuilder
.setContentTitle(context.getString(R.string.updating_download_data)) .setContentTitle(resources.getString(R.string.updating_download_data))
.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, resources,
android.R.drawable.stat_sys_download android.R.drawable.stat_sys_download
) )
) )
@ -185,11 +201,11 @@ class NotificationUtil(var context: Context) {
val notificationBuilder = getBuilder(DOWNLOAD_FINISHED_CHANNEL_ID) val notificationBuilder = getBuilder(DOWNLOAD_FINISHED_CHANNEL_ID)
notificationBuilder notificationBuilder
.setContentTitle("${context.getString(R.string.downloaded)} $title") .setContentTitle("${resources.getString(R.string.downloaded)} $title")
.setSmallIcon(R.drawable.ic_launcher_foreground_large) .setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, resources,
R.drawable.ic_launcher_foreground_large R.drawable.ic_launcher_foreground_large
) )
) )
@ -228,8 +244,8 @@ class NotificationUtil(var context: Context) {
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
) )
notificationBuilder.addAction(0, context.getString(R.string.Open_File), openNotificationPendingIntent) notificationBuilder.addAction(0, resources.getString(R.string.Open_File), openNotificationPendingIntent)
notificationBuilder.addAction(0, context.getString(R.string.share), shareNotificationPendingIntent) notificationBuilder.addAction(0, resources.getString(R.string.share), shareNotificationPendingIntent)
}catch (_: Exception){} }catch (_: Exception){}
} }
notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID, notificationBuilder.build()) notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID, notificationBuilder.build())
@ -263,12 +279,12 @@ class NotificationUtil(var context: Context) {
.createPendingIntent() .createPendingIntent()
notificationBuilder notificationBuilder
.setContentTitle("${context.getString(R.string.failed_download)}: $title") .setContentTitle("${resources.getString(R.string.failed_download)}: $title")
.setContentText(error) .setContentText(error)
.setSmallIcon(R.drawable.ic_launcher_foreground_large) .setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, resources,
R.drawable.ic_launcher_foreground_large R.drawable.ic_launcher_foreground_large
) )
) )
@ -278,7 +294,7 @@ class NotificationUtil(var context: Context) {
.clearActions() .clearActions()
if (logID != null){ if (logID != null){
notificationBuilder.setContentIntent(errorPendingIntent) 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()) notificationManager.notify(DOWNLOAD_FINISHED_NOTIFICATION_ID, notificationBuilder.build())
} }
@ -299,7 +315,7 @@ class NotificationUtil(var context: Context) {
val notificationBuilder = getBuilder(channel) val notificationBuilder = getBuilder(channel)
var contentText = "" 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(), "") contentText += desc.replace("\\[.*?\\] ".toRegex(), "")
val pauseIntent = Intent(context, PauseDownloadNotificationReceiver::class.java) val pauseIntent = Intent(context, PauseDownloadNotificationReceiver::class.java)
@ -326,8 +342,8 @@ class NotificationUtil(var context: Context) {
.setContentTitle(title) .setContentTitle(title)
.setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
.clearActions() .clearActions()
.addAction(0, context.getString(R.string.pause), pauseNotificationPendingIntent) .addAction(0, resources.getString(R.string.pause), pauseNotificationPendingIntent)
.addAction(0, context.getString(R.string.cancel), cancelNotificationPendingIntent) .addAction(0, resources.getString(R.string.cancel), cancelNotificationPendingIntent)
notificationManager.notify(id, notificationBuilder.build()) notificationManager.notify(id, notificationBuilder.build())
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
@ -342,13 +358,13 @@ class NotificationUtil(var context: Context) {
val notificationBuilder = getBuilder(downloadMiscChannelId) val notificationBuilder = getBuilder(downloadMiscChannelId)
return notificationBuilder return notificationBuilder
.setContentTitle(context.getString(R.string.move_temporary_files)) .setContentTitle(resources.getString(R.string.move_temporary_files))
.setOngoing(true) .setOngoing(true)
.setCategory(Notification.CATEGORY_PROGRESS) .setCategory(Notification.CATEGORY_PROGRESS)
.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, resources,
android.R.drawable.stat_sys_download android.R.drawable.stat_sys_download
) )
) )
@ -367,7 +383,7 @@ class NotificationUtil(var context: Context) {
val contentText = "${progress}/${totalFiles}" val contentText = "${progress}/${totalFiles}"
try { try {
notificationBuilder.setProgress(100, progress, false) 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)) .setStyle(NotificationCompat.BigTextStyle().bigText(contentText))
notificationManager.notify(id, notificationBuilder.build()) notificationManager.notify(id, notificationBuilder.build())
} catch (e: Exception) { } catch (e: Exception) {
@ -383,7 +399,7 @@ class NotificationUtil(var context: Context) {
.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, resources,
android.R.drawable.stat_sys_download android.R.drawable.stat_sys_download
) )
) )
@ -394,26 +410,16 @@ class NotificationUtil(var context: Context) {
return notificationBuilder.build() return notificationBuilder.build()
} }
fun createFormatsUpdateNotification(workID: Int): Notification { fun createFormatsUpdateNotification(): Notification {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) 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 return notificationBuilder
.setContentTitle(context.getString(R.string.update_formats_background)) .setContentTitle(resources.getString(R.string.update_formats_background))
.setOngoing(true) .setOngoing(true)
.setCategory(Notification.CATEGORY_PROGRESS) .setCategory(Notification.CATEGORY_PROGRESS)
.setSmallIcon(android.R.drawable.stat_sys_download) .setSmallIcon(android.R.drawable.stat_sys_download)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, resources,
android.R.drawable.stat_sys_download android.R.drawable.stat_sys_download
) )
) )
@ -423,23 +429,37 @@ class NotificationUtil(var context: Context) {
.setProgress(PROGRESS_MAX, PROGRESS_CURR, false) .setProgress(PROGRESS_MAX, PROGRESS_CURR, false)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
.clearActions() .clearActions()
.addAction(0, context.getString(R.string.cancel), cancelNotificationPendingIntent)
.build() .build()
} }
fun updateFormatUpdateNotification( fun updateFormatUpdateNotification(
id: Int, workID: Int,
workTag: String,
progress: Int, progress: Int,
queue: Int, queue: Int,
) { ) {
val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) 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 { try {
notificationBuilder.setProgress(queue, progress, false) 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)) .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) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }
@ -460,11 +480,11 @@ class NotificationUtil(var context: Context) {
.createPendingIntent() .createPendingIntent()
notificationBuilder 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) .setSmallIcon(R.drawable.ic_launcher_foreground_large)
.setLargeIcon( .setLargeIcon(
BitmapFactory.decodeResource( BitmapFactory.decodeResource(
context.resources, resources,
R.drawable.ic_launcher_foreground_large R.drawable.ic_launcher_foreground_large
) )
) )

View file

@ -17,6 +17,7 @@ import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.await import androidx.work.await
import androidx.work.workDataOf import androidx.work.workDataOf
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.DownloadDao import com.deniscerri.ytdlnis.database.dao.DownloadDao
@ -49,7 +50,7 @@ class DownloadWorker(
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
if (isStopped) return Result.success() if (isStopped) return Result.success()
val notificationUtil = NotificationUtil(context) val notificationUtil = NotificationUtil(App.instance)
val infoUtil = InfoUtil(context) val infoUtil = InfoUtil(context)
val dbManager = DBManager.getInstance(context) val dbManager = DBManager.getInstance(context)
val dao = dbManager.downloadDao val dao = dbManager.downloadDao
@ -119,6 +120,8 @@ class DownloadWorker(
val keepCache = sharedPreferences.getBoolean("keep_cache", false) val keepCache = sharedPreferences.getBoolean("keep_cache", false)
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false) val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !sharedPreferences.getBoolean("incognito", false)
val commandString = infoUtil.parseYTDLRequestString(request)
val logItem = LogItem( val logItem = LogItem(
0, 0,
downloadItem.title.ifEmpty { downloadItem.url }, downloadItem.title.ifEmpty { downloadItem.url },
@ -126,7 +129,7 @@ class DownloadWorker(
"Title: ${downloadItem.title}\n" + "Title: ${downloadItem.title}\n" +
"URL: ${downloadItem.url}\n" + "URL: ${downloadItem.url}\n" +
"Type: ${downloadItem.type}\n" + "Type: ${downloadItem.type}\n" +
"Command:\n ${infoUtil.parseYTDLRequestString(request)}\n\n", "Command:\n ${commandString}\n\n",
downloadItem.format, downloadItem.format,
downloadItem.type, downloadItem.type,
System.currentTimeMillis(), System.currentTimeMillis(),
@ -142,6 +145,8 @@ class DownloadWorker(
} }
val noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()
runCatching { runCatching {
YoutubeDL.getInstance().execute(request, downloadItem.id.toString()){ progress, _, line -> 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)) setProgressAsync(workDataOf("progress" to progress.toInt(), "output" to line.chunked(5000).first().toString(), "id" to downloadItem.id))
@ -162,16 +167,18 @@ class DownloadWorker(
runBlocking { runBlocking {
var finalPaths : List<String>? var finalPaths : List<String>?
//if there was no cache used if (noCache){
if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){
setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id)) 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)) finalPaths = File(FileUtil.formatPath(downloadLocation))
.walkTopDown() .walkTopDown()
.filter { it.isFile && p.contains(it.nameWithoutExtension) } .filter { it.isFile && p.any { f -> f.contains(it.nameWithoutExtension) }}
.sortedByDescending { it.length() } .sortedByDescending { it.length() }
.map { it.absolutePath } .map { it.absolutePath }
.toList() .toList()
FileUtil.scanMedia(finalPaths, context) FileUtil.scanMedia(finalPaths, context)
if (finalPaths.isEmpty()){ if (finalPaths.isEmpty()){
finalPaths = listOf(context.getString(R.string.unfound_file)) finalPaths = listOf(context.getString(R.string.unfound_file))
@ -208,7 +215,7 @@ class DownloadWorker(
val unixtime = System.currentTimeMillis() / 1000 val unixtime = System.currentTimeMillis() / 1000
val file = File(finalPaths?.first()!!) val file = File(finalPaths?.first()!!)
downloadItem.format.filesize = file.length() 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) historyDao.insert(historyItem)
} }

View file

@ -15,6 +15,7 @@ import androidx.work.ForegroundInfo
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
@ -43,7 +44,7 @@ class MoveCacheFilesWorker(
workerParams: WorkerParameters workerParams: WorkerParameters
) : Worker(context, workerParams) { ) : Worker(context, workerParams) {
override fun doWork(): Result { override fun doWork(): Result {
val notificationUtil = NotificationUtil(context) val notificationUtil = NotificationUtil(App.instance)
val id = System.currentTimeMillis().toInt() val id = System.currentTimeMillis().toInt()
val downloadFolders = File(context.cacheDir.absolutePath + "/downloads") val downloadFolders = File(context.cacheDir.absolutePath + "/downloads")

View file

@ -72,6 +72,12 @@ class TerminalDownloadWorker(
if (cookiesFile.exists()){ if (cookiesFile.exists()){
request.addOption("--cookies", cookiesFile.absolutePath) 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) request.addOption("-P", FileUtil.getDefaultCommandPath() + "/" + itemId)
@ -83,7 +89,7 @@ class TerminalDownloadWorker(
"Terminal Download", "Terminal Download",
"Downloading:\n" + "Downloading:\n" +
"Terminal Download\n" + "Terminal Download\n" +
"Command: ${infoUtil.parseYTDLRequestString(request)}\n\n", "Command: ${command}\n\n",
Format(), Format(),
DownloadViewModel.Type.command, DownloadViewModel.Type.command,
System.currentTimeMillis(), System.currentTimeMillis(),
@ -110,10 +116,9 @@ class TerminalDownloadWorker(
line, progress.toInt(), 0, title, line, progress.toInt(), 0, title,
NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID
) )
if (logDownloads){ CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(Dispatchers.IO).launch { if (logDownloads) logRepo.update(line, logItem.id)
logRepo.update(line, logItem.id) dao.updateLog(line, itemId.toLong())
}
} }
} }
}.onSuccess { }.onSuccess {
@ -132,39 +137,30 @@ class TerminalDownloadWorker(
} }
return runBlocking { return runBlocking {
it.out.split("\n").forEach { CoroutineScope(Dispatchers.IO).launch {
setProgress(workDataOf("progress" to 100, "output" to it, "id" to itemId, "log" to logDownloads)) if (logDownloads) logRepo.update(it.out, logItem.id)
Thread.sleep(50) dao.updateLog(it.out, itemId.toLong())
} Thread.sleep(1000)
dao.delete(itemId.toLong())
if (logDownloads){
CoroutineScope(Dispatchers.IO).launch {
logRepo.update(it.out, logItem.id)
}
} }
notificationUtil.cancelDownloadNotification(itemId) notificationUtil.cancelDownloadNotification(itemId)
dao.delete(itemId.toLong())
return@runBlocking Result.success() Result.success()
} }
}.onFailure { }.onFailure {
return runBlocking { return runBlocking {
it.message?.chunked(10000)?.forEach { CoroutineScope(Dispatchers.IO).launch {
setProgress(workDataOf("progress" to 100, "output" to it, "id" to itemId, "log" to logDownloads)) if (it.message != null){
Thread.sleep(100) if (logDownloads) logRepo.update(it.message!!, logItem.id)
} dao.updateLog(it.message!!, itemId.toLong())
Thread.sleep(1000)
if (logDownloads){ dao.delete(itemId.toLong())
CoroutineScope(Dispatchers.IO).launch {
if (it.message != null){
logRepo.update(it.message!!, logItem.id)
}
} }
} }
notificationUtil.cancelDownloadNotification(itemId)
File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively() File(FileUtil.getDefaultCommandPath() + "/" + itemId).deleteRecursively()
Log.e(TAG, context.getString(R.string.failed_download), it) Log.e(TAG, context.getString(R.string.failed_download), it)
notificationUtil.cancelDownloadNotification(itemId) Result.failure()
dao.delete(itemId.toLong())
return@runBlocking Result.failure()
} }

View file

@ -1,50 +1,23 @@
package com.deniscerri.ytdlnis.work package com.deniscerri.ytdlnis.work
import android.content.Context import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log 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.ForegroundInfo
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.App 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.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.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.NotificationUtil
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 kotlinx.coroutines.runBlocking
import java.io.File
class UpdatePlaylistFormatsWorker( class UpdatePlaylistFormatsWorker(
private val context: Context, private val context: Context,
workerParams: WorkerParameters workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) { ) : Worker(context, workerParams) {
override suspend fun doWork(): Result { override fun doWork(): Result {
val dbManager = DBManager.getInstance(context) val dbManager = DBManager.getInstance(context)
val dao = dbManager.downloadDao val dao = dbManager.downloadDao
val resDao = dbManager.resultDao val resDao = dbManager.resultDao
@ -55,47 +28,59 @@ class UpdatePlaylistFormatsWorker(
val workID = inputData.getInt("id", 0) val workID = inputData.getInt("id", 0)
if (workID == 0) return Result.failure() if (workID == 0) return Result.failure()
val notification = notificationUtil.createFormatsUpdateNotification(workID) val notification = notificationUtil.createFormatsUpdateNotification()
val foregroundInfo = ForegroundInfo(FORMAT_UPDATING_NOTIFICATION_ID, notification) val foregroundInfo = ForegroundInfo(workID, notification)
setForeground(foregroundInfo) setForegroundAsync(foregroundInfo)
var count = 0 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++ count++
return@forEach notificationUtil.updateFormatUpdateNotification(workID, UpdatePlaylistFormatsWorker::class.java.name, count, ids.size)
}else{
throw Exception()
} }
}
runCatching { Result.success()
d.allFormats.clear() }catch (e: Exception){
d.allFormats.addAll(infoUtil.getFormats(d.url)) notificationUtil.cancelDownloadNotification(workID)
d.format = vm.getFormat(d.allFormats,d.type) ids.clear()
Result.failure()
r.formats.clear() }finally {
r.formats.addAll(d.allFormats) if (ids.isNotEmpty()){
notificationUtil.showFormatsUpdatedNotification(ids)
dao.update(d)
resDao.update(r)
}
count++
notificationUtil.updateFormatUpdateNotification(FORMAT_UPDATING_NOTIFICATION_ID, count, ids.size)
}else{
notificationUtil.cancelDownloadNotification(FORMAT_UPDATING_NOTIFICATION_ID)
} }
} }
}
override fun onStopped() {
Log.e("asd", "Asd")
runBlocking { super.onStopped()
notificationUtil.showFormatsUpdatedNotification(ids)
}
return Result.success()
} }
} }

View file

@ -18,4 +18,7 @@
</androidx.recyclerview.widget.RecyclerView> </androidx.recyclerview.widget.RecyclerView>
<include layout="@layout/history_no_results"
android:visibility="gone" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -43,14 +43,16 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<RelativeLayout <RelativeLayout
android:id="@+id/rl"
android:layout_width="match_parent" android:layout_width="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"
android:layout_height="match_parent"> android:layout_height="match_parent">
<WebView <androidx.compose.ui.platform.ComposeView
android:id="@+id/webview" android:id="@+id/webview_compose"
android:layout_width="match_parent" tools:composableName="com.deniscerri.ytdlnis.ui.more.WebViewActivity.WebViewView"
android:layout_height="match_parent" /> android:layout_height="match_parent"
android:layout_width="match_parent"/>
</RelativeLayout> </RelativeLayout>

View file

@ -6,6 +6,12 @@
android:title="@string/delete_all_cookies" android:title="@string/delete_all_cookies"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/use_header"
android:title="@string/user_agent_header"
android:checkable="true"
app:showAsAction="never" />
<item <item
android:id="@+id/import_clipboard" android:id="@+id/import_clipboard"
android:title="@string/import_from_clipboard" android:title="@string/import_from_clipboard"

View file

@ -307,4 +307,7 @@
<string name="preferred_audio_codec">Preferred Audio Codec</string> <string name="preferred_audio_codec">Preferred Audio Codec</string>
<string name="loading">Loading</string> <string name="loading">Loading</string>
<string name="sponsorblock_api_url">Sponsorblock API URL</string> <string name="sponsorblock_api_url">Sponsorblock API URL</string>
<string name="remember_download_type">Remember Download Type</string>
<string name="remember_download_type_summary">Select the last opened download type for the current item</string>
<string name="user_agent_header">User-Agent header</string>
</resources> </resources>

View file

@ -53,6 +53,19 @@
app:summary="@string/preferred_download_type_summary" app:summary="@string/preferred_download_type_summary"
app:title="@string/preferred_download_type" /> app:title="@string/preferred_download_type" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_download_type"
android:key="remember_download_type"
app:summary="@string/remember_download_type_summary"
app:title="@string/remember_download_type" />
<Preference
android:defaultValue="video"
android:key="last_used_download_type"
app:isPreferenceVisible="false" />
<SeekBarPreference <SeekBarPreference
android:defaultValue="3" android:defaultValue="3"
android:icon="@drawable/ic_lines" android:icon="@drawable/ic_lines"
@ -130,5 +143,15 @@
/> />
</PreferenceCategory> </PreferenceCategory>
<Preference
app:defaultValue=""
android:key="use_header"
app:isPreferenceVisible="false" />
<Preference
app:defaultValue=""
android:key="useragent_header"
app:isPreferenceVisible="false" />
</PreferenceScreen> </PreferenceScreen>

View file

@ -1,38 +1,49 @@
download audio/video files from more than 1000 websites <b>YTDLnis</b> 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. <b>process playlists</b>
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 - edit every playlist item separately just like in a normal download item.
select a filename template for all items - select a common format for all items and/or select multiple audio formats in case you are downloading them as a video
batch update download type to audio/video/custom command in one click - select a download path for all items
queue downloads and schedule them by date and time - select a filename template for all items
you can also schedule multiple items at the same time - batch update download type to audio/video/custom command in one click
download multiple items at the same time - queue downloads and schedule them by date and time
use custom commands and templates or go full yt-dlp mode with a built in terminal - you can also schedule multiple items at the same time
you can backup and restore templates, so you can share with your buddies - download multiple items at the same time
COOKIES support. Log in with your accounts and download private/unavailable videos, unlock premium formats etc. - use custom commands and templates or go full yt-dlp mode with a built in terminal
cut videos based on timestamps and video chapters (This yt-dlp feature is experimental on the original project) - you can backup and restore templates, so you can share with your buddies
you can make unlimited cuts
remove sponsorblock elements from the item <b>COOKIES support</b>
embed them as a chapters in your video
embed subtitles/metadata/chapters etc - Log in with your accounts and download private/unavailable videos, unlock premium formats etc.
modify metadata such as title and author
split item into separate files depending on its chapters <b>Different ways of modifying the file</b>
select different download formats
bottom card right from the share menu, no need to open the app - cut videos based on timestamps and video chapters (This yt-dlp feature is experimental on the original project)
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 - you can make unlimited cuts
search or insert a link from the app - remove sponsorblock elements from the item
you can stack searches so you can process them at the same time - embed them as a chapters in your video
log downloads in case of problems - embed subtitles/metadata/chapters etc
re-download cancelled or failed downloads - modify metadata such as title and author
you can use gestures to swipe left to redownload and right to delete - split item into separate files depending on its chapters
you can long click the redownload button in the details sheet to show the download card for more functionality - select different download formats
incognito mode when you don't want to save a download history or logs
quick download mode <b>Other features</b>
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 - bottom card right from the share menu, no need to open the app
most yt-dlp features are implemented, suggestions are welcome - 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
Material You interface - search or insert a link from the app
Theming options - you can stack searches so you can process them at the same time
Backup and restore features. (Almost everything can be backed up) - log downloads in case of problems
MVVM Architecture w/ WorkManager - 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