implemented download scheduling

This commit is contained in:
Denis Çerri 2023-02-11 18:26:13 +01:00
parent 2fd96cf3b2
commit d860511ce9
No known key found for this signature in database
GPG key ID: 95C43D517D830350
29 changed files with 420 additions and 597 deletions

View file

@ -0,0 +1 @@
-dontobfuscate

View file

@ -1,3 +1,6 @@
import com.android.build.gradle.internal.dsl.ManagedVirtualDevice
plugins { plugins {
id 'com.android.application' id 'com.android.application'
id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1' id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1'
@ -48,6 +51,19 @@ android {
androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
} }
testOptions{
managedDevices {
devices{
pixel4Api31(com.android.build.api.dsl.ManagedVirtualDevice){
device = "Pixel 2"
apiLevel = 31
systemImageSource = "aosp"
}
}
}
}
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
@ -55,6 +71,16 @@ android {
debuggable true debuggable true
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
} }
benchmark {
initWith buildTypes.release
proguardFiles 'baseline-profiles-rules.pro'
}
debug {
initWith buildTypes.release
proguardFiles 'baseline-profiles-rules.pro'
}
} }
buildFeatures { buildFeatures {
@ -95,7 +121,7 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appCompatVer" implementation "androidx.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation 'com.google.android.material:material:1.7.0' implementation 'com.google.android.material:material:1.8.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.core:core:1.9.0' implementation 'androidx.core:core:1.9.0'
implementation 'androidx.recyclerview:recyclerview:1.2.1' implementation 'androidx.recyclerview:recyclerview:1.2.1'
@ -104,9 +130,15 @@ dependencies {
implementation 'androidx.navigation:navigation-ui:2.5.3' implementation 'androidx.navigation:navigation-ui:2.5.3'
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.core:core-ktx:1.9.0' implementation 'androidx.core:core-ktx:1.9.0'
implementation 'androidx.test.ext:junit-ktx:1.1.5'
testImplementation "junit:junit:$junitVer" testImplementation "junit:junit:$junitVer"
androidTestImplementation "junit:junit:$junitVer"
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer" androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVer" androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVer"
androidTestImplementation "androidx.benchmark:benchmark-macro-junit4:1.2.0-alpha09"
androidTestImplementation "androidx.test:runner:1.5.2"
androidTestImplementation "androidx.test:core:1.5.0"
androidTestImplementation "androidx.test:rules:1.5.0"
implementation "io.reactivex.rxjava2:rxandroid:2.1.0" implementation "io.reactivex.rxjava2:rxandroid:2.1.0"
implementation "com.devbrackets.android:exomedia:4.3.0" implementation "com.devbrackets.android:exomedia:4.3.0"

View file

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 1, "version": 1,
"identityHash": "e53569dbe0c02d62eda598075d73d5b1", "identityHash": "9197d2ce894417365b9012ace8bcf284",
"entities": [ "entities": [
{ {
"tableName": "results", "tableName": "results",
@ -160,7 +160,7 @@
}, },
{ {
"tableName": "downloads", "tableName": "downloads",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT NOT NULL, `removeAudio` INTEGER NOT NULL DEFAULT 0, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `embedSubs` INTEGER NOT NULL, `addChapters` INTEGER NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued')", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `url` TEXT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `thumb` TEXT NOT NULL, `duration` TEXT NOT NULL, `type` TEXT NOT NULL, `format` TEXT NOT NULL, `downloadPath` TEXT NOT NULL, `website` TEXT NOT NULL, `downloadSize` TEXT NOT NULL, `playlistTitle` TEXT NOT NULL, `embedSubs` INTEGER NOT NULL, `addChapters` INTEGER NOT NULL, `SaveThumb` INTEGER NOT NULL, `status` TEXT NOT NULL DEFAULT 'Queued', `downloadStartTime` INTEGER NOT NULL DEFAULT 0)",
"fields": [ "fields": [
{ {
"fieldPath": "id", "fieldPath": "id",
@ -210,13 +210,6 @@
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": true
}, },
{
"fieldPath": "removeAudio",
"columnName": "removeAudio",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{ {
"fieldPath": "downloadPath", "fieldPath": "downloadPath",
"columnName": "downloadPath", "columnName": "downloadPath",
@ -265,6 +258,13 @@
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true, "notNull": true,
"defaultValue": "'Queued'" "defaultValue": "'Queued'"
},
{
"fieldPath": "downloadStartTime",
"columnName": "downloadStartTime",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
} }
], ],
"primaryKey": { "primaryKey": {
@ -312,7 +312,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e53569dbe0c02d62eda598075d73d5b1')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9197d2ce894417365b9012ace8bcf284')"
] ]
} }
} }

View file

@ -0,0 +1,18 @@
package com.deniscerri.ytdlnis
import androidx.benchmark.macro.junit4.BaselineProfileRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule val baselineProfileRule = BaselineProfileRule()
@Test
fun startup() =
baselineProfileRule.collectBaselineProfile(packageName = "com.deniscerri.ytdl") {
startActivityAndWait()
}
}

View file

@ -48,40 +48,13 @@ class App : Application() {
.getInt("concurrent_downloads", 1))) .getInt("concurrent_downloads", 1)))
.build()) .build())
configureRxJavaErrorHandler() try {
Completable.fromAction { initLibraries() }.subscribeOn(Schedulers.io()) initLibraries()
.observeOn(AndroidSchedulers.mainThread()) }catch (e: Exception){
.subscribe(object : DisposableCompletableObserver() { Toast.makeText(this@App, e.message, Toast.LENGTH_SHORT).show()
override fun onComplete() { e.printStackTrace()
// it worked
}
override fun onError(e: Throwable) {
if (BuildConfig.DEBUG) Log.e(TAG, "failed to initialize youtubedl-android", e)
Toast.makeText(
applicationContext,
"initialization failed: " + e.localizedMessage,
Toast.LENGTH_SHORT
).show()
}
})
}
private fun configureRxJavaErrorHandler() {
var err = Throwable()
RxJavaPlugins.setErrorHandler { e: Throwable ->
if (e is UndeliverableException) {
// As UndeliverableException is a wrapper, get the cause of it to get the "real" exception
err = e.cause!!
}
if (e is InterruptedException) {
// fine, some blocking code was interrupted by a dispose call
return@setErrorHandler
}
Log.e(TAG, "Undeliverable exception received, not sure what to do", err)
} }
} }
@Throws(YoutubeDLException::class) @Throws(YoutubeDLException::class)
private fun initLibraries() { private fun initLibraries() {
YoutubeDL.getInstance().init(this) YoutubeDL.getInstance().init(this)

View file

@ -1,7 +1,6 @@
package com.deniscerri.ytdlnis package com.deniscerri.ytdlnis
import android.Manifest import android.Manifest
import android.app.ActivityManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
@ -21,11 +20,9 @@ import androidx.core.view.WindowInsetsCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.databinding.ActivityMainBinding import com.deniscerri.ytdlnis.databinding.ActivityMainBinding
import com.deniscerri.ytdlnis.service.IDownloaderListener import com.deniscerri.ytdlnis.service.IDownloaderListener
import com.deniscerri.ytdlnis.service.IDownloaderService import com.deniscerri.ytdlnis.service.IDownloaderService
import com.deniscerri.ytdlnis.ui.DownloadFragment
import com.deniscerri.ytdlnis.ui.HistoryFragment import com.deniscerri.ytdlnis.ui.HistoryFragment
import com.deniscerri.ytdlnis.ui.HomeFragment import com.deniscerri.ytdlnis.ui.HomeFragment
import com.deniscerri.ytdlnis.ui.MoreFragment import com.deniscerri.ytdlnis.ui.MoreFragment
@ -47,7 +44,6 @@ class MainActivity : AppCompatActivity() {
lateinit var context: Context lateinit var context: Context
private lateinit var homeFragment: HomeFragment private lateinit var homeFragment: HomeFragment
private lateinit var historyFragment: HistoryFragment private lateinit var historyFragment: HistoryFragment
private lateinit var downloadFragment: DownloadFragment
private lateinit var workManager: WorkManager private lateinit var workManager: WorkManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -63,7 +59,6 @@ class MainActivity : AppCompatActivity() {
homeFragment = HomeFragment() homeFragment = HomeFragment()
historyFragment = HistoryFragment() historyFragment = HistoryFragment()
downloadFragment = DownloadFragment()
moreFragment = MoreFragment() moreFragment = MoreFragment()
initFragments() initFragments()
binding.bottomNavigationView.setOnItemSelectedListener { item: MenuItem -> binding.bottomNavigationView.setOnItemSelectedListener { item: MenuItem ->
@ -79,18 +74,10 @@ class MainActivity : AppCompatActivity() {
R.id.history -> { R.id.history -> {
if (lastFragment === historyFragment) { if (lastFragment === historyFragment) {
historyFragment.scrollToTop() historyFragment.scrollToTop()
} else {
this.title = getString(R.string.history)
}
replaceFragment(historyFragment)
}
R.id.downloads -> {
if (lastFragment === downloadFragment) {
downloadFragment.scrollToTop()
} else { } else {
this.title = getString(R.string.downloads) this.title = getString(R.string.downloads)
} }
replaceFragment(downloadFragment) replaceFragment(historyFragment)
} }
R.id.more -> { R.id.more -> {
if (lastFragment === moreFragment) { if (lastFragment === moreFragment) {
@ -129,11 +116,14 @@ class MainActivity : AppCompatActivity() {
Log.e(TAG, action) Log.e(TAG, action)
homeFragment = HomeFragment() homeFragment = HomeFragment()
historyFragment = HistoryFragment() historyFragment = HistoryFragment()
downloadFragment = DownloadFragment()
moreFragment = MoreFragment() moreFragment = MoreFragment()
if (type.equals("application/txt", ignoreCase = true)) { if (type.equals("application/txt", ignoreCase = true)) {
try { try {
val uri = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM) val uri = if (Build.VERSION.SDK_INT >= 33){
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
}else{
intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
val `is` = contentResolver.openInputStream(uri!!) val `is` = contentResolver.openInputStream(uri!!)
val textBuilder = StringBuilder() val textBuilder = StringBuilder()
val reader: Reader = BufferedReader( val reader: Reader = BufferedReader(
@ -164,11 +154,9 @@ class MainActivity : AppCompatActivity() {
fm.beginTransaction() fm.beginTransaction()
.replace(R.id.frame_layout, homeFragment) .replace(R.id.frame_layout, homeFragment)
.add(R.id.frame_layout, historyFragment) .add(R.id.frame_layout, historyFragment)
.add(R.id.frame_layout, downloadFragment)
.add(R.id.frame_layout, moreFragment) .add(R.id.frame_layout, moreFragment)
.hide(historyFragment) .hide(historyFragment)
.hide(moreFragment) .hide(moreFragment)
.hide(downloadFragment)
.commit() .commit()
lastFragment = homeFragment lastFragment = homeFragment
listeners = ArrayList() listeners = ArrayList()
@ -179,106 +167,10 @@ class MainActivity : AppCompatActivity() {
lastFragment = f lastFragment = f
} }
fun startDownloadService(
downloadQueue: ArrayList<ResultItem>,
awaitingListener: IDownloaderListener
) {
// addQueueToDownloads(downloadQueue)
// if (isDownloadServiceRunning) {
// iDownloaderService?.updateQueue(downloadQueue)
// return
// }
// if (!listeners.contains(awaitingListener)) listeners.add(awaitingListener)
// val serviceIntent = Intent(context, DownloaderService::class.java)
// serviceIntent.putParcelableArrayListExtra("queue", downloadQueue)
// context.applicationContext.startService(serviceIntent)
// context.applicationContext.bindService(serviceIntent, serviceConnection, BIND_AUTO_CREATE)
}
private fun addQueueToDownloads(downloadQueue: ArrayList<ResultItem>) {
// try {
// val sharedPreferences = context.getSharedPreferences("root_preferences", MODE_PRIVATE)
// if (!sharedPreferences.getBoolean("incognito", false)) {
// val databaseManager = DatabaseManager(context)
// for (i in downloadQueue.indices.reversed()) {
// val v = downloadQueue[i]
// v.isQueuedDownload = true
// databaseManager.addToHistory(v)
// }
// databaseManager.close()
// //downloadsFragment.initCards()
// }
// } catch (e: Exception) {
// e.printStackTrace()
// }
}
fun stopDownloadService() {
// if (!isDownloadServiceRunning) return
// try {
// iDownloaderService!!.removeActivity(this)
// context.applicationContext.unbindService(serviceConnection)
// context.applicationContext.stopService(
// Intent(
// context.applicationContext,
// DownloaderService::class.java
// )
// )
// } catch (ignored: Exception) {
// }
// isDownloadServiceRunning = false
}
fun cancelDownloadService() {
if (!isDownloadServiceRunning) return
iDownloaderService!!.cancelDownload(true)
stopDownloadService()
}
fun cancelAllDownloads() { fun cancelAllDownloads() {
workManager.cancelAllWork(); workManager.cancelAllWork();
} }
fun removeItemFromDownloadQueue(video: ResultItem?, type: String?) {
iDownloaderService!!.removeItemFromDownloadQueue(video, type)
}
fun isDownloadServiceRunning(): Boolean {
// val service = getService(DownloaderService::class.java)
// if (service != null) {
// if (service.foreground) {
// isDownloadServiceRunning = true
// return true
// }
// }
return false
}
private fun reconnectDownloadService() {
// val service = getService(DownloaderService::class.java)
// if (service != null) {
// val serviceIntent = Intent(context.applicationContext, DownloaderService::class.java)
// serviceIntent.putExtra("rebind", true)
// context.applicationContext.bindService(
// serviceIntent,
// serviceConnection,
// BIND_AUTO_CREATE
// )
// isDownloadServiceRunning = true
// }
}
private fun getService(className: Class<*>): ActivityManager.RunningServiceInfo? {
val manager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
for (service in manager.getRunningServices(Int.MAX_VALUE)) {
if (className.name == service.service.className) {
return service
}
}
return null
}
private fun checkUpdate() { private fun checkUpdate() {
val preferences = context.getSharedPreferences("root_preferences", MODE_PRIVATE) val preferences = context.getSharedPreferences("root_preferences", MODE_PRIVATE)
if (preferences.getBoolean("update_app", false)) { if (preferences.getBoolean("update_app", false)) {

View file

@ -6,6 +6,7 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type import java.lang.reflect.Type
import java.util.Calendar
class Converters { class Converters {

View file

@ -49,4 +49,7 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE url=:url AND (status='Error' OR status='Cancelled') LIMIT 1") @Query("SELECT * FROM downloads WHERE url=:url AND (status='Error' OR status='Cancelled') LIMIT 1")
fun checkIfErrorOrCancelled(url: String) : DownloadItem fun checkIfErrorOrCancelled(url: String) : DownloadItem
@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
} }

View file

@ -4,6 +4,7 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import java.util.*
@Entity(tableName = "downloads") @Entity(tableName = "downloads")
data class DownloadItem( data class DownloadItem(
@ -16,8 +17,6 @@ data class DownloadItem(
val duration: String, val duration: String,
var type: DownloadViewModel.Type, var type: DownloadViewModel.Type,
var format: Format, var format: Format,
@ColumnInfo(defaultValue = "0")
var removeAudio: Boolean,
var downloadPath: String, var downloadPath: String,
val website: String, val website: String,
val downloadSize: String, val downloadSize: String,
@ -26,5 +25,7 @@ data class DownloadItem(
var addChapters: Boolean, var addChapters: Boolean,
var SaveThumb: Boolean, var SaveThumb: Boolean,
@ColumnInfo(defaultValue = "Queued") @ColumnInfo(defaultValue = "Queued")
var status: String var status: String,
@ColumnInfo(defaultValue = "0")
var downloadStartTime: Long
) )

View file

@ -1,6 +1,7 @@
package com.deniscerri.ytdlnis.database.repository package com.deniscerri.ytdlnis.database.repository
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import com.deniscerri.ytdlnis.database.Converters
import com.deniscerri.ytdlnis.database.dao.DownloadDao import com.deniscerri.ytdlnis.database.dao.DownloadDao
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
@ -49,7 +50,18 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.queueAllProcessing() downloadDao.queueAllProcessing()
} }
fun checkIfPresent(item: ResultItem): DownloadItem{ fun checkIfPresentForProcessing(item: ResultItem): DownloadItem{
return downloadDao.checkIfErrorOrCancelled(item.url) return downloadDao.checkIfErrorOrCancelled(item.url)
} }
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
}
}
} }

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Activity import android.app.Activity
import android.app.Application import android.app.Application
import android.content.SharedPreferences import android.content.SharedPreferences
import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -22,6 +23,8 @@ import com.deniscerri.ytdlnis.work.DownloadWorker
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.*
import java.util.concurrent.TimeUnit
class DownloadViewModel(application: Application) : AndroidViewModel(application) { class DownloadViewModel(application: Application) : AndroidViewModel(application) {
private val repository : DownloadRepository private val repository : DownloadRepository
@ -31,8 +34,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val activeDownloads : LiveData<List<DownloadItem>> val activeDownloads : LiveData<List<DownloadItem>>
val processingDownloads : LiveData<List<DownloadItem>> val processingDownloads : LiveData<List<DownloadItem>>
private var bestVideoFormat: Format private var bestVideoFormat : Format
private var bestAudioFormat: Format private var bestAudioFormat : Format
enum class Type { enum class Type {
audio, video, command audio, video, command
} }
@ -90,8 +93,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
resultItem.thumb, resultItem.thumb,
resultItem.duration, resultItem.duration,
type, type,
getFormat(resultItem, type), false, getFormat(resultItem, type),
"", resultItem.website, "", resultItem.playlistTitle, embedSubs, addChapters, saveThumb, DownloadRepository.Status.Processing.toString() "", resultItem.website, "", resultItem.playlistTitle, embedSubs, addChapters, saveThumb, DownloadRepository.Status.Processing.toString(), 0
) )
} }
@ -133,7 +136,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
items.forEachIndexed { i, it -> items.forEachIndexed { i, it ->
val tmpDownloadItem = downloadItems[i] val tmpDownloadItem = downloadItems[i]
try { try {
val item = repository.checkIfPresent(it!!) val item = repository.checkIfPresentForProcessing(it!!)
tmpDownloadItem.id = item.id tmpDownloadItem.id = item.id
tmpDownloadItem.status = DownloadRepository.Status.Processing.toString() tmpDownloadItem.status = DownloadRepository.Status.Processing.toString()
repository.update(tmpDownloadItem) repository.update(tmpDownloadItem)
@ -162,15 +165,24 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val context = getApplication<App>().applicationContext val context = getApplication<App>().applicationContext
items.forEach { items.forEach {
it.status = DownloadRepository.Status.Queued.toString() it.status = DownloadRepository.Status.Queued.toString()
it.id = repository.checkIfReDownloadingErroredOrCancelled(it)
if (it.id == 0L){ if (it.id == 0L){
val id = repository.insert(it) val id = repository.insert(it)
it.id = id it.id = id
}else repository.update(it) }else repository.update(it)
val currentTime = System.currentTimeMillis()
var delay = if (it.downloadStartTime != 0L){
it.downloadStartTime - currentTime
} else 0
if (delay < 0L) delay = 0L
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>() val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.setInputData(Data.Builder().putLong("id", it.id).build()) .setInputData(Data.Builder().putLong("id", it.id).build())
.addTag("download") .addTag("download")
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
.build() .build()
WorkManager.getInstance(context).beginUniqueWork( WorkManager.getInstance(context).beginUniqueWork(
it.id.toString(), it.id.toString(),
ExistingWorkPolicy.KEEP, ExistingWorkPolicy.KEEP,

View file

@ -19,7 +19,7 @@ class CancelDownloadNotificationReceiver : BroadcastReceiver() {
if (message != null) { if (message != null) {
val notificationUtil = NotificationUtil(c) val notificationUtil = NotificationUtil(c)
YoutubeDL.getInstance().destroyProcessById(id.toString()); YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(c).cancelUniqueWork(id.toString()) WorkManager.getInstance(c).cancelUniqueWork(id.toString())
notificationUtil.cancelDownloadNotification(id) notificationUtil.cancelDownloadNotification(id)
} }

View file

@ -1,282 +0,0 @@
package com.deniscerri.ytdlnis.ui
import android.app.Activity
import android.content.*
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.*
import android.view.View.*
import android.widget.*
import androidx.core.content.FileProvider
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdlnis.adapter.QueuedDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.HistoryItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHistoryBinding
import com.deniscerri.ytdlnis.util.FileUtil
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.chip.ChipGroup
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import java.io.File
/**
* A fragment representing a list of Items.
*/
class DownloadFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener, QueuedDownloadAdapter.OnItemClickListener,
OnClickListener, OnLongClickListener {
private lateinit var downloadViewModel : DownloadViewModel
private var downloading = false
private var fragmentView: View? = null
private var activity: Activity? = null
private var mainActivity: MainActivity? = null
private var fragmentContext: Context? = null
private var layoutinflater: LayoutInflater? = null
private var shimmerCards: ShimmerFrameLayout? = null
private var topAppBar: MaterialToolbar? = null
private var activeRecyclerView: RecyclerView? = null
private var othersRecyclerView: RecyclerView? = null
private var downloadsAdapter: ActiveDownloadAdapter? = null
private var queuedDownloadsAdapter: QueuedDownloadAdapter? = null
private var bottomSheet: BottomSheetDialog? = null
private var sortSheet: BottomSheetDialog? = null
private var uiHandler: Handler? = null
private var noResults: RelativeLayout? = null
private var websiteGroup: ChipGroup? = null
private var downloadsList: List<DownloadItem?>? = null
private var allDownloadsList: List<DownloadItem?>? = null
private var selectedObjects: ArrayList<DownloadItem>? = null
private var deleteFab: ExtendedFloatingActionButton? = null
private var fileUtil: FileUtil? = null
private var _binding : FragmentHistoryBinding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentHistoryBinding.inflate(inflater, container, false)
fragmentView = inflater.inflate(R.layout.fragment_downloads, container, false)
activity = getActivity()
mainActivity = activity as MainActivity?
return fragmentView
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
fragmentContext = context
layoutinflater = LayoutInflater.from(context)
topAppBar = view.findViewById(R.id.downloads_toolbar)
noResults = view.findViewById(R.id.no_results)
websiteGroup = view.findViewById(R.id.website_chip_group)
deleteFab = view.findViewById(R.id.delete_selected_fab)
fileUtil = FileUtil()
deleteFab?.tag = "deleteSelected"
deleteFab?.setOnClickListener(this)
uiHandler = Handler(Looper.getMainLooper())
selectedObjects = ArrayList()
downloading = mainActivity!!.isDownloadServiceRunning()
downloadsList = mutableListOf()
allDownloadsList = mutableListOf()
downloadsAdapter =
ActiveDownloadAdapter(
this,
requireActivity()
)
activeRecyclerView = view.findViewById(R.id.recyclerviewactivedownloads)
activeRecyclerView?.layoutManager = LinearLayoutManager(context)
activeRecyclerView?.adapter = downloadsAdapter
queuedDownloadsAdapter =
QueuedDownloadAdapter(
this,
requireActivity()
)
othersRecyclerView = view.findViewById(R.id.recyclerviewotherdownloads)
othersRecyclerView?.layoutManager = LinearLayoutManager(context)
othersRecyclerView?.adapter = queuedDownloadsAdapter
noResults?.visibility = GONE
shimmerCards?.visibility = GONE
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
downloadViewModel.activeDownloads.observe(viewLifecycleOwner){
// update active
}
downloadViewModel.queuedDownloads.observe(viewLifecycleOwner){
// update queued
}
}
fun scrollToTop() {
activeRecyclerView!!.scrollToPosition(0)
Handler(Looper.getMainLooper()).post {
(topAppBar!!.parent as AppBarLayout).setExpanded(
true,
true
)
}
}
override fun onLongClick(v: View): Boolean {
val id = v.id
if (id == R.id.bottom_sheet_link) {
copyLinkToClipBoard(v.tag as Long)
return true
}
return false
}
private fun removeSelectedItems() {
// if (bottomSheet != null) bottomSheet!!.hide()
// val deleteFile = booleanArrayOf(false)
// val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
// deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
// deleteDialog.setMultiChoiceItems(
// arrayOf(getString(R.string.delete_files_too)),
// booleanArrayOf(false)
// ) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
// deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
// deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
// for (item in selectedObjects!!){
// historyViewModel.delete(item, deleteFile[0])
// }
// selectedObjects = ArrayList()
// historyAdapter!!.clearCheckeditems()
// deleteFab!!.visibility = GONE
// }
// deleteDialog.show()
}
private fun removeItem(id: Long) {
// if (bottomSheet != null) bottomSheet!!.hide()
// val deleteFile = booleanArrayOf(false)
// val v = findItem(id)
// val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
// deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + v!!.title + "\"!")
// deleteDialog.setMultiChoiceItems(
// arrayOf(getString(R.string.delete_file_too)),
// booleanArrayOf(false)
// ) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
// deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
// deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
// historyViewModel.delete(v, deleteFile[0])
// }
// deleteDialog.show()
}
private fun copyLinkToClipBoard(id: Long) {
val url = findItem(id)?.url
val clipboard = context?.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip = ClipData.newPlainText(getString(R.string.url), url)
clipboard.setPrimaryClip(clip)
if (bottomSheet != null) bottomSheet!!.hide()
Toast.makeText(context, getString(R.string.link_copied_to_clipboard), Toast.LENGTH_SHORT)
.show()
}
private fun openLinkIntent(id: Long) {
val url = findItem(id)?.url
val i = Intent(Intent.ACTION_VIEW)
i.data = Uri.parse(url)
if (bottomSheet != null) bottomSheet!!.hide()
startActivity(i)
}
private fun openFileIntent(id: Long) {
val downloadPath = findItem(id)!!.downloadPath
val file = File(downloadPath)
val uri = FileProvider.getUriForFile(
fragmentContext!!,
fragmentContext!!.packageName + ".fileprovider",
file
)
val mime = mainActivity!!.contentResolver.getType(uri)
val i = Intent(Intent.ACTION_VIEW)
i.setDataAndType(uri, mime)
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
startActivity(i)
}
override fun onCardClick(videoID: Long, isPresent: Boolean) {
bottomSheet = BottomSheetDialog(fragmentContext!!)
bottomSheet!!.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet!!.setContentView(R.layout.history_item_details_bottom_sheet)
val video = findItem(videoID)
val title = bottomSheet!!.findViewById<TextView>(R.id.bottom_sheet_title)
title!!.text = video!!.title
val author = bottomSheet!!.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = video.author
val link = bottomSheet!!.findViewById<Button>(R.id.bottom_sheet_link)
val url = video.url
link!!.text = url
link.tag = videoID
link.setOnClickListener(this)
link.setOnLongClickListener(this)
val remove = bottomSheet!!.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = videoID
remove.setOnClickListener(this)
val openFile = bottomSheet!!.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.tag = videoID
openFile.setOnClickListener(this)
if (!isPresent) openFile.visibility = GONE
bottomSheet!!.show()
bottomSheet!!.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
override fun onCardSelect(videoID: Long, isChecked: Boolean) {
val item = findItem(videoID)
if (isChecked) selectedObjects!!.add(item!!)
else selectedObjects!!.remove(item)
if (selectedObjects!!.size > 1) {
deleteFab!!.visibility = VISIBLE
} else {
deleteFab!!.visibility = GONE
}
}
override fun onButtonClick(position: Int) {
// val vid = downloadsObjects!![position]
// try {
// //mainActivity!!.removeItemFromDownloadQueue(vid, vid!!.downloadedType)
// } catch (e: Exception) {
// val info = DownloadInfo()
// info.video = vid
// info.downloadType = vid!!.downloadedType
// }
}
private fun findItem(id : Long): DownloadItem? {
return downloadsList?.find { it?.id == id }
}
companion object {
private const val TAG = "downloadsFragment"
}
override fun onClick(p0: View?) {
TODO("Not yet implemented")
}
}

View file

@ -330,10 +330,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
val deleteFile = booleanArrayOf(false) val deleteFile = booleanArrayOf(false)
val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!) val deleteDialog = MaterialAlertDialogBuilder(fragmentContext!!)
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!") deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
deleteDialog.setMultiChoiceItems( val path = item.downloadPath
arrayOf(getString(R.string.delete_file_too)), val file = File(path)
booleanArrayOf(false) if (file.exists() && path.isNotEmpty()) {
) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b } deleteDialog.setMultiChoiceItems(
arrayOf(getString(R.string.delete_file_too)),
booleanArrayOf(false)
) { _: DialogInterface?, _: Int, b: Boolean -> deleteFile[0] = b }
}
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
historyViewModel.delete(item, deleteFile[0]) historyViewModel.delete(item, deleteFile[0])

View file

@ -95,7 +95,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, View.OnClickLi
fileUtil = FileUtil() fileUtil = FileUtil()
uiHandler = Handler(Looper.getMainLooper()) uiHandler = Handler(Looper.getMainLooper())
selectedObjects = ArrayList() selectedObjects = ArrayList()
downloading = mainActivity!!.isDownloadServiceRunning()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
@ -391,8 +390,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, View.OnClickLi
downloadFabs!!.visibility = VISIBLE downloadFabs!!.visibility = VISIBLE
} else { } else {
downloadFabs!!.visibility = GONE downloadFabs!!.visibility = GONE
if (resultsList!!.size > 1 && resultsList!![1]!!.playlistTitle != getString(R.string.trendingPlaylist)) { if (resultsList!![1]!!.playlistTitle.isNotEmpty() && resultsList!![1]!!.playlistTitle != getString(R.string.trendingPlaylist)){
downloadAllFabCoordinator!!.visibility = VISIBLE downloadAllFabCoordinator!!.visibility = VISIBLE
}else{
downloadAllFabCoordinator!!.visibility = GONE
} }
} }
} }

View file

@ -35,7 +35,6 @@ class DownloadAudioFragment(private var resultItem: ResultItem) : Fragment() {
private var fragmentView: View? = null private var fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private var mainActivity: MainActivity? = null private var mainActivity: MainActivity? = null
private lateinit var resultViewModel : ResultViewModel
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var fileUtil : FileUtil private lateinit var fileUtil : FileUtil

View file

@ -1,13 +1,14 @@
package com.deniscerri.ytdlnis.ui.downloadcard package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.DatePickerDialog
import android.app.Dialog import android.app.Dialog
import android.app.TimePickerDialog
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.widget.Button import android.widget.Button
import android.widget.FrameLayout
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
@ -16,10 +17,10 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import java.util.*
class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val type: Type) : BottomSheetDialogFragment() { class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val type: Type) : BottomSheetDialogFragment() {
@ -103,6 +104,39 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
} }
} }
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
scheduleBtn.setOnClickListener{
val currentDate = Calendar.getInstance()
val date = Calendar.getInstance()
val datepicker = DatePickerDialog(
requireContext(),
{ view, year, monthOfYear, dayOfMonth ->
date[year, monthOfYear] = dayOfMonth
TimePickerDialog(context,
{ _, hourOfDay, minute ->
date[Calendar.HOUR_OF_DAY] = hourOfDay
date[Calendar.MINUTE] = minute
val item: DownloadItem = getDownloadItem();
item.downloadStartTime = date.timeInMillis
downloadViewModel.queueDownloads(listOf(item))
dismiss()
},
currentDate.get(Calendar.HOUR_OF_DAY),
currentDate.get(Calendar.MINUTE),
false
).show()
},
currentDate.get(Calendar.YEAR),
currentDate.get(Calendar.MONTH),
currentDate.get(Calendar.DATE)
)
datepicker.datePicker.minDate = System.currentTimeMillis() - 1000
datepicker.show()
}
val cancelBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_cancel_button) val cancelBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_cancel_button)
cancelBtn.setOnClickListener{ cancelBtn.setOnClickListener{
dismiss() dismiss()
@ -110,25 +144,29 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
val download = view.findViewById<Button>(R.id.bottomsheet_download_button) val download = view.findViewById<Button>(R.id.bottomsheet_download_button)
download!!.setOnClickListener { download!!.setOnClickListener {
val item: DownloadItem = when(tabLayout.selectedTabPosition){ val item: DownloadItem = getDownloadItem();
0 -> {
val f = fragmentManager.findFragmentByTag("f0") as DownloadAudioFragment
f.downloadItem
}
1 -> {
val f = fragmentManager.findFragmentByTag("f1") as DownloadVideoFragment
f.downloadItem
}
else -> {
val f = fragmentManager.findFragmentByTag("f2") as DownloadCommandFragment
f.downloadItem
}
}
downloadViewModel.queueDownloads(listOf(item)) downloadViewModel.queueDownloads(listOf(item))
dismiss() dismiss()
} }
} }
private fun getDownloadItem() : DownloadItem{
when(tabLayout.selectedTabPosition){
0 -> {
val f = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
return f.downloadItem
}
1 -> {
val f = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
return f.downloadItem
}
else -> {
val f = fragmentManager?.findFragmentByTag("f2") as DownloadCommandFragment
return f.downloadItem
}
}
}
override fun onCancel(dialog: DialogInterface) { override fun onCancel(dialog: DialogInterface) {
super.onCancel(dialog) super.onCancel(dialog)
cleanUp() cleanUp()

View file

@ -1,25 +1,48 @@
package com.deniscerri.ytdlnis.ui.downloadcard package com.deniscerri.ytdlnis.ui.downloadcard
import android.app.Activity import android.app.Activity
import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
import com.deniscerri.ytdlnis.database.models.Format import com.deniscerri.ytdlnis.database.models.Format
import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil
import com.google.android.material.chip.Chip
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.launch
class DownloadCommandFragment(private val resultItem: ResultItem) : Fragment() { class DownloadCommandFragment(private val resultItem: ResultItem) : Fragment() {
private var _binding : FragmentHomeBinding? = null private var _binding : FragmentHomeBinding? = null
private var fragmentView: View? = null private var fragmentView: View? = null
private var activity: Activity? = null private var activity: Activity? = null
private var mainActivity: MainActivity? = null private var mainActivity: MainActivity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var fileUtil : FileUtil
lateinit var downloadItem : DownloadItem private lateinit var saveDir : TextInputLayout
private lateinit var commandTemplateDao : CommandTemplateDao
lateinit var downloadItem: DownloadItem
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
@ -30,13 +53,113 @@ class DownloadCommandFragment(private val resultItem: ResultItem) : Fragment() {
fragmentView = inflater.inflate(R.layout.fragment_download_command, container, false) fragmentView = inflater.inflate(R.layout.fragment_download_command, container, false)
activity = getActivity() activity = getActivity()
mainActivity = activity as MainActivity? mainActivity = activity as MainActivity?
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
downloadItem = downloadViewModel.createDownloadItemFromResult(resultItem, DownloadViewModel.Type.command)
fileUtil = FileUtil()
commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao
return fragmentView return fragmentView
}
//get formats override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// val templates = repository.getTemplates() super.onViewCreated(view, savedInstanceState)
// templates.forEach { lifecycleScope.launch {
// formats.add(Format(0, item.id, it.title, "", 0, it.content))
// } val sharedPreferences = requireContext().getSharedPreferences("root_preferences", Activity.MODE_PRIVATE)
try {
val commands = commandTemplateDao.getAllTemplates()
val id = sharedPreferences.getLong("commandTemplate", commands[0].id)
val chosenCommand = commands.find { it.id == id }
downloadItem.format = Format(
chosenCommand!!.title,
"",
0,
chosenCommand.content
)
val templates = commandTemplateDao.getAllTemplates()
val templateTitles = templates.map {it.title}
val commandTemplates = view.findViewById<TextInputLayout>(R.id.template)
val autoCompleteTextView =
view.findViewById<AutoCompleteTextView>(R.id.template_textview)
autoCompleteTextView?.setAdapter(
ArrayAdapter(
requireContext(),
android.R.layout.simple_dropdown_item_1line,
templateTitles
)
)
if (templateTitles.isNotEmpty()) {
autoCompleteTextView!!.setText(downloadItem.format.format_id, false)
}
(commandTemplates!!.editText as AutoCompleteTextView?)!!.onItemClickListener =
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, index: Int, _: Long ->
// TODO
}
val templateContent = view.findViewById<TextView>(R.id.template_content_textview)
templateContent.text = downloadItem.format.format_note
saveDir = view.findViewById(R.id.outputPath)
val downloadPath = sharedPreferences.getString(
"command_path",
getString(R.string.command_path)
)
downloadItem.downloadPath = downloadPath!!
saveDir.editText!!.setText(
fileUtil.formatPath(downloadPath)
)
saveDir.editText!!.isFocusable = false;
saveDir.editText!!.isClickable = true;
saveDir.editText!!.setOnClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
commandPathResultLauncher.launch(intent)
}
val embedSubs = view.findViewById<Chip>(R.id.embed_subtitles)
embedSubs!!.isChecked = embedSubs.isChecked
embedSubs.setOnClickListener {
downloadItem.embedSubs = embedSubs.isChecked
}
val addChapters = view.findViewById<Chip>(R.id.add_chapters)
addChapters!!.isChecked = addChapters.isChecked
addChapters.setOnClickListener{
downloadItem.addChapters = addChapters.isChecked
}
val saveThumbnail = view.findViewById<Chip>(R.id.save_thumbnail)
saveThumbnail!!.isChecked = saveThumbnail.isChecked
saveThumbnail.setOnClickListener {
downloadItem.SaveThumb = saveThumbnail.isChecked
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
private var commandPathResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
result.data?.data?.let {
activity?.contentResolver?.takePersistableUriPermission(
it,
Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
)
}
downloadItem.downloadPath = result.data?.data.toString()
//downloadviewmodel.updateDownload(downloadItem)
saveDir.editText?.setText(fileUtil.formatPath(result.data?.data.toString()), TextView.BufferType.EDITABLE)
}
} }
} }

View file

@ -1,7 +1,9 @@
package com.deniscerri.ytdlnis.ui.downloadcard package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.DatePickerDialog
import android.app.Dialog import android.app.Dialog
import android.app.TimePickerDialog
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -31,12 +33,14 @@ import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.*
class DownloadMultipleBottomSheetDialog(private val items: List<DownloadItem>) : BottomSheetDialogFragment(), ConfigureMultipleDownloadsAdapter.OnItemClickListener, View.OnClickListener { class DownloadMultipleBottomSheetDialog(private val items: List<DownloadItem>) : BottomSheetDialogFragment(), ConfigureMultipleDownloadsAdapter.OnItemClickListener, View.OnClickListener {
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var listAdapter : ConfigureMultipleDownloadsAdapter private lateinit var listAdapter : ConfigureMultipleDownloadsAdapter
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var behavior: BottomSheetBehavior<View>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -58,6 +62,12 @@ class DownloadMultipleBottomSheetDialog(private val items: List<DownloadItem>) :
dialog.setContentView(view) dialog.setContentView(view)
view.minimumHeight = resources.displayMetrics.heightPixels view.minimumHeight = resources.displayMetrics.heightPixels
dialog.setOnShowListener {
behavior = BottomSheetBehavior.from(view.parent as View)
behavior.skipCollapsed = true
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
listAdapter = listAdapter =
ConfigureMultipleDownloadsAdapter( ConfigureMultipleDownloadsAdapter(
this, this,
@ -76,6 +86,39 @@ class DownloadMultipleBottomSheetDialog(private val items: List<DownloadItem>) :
} }
} }
val scheduleBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_schedule_button)
scheduleBtn.setOnClickListener{
val currentDate = Calendar.getInstance()
val date = Calendar.getInstance()
val datepicker = DatePickerDialog(
requireContext(),
{ view, year, monthOfYear, dayOfMonth ->
date[year, monthOfYear] = dayOfMonth
TimePickerDialog(context,
{ _, hourOfDay, minute ->
date[Calendar.HOUR_OF_DAY] = hourOfDay
date[Calendar.MINUTE] = minute
items.forEach {
it.downloadStartTime = date.timeInMillis
}
downloadViewModel.queueDownloads(items)
dismiss()
},
currentDate.get(Calendar.HOUR_OF_DAY),
currentDate.get(Calendar.MINUTE),
false
).show()
},
currentDate.get(Calendar.YEAR),
currentDate.get(Calendar.MONTH),
currentDate.get(Calendar.DATE)
)
datepicker.datePicker.minDate = System.currentTimeMillis() - 1000
datepicker.show()
}
val cancelBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_cancel_button) val cancelBtn = view.findViewById<MaterialButton>(R.id.bottomsheet_cancel_button)
cancelBtn.setOnClickListener{ cancelBtn.setOnClickListener{
dismiss() dismiss()

View file

@ -174,28 +174,23 @@ class DownloadVideoFragment(private val resultItem: ResultItem) : Fragment() {
val embedSubs = view.findViewById<Chip>(R.id.embed_subtitles) val embedSubs = view.findViewById<Chip>(R.id.embed_subtitles)
embedSubs!!.isChecked = embedSubs.isChecked embedSubs!!.isChecked = downloadItem.embedSubs
embedSubs.setOnClickListener { embedSubs.setOnClickListener {
downloadItem.embedSubs = embedSubs.isChecked downloadItem.embedSubs = embedSubs.isChecked
} }
val addChapters = view.findViewById<Chip>(R.id.add_chapters) val addChapters = view.findViewById<Chip>(R.id.add_chapters)
addChapters!!.isChecked = addChapters.isChecked addChapters!!.isChecked = downloadItem.addChapters
addChapters.setOnClickListener{ addChapters.setOnClickListener{
downloadItem.addChapters = addChapters.isChecked downloadItem.addChapters = addChapters.isChecked
} }
val saveThumbnail = view.findViewById<Chip>(R.id.save_thumbnail) val saveThumbnail = view.findViewById<Chip>(R.id.save_thumbnail)
saveThumbnail!!.isChecked = saveThumbnail.isChecked saveThumbnail!!.isChecked = downloadItem.SaveThumb
saveThumbnail.setOnClickListener { saveThumbnail.setOnClickListener {
downloadItem.SaveThumb = saveThumbnail.isChecked downloadItem.SaveThumb = saveThumbnail.isChecked
} }
val removeAudio = view.findViewById<Chip>(R.id.remove_audio)
removeAudio.setOnClickListener {
downloadItem.removeAudio = removeAudio.isChecked
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
} }

View file

@ -12,6 +12,7 @@ import androidx.work.ForegroundInfo
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.workDataOf import androidx.work.workDataOf
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.MainActivity
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.DBManager import com.deniscerri.ytdlnis.database.DBManager
@ -101,8 +102,12 @@ class DownloadWorker(
request.addOption("--sponsorblock-remove", filters) request.addOption("--sponsorblock-remove", filters)
} }
request.addCommands(listOf("--replace-in-metadata","title",".*.",downloadItem.title)); if(downloadItem.title.isNotEmpty()){
request.addCommands(listOf("--replace-in-metadata","uploader",".*.",downloadItem.author)); request.addCommands(listOf("--replace-in-metadata","title",".*.",downloadItem.title));
}
if (downloadItem.author.isNotEmpty()){
request.addCommands(listOf("--replace-in-metadata","uploader",".*.",downloadItem.author));
}
when(type){ when(type){
DownloadViewModel.Type.audio -> { DownloadViewModel.Type.audio -> {
@ -152,13 +157,16 @@ class DownloadWorker(
if (embedSubs) { if (embedSubs) {
request.addOption("--embed-subs", "") request.addOption("--embed-subs", "")
} }
val defaultFormats = context.resources.getStringArray(R.array.video_formats)
var videoFormatID = downloadItem.format.format_id var videoFormatID = downloadItem.format.format_id
Log.e(TAG, videoFormatID) Log.e(TAG, videoFormatID)
var formatArgument = "bestvideo+bestaudio/best" var formatArgument = "bestvideo+bestaudio/best"
if (videoFormatID.isNotEmpty()) { if (videoFormatID.isNotEmpty()) {
if (videoFormatID == "Best Quality") videoFormatID = "bestvideo" if (videoFormatID == "Best Quality") videoFormatID = "bestvideo"
else if (videoFormatID == "Worst Quality") videoFormatID = "worst" else if (videoFormatID == "Worst Quality") videoFormatID = "worst"
formatArgument = videoFormatID + if (downloadItem.removeAudio) "" else "+bestaudio/$videoFormatID" else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]"
formatArgument = videoFormatID + "+bestaudio/best/$videoFormatID"
} }
Log.e(TAG, formatArgument) Log.e(TAG, formatArgument)
request.addOption("-f", formatArgument) request.addOption("-f", formatArgument)

View file

@ -8,8 +8,9 @@
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="20dp" android:paddingTop="20dp"
android:paddingBottom="0dp" android:paddingBottom="0dp"
android:paddingHorizontal="20dp"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/bottom_sheet_title" android:id="@+id/bottom_sheet_title"

View file

@ -96,8 +96,7 @@
style="@style/Widget.Material3.TextInputLayout.FilledBox" style="@style/Widget.Material3.TextInputLayout.FilledBox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:hint="@string/save_dir" android:hint="@string/save_dir">
app:errorEnabled="true">
<com.google.android.material.textfield.TextInputEditText <com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -11,76 +11,15 @@
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/title_textinput" android:id="@+id/template"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:errorEnabled="true"
android:hint="@string/title"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="text"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/author_textinput"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="40"
app:errorEnabled="true"
android:hint="@string/author"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="text"
android:layout_height="wrap_content"
/>
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/downloadContainer"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="0dp"
android:layout_weight="45"
android:paddingStart="10dp"
android:layout_height="wrap_content"
android:hint="@string/container">
<AutoCompleteTextView
android:id="@+id/container_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
app:simpleItems="@array/audio_containers"
/>
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/format"
style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu" style="@style/Widget.Material3.TextInputLayout.FilledBox.ExposedDropdownMenu"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingBottom="10dp" android:paddingBottom="10dp"
android:hint="@string/video_format"> android:hint="@string/commands">
<AutoCompleteTextView <AutoCompleteTextView
android:id="@+id/format_textview" android:id="@+id/template_textview"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:inputType="none" android:inputType="none"
@ -89,12 +28,25 @@
</com.google.android.material.textfield.TextInputLayout> </com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.card.MaterialCardView
style="@style/Widget.Material3.CardView.Filled"
android:layout_width="match_parent"
android:layout_marginBottom="10dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/template_content_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="20dp"/>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.textfield.TextInputLayout <com.google.android.material.textfield.TextInputLayout
android:id="@+id/outputPath" android:id="@+id/outputPath"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:errorEnabled="true"
android:hint="@string/save_dir" android:hint="@string/save_dir"
style="@style/Widget.Material3.TextInputLayout.FilledBox"> style="@style/Widget.Material3.TextInputLayout.FilledBox">
@ -108,6 +60,7 @@
<LinearLayout <LinearLayout
android:id="@+id/adjust_commands" android:id="@+id/adjust_commands"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:padding="10dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
> >

View file

@ -95,7 +95,6 @@
android:id="@+id/outputPath" android:id="@+id/outputPath"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:errorEnabled="true"
android:hint="@string/save_dir" android:hint="@string/save_dir"
style="@style/Widget.Material3.TextInputLayout.FilledBox"> style="@style/Widget.Material3.TextInputLayout.FilledBox">
@ -109,6 +108,7 @@
<LinearLayout <LinearLayout
android:id="@+id/adjust_video" android:id="@+id/adjust_video"
android:layout_width="match_parent" android:layout_width="match_parent"
android:padding="10dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical" android:orientation="vertical"
> >
@ -120,46 +120,45 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="@string/adjust_video" /> android:text="@string/adjust_video" />
<com.google.android.material.chip.ChipGroup <HorizontalScrollView
android:id="@+id/chipGroup"
android:layout_width="wrap_content" android:layout_width="wrap_content"
app:singleLine="false"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<com.google.android.material.chip.Chip <com.google.android.material.chip.ChipGroup
android:id="@+id/embed_subtitles" android:id="@+id/chipGroup"
style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" app:singleLine="false"
android:checked="false" android:layout_height="wrap_content">
android:text="@string/embed_subtitles"/>
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/add_chapters" android:id="@+id/embed_subtitles"
style="@style/Widget.Material3.Chip.Filter.Elevated" style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="false" android:checked="false"
android:text="@string/add_chapter"/> android:text="@string/embed_subtitles"/>
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/save_thumbnail" android:id="@+id/add_chapters"
style="@style/Widget.Material3.Chip.Filter.Elevated" style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="false" android:checked="false"
android:text="@string/save_thumb"/> android:text="@string/add_chapter"/>
<com.google.android.material.chip.Chip <com.google.android.material.chip.Chip
android:id="@+id/remove_audio" android:id="@+id/save_thumbnail"
style="@style/Widget.Material3.Chip.Filter.Elevated" style="@style/Widget.Material3.Chip.Filter.Elevated"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:checked="false" android:checked="false"
android:text="@string/remove_audio"/> android:text="@string/save_thumb"/>
</com.google.android.material.chip.ChipGroup> </com.google.android.material.chip.ChipGroup>
</HorizontalScrollView>
</LinearLayout> </LinearLayout>

View file

@ -18,7 +18,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:layout_scrollFlags="scroll|enterAlways|snap" app:layout_scrollFlags="scroll|enterAlways|snap"
app:title="@string/history" app:title="@string/downloads"
app:menu="@menu/history_menu" app:menu="@menu/history_menu"
android:theme="@style/Toolbar" android:theme="@style/Toolbar"
/> />

View file

@ -4,12 +4,9 @@
<item android:title="@string/home" <item android:title="@string/home"
android:icon="@drawable/ic_home" android:icon="@drawable/ic_home"
android:id="@+id/home"/> android:id="@+id/home"/>
<item android:title="@string/history"
android:icon="@drawable/ic_history"
android:id="@+id/history"/>
<item android:title="@string/downloads" <item android:title="@string/downloads"
android:icon="@drawable/ic_downloads" android:icon="@drawable/ic_downloads"
android:id="@+id/downloads"/> android:id="@+id/history"/>
<item android:title="@string/more" <item android:title="@string/more"
android:icon="@drawable/ic_more" android:icon="@drawable/ic_more"
android:id="@+id/more"/> android:id="@+id/more"/>

View file

@ -163,6 +163,6 @@
<string name="defaultValue">Default</string> <string name="defaultValue">Default</string>
<string name="container">Container</string> <string name="container">Container</string>
<string name="template">Template</string> <string name="template">Template</string>
<string name="history">History</string>
<string name="logs">Logs</string> <string name="logs">Logs</string>
<string name="commands">Commands</string>
</resources> </resources>

View file

@ -12,7 +12,7 @@ buildscript {
versionCode = versionMajor * 100000 + versionMinor * 1000 + versionPatch * 100 + versionBuild versionCode = versionMajor * 100000 + versionMinor * 1000 + versionPatch * 100 + versionBuild
versionName = "${versionMajor}.${versionMinor}.${versionPatch}" versionName = "${versionMajor}.${versionMinor}.${versionPatch}"
// dependency versions // dependency versions
appCompatVer = '1.6.0' appCompatVer = '1.6.1'
junitVer = '4.13.2' junitVer = '4.13.2'
androidJunitVer = '1.1.5' androidJunitVer = '1.1.5'
espressoVer = '3.5.1' espressoVer = '3.5.1'
@ -22,7 +22,7 @@ buildscript {
// supports java 1.6 // supports java 1.6
commonsCompressVer = '1.12' commonsCompressVer = '1.12'
youtubedlAndroidVer = "a3b971724d" youtubedlAndroidVer = "a3b971724d"
workVer = "2.7.1" workVer = "2.8.0"
composeVer = "1.4.0-alpha02" composeVer = "1.4.0-alpha02"
kotlinVer = "1.7.21" kotlinVer = "1.7.21"
coroutineVer = "1.6.4" coroutineVer = "1.6.4"