alot of stuff

-added download now option when u highlight only scheduled items in the context menu

-tappin on the errored notification will send you to errored tab, if you have logs disabled. If logs on it will send u to the log

-added preferred format ordering. By ID, FILESIZE, CONTAINER. Formats grouped by container will also be sorted by filesize

-fixed app not showing formats when u try to modify a current download item and its a different type (audio formats not showing if its a video type and vice versa)

-fixed app showing generic formats in cases where the format length was the same as that of generic formats (silly mistake)

-made the app store static strings for 'best' and 'worst' so that there isnt any confusion when u try to change the language and the stored downloads will have the other language's string

-added collected filesize on top of the download queue

-potentially fixed app not crashing when going to queued screen? idk

-fixed app not moving files when its a fresh install and u havent tried to change the download path. Now the app will just straight up download in that path if it finds that it can write in it. If not, it will use the old method of moving through URI's

-added download retries options
--retries and --fragment-retries
This commit is contained in:
deniscerri 2023-06-25 22:07:08 +02:00
parent b25d8410d1
commit b1b5dbc608
No known key found for this signature in database
GPG key ID: 95C43D517D830350
31 changed files with 315 additions and 150 deletions

View file

@ -10,7 +10,7 @@ def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 6 def versionMinor = 6
def versionPatch = 3 def versionPatch = 3
def versionBuild = 1 // bump for dogfood builds, public betas, etc. def versionBuild = 2 // bump for dogfood builds, public betas, etc.
def versionExt = "" def versionExt = ""
if (versionBuild > 0){ if (versionBuild > 0){
@ -64,7 +64,8 @@ android {
buildTypes { buildTypes {
release { release {
minifyEnabled true minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false debuggable false
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
@ -157,7 +158,7 @@ dependencies {
androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer"
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVer" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVer"
androidTestImplementation "androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha13" androidTestImplementation "androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha16"
androidTestImplementation "androidx.test:runner:1.5.2" androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation "androidx.test:core:1.5.0" androidTestImplementation "androidx.test:core:1.5.0"
androidTestImplementation "androidx.test:rules:1.5.0" androidTestImplementation "androidx.test:rules:1.5.0"
@ -175,10 +176,10 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:$workVer" implementation "androidx.work:work-runtime-ktx:$workVer"
implementation "androidx.room:room-runtime:2.5.1" implementation "androidx.room:room-runtime:2.5.2"
implementation "androidx.room:room-ktx:2.5.1" implementation "androidx.room:room-ktx:2.5.2"
kapt "androidx.room:room-compiler:2.5.1" kapt "androidx.room:room-compiler:2.5.2"
androidTestImplementation "androidx.room:room-testing:2.5.1" androidTestImplementation "androidx.room:room-testing:2.5.2"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation "androidx.compose.runtime:runtime:$composeVer" implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation "com.google.truth:truth:1.1.3" androidTestImplementation "com.google.truth:truth:1.1.3"
@ -190,7 +191,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.10" implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.10"
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation "com.google.android.exoplayer:exoplayer:2.18.5" implementation "com.google.android.exoplayer:exoplayer:2.18.7"
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4' implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation "com.github.Irineu333:Highlight-KT:1.0.4" implementation "com.github.Irineu333:Highlight-KT:1.0.4"

View file

@ -1,9 +1,9 @@
package com.deniscerri.ytdlnis.database.dao package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.TemplateShortcut import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface CommandTemplateDao { interface CommandTemplateDao {
@ -11,10 +11,10 @@ interface CommandTemplateDao {
fun getAllTemplates() : List<CommandTemplate> fun getAllTemplates() : List<CommandTemplate>
@Query("SELECT * FROM commandTemplates ORDER BY id DESC") @Query("SELECT * FROM commandTemplates ORDER BY id DESC")
fun getAllTemplatesLiveData() : LiveData<List<CommandTemplate>> fun getAllTemplatesFlow() : Flow<List<CommandTemplate>>
@Query("SELECT * FROM templateShortcuts ORDER BY id DESC") @Query("SELECT * FROM templateShortcuts ORDER BY id DESC")
fun getAllShortcutsLiveData() : LiveData<List<TemplateShortcut>> fun getAllShortcutsFlow() : Flow<List<TemplateShortcut>>
@Query("SELECT * FROM templateShortcuts ORDER BY id DESC") @Query("SELECT * FROM templateShortcuts ORDER BY id DESC")
fun getAllShortcuts() : List<TemplateShortcut> fun getAllShortcuts() : List<TemplateShortcut>

View file

@ -1,8 +1,8 @@
package com.deniscerri.ytdlnis.database.dao package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface CookieDao { interface CookieDao {
@ -10,7 +10,7 @@ interface CookieDao {
fun getAllCookies() : List<CookieItem> fun getAllCookies() : List<CookieItem>
@Query("SELECT * FROM cookies ORDER BY id DESC") @Query("SELECT * FROM cookies ORDER BY id DESC")
fun getAllCookiesLiveData() : LiveData<List<CookieItem>> fun getAllCookiesFlow() : Flow<List<CookieItem>>
@Query("SELECT EXISTS(SELECT * FROM cookies WHERE url=:url LIMIT 1)") @Query("SELECT EXISTS(SELECT * FROM cookies WHERE url=:url LIMIT 1)")
fun checkIfExistsWithSameURL(url: String) : Boolean fun checkIfExistsWithSameURL(url: String) : Boolean

View file

@ -1,20 +1,20 @@
package com.deniscerri.ytdlnis.database.dao package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface DownloadDao { interface DownloadDao {
@Query("SELECT * FROM downloads ORDER BY status") @Query("SELECT * FROM downloads ORDER BY status")
fun getAllDownloads() : LiveData<List<DownloadItem>> fun getAllDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Active'") @Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloads() : LiveData<List<DownloadItem>> fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT COUNT(*) FROM downloads WHERE status='Active'") @Query("SELECT COUNT(*) FROM downloads WHERE status='Active'")
fun getActiveDownloadsCount() : LiveData<Int> fun getActiveDownloadsCount() : Flow<Int>
@Query("SELECT * FROM downloads WHERE status='Active'") @Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloadsList() : List<DownloadItem> fun getActiveDownloadsList() : List<DownloadItem>
@ -23,25 +23,25 @@ interface DownloadDao {
fun getActiveAndQueuedDownloadsList() : List<DownloadItem> fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id") @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id")
fun getQueuedDownloads() : LiveData<List<DownloadItem>> fun getQueuedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id") @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id")
fun getQueuedDownloadsList() : List<DownloadItem> fun getQueuedDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloads() : LiveData<List<DownloadItem>> fun getCancelledDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC")
fun getCancelledDownloadsList() : List<DownloadItem> fun getCancelledDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloads() : LiveData<List<DownloadItem>> fun getErroredDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC")
fun getErroredDownloadsList() : List<DownloadItem> fun getErroredDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE status='Processing' ORDER BY id DESC") @Query("SELECT * FROM downloads WHERE status='Processing' ORDER BY id DESC")
fun getProcessingDownloads() : LiveData<List<DownloadItem>> fun getProcessingDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1")
fun getDownloadById(id: Long) : DownloadItem fun getDownloadById(id: Long) : DownloadItem

View file

@ -1,8 +1,8 @@
package com.deniscerri.ytdlnis.database.dao package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.HistoryItem
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface HistoryDao { interface HistoryDao {
@ -27,7 +27,7 @@ interface HistoryDao {
@Query("SELECT * FROM history") @Query("SELECT * FROM history")
fun getAllHistory() : LiveData<List<HistoryItem>> fun getAllHistory() : Flow<List<HistoryItem>>
@Query("SELECT * FROM history") @Query("SELECT * FROM history")
fun getAllHistoryList() : List<HistoryItem> fun getAllHistoryList() : List<HistoryItem>

View file

@ -1,16 +1,16 @@
package com.deniscerri.ytdlnis.database.dao package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.* import androidx.room.*
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface ResultDao { interface ResultDao {
@Query("SELECT * FROM results") @Query("SELECT * FROM results")
fun getResults() : LiveData<List<ResultItem>> fun getResults() : Flow<List<ResultItem>>
@Query("SELECT COUNT(id) FROM results") @Query("SELECT COUNT(id) FROM results")
fun getCount() : LiveData<Int> fun getCount() : Flow<Int>
@Query("SELECT COUNT(id) FROM results") @Query("SELECT COUNT(id) FROM results")
fun getCountInt() :Int fun getCountInt() :Int

View file

@ -4,10 +4,11 @@ import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.TemplateShortcut import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import kotlinx.coroutines.flow.Flow
class CommandTemplateRepository(private val commandDao: CommandTemplateDao) { class CommandTemplateRepository(private val commandDao: CommandTemplateDao) {
val items : LiveData<List<CommandTemplate>> = commandDao.getAllTemplatesLiveData() val items : Flow<List<CommandTemplate>> = commandDao.getAllTemplatesFlow()
val shortcuts : LiveData<List<TemplateShortcut>> = commandDao.getAllShortcutsLiveData() val shortcuts : Flow<List<TemplateShortcut>> = commandDao.getAllShortcutsFlow()
fun getAll() : List<CommandTemplate> { fun getAll() : List<CommandTemplate> {
return commandDao.getAllTemplates() return commandDao.getAllTemplates()

View file

@ -3,9 +3,10 @@ package com.deniscerri.ytdlnis.database.repository
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.dao.CookieDao import com.deniscerri.ytdlnis.database.dao.CookieDao
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
import kotlinx.coroutines.flow.Flow
class CookieRepository(private val cookieDao: CookieDao) { class CookieRepository(private val cookieDao: CookieDao) {
val items : LiveData<List<CookieItem>> = cookieDao.getAllCookiesLiveData() val items : Flow<List<CookieItem>> = cookieDao.getAllCookiesFlow()
fun getAll() : List<CookieItem> { fun getAll() : List<CookieItem> {
return cookieDao.getAllCookies() return cookieDao.getAllCookies()

View file

@ -4,15 +4,16 @@ import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.Converters import com.deniscerri.ytdlnis.database.Converters
import com.deniscerri.ytdlnis.database.dao.DownloadDao import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import kotlinx.coroutines.flow.Flow
class DownloadRepository(private val downloadDao: DownloadDao) { class DownloadRepository(private val downloadDao: DownloadDao) {
val allDownloads : LiveData<List<DownloadItem>> = downloadDao.getAllDownloads() val allDownloads : Flow<List<DownloadItem>> = downloadDao.getAllDownloads()
val activeDownloads : LiveData<List<DownloadItem>> = downloadDao.getActiveDownloads() val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads()
val activeDownloadsCount : LiveData<Int> = downloadDao.getActiveDownloadsCount() val activeDownloadsCount : Flow<Int> = downloadDao.getActiveDownloadsCount()
val queuedDownloads : LiveData<List<DownloadItem>> = downloadDao.getQueuedDownloads() val queuedDownloads : Flow<List<DownloadItem>> = downloadDao.getQueuedDownloads()
val cancelledDownloads : LiveData<List<DownloadItem>> = downloadDao.getCancelledDownloads() val cancelledDownloads : Flow<List<DownloadItem>> = downloadDao.getCancelledDownloads()
val erroredDownloads : LiveData<List<DownloadItem>> = downloadDao.getErroredDownloads() val erroredDownloads : Flow<List<DownloadItem>> = downloadDao.getErroredDownloads()
val processingDownloads : LiveData<List<DownloadItem>> = downloadDao.getProcessingDownloads() val processingDownloads : Flow<List<DownloadItem>> = downloadDao.getProcessingDownloads()
enum class Status { enum class Status {
Active, Queued, Error, Processing, Cancelled Active, Queued, Error, Processing, Cancelled

View file

@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.dao.HistoryDao import com.deniscerri.ytdlnis.database.dao.HistoryDao
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
class HistoryRepository(private val historyDao: HistoryDao) { class HistoryRepository(private val historyDao: HistoryDao) {
val items : LiveData<List<HistoryItem>> = historyDao.getAllHistory() val items : Flow<List<HistoryItem>> = historyDao.getAllHistory()
enum class HistorySort{ enum class HistorySort{
DESC, ASC DESC, ASC
} }
@ -59,9 +61,11 @@ class HistoryRepository(private val historyDao: HistoryDao) {
} }
suspend fun clearDeletedHistory(){ suspend fun clearDeletedHistory(){
items.value?.forEach { item -> items.collectLatest {
if (!FileUtil.exists(item.downloadPath)){ it.forEach { item ->
historyDao.delete(item.id) if (!FileUtil.exists(item.downloadPath)){
historyDao.delete(item.id)
}
} }
} }
} }

View file

@ -2,18 +2,15 @@ package com.deniscerri.ytdlnis.database.repository
import android.content.Context import android.content.Context
import android.util.Log import android.util.Log
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.dao.ResultDao import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
class ResultRepository(private val resultDao: ResultDao, private val commandTemplateDao: CommandTemplateDao, private val context: Context) { class ResultRepository(private val resultDao: ResultDao, private val context: Context) {
private val tag: String = "ResultRepository" private val tag: String = "ResultRepository"
val allResults : LiveData<List<ResultItem>> = resultDao.getResults() val allResults : Flow<List<ResultItem>> = resultDao.getResults()
var itemCount = MutableStateFlow(-1) var itemCount = MutableStateFlow(-1)
suspend fun insert(it: ResultItem){ suspend fun insert(it: ResultItem){

View file

@ -5,6 +5,7 @@ import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE import android.content.Context.CLIPBOARD_SERVICE
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.CommandTemplate
@ -27,8 +28,8 @@ class CommandTemplateViewModel(private val application: Application) : AndroidVi
init { init {
val dao = DBManager.getInstance(application).commandTemplateDao val dao = DBManager.getInstance(application).commandTemplateDao
repository = CommandTemplateRepository(dao) repository = CommandTemplateRepository(dao)
items = repository.items items = repository.items.asLiveData()
shortcuts = repository.shortcuts shortcuts = repository.shortcuts.asLiveData()
} }
fun getTemplate(itemId: Long): CommandTemplate { fun getTemplate(itemId: Long): CommandTemplate {

View file

@ -10,6 +10,7 @@ import android.util.Log
import android.webkit.CookieManager import android.webkit.CookieManager
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.models.CookieItem
@ -31,7 +32,7 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a
init { init {
val dao = DBManager.getInstance(application).cookieDao val dao = DBManager.getInstance(application).cookieDao
repository = CookieRepository(dao) repository = CookieRepository(dao)
items = repository.items items = repository.items.asLiveData()
} }
fun getAll(): List<CookieItem> { fun getAll(): List<CookieItem> {

View file

@ -11,6 +11,7 @@ import android.util.DisplayMetrics
import android.widget.Toast import android.widget.Toast
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.Constraints import androidx.work.Constraints
@ -77,13 +78,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
infoUtil = InfoUtil(application) infoUtil = InfoUtil(application)
allDownloads = repository.allDownloads allDownloads = repository.allDownloads.asLiveData()
queuedDownloads = repository.queuedDownloads queuedDownloads = repository.queuedDownloads.asLiveData()
activeDownloads = repository.activeDownloads activeDownloads = repository.activeDownloads.asLiveData()
activeDownloadsCount = repository.activeDownloadsCount activeDownloadsCount = repository.activeDownloadsCount.asLiveData()
processingDownloads = repository.processingDownloads processingDownloads = repository.processingDownloads.asLiveData()
cancelledDownloads = repository.cancelledDownloads cancelledDownloads = repository.cancelledDownloads.asLiveData()
erroredDownloads = repository.erroredDownloads erroredDownloads = repository.erroredDownloads.asLiveData()
videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString() videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString()
formatIDPreference = sharedPreferences.getString("format_id", "").toString() formatIDPreference = sharedPreferences.getString("format_id", "").toString()
@ -95,15 +96,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
resources = Resources(application.assets, metrics, confTmp) resources = Resources(application.assets, metrics, confTmp)
val videoFormat = resources.getStringArray(R.array.video_formats)
val videoFormatValues = resources.getStringArray(R.array.video_formats_values) val videoFormatValues = resources.getStringArray(R.array.video_formats_values)
var videoContainer = sharedPreferences.getString("video_format", "Default") var videoContainer = sharedPreferences.getString("video_format", "Default")
if (videoContainer == "Default") videoContainer = App.instance.getString(R.string.defaultValue) if (videoContainer == "Default") videoContainer = App.instance.getString(R.string.defaultValue)
defaultVideoFormats = mutableListOf() defaultVideoFormats = mutableListOf()
videoFormat.forEachIndexed { index , it -> videoFormatValues.forEach {
val tmp = Format( val tmp = Format(
videoFormatValues[index], it,
videoContainer!!, videoContainer!!,
"", "",
"", "",
@ -125,7 +125,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
"", "",
"", "",
0, 0,
resources.getString(R.string.best_quality) "best"
) )
} }
@ -381,7 +381,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
} }
fun getGenericVideoFormats() : MutableList<Format>{ fun getGenericVideoFormats() : MutableList<Format>{
val videoFormats = resources.getStringArray(R.array.video_formats) val videoFormats = resources.getStringArray(R.array.video_formats_values)
val formats = mutableListOf<Format>() val formats = mutableListOf<Format>()
var containerPreference = sharedPreferences.getString("video_format", "Default") var containerPreference = sharedPreferences.getString("video_format", "Default")
if (containerPreference == "Default") containerPreference = resources.getString(R.string.defaultValue) if (containerPreference == "Default") containerPreference = resources.getString(R.string.defaultValue)

View file

@ -5,6 +5,7 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.models.HistoryItem
@ -28,7 +29,7 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
init { init {
val dao = DBManager.getInstance(application).historyDao val dao = DBManager.getInstance(application).historyDao
repository = HistoryRepository(dao) repository = HistoryRepository(dao)
allItems = repository.items allItems = repository.items.asLiveData()
_items.addSource(allItems){ _items.addSource(allItems){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!) filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!)

View file

@ -3,10 +3,10 @@ package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.App
@ -17,7 +17,6 @@ import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
import com.deniscerri.ytdlnis.database.repository.ResultRepository import com.deniscerri.ytdlnis.database.repository.ResultRepository
import com.deniscerri.ytdlnis.database.repository.SearchHistoryRepository import com.deniscerri.ytdlnis.database.repository.SearchHistoryRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.regex.Pattern import java.util.regex.Pattern
@ -33,9 +32,9 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
init { init {
val dao = DBManager.getInstance(application).resultDao val dao = DBManager.getInstance(application).resultDao
val commandDao = DBManager.getInstance(application).commandTemplateDao val commandDao = DBManager.getInstance(application).commandTemplateDao
repository = ResultRepository(dao, commandDao, getApplication<Application>().applicationContext) repository = ResultRepository(dao, getApplication<Application>().applicationContext)
searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao) searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao)
items = repository.allResults items = repository.allResults.asLiveData()
loadingItems.postValue(false) loadingItems.postValue(false)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
} }

View file

@ -76,7 +76,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO) { downloadItem = withContext(Dispatchers.IO) {
if (currentDownloadItem != null && currentDownloadItem!!.type == Type.audio){ if (currentDownloadItem != null){
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java) val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java) Gson().fromJson(string, DownloadItem::class.java)
}else{ }else{
@ -126,7 +126,17 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
if (free == "?") freeSpace.visibility = View.GONE if (free == "?") freeSpace.visibility = View.GONE
var formats = mutableListOf<Format>() var formats = mutableListOf<Format>()
formats.addAll(resultItem.formats.filter { it.format_note.contains("audio", ignoreCase = true) }) if (currentDownloadItem == null) {
formats.addAll(resultItem.formats.filter { it.format_note.contains("audio", ignoreCase = true) })
}else{
//if its updating a present downloaditem and its the wrong category
if (currentDownloadItem!!.type != Type.audio){
downloadItem.type = Type.audio
downloadItem.format =
downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
.maxByOrNull { it.filesize }!!
}
}
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) }) if (formats.isEmpty()) formats.addAll(downloadItem.allFormats.filter { it.format_note.contains("audio", ignoreCase = true) })
val containers = requireContext().resources.getStringArray(R.array.audio_containers) val containers = requireContext().resources.getStringArray(R.array.audio_containers)

View file

@ -77,7 +77,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO){ downloadItem = withContext(Dispatchers.IO){
if (currentDownloadItem != null && currentDownloadItem!!.type == Type.video){ if (currentDownloadItem != null){
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java) val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java) Gson().fromJson(string, DownloadItem::class.java)
}else{ }else{
@ -127,8 +127,19 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free) freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE if (free == "?") freeSpace.visibility = View.GONE
var formats = mutableListOf<Format>() var formats = mutableListOf<Format>()
formats.addAll(resultItem.formats) if (currentDownloadItem == null) {
formats.addAll(resultItem.formats)
}else{
//if its updating a present downloaditem and its the wrong category
if (currentDownloadItem!!.type != Type.video){
downloadItem.type = Type.video
downloadItem.format =
downloadItem.allFormats.filter { it.vcodec.isNotEmpty() }
.maxByOrNull { it.filesize }!!
}
}
if (formats.isEmpty()) formats.addAll(downloadItem.allFormats) if (formats.isEmpty()) formats.addAll(downloadItem.allFormats)
val containers = requireContext().resources.getStringArray(R.array.video_containers) val containers = requireContext().resources.getStringArray(R.array.video_containers)

View file

@ -15,9 +15,7 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil import com.deniscerri.ytdlnis.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
@ -27,6 +25,7 @@ import com.google.android.material.card.MaterialCardView
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.internal.format
import java.util.* import java.util.*
@ -45,6 +44,11 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private lateinit var videoTitle : TextView private lateinit var videoTitle : TextView
private lateinit var audioTitle : TextView private lateinit var audioTitle : TextView
private lateinit var sortBy : FormatSorting
enum class FormatSorting {
filesize, container, id
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity().applicationContext) infoUtil = InfoUtil(requireActivity().applicationContext)
@ -61,6 +65,8 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
val view = LayoutInflater.from(context).inflate(R.layout.format_select_bottom_sheet, null) val view = LayoutInflater.from(context).inflate(R.layout.format_select_bottom_sheet, null)
dialog.setContentView(view) dialog.setContentView(view)
sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!)
dialog.setOnShowListener { dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View) behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics() val displayMetrics = DisplayMetrics()
@ -78,10 +84,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
okBtn = view.findViewById(R.id.format_ok) okBtn = view.findViewById(R.id.format_ok)
shimmers.visibility = View.GONE shimmers.visibility = View.GONE
val hasGenericFormats = when(items.first()!!.type){ val hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" }
Type.audio -> formats.first().size == resources.getStringArray(R.array.audio_formats).size
else -> formats.first().size == resources.getStringArray(R.array.video_formats).size
}
if (items.size > 1){ if (items.size > 1){
if (!hasGenericFormats){ if (!hasGenericFormats){
@ -131,10 +134,10 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
infoUtil.getFormats(items.first()!!.url) infoUtil.getFormats(items.first()!!.url)
} }
res.filter { it.format_note != "storyboard" } res.filter { it.format_note != "storyboard" }
if(items.first()?.type == Type.audio){ chosenFormats = if(items.first()?.type == Type.audio){
chosenFormats = res.filter { it.format_note.contains("audio", ignoreCase = true) } res.filter { it.format_note.contains("audio", ignoreCase = true) }
}else{ }else{
chosenFormats = res res
} }
if (chosenFormats.isEmpty()) throw Exception() if (chosenFormats.isEmpty()) throw Exception()
//playlist format filtering //playlist format filtering
@ -216,7 +219,14 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
} }
} }
private fun addFormatsToView(){ private fun addFormatsToView(){
val canMultiSelectAudio = items.first()?.type == Type.video && chosenFormats.find { it.format_note.contains("audio", ignoreCase = true) } != null //sort
val finalFormats: List<Format> = when(sortBy){
FormatSorting.container -> chosenFormats.groupBy { it.container }.flatMap { it.value }
FormatSorting.id -> chosenFormats.sortedBy { it.format_id }
FormatSorting.filesize -> chosenFormats
}
val canMultiSelectAudio = items.first()?.type == Type.video && finalFormats.find { it.format_note.contains("audio", ignoreCase = true) } != null
videoFormatList.removeAllViews() videoFormatList.removeAllViews()
audioFormatList.removeAllViews() audioFormatList.removeAllViews()
@ -226,7 +236,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
audioTitle.visibility = View.GONE audioTitle.visibility = View.GONE
okBtn.visibility = View.GONE okBtn.visibility = View.GONE
}else{ }else{
if (chosenFormats.count { it.vcodec.isBlank() || it.vcodec == "none" } == 0){ if (finalFormats.count { it.vcodec.isBlank() || it.vcodec == "none" } == 0){
audioFormatList.visibility = View.GONE audioFormatList.visibility = View.GONE
audioTitle.visibility = View.GONE audioTitle.visibility = View.GONE
videoTitle.visibility = View.GONE videoTitle.visibility = View.GONE
@ -239,8 +249,8 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
} }
} }
for (i in chosenFormats.lastIndex downTo 0){ for (i in finalFormats.lastIndex downTo 0){
val format = chosenFormats[i] val format = finalFormats[i]
val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null) val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null)
formatItem.tag = "${format.format_id}${format.format_note}" formatItem.tag = "${format.format_id}${format.format_note}"
UiUtil.populateFormatCard(formatItem as MaterialCardView, format, null) UiUtil.populateFormatCard(formatItem as MaterialCardView, format, null)
@ -250,7 +260,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
val clickedCard = (clickedformat as MaterialCardView) val clickedCard = (clickedformat as MaterialCardView)
if (format.vcodec.isNotBlank() && format.vcodec != "none") { if (format.vcodec.isNotBlank() && format.vcodec != "none") {
if (clickedCard.isChecked) { if (clickedCard.isChecked) {
listener.onFormatClick(List(items.size){chosenFormats}, listOf(FormatTuple(format, null))) listener.onFormatClick(List(items.size){finalFormats}, listOf(FormatTuple(format, null)))
dismiss() dismiss()
} }
videoFormatList.forEach { (it as MaterialCardView).isChecked = false } videoFormatList.forEach { (it as MaterialCardView).isChecked = false }
@ -269,14 +279,14 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
} }
}else{ }else{
if (items.size == 1){ if (items.size == 1){
listener.onFormatClick(List(items.size){chosenFormats}, listOf(FormatTuple(format, null))) listener.onFormatClick(List(items.size){finalFormats}, listOf(FormatTuple(format, null)))
}else{ }else{
val selectedFormats = mutableListOf<Format>() val selectedFormats = mutableListOf<Format>()
formatCollection.forEach { formatCollection.forEach {
selectedFormats.add(it.first{ f -> f.format_id == format.format_id}) selectedFormats.add(it.first{ f -> f.format_id == format.format_id})
} }
if (selectedFormats.isEmpty()) { if (selectedFormats.isEmpty()) {
items.forEach { items.forEach { _ ->
selectedFormats.add(format) selectedFormats.add(format)
} }
} }

View file

@ -102,6 +102,10 @@ class DownloadQueueMainFragment : Fragment(){
}) })
mainActivity.hideBottomNavigation() mainActivity.hideBottomNavigation()
initMenu() initMenu()
if (arguments?.getString("tab") != null){
tabLayout.selectTab(tabLayout.getTabAt(3))
}
} }
private fun initMenu() { private fun initMenu() {

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.downloads package com.deniscerri.ytdlnis.ui.downloads
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Canvas import android.graphics.Canvas
@ -59,6 +60,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
private lateinit var queuedRecyclerView : RecyclerView private lateinit var queuedRecyclerView : RecyclerView
private lateinit var queuedDownloads : GenericDownloadAdapter private lateinit var queuedDownloads : GenericDownloadAdapter
private lateinit var notificationUtil: NotificationUtil private lateinit var notificationUtil: NotificationUtil
private lateinit var fileSize: TextView
private var selectedObjects: ArrayList<DownloadItem>? = null private var selectedObjects: ArrayList<DownloadItem>? = null
private var actionMode : ActionMode? = null private var actionMode : ActionMode? = null
private lateinit var items : MutableList<DownloadItem> private lateinit var items : MutableList<DownloadItem>
@ -68,7 +70,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false) fragmentView = inflater.inflate(R.layout.fragment_inqueue, container, false)
activity = getActivity() activity = getActivity()
notificationUtil = NotificationUtil(requireContext()) notificationUtil = NotificationUtil(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
@ -77,9 +79,10 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
return fragmentView return fragmentView
} }
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
fileSize = view.findViewById(R.id.filesize)
queuedDownloads = queuedDownloads =
GenericDownloadAdapter( GenericDownloadAdapter(
this, this,
@ -97,6 +100,11 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
downloadViewModel.queuedDownloads.observe(viewLifecycleOwner) { downloadViewModel.queuedDownloads.observe(viewLifecycleOwner) {
items = it.toMutableList() items = it.toMutableList()
if (it.isEmpty()) fileSize.visibility = View.GONE
else{
fileSize.visibility = View.VISIBLE
fileSize.text = "${getString(R.string.file_size)}: ~ ${FileUtil.convertFileSize(it.sumOf { i -> i.format.filesize })}"
}
queuedDownloads.submitList(it) queuedDownloads.submitList(it)
} }
} }
@ -232,6 +240,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
override fun onCardSelect(itemID: Long, isChecked: Boolean) { override fun onCardSelect(itemID: Long, isChecked: Boolean) {
val item = items.find { it.id == itemID } val item = items.find { it.id == itemID }
val now = System.currentTimeMillis()
if (isChecked) { if (isChecked) {
selectedObjects!!.add(item!!) selectedObjects!!.add(item!!)
if (actionMode == null){ if (actionMode == null){
@ -248,6 +257,9 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
actionMode?.finish() actionMode?.finish()
} }
} }
if (actionMode != null){
actionMode!!.menu.getItem(1).isVisible = selectedObjects!!.all { it.downloadStartTime > now }
}
} }
private fun removeItem(itemID: Long){ private fun removeItem(itemID: Long){
@ -311,6 +323,16 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
deleteDialog.show() deleteDialog.show()
true true
} }
R.id.download -> {
for (obj in selectedObjects!!){
WorkManager.getInstance(requireContext()).cancelUniqueWork(obj.id.toInt().toString())
}
selectedObjects!!.forEach { it.downloadStartTime = 0L }
lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.queueDownloads(selectedObjects!!)
}
true
}
R.id.select_all -> { R.id.select_all -> {
queuedDownloads.checkAll(items) queuedDownloads.checkAll(items)
selectedObjects?.clear() selectedObjects?.clear()

View file

@ -90,39 +90,28 @@ object FileUtil {
if(it.name.contains(".part-Frag")) return@forEach if(it.name.contains(".part-Frag")) return@forEach
//sending to main or SD CARD val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*"
if (currentDirectory.exists() && Build.VERSION.SDK_INT >= 26){ destFile = File(currentDirectory.absolutePath + "/${it.name}")
if (Build.VERSION.SDK_INT >= 26 ){
Files.move(it.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}else{
it.renameTo(destFile)
}
fileList.add(destFile)
}else{
//sending to USB OTG
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*"
destFile = File(currentDirectory.absolutePath + "/${it.name}")
val dest = Uri.parse(destDir).run { val dest = Uri.parse(destDir).run {
DocumentsContract.buildDocumentUriUsingTree( DocumentsContract.buildDocumentUriUsingTree(
this, this,
DocumentsContract.getTreeDocumentId(this) DocumentsContract.getTreeDocumentId(this)
) )
}
val destUri = DocumentsContract.createDocument(
context.contentResolver,
dest,
mimeType,
it.name
) ?: return@forEach
val inputStream = it.inputStream()
val outputStream =
context.contentResolver.openOutputStream(destUri) ?: return@forEach
inputStream.copyTo(outputStream)
inputStream.closeQuietly()
outputStream.closeQuietly()
} }
val destUri = DocumentsContract.createDocument(
context.contentResolver,
dest,
mimeType,
it.name
) ?: return@forEach
val inputStream = it.inputStream()
val outputStream =
context.contentResolver.openOutputStream(destUri) ?: return@forEach
inputStream.copyTo(outputStream)
inputStream.closeQuietly()
outputStream.closeQuietly()
fileList.add(destFile) fileList.add(destFile)
}catch (e: java.lang.Exception) { }catch (e: java.lang.Exception) {

View file

@ -245,6 +245,15 @@ class NotificationUtil(var context: Context) {
.setArguments(bundle) .setArguments(bundle)
.createPendingIntent() .createPendingIntent()
val tabBundle = Bundle()
tabBundle.putString("tab", "error")
val errorTabPendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment)
.setArguments(tabBundle)
.createPendingIntent()
notificationBuilder notificationBuilder
.setContentTitle("${context.getString(R.string.failed_download)}: $title") .setContentTitle("${context.getString(R.string.failed_download)}: $title")
.setContentText(error) .setContentText(error)
@ -255,6 +264,7 @@ class NotificationUtil(var context: Context) {
R.drawable.ic_launcher_foreground_large R.drawable.ic_launcher_foreground_large
) )
) )
.setContentIntent(errorTabPendingIntent)
.setPriority(NotificationCompat.PRIORITY_MAX) .setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions() .clearActions()

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdlnis.work package com.deniscerri.ytdlnis.work
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log import android.util.Log
@ -91,6 +92,16 @@ class DownloadWorker(
tempFileDir.delete() tempFileDir.delete()
tempFileDir.mkdirs() tempFileDir.mkdirs()
var useCacheFolder = true
val downloadLocationFile = File(downloadLocation)
if (downloadLocationFile.canWrite()) {
File(downloadLocation).mkdirs()
if(Build.VERSION.SDK_INT > 23) request.addOption("-P", "temp:" + tempFileDir.absolutePath)
useCacheFolder = false
}
val pathUsed = if (useCacheFolder) tempFileDir.absolutePath else downloadLocation
val aria2 = sharedPreferences.getBoolean("aria2", false) val aria2 = sharedPreferences.getBoolean("aria2", false)
if (aria2) { if (aria2) {
request.addOption("--downloader", "libaria2c.so") request.addOption("--downloader", "libaria2c.so")
@ -99,6 +110,13 @@ class DownloadWorker(
val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1) val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
if (concurrentFragments > 1) request.addOption("-N", concurrentFragments) if (concurrentFragments > 1) request.addOption("-N", concurrentFragments)
} }
val retries = sharedPreferences.getString("--retries", "")!!
val fragmentRetries = sharedPreferences.getString("--fragment_retries", "")!!
if(retries.isNotEmpty()) request.addOption("retries", retries)
if(fragmentRetries.isNotEmpty()) request.addOption("fragment-retries", fragmentRetries)
val limitRate = sharedPreferences.getString("limit_rate", "") val limitRate = sharedPreferences.getString("limit_rate", "")
if (limitRate != "") request.addOption("-r", limitRate!!) if (limitRate != "") request.addOption("-r", limitRate!!)
if(downloadItem.type != DownloadViewModel.Type.command){ if(downloadItem.type != DownloadViewModel.Type.command){
@ -215,9 +233,9 @@ class DownloadWorker(
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){ if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters") request.addOption("--split-chapters")
request.addOption("-P", tempFileDir.absolutePath) request.addOption("-P", pathUsed)
}else{ }else{
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s") request.addOption("-o", pathUsed + "/${downloadItem.customFileNameTemplate}.%(ext)s")
} }
} }
@ -281,9 +299,9 @@ class DownloadWorker(
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){ if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters") request.addOption("--split-chapters")
request.addOption("-P", tempFileDir.absolutePath) request.addOption("-P", pathUsed)
}else{ }else{
request.addOption("-o", tempFileDir.absolutePath + "/${downloadItem.customFileNameTemplate}.%(ext)s") request.addOption("-o", pathUsed + "/${downloadItem.customFileNameTemplate}.%(ext)s")
} }
} }
@ -294,7 +312,7 @@ class DownloadWorker(
writeText(downloadItem.format.format_note) writeText(downloadItem.format.format_note)
}.absolutePath }.absolutePath
) )
request.addOption("-P", tempFileDir.absolutePath) request.addOption("-P", pathUsed)
} }
} }
@ -327,33 +345,43 @@ class DownloadWorker(
} }
} }
}.onSuccess { }.onSuccess {
//move file from internal to set download directory
setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads))
var finalPaths : List<String>?
try {
finalPaths = FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p ->
setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads))
}
if (finalPaths.isNotEmpty()){
setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id, "log" to logDownloads))
}else{
finalPaths = listOf(context.getString(R.string.unfound_file))
}
}catch (e: Exception){
finalPaths = listOf(context.getString(R.string.unfound_file))
e.printStackTrace()
handler.postDelayed({
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}, 1000)
}
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao) val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
var finalPaths: List<String>
if (useCacheFolder){
//move file from internal to set download directory
setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads))
try {
finalPaths = FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p ->
setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads))
}
if (finalPaths.isNotEmpty()){
setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id, "log" to logDownloads))
}else{
finalPaths = listOf(context.getString(R.string.unfound_file))
}
}catch (e: Exception){
finalPaths = listOf(context.getString(R.string.unfound_file))
e.printStackTrace()
handler.postDelayed({
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}, 1000)
}
}else{
val now = System.currentTimeMillis()
finalPaths = downloadLocationFile
.walkTopDown()
.filter { it.isFile && it.absolutePath.contains(downloadItem.title) && it.lastModified() > now - 10000000}
.map { it.absolutePath }
.toList()
if (finalPaths.isEmpty()) finalPaths = listOf(context.getString(R.string.unfound_file))
}
//put download in history //put download in history
val incognito = sharedPreferences.getBoolean("incognito", false) val incognito = sharedPreferences.getBoolean("incognito", false)
if (!incognito) { if (!incognito) {
val unixtime = System.currentTimeMillis() / 1000 val unixtime = System.currentTimeMillis() / 1000
val file = File(finalPaths?.first()!!) val file = File(finalPaths.first()!!)
downloadItem.format.filesize = file.length() downloadItem.format.filesize = file.length()
val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id) val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id)
runBlocking { runBlocking {
@ -363,9 +391,7 @@ class DownloadWorker(
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
notificationUtil.createDownloadFinished( notificationUtil.createDownloadFinished(
downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else listOf( downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths,
finalPaths!!.maxByOrNull { it.length }!!
),
NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID
) )

View file

@ -0,0 +1,4 @@
<vector android:autoMirrored="true" android:height="24dp" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
</vector>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:id="@+id/filesize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingHorizontal="30dp"
android:paddingVertical="10dp"
android:text="@string/file_size"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filesize">
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -8,6 +8,13 @@
android:title="@string/remove_results" android:title="@string/remove_results"
android:icon="@drawable/baseline_delete_24" android:icon="@drawable/baseline_delete_24"
app:showAsAction="ifRoom" /> app:showAsAction="ifRoom" />
<item
android:id="@+id/download"
android:title="@string/download"
android:icon="@drawable/baseline_download_24"
app:showAsAction="ifRoom" />
<item <item
android:id="@+id/select_all" android:id="@+id/select_all"
android:title="@string/select_all" android:title="@string/select_all"

View file

@ -58,8 +58,20 @@
</string-array> </string-array>
<string-array name="audio_formats"> <string-array name="audio_formats">
<item>@string/worst_quality</item> <item>worst</item>
<item>@string/best_quality</item> <item>best</item>
</string-array>
<string-array name="format_ordering">
<item>@string/file_size</item>
<item>@string/container</item>
<item>ID</item>
</string-array>
<string-array name="format_ordering_values">
<item>filesize</item>
<item>container</item>
<item>id</item>
</string-array> </string-array>
<string-array name="sponsorblock_settings_entries"> <string-array name="sponsorblock_settings_entries">

View file

@ -279,4 +279,7 @@
<string name="piped_instance">Piped Instance</string> <string name="piped_instance">Piped Instance</string>
<string name="piped_instance_summary">Write a PIPED API Server, the app can use for YouTube queries and formats</string> <string name="piped_instance_summary">Write a PIPED API Server, the app can use for YouTube queries and formats</string>
<string name="custom_audio_quality">Use Custom Audio Quality</string> <string name="custom_audio_quality">Use Custom Audio Quality</string>
<string name="retries">Retries</string>
<string name="fragment_retries">Fragment Retries</string>
<string name="format_order">Format Order</string>
</resources> </resources>

View file

@ -104,6 +104,14 @@
app:summary="@string/log_downloads_summary" app:summary="@string/log_downloads_summary"
app:title="@string/log_downloads" /> app:title="@string/log_downloads" />
<EditTextPreference
app:key="retries"
app:title="@string/retries" />
<EditTextPreference
app:key="fragment_retries"
app:title="@string/fragment_retries" />
<Preference <Preference
app:icon="@drawable/ic_battery" app:icon="@drawable/ic_battery"
app:key="ignore_battery" app:key="ignore_battery"

View file

@ -38,6 +38,15 @@
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:title="@string/format_source" /> app:title="@string/format_source" />
<ListPreference
android:defaultValue="filesize"
android:entries="@array/format_ordering"
android:entryValues="@array/format_ordering_values"
android:icon="@drawable/baseline_sort_24"
app:key="format_order"
app:useSimpleSummaryProvider="true"
app:title="@string/format_order" />
<EditTextPreference <EditTextPreference
app:key="piped_instance" app:key="piped_instance"
app:defaultValue="https://pipedapi.kavin.rocks" app:defaultValue="https://pipedapi.kavin.rocks"