diff --git a/app/build.gradle b/app/build.gradle index 092840c4..eecc9d4e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -130,10 +130,10 @@ dependencies { implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer" implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer" implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer" -// -// implementation "io.github.junkfood02.youtubedl-android:library:0.16.0" -// implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.16.0" -// implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.0" + +// implementation "io.github.junkfood02.youtubedl-android:library:0.16.1" +// implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.16.1" +// implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.1" implementation "androidx.appcompat:appcompat:$appCompatVer" implementation "androidx.constraintlayout:constraintlayout:2.1.4" @@ -143,10 +143,10 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.1' implementation "androidx.navigation:navigation-fragment-ktx:$navVer" implementation "androidx.navigation:navigation-ui-ktx:$navVer" - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.test.ext:junit-ktx:1.1.5' - implementation 'androidx.compose.ui:ui-android:1.6.5' - implementation 'androidx.preference:preference:1.2.1' + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.test.ext:junit-ktx:1.2.1' + implementation 'androidx.compose.ui:ui-android:1.6.8' + implementation 'androidx.preference:preference-ktx:1.2.1' testImplementation "junit:junit:$junitVer" androidTestImplementation "junit:junit:$junitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" @@ -167,10 +167,10 @@ dependencies { implementation "androidx.room:room-runtime:$roomVer" implementation "androidx.room:room-ktx:$roomVer" ksp "androidx.room:room-compiler:$roomVer" - implementation 'androidx.paging:paging-runtime-ktx:3.2.1' + implementation 'androidx.paging:paging-runtime-ktx:3.3.0' implementation "androidx.room:room-paging:$roomVer" androidTestImplementation "androidx.room:room-testing:$roomVer" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.3' implementation "androidx.compose.runtime:runtime:$composeVer" androidTestImplementation 'com.google.truth:truth:1.1.5' @@ -182,7 +182,7 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4' - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.2" + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.3" implementation "com.github.Irineu333:Highlight-KT:1.0.4" // For media playback using ExoPlayer diff --git a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt index d9ca49a9..8f7002c8 100644 --- a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt @@ -258,13 +258,10 @@ class MainActivity : BaseActivity() { navigationView.visibilityChanged { if (it.isVisible){ val curr = navController.currentDestination?.id - if (curr != R.id.homeFragment && curr != R.id.historyFragment && curr != R.id.moreFragment) hideBottomNavigation() + if (!showingNavbarItems.contains(curr)) hideBottomNavigation() } } - when(preferences.getString("start_destination", "")) { - "Queue" -> if (savedInstanceState == null) navController.navigate(R.id.downloadQueueMainFragment) - } cookieViewModel.updateCookiesFile() val intent = intent diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt index 7590968a..14c528fa 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt @@ -1,5 +1,6 @@ package com.deniscerri.ytdl.database.dao +import android.util.Log import androidx.paging.PagingSource import androidx.room.Dao import androidx.room.Insert @@ -25,7 +26,7 @@ interface DownloadDao { fun getActiveDownloads() : Flow> @Query("SELECT * FROM downloads WHERE status = 'Processing'") - fun getProcessingDownloads() : Flow> + fun getProcessingDownloads() : Flow> @Query("SELECT COUNT(*) FROM downloads WHERE status in (:statuses)") fun getDownloadsCountFlow(statuses: List) : Flow @@ -54,15 +55,18 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status = 'Processing'") fun getProcessingDownloadsList() : List - @Query("UPDATE downloads set downloadStartTime=:time, status='Scheduled' WHERE status ='Processing'") - suspend fun updateProcessingDownloadTime(time: Long) - @Query("UPDATE downloads set downloadPath=:path WHERE status ='Processing'") suspend fun updateProcessingDownloadPath(path: String) @Query("SELECT * FROM downloads WHERE status='Active'") fun getActiveDownloadsList() : List + @Query("SELECT * FROM downloads WHERE url=:url AND status='Processing'") + fun getProcessingDownloadsByUrl(url: String) : List + + @Query("DELETE from downloads where status = 'Processing' AND url=:url") + suspend fun deleteProcessingByUrl(url: String) + @Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')") fun getActiveAndQueuedDownloadsList() : List @Query("UPDATE downloads SET status='Queued' where status = 'Active'") @@ -179,6 +183,13 @@ interface DownloadDao { @Upsert suspend fun update(item: DownloadItem) + @Transaction + suspend fun updateAll(list: List){ + list.forEach { + update(it) + } + } + @Query("UPDATE downloads set status=:status where id=:id") suspend fun setStatus(id: Long, status: String) 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 56f4effa..927cc12c 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 @@ -49,9 +49,15 @@ interface ResultDao { @Query("DELETE FROM results WHERE id=:id") suspend fun delete(id: Long) + @Query("DELETE FROM results WHERE url=:url") + suspend fun deleteByUrl(url: String) + @Query("SELECT * FROM results WHERE url=:url LIMIT 1") fun getResultByURL(url: String) : ResultItem? + @Query("SELECT * FROM results WHERE url=:url") + fun getAllByURL(url: String) : List + @Query("SELECT * FROM results where id=:id LIMIT 1") fun getResultByID(id: Long): ResultItem? 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 1aee5834..dc67c8f6 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 @@ -26,6 +26,7 @@ import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItemSimple import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.FileUtil +import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.DownloadWorker import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged @@ -42,7 +43,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) { pagingSourceFactory = {downloadDao.getAllDownloads()} ) val activeDownloads : Flow> = downloadDao.getActiveDownloads().distinctUntilChanged() - val processingDownloads : Flow> = downloadDao.getProcessingDownloads().distinctUntilChanged() + val processingDownloads : Flow> = downloadDao.getProcessingDownloads().distinctUntilChanged() val queuedDownloads : Pager = Pager( config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), pagingSourceFactory = {downloadDao.getQueuedDownloads()} @@ -101,6 +102,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) { downloadDao.update(item) } + suspend fun updateAll(list: List) { + downloadDao.updateAll(list) + } + suspend fun updateWithoutUpsert(item: DownloadItem){ kotlin.runCatching { downloadDao.updateWithoutUpsert(item) } } @@ -122,6 +127,14 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.getActiveDownloadsList() } + fun getProcessingDownloadsByUrl(url: String) : List { + return downloadDao.getProcessingDownloadsByUrl(url) + } + + suspend fun deleteProcessingByUrl(url: String) { + downloadDao.deleteProcessingByUrl(url) + } + fun getProcessingDownloads() : List { return downloadDao.getProcessingDownloadsList() } @@ -130,10 +143,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.getActiveAndQueuedDownloadsList() } - suspend fun updateProcessingDownloadTime(time: Long) { - downloadDao.updateProcessingDownloadTime(time) - } - fun getActiveAndQueuedDownloadIDs() : List { return downloadDao.getActiveAndQueuedDownloadIDs() } @@ -203,14 +212,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) { downloadDao.removeAllLogID() } - fun getFilteredProcessingDownloads(ids: List) : Flow> { - return downloadDao.getProcessingDownloads() - .map { - it.filter { ids.contains(it.id) } - } - } - - @SuppressLint("RestrictedApi") suspend fun startDownloadWorker(queuedItems: List, context: Context, inputData: Data.Builder = Data.Builder()) { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) @@ -230,12 +231,16 @@ class DownloadRepository(private val downloadDao: DownloadDao) { if (delay <= 60000L) delay = 0L } + val useAlarmForScheduling = sharedPreferences.getBoolean("use_alarm_for_scheduling", false) + + if (delay > 0L && useAlarmForScheduling) { + AlarmScheduler(context).scheduleAt(queuedItems.minBy { it.downloadStartTime }.downloadStartTime) + return + } + val workConstraints = Constraints.Builder() if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED) - else { - //workConstraints.setRequiredNetworkType(NetworkType.CONNECTED) - } val workRequest = OneTimeWorkRequestBuilder() .addTag("download") @@ -243,10 +248,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) { .setInitialDelay(delay, TimeUnit.MILLISECONDS) .setInputData(inputData.build()) - queuedItems.forEach { - workRequest.addTag(it.id.toString()) - } - workManager.enqueueUniqueWork( System.currentTimeMillis().toString(), ExistingWorkPolicy.REPLACE, 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 1b9d4332..c12cdd33 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 @@ -5,7 +5,9 @@ import android.util.Patterns import com.deniscerri.ytdl.database.dao.ResultDao import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.ResultItem +import com.deniscerri.ytdl.util.Extensions.isYoutubeURL import com.deniscerri.ytdl.util.InfoUtil +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import java.util.regex.Pattern @@ -88,6 +90,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co if (tmpToken.isEmpty()) break if (tmpToken == nextPageToken) break nextPageToken = tmpToken + delay(1000) } while (true) itemCount.value = items.size return items @@ -117,6 +120,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co resultDao.delete(item.id) } + suspend fun deleteByUrl(url: String) { + resultDao.deleteByUrl(url) + } + suspend fun deleteAll(){ itemCount.value = 0 resultDao.deleteAll() @@ -134,6 +141,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co return resultDao.getResultByURL(url) } + fun getAllByURL(url: String) : List { + return resultDao.getAllByURL(url) + } + fun getAllByIDs(ids: List) : List { return resultDao.getAllByIDs(ids) } @@ -162,9 +173,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co private fun getQueryType(inputQuery: String) : SourceType { var type = SourceType.SEARCH_QUERY - val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)") - val m = p.matcher(inputQuery) - if (m.find()) { + if (inputQuery.isYoutubeURL()) { type = SourceType.YOUTUBE_VIDEO if (inputQuery.contains("playlist?list=")) { type = SourceType.YOUTUBE_PLAYLIST 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 a6970203..eb724677 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,7 +5,13 @@ 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.core.content.res.ResourcesCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData @@ -34,18 +40,26 @@ 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.ui.downloadcard.FormatTuple +import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.FileUtil +import com.deniscerri.ytdl.util.FormatSorter 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.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import kotlinx.parcelize.Parcelize +import okhttp3.internal.format import java.io.File import java.util.Locale @@ -53,14 +67,15 @@ 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 + private val resources : Resources + val allDownloads : Flow> val queuedDownloads : Flow> val activeDownloads : Flow> - val processingDownloads : Flow> + val processingDownloads : Flow> val cancelledDownloads : Flow> val erroredDownloads : Flow> val savedDownloads : Flow> @@ -74,7 +89,15 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val savedDownloadsCount : Flow val scheduledDownloadsCount : Flow - val alreadyExistsUiState: MutableStateFlow> + @Parcelize + data class AlreadyExistsIDs( + var downloadItemID: Long, + var historyItemID : Long? + ) : Parcelable + + val alreadyExistsUiState: MutableStateFlow> = MutableStateFlow( + mutableListOf() + ) private var extraCommandsForAudio: String = "" private var extraCommandsForVideo: String = "" @@ -87,15 +110,19 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel auto, audio, video, command } - var processingItemsFlow : ProcessingItemsJob? = null + private val urlsForAudioType = listOf( + "music", + "audio", + "soundcloud" + ) + var processingItems = MutableStateFlow(false) + var processingItemsJob : Job? = null init { 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) @@ -125,6 +152,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } } + + val confTmp = Configuration(application.resources.configuration) + confTmp.setLocale(Locale(sharedPreferences.getString("app_language", "en")!!)) + val metrics = DisplayMetrics() + resources = Resources(application.assets, metrics, confTmp) } fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) { @@ -155,15 +187,130 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } fun getDownloadType(t: Type? = null, url: String) : Type { - return sharedDownloadViewModel.getDownloadType(t, url) + 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 { - return sharedDownloadViewModel.createDownloadItemFromResult(result, url, givenType) + 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).30B - %(title).170B") + Type.video -> sharedPreferences.getString("file_name_template", "%(uploader).30B - %(title).170B") + 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, + "", + if (resultItem.playlistTitle == resultRepository.YTDLNIS_SEARCH) "" else resultItem.playlistTitle, + audioPreferences, + videoPreferences, + extraCommands, + customFileNameTemplate!!, + saveThumb, + DownloadRepository.Status.Queued.toString(), + 0, + null, + playlistURL = resultItem.playlistURL, + playlistIndex = resultItem.playlistIndex, + incognito = sharedPreferences.getBoolean("incognito", false) + ) } fun createResultItemFromDownload(downloadItem: DownloadItem) : ResultItem { - return sharedDownloadViewModel.createResultItemFromDownload(downloadItem) + 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 createResultItemFromHistory(downloadItem: HistoryItem) : ResultItem { @@ -187,7 +334,22 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } fun createEmptyResultItem(url: String) : ResultItem { - return sharedDownloadViewModel.createEmptyResultItem(url) + return ResultItem( + 0, + url, + "", + "", + "", + "", + "", + "", + arrayListOf(), + "", + arrayListOf(), + "", + null, + System.currentTimeMillis() + ) } fun switchDownloadType(list: List, type: Type) : List{ @@ -300,73 +462,112 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } - fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> { - return sharedDownloadViewModel.getPreferredAudioRequirements() - } - - //requirement and importance - @SuppressLint("RestrictedApi") - fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> { - return sharedDownloadViewModel.getPreferredVideoRequirements() - } - fun getFormat(formats: List, type: Type) : Format { - return sharedDownloadViewModel.getFormat(formats, type) + when(type) { + Type.audio -> { + return cloneFormat ( + try { + val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" } + FormatSorter(application).sortAudioFormats(theFormats).first() + }catch (e: Exception){ + infoUtil.getGenericAudioFormats(resources).first() + } + ) + + } + Type.video -> { + return cloneFormat( + try { + val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty { + infoUtil.getGenericVideoFormats(resources).sortedByDescending { it.filesize } + } + + FormatSorter(application).sortVideoFormats(theFormats).first() + }catch (e: Exception){ + infoUtil.getGenericVideoFormats(resources).first() + } + ) + } + 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) + } + } + } + + private fun cloneFormat(item: Format) : Format { + val string = Gson().toJson(item, Format::class.java) + return Gson().fromJson(string, Format::class.java) } fun getPreferredAudioFormats(formats: List) : ArrayList{ - return sharedDownloadViewModel.getPreferredAudioFormats(formats) + val preferredAudioFormats = arrayListOf() + val audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() } + 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 sharedDownloadViewModel.generateCommandFormat(c) + return Format( + c.title, + c.id.toString(), + "", + "", + "", + 0, + c.content.replace("\n", " ") + ) } - data class ProcessingItemsJob( - var job: Job? = null, - var originItemType: String, - var originItemIDs: List, - var processingDownloadItemIDs: MutableList = mutableListOf() - ) - private fun updateProcessingJobData(item: ProcessingItemsJob?) { - processingItemsFlow = item - } fun turnDownloadItemsToProcessingDownloads(itemIDs: List) = viewModelScope.launch(Dispatchers.IO){ - updateProcessingJobData(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs)) val job = viewModelScope.launch(Dispatchers.IO) { repository.deleteProcessing() processingItems.emit(true) try { itemIDs.forEachIndexed { idx, it -> val item = repository.getItemByID(it) - if (processingItemsFlow?.job?.isCancelled == true) throw CancellationException() + if (processingItemsJob?.isCancelled == true) throw CancellationException() item.id = 0 item.status = DownloadRepository.Status.Processing.toString() - processingItemsFlow?.apply { - val id = repository.insert(item) - processingDownloadItemIDs.add(id) - updateProcessingJobData(this) - } + repository.insert(item) } processingItems.emit(false) } catch (e: Exception) { deleteProcessing() - updateProcessingJobData(null) processingItems.emit(false) } } - updateProcessingJobData(ProcessingItemsJob(job, DownloadItem::class.java.toString(), itemIDs)) + processingItemsJob = job } fun turnResultItemsToProcessingDownloads(itemIDs: List, downloadNow: Boolean = false) = viewModelScope.launch(Dispatchers.IO) { - updateProcessingJobData(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs)) val job = viewModelScope.launch(Dispatchers.IO) { repository.deleteProcessing() processingItems.emit(true) try { + val toInsert = mutableListOf() itemIDs.forEach { id -> val item = resultRepository.getItemByID(id) ?: return@forEach val preferredType = getDownloadType(url = item.url).toString() @@ -375,7 +576,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel )) downloadItem.status = DownloadRepository.Status.Processing.toString() - if (processingItemsFlow?.job?.isCancelled == true) { + if (processingItemsJob?.isCancelled == true) { throw CancellationException() } @@ -383,21 +584,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel downloadItem.status = DownloadRepository.Status.Queued.toString() queueDownloads(listOf(downloadItem)) }else{ - processingItemsFlow?.apply { - processingDownloadItemIDs.add(repository.insert(downloadItem)) - updateProcessingJobData(this) - } + toInsert.add(downloadItem) + //repository.insert(downloadItem) } } + repository.insertAll(toInsert) processingItems.emit(false) }catch (e: Exception) { deleteProcessing() - updateProcessingJobData(null) processingItems.emit(false) } } - updateProcessingJobData(ProcessingItemsJob(job, ResultItem::class.java.toString(), itemIDs)) + processingItemsJob = job + } fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){ @@ -423,10 +623,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } fun deleteScheduled() = viewModelScope.launch(Dispatchers.IO) { - val scheduledIds = repository.getScheduledDownloadIDs() - scheduledIds.forEach { - WorkManager.getInstance(application).cancelAllWorkByTag(it.toString()) - } repository.deleteScheduled() } @@ -451,7 +647,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) { - processingItemsFlow?.apply { this.job?.cancel(CancellationException()) } + processingItemsJob?.apply { cancel(CancellationException()) } repository.cancelActiveQueued() } @@ -574,9 +770,193 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel queueDownloads(processingItems) } - suspend fun queueDownloads(items: List, ign : Boolean = false) : List { - val ids = sharedDownloadViewModel.queueDownloads(items, ign) - return ids + suspend fun queueDownloads(items: List, ignoreDuplicates : Boolean = false) { + 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 + //if history id is empty, it just found an existing item in the queue/active list + val existingItemIDs = mutableListOf() + + val downloadArchive = runCatching { + File(FileUtil.getDownloadArchivePath(context)).useLines { it.toList() } + } + .getOrElse { listOf() } + .map { it.split(" ")[1] } + + val checkDuplicate = sharedPreferences.getString("prevent_duplicate_downloads", "")!! + val activeAndQueuedDownloads = withContext(Dispatchers.IO){ + repository.getActiveAndQueuedDownloads() + } + + items.forEach { + if (it.status != DownloadRepository.Status.Scheduled.toString()) { + it.status = DownloadRepository.Status.Queued.toString() + } + + //CHECK DUPLICATES + var alreadyExists = false + if (checkDuplicate.isNotEmpty() && !ignoreDuplicates){ + 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 existingDownload = activeAndQueuedDownloads.firstOrNull { a -> a.type == it.type && a.url == it.url } + 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 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){ + queuedItems.add(it) + } + + + } + + repository.updateAll(queuedItems) + + //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 (!sharedPreferences.getBoolean("paused_downloads", false)) { + repository.startDownloadWorker(queuedItems, context) + } + + if(!useScheduler){ + CoroutineScope(Dispatchers.IO).launch { + queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { + kotlin.runCatching { + resultRepository.updateDownloadItem(it)?.apply { + repository.updateWithoutUpsert(this) + } + } + } + } + }else{ + CoroutineScope(Dispatchers.IO).launch { + queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach { + kotlin.runCatching { + resultRepository.updateDownloadItem(it)?.apply { + repository.updateWithoutUpsert(this) + } + } + } + } + } + } + + + if (existingItemIDs.isNotEmpty()){ + alreadyExistsUiState.value = existingItemIDs.toList() + } } fun getQueuedCollectedFileSize() : Long { @@ -599,17 +979,25 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel dao.updateProcessingtoSavedStatus() } - suspend fun updateProcessingFormat(selectedFormats: List): List { + + fun updateAllProcessingFormats(formatTuples : List) = viewModelScope.launch(Dispatchers.IO) { val items = repository.getProcessingDownloads() - items.forEachIndexed { index, i -> - selectedFormats[index].format?.apply { - i.format = this + items.forEach { + val ft = formatTuples.first { ft -> ft.url == it.url }.formatTuple + ft.format?.apply { + it.format = this } - if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) } - repository.update(i) + + if (it.type == Type.video) { + ft.audioFormats?.map { a -> a.format_id }?.let { list -> + it.videoPreferences.audioFormatIDs.clear() + it.videoPreferences.audioFormatIDs.addAll(list) + } + } + + repository.update(it) } - return items.map { itm -> itm.format.filesize } } suspend fun updateProcessingCommandFormat(format: Format){ @@ -624,37 +1012,50 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel dao.updateProcessingDownloadPath(path) } - fun getProcessingDownloadsCount() : Int { - return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Processing.toString())) - } - fun getProcessingDownloads() : List { return repository.getProcessingDownloads() } + fun updateDownloadItemFormats(id: Long, list: List) = viewModelScope.launch(Dispatchers.IO) { + val item = repository.getItemByID(id) + item.allFormats.clear() + item.allFormats.addAll(list) + item.format = getFormat(list, item.type) - suspend fun updateProcessingAllFormats(formatCollection: List>) { - val items = repository.getProcessingDownloads() - items.forEachIndexed { index, i -> - i.allFormats.clear() - if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) { - runCatching { - i.allFormats.addAll(formatCollection[index]) - } + runCatching { + resultRepository.getAllByURL(item.url).forEach { + it.formats.clear() + it.formats.addAll(list) + resultRepository.update(it) } - i.format = getFormat(i.allFormats, i.type) - kotlin.runCatching { - dbManager.resultDao.getResultByURL(i.url)?.apply { - this.formats = formatCollection[index].toMutableList() - dbManager.resultDao.update(this) - } - } - repository.update(i) } } + fun updateProcessingFormatByUrl(url: String, list: List) = viewModelScope.launch(Dispatchers.IO) { + val items = repository.getProcessingDownloadsByUrl(url) + items.forEach { item -> + item.allFormats.clear() + item.allFormats.addAll(list) + item.format = getFormat(list, item.type) + repository.update(item) + } + + kotlin.runCatching { + resultRepository.getAllByURL(url).forEach { + it.formats.clear() + it.formats.addAll(list) + resultRepository.update(it) + } + } + } + + fun removeUnavailableDownloadAndResultByURL(url: String) = viewModelScope.launch(Dispatchers.IO) { + repository.deleteProcessingByUrl(url) + resultRepository.deleteByUrl(url) + } + suspend fun continueUpdatingFormatsOnBackground(){ - val ids = dao.getProcessingDownloadsList().map { it.id } + val ids = repository.getProcessingDownloads().map { it.id } dao.updateProcessingtoSavedStatus() val id = System.currentTimeMillis().toInt() @@ -685,12 +1086,21 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } } - suspend fun updateProcessingDownloadTime(time: Long) { - repository.updateProcessingDownloadTime(time) + suspend fun updateProcessingDownloadTimeAndQueueScheduled(time: Long) { + val processing = repository.getProcessingDownloads() + processing.forEach { + it.downloadStartTime = time + it.status = DownloadRepository.Status.Scheduled.toString() + } + queueDownloads(processing) } fun checkIfAllProcessingItemsHaveSameType() : Pair { val types = dao.getProcessingDownloadTypes() + if (types.isEmpty()) { + return Pair(false, Type.command) + } + return Pair(types.size == 1, Type.valueOf(types.first())) } 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 deleted file mode 100644 index 37138c49..00000000 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/SharedDownloadViewModel.kt +++ /dev/null @@ -1,665 +0,0 @@ -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.FormatSorter -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 okhttp3.internal.immutableListOf -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).30B - %(title).170B") - Type.video -> sharedPreferences.getString("file_name_template", "%(uploader).30B - %(title).170B") - 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, - "", - if (resultItem.playlistTitle == resultRepository.YTDLNIS_SEARCH) "" else resultItem.playlistTitle, - audioPreferences, - videoPreferences, - extraCommands, - customFileNameTemplate!!, - saveThumb, - DownloadRepository.Status.Queued.toString(), - 0, - null, - playlistURL = resultItem.playlistURL, - playlistIndex = resultItem.playlistIndex, - incognito = sharedPreferences.getBoolean("incognito", false) - ) - - } - - 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() - ) - - } - - fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> { - val requirements: MutableList<(Format) -> Int> = mutableListOf() - - val itemValues = resources.getStringArray(R.array.format_importance_audio_values).toSet() - val prefAudio = sharedPreferences.getString("format_importance_audio", itemValues.joinToString(","))!! - - prefAudio.split(",").forEachIndexed { idx, s -> - val importance = (itemValues.size - idx) * 10 - when(s) { - "id" -> { - requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) importance else 0} - } - "language" -> { - sharedPreferences.getString("audio_language", "")?.apply { - if (this.isNotBlank()){ - requirements.add { it: Format -> if (it.lang?.contains(this) == true) importance else 0 } - } - } - } - "codec" -> { - requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) importance else 0} - } - "container" -> { - requirements.add {it: Format -> if (it.container == audioContainer) importance else 0 } - } - } - } - - return requirements - } - - //requirement and importance - @SuppressLint("RestrictedApi") - fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> { - val requirements: MutableList<(Format) -> Int> = mutableListOf() - - val itemValues = resources.getStringArray(R.array.format_importance_video_values).toSet() - val prefVideo = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!! - - prefVideo.split(",").forEachIndexed { idx, s -> - var importance = (itemValues.size - idx) * 10 - - when(s) { - "id" -> { - requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) importance else 0 } - } - "resolution" -> { - 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)) (importance) else 0 } - } - "best" -> { - requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (importance) 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)) (importance - index - 1) else 0 } - } - } - } - - } - } - "codec" -> { - requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) importance else 0 } - } - "no_audio" -> { - requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") importance else 0 } - } - "container" -> { - requirements.add { it: Format -> - if (videoContainer == "mp4") - if (it.container.equals("mpeg_4", true)) importance else 0 - else - if (it.container.equals(videoContainer, true)) importance 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" } - FormatSorter(context).sortAudioFormats(theFormats).first() -// -// 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 } - } - - FormatSorter(context).sortVideoFormats(theFormats).first() -// -// 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 (it.status != DownloadRepository.Status.Scheduled.toString()) - 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()){ - if (!sharedPreferences.getBoolean("paused_downloads", false)) { - 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/ui/adapter/ConfigureMultipleDownloadsAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt index 1fdfaa30..c6009b71 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/ConfigureMultipleDownloadsAdapter.kt @@ -11,13 +11,14 @@ import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView import androidx.core.view.isVisible +import androidx.paging.PagingDataAdapter 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.DownloadItem +import com.deniscerri.ytdl.database.models.DownloadItemSimple import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.util.Extensions.loadThumbnail import com.deniscerri.ytdl.util.Extensions.popup @@ -25,7 +26,8 @@ import com.deniscerri.ytdl.util.FileUtil import com.google.android.material.button.MaterialButton import java.util.Locale -class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter(AsyncDifferConfig.Builder( +class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter( + AsyncDifferConfig.Builder( DIFF_CALLBACK ).build()) { private val onItemClickListener: OnItemClickListener @@ -155,12 +157,12 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener } companion object { - private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean { + private val DIFF_CALLBACK: DiffUtil.ItemCallback = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean { return oldItem.url == newItem.url } - override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean { + override fun areContentsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean { return oldItem.title == newItem.title && oldItem.author == newItem.author && oldItem.type == newItem.type && diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/NavBarOptionsAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/NavBarOptionsAdapter.kt index 61603ff4..9a4f9ba5 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/NavBarOptionsAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/NavBarOptionsAdapter.kt @@ -41,6 +41,9 @@ class NavBarOptionsAdapter( val noHome = listOf(R.id.terminalActivity) holder.binding.apply { title.text = item.title + title.contentDescription = item.title + + checkbox.isChecked = item.isVisible || essential.contains(item.itemId) checkbox.isEnabled = !essential.contains(item.itemId) home.setImageResource( @@ -52,11 +55,8 @@ class NavBarOptionsAdapter( if (!checkbox.isChecked || selectedHomeTabId == item.itemId) { return@setOnClickListener } - val oldSelection = items.indexOfFirst { it.itemId == selectedHomeTabId } selectedHomeTabId = item.itemId - listOf(position, oldSelection).forEach { - notifyItemChanged(it) - } + notifyDataSetChanged() } checkbox.setOnClickListener { item.isVisible = checkbox.isChecked diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt index b30101ab..75185027 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt @@ -113,26 +113,28 @@ class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: Downlo viewPager2.adapter = fragmentAdapter viewPager2.isSaveFromParentEnabled = false - when(currentDownloadItem.type) { - Type.audio -> { - tabLayout.selectTab(tabLayout.getTabAt(0)) - viewPager2.setCurrentItem(0, false) - } - Type.video -> { - if (isAudioOnly){ - tabLayout.getTabAt(0)!!.select() + view.post { + when(currentDownloadItem.type) { + Type.audio -> { + tabLayout.selectTab(tabLayout.getTabAt(0)) viewPager2.setCurrentItem(0, false) - Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show() - }else{ - tabLayout.getTabAt(1)!!.select() - viewPager2.setCurrentItem(1, false) } - } - else -> { - tabLayout.selectTab(tabLayout.getTabAt(2)) - viewPager2.postDelayed( { - viewPager2.setCurrentItem(2, false) - }, 200) + Type.video -> { + if (isAudioOnly){ + tabLayout.getTabAt(0)!!.select() + viewPager2.setCurrentItem(0, false) + Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show() + }else{ + tabLayout.getTabAt(1)!!.select() + viewPager2.setCurrentItem(1, false) + } + } + else -> { + tabLayout.selectTab(tabLayout.getTabAt(2)) + viewPager2.postDelayed( { + viewPager2.setCurrentItem(2, false) + }, 200) + } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt index 0694d2b2..91bdfe8f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadAudioFragment.kt @@ -226,26 +226,30 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private val chosenFormat = downloadItem.format UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null) val listener = object : OnFormatClickListener { - override fun onFormatClick(item: List) { - item.first().format?.apply { + override fun onFormatClick(formatTuple: FormatTuple) { + formatTuple.format?.apply { downloadItem.format = this UiUtil.populateFormatCard(requireContext(), formatCard, this, null) } } - override fun onFormatsUpdated(allFormats: List>) { + override fun onFormatsUpdated(allFormats: List) { lifecycleScope.launch(Dispatchers.IO) { resultItem?.apply { this.formats.removeAll(formats.toSet()) - this.formats.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) }) + this.formats.addAll(allFormats.filter { !genericAudioFormats.contains(it) }) resultViewModel.update(this) kotlin.runCatching { val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment f1.updateUI(this) } } + + currentDownloadItem?.apply { + downloadViewModel.updateDownloadItemFormats(this.id, allFormats.filter { !genericAudioFormats.contains(it) }) + } } - formats = allFormats.first().filter { !genericAudioFormats.contains(it) }.toMutableList() + formats = allFormats.filter { !genericAudioFormats.contains(it) }.toMutableList() formats.removeAll(genericAudioFormats) val preferredFormat = downloadViewModel.getFormat(formats, Type.audio) downloadItem.format = preferredFormat @@ -255,7 +259,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private } formatCard.setOnClickListener{ if (parentFragmentManager.findFragmentByTag("formatSheet") == null){ - val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listOf(formats.ifEmpty { genericAudioFormats }), listener) + val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listener) bottomSheet.show(parentFragmentManager, "formatSheet") } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt index 073f7940..bec8a4cf 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt @@ -184,7 +184,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { //check if the item has formats and its audio-only val formats = result.formats - val isAudioOnly = formats.isNotEmpty() && formats.none { !it.format_note.contains("audio") } + var isAudioOnly = formats.isNotEmpty() && formats.none { !it.format_note.contains("audio") } if (isAudioOnly){ (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f @@ -216,26 +216,28 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { viewPager2.adapter = fragmentAdapter viewPager2.isSaveFromParentEnabled = false - when(type) { - Type.audio -> { - tabLayout.getTabAt(0)!!.select() - viewPager2.setCurrentItem(0, false) - } - Type.video -> { - if (isAudioOnly){ + view.post { + when(type) { + Type.audio -> { tabLayout.getTabAt(0)!!.select() viewPager2.setCurrentItem(0, false) - Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show() - }else{ - tabLayout.getTabAt(1)!!.select() - viewPager2.setCurrentItem(1, false) } - } - else -> { - tabLayout.getTabAt(2)!!.select() - viewPager2.postDelayed( { - viewPager2.setCurrentItem(2, false) - }, 200) + Type.video -> { + if (isAudioOnly){ + tabLayout.getTabAt(0)!!.select() + viewPager2.setCurrentItem(0, false) + Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show() + }else{ + tabLayout.getTabAt(1)!!.select() + viewPager2.setCurrentItem(1, false) + } + } + else -> { + tabLayout.getTabAt(2)!!.select() + viewPager2.postDelayed( { + viewPager2.setCurrentItem(2, false) + }, 200) + } } } @@ -592,6 +594,15 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { resultViewModel.updateFormatsResultData.collectLatest { formats -> if (formats == null) return@collectLatest kotlin.runCatching { + isAudioOnly = formats.isNotEmpty() && formats.none { !it.format_note.contains("audio") } + if (isAudioOnly){ + (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true + (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f + Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show() + tabLayout.getTabAt(0)!!.select() + viewPager2.setCurrentItem(0, false) + } + lifecycleScope.launch { withContext(Dispatchers.Main){ runCatching { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt index 1d9e2ccb..1a91a1c4 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt @@ -201,9 +201,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure toggleLoading(true) lifecycleScope.launch { withContext(Dispatchers.IO){ - downloadViewModel.updateProcessingDownloadTime(cal.timeInMillis) downloadViewModel.deleteAllWithID(currentDownloadIDs) - downloadViewModel.queueProcessingDownloads() + downloadViewModel.updateProcessingDownloadTimeAndQueueScheduled(cal.timeInMillis) } dismiss() } @@ -231,10 +230,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure downloadViewModel.deleteAllWithID(currentDownloadIDs) downloadViewModel.moveProcessingToSavedCategory() } - getProcessingItemsData()?.apply { - this.job?.cancel(CancellationException()) - this.job = null - } + + downloadViewModel.processingItemsJob?.cancel(CancellationException()) + downloadViewModel.processingItemsJob = null + dismiss() } } @@ -242,29 +241,27 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure true } - val formatListener = object : OnFormatClickListener { - override fun onFormatClick(selectedFormats: List) { - CoroutineScope(Dispatchers.IO).launch { - downloadViewModel.updateProcessingFormat(selectedFormats) - } + val formatListener = object : OnMultipleFormatClickListener { + override fun onFormatClick(formatTuple: List) { + downloadViewModel.updateAllProcessingFormats(formatTuple) } - override fun onFormatsUpdated(allFormats: List>) { - CoroutineScope(Dispatchers.IO).launch { - downloadViewModel.updateProcessingAllFormats(allFormats) - } + override fun onFormatUpdated(url: String, formats: List) { + downloadViewModel.updateProcessingFormatByUrl(url, formats) } + override fun onItemUnavailable(url: String) { + downloadViewModel.removeUnavailableDownloadAndResultByURL(url) + } override fun onContinueOnBackground() { requireActivity().lifecycleScope.launch { withContext(Dispatchers.IO){ downloadViewModel.continueUpdatingFormatsOnBackground() } - getProcessingItemsData()?.apply { - this.job?.cancel(CancellationException()) - this.job = null - } + downloadViewModel.processingItemsJob?.cancel(CancellationException()) + downloadViewModel.processingItemsJob = null + dismiss() } } @@ -421,20 +418,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure val items = withContext(Dispatchers.IO){ downloadViewModel.getProcessingDownloads() } - val flatFormatCollection = items.map { it.allFormats }.flatten() - val commonFormats = withContext(Dispatchers.IO){ - flatFormatCollection.groupingBy { it.format_id }.eachCount().filter { it.value == items.size }.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }.map { it.value } - } - - val formats = if (commonFormats.isNotEmpty() && items.none{it.allFormats.isEmpty()}) { - items.map { it.allFormats } - }else{ - when(items.first().type){ - DownloadViewModel.Type.audio -> listOf>(infoUtil.getGenericAudioFormats(requireContext().resources)) - else -> listOf>(infoUtil.getGenericVideoFormats(requireContext().resources)) - } - } - val bottomSheet = FormatSelectionBottomSheetDialog(items, formats, formatListener) + val bottomSheet = FormatSelectionBottomSheetDialog(items, _multipleFormatsListener = formatListener) bottomSheet.show(parentFragmentManager, "formatSheet") } } @@ -661,10 +645,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading } } - private fun toggleLoadingShimmerTitle(show: Boolean) { - - } - private fun updateFileSize(items: List){ if (items.all { it > 5L }){ val size = FileUtil.convertFileSize(items.sum()) @@ -822,12 +802,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure override fun onDismiss(dialog: DialogInterface) { lifecycleScope.launch { withContext(Dispatchers.IO){ - getProcessingItemsData()?.apply { - if (this.job?.isActive == true){ - this.job?.cancel(CancellationException()) - downloadViewModel.deleteAllWithID(this.processingDownloadItemIDs) - } - } + downloadViewModel.processingItemsJob?.cancel(CancellationException()) + downloadViewModel.processingItemsJob = null downloadViewModel.deleteProcessing() } } @@ -912,8 +888,5 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure } } - private fun getProcessingItemsData() : DownloadViewModel.ProcessingItemsJob? { - return downloadViewModel.processingItemsFlow - } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadVideoFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadVideoFragment.kt index 78c0f99d..4971d61d 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadVideoFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadVideoFragment.kt @@ -60,7 +60,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private private lateinit var saveDir : TextInputLayout private lateinit var freeSpace : TextView private lateinit var infoUtil: InfoUtil + private lateinit var genericVideoFormats: MutableList + private lateinit var genericAudioFormats: MutableList lateinit var downloadItem: DownloadItem @@ -77,6 +79,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] infoUtil = InfoUtil(requireContext()) genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources) + genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList() return fragmentView @@ -234,34 +237,38 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private val chosenFormat = downloadItem.format UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) }) val listener = object : OnFormatClickListener { - override fun onFormatClick(item: List) { - item.first().format?.apply { + override fun onFormatClick(formatTuple: FormatTuple) { + formatTuple.format?.apply { downloadItem.format = this } downloadItem.videoPreferences.audioFormatIDs.clear() - item.first().audioFormats?.map { it.format_id }?.let { + formatTuple.audioFormats?.map { it.format_id }?.let { downloadItem.videoPreferences.audioFormatIDs.addAll(it) } UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, - if(downloadItem.videoPreferences.removeAudio) listOf() else item.first().audioFormats + if(downloadItem.videoPreferences.removeAudio) listOf() else formatTuple.audioFormats ) } - override fun onFormatsUpdated(allFormats: List>) { + override fun onFormatsUpdated(allFormats: List) { lifecycleScope.launch { withContext(Dispatchers.IO){ resultItem?.apply { this.formats.removeAll(formats) - this.formats.addAll(allFormats.first().filter { !genericVideoFormats.contains(it) }) + this.formats.addAll(allFormats.filter { !genericVideoFormats.contains(it) }) resultViewModel.update(this) kotlin.runCatching { val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment f1.updateUI(this) } } + + currentDownloadItem?.apply { + downloadViewModel.updateDownloadItemFormats(this.id, allFormats.filter { !genericVideoFormats.contains(it) }) + } } } - formats = allFormats.first().filter { !genericVideoFormats.contains(it) }.toMutableList() + formats = allFormats.filter { !genericVideoFormats.contains(it) }.toMutableList() val preferredFormat = downloadViewModel.getFormat(formats, Type.video) val preferredAudioFormats = downloadViewModel.getPreferredAudioFormats(formats) downloadItem.format = preferredFormat @@ -274,7 +281,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private } formatCard.setOnClickListener{ if (parentFragmentManager.findFragmentByTag("formatSheet") == null){ - val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listOf(formats.ifEmpty { genericVideoFormats }), listener) + val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listener) bottomSheet.show(parentFragmentManager, "formatSheet") } } @@ -413,6 +420,10 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private @SuppressLint("RestrictedApi") fun updateSelectedAudioFormat(format: Format){ + if (genericAudioFormats.contains(format)) { + return + } + downloadItem.videoPreferences.audioFormatIDs.clear() downloadItem.videoPreferences.audioFormatIDs.addAll(arrayListOf(format.format_id)) val formatCard = requireView().findViewById(R.id.format_card_constraintLayout) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadsAlreadyExistDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadsAlreadyExistDialog.kt index fd08a052..6d9cd033 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadsAlreadyExistDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadsAlreadyExistDialog.kt @@ -18,9 +18,9 @@ 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.database.viewmodel.DownloadViewModel.AlreadyExistsIDs import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel -import com.deniscerri.ytdl.database.viewmodel.SharedDownloadViewModel.AlreadyExistsIDs import com.deniscerri.ytdl.ui.adapter.AlreadyExistsAdapter import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.UiUtil diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index 7ff5d852..a85ca85c 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -7,8 +7,10 @@ import android.os.Bundle import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup import android.view.Window import android.widget.* +import androidx.compose.foundation.layout.PaddingValues import androidx.core.view.children import androidx.core.view.forEach import androidx.core.view.isVisible @@ -21,6 +23,7 @@ import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdl.database.viewmodel.ResultViewModel +import com.deniscerri.ytdl.util.Extensions.isYoutubeURL import com.deniscerri.ytdl.util.FormatSorter import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.UiUtil @@ -31,46 +34,57 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.card.MaterialCardView import com.google.android.material.chip.Chip import com.google.android.material.snackbar.Snackbar +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import okhttp3.internal.format import java.util.regex.Pattern -class FormatSelectionBottomSheetDialog(private val _items: List? = null, private var _formats: List>? = null, private val _listener: OnFormatClickListener? = null) : BottomSheetDialogFragment() { +class FormatSelectionBottomSheetDialog( + private val _items: List? = null, + private val _listener: OnFormatClickListener? = null, + private val _multipleFormatsListener: OnMultipleFormatClickListener? = null +) : BottomSheetDialogFragment() { + private lateinit var behavior: BottomSheetBehavior private lateinit var infoUtil: InfoUtil + private lateinit var view: View + private lateinit var continueInBackgroundSnackBar : Snackbar private lateinit var downloadViewModel: DownloadViewModel private lateinit var sharedPreferences: SharedPreferences - private lateinit var formatCollection: MutableList> - private lateinit var chosenFormats: List - private var selectedVideo : Format? = null - private lateinit var selectedAudios : MutableList - private lateinit var videoFormatList : LinearLayout private lateinit var audioFormatList : LinearLayout private lateinit var okBtn : Button + private lateinit var refreshBtn: Button private lateinit var videoTitle : TextView private lateinit var audioTitle : TextView + private lateinit var chosenFormats: List + private var selectedVideo : Format? = null + private lateinit var selectedAudios : MutableList + private lateinit var sortBy : FormatSorting private lateinit var filterBy : FormatCategory private lateinit var filterBtn : Button - private lateinit var continueInBackgroundSnackBar : Snackbar - private lateinit var view: View private var updateFormatsJob: Job? = null + private var isMissingFormats: Boolean = false - private var hasGenericFormats: Boolean = false - - private lateinit var items: List - private lateinit var formats: List> + private lateinit var items: MutableList + private lateinit var formats: MutableList private lateinit var listener: OnFormatClickListener + private lateinit var multipleFormatsListener: OnMultipleFormatClickListener private var currentFormatSource : String? = null + private lateinit var genericAudioFormats : List + private lateinit var genericVideoFormats : List + enum class FormatSorting { filesize, container, codec, id } @@ -82,7 +96,6 @@ class FormatSelectionBottomSheetDialog(private val _items: List? override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) infoUtil = InfoUtil(requireActivity().applicationContext) - formatCollection = mutableListOf() chosenFormats = listOf() selectedAudios = mutableListOf() sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) @@ -101,9 +114,27 @@ class FormatSelectionBottomSheetDialog(private val _items: List? return } - items = _items - formats = _formats!! - listener = _listener!! + items = _items.distinctBy { it!!.url }.toMutableList() + if (items.size == 1) { + formats = items.first()!!.allFormats + }else{ + val flatFormatCollection = items.map { it!!.allFormats }.flatten() + formats = flatFormatCollection.groupingBy { it.format_id }.eachCount() + .filter { it.value == items.size } + .mapValues { flatFormatCollection.first { f -> f.format_id == it.key } } + .map { it.value }.toMutableList() + } + + _listener?.apply { + listener = this + } + + _multipleFormatsListener?.apply { + multipleFormatsListener = this + } + + genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources) + genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources) sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!) filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!) @@ -126,32 +157,25 @@ class FormatSelectionBottomSheetDialog(private val _items: List? okBtn = view.findViewById(R.id.format_ok) shimmers.visibility = View.GONE - hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" || it.format_id == "ba" } - filterBtn.isVisible = !hasGenericFormats + isMissingFormats = formats.isEmpty() && items.any { it!!.allFormats.isEmpty() } if (items.size > 1){ - - if (!hasGenericFormats){ - formatCollection.addAll(formats) - val flattenFormats = formats.flatten() - val commonFormats = flattenFormats.groupingBy { it.format_id }.eachCount().filter { it.value == items.size }.mapValues { flattenFormats.first { f -> f.format_id == it.key } }.map { it.value } - chosenFormats = commonFormats.mapTo(mutableListOf()) {it.copy()} + if (!isMissingFormats){ + chosenFormats = formats.mapTo(mutableListOf()) {it.copy()} chosenFormats = when(items.first()?.type){ Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } else -> chosenFormats } chosenFormats.forEach { - it.filesize = - flattenFormats.filter { f -> f.format_id == it.format_id } - .sumOf { itt -> itt.filesize } + it.filesize = items.map { itm -> itm!!.allFormats }.flatten().filter { f -> f.format_id == it.format_id }.sumOf { itt -> itt.filesize } } }else{ - chosenFormats = formats.flatten() + chosenFormats = formats } addFormatsToView() }else{ - chosenFormats = formats.flatten() - if(!hasGenericFormats){ + chosenFormats = formats + if(!isMissingFormats){ if(items.first()?.type == Type.audio){ chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) } } @@ -159,41 +183,22 @@ class FormatSelectionBottomSheetDialog(private val _items: List? addFormatsToView() } - val refreshBtn = view.findViewById