diff --git a/.idea/misc.xml b/.idea/misc.xml index f58d8de1..34572e74 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,4 @@ - + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index b4b2222b..09936bfe 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,15 +2,15 @@ plugins { id 'com.android.application' id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' id 'org.jetbrains.kotlin.android' - id 'kotlin-kapt' + id 'com.google.devtools.ksp' id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10" } def properties = new Properties() def versionMajor = 1 def versionMinor = 6 -def versionPatch = 3 -def versionBuild = 17 // bump for dogfood builds, public betas, etc. +def versionPatch = 5 +def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionExt = "" if (versionBuild > 0){ @@ -49,10 +49,8 @@ android { useSupportLibrary true } - kapt { - arguments { - arg("room.schemaLocation", "$projectDir/schemas") - } + ksp { + arg("room.schemaLocation", "$projectDir/schemas") } } @@ -76,6 +74,8 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' debuggable false signingConfig signingConfigs.debug + applicationIdSuffix '.beta' + versionNameSuffix '-Beta' } debug { @@ -98,7 +98,7 @@ android { disable 'MissingTranslation' } - android.applicationVariants.all { variant -> + android.applicationVariants.configureEach { variant -> variant.outputs.each { output -> def baseAbiVersionCode = project.abiCodes.get(output.getFilter(com.android.build.OutputFile.ABI)) if (baseAbiVersionCode != null) { @@ -149,7 +149,7 @@ dependencies { implementation 'com.google.android.material:material:1.9.0-rc01' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.recyclerview:recyclerview:1.3.1' - implementation 'androidx.preference:preference:1.2.1' + 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.10.1' @@ -174,16 +174,18 @@ dependencies { implementation "androidx.room:room-runtime:2.5.2" implementation "androidx.room:room-ktx:2.5.2" - kapt "androidx.room:room-compiler:2.5.2" + ksp "androidx.room:room-compiler:2.5.2" + implementation 'androidx.paging:paging-runtime-ktx:3.2.0' + implementation "androidx.room:room-paging:2.5.2" androidTestImplementation "androidx.room:room-testing:2.5.2" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' implementation "androidx.compose.runtime:runtime:$composeVer" - androidTestImplementation "com.google.truth:truth:1.1.3" + androidTestImplementation 'com.google.truth:truth:1.1.5' api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVer" api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutineVer" - implementation 'com.google.code.gson:gson:2.9.1' + implementation 'com.google.code.gson:gson:2.10.1' implementation "com.squareup.okhttp3:okhttp:5.0.0-alpha.10" implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' @@ -200,7 +202,7 @@ dependencies { implementation("androidx.media3:media3-ui:$media3_version") // For RTSP playback support with ExoPlayer implementation("androidx.media3:media3-exoplayer-rtsp:$media3_version") - implementation("androidx.media3:media3-datasource-cronet:$media3_version") implementation "com.anggrayudi:storage:1.5.5" + implementation 'me.zhanghai.android.fastscroll:library:1.3.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 91a929f8..94e2d12e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,17 @@ + + + + + + + + + + + @@ -204,7 +217,7 @@ @@ -212,18 +225,6 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/provider_paths" /> - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/App.kt b/app/src/main/java/com/deniscerri/ytdlnis/App.kt index 958013c7..1fbf7f9a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/App.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/App.kt @@ -9,14 +9,11 @@ import androidx.work.Configuration import androidx.work.Constraints import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.NetworkType -import androidx.work.PeriodicWorkRequest import androidx.work.PeriodicWorkRequestBuilder import androidx.work.WorkManager -import androidx.work.WorkQuery import com.deniscerri.ytdlnis.util.NotificationUtil import com.deniscerri.ytdlnis.util.UpdateUtil import com.deniscerri.ytdlnis.work.UpdateYTDLWorker -import com.google.android.gms.common.internal.Constants import com.yausername.aria2c.Aria2c import com.yausername.ffmpeg.FFmpeg import com.yausername.youtubedl_android.YoutubeDL @@ -50,21 +47,27 @@ class App : Application() { } } - //init yt-dlp auto update with work request - val constraints = Constraints.Builder() - .setRequiredNetworkType(NetworkType.UNMETERED) - .setRequiresDeviceIdle(true) - .build() + if (sharedPreferences.getBoolean("auto_update_ytdlp", true)){ + //init yt-dlp auto update with work request + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresDeviceIdle(true) + .build() + + val periodicWorkRequest = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) + .setConstraints(constraints) + .build() + + WorkManager.getInstance(this@App).enqueueUniquePeriodicWork( + "ytdlp-Updater", + ExistingPeriodicWorkPolicy.KEEP, + periodicWorkRequest + ) + }else{ + WorkManager.getInstance(this@App).cancelUniqueWork("ytdlp-Updater") + } - val periodicWorkRequest = PeriodicWorkRequestBuilder(1, TimeUnit.DAYS) - .setConstraints(constraints) - .build() - WorkManager.getInstance(this@App).enqueueUniquePeriodicWork( - "ytdlp-Updater", - ExistingPeriodicWorkPolicy.KEEP, - periodicWorkRequest - ) }catch (e: Exception){ Looper.prepare().runCatching { @@ -73,14 +76,6 @@ class App : Application() { e.printStackTrace() } } - - WorkManager.initialize( - this@App, - Configuration.Builder() - .setExecutor(Executors.newFixedThreadPool( - sharedPreferences.getInt("concurrent_downloads", 1))) - .build()) - } @Throws(YoutubeDLException::class) private fun initLibraries() { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt index c7deb943..7ef835dc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt @@ -5,6 +5,7 @@ import android.content.SharedPreferences import android.graphics.Color import android.os.Handler import android.os.Looper +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -106,14 +107,14 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: } val formatDetailsChip = card.findViewById(R.id.format_note) - val formatDetailsText = StringBuilder(item.format.format_note.uppercase()) - formatDetailsText.append(" \t •\t ${item.container.uppercase().ifEmpty { item.format.container.uppercase() }}") + val sideDetails = mutableListOf() + sideDetails.add(item.format.format_note.uppercase()) + sideDetails.add(item.container.uppercase().ifEmpty { item.format.container.uppercase() }) val fileSize = FileUtil.convertFileSize(item.format.filesize) - if (fileSize != "?") formatDetailsText.append(" \t •\t $fileSize") - - formatDetailsChip.text = formatDetailsText + if (fileSize != "?") sideDetails.add(fileSize) + formatDetailsChip.text = sideDetails.joinToString(" · ") //OUTPUT val output = card.findViewById(R.id.output) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/GenericDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/GenericDownloadAdapter.kt index 6e171ea0..1071d67f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/GenericDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/GenericDownloadAdapter.kt @@ -3,7 +3,6 @@ package com.deniscerri.ytdlnis.adapter import android.annotation.SuppressLint import android.app.Activity import android.content.SharedPreferences -import android.graphics.Color import android.os.Handler import android.os.Looper import android.view.LayoutInflater @@ -11,10 +10,10 @@ import android.view.View import android.view.ViewGroup 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.ytdlnis.R import com.deniscerri.ytdlnis.database.models.DownloadItem @@ -24,16 +23,18 @@ import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.squareup.picasso.Picasso -class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) { +class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : PagingDataAdapter(DIFF_CALLBACK) { private val onItemClickListener: OnItemClickListener private val activity: Activity - private val checkedItems: ArrayList + val checkedItems: ArrayList + var inverted: Boolean private val sharedPreferences: SharedPreferences init { checkedItems = ArrayList() this.onItemClickListener = onItemClickListener this.activity = activity + this.inverted = false sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity) } @@ -53,13 +54,16 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: override fun onBindViewHolder(holder: ViewHolder, position: Int) { val item = getItem(position) val card = holder.cardView + card.isVisible = item != null + if (item == null) return + card.tag = item.id.toString() val uiHandler = Handler(Looper.getMainLooper()) val thumbnail = card.findViewById(R.id.downloads_image_view) // THUMBNAIL ---------------------------------- if (!sharedPreferences.getStringSet("hide_thumbnails", emptySet())!!.contains("queue")){ - val imageURL = item!!.thumb + val imageURL = item.thumb if (imageURL.isNotEmpty()) { uiHandler.post { Picasso.get().load(imageURL).into(thumbnail) } } else { @@ -127,7 +131,7 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: actionButton.setOnClickListener { onItemClickListener.onActionButtonClick(item.id) } - if (checkedItems.contains(item.id)) { + if ((checkedItems.contains(item.id) && !inverted) || (!checkedItems.contains(item.id) && inverted)) { card.isChecked = true card.strokeWidth = 5 } else { @@ -135,67 +139,69 @@ class GenericDownloadAdapter(onItemClickListener: OnItemClickListener, activity: card.strokeWidth = 0 } card.setOnClickListener { - if (checkedItems.size > 0) { - checkCard(card, item.id) + if (checkedItems.size > 0 || inverted) { + checkCard(card, item.id, position) } else { onItemClickListener.onCardClick(item.id) } } card.setOnLongClickListener { - checkCard(card, item.id) + checkCard(card, item.id, position) true } } @SuppressLint("NotifyDataSetChanged") - fun clearCheckeditems() { - for (i in 0 until itemCount){ - val item = getItem(i) - if (checkedItems.find { it == item?.id } != null){ - checkedItems.remove(item?.id) - notifyItemChanged(i) - } - } - + fun clearCheckedItems() { + inverted = false checkedItems.clear() - } - - @SuppressLint("NotifyDataSetChanged") - fun checkAll(items: List?){ - checkedItems.clear() - checkedItems.addAll(items!!.map { it!!.id }) notifyDataSetChanged() } @SuppressLint("NotifyDataSetChanged") - fun invertSelected(items: List?){ - val invertedList = mutableListOf() - items?.forEach { - if (!checkedItems.contains(it!!.id)) invertedList.add(it.id) - } + fun checkAll() { checkedItems.clear() - checkedItems.addAll(invertedList) + inverted = true notifyDataSetChanged() } - private fun checkCard(card: MaterialCardView, itemID: Long) { + @SuppressLint("NotifyDataSetChanged") + fun invertSelected() { + inverted = !inverted + notifyDataSetChanged() + } + + fun getSelectedObjectsCount(totalSize: Int) : Int{ + return if (inverted){ + totalSize - checkedItems.size + }else{ + checkedItems.size + } + } + + + + private fun checkCard(card: MaterialCardView, itemID: Long, position: Int) { if (card.isChecked) { card.strokeWidth = 0 - checkedItems.remove(itemID) + if (inverted) checkedItems.add(itemID) + else checkedItems.remove(itemID) } else { card.strokeWidth = 5 - checkedItems.add(itemID) + if (inverted) checkedItems.remove(itemID) + else checkedItems.add(itemID) } + card.isChecked = !card.isChecked - onItemClickListener.onCardSelect(itemID, card.isChecked) + onItemClickListener.onCardSelect(card.isChecked, position) } interface OnItemClickListener { fun onActionButtonClick(itemID: Long) fun onCardClick(itemID: Long) - fun onCardSelect(itemID: Long, isChecked: Boolean) + fun onCardSelect(isChecked: Boolean, position: Int) } companion object { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt index b676fd0f..76719fce 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt @@ -21,6 +21,7 @@ import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.ResultItem import com.squareup.picasso.Picasso +import org.w3c.dom.Text import java.util.* @@ -70,17 +71,9 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi uiHandler.post { Picasso.get().load(R.color.black).into(thumbnail) } } - // TITLE ---------------------------------- - val itemTitle = card.findViewById(R.id.title) - var title = item!!.title - if (title.length > 100) { - title = title.substring(0, 40) + "..." - } - itemTitle.text = title - - - val duration = card.findViewById(R.id.duration) - duration.text = item.duration + card.findViewById(R.id.title).text = item!!.title + card.findViewById(R.id.author).text = item.author + card.findViewById(R.id.duration).text = item.duration // CHECKBOX ---------------------------------- val check = card.findViewById(R.id.checkBox) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt index f4850317..5537c399 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/Converters.kt @@ -13,16 +13,9 @@ import java.lang.reflect.Type class Converters { @TypeConverter - fun stringToListOfFormats(value: String?): ArrayList { - val listType: Type = object : TypeToken?>() {}.type - return Gson().fromJson(value, listType) - } - + fun stringToListOfFormats(value: String) = Gson().fromJson(value, Array::class.java).toMutableList() @TypeConverter - fun listOfFormatsToString(list: ArrayList?): String { - val gson = Gson() - return gson.toJson(list) - } + fun listOfFormatsToString(list: List?) = Gson().toJson(list).toString() @TypeConverter fun formatToString(format: Format): String = Gson().toJson(format) @@ -53,13 +46,10 @@ class Converters { fun stringToVideoPreferences(string: String): VideoPreferences = Gson().fromJson(string, VideoPreferences::class.java) @TypeConverter - fun stringToListOfChapters(value: String?): ArrayList? { - val listType: Type = object : TypeToken?>() {}.type - return Gson().fromJson(value, listType) - } + fun stringToListOfChapters(value: String?) = Gson().fromJson(value, Array::class.java).toMutableList() @TypeConverter - fun listOfChaptersToString(list: ArrayList?): String { + fun listOfChaptersToString(list: List?): String { val gson = Gson() return gson.toJson(list) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt index ac349fc0..70357a75 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/DBManager.kt @@ -1,9 +1,14 @@ package com.deniscerri.ytdlnis.database import android.content.Context +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.PagingSource import androidx.room.* import com.deniscerri.ytdlnis.database.dao.* import com.deniscerri.ytdlnis.database.models.* +import kotlinx.coroutines.flow.Flow @TypeConverters(Converters::class) @Database( diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt index 15550964..c8fdeb4f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt @@ -1,20 +1,26 @@ package com.deniscerri.ytdlnis.database.dao +import androidx.paging.PagingSource import androidx.room.* import com.deniscerri.ytdlnis.database.models.DownloadItem +import com.deniscerri.ytdlnis.database.models.Format +import com.deniscerri.ytdlnis.database.repository.DownloadRepository import kotlinx.coroutines.flow.Flow @Dao interface DownloadDao { @Query("SELECT * FROM downloads ORDER BY status") - fun getAllDownloads() : Flow> + fun getAllDownloads() : PagingSource @Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'") fun getActiveDownloads() : Flow> @Query("SELECT COUNT(*) FROM downloads WHERE status='Active' or status='Paused'") - fun getActiveDownloadsCount() : Flow + fun getActiveDownloadsCountFlow() : Flow + + @Query("SELECT COUNT(*) FROM downloads WHERE status in (:status)") + fun getDownloadsCountByStatus(status : List) : Flow @Query("SELECT * FROM downloads WHERE status='Active' or status='Paused'") fun getActiveDownloadsList() : List @@ -22,26 +28,41 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") fun getActiveAndQueuedDownloadsList() : List + @Query("SELECT * FROM downloads WHERE status='Active' or status='Queued' or status='QueuedPaused' or status='Paused'") + fun getActiveAndQueuedDownloads() : Flow> + @Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' ORDER BY downloadStartTime, id") - fun getQueuedDownloads() : Flow> + fun getQueuedDownloads() : PagingSource + + @Query("SELECT format FROM downloads WHERE status='Queued' or status='QueuedPaused'") + fun getSelectedFormatFromQueued() : List + + @Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status='Queued' ORDER BY downloadStartTime, id LIMIT 20") + fun getQueuedDownloadsThatAreNotScheduledChunked(currentTime: Long) : Flow> + + @Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' or status='Paused' ORDER BY downloadStartTime, id") + fun getQueuedAndPausedDownloads() : Flow> @Query("SELECT * FROM downloads WHERE status='Queued' or status='QueuedPaused' ORDER BY downloadStartTime, id") fun getQueuedDownloadsList() : List @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") - fun getCancelledDownloads() : Flow> + fun getCancelledDownloads() : PagingSource @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") fun getCancelledDownloadsList() : List + @Query("SELECT * FROM downloads WHERE status LIKE '%Paused%'") + fun getPausedDownloadsList() : List + @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") - fun getErroredDownloads() : Flow> + fun getErroredDownloads() : PagingSource @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") fun getErroredDownloadsList() : List @Query("SELECT * FROM downloads WHERE status='Saved' ORDER BY id DESC") - fun getSavedDownloads() : Flow> + fun getSavedDownloads() : PagingSource @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") fun getDownloadById(id: Long) : DownloadItem @@ -52,6 +73,22 @@ interface DownloadDao { @Query("SELECT status FROM downloads WHERE id=:id") fun checkStatus(id: Long) : String + @Query("UPDATE downloads " + + "SET status = CASE " + + " WHEN status = 'Active' THEN 'Paused' " + + " WHEN status = 'Queued' THEN 'QueuedPaused' " + + " ELSE status " + + " END;") + fun pauseActiveAndQueued() + + @Query("UPDATE downloads " + + "SET status = CASE " + + " WHEN status = 'Paused' THEN 'Active' " + + " WHEN status = 'QueuedPaused' THEN 'Queued' " + + " ELSE status " + + " END;") + fun unPauseActiveAndQueued() + @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insert(item: DownloadItem) : Long @@ -73,7 +110,7 @@ interface DownloadDao { @Query("DELETE FROM downloads WHERE status='Saved'") suspend fun deleteSaved() - @Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued'") + @Query("UPDATE downloads SET status='Cancelled' WHERE status='Queued' or status='QueuedPaused'") suspend fun cancelQueued() @Query("DELETE FROM downloads WHERE status='Processing' AND id=:id") @@ -82,6 +119,9 @@ interface DownloadDao { @Upsert suspend fun update(item: DownloadItem) + @Upsert + fun updateMultiple(items :List) + @Query("SELECT * FROM downloads ORDER BY id DESC LIMIT 1") fun getLatest() : DownloadItem @@ -94,4 +134,38 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE url=:url AND format=:format AND (status='Error' OR status='Cancelled') LIMIT 1") fun getUnfinishedByURLAndFormat(url: String, format: String) : DownloadItem + + + @Query("SELECT COUNT(downloadStartTime) = 0 from downloads WHERE id in (:items) AND " + + "CASE :inverted " + + "WHEN :inverted = 'false' THEN downloadStartTime > :currentStartTime " + + "WHEN :inverted = 'true' THEN downloadStartTime < :currentStartTime ELSE downloadStartTime < -1 END") + fun checkAllQueuedItemsAreScheduledAfterNow(items: List, inverted: String, currentStartTime: Long) : Boolean + + + @Query("Select id from downloads where id not in (:list) and status in (:status)") + fun getDownloadIDsNotPresentInList(list: List, status: List) : List + + @Query("UPDATE downloads SET downloadStartTime=0 where id in (:list)") + fun resetScheduleTimeForItems(list: List) + + @Query("Update downloads SET status='Queued', downloadStartTime = 0 WHERE id in (:list)") + fun reQueueDownloadItems(list: List) + + @Transaction + fun putAtTopOfTheQueue(id: Long){ + val downloads = getQueuedDownloadsList() + val newID = downloads.first().id + + updateDownloadID(id, -id) + resetScheduleTimeForItems(listOf(-id)) + + downloads.reversed().dropWhile { it.id == id }.forEach { + updateDownloadID(it.id, it.id + 1) + } + updateDownloadID(-id, newID) + } + + @Query("Update downloads set id=:newId where id=:id") + fun updateDownloadID(id: Long, newId: Long) } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/SearchHistoryDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/SearchHistoryDao.kt index e0e38f8a..94bd2056 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/SearchHistoryDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/SearchHistoryDao.kt @@ -12,7 +12,7 @@ interface SearchHistoryDao { fun getAllByKeyword(keyword: String) : List @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insert(new: SearchHistoryItem) + suspend fun insert(newItem: SearchHistoryItem) @Query("DELETE FROM searchHistory") suspend fun deleteAll() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt index 20e574d4..bc34fd41 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/DownloadItem.kt @@ -20,7 +20,7 @@ data class DownloadItem( var container: String, @ColumnInfo(defaultValue = "") var downloadSections: String, - val allFormats: ArrayList, + val allFormats: MutableList, var downloadPath: String, var website: String, val downloadSize: String, diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt index 8c44e532..4da30423 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/ResultItem.kt @@ -15,9 +15,9 @@ data class ResultItem( val thumb: String, val website: String, var playlistTitle: String, - var formats: ArrayList, + var formats: MutableList, @ColumnInfo(defaultValue = "") var urls: String, - var chapters: ArrayList?, + var chapters: MutableList?, var creationTime: Long = System.currentTimeMillis() / 1000, ) \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt index 87c695c7..206acc30 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt @@ -1,19 +1,36 @@ package com.deniscerri.ytdlnis.database.repository -import androidx.lifecycle.LiveData -import com.deniscerri.ytdlnis.database.Converters +import androidx.paging.Pager +import androidx.paging.PagingConfig import com.deniscerri.ytdlnis.database.dao.DownloadDao import com.deniscerri.ytdlnis.database.models.DownloadItem import kotlinx.coroutines.flow.Flow + class DownloadRepository(private val downloadDao: DownloadDao) { - val allDownloads : Flow> = downloadDao.getAllDownloads() + val allDownloads : Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getAllDownloads()} + ) val activeDownloads : Flow> = downloadDao.getActiveDownloads() - val activeDownloadsCount : Flow = downloadDao.getActiveDownloadsCount() - val queuedDownloads : Flow> = downloadDao.getQueuedDownloads() - val cancelledDownloads : Flow> = downloadDao.getCancelledDownloads() - val erroredDownloads : Flow> = downloadDao.getErroredDownloads() - val savedDownloads : Flow> = downloadDao.getSavedDownloads() + val queuedDownloads : Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getQueuedDownloads()} + ) + val cancelledDownloads : Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getCancelledDownloads()} + ) + val erroredDownloads : Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getErroredDownloads()} + ) + val savedDownloads : Pager = Pager( + config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1), + pagingSourceFactory = {downloadDao.getSavedDownloads()} + ) + + val activeDownloadsCount : Flow = downloadDao.getActiveDownloadsCountFlow() enum class Status { Active, Paused, Queued, QueuedPaused, Error, Cancelled, Saved @@ -27,8 +44,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.insertAll(items) } - suspend fun delete(item: DownloadItem){ - downloadDao.delete(item.id) + suspend fun delete(id: Long){ + downloadDao.delete(id) } suspend fun update(item: DownloadItem){ @@ -61,6 +78,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.getCancelledDownloadsList() } + fun getPausedDownloads() : List { + return downloadDao.getPausedDownloadsList() + } + fun getErroredDownloads() : List { return downloadDao.getErroredDownloadsList() } @@ -81,20 +102,12 @@ class DownloadRepository(private val downloadDao: DownloadDao) { downloadDao.cancelQueued() } - fun getLastDownloadId() : Long { - return downloadDao.getLastDownloadId() + fun pauseDownloads(){ + downloadDao.pauseActiveAndQueued() } - - fun checkIfReDownloadingErroredOrCancelled(item: DownloadItem) : Long { - val converters = Converters() - val format = converters.formatToString(item.format) - return try { - val i = downloadDao.getUnfinishedByURLAndFormat(item.url, format) - i.id - }catch (e: Exception){ - 0L - } + fun unPauseDownloads(){ + downloadDao.unPauseActiveAndQueued() } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt index 4d83af70..7e4b7cc8 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt @@ -12,23 +12,24 @@ import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope -import com.anggrayudi.storage.file.dataDirectory +import com.deniscerri.ytdlnis.BuildConfig import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.models.CookieItem import com.deniscerri.ytdlnis.database.repository.CookieRepository import com.deniscerri.ytdlnis.ui.more.WebViewActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.serialization.json.Json import java.io.File +import java.util.Date class CookieViewModel(private val application: Application) : AndroidViewModel(application) { private val repository: CookieRepository val items: LiveData> - private val jsonFormat = Json { prettyPrint = true } - private val cookieHeader = "# Netscape HTTP Cookie File\n" + - "# WebView Generated by the YTDLnis app\n" + private val cookieHeader = + "# Netscape HTTP Cookie File\n" + + "# WebView Generated by the YTDLnis app\n" + + "# This is a generated file! Do not edit." init { val dao = DBManager.getInstance(application).cookieDao @@ -72,8 +73,7 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a if (!hasCookies()) throw Exception("There is no cookies in the database!") flush() } - - val dbPath = File("/data/data/com.deniscerri.ytdl/").walkTopDown().find { it.name == "Cookies" } + val dbPath = File("/data/data/${BuildConfig.APPLICATION_ID}/").walkTopDown().find { it.name == "Cookies" } ?: throw Exception("Cookies File not found!") SQLiteDatabase.openDatabase( @@ -139,11 +139,12 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a application.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager var clip = clipboard.primaryClip!!.getItemAt(0).text Log.e("Aaa", clip.toString()) - if (clip.startsWith(cookieHeader)){ + + if (clip.startsWith("# Netscape HTTP Cookie File")){ clip = clip.removePrefix(cookieHeader) val cookie = CookieItem( 0, - "Cookie Import [${System.currentTimeMillis()}]", + "Cookie Import at [${Date()}]", clip.toString() ) insert(cookie) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt index 2145255e..66d54ad1 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt @@ -8,22 +8,20 @@ import android.content.res.Resources import android.net.ConnectivityManager import android.os.Looper import android.util.DisplayMetrics -import android.util.Log import android.widget.Toast import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData import androidx.preference.PreferenceManager import androidx.work.Constraints -import androidx.work.Data import androidx.work.ExistingWorkPolicy import androidx.work.NetworkType -import androidx.work.OneTimeWorkRequest import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkInfo import androidx.work.WorkManager -import androidx.work.WorkRequest -import androidx.work.workDataOf +import androidx.work.await import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.DBManager @@ -44,18 +42,12 @@ import com.deniscerri.ytdlnis.work.DownloadWorker import com.google.gson.Gson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext import java.io.File import java.util.Locale -import java.util.UUID import java.util.concurrent.TimeUnit -import kotlin.random.Random -import kotlin.random.nextInt class DownloadViewModel(application: Application) : AndroidViewModel(application) { @@ -64,13 +56,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application private val sharedPreferences: SharedPreferences private val commandTemplateDao: CommandTemplateDao private val infoUtil : InfoUtil - val allDownloads : LiveData> - val queuedDownloads : LiveData> + val allDownloads : Flow> + val queuedDownloads : Flow> val activeDownloads : LiveData> val activeDownloadsCount : LiveData - val cancelledDownloads : LiveData> - val erroredDownloads : LiveData> - val savedDownloads : LiveData> + val cancelledDownloads : Flow> + val erroredDownloads : Flow> + val savedDownloads : Flow> private var bestVideoFormat : Format private var bestAudioFormat : Format @@ -81,6 +73,11 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application private val audioFormatIDPreference: List private val resources : Resources private var extraCommands: String = "" + + private var audioContainer: String? + private var videoContainer: String? + private var videoCodec: String? + enum class Type { audio, video, command } @@ -93,13 +90,13 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application commandTemplateDao = DBManager.getInstance(application).commandTemplateDao infoUtil = InfoUtil(application) - allDownloads = repository.allDownloads.asLiveData() - queuedDownloads = repository.queuedDownloads.asLiveData() + allDownloads = repository.allDownloads.flow + queuedDownloads = repository.queuedDownloads.flow activeDownloads = repository.activeDownloads.asLiveData() activeDownloadsCount = repository.activeDownloadsCount.asLiveData() - savedDownloads = repository.savedDownloads.asLiveData() - cancelledDownloads = repository.cancelledDownloads.asLiveData() - erroredDownloads = repository.erroredDownloads.asLiveData() + savedDownloads = repository.savedDownloads.flow + cancelledDownloads = repository.cancelledDownloads.flow + erroredDownloads = repository.erroredDownloads.flow viewModelScope.launch(Dispatchers.IO){ if (sharedPreferences.getBoolean("use_extra_commands", false)){ extraCommands = commandTemplateDao.getAllTemplatesAsExtraCommands().joinToString(" ") @@ -107,8 +104,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString() - formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",") - audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",") + formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() } + audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() } val confTmp = Configuration(application.resources.configuration) confTmp.locale = Locale(sharedPreferences.getString("app_language", "en")!!) @@ -117,7 +114,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val videoFormatValues = resources.getStringArray(R.array.video_formats_values) - val videoContainer = sharedPreferences.getString("video_format", "Default") + videoContainer = sharedPreferences.getString("video_format", "Default") defaultVideoFormats = mutableListOf() videoFormatValues.forEach { @@ -132,29 +129,55 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application ) defaultVideoFormats.add(tmp) } + formatIDPreference.reversed().forEach { + val tmp = Format( + it, + videoContainer!!, + "", + "", + "", + 0, + it + ) + defaultVideoFormats.add(tmp) + } bestVideoFormat = defaultVideoFormats.last() - val audioContainer = sharedPreferences.getString("audio_format", "mp3") - bestAudioFormat = Format( - "best", - audioContainer!!, - "", - "", - "", - 0, - "best" - ) + audioContainer = sharedPreferences.getString("audio_format", "mp3") + bestAudioFormat = if (audioFormatIDPreference.isEmpty()){ + Format( + "best", + audioContainer!!, + "", + "", + "", + 0, + "best" + ) + }else{ + Format( + audioFormatIDPreference.first(), + audioContainer!!, + "", + "", + "", + 0, + audioFormatIDPreference.first() + ) + } + + videoCodec = sharedPreferences.getString("video_codec", "") } - fun deleteDownload(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO) { - repository.delete(item) + fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) { + repository.delete(id) } suspend fun updateDownload(item: DownloadItem){ if (sharedPreferences.getBoolean("incognito", false)){ if (item.status == DownloadRepository.Status.Cancelled.toString() || item.status == DownloadRepository.Status.Error.toString()){ - repository.delete(item) + repository.delete(item.id) } }else{ repository.update(item) @@ -345,7 +368,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return cloneFormat ( try { try{ - formats.first { it.format_note.contains("audio", ignoreCase = true) && audioFormatIDPreference.contains(it.format_id)} + formats.filter { it.format_note.contains("audio", ignoreCase = true)}.first { + audioFormatIDPreference.contains(it.format_id) || + it.container == audioContainer + } }catch (e: Exception){ formats.last { it.format_note.contains("audio", ignoreCase = true) } } @@ -358,24 +384,19 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application Type.video -> { return cloneFormat( try { - val theFormats = formats.ifEmpty { defaultVideoFormats } - try { - formats.first { !it.format_note.contains("audio", ignoreCase = true) && formatIDPreference.contains(it.format_id)} - }catch (e: Exception){ - when (videoQualityPreference) { - "worst" -> { - theFormats.first {!it.format_note.contains("audio", ignoreCase = true) } - } - "best" -> { - theFormats.last {!it.format_note.contains("audio", ignoreCase = true) } - } - else -> { - try{ - theFormats.last {it.format_note.contains(videoQualityPreference.substring(0, videoQualityPreference.length - 1)) } - }catch (e: Exception){ - theFormats.last { !it.format_note.contains("audio", ignoreCase = true) } - } - } + val theFormats = formats.filter { !it.format_note.contains("audio", true) }.ifEmpty { defaultVideoFormats } + when (videoQualityPreference) { + "worst" -> { + theFormats.first() + } + else /*best*/ -> { + val requirements: MutableList<(Format) -> Boolean> = mutableListOf() + requirements.add { it: Format -> formatIDPreference.contains(it.format_id) } + requirements.add { it: Format -> if (videoContainer == "mp4") it.container.equals("mpeg_4", true) else it.container.equals(videoContainer, true)} + requirements.add { it: Format -> it.format_note.contains(videoQualityPreference.substring(0, videoQualityPreference.length - 1)) } + requirements.add { it: Format -> it.vcodec.startsWith(videoCodec!!, true) } + + theFormats.reversed().maxByOrNull { f -> requirements.count{ req -> req(f)} } ?: throw Exception() } } }catch (e: Exception){ @@ -406,6 +427,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val audioFormats = resources.getStringArray(R.array.audio_formats) val formats = mutableListOf() val containerPreference = sharedPreferences.getString("audio_format", "") + audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) } audioFormats.forEach { formats.add(Format(it, containerPreference!!,"","", "",0, it)) } return formats } @@ -414,6 +436,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val videoFormats = resources.getStringArray(R.array.video_formats_values) val formats = mutableListOf() val containerPreference = sharedPreferences.getString("video_format", "") + formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"Default","", "",0, it)) } videoFormats.forEach { formats.add(Format(it, containerPreference!!,"Default","", "",0, it)) } return formats } @@ -436,6 +459,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application repository.insert(item) } + fun pauseDownloads() = viewModelScope.launch(Dispatchers.IO){ + repository.pauseDownloads() + } + + fun unPauseDownloads() = viewModelScope.launch(Dispatchers.IO){ + repository.unPauseDownloads() + } + fun insertAll(items: List)= viewModelScope.launch(Dispatchers.IO){ items.forEach{ repository.insert(it) @@ -466,6 +497,10 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return repository.getCancelledDownloads() } + fun getPausedDownloads() : List { + return repository.getPausedDownloads() + } + fun getErrored() : List { return repository.getErroredDownloads() } @@ -483,10 +518,24 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return repository.getActiveAndQueuedDownloads() } + suspend fun resetScheduleTimeForItemsAndStartDownload(items: List) = CoroutineScope(Dispatchers.IO).launch { + dbManager.downloadDao.resetScheduleTimeForItems(items) + startDownloadWorker(emptyList()) + } + + suspend fun putAtTopOfQueue(id: Long) = CoroutineScope(Dispatchers.IO).launch{ + dbManager.downloadDao.putAtTopOfTheQueue(id) + startDownloadWorker(emptyList()) + } + + suspend fun reQueueDownloadItems(items: List) = CoroutineScope(Dispatchers.IO).launch { + dbManager.downloadDao.reQueueDownloadItems(items) + startDownloadWorker(emptyList()) + } + suspend fun queueDownloads(items: List) = CoroutineScope(Dispatchers.IO).launch { val context = App.instance val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads() - val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) val queuedItems = mutableListOf() var exists = false @@ -518,33 +567,50 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } } + if (queuedItems.isNotEmpty()){ + startDownloadWorker(queuedItems) + } + } + + private suspend fun startDownloadWorker(queuedItems: List) { + val context = App.instance + val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) val workManager = WorkManager.getInstance(context) - queuedItems.forEach { + val currentWork = workManager.getWorkInfosByTag("download").await() + if (currentWork.size == 0 || currentWork.none{ it.state == WorkInfo.State.RUNNING } || (queuedItems.isNotEmpty() && queuedItems[0].downloadStartTime != 0L)){ + val currentTime = System.currentTimeMillis() - var delay = if (it.downloadStartTime != 0L){ - it.downloadStartTime - currentTime - } else 0 - if (delay < 0L) delay = 1L + var delay = 1000L + if (queuedItems.isNotEmpty()){ + delay = if (queuedItems[0].downloadStartTime != 0L){ + queuedItems[0].downloadStartTime.minus(currentTime) + } else 0 + if (delay <= 0L) delay = 1000L + } + val workConstraints = Constraints.Builder() if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED) val workRequest = OneTimeWorkRequestBuilder() - .setInputData(Data.Builder().putLong("id", it.id).build()) - .addTag(it.id.toString()) .addTag("download") .setConstraints(workConstraints.build()) - .setInitialDelay(delay, TimeUnit.SECONDS) - .build() + .setInitialDelay(delay, TimeUnit.MILLISECONDS) + + if (delay > 1000L){ + queuedItems.forEach { + workRequest.addTag(it.id.toString()) + } + } workManager.enqueueUniqueWork( - it.id.toString(), + System.currentTimeMillis().toString(), ExistingWorkPolicy.REPLACE, - workRequest + workRequest.build() ) - } + } val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered if (!allowMeteredNetworks && isCurrentNetworkMetered){ @@ -553,12 +619,29 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } } - items.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { + queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }?.forEach { try{ updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao) }catch (ignored: Exception){} } } + + fun getQueuedCollectedFileSize() : Long { + return dbManager.downloadDao.getSelectedFormatFromQueued().sumOf { it.filesize } + } + + fun getTotalSize(status: List) : LiveData { + return dbManager.downloadDao.getDownloadsCountByStatus(status.map { it.toString() }).asLiveData() + } + + fun checkAllQueuedItemsAreScheduledAfterNow(items: List, inverted: Boolean, currentStartTime: Long) : Boolean { + return dbManager.downloadDao.checkAllQueuedItemsAreScheduledAfterNow(items, inverted.toString(), currentStartTime) + } + + fun getItemIDsNotPresentIn(items: List, status: List) : List { + return dbManager.downloadDao.getDownloadIDsNotPresentInList(items.ifEmpty { listOf(-1L) }, status.map { it.toString() }) + } + private fun updateDownloadItem( downloadItem: DownloadItem, infoUtil: InfoUtil, diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt index 0088aca4..9b0b2e08 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt @@ -28,10 +28,13 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) val items : LiveData> val loadingItems = MutableLiveData() private val sharedPreferences: SharedPreferences + var state: ResultsState = ResultsState.IDLE + enum class ResultsState { + PROCESSING, IDLE + } init { val dao = DBManager.getInstance(application).resultDao - val commandDao = DBManager.getInstance(application).commandTemplateDao repository = ResultRepository(dao, getApplication().applicationContext) searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao) items = repository.allResults.asLiveData() @@ -61,6 +64,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) } } suspend fun parseQueries(inputQueries: List){ + state = ResultsState.PROCESSING if (inputQueries.size == 1){ parseQuery(inputQueries[0], true) }else { @@ -69,6 +73,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) inputQueries.forEach { parseQuery(it, false) } + state = ResultsState.IDLE loadingItems.postValue(false) } } @@ -97,7 +102,10 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) } catch (e: Exception) { Log.e(tag, e.toString()) } - if (resetResults) loadingItems.postValue(false) + if (resetResults) { + state = ResultsState.IDLE + loadingItems.postValue(false) + } res } } @@ -107,7 +115,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) val m = p.matcher(inputQuery) if (m.find()) { type = "YT_Video" - if (inputQuery.contains("playlist?list=")) { + if (inputQuery.contains("playlist?list=") && !inputQuery.contains("music.youtu")) { type = "YT_Playlist" } } else if (inputQuery.contains("http")) { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelDownloadNotificationReceiver.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelDownloadNotificationReceiver.kt index 5dbe0619..8cf273bc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelDownloadNotificationReceiver.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/CancelDownloadNotificationReceiver.kt @@ -20,10 +20,8 @@ class CancelDownloadNotificationReceiver : BroadcastReceiver() { if (message != null) { runCatching { val notificationUtil = NotificationUtil(c) - WorkManager.getInstance(c).cancelUniqueWork(id.toString()) - notificationUtil.cancelDownloadNotification(id) YoutubeDL.getInstance().destroyProcessById(id.toString()) - + notificationUtil.cancelDownloadNotification(id) val dbManager = DBManager.getInstance(c) CoroutineScope(Dispatchers.IO).launch{ runCatching { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/PauseDownloadNotificationReceiver.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/PauseDownloadNotificationReceiver.kt index bc816fd5..b1fb0ecc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/PauseDownloadNotificationReceiver.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/PauseDownloadNotificationReceiver.kt @@ -3,8 +3,13 @@ package com.deniscerri.ytdlnis.receiver import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import com.deniscerri.ytdlnis.database.DBManager +import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.util.NotificationUtil import com.yausername.youtubedl_android.YoutubeDL +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class PauseDownloadNotificationReceiver : BroadcastReceiver() { override fun onReceive(c: Context, intent: Intent) { @@ -16,6 +21,14 @@ class PauseDownloadNotificationReceiver : BroadcastReceiver() { notificationUtil.cancelDownloadNotification(id) YoutubeDL.getInstance().destroyProcessById(id.toString()) notificationUtil.createResumeDownload(id, title, NotificationUtil.DOWNLOAD_SERVICE_CHANNEL_ID) + val dbManager = DBManager.getInstance(c) + CoroutineScope(Dispatchers.IO).launch{ + runCatching { + val item = dbManager.downloadDao.getDownloadById(id.toLong()) + item.status = DownloadRepository.Status.Paused.toString() + dbManager.downloadDao.update(item) + } + } } } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt index 8255ff22..bbca7fe9 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt @@ -32,7 +32,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.ui.BaseActivity import com.deniscerri.ytdlnis.ui.downloadcard.DownloadBottomSheetDialog -import com.deniscerri.ytdlnis.ui.downloadcard.SelectPlaylistItemsBottomSheetDialog +import com.deniscerri.ytdlnis.ui.downloadcard.SelectPlaylistItemsDialog import com.deniscerri.ytdlnis.util.ThemeUtil import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -95,10 +95,9 @@ class ShareActivity : BaseActivity() { askPermissions() val action = intent.action - val type = intent.clipData Log.e("aa", intent.toString()) - if (Intent.ACTION_SEND == action && type != null) { - if (intent.getStringExtra(Intent.EXTRA_TEXT) == null){ + if (Intent.ACTION_SEND == action || Intent.ACTION_VIEW == action) { + if (intent.getStringExtra(Intent.EXTRA_TEXT) == null && Intent.ACTION_SEND == action){ intent.setClass(this, MainActivity::class.java) startActivity(intent) finishAffinity() @@ -107,7 +106,7 @@ class ShareActivity : BaseActivity() { runCatching { supportFragmentManager.popBackStack() } - quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false)) + quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false) || sharedPreferences.getString("preferred_download_type", "video") == "command") val loadingBottomSheet = BottomSheetDialog(this) loadingBottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) @@ -118,7 +117,11 @@ class ShareActivity : BaseActivity() { this.finish() } - val inputQuery = intent.getStringExtra(Intent.EXTRA_TEXT)!! + val inputQuery = when(action){ + Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!! + else -> intent.dataString!! + } + lifecycleScope.launch { try { val result = resultViewModel.getItemByURL(inputQuery) @@ -166,7 +169,7 @@ class ShareActivity : BaseActivity() { private fun showSelectPlaylistItems(it: List){ if (sharedPreferences.getBoolean("download_card", true)){ - val bottomSheet = SelectPlaylistItemsBottomSheetDialog(it, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)) + val bottomSheet = SelectPlaylistItemsDialog(it, DownloadViewModel.Type.valueOf(sharedPreferences.getString("preferred_download_type", "video")!!)) bottomSheet.show(supportFragmentManager, "downloadPlaylistSheet") }else{ lifecycleScope.launch(Dispatchers.IO){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt index 512b4c38..37ec610f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt @@ -6,6 +6,8 @@ import android.content.* import android.content.Context.CLIPBOARD_SERVICE import android.content.res.ColorStateList import android.graphics.Color +import android.graphics.drawable.ShapeDrawable +import android.graphics.drawable.shapes.OvalShape import android.os.Bundle import android.os.Handler import android.os.Looper @@ -40,6 +42,8 @@ import com.deniscerri.ytdlnis.ui.downloadcard.DownloadMultipleBottomSheetDialog import com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.ThemeUtil +import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll +import com.devbrackets.android.exomedia.core.video.mp.NativeVideoDelegate import com.facebook.shimmer.ShimmerFrameLayout import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.MaterialToolbar @@ -55,6 +59,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import me.zhanghai.android.fastscroll.FastScrollerBuilder import java.util.* @@ -146,6 +151,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene recyclerView = view.findViewById(R.id.recyclerViewHome) recyclerView?.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size)) recyclerView?.adapter = homeAdapter + recyclerView?.enableFastScroll() resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel.items.observe(viewLifecycleOwner) { @@ -251,12 +257,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene override fun onResume() { super.onResume() if(arguments?.getString("url") == null){ - resultViewModel.checkTrending() + if (resultViewModel.state == ResultViewModel.ResultsState.IDLE){ + resultViewModel.checkTrending() + }else{ + resultViewModel.loadingItems.postValue(true) + } }else{ - arguments arguments?.remove("url") - arguments?.remove("showDownloadsWithUpdatedFormats") } + arguments?.remove("showDownloadsWithUpdatedFormats") if (searchView?.currentTransitionState == SearchView.TransitionState.SHOWN){ lifecycleScope.launch { updateSearchViewItems(searchView?.editText?.text, linkYouCopied) @@ -533,7 +542,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene resultViewModel.deleteAll() lifecycleScope.launch(Dispatchers.IO){ Thread.sleep(300) - if(sharedPreferences!!.getBoolean("quick_download", false)){ + if(sharedPreferences!!.getBoolean("quick_download", false) || sharedPreferences!!.getString("preferred_download_type", "video") == "command"){ if (queryList.size == 1 && queryList.first().matches("(https?://(?:www\\.|(?!www))[a-zA-Z\\d][a-zA-Z\\d-]+[a-zA-Z\\d]\\.\\S{2,}|www\\.[a-zA-Z\\d][a-zA-Z\\d-]+[a-zA-Z\\d]\\.\\S{2,}|https?://(?:www\\.|(?!www))[a-zA-Z\\d]+\\.\\S{2,}|www\\.[a-zA-Z\\d]+\\.\\S{2,})".toRegex())){ val resultItem = downloadViewModel.createEmptyResultItem(queryList.first()) if (sharedPreferences!!.getBoolean("download_card", true)) { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/AddExtraCommandsDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/AddExtraCommandsDialog.kt index 41ee3576..eb72b99f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/AddExtraCommandsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/AddExtraCommandsDialog.kt @@ -37,7 +37,7 @@ import java.util.* import java.util.regex.Pattern -class AddExtraCommandsDialog(private val item: DownloadItem, private val callback: ExtraCommandsListener) : BottomSheetDialogFragment() { +class AddExtraCommandsDialog(private val item: DownloadItem?, private val callback: ExtraCommandsListener) : BottomSheetDialogFragment() { private lateinit var infoUtil: InfoUtil private lateinit var sharedPreferences: SharedPreferences private lateinit var commandTemplateViewModel: CommandTemplateViewModel @@ -86,9 +86,14 @@ class AddExtraCommandsDialog(private val item: DownloadItem, private val callbac val text = view.findViewById(R.id.command) val templates = view.findViewById