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