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

View file

@ -2,11 +2,11 @@
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "6b5efd73749a7138035e1f00349715c8",
"identityHash": "0647c4b527efea884e1dc26157cd4189",
"entities": [
{
"tableName": "results",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `creationTime` INTEGER NOT NULL)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL DEFAULT '', `website` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `formats` TEXT NOT NULL, `urls` TEXT NOT NULL DEFAULT '', `chapters` TEXT, `creationTime` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
@ -42,7 +42,8 @@
"fieldPath": "thumb",
"columnName": "thumb",
"affinity": "TEXT",
"notNull": true
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "website",
@ -93,7 +94,7 @@
},
{
"tableName": "history",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadId` INTEGER NOT NULL DEFAULT 0)",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `duration` TEXT NOT NULL, `thumb` TEXT NOT NULL, `type` TEXT NOT NULL, `time` INTEGER NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadId` INTEGER NOT NULL DEFAULT 0, `command` TEXT NOT NULL DEFAULT '')",
"fields": [
{
"fieldPath": "id",
@ -167,6 +168,13 @@
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "command",
"columnName": "command",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
}
],
"primaryKey": {
@ -538,7 +546,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6b5efd73749a7138035e1f00349715c8')"
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0647c4b527efea884e1dc26157cd4189')"
]
}
}

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>
<receiver android:name=".receiver.CancelDownloadNotificationReceiver" />
<receiver android:name=".receiver.CancelWorkReceiver" />
<receiver android:name=".receiver.PauseDownloadNotificationReceiver" />
<receiver android:name=".receiver.AlarmStartReceiver" />
<receiver android:name=".receiver.AlarmStopReceiver" />

View file

@ -5,19 +5,7 @@ import android.os.Looper
import android.widget.Toast
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import androidx.work.Configuration
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.deniscerri.ytdlnis.work.UpdateYTDLWorker
import com.google.android.material.snackbar.Snackbar
import com.yausername.aria2c.Aria2c
import com.yausername.ffmpeg.FFmpeg
import com.yausername.youtubedl_android.YoutubeDL
@ -27,8 +15,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
class App : Application() {

View file

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

View file

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

View file

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

View file

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

View file

@ -11,15 +11,38 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface TerminalDao {
@Query("SELECT * FROM terminalDownloads ORDER BY id")
@Query("SELECT id,command FROM terminalDownloads ORDER BY id")
fun getActiveTerminalDownloads() : List<TerminalItem>
@Query("SELECT * FROM terminalDownloads ORDER BY id")
@Query("SELECT id,command FROM terminalDownloads ORDER BY id")
fun getActiveTerminalDownloadsFlow() : Flow<List<TerminalItem>>
@Query("SELECT * from terminalDownloads where id=:id")
fun getActiveTerminalFlow(id: Long) : Flow<TerminalItem>
@Query("SELECT COUNT(*) FROM terminalDownloads")
fun getActiveTerminalsCount() : Int
@Query("UPDATE terminalDownloads set log=:l where id=:id")
fun updateTerminalLog(l: String, id: Long)
@Query("SELECT * FROM terminalDownloads WHERE id=:id LIMIT 1")
fun getTerminalById(id: Long) : TerminalItem?
@Transaction
fun updateLog(line: String, id: Long){
val t = getTerminalById(id) ?: return
val log = t.log ?: ""
val lines = log.split("\n")
//clean dublicate progress + add newline
val l = if (listOf(line, lines.last()).all { it.contains("[download")}){
lines.dropLast(1).joinToString("\n")
}else{
log
} + "\n${line}"
updateTerminalLog(l, id)
}
@Insert
suspend fun insert(item: TerminalItem) : Long

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -12,6 +12,8 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isEmpty
import androidx.fragment.app.Fragment
@ -140,17 +142,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
val callback = FileUtil.getURIFromResult(this@DownloadAudioFragment, requireActivity()){result ->
downloadItem.downloadPath = result
//downloadViewModel.updateDownload(downloadItem)
saveDir.editText?.setText(FileUtil.formatPath(result), TextView.BufferType.EDITABLE)
val free = FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace)
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE
}
callback.launch(intent)
pathResultLauncher.launch(intent)
}
freeSpace = view.findViewById(R.id.freespace)
val free = FileUtil.convertFileSize(
@ -296,4 +288,28 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
state.putBoolean("updated", true)
onViewCreated(requireView(),savedInstanceState = state)
}
private var pathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
downloadItem.downloadPath = result.data?.data.toString()
//downloadViewModel.updateDownload(downloadItem)
saveDir.editText?.setText(FileUtil.formatPath(result.data?.data.toString()), TextView.BufferType.EDITABLE)
val free = FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace)
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE
}
}
}

View file

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

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.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ -132,7 +134,10 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
item.downloadStartTime = it.timeInMillis
}
runBlocking {
downloadViewModel.queueDownloads(items)
val chunks = items.chunked(10)
for (c in chunks) {
downloadViewModel.queueDownloads(c)
}
val first = items.first()
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()
@ -145,7 +150,11 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
scheduleBtn.isEnabled = false
download.isEnabled = false
runBlocking {
downloadViewModel.queueDownloads(items)
val chunks = items.chunked(10)
for (c in chunks) {
downloadViewModel.queueDownloads(c)
}
}
dismiss()
}
@ -206,13 +215,13 @@ class DownloadMultipleBottomSheetDialog(private var items: MutableList<DownloadI
}
override fun onContinueOnBackground(items: List<DownloadItem?>) {
override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch {
val ids = mutableListOf<Long>()
runBlocking {
items.map { it!!.status = DownloadRepository.Status.Saved.toString() }
items.map { it.status = DownloadRepository.Status.Saved.toString() }
items.forEach {
ids.add(downloadViewModel.repository.insert(it!!))
ids.add(downloadViewModel.repository.insert(it))
}
}
val id = System.currentTimeMillis().toInt()

View file

@ -14,6 +14,7 @@ import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
@ -130,16 +131,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
val callback = FileUtil.getURIFromResult(this@DownloadVideoFragment, requireActivity()){ result ->
downloadItem.downloadPath = result
saveDir.editText?.setText(FileUtil.formatPath(result), TextView.BufferType.EDITABLE)
val free = FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace)
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE
}
callback.launch(intent)
pathResultLauncher.launch(intent)
}
freeSpace = view.findViewById(R.id.freespace)
@ -311,5 +303,27 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
onViewCreated(requireView(),savedInstanceState = state)
}
private var pathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
downloadItem.downloadPath = result.data?.data.toString()
saveDir.editText?.setText(FileUtil.formatPath(result.data?.data.toString()), TextView.BufferType.EDITABLE)
val free = FileUtil.convertFileSize(
File(FileUtil.formatPath(downloadItem.downloadPath)).freeSpace)
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -1,18 +1,29 @@
package com.deniscerri.ytdlnis.ui.more
import android.annotation.SuppressLint
import android.content.SharedPreferences
import android.os.Bundle
import android.util.Log
import android.webkit.CookieManager
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.CookieItem
import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel
import com.deniscerri.ytdlnis.ui.BaseActivity
import com.google.accompanist.web.AccompanistWebChromeClient
import com.google.accompanist.web.AccompanistWebViewClient
import com.google.accompanist.web.WebView
import com.google.accompanist.web.rememberWebViewState
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton
@ -24,12 +35,14 @@ import kotlinx.coroutines.withContext
class WebViewActivity : BaseActivity() {
private lateinit var cookiesViewModel: CookieViewModel
private lateinit var webView: WebView
private lateinit var webViewCompose: ComposeView
private lateinit var toolbar: MaterialToolbar
private lateinit var generateBtn: MaterialButton
private lateinit var cookieManager: CookieManager
private lateinit var url: String
private lateinit var cookies: String
private lateinit var webViewClient: AccompanistWebViewClient
private lateinit var preferences: SharedPreferences
@SuppressLint("SetJavaScriptEnabled")
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@ -40,13 +53,20 @@ class WebViewActivity : BaseActivity() {
val appbar = findViewById<AppBarLayout>(R.id.webview_appbarlayout)
toolbar = appbar.findViewById(R.id.webviewToolbar)
generateBtn = toolbar.findViewById(R.id.generate)
webViewCompose = findViewById(R.id.webview_compose)
cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookies(null)
preferences = PreferenceManager.getDefaultSharedPreferences(this)
webView = findViewById(R.id.webview)
webView.settings.javaScriptEnabled = true
webView.settings.domStorageEnabled = true
webView.settings.javaScriptCanOpenWindowsAutomatically = true
webViewClient = object : AccompanistWebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
kotlin.runCatching {
toolbar.title = view?.title ?: ""
cookies = CookieManager.getInstance().getCookie(view?.url)
}
}
}
cookieManager = CookieManager.getInstance()
@ -55,21 +75,8 @@ class WebViewActivity : BaseActivity() {
onBackPressedDispatcher.onBackPressed()
}
val webViewClient: WebViewClient = object : WebViewClient() {
override fun onPageFinished(view: WebView, url: String) {
super.onPageFinished(view, url)
kotlin.runCatching {
toolbar.title = view.title
cookies = CookieManager.getInstance().getCookie(view.url)
}
}
}
webView.webViewClient = webViewClient
generateBtn.setOnClickListener {
cookiesViewModel.getCookiesFromDB().getOrNull()?.let {
Log.e("asas", it)
kotlin.runCatching {
lifecycleScope.launch {
withContext(Dispatchers.IO){
@ -91,8 +98,40 @@ class WebViewActivity : BaseActivity() {
onBackPressedDispatcher.onBackPressed()
}
webView.loadUrl(url)
webViewCompose.apply {
setContent { WebViewView() }
}
}
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun WebViewView() {
val webViewChromeClient = remember {
object : AccompanistWebChromeClient() {
}
}
Scaffold(modifier = Modifier.fillMaxSize()) { paddingValues ->
WebView(
state = rememberWebViewState(url), client = webViewClient, chromeClient = webViewChromeClient,
modifier = Modifier
.padding(paddingValues)
.fillMaxSize(),
captureBackPresses = false, factory = { context ->
WebView(context).apply {
settings.run {
javaScriptEnabled = true
domStorageEnabled = true
javaScriptCanOpenWindowsAutomatically = true
preferences.edit().putString("useragent_header", userAgentString).apply()
}
cookieManager.setAcceptThirdPartyCookies(this, true)
}
}
)
}
}
companion object {

View file

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

View file

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

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.floatingactionbutton.ExtendedFloatingActionButton
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.StringBuilder
import kotlin.properties.Delegates
@ -102,11 +105,13 @@ class TerminalFragment : Fragment() {
topAppBar!!.setNavigationOnClickListener { requireActivity().finish() }
input = view.findViewById(R.id.command_edittext)
fab = view.findViewById(R.id.command_fab)
if (arguments?.getLong("id") != null){
downloadID = requireArguments().getLong("id")
if(downloadID != 0L){
input!!.visibility = View.GONE
showCancelFab()
}
}
@ -145,6 +150,7 @@ class TerminalFragment : Fragment() {
}else{
bottomAppBar.menu.getItem(1).icon?.alpha = 255
}
}
bottomAppBar.setOnMenuItemClickListener {
when(it.itemId){
@ -195,7 +201,6 @@ class TerminalFragment : Fragment() {
output!!.setTextIsSelectable(true)
output!!.layoutParams!!.width = LayoutParams.WRAP_CONTENT
input!!.requestFocus()
fab = view.findViewById(R.id.command_fab)
fab!!.setOnClickListener {
if (fab!!.text == getString(R.string.run_command)){
input!!.visibility = View.GONE
@ -205,7 +210,7 @@ class TerminalFragment : Fragment() {
lifecycleScope.launch {
val command = input!!.text.toString().replace("yt-dlp", "")
downloadID = withContext(Dispatchers.IO){
terminalViewModel.insert(TerminalItem(command = command))
terminalViewModel.insert(TerminalItem(command = command, log = output!!.text.toString()))
}
terminalViewModel.startTerminalDownloadWorker(TerminalItem(downloadID, command))
runWorkerListener()
@ -293,6 +298,22 @@ class TerminalFragment : Fragment() {
}
private fun runWorkerListener(){
CoroutineScope(Dispatchers.IO).launch {
terminalViewModel.getTerminal(downloadID).collectLatest {
kotlin.runCatching {
requireActivity().runOnUiThread{
if (it != null){
if (!it.log.isNullOrBlank()) {
output?.text = it.log
}
output?.scrollTo(0, output!!.height)
scrollView?.fullScroll(View.FOCUS_DOWN)
}
}
}
}
}
WorkManager.getInstance(requireContext())
.getWorkInfosByTagLiveData(downloadID.toString())
@ -310,25 +331,9 @@ class TerminalFragment : Fragment() {
}
return@forEach
}
val outputData = work.progress.getString("output")
if (!outputData.isNullOrBlank()){
val pieces = output!!.text.split("\n").toMutableList()
val lastLine = pieces.last()
requireActivity().runOnUiThread{
kotlin.runCatching {
showCancelFab()
if (listOf(lastLine, outputData).all { it.contains("[download]") }){
output!!.text = pieces.dropLast(1).joinToString("\n")
}
output!!.append("\n$outputData")
output!!.scrollTo(0, output!!.height)
scrollView!!.fullScroll(View.FOCUS_DOWN)
}
}
}
}
}
}
}

View file

@ -309,19 +309,4 @@ object FileUtil {
return "${DecimalFormat("#,##0.#", symbols).format(s / 1024.0.pow(digitGroups.toDouble()))} ${units[digitGroups]}"
}
fun getURIFromResult(f: Fragment, a: FragmentActivity?, callback: (data: String) -> Unit) = f.registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
a?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
callback(result.data?.data.toString())
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,50 +1,23 @@
package com.deniscerri.ytdlnis.work
import android.content.Context
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.NavDeepLinkBuilder
import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ForegroundInfo
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.Converters
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.util.NotificationUtil.Companion.FORMAT_UPDATING_NOTIFICATION_ID
import com.deniscerri.ytdlnis.util.UpdateUtil
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
class UpdatePlaylistFormatsWorker(
private val context: Context,
workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
) : Worker(context, workerParams) {
override fun doWork(): Result {
val dbManager = DBManager.getInstance(context)
val dao = dbManager.downloadDao
val resDao = dbManager.resultDao
@ -55,47 +28,59 @@ class UpdatePlaylistFormatsWorker(
val workID = inputData.getInt("id", 0)
if (workID == 0) return Result.failure()
val notification = notificationUtil.createFormatsUpdateNotification(workID)
val foregroundInfo = ForegroundInfo(FORMAT_UPDATING_NOTIFICATION_ID, notification)
setForeground(foregroundInfo)
val notification = notificationUtil.createFormatsUpdateNotification()
val foregroundInfo = ForegroundInfo(workID, notification)
setForegroundAsync(foregroundInfo)
var count = 0
ids.forEach {
if (!isStopped){
val d = dao.getDownloadById(it)
val r = resDao.getResultByURL(d.url)
if (d.allFormats.isNotEmpty()){
return try{
ids.forEach {
if (!isStopped){
val d = dao.getDownloadById(it)
val r = resDao.getResultByURL(d.url)
if (d.allFormats.isNotEmpty()){
count++
return@forEach
}
runCatching {
d.allFormats.clear()
d.allFormats.addAll(infoUtil.getFormats(d.url))
d.format = vm.getFormat(d.allFormats,d.type)
r.formats.clear()
r.formats.addAll(d.allFormats)
runBlocking {
resDao.update(r)
dao.delete(d.id)
}
}
count++
return@forEach
notificationUtil.updateFormatUpdateNotification(workID, UpdatePlaylistFormatsWorker::class.java.name, count, ids.size)
}else{
throw Exception()
}
}
runCatching {
d.allFormats.clear()
d.allFormats.addAll(infoUtil.getFormats(d.url))
d.format = vm.getFormat(d.allFormats,d.type)
r.formats.clear()
r.formats.addAll(d.allFormats)
dao.update(d)
resDao.update(r)
}
count++
notificationUtil.updateFormatUpdateNotification(FORMAT_UPDATING_NOTIFICATION_ID, count, ids.size)
}else{
notificationUtil.cancelDownloadNotification(FORMAT_UPDATING_NOTIFICATION_ID)
Result.success()
}catch (e: Exception){
notificationUtil.cancelDownloadNotification(workID)
ids.clear()
Result.failure()
}finally {
if (ids.isNotEmpty()){
notificationUtil.showFormatsUpdatedNotification(ids)
}
}
}
runBlocking {
notificationUtil.showFormatsUpdatedNotification(ids)
}
return Result.success()
override fun onStopped() {
Log.e("asd", "Asd")
super.onStopped()
}
}

View file

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

View file

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

View file

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

View file

@ -307,4 +307,7 @@
<string name="preferred_audio_codec">Preferred Audio Codec</string>
<string name="loading">Loading</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>

View file

@ -53,6 +53,19 @@
app:summary="@string/preferred_download_type_summary"
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
android:defaultValue="3"
android:icon="@drawable/ic_lines"
@ -130,5 +143,15 @@
/>
</PreferenceCategory>
<Preference
app:defaultValue=""
android:key="use_header"
app:isPreferenceVisible="false" />
<Preference
app:defaultValue=""
android:key="useragent_header"
app:isPreferenceVisible="false" />
</PreferenceScreen>

View file

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