diff --git a/app/build.gradle b/app/build.gradle
index 237f29b1..c59aee13 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -10,13 +10,10 @@ plugins {
def properties = new Properties()
def versionMajor = 1
def versionMinor = 7
-def versionPatch = 5
+def versionPatch = 6
def versionBuild = 0 // bump for dogfood builds, public betas, etc.
-def versionExt = ""
-
-if (versionBuild > 0){
- versionExt = ".${versionBuild}-beta"
-}
+def isBeta = true
+def versionExt = isBeta ? ".${versionBuild}-beta" : ""
android {
namespace 'com.deniscerri.ytdl'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 07e26678..3e875b9c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -19,8 +19,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -136,6 +161,15 @@
+
+
+
+
+
+
+
+
+
@@ -181,6 +215,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -356,6 +417,8 @@
+
+
+
+
)
- @Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused')")
+ @Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','QueuedPaused','Active','ActivePaused', 'Scheduled')")
suspend fun cancelActiveQueued()
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
@@ -229,9 +230,12 @@ interface DownloadDao {
@Query("Select url from downloads where id in (:ids)")
fun getURLsByID(ids: List) : List
- @Query("UPDATE downloads SET downloadStartTime=0 where id in (:list)")
+ @Query("UPDATE downloads SET downloadStartTime=0, status='Queued' where id in (:list)")
suspend fun resetScheduleTimeForItems(list: List)
+ @Query("UPDATE downloads SET status='Queued' where status in ('QueuedPaused', 'ActivePaused')")
+ suspend fun resetPausedItems()
+
@Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)")
suspend fun reQueueDownloadItems(list: List)
diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/ResultDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/ResultDao.kt
index b72fe343..5da9d4cf 100644
--- a/app/src/main/java/com/deniscerri/ytdl/database/dao/ResultDao.kt
+++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/ResultDao.kt
@@ -37,6 +37,9 @@ interface ResultDao {
return insertMultiple(items.filter { getResultByURL(it.url) == null })
}
+ @Query("SELECT * FROM results WHERE id IN (:ids)")
+ fun getAllByIDs(ids: List) : List
+
@Update(onConflict = OnConflictStrategy.REPLACE)
suspend fun update(item: ResultItem)
diff --git a/app/src/main/java/com/deniscerri/ytdl/database/models/AlreadyExistsItem.kt b/app/src/main/java/com/deniscerri/ytdl/database/models/AlreadyExistsItem.kt
new file mode 100644
index 00000000..cc021132
--- /dev/null
+++ b/app/src/main/java/com/deniscerri/ytdl/database/models/AlreadyExistsItem.kt
@@ -0,0 +1,12 @@
+package com.deniscerri.ytdl.database.models
+
+import android.os.Parcelable
+import androidx.room.Entity
+import androidx.room.PrimaryKey
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class AlreadyExistsItem(
+ var downloadItem: DownloadItem,
+ var historyID: Long? = null
+) : Parcelable
diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt
index c5a3408c..ee49a25d 100644
--- a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt
+++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt
@@ -222,6 +222,9 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
+ else {
+ workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
+ }
val workRequest = OneTimeWorkRequestBuilder()
.addTag("download")
diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt
index be0dead5..a69e1319 100644
--- a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt
+++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt
@@ -129,6 +129,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return resultDao.getResultByURL(url)
}
+ fun getAllByIDs(ids: List) : List {
+ return resultDao.getAllByIDs(ids)
+ }
+
suspend fun getResultsFromSource(inputQuery: String, resetResults: Boolean, addToResults: Boolean = true, singleItem: Boolean = false) : ArrayList {
return when(getQueryType(inputQuery)){
SourceType.YOUTUBE_VIDEO -> {
@@ -151,9 +155,9 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
}
- fun getQueryType(inputQuery: String) : SourceType {
+ private fun getQueryType(inputQuery: String) : SourceType {
var type = SourceType.SEARCH_QUERY
- val p = Pattern.compile("(^(https?)://(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
+ val p = Pattern.compile("((^(https?)://)?(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val m = p.matcher(inputQuery)
if (m.find()) {
type = SourceType.YOUTUBE_VIDEO
diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt
index c31f0a0c..13dddb62 100644
--- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt
+++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt
@@ -5,8 +5,11 @@ import android.app.Application
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
+import android.os.Handler
+import android.os.Looper
+import android.os.Parcelable
import android.util.DisplayMetrics
-import android.util.Log
+import android.widget.Toast
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
@@ -41,14 +44,18 @@ import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker
import com.google.gson.Gson
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
+import kotlinx.parcelize.Parcelize
import java.io.File
import java.util.Locale
@@ -56,6 +63,7 @@ import java.util.Locale
class DownloadViewModel(private val application: Application) : AndroidViewModel(application) {
private val dbManager: DBManager
val repository : DownloadRepository
+ private val sharedDownloadViewModel: SharedDownloadViewModel
private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao
private val infoUtil : InfoUtil
@@ -78,6 +86,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val scheduledDownloadsCount : Flow
val pausedDownloadsCount: Flow
+ val alreadyExistsUiState: MutableStateFlow>
+
private var bestVideoFormat : Format
private var bestAudioFormat : Format
private var defaultVideoFormats : MutableList
@@ -97,16 +107,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
private val historyRepository: HistoryRepository
private val resultRepository: ResultRepository
- data class AlreadyExistsUIState(
- var historyItems: MutableList,
- var downloadItems : MutableList
- )
-
- val alreadyExistsUiState: MutableStateFlow = MutableStateFlow(AlreadyExistsUIState(
- historyItems = mutableListOf(),
- downloadItems = mutableListOf()
- ))
-
enum class Type {
auto, audio, video, command
}
@@ -121,6 +121,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dbManager = DBManager.getInstance(application)
dao = dbManager.downloadDao
repository = DownloadRepository(dao)
+ sharedDownloadViewModel = SharedDownloadViewModel(application)
+ alreadyExistsUiState = sharedDownloadViewModel.alreadyExistsUiState
historyRepository = HistoryRepository(dbManager.historyDao)
resultRepository = ResultRepository(dbManager.resultDao, application)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
@@ -168,7 +170,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
audioContainer = sharedPreferences.getString("audio_format", "mp3")
bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
- infoUtil.getGenericAudioFormats(resources).last()
+ infoUtil.getGenericAudioFormats(resources).first()
}else{
Format(
audioFormatIDPreference.first().split("+").first(),
@@ -204,132 +206,24 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return repository.getItemByID(id)
}
+ fun getAllByIDs(ids: List) : List {
+ return repository.getAllItemsByIDs(ids)
+ }
+
fun getHistoryItemById(id: Long) : HistoryItem? {
return historyRepository.getItem(id)
}
fun getDownloadType(t: Type? = null, url: String) : Type {
- var type = t
-
- if (type == null){
- val preferredDownloadType = sharedPreferences.getString("preferred_download_type", Type.auto.toString())
- type = if (sharedPreferences.getBoolean("remember_download_type", false)){
- Type.valueOf(sharedPreferences.getString("last_used_download_type",
- preferredDownloadType)!!)
- }else{
- Type.valueOf(preferredDownloadType!!)
- }
- }
-
- return when(type){
- Type.auto -> {
- if (urlsForAudioType.any { url.contains(it) }){
- Type.audio
- }else{
- Type.video
- }
- }
-
- else -> type
- }
+ return sharedDownloadViewModel.getDownloadType(t, url)
}
fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem {
- val resultItem = result ?: createEmptyResultItem(url)
-
- val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
- val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
- val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
- val addChapters = sharedPreferences.getBoolean("add_chapters", false)
- val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
- val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
- val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false)
-
- var type = getDownloadType(givenType, resultItem.url)
- if(type == Type.command && commandTemplateDao.getTotalNumber() == 0) type = Type.video
-
- val customFileNameTemplate = when(type) {
- Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)s - %(title)s")
- Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s")
- else -> ""
- }
-
- val downloadPath = when(type){
- Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())
- Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())
- else -> sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
- }
-
- val container = when(type){
- Type.audio -> sharedPreferences.getString("audio_format", "")
- else -> sharedPreferences.getString("video_format", "")
- }
-
-
- val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
-
- val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!))
-
-
- val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats)
-
- val videoPreferences = VideoPreferences(
- embedSubs,
- addChapters, false,
- ArrayList(sponsorblock),
- saveSubs,
- saveAutoSubs,
- audioFormatIDs = preferredAudioFormats
- )
-
- val extraCommands = when(type){
- Type.audio -> extraCommandsForAudio
- Type.video -> extraCommandsForVideo
- else -> ""
- }
-
- return DownloadItem(0,
- resultItem.url,
- resultItem.title,
- resultItem.author,
- resultItem.thumb,
- resultItem.duration,
- type,
- getFormat(resultItem.formats, type),
- container!!,
- "",
- resultItem.formats,
- downloadPath!!, resultItem.website,
- "",
- resultItem.playlistTitle,
- audioPreferences,
- videoPreferences,
- extraCommands,
- customFileNameTemplate!!,
- saveThumb,
- DownloadRepository.Status.Queued.toString(), 0, null, playlistURL = resultItem.playlistURL, playlistIndex = resultItem.playlistIndex
- )
-
+ return sharedDownloadViewModel.createDownloadItemFromResult(result, url, givenType)
}
fun createResultItemFromDownload(downloadItem: DownloadItem) : ResultItem {
- return ResultItem(
- 0,
- downloadItem.url,
- downloadItem.title,
- downloadItem.author,
- downloadItem.duration,
- downloadItem.thumb,
- downloadItem.website,
- downloadItem.playlistTitle,
- downloadItem.allFormats,
- "",
- arrayListOf(),
- downloadItem.playlistURL,
- downloadItem.playlistIndex,
- System.currentTimeMillis()
- )
-
+ return sharedDownloadViewModel.createResultItemFromDownload(downloadItem)
}
fun createResultItemFromHistory(downloadItem: HistoryItem) : ResultItem {
@@ -353,23 +247,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun createEmptyResultItem(url: String) : ResultItem {
- return ResultItem(
- 0,
- url,
- "",
- "",
- "",
- "",
- "",
- "",
- arrayListOf(),
- "",
- arrayListOf(),
- "",
- null,
- System.currentTimeMillis()
- )
-
+ return sharedDownloadViewModel.createEmptyResultItem(url)
}
fun switchDownloadType(list: List, type: Type) : List{
@@ -471,166 +349,140 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
- val requirements: MutableList<(Format) -> Int> = mutableListOf()
- requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) 10 else 0}
-
- sharedPreferences.getString("audio_language", "")?.apply {
- if (this.isNotBlank()){
- requirements.add { it: Format -> if (it.lang?.contains(this) == true) 3 else 0 }
- }
- }
-
- requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) 2 else 0}
- requirements.add {it: Format -> if (it.container == audioContainer) 1 else 0 }
- return requirements
+ return sharedDownloadViewModel.getPreferredVideoRequirements()
}
//requirement and importance
@SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
- val requirements: MutableList<(Format) -> Int> = mutableListOf()
- //format id
- requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) 20 else 0 }
- //resolutions
- application.getStringArray(R.array.video_formats_values)
- .filter { it.contains("_") }
- .map{ it.split("_")[0].dropLast(1)
- }.toMutableList().apply {
- when(videoQualityPreference) {
- "worst" -> {
- requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (15) else 0 }
- }
- "best" -> {
- requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (15) else 0 }
- }
- else -> {
- val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) }
- val preference = this[preferenceIndex]
- for(i in 0..preferenceIndex){
- removeAt(0)
- }
- add(0, preference)
- forEachIndexed { index, res ->
- requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (15 - index - 1) else 0 }
- }
- }
- }
-
- }
- requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) 5 else 0 }
- requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") 1 else 0 }
- requirements.add { it: Format ->
- if (videoContainer == "mp4")
- if (it.container.equals("mpeg_4", true)) 1 else 0
- else
- if (it.container.equals(videoContainer, true)) 1 else 0
- }
- return requirements
+ return sharedDownloadViewModel.getPreferredVideoRequirements()
}
fun getFormat(formats: List, type: Type) : Format {
- when(type) {
- Type.audio -> {
- return cloneFormat (
- try {
- val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
- val requirements = getPreferredAudioRequirements()
- theFormats.maxByOrNull { f -> requirements.sumOf{ req -> req(f)} } ?: throw Exception()
- }catch (e: Exception){
- bestAudioFormat
- }
- )
-
- }
- Type.video -> {
- return cloneFormat(
- try {
- val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty {
- defaultVideoFormats.sortedByDescending { it.filesize }
- }
- when (videoQualityPreference) {
- "worst" -> {
- theFormats.last()
- }
- else /*best*/ -> {
- val requirements = getPreferredVideoRequirements()
- theFormats.run {
- if (sharedPreferences.getBoolean("prefer_smaller_formats", false)){
- sortedBy { it.filesize }.maxByOrNull { f -> requirements.sumOf { req -> req(f) } } ?: throw Exception()
- }else{
- sortedByDescending { it.filesize }.maxByOrNull { f ->
- val summ = requirements.sumOf { req -> req(f) }
- summ
- } ?: throw Exception()
- }
- }
- }
- }
- }catch (e: Exception){
- bestVideoFormat
- }
- )
- }
- else -> {
- val lastUsedCommandTemplate = sharedPreferences.getString("lastCommandTemplateUsed", "")!!
- val c = if (lastUsedCommandTemplate.isBlank()){
- commandTemplateDao.getFirst() ?: CommandTemplate(0,"","", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
- }else{
- commandTemplateDao.getTemplateByContent(lastUsedCommandTemplate) ?: CommandTemplate(0, "", lastUsedCommandTemplate, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
- }
- return generateCommandFormat(c)
- }
- }
+ return sharedDownloadViewModel.getFormat(formats, type)
}
fun getPreferredAudioFormats(formats: List) : ArrayList{
- val preferredAudioFormats = arrayListOf()
- for (f in formats.sortedBy { it.format_id }){
- val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
- if (fId != null) {
- if (fId.split("+").all { formats.map { f-> f.format_id }.contains(it) }){
- preferredAudioFormats.addAll(fId.split("+"))
- break
- }
- }
- }
- if (preferredAudioFormats.isEmpty()){
- val audioF = getFormat(formats, Type.audio)
- if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
- preferredAudioFormats.add(audioF.format_id)
- }
- }
- return preferredAudioFormats
+ return sharedDownloadViewModel.getPreferredAudioFormats(formats)
}
fun generateCommandFormat(c: CommandTemplate) : Format {
- return Format(
- c.title,
- c.id.toString(),
- "",
- "",
- "",
- 0,
- c.content.replace("\n", " ")
- )
+ return sharedDownloadViewModel.generateCommandFormat(c)
}
+ data class ProcessingItemsJob(
+ var jobID: Long = 0,
+ var job: Job? = null,
+ var itemType: String,
+ var itemIDs: List
+ )
-
- fun getLatestCommandTemplateAsFormat() : Format {
- val t = commandTemplateDao.getFirst()!!
- return Format(t.title, "", "", "", "", 0, t.content)
- }
-
- fun turnResultItemsToDownloadItems(items: List) : List {
- val list : MutableList = mutableListOf()
- items.forEach {
- val preferredType = getDownloadType(url = it!!.url).toString()
- list.add(createDownloadItemFromResult(result = it, givenType = Type.valueOf(
- preferredType
- )))
+ val loadingProcessingItems: MutableStateFlow = MutableStateFlow(false)
+ var loadingProcessingDownloadsJobs: MutableStateFlow> = MutableStateFlow(listOf())
+ private fun cancelAllLoadingProcessingDownloads() {
+ loadingProcessingDownloadsJobs.value.onEach {
+ it.job?.cancel(CancellationException())
}
- return list
+ }
+
+ fun getLoadingProcessingDownloadJob(jobID: Long) : ProcessingItemsJob? {
+ return loadingProcessingDownloadsJobs.value.firstOrNull { it.jobID == jobID }
+ }
+
+ fun cancelLoadingProcessingDownloads(jobID: Long) {
+ loadingProcessingDownloadsJobs.value.firstOrNull {it.jobID == jobID }?.apply {
+ this.job?.cancel(CancellationException())
+ }
+ }
+
+ fun turnDownloadItemsToProcessingDownloads(itemIDs: List) : Long {
+ val jobID = System.currentTimeMillis()
+ val job = viewModelScope.launch(Dispatchers.IO) {
+ val insertedIDs = mutableListOf()
+ try {
+ loadingProcessingItems.emit(true)
+
+ itemIDs.chunked(100).forEach { ids ->
+ val items = repository.getAllItemsByIDs(ids)
+ items.apply {
+ if (!isActive) {
+ throw CancellationException()
+ }
+ this.forEach {
+ it.id = 0
+ it.status = DownloadRepository.Status.Processing.toString()
+ insertedIDs.add(repository.insert(it))
+ }
+ }
+ }
+
+ loadingProcessingItems.emit(false)
+ } catch (e: Exception) {
+ repository.deleteAllWithIDs(insertedIDs)
+ loadingProcessingItems.emit(false)
+ }
+ }
+
+ viewModelScope.launch(Dispatchers.IO) {
+ val currentJobs = loadingProcessingDownloadsJobs.value.toMutableList()
+ currentJobs.add(
+ ProcessingItemsJob(jobID, job, DownloadItem::class.java.toString(), itemIDs)
+ )
+ loadingProcessingDownloadsJobs.emit(currentJobs)
+ }
+
+ return jobID
+ }
+
+
+ fun turnResultItemsToProcessingDownloads(itemIds: List, downloadNow: Boolean = false) : Long {
+ val jobID = System.currentTimeMillis()
+ val job = viewModelScope.launch(Dispatchers.IO) {
+ val insertedIds = mutableListOf()
+ try {
+ loadingProcessingItems.emit(true)
+
+ itemIds.chunked(100).forEach { ids ->
+ val items = resultRepository.getAllByIDs(ids)
+ val downloadItems = items.map {
+ val preferredType = getDownloadType(url = it.url).toString()
+ val tmp = createDownloadItemFromResult(result = it, givenType = Type.valueOf(
+ preferredType
+ ))
+ tmp.status = DownloadRepository.Status.Processing.toString()
+ tmp
+ }
+ if (!isActive) {
+ throw CancellationException()
+ }
+
+ if (downloadNow) {
+ downloadItems.forEach {
+ it.status = DownloadRepository.Status.Queued.toString()
+ }
+ queueDownloads(downloadItems)
+ }else{
+ downloadItems.forEach {
+ insertedIds.add(repository.insert(it))
+ }
+ }
+ }
+ loadingProcessingItems.emit(false)
+ }catch (e: Exception) {
+ repository.deleteAllWithIDs(insertedIds)
+ loadingProcessingItems.emit(false)
+ }
+ }
+
+ viewModelScope.launch(Dispatchers.IO) {
+ val currentJobs = loadingProcessingDownloadsJobs.value.toMutableList()
+ currentJobs.add(
+ ProcessingItemsJob(jobID, job, ResultItem::class.java.toString(), itemIds)
+ )
+ loadingProcessingDownloadsJobs.emit(currentJobs)
+ }
+
+ return jobID
}
fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){
@@ -688,6 +540,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
+ cancelAllLoadingProcessingDownloads()
repository.cancelActiveQueued()
}
@@ -706,11 +559,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return dao.getSavedDownloadsList()
}
- private fun cloneFormat(item: Format) : Format {
- val string = Gson().toJson(item, Format::class.java)
- return Gson().fromJson(string, Format::class.java)
- }
-
fun getActiveDownloads() : List{
return repository.getActiveDownloads()
}
@@ -725,6 +573,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
suspend fun resetScheduleTimeForItemsAndStartDownload(items: List) = CoroutineScope(Dispatchers.IO).launch {
dbManager.downloadDao.resetScheduleTimeForItems(items)
+ dbManager.downloadDao.resetPausedItems()
repository.startDownloadWorker(emptyList(), application)
}
@@ -789,173 +638,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateDownloadID(-current, id)
}
- suspend fun reQueueDownloadItems(items: List) = CoroutineScope(Dispatchers.IO).launch {
+ fun reQueueDownloadItems(items: List) = viewModelScope.launch(Dispatchers.IO) {
dbManager.downloadDao.reQueueDownloadItems(items)
repository.startDownloadWorker(emptyList(), application)
}
- suspend fun queueDownloads(items: List, ign : Boolean = false) : Pair, List> {
- val context = App.instance
- val alarmScheduler = AlarmScheduler(context)
- val activeAndQueuedDownloads = withContext(Dispatchers.IO){
- repository.getActiveAndQueuedDownloads()
- }
- val queuedItems = mutableListOf()
- val existingDownloads = mutableListOf()
- val existingHistory = mutableListOf()
-
- //if scheduler is on
- val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
- if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
- alarmScheduler.schedule()
- }
-
- if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
- items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
- }
-
- val downloadArchive = runCatching { File(FileUtil.getDownloadArchivePath(application)).useLines { it.toList() } }.getOrElse { listOf() }
- .map { it.split(" ")[1] }
- items.forEach {
- if (! listOf(DownloadRepository.Status.ActivePaused, DownloadRepository.Status.Scheduled).toListString().contains(it.status))
- it.status = DownloadRepository.Status.Queued.toString()
- var alreadyExists = false
-
- val checkDuplicate = sharedPreferences.getString("prevent_duplicate_downloads", "")!!
- if (checkDuplicate.isNotEmpty() && !ign){
- when(checkDuplicate){
- "download_archive" -> {
- if (downloadArchive.any { d -> it.url.contains(d) }){
- alreadyExists = true
- if (it.id == 0L) {
- it.status = DownloadRepository.Status.Processing.toString()
- val id = runBlocking {
- repository.insert(it)
- }
- it.id = id
- }
- existingDownloads.add(it.id)
- }
- }
- "url_type" -> {
- val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
- d.id = 0
- d.logID = null
- d.customFileNameTemplate = it.customFileNameTemplate
- d.status = DownloadRepository.Status.Queued.toString()
- d.toString() == it.toString()
- }
-
- if (existingDownload != null){
- it.status = DownloadRepository.Status.Processing.toString()
- val id = runBlocking {
- repository.insert(it)
- }
- alreadyExists = true
- existingDownloads.add(id)
- }else{
- //check if downloaded and file exists
- val history = withContext(Dispatchers.IO){
- historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
- }
-
- val existingHistoryItem = history.firstOrNull {
- h -> h.type == it.type
- }
-
- if (existingHistoryItem != null){
- alreadyExists = true
- existingHistory.add(existingHistoryItem.id)
- }
- }
- }
- "config" -> {
- val currentCommand = infoUtil.buildYoutubeDLRequest(it)
- val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
- val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
- d.id = 0
- d.logID = null
- d.customFileNameTemplate = it.customFileNameTemplate
- d.status = DownloadRepository.Status.Queued.toString()
- d.toString() == it.toString()
- }
-
- if (existingDownload != null){
- it.status = DownloadRepository.Status.Processing.toString()
- val id = runBlocking {
- repository.insert(it)
- }
- alreadyExists = true
- existingDownloads.add(id)
- }else{
- //check if downloaded and file exists
- val history = withContext(Dispatchers.IO){
- historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
- }
-
- val existingHistoryItem = history.firstOrNull {
- h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
- }
-
- if (existingHistoryItem != null){
- alreadyExists = true
- existingHistory.add(existingHistoryItem.id)
- }
- }
- }
- }
- }
-
- if (!alreadyExists){
- if (it.id == 0L){
- val id = runBlocking {
- repository.insert(it)
- }
- it.id = id
- }else if (listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.Scheduled).toListString().contains(it.status)){
- withContext(Dispatchers.IO){
- repository.update(it)
- }
- }
-
- queuedItems.add(it)
- }
-
- }
-
- if (existingDownloads.isNotEmpty() || existingHistory.isNotEmpty()){
- alreadyExistsUiState.update { u -> u.copy(existingHistory, existingDownloads) }
- }
-
- if (queuedItems.isNotEmpty()){
- if (!useScheduler || alarmScheduler.isDuringTheScheduledTime()){
- repository.startDownloadWorker(queuedItems, context)
-
- if(!useScheduler){
- queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
- CoroutineScope(Dispatchers.IO).launch {
- runCatching {
- resultRepository.updateDownloadItem(it)?.apply {
- repository.updateWithoutUpsert(this)
- }
- }
- }
- }
- }else{
- queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
- CoroutineScope(Dispatchers.IO).launch {
- runCatching {
- resultRepository.updateDownloadItem(it)?.apply {
- repository.updateWithoutUpsert(this)
- }
- }
- }
- }
- }
- }
- }
-
- return Pair(existingDownloads, existingHistory)
+ suspend fun queueDownloads(items: List, ign : Boolean = false) : List {
+ val ids = sharedDownloadViewModel.queueDownloads(items, ign)
+ return ids
}
fun getQueuedCollectedFileSize() : Long {
@@ -978,19 +668,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateProcessingtoSavedStatus()
}
- suspend fun downloadProcessingDownloads(timeInMillis: Long = 0){
- repository.getProcessingDownloads().apply {
- if (timeInMillis > 0){
- this.forEach {
- it.status = DownloadRepository.Status.Scheduled.toString()
- it.downloadStartTime = timeInMillis
- }
- }
-
- queueDownloads(this)
- }
- }
-
suspend fun updateProcessingFormat(selectedFormats: List): List {
val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i ->
@@ -1098,16 +775,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val first = dao.getFirstProcessingDownload()
}
- suspend fun addDownloadsToProcessing(ids: List) {
- repository.deleteProcessing()
- val items = repository.getAllItemsByIDs(ids)
- items.forEach {
- it.id = 0
- it.status = DownloadRepository.Status.Processing.toString()
- insert(it)
- }
- }
-
fun getURLsByStatus(list: List) : List {
return dao.getURLsByStatus(list.map { it.toString() })
}
diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt
index 1738ba14..681a6e77 100644
--- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt
+++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ObserveSourcesViewModel.kt
@@ -10,6 +10,7 @@ import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.PeriodicWorkRequest
import androidx.work.PeriodicWorkRequestBuilder
@@ -124,7 +125,15 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
}
//schedule for next time
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
+ val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
+
val workConstraints = Constraints.Builder()
+ if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
+ else {
+ workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
+ }
+
val workRequest = OneTimeWorkRequestBuilder()
.addTag("observeSources")
.addTag(it.id.toString())
diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt
new file mode 100644
index 00000000..533ab203
--- /dev/null
+++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt
@@ -0,0 +1,616 @@
+package com.deniscerri.ytdl.database.viewmodel
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.SharedPreferences
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.os.Handler
+import android.os.Looper
+import android.os.Parcelable
+import android.util.DisplayMetrics
+import android.widget.Toast
+import androidx.preference.PreferenceManager
+import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
+import com.deniscerri.ytdl.App
+import com.deniscerri.ytdl.R
+import com.deniscerri.ytdl.database.DBManager
+import com.deniscerri.ytdl.database.dao.CommandTemplateDao
+import com.deniscerri.ytdl.database.dao.DownloadDao
+import com.deniscerri.ytdl.database.models.AudioPreferences
+import com.deniscerri.ytdl.database.models.CommandTemplate
+import com.deniscerri.ytdl.database.models.DownloadItem
+import com.deniscerri.ytdl.database.models.Format
+import com.deniscerri.ytdl.database.models.ResultItem
+import com.deniscerri.ytdl.database.models.VideoPreferences
+import com.deniscerri.ytdl.database.repository.DownloadRepository
+import com.deniscerri.ytdl.database.repository.HistoryRepository
+import com.deniscerri.ytdl.database.repository.ResultRepository
+import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
+import com.deniscerri.ytdl.util.Extensions.toListString
+import com.deniscerri.ytdl.util.FileUtil
+import com.deniscerri.ytdl.util.InfoUtil
+import com.deniscerri.ytdl.work.AlarmScheduler
+import com.google.gson.Gson
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.withContext
+import kotlinx.parcelize.Parcelize
+import java.io.File
+import java.util.Locale
+
+
+class SharedDownloadViewModel(private val context: Context) {
+ private val dbManager: DBManager = DBManager.getInstance(context)
+ val repository : DownloadRepository
+ private val sharedPreferences: SharedPreferences
+ private val commandTemplateDao: CommandTemplateDao
+ private val infoUtil : InfoUtil
+
+ private var bestVideoFormat : Format
+ private var bestAudioFormat : Format
+ private var defaultVideoFormats : MutableList
+
+ private val videoQualityPreference: String
+ private val formatIDPreference: List
+ private val audioFormatIDPreference: List
+ private val resources : Resources
+ private var extraCommandsForAudio: String = ""
+ private var extraCommandsForVideo: String = ""
+
+ private var audioContainer: String?
+ private var videoContainer: String?
+ private var videoCodec: String?
+ private var audioCodec: String?
+ private val dao: DownloadDao
+ private val historyRepository: HistoryRepository
+ private val resultRepository: ResultRepository
+
+ @Parcelize
+ data class AlreadyExistsIDs(
+ var downloadItemID: Long,
+ var historyItemID : Long?
+ ) : Parcelable
+
+ val alreadyExistsUiState: MutableStateFlow> = MutableStateFlow(
+ mutableListOf()
+ )
+
+ private val urlsForAudioType = listOf(
+ "music",
+ "audio",
+ "soundcloud"
+ )
+
+ init {
+ dao = dbManager.downloadDao
+ repository = DownloadRepository(dao)
+ historyRepository = HistoryRepository(dbManager.historyDao)
+ resultRepository = ResultRepository(dbManager.resultDao, context)
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+ commandTemplateDao = DBManager.getInstance(context).commandTemplateDao
+ infoUtil = InfoUtil(context)
+
+ CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
+ if (sharedPreferences.getBoolean("use_extra_commands", false)){
+ extraCommandsForAudio = commandTemplateDao.getAllTemplatesAsExtraCommandsForAudio().joinToString(" ")
+ extraCommandsForVideo = commandTemplateDao.getAllTemplatesAsExtraCommandsForVideo().joinToString(" ")
+ }
+ }
+
+ videoQualityPreference = sharedPreferences.getString("video_quality", "best").toString()
+ formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
+ audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
+
+ val confTmp = Configuration(context.resources.configuration)
+ confTmp.setLocale(Locale(sharedPreferences.getString("app_language", "en")!!))
+ val metrics = DisplayMetrics()
+ resources = Resources(context.assets, metrics, confTmp)
+
+
+ videoContainer = sharedPreferences.getString("video_format", "Default")
+ defaultVideoFormats = infoUtil.getGenericVideoFormats(resources)
+ bestVideoFormat = defaultVideoFormats.first()
+
+ audioContainer = sharedPreferences.getString("audio_format", "mp3")
+ bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
+ infoUtil.getGenericAudioFormats(resources).first()
+ }else{
+ Format(
+ audioFormatIDPreference.first().split("+").first(),
+ audioContainer!!,
+ "",
+ "",
+ "",
+ 0,
+ audioFormatIDPreference.first().split("+").first()
+ )
+ }
+
+ videoCodec = sharedPreferences.getString("video_codec", "")
+ audioCodec = sharedPreferences.getString("audio_codec", "")
+ }
+
+
+ fun getDownloadType(t: Type? = null, url: String) : Type {
+ var type = t
+
+ if (type == null){
+ val preferredDownloadType = sharedPreferences.getString("preferred_download_type", Type.auto.toString())
+ type = if (sharedPreferences.getBoolean("remember_download_type", false)){
+ Type.valueOf(sharedPreferences.getString("last_used_download_type",
+ preferredDownloadType)!!)
+ }else{
+ Type.valueOf(preferredDownloadType!!)
+ }
+ }
+
+ return when(type){
+ Type.auto -> {
+ if (urlsForAudioType.any { url.contains(it) }){
+ Type.audio
+ }else{
+ Type.video
+ }
+ }
+
+ else -> type
+ }
+ }
+
+ fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem {
+ val resultItem = result ?: createEmptyResultItem(url)
+
+ val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
+ val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
+ val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
+ val addChapters = sharedPreferences.getBoolean("add_chapters", false)
+ val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
+ val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
+ val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false)
+
+ var type = getDownloadType(givenType, resultItem.url)
+ if(type == Type.command && commandTemplateDao.getTotalNumber() == 0) type = Type.video
+
+ val customFileNameTemplate = when(type) {
+ Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)s - %(title)s")
+ Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s")
+ else -> ""
+ }
+
+ val downloadPath = when(type){
+ Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())
+ Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())
+ else -> sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
+ }
+
+ val container = when(type){
+ Type.audio -> sharedPreferences.getString("audio_format", "")
+ else -> sharedPreferences.getString("video_format", "")
+ }
+
+
+ val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
+
+ val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!))
+
+
+ val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats)
+
+ val videoPreferences = VideoPreferences(
+ embedSubs,
+ addChapters, false,
+ ArrayList(sponsorblock),
+ saveSubs,
+ saveAutoSubs,
+ audioFormatIDs = preferredAudioFormats
+ )
+
+ val extraCommands = when(type){
+ Type.audio -> extraCommandsForAudio
+ Type.video -> extraCommandsForVideo
+ else -> ""
+ }
+
+ return DownloadItem(0,
+ resultItem.url,
+ resultItem.title,
+ resultItem.author,
+ resultItem.thumb,
+ resultItem.duration,
+ type,
+ getFormat(resultItem.formats, type),
+ container!!,
+ "",
+ resultItem.formats,
+ downloadPath!!, resultItem.website,
+ "",
+ resultItem.playlistTitle,
+ audioPreferences,
+ videoPreferences,
+ extraCommands,
+ customFileNameTemplate!!,
+ saveThumb,
+ DownloadRepository.Status.Queued.toString(), 0, null, playlistURL = resultItem.playlistURL, playlistIndex = resultItem.playlistIndex
+ )
+
+ }
+
+ fun createResultItemFromDownload(downloadItem: DownloadItem) : ResultItem {
+ return ResultItem(
+ 0,
+ downloadItem.url,
+ downloadItem.title,
+ downloadItem.author,
+ downloadItem.duration,
+ downloadItem.thumb,
+ downloadItem.website,
+ downloadItem.playlistTitle,
+ downloadItem.allFormats,
+ "",
+ arrayListOf(),
+ downloadItem.playlistURL,
+ downloadItem.playlistIndex,
+ System.currentTimeMillis()
+ )
+
+ }
+
+ fun createEmptyResultItem(url: String) : ResultItem {
+ return ResultItem(
+ 0,
+ url,
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ arrayListOf(),
+ "",
+ arrayListOf(),
+ "",
+ null,
+ System.currentTimeMillis()
+ )
+
+ }
+
+ private fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
+ val requirements: MutableList<(Format) -> Int> = mutableListOf()
+ requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) 10 else 0}
+
+ sharedPreferences.getString("audio_language", "")?.apply {
+ if (this.isNotBlank()){
+ requirements.add { it: Format -> if (it.lang?.contains(this) == true) 3 else 0 }
+ }
+ }
+
+ requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) 2 else 0}
+ requirements.add {it: Format -> if (it.container == audioContainer) 1 else 0 }
+ return requirements
+ }
+
+ //requirement and importance
+ @SuppressLint("RestrictedApi")
+ fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
+ val requirements: MutableList<(Format) -> Int> = mutableListOf()
+ //format id
+ requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) 20 else 0 }
+ //resolutions
+ context.getStringArray(R.array.video_formats_values)
+ .filter { it.contains("_") }
+ .map{ it.split("_")[0].dropLast(1)
+ }.toMutableList().apply {
+ when(videoQualityPreference) {
+ "worst" -> {
+ requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (15) else 0 }
+ }
+ "best" -> {
+ requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (15) else 0 }
+ }
+ else -> {
+ val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) }
+ val preference = this[preferenceIndex]
+ for(i in 0..preferenceIndex){
+ removeAt(0)
+ }
+ add(0, preference)
+ forEachIndexed { index, res ->
+ requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (15 - index - 1) else 0 }
+ }
+ }
+ }
+
+ }
+ requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) 5 else 0 }
+ requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") 1 else 0 }
+ requirements.add { it: Format ->
+ if (videoContainer == "mp4")
+ if (it.container.equals("mpeg_4", true)) 1 else 0
+ else
+ if (it.container.equals(videoContainer, true)) 1 else 0
+ }
+ return requirements
+ }
+
+ fun getFormat(formats: List, type: Type) : Format {
+ when(type) {
+ Type.audio -> {
+ return cloneFormat (
+ try {
+ val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
+ val requirements = getPreferredAudioRequirements()
+ theFormats.maxByOrNull { f -> requirements.sumOf{ req -> req(f)} } ?: throw Exception()
+ }catch (e: Exception){
+ bestAudioFormat
+ }
+ )
+
+ }
+ Type.video -> {
+ return cloneFormat(
+ try {
+ val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty {
+ defaultVideoFormats.sortedByDescending { it.filesize }
+ }
+ when (videoQualityPreference) {
+ "worst" -> {
+ theFormats.last()
+ }
+ else /*best*/ -> {
+ val requirements = getPreferredVideoRequirements()
+ theFormats.run {
+ if (sharedPreferences.getBoolean("prefer_smaller_formats", false)){
+ sortedBy { it.filesize }.maxByOrNull { f -> requirements.sumOf { req -> req(f) } } ?: throw Exception()
+ }else{
+ sortedByDescending { it.filesize }.maxByOrNull { f ->
+ val summ = requirements.sumOf { req -> req(f) }
+ summ
+ } ?: throw Exception()
+ }
+ }
+ }
+ }
+ }catch (e: Exception){
+ bestVideoFormat
+ }
+ )
+ }
+ else -> {
+ val lastUsedCommandTemplate = sharedPreferences.getString("lastCommandTemplateUsed", "")!!
+ val c = if (lastUsedCommandTemplate.isBlank()){
+ commandTemplateDao.getFirst() ?: CommandTemplate(0,"","", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
+ }else{
+ commandTemplateDao.getTemplateByContent(lastUsedCommandTemplate) ?: CommandTemplate(0, "", lastUsedCommandTemplate, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
+ }
+ return generateCommandFormat(c)
+ }
+ }
+ }
+
+ fun getPreferredAudioFormats(formats: List) : ArrayList{
+ val preferredAudioFormats = arrayListOf()
+ for (f in formats.sortedBy { it.format_id }){
+ val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
+ if (fId != null) {
+ if (fId.split("+").all { formats.map { f-> f.format_id }.contains(it) }){
+ preferredAudioFormats.addAll(fId.split("+"))
+ break
+ }
+ }
+ }
+ if (preferredAudioFormats.isEmpty()){
+ val audioF = getFormat(formats, Type.audio)
+ if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
+ preferredAudioFormats.add(audioF.format_id)
+ }
+ }
+ return preferredAudioFormats
+ }
+
+ fun generateCommandFormat(c: CommandTemplate) : Format {
+ return Format(
+ c.title,
+ c.id.toString(),
+ "",
+ "",
+ "",
+ 0,
+ c.content.replace("\n", " ")
+ )
+ }
+
+ private fun cloneFormat(item: Format) : Format {
+ val string = Gson().toJson(item, Format::class.java)
+ return Gson().fromJson(string, Format::class.java)
+ }
+
+ suspend fun queueDownloads(items: List, ign : Boolean = false) : List {
+ val context = App.instance
+ val alarmScheduler = AlarmScheduler(context)
+ val queuedItems = mutableListOf()
+ //download id, history item id
+ //history item id if the existing item is already downloaded
+ val existingItemIDs = mutableListOf()
+
+ if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
+ items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
+ }
+
+ val downloadArchive = runCatching { File(FileUtil.getDownloadArchivePath(context)).useLines { it.toList() } }.getOrElse { listOf() }
+ .map { it.split(" ")[1] }
+ items.forEach {
+ if (! listOf(DownloadRepository.Status.ActivePaused, DownloadRepository.Status.Scheduled).toListString().contains(it.status))
+ it.status = DownloadRepository.Status.Queued.toString()
+ var alreadyExists = false
+
+ val checkDuplicate = sharedPreferences.getString("prevent_duplicate_downloads", "")!!
+ if (checkDuplicate.isNotEmpty() && !ign){
+ when(checkDuplicate){
+ "download_archive" -> {
+ if (downloadArchive.any { d -> it.url.contains(d) }){
+ alreadyExists = true
+ if (it.id == 0L) {
+ it.status = DownloadRepository.Status.Processing.toString()
+ val id = runBlocking {
+ repository.insert(it)
+ }
+ it.id = id
+ }
+ existingItemIDs.add(AlreadyExistsIDs(it.id, null))
+ }
+ }
+ "url_type" -> {
+ val activeAndQueuedDownloads = withContext(Dispatchers.IO){
+ repository.getActiveAndQueuedDownloads()
+ }
+ val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
+ d.id = 0
+ d.logID = null
+ d.customFileNameTemplate = it.customFileNameTemplate
+ d.status = DownloadRepository.Status.Queued.toString()
+ d.toString() == it.toString()
+ }
+
+ if (existingDownload != null){
+ it.status = DownloadRepository.Status.Processing.toString()
+ val id = runBlocking {
+ repository.insert(it)
+ }
+ it.id = id
+ alreadyExists = true
+ existingItemIDs.add(AlreadyExistsIDs(it.id, null))
+ }else{
+ //check if downloaded and file exists
+ val history = withContext(Dispatchers.IO){
+ historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
+ }
+
+ val existingHistoryItem = history.firstOrNull {
+ h -> h.type == it.type
+ }
+
+ if (existingHistoryItem != null){
+ alreadyExists = true
+ it.status = DownloadRepository.Status.Processing.toString()
+ val id = runBlocking {
+ repository.insert(it)
+ }
+ existingItemIDs.add(AlreadyExistsIDs(id, existingHistoryItem.id))
+ }
+ }
+ }
+ "config" -> {
+ val currentCommand = infoUtil.buildYoutubeDLRequest(it)
+ val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
+ val activeAndQueuedDownloads = withContext(Dispatchers.IO){
+ repository.getActiveAndQueuedDownloads()
+ }
+ val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
+ d.id = 0
+ d.logID = null
+ d.customFileNameTemplate = it.customFileNameTemplate
+ d.status = DownloadRepository.Status.Queued.toString()
+ d.toString() == it.toString()
+ }
+
+ if (existingDownload != null){
+ it.status = DownloadRepository.Status.Processing.toString()
+ val id = runBlocking {
+ repository.insert(it)
+ }
+ alreadyExists = true
+ existingItemIDs.add(AlreadyExistsIDs(id, null))
+ }else{
+ //check if downloaded and file exists
+ val history = withContext(Dispatchers.IO){
+ historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
+ }
+
+ val existingHistoryItem = history.firstOrNull {
+ h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
+ }
+
+ if (existingHistoryItem != null){
+ alreadyExists = true
+ it.status = DownloadRepository.Status.Processing.toString()
+ val id = runBlocking {
+ repository.insert(it)
+ }
+ existingItemIDs.add(AlreadyExistsIDs(id, existingHistoryItem.id))
+ }
+ }
+ }
+ }
+ }
+
+ if (!alreadyExists){
+ if (it.id == 0L){
+ val id = runBlocking {
+ repository.insert(it)
+ }
+ it.id = id
+ }else if (listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.Scheduled).toListString().contains(it.status)){
+ withContext(Dispatchers.IO){
+ repository.update(it)
+ }
+ }
+
+ queuedItems.add(it)
+ }
+
+ }
+
+ if (existingItemIDs.isNotEmpty()){
+ alreadyExistsUiState.value = existingItemIDs.toList()
+ }
+
+
+ //if scheduler is on
+ val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
+ if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
+ if (alarmScheduler.canSchedule()){
+ alarmScheduler.schedule()
+ }else{
+ sharedPreferences.edit().putBoolean("use_scheduler", false).apply()
+ Handler(Looper.getMainLooper()).post {
+ Toast.makeText(context, context.getString(R.string.enable_alarm_permission), Toast.LENGTH_LONG).show()
+ }
+ }
+ }else{
+ if (queuedItems.isNotEmpty()){
+ repository.startDownloadWorker(queuedItems, context)
+
+ if(!useScheduler){
+ queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
+ CoroutineScope(Dispatchers.IO).launch {
+ runCatching {
+ resultRepository.updateDownloadItem(it)?.apply {
+ repository.updateWithoutUpsert(this)
+ }
+ }
+ }
+ }
+ }else{
+ queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
+ CoroutineScope(Dispatchers.IO).launch {
+ runCatching {
+ resultRepository.updateDownloadItem(it)?.apply {
+ repository.updateWithoutUpsert(this)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return existingItemIDs
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/deniscerri/ytdl/receiver/CancelScheduleAlarmReceiver.kt b/app/src/main/java/com/deniscerri/ytdl/receiver/CancelScheduleAlarmReceiver.kt
new file mode 100644
index 00000000..590bf6eb
--- /dev/null
+++ b/app/src/main/java/com/deniscerri/ytdl/receiver/CancelScheduleAlarmReceiver.kt
@@ -0,0 +1,34 @@
+package com.deniscerri.ytdl.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.preference.PreferenceManager
+import androidx.work.Constraints
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import com.deniscerri.ytdl.work.CancelScheduledDownloadWorker
+import com.deniscerri.ytdl.work.DownloadWorker
+import java.util.concurrent.TimeUnit
+
+class CancelScheduleAlarmReceiver : BroadcastReceiver() {
+ override fun onReceive(ctx: Context?, p1: Intent?) {
+ ctx?.apply {
+ val workConstraints = Constraints.Builder()
+ val workRequest2 = OneTimeWorkRequestBuilder()
+ .addTag("cancelScheduledDownload")
+ .setConstraints(workConstraints.build())
+ .setInitialDelay( 0L, TimeUnit.MILLISECONDS)
+
+ WorkManager.getInstance(this).enqueueUniqueWork(
+ System.currentTimeMillis().toString(),
+ ExistingWorkPolicy.REPLACE,
+ workRequest2.build()
+ )
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/deniscerri/ytdl/receiver/ScheduleAlarmReceiver.kt b/app/src/main/java/com/deniscerri/ytdl/receiver/ScheduleAlarmReceiver.kt
new file mode 100644
index 00000000..01e45b4f
--- /dev/null
+++ b/app/src/main/java/com/deniscerri/ytdl/receiver/ScheduleAlarmReceiver.kt
@@ -0,0 +1,38 @@
+package com.deniscerri.ytdl.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import androidx.preference.PreferenceManager
+import androidx.work.Constraints
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import com.deniscerri.ytdl.work.DownloadWorker
+import java.util.concurrent.TimeUnit
+
+class ScheduleAlarmReceiver : BroadcastReceiver() {
+ override fun onReceive(ctx: Context?, p1: Intent?) {
+ ctx?.apply {
+ val workConstraints = Constraints.Builder()
+ val preferences = PreferenceManager.getDefaultSharedPreferences(this)
+ val allowMeteredNetworks = preferences.getBoolean("metered_networks", true)
+ if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
+
+
+ val workRequest = OneTimeWorkRequestBuilder()
+ .addTag("scheduledDownload")
+ .addTag("download")
+ .setConstraints(workConstraints.build())
+ .setInitialDelay(0L, TimeUnit.MILLISECONDS)
+
+ WorkManager.getInstance(this).enqueueUniqueWork(
+ System.currentTimeMillis().toString(),
+ ExistingWorkPolicy.REPLACE,
+ workRequest.build()
+ )
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt b/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt
index cfb1598e..911d8252 100644
--- a/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt
+++ b/app/src/main/java/com/deniscerri/ytdl/receiver/ShareActivity.kt
@@ -21,6 +21,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.WindowManager
import androidx.core.app.ActivityCompat
+import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.lifecycle.ViewModelProvider
@@ -34,9 +35,11 @@ import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.viewmodel.CookieViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
+import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.util.ThemeUtil
+import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -52,6 +55,7 @@ class ShareActivity : BaseActivity() {
lateinit var context: Context
private lateinit var resultViewModel: ResultViewModel
+ private lateinit var historyViewModel: HistoryViewModel
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var cookieViewModel: CookieViewModel
private lateinit var sharedPreferences: SharedPreferences
@@ -115,6 +119,7 @@ class ShareActivity : BaseActivity() {
context = baseContext
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
+ historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
@@ -208,6 +213,18 @@ class ShareActivity : BaseActivity() {
}
this@ShareActivity.finish()
}
+
+ downloadViewModel.alreadyExistsUiState.collectLatest { res ->
+ if (res.isNotEmpty()){
+ withContext(Dispatchers.Main){
+ val bundle = bundleOf(
+ Pair("duplicates", res)
+ )
+ navController.navigate(R.id.downloadsAlreadyExistDialog2, bundle)
+ }
+ downloadViewModel.alreadyExistsUiState.value = mutableListOf()
+ }
+ }
}
}
}
diff --git a/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt b/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt
new file mode 100644
index 00000000..cff70e42
--- /dev/null
+++ b/app/src/main/java/com/deniscerri/ytdl/services/ProcessDownloadsInBackgroundService.kt
@@ -0,0 +1,136 @@
+package com.deniscerri.ytdl.services
+
+import android.app.Service
+import android.content.Intent
+import android.os.Binder
+import android.os.IBinder
+import androidx.core.content.IntentCompat
+import com.deniscerri.ytdl.database.DBManager
+import com.deniscerri.ytdl.database.models.DownloadItem
+import com.deniscerri.ytdl.database.models.ResultItem
+import com.deniscerri.ytdl.database.repository.DownloadRepository
+import com.deniscerri.ytdl.database.repository.ResultRepository
+import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
+import com.deniscerri.ytdl.database.viewmodel.SharedDownloadViewModel
+import com.deniscerri.ytdl.util.NotificationUtil
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.launch
+
+
+class ProcessDownloadsInBackgroundService : Service() {
+
+ private val binder: IBinder = LocalBinder()
+ private val queueProcessingDownloadsJobList = mutableListOf()
+ private lateinit var repository: DownloadRepository
+ private lateinit var resultRepository: ResultRepository
+ private lateinit var downloadViewModel: SharedDownloadViewModel
+ inner class LocalBinder: Binder() {
+ val service: ProcessDownloadsInBackgroundService
+ get() = this@ProcessDownloadsInBackgroundService
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+ val dbManager = DBManager.getInstance(this)
+ repository = DownloadRepository(dbManager.downloadDao)
+ resultRepository = ResultRepository(dbManager.resultDao, this)
+ downloadViewModel = SharedDownloadViewModel(this)
+ }
+
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ val notificationUtil = NotificationUtil(this)
+ startForeground(System.currentTimeMillis().toInt(), notificationUtil.createProcessingDownloads())
+
+ val itemType = intent.getStringExtra("itemType") ?: ""
+ val itemIDs = intent.getLongArrayExtra("itemIDs") ?: longArrayOf()
+ val jobData = DownloadViewModel.ProcessingItemsJob(
+ itemType = itemType,
+ itemIDs = itemIDs.toList()
+ )
+ val timeInMillis = intent.getLongExtra("timeInMillis", 0)
+ CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
+ runJob(jobData, timeInMillis)
+ }
+ return super.onStartCommand(intent, flags, startId)
+ }
+
+ override fun onBind(intent: Intent): IBinder {
+ return binder
+ }
+
+ private fun DownloadItem.setAsScheduling(timeInMillis: Long) {
+ status = DownloadRepository.Status.Scheduled.toString()
+ downloadStartTime = timeInMillis
+ }
+
+ private suspend fun runJob(jobData: DownloadViewModel.ProcessingItemsJob, timeInMillis: Long = 0) {
+ val job = CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
+ if (jobData.itemType != "") {
+ val itemIDS = jobData.itemIDs
+ val processingType = jobData.itemType
+ when(processingType) {
+ ResultItem::class.java.toString() -> {
+ itemIDS.chunked(100).map { ids ->
+ resultRepository.getAllByIDs(ids).map {
+ downloadViewModel.createDownloadItemFromResult(
+ result = it, givenType = DownloadViewModel.Type.valueOf(
+ downloadViewModel.getDownloadType(url = it.url).toString()
+ )
+ )
+ }.apply {
+ if (timeInMillis > 0) {
+ this.forEach {
+ it.setAsScheduling(timeInMillis)
+ }
+ }
+ downloadViewModel.queueDownloads(this)
+ }
+ }
+ }
+
+ DownloadItem::class.java.toString() -> {
+ itemIDS.chunked(100).map { ids ->
+ repository.getAllItemsByIDs(ids).apply {
+ if (timeInMillis > 0) {
+ this.forEach {
+ it.setAsScheduling(timeInMillis)
+ }
+ }
+ downloadViewModel.queueDownloads(this)
+ }
+ }
+ }
+ }
+ }else {
+ repository.getProcessingDownloads().apply {
+ if (timeInMillis > 0){
+ this.forEach {
+ it.setAsScheduling(timeInMillis)
+ }
+ }
+
+ downloadViewModel.queueDownloads(this)
+ }
+ }
+ }
+ job.invokeOnCompletion {
+ queueProcessingDownloadsJobList.remove(job)
+ if (queueProcessingDownloadsJobList.isEmpty()){
+ stopForeground(true)
+ stopSelf()
+ }
+ }
+ queueProcessingDownloadsJobList.add(job)
+ }
+
+ fun cancelAllProcessingJobs(){
+ queueProcessingDownloadsJobList.onEach { it.cancel(CancellationException()) }
+ }
+
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt
index 65424e31..06d6a4a3 100644
--- a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt
+++ b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt
@@ -20,6 +20,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
+import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.isVisible
@@ -59,12 +60,14 @@ import com.google.android.material.search.SearchBar
import com.google.android.material.search.SearchView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
+import kotlin.collections.ArrayList
class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggestionsAdapter.OnItemClickListener, OnClickListener {
@@ -76,7 +79,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
private var downloadSelectedFab: ExtendedFloatingActionButton? = null
private var downloadAllFab: ExtendedFloatingActionButton? = null
private var clipboardFab: ExtendedFloatingActionButton? = null
- private var homeFabs: CoordinatorLayout? = null
+ private var homeFabs: LinearLayout? = null
private var infoUtil: InfoUtil? = null
private var downloadQueue: ArrayList? = null
@@ -216,12 +219,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
inputQueries!!.addAll(argList)
}
- if(arguments?.getBoolean("search") == true){
- requireView().post {
- searchBar?.performClick()
- }
- }
-
if (inputQueries != null) {
lifecycleScope.launch(Dispatchers.IO){
resultViewModel.deleteAll()
@@ -276,22 +273,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
lifecycleScope.launch {
launch{
downloadViewModel.alreadyExistsUiState.collectLatest { res ->
- if (res.downloadItems.isNotEmpty() || res.historyItems.isNotEmpty()) {
+ if (res.isNotEmpty()){
withContext(Dispatchers.Main){
- kotlin.runCatching {
- UiUtil.handleExistingDownloadsResponse(
- requireActivity(),
- requireActivity().lifecycleScope,
- requireActivity().supportFragmentManager,
- res,
- downloadViewModel,
- historyViewModel)
- }
+ val bundle = bundleOf(
+ Pair("duplicates", ArrayList(res))
+ )
+ delay(500)
+ findNavController().navigate(R.id.downloadsAlreadyExistDialog, bundle)
}
- downloadViewModel.alreadyExistsUiState.value = DownloadViewModel.AlreadyExistsUIState(
- mutableListOf(),
- mutableListOf()
- )
+ downloadViewModel.alreadyExistsUiState.value = mutableListOf()
}
}
}
@@ -313,15 +303,24 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
arguments?.remove("showDownloadsWithUpdatedFormats")
CoroutineScope(Dispatchers.IO).launch {
val ids = arguments?.getLongArray("downloadIds") ?: return@launch
- downloadViewModel.updateItemsWithIdsToProcessingStatus(ids.toList())
+ val jobID = downloadViewModel.turnDownloadItemsToProcessingDownloads(ids.toList())
withContext(Dispatchers.Main){
- findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
+ findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
+ Pair("processingDownloadsJobID", jobID)
+ ))
}
}
}
+
+ if(arguments?.getBoolean("search") == true){
+ arguments?.remove("search")
+ requireView().post {
+ searchBar?.performClick()
+ }
+ }
if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){
- updateSearchViewItems(searchView?.editText?.text)
+ updateSearchViewItems(searchView?.editText?.text.toString())
}
requireView().post {
@@ -405,13 +404,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
chipGroupDivider?.visibility = VISIBLE
}
- updateSearchViewItems(searchView!!.editText.text)
+ updateSearchViewItems(searchView!!.editText.text.toString())
}
}
searchView!!.editText.doAfterTextChanged {
if (searchView!!.currentTransitionState != SearchView.TransitionState.SHOWN) return@doAfterTextChanged
- updateSearchViewItems(it)
+ updateSearchViewItems(it.toString())
}
searchView!!.editText.setOnTouchListener(OnTouchListener { _, event ->
@@ -454,7 +453,11 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchBar!!.setOnMenuItemClickListener { m: MenuItem ->
when (m.itemId) {
R.id.delete_results -> {
- resultViewModel.cancelParsingQueries()
+ lifecycleScope.launch {
+ withContext(Dispatchers.IO){
+ resultViewModel.cancelParsingQueries()
+ }
+ }
resultViewModel.getTrending()
selectedObjects = ArrayList()
searchBar!!.setText("")
@@ -479,7 +482,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
@SuppressLint("InflateParams")
- private fun updateSearchViewItems(searchQuery: Editable?) = lifecycleScope.launch(Dispatchers.Main) {
+ private fun updateSearchViewItems(searchQuery: String) = lifecycleScope.launch(Dispatchers.Main) {
lifecycleScope.launch {
if (searchView!!.editText.text.isEmpty()){
searchView!!.editText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
@@ -490,13 +493,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val combinedList = mutableListOf()
val history = withContext(Dispatchers.IO){
- resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery!!) }
+ resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery) }
}.map {
SearchSuggestionItem(it, SearchSuggestionType.HISTORY)
}
val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){
withContext(Dispatchers.IO){
- infoUtil!!.getSearchSuggestions(searchQuery.toString())
+ infoUtil!!.getSearchSuggestions(searchQuery)
}
}else{
emptyList()
@@ -509,12 +512,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val url = checkClipboard()
url?.apply {
- if (this.isNotEmpty()){
+ var alreadyHasThem = this.all { queriesChipGroup?.children?.any { c -> (c as Chip).text.contains(it) } == true }
+ if (this.isNotEmpty() && !alreadyHasThem){
combinedList.add(0, SearchSuggestionItem(this.joinToString("\n"), SearchSuggestionType.CLIPBOARD))
}
}
-
searchSuggestionsAdapter?.submitList(combinedList)
// history.forEach { s ->
@@ -729,21 +732,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} catch (e: Exception) {""}
if (viewIdName.isNotEmpty()) {
if (viewIdName == "downloadAll") {
- lifecycleScope.launch {
- val downloadList = withContext(Dispatchers.IO){
- downloadViewModel.turnResultItemsToDownloadItems(resultsList!!)
- }
- if (sharedPreferences!!.getBoolean("download_card", true)) {
- CoroutineScope(Dispatchers.IO).launch {
- downloadViewModel.insertToProcessing(downloadList)
- }
-
- findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
- } else {
- downloadList.chunked(100).forEach {
- downloadViewModel.queueDownloads(it)
- }
- }
+ val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true)
+ val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(resultsList!!.map { it!!.id }, downloadNow = !showDownloadCard)
+ if (showDownloadCard){
+ findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
+ Pair("processingDownloadsJobID", jobID)
+ ))
}
}
}
@@ -806,24 +800,18 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
R.id.download -> {
lifecycleScope.launch {
- if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects.size == 1) {
+ val showDownloadCard = sharedPreferences!!.getBoolean("download_card", true)
+ if (showDownloadCard && selectedObjects.size == 1) {
showSingleDownloadSheet(
selectedObjects[0],
downloadViewModel.getDownloadType(url = selectedObjects[0].url)
)
}else{
- val downloadList = withContext(Dispatchers.IO){
- downloadViewModel.turnResultItemsToDownloadItems(selectedObjects)
- }
-
- if (sharedPreferences!!.getBoolean("download_card", true)) {
- CoroutineScope(Dispatchers.IO).launch {
- downloadViewModel.insertToProcessing(downloadList)
- }
-
- findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2)
- } else {
- downloadViewModel.queueDownloads(downloadList)
+ val jobID = downloadViewModel.turnResultItemsToProcessingDownloads(selectedObjects.map { it.id }, downloadNow = !showDownloadCard)
+ if (showDownloadCard){
+ findNavController().navigate(R.id.downloadMultipleBottomSheetDialog2, bundleOf(
+ Pair("processingDownloadsJobID", jobID)
+ ))
}
}
clearCheckedItems()
@@ -835,7 +823,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
homeAdapter?.checkAll(resultsList)
selectedObjects.clear()
resultsList?.forEach { selectedObjects.add(it!!) }
- mode?.title = getString(R.string.all_items_selected)
+ mode?.title = "(${selectedObjects.size}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {
@@ -905,14 +893,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
- override fun onSearchSuggestionAdd(t: String) {
- val items = t.split("\n")
+ override fun onSearchSuggestionAdd(text: String) {
+ val items = text.split("\n")
- items.forEach {text ->
- val present = queriesChipGroup!!.children.firstOrNull { (it as Chip).text.toString() == text }
+ items.forEach {t ->
+ val present = queriesChipGroup!!.children.firstOrNull { (it as Chip).text.toString() == t }
if (present == null) {
val chip = layoutinflater!!.inflate(R.layout.input_chip, queriesChipGroup, false) as Chip
- chip.text = text
+ chip.text = t
chip.chipBackgroundColor = ColorStateList.valueOf(MaterialColors.getColor(requireContext(), R.attr.colorSecondaryContainer, Color.BLACK))
chip.setOnClickListener {
if (queriesChipGroup!!.childCount == 1) queriesConstraint!!.visibility = View.GONE
@@ -924,17 +912,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
searchView!!.editText.setText("")
- if (queriesChipGroup!!.childCount == 0) queriesConstraint!!.visibility = View.GONE
- else queriesConstraint!!.visibility = View.VISIBLE
+ queriesConstraint?.isVisible = queriesChipGroup?.childCount!! > 0
- val clipBoardItem = searchSuggestionsRecyclerView?.layoutManager?.findViewByPosition(0)
- clipBoardItem?.apply {
- if ((this as ConstraintLayout).findViewById(R.id.suggestion_text).text == getString(R.string.link_you_copied)){
- searchSuggestionsAdapter?.notifyItemRemoved(0)
+ searchSuggestionsAdapter?.getList()?.apply {
+ if (this.first().type == SearchSuggestionType.CLIPBOARD){
+ val newList = this.toMutableList().drop(1)
+ searchSuggestionsAdapter?.submitList(newList)
}
}
-
-
}
override fun onSearchSuggestionLongClick(text: String, position: Int) {
@@ -943,7 +928,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
resultViewModel.removeSearchQueryFromHistory(text)
- updateSearchViewItems(searchView!!.editText.text)
+ updateSearchViewItems(searchView!!.editText.text.toString())
}
deleteDialog.show()
}
diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/AlreadyExistsAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/AlreadyExistsAdapter.kt
index 0fa2593a..d429e2a5 100644
--- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/AlreadyExistsAdapter.kt
+++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/AlreadyExistsAdapter.kt
@@ -2,27 +2,34 @@ package com.deniscerri.ytdl.ui.adapter
import android.app.Activity
import android.content.SharedPreferences
+import android.os.Build
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
+import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
+import android.widget.PopupMenu
import android.widget.TextView
+import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R
+import com.deniscerri.ytdl.database.models.AlreadyExistsItem
import com.deniscerri.ytdl.database.models.DownloadItem
+import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.loadThumbnail
import com.deniscerri.ytdl.util.Extensions.popup
import com.deniscerri.ytdl.util.FileUtil
+import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.card.MaterialCardView
-class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter, AlreadyExistsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
+class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter(AsyncDifferConfig.Builder(
DIFF_CALLBACK
).build()) {
private val onItemClickListener: OnItemClickListener
@@ -39,7 +46,7 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
val cardView: MaterialCardView
init {
- cardView = itemView as MaterialCardView
+ cardView = itemView.findViewById(R.id.download_card_view)
}
}
@@ -51,12 +58,9 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val alreadyExistsItem = getItem(position) ?: return
- val item = alreadyExistsItem.first
- val historyID = alreadyExistsItem.second
-
val card = holder.cardView
- card.tag = item.id.toString()
- card.popup()
+ card.tag = alreadyExistsItem.downloadItem.id.toString()
+ val item = alreadyExistsItem.downloadItem
val uiHandler = Handler(Looper.getMainLooper())
val thumbnail = card.findViewById(R.id.downloads_image_view)
@@ -76,6 +80,20 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
}
itemTitle.text = title.ifEmpty { item.url }
+ //DOWNLOAD TYPE -----------------------------
+ val type = card.findViewById(R.id.download_type)
+ when(item.type){
+ DownloadViewModel.Type.audio -> type.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ R.drawable.ic_music_formatcard, 0,0,0
+ )
+ DownloadViewModel.Type.video -> type.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ R.drawable.ic_video_formatcard, 0,0,0
+ )
+ else -> type.setCompoundDrawablesRelativeWithIntrinsicBounds(
+ R.drawable.ic_terminal_formatcard, 0,0,0
+ )
+ }
+
val formatNote = card.findViewById(R.id.format_note)
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE
@@ -102,45 +120,64 @@ class AlreadyExistsAdapter(onItemClickListener: OnItemClickListener, activity: A
if (fileSizeReadable == "?") fileSize.visibility = View.GONE
else fileSize.text = fileSizeReadable
- val editBtn = card.findViewById