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 versionMinor = 6
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 = ""
if (versionBuild > 0){
@ -64,7 +64,8 @@ android {
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false
signingConfig signingConfigs.debug
}
@ -157,7 +158,7 @@ dependencies {
androidTestImplementation "junit:junit:$junitVer"
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
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:core:1.5.0"
androidTestImplementation "androidx.test:rules:1.5.0"
@ -175,10 +176,10 @@ dependencies {
implementation "androidx.work:work-runtime-ktx:$workVer"
implementation "androidx.room:room-runtime:2.5.1"
implementation "androidx.room:room-ktx:2.5.1"
kapt "androidx.room:room-compiler:2.5.1"
androidTestImplementation "androidx.room:room-testing:2.5.1"
implementation "androidx.room:room-runtime:2.5.2"
implementation "androidx.room:room-ktx:2.5.2"
kapt "androidx.room:room-compiler:2.5.2"
androidTestImplementation "androidx.room:room-testing:2.5.2"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation "com.google.truth:truth:1.1.3"
@ -190,7 +191,7 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.10"
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 "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation "com.github.Irineu333:Highlight-KT:1.0.4"

View file

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

View file

@ -1,8 +1,8 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.CookieItem
import kotlinx.coroutines.flow.Flow
@Dao
interface CookieDao {
@ -10,7 +10,7 @@ interface CookieDao {
fun getAllCookies() : List<CookieItem>
@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)")
fun checkIfExistsWithSameURL(url: String) : Boolean

View file

@ -1,20 +1,20 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.DownloadItem
import kotlinx.coroutines.flow.Flow
@Dao
interface DownloadDao {
@Query("SELECT * FROM downloads ORDER BY status")
fun getAllDownloads() : LiveData<List<DownloadItem>>
fun getAllDownloads() : Flow<List<DownloadItem>>
@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'")
fun getActiveDownloadsCount() : LiveData<Int>
fun getActiveDownloadsCount() : Flow<Int>
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloadsList() : List<DownloadItem>
@ -23,25 +23,25 @@ interface DownloadDao {
fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@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")
fun getQueuedDownloadsList() : List<DownloadItem>
@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")
fun getCancelledDownloadsList() : List<DownloadItem>
@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")
fun getErroredDownloadsList() : List<DownloadItem>
@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")
fun getDownloadById(id: Long) : DownloadItem

View file

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

View file

@ -1,16 +1,16 @@
package com.deniscerri.ytdlnis.database.dao
import androidx.lifecycle.LiveData
import androidx.room.*
import com.deniscerri.ytdlnis.database.models.ResultItem
import kotlinx.coroutines.flow.Flow
@Dao
interface ResultDao {
@Query("SELECT * FROM results")
fun getResults() : LiveData<List<ResultItem>>
fun getResults() : Flow<List<ResultItem>>
@Query("SELECT COUNT(id) FROM results")
fun getCount() : LiveData<Int>
fun getCount() : Flow<Int>
@Query("SELECT COUNT(id) FROM results")
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.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.TemplateShortcut
import kotlinx.coroutines.flow.Flow
class CommandTemplateRepository(private val commandDao: CommandTemplateDao) {
val items : LiveData<List<CommandTemplate>> = commandDao.getAllTemplatesLiveData()
val shortcuts : LiveData<List<TemplateShortcut>> = commandDao.getAllShortcutsLiveData()
val items : Flow<List<CommandTemplate>> = commandDao.getAllTemplatesFlow()
val shortcuts : Flow<List<TemplateShortcut>> = commandDao.getAllShortcutsFlow()
fun getAll() : List<CommandTemplate> {
return commandDao.getAllTemplates()

View file

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

View file

@ -4,15 +4,16 @@ import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.Converters
import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.models.DownloadItem
import kotlinx.coroutines.flow.Flow
class DownloadRepository(private val downloadDao: DownloadDao) {
val allDownloads : LiveData<List<DownloadItem>> = downloadDao.getAllDownloads()
val activeDownloads : LiveData<List<DownloadItem>> = downloadDao.getActiveDownloads()
val activeDownloadsCount : LiveData<Int> = downloadDao.getActiveDownloadsCount()
val queuedDownloads : LiveData<List<DownloadItem>> = downloadDao.getQueuedDownloads()
val cancelledDownloads : LiveData<List<DownloadItem>> = downloadDao.getCancelledDownloads()
val erroredDownloads : LiveData<List<DownloadItem>> = downloadDao.getErroredDownloads()
val processingDownloads : LiveData<List<DownloadItem>> = downloadDao.getProcessingDownloads()
val allDownloads : Flow<List<DownloadItem>> = downloadDao.getAllDownloads()
val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads()
val activeDownloadsCount : Flow<Int> = downloadDao.getActiveDownloadsCount()
val queuedDownloads : Flow<List<DownloadItem>> = downloadDao.getQueuedDownloads()
val cancelledDownloads : Flow<List<DownloadItem>> = downloadDao.getCancelledDownloads()
val erroredDownloads : Flow<List<DownloadItem>> = downloadDao.getErroredDownloads()
val processingDownloads : Flow<List<DownloadItem>> = downloadDao.getProcessingDownloads()
enum class Status {
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.models.HistoryItem
import com.deniscerri.ytdlnis.util.FileUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
class HistoryRepository(private val historyDao: HistoryDao) {
val items : LiveData<List<HistoryItem>> = historyDao.getAllHistory()
val items : Flow<List<HistoryItem>> = historyDao.getAllHistory()
enum class HistorySort{
DESC, ASC
}
@ -59,9 +61,11 @@ class HistoryRepository(private val historyDao: HistoryDao) {
}
suspend fun clearDeletedHistory(){
items.value?.forEach { item ->
if (!FileUtil.exists(item.downloadPath)){
historyDao.delete(item.id)
items.collectLatest {
it.forEach { item ->
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.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.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.util.InfoUtil
import kotlinx.coroutines.flow.Flow
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"
val allResults : LiveData<List<ResultItem>> = resultDao.getResults()
val allResults : Flow<List<ResultItem>> = resultDao.getResults()
var itemCount = MutableStateFlow(-1)
suspend fun insert(it: ResultItem){

View file

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

View file

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

View file

@ -11,6 +11,7 @@ import android.util.DisplayMetrics
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
import androidx.work.Constraints
@ -77,13 +78,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
infoUtil = InfoUtil(application)
allDownloads = repository.allDownloads
queuedDownloads = repository.queuedDownloads
activeDownloads = repository.activeDownloads
activeDownloadsCount = repository.activeDownloadsCount
processingDownloads = repository.processingDownloads
cancelledDownloads = repository.cancelledDownloads
erroredDownloads = repository.erroredDownloads
allDownloads = repository.allDownloads.asLiveData()
queuedDownloads = repository.queuedDownloads.asLiveData()
activeDownloads = repository.activeDownloads.asLiveData()
activeDownloadsCount = repository.activeDownloadsCount.asLiveData()
processingDownloads = repository.processingDownloads.asLiveData()
cancelledDownloads = repository.cancelledDownloads.asLiveData()
erroredDownloads = repository.erroredDownloads.asLiveData()
videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString()
formatIDPreference = sharedPreferences.getString("format_id", "").toString()
@ -95,15 +96,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
resources = Resources(application.assets, metrics, confTmp)
val videoFormat = resources.getStringArray(R.array.video_formats)
val videoFormatValues = resources.getStringArray(R.array.video_formats_values)
var videoContainer = sharedPreferences.getString("video_format", "Default")
if (videoContainer == "Default") videoContainer = App.instance.getString(R.string.defaultValue)
defaultVideoFormats = mutableListOf()
videoFormat.forEachIndexed { index , it ->
videoFormatValues.forEach {
val tmp = Format(
videoFormatValues[index],
it,
videoContainer!!,
"",
"",
@ -125,7 +125,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
"",
"",
0,
resources.getString(R.string.best_quality)
"best"
)
}
@ -381,7 +381,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
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>()
var containerPreference = sharedPreferences.getString("video_format", "Default")
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.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.models.HistoryItem
@ -28,7 +29,7 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
init {
val dao = DBManager.getInstance(application).historyDao
repository = HistoryRepository(dao)
allItems = repository.items
allItems = repository.items.asLiveData()
_items.addSource(allItems){
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.content.SharedPreferences
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
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.SearchHistoryRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.regex.Pattern
@ -33,9 +32,9 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
init {
val dao = DBManager.getInstance(application).resultDao
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)
items = repository.allResults
items = repository.allResults.asLiveData()
loadingItems.postValue(false)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
}

View file

@ -76,7 +76,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
super.onViewCreated(view, savedInstanceState)
lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO) {
if (currentDownloadItem != null && currentDownloadItem!!.type == Type.audio){
if (currentDownloadItem != null){
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java)
}else{
@ -126,7 +126,17 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr
if (free == "?") freeSpace.visibility = View.GONE
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) })
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)
lifecycleScope.launch {
downloadItem = withContext(Dispatchers.IO){
if (currentDownloadItem != null && currentDownloadItem!!.type == Type.video){
if (currentDownloadItem != null){
val string = Gson().toJson(currentDownloadItem, DownloadItem::class.java)
Gson().fromJson(string, DownloadItem::class.java)
}else{
@ -127,8 +127,19 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr
freeSpace.text = String.format( getString(R.string.freespace) + ": " + free)
if (free == "?") freeSpace.visibility = View.GONE
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)
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.database.models.DownloadItem
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.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout
@ -27,6 +25,7 @@ import com.google.android.material.card.MaterialCardView
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.internal.format
import java.util.*
@ -45,6 +44,11 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
private lateinit var videoTitle : TextView
private lateinit var audioTitle : TextView
private lateinit var sortBy : FormatSorting
enum class FormatSorting {
filesize, container, id
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
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)
dialog.setContentView(view)
sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!)
dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View)
val displayMetrics = DisplayMetrics()
@ -78,10 +84,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
okBtn = view.findViewById(R.id.format_ok)
shimmers.visibility = View.GONE
val hasGenericFormats = when(items.first()!!.type){
Type.audio -> formats.first().size == resources.getStringArray(R.array.audio_formats).size
else -> formats.first().size == resources.getStringArray(R.array.video_formats).size
}
val hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" }
if (items.size > 1){
if (!hasGenericFormats){
@ -131,10 +134,10 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
infoUtil.getFormats(items.first()!!.url)
}
res.filter { it.format_note != "storyboard" }
if(items.first()?.type == Type.audio){
chosenFormats = res.filter { it.format_note.contains("audio", ignoreCase = true) }
chosenFormats = if(items.first()?.type == Type.audio){
res.filter { it.format_note.contains("audio", ignoreCase = true) }
}else{
chosenFormats = res
res
}
if (chosenFormats.isEmpty()) throw Exception()
//playlist format filtering
@ -216,7 +219,14 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
}
}
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()
audioFormatList.removeAllViews()
@ -226,7 +236,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
audioTitle.visibility = View.GONE
okBtn.visibility = View.GONE
}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
audioTitle.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){
val format = chosenFormats[i]
for (i in finalFormats.lastIndex downTo 0){
val format = finalFormats[i]
val formatItem = LayoutInflater.from(context).inflate(R.layout.format_item, null)
formatItem.tag = "${format.format_id}${format.format_note}"
UiUtil.populateFormatCard(formatItem as MaterialCardView, format, null)
@ -250,7 +260,7 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
val clickedCard = (clickedformat as MaterialCardView)
if (format.vcodec.isNotBlank() && format.vcodec != "none") {
if (clickedCard.isChecked) {
listener.onFormatClick(List(items.size){chosenFormats}, listOf(FormatTuple(format, null)))
listener.onFormatClick(List(items.size){finalFormats}, listOf(FormatTuple(format, null)))
dismiss()
}
videoFormatList.forEach { (it as MaterialCardView).isChecked = false }
@ -269,14 +279,14 @@ class FormatSelectionBottomSheetDialog(private val items: List<DownloadItem?>, p
}
}else{
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{
val selectedFormats = mutableListOf<Format>()
formatCollection.forEach {
selectedFormats.add(it.first{ f -> f.format_id == format.format_id})
}
if (selectedFormats.isEmpty()) {
items.forEach {
items.forEach { _ ->
selectedFormats.add(format)
}
}

View file

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

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdlnis.ui.downloads
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.graphics.Canvas
@ -59,6 +60,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
private lateinit var queuedRecyclerView : RecyclerView
private lateinit var queuedDownloads : GenericDownloadAdapter
private lateinit var notificationUtil: NotificationUtil
private lateinit var fileSize: TextView
private var selectedObjects: ArrayList<DownloadItem>? = null
private var actionMode : ActionMode? = null
private lateinit var items : MutableList<DownloadItem>
@ -68,7 +70,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
fragmentView = inflater.inflate(R.layout.fragment_generic_download_queue, container, false)
fragmentView = inflater.inflate(R.layout.fragment_inqueue, container, false)
activity = getActivity()
notificationUtil = NotificationUtil(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
@ -77,9 +79,10 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
return fragmentView
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fileSize = view.findViewById(R.id.filesize)
queuedDownloads =
GenericDownloadAdapter(
this,
@ -97,6 +100,11 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
downloadViewModel.queuedDownloads.observe(viewLifecycleOwner) {
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)
}
}
@ -232,6 +240,7 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
override fun onCardSelect(itemID: Long, isChecked: Boolean) {
val item = items.find { it.id == itemID }
val now = System.currentTimeMillis()
if (isChecked) {
selectedObjects!!.add(item!!)
if (actionMode == null){
@ -248,6 +257,9 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
actionMode?.finish()
}
}
if (actionMode != null){
actionMode!!.menu.getItem(1).isVisible = selectedObjects!!.all { it.downloadStartTime > now }
}
}
private fun removeItem(itemID: Long){
@ -311,6 +323,16 @@ class QueuedDownloadsFragment : Fragment(), GenericDownloadAdapter.OnItemClickLi
deleteDialog.show()
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 -> {
queuedDownloads.checkAll(items)
selectedObjects?.clear()

View file

@ -90,39 +90,28 @@ object FileUtil {
if(it.name.contains(".part-Frag")) return@forEach
//sending to main or SD CARD
if (currentDirectory.exists() && Build.VERSION.SDK_INT >= 26){
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 mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*"
destFile = File(currentDirectory.absolutePath + "/${it.name}")
val dest = Uri.parse(destDir).run {
DocumentsContract.buildDocumentUriUsingTree(
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 dest = Uri.parse(destDir).run {
DocumentsContract.buildDocumentUriUsingTree(
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()
fileList.add(destFile)
}catch (e: java.lang.Exception) {

View file

@ -245,6 +245,15 @@ class NotificationUtil(var context: Context) {
.setArguments(bundle)
.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
.setContentTitle("${context.getString(R.string.failed_download)}: $title")
.setContentText(error)
@ -255,6 +264,7 @@ class NotificationUtil(var context: Context) {
R.drawable.ic_launcher_foreground_large
)
)
.setContentIntent(errorTabPendingIntent)
.setPriority(NotificationCompat.PRIORITY_MAX)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.clearActions()

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdlnis.work
import android.content.Context
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
@ -91,6 +92,16 @@ class DownloadWorker(
tempFileDir.delete()
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)
if (aria2) {
request.addOption("--downloader", "libaria2c.so")
@ -99,6 +110,13 @@ class DownloadWorker(
val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
if (concurrentFragments > 1) request.addOption("-N", concurrentFragments)
}
val retries = sharedPreferences.getString("--retries", "")!!
val fragmentRetries = sharedPreferences.getString("--fragment_retries", "")!!
if(retries.isNotEmpty()) request.addOption("retries", retries)
if(fragmentRetries.isNotEmpty()) request.addOption("fragment-retries", fragmentRetries)
val limitRate = sharedPreferences.getString("limit_rate", "")
if (limitRate != "") request.addOption("-r", limitRate!!)
if(downloadItem.type != DownloadViewModel.Type.command){
@ -215,9 +233,9 @@ class DownloadWorker(
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
request.addOption("-P", tempFileDir.absolutePath)
request.addOption("-P", pathUsed)
}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()){
request.addOption("--split-chapters")
request.addOption("-P", tempFileDir.absolutePath)
request.addOption("-P", pathUsed)
}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)
}.absolutePath
)
request.addOption("-P", tempFileDir.absolutePath)
request.addOption("-P", pathUsed)
}
}
@ -327,33 +345,43 @@ class DownloadWorker(
}
}
}.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)
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
val incognito = sharedPreferences.getBoolean("incognito", false)
if (!incognito) {
val unixtime = System.currentTimeMillis() / 1000
val file = File(finalPaths?.first()!!)
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)
runBlocking {
@ -363,9 +391,7 @@ class DownloadWorker(
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
notificationUtil.createDownloadFinished(
downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else listOf(
finalPaths!!.maxByOrNull { it.length }!!
),
downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths,
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:icon="@drawable/baseline_delete_24"
app:showAsAction="ifRoom" />
<item
android:id="@+id/download"
android:title="@string/download"
android:icon="@drawable/baseline_download_24"
app:showAsAction="ifRoom" />
<item
android:id="@+id/select_all"
android:title="@string/select_all"

View file

@ -58,8 +58,20 @@
</string-array>
<string-array name="audio_formats">
<item>@string/worst_quality</item>
<item>@string/best_quality</item>
<item>worst</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 name="sponsorblock_settings_entries">

View file

@ -279,4 +279,7 @@
<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="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>

View file

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

View file

@ -38,6 +38,15 @@
app:useSimpleSummaryProvider="true"
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
app:key="piped_instance"
app:defaultValue="https://pipedapi.kavin.rocks"