pull/262/head
deniscerri 3 years ago
parent 025ec65c27
commit 6b89d3e3fd
No known key found for this signature in database
GPG Key ID: 95C43D517D830350

@ -10,7 +10,7 @@ def properties = new Properties()
def versionMajor = 1
def versionMinor = 6
def versionPatch = 5
def versionBuild = 2 // bump for dogfood builds, public betas, etc.
def versionBuild = 4 // bump for dogfood builds, public betas, etc.
def versionExt = ""
if (versionBuild > 0){
@ -63,6 +63,7 @@ android {
release {
minifyEnabled true
shrinkResources true
manifestPlaceholders = [appName: RELEASE_NAME]
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false
signingConfig signingConfigs.debug
@ -71,6 +72,7 @@ android {
beta {
minifyEnabled true
shrinkResources true
manifestPlaceholders = [appName: BETA_NAME]
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false
signingConfig signingConfigs.debug
@ -80,6 +82,7 @@ android {
debug {
minifyEnabled false
manifestPlaceholders = [appName: RELEASE_NAME]
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
debuggable true
signingConfig signingConfigs.debug
@ -87,6 +90,7 @@ android {
benchmark {
initWith release
manifestPlaceholders = [appName: RELEASE_NAME]
proguardFiles 'baseline-profiles-rules.pro'
debuggable false
matchingFallbacks ['release']

@ -15,16 +15,20 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<application
android:name=".App"
android:allowBackup="false"
android:configChanges="layoutDirection|locale"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true"
android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:label="${appName}"
android:localeConfig="@xml/locales_config"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
@ -38,14 +42,14 @@
<activity
android:name=".MainActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:exported="true"
android:windowSoftInputMode="adjustPan">
</activity>
<activity
android:name=".receiver.ShareActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleInstance"
@ -71,7 +75,7 @@
</activity>
<activity
android:name=".receiver.ShareFileActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleInstance"
@ -92,7 +96,7 @@
</activity>
<activity
android:name=".ui.more.TerminalActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:exported="true"
android:label="@string/terminal"
android:parentActivityName=".MainActivity">
@ -122,7 +126,7 @@
android:enabled="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:targetActivity=".MainActivity"
android:windowSoftInputMode="adjustPan"
android:exported="true">
@ -144,7 +148,7 @@
android:exported="true"
android:icon="@mipmap/ic_launcher_dark"
android:roundIcon="@mipmap/ic_launcher_round_dark"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:targetActivity=".MainActivity"
android:windowSoftInputMode="adjustPan">
<intent-filter>
@ -164,7 +168,7 @@
android:exported="true"
android:icon="@mipmap/ic_launcher_light"
android:roundIcon="@mipmap/ic_launcher_round_light"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:targetActivity=".MainActivity"
android:windowSoftInputMode="adjustPan">
<intent-filter>
@ -180,7 +184,7 @@
<activity
android:name=".ui.more.WebViewActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:exported="true"
android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize">
@ -193,10 +197,12 @@
<receiver android:name=".receiver.CancelDownloadNotificationReceiver" />
<receiver android:name=".receiver.PauseDownloadNotificationReceiver" />
<receiver android:name=".receiver.AlarmStartReceiver" />
<receiver android:name=".receiver.AlarmStopReceiver" />
<activity
android:name=".receiver.ResumeActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:excludeFromRecents="true"
android:exported="true"
android:launchMode="singleInstance"

@ -12,33 +12,25 @@ import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.provider.Settings
import android.util.Log
import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets
import android.widget.CheckBox
import android.widget.TextView
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.navOptions
import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
@ -60,7 +52,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.Reader
@ -157,7 +148,6 @@ class MainActivity : BaseActivity() {
activeDownloadsBadge.number = it
}
}
window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this)
}
if (navigationView is NavigationView){
(navigationView as NavigationView).setupWithNavController(navController)

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

@ -7,12 +7,15 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface LogDao {
@Query("SELECT * FROM logs ORDER BY id DESC")
@Query("SELECT id, title, downloadType, format, downloadTime, '' as content FROM logs ORDER BY id DESC")
fun getAllLogs() : List<LogItem>
@Query("SELECT * FROM logs ORDER BY id DESC")
@Query("SELECT id, title, downloadType, format, downloadTime, '' as content FROM logs ORDER BY id DESC")
fun getAllLogsFlow() : Flow<List<LogItem>>
@Query("SELECT * FROM logs WHERE id=:id LIMIT 1")
fun getLogFlowByID(id: Long) : Flow<LogItem>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: LogItem) : Long

@ -12,6 +12,10 @@ class LogRepository(private val logDao: LogDao) {
return logDao.getAllLogs()
}
fun getLogFlowByID(id: Long) : Flow<LogItem> {
return logDao.getLogFlowByID(id)
}
suspend fun insert(item: LogItem) : Long{
return logDao.insert(item)

@ -38,14 +38,19 @@ import com.deniscerri.ytdlnis.database.models.VideoPreferences
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
import java.util.concurrent.TimeUnit
@ -554,10 +559,17 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
suspend fun queueDownloads(items: List<DownloadItem>) = CoroutineScope(Dispatchers.IO).launch {
val context = App.instance
val alarmScheduler = AlarmScheduler(context)
val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads()
val queuedItems = mutableListOf<DownloadItem>()
var exists = false
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
alarmScheduler.schedule()
}
items.forEach {
if (it.status != DownloadRepository.Status.Paused.toString()) it.status = DownloadRepository.Status.Queued.toString()
if (activeAndQueuedDownloads.firstOrNull{d ->
@ -586,7 +598,23 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
}
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || items.any { it.downloadStartTime > 0L } ){
startDownloadWorker(queuedItems)
if(!useScheduler){
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){}
}
}else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
try{
updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao)
}catch (ignored: Exception){}
}
}
}
}
private suspend fun startDownloadWorker(queuedItems: List<DownloadItem>) {
@ -598,12 +626,12 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
if (currentWork.size == 0 || currentWork.none{ it.state == WorkInfo.State.RUNNING } || (queuedItems.isNotEmpty() && queuedItems[0].downloadStartTime != 0L)){
val currentTime = System.currentTimeMillis()
var delay = 1000L
var delay = 0L
if (queuedItems.isNotEmpty()){
delay = if (queuedItems[0].downloadStartTime != 0L){
queuedItems[0].downloadStartTime.minus(currentTime)
} else 0
if (delay <= 0L) delay = 1000L
if (delay <= 60000L) delay = 0L
}
@ -635,12 +663,6 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show()
}
}
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 {

@ -1,6 +1,7 @@
package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData
@ -10,6 +11,7 @@ import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
class LogViewModel(private val application: Application) : AndroidViewModel(application) {
@ -24,6 +26,10 @@ class LogViewModel(private val application: Application) : AndroidViewModel(appl
}
fun getLogFlowByID(id: Long) : LiveData<LogItem> {
return repository.getLogFlowByID(id).asLiveData()
}
fun getItemById(id: Long): LogItem{
return repository.getItem(id)
}

@ -0,0 +1,30 @@
package com.deniscerri.ytdlnis.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.work.Constraints
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.work.DownloadWorker
import java.util.concurrent.TimeUnit
class AlarmStartReceiver : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
Log.e("assa", "Start")
val workConstraints = Constraints.Builder()
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(1000L, TimeUnit.MILLISECONDS)
WorkManager.getInstance(p0!!).enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
}

@ -0,0 +1,52 @@
package com.deniscerri.ytdlnis.receiver
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.database.DBManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.NotificationUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class AlarmStopReceiver : BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
val dbManager = DBManager.getInstance(p0!!)
val ytdl = YoutubeDL.getInstance()
val notificationUtil = NotificationUtil(p0)
val preferences = PreferenceManager.getDefaultSharedPreferences(p0)
val alarmScheduler = AlarmScheduler(p0)
CoroutineScope(Dispatchers.IO).launch {
runCatching {
Log.e("assa", "assssssssssssssssssssssssssssss")
val active = dbManager.downloadDao.getActiveDownloadsList()
val startingTime = preferences.getString("schedule_start", "00:00")!!
val sTime = Calendar.getInstance()
sTime.set(Calendar.HOUR_OF_DAY, startingTime.split(":")[0].toInt())
sTime.set(Calendar.MINUTE, startingTime.split(":")[1].toInt())
sTime.set(Calendar.SECOND, 0)
val time = alarmScheduler.calculateNextTime(sTime)
active.forEach {
ytdl.destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
it.status = DownloadRepository.Status.Queued.toString()
dbManager.downloadDao.update(it)
}
AlarmScheduler(p0).schedule()
}
}
}
}

@ -114,6 +114,7 @@ class ShareActivity : BaseActivity() {
loadingBottomSheet.show()
val cancel = loadingBottomSheet.findViewById<TextView>(R.id.cancel)
cancel!!.setOnClickListener {
resultViewModel.deleteAll()
this.finish()
}

@ -8,10 +8,12 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.ThemeUtil
import com.google.android.material.elevation.SurfaceColors
open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtil.updateTheme(this)
window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this)
super.onCreate(savedInstanceState)
}
}

@ -28,7 +28,9 @@ import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
@ -78,7 +80,10 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri
overScrollMode = View.OVER_SCROLL_NEVER
}
val fragments = mutableListOf(DownloadAudioFragment(resultItem, downloadItem), DownloadVideoFragment(resultItem, downloadItem))
val fragments = mutableListOf(
DownloadAudioFragment(resultItem, downloadItem),
DownloadVideoFragment(resultItem, downloadItem)
)
var commandTemplateNr = 0
lifecycleScope.launch{
withContext(Dispatchers.IO){

@ -1,30 +1,18 @@
package com.deniscerri.ytdlnis.ui.downloadcard
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -36,21 +24,15 @@ import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.databinding.ExtraCommandsBottomSheetBinding
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File

@ -38,6 +38,7 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import java.text.SimpleDateFormat
import java.util.*
@ -80,7 +81,6 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
}
}
tabLayout = view.findViewById(R.id.download_tablayout)
viewPager2 = view.findViewById(R.id.download_viewpager)
@ -88,6 +88,7 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
isNestedScrollingEnabled = false
overScrollMode = View.OVER_SCROLL_NEVER
}
downloadAudioFragment = DownloadAudioFragment(resultItem, downloadItem)
downloadVideoFragment = DownloadVideoFragment(resultItem, downloadItem)
val fragments = mutableListOf(downloadAudioFragment, downloadVideoFragment)

@ -7,6 +7,7 @@ import android.content.Intent
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
@ -185,8 +186,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
},
editSelectedClicked =
{
var current = templates.find { it.id == commandTemplateCard.findViewById<android.widget.TextView>(
R.id.id).toString().toLong() }
var current = templates.find { it.id == commandTemplateCard.tag }
if (current == null) current =
CommandTemplate(
0,
@ -197,7 +197,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it)
chosenCommandView.editText!!.setText(it.content)
downloadItem.format = com.deniscerri.ytdlnis.database.models.Format(
populateCommandCard(commandTemplateCard, it)
downloadItem.format = Format(
it.title,
"",
"",

@ -2,31 +2,18 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.icu.text.IDNA.Info
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.AdapterView
import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.EditText
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -42,10 +29,7 @@ import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers

@ -16,6 +16,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem
@ -131,7 +132,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
WorkManager.getInstance(requireContext())
.getWorkInfosByTagLiveData("download")
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING))
.observe(viewLifecycleOwner){ list ->
list.forEach {work ->
if (work == null) return@forEach

@ -9,6 +9,7 @@ import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.FileObserver
import android.os.PersistableBundle
import android.util.Log
import android.view.MenuItem
import android.view.View
@ -76,7 +77,7 @@ class TerminalActivity : BaseActivity() {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_terminal)
downloadID = System.currentTimeMillis().toInt() % 100000
downloadID = savedInstanceState?.getInt("downloadID") ?: (System.currentTimeMillis().toInt() % 100000)
downloadFile = File(cacheDir.absolutePath + "/$downloadID.txt")
if (! downloadFile.exists()) downloadFile.createNewFile()
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
@ -233,6 +234,22 @@ class TerminalActivity : BaseActivity() {
input?.enableTextHighlight()
output?.enableTextHighlight()
input?.setText(savedInstanceState?.getString("input") ?: "yt-dlp ")
input!!.requestFocus()
input!!.setSelection(input!!.text.length)
output?.text = savedInstanceState?.getString("output") ?: ""
if (savedInstanceState?.getBoolean("run") == true){
showCancelFab()
}
}
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
super.onSaveInstanceState(outState)
outState.putString("input", input?.text.toString())
outState.putString("output", output?.text.toString())
outState.putBoolean("run", fab!!.text == getString(R.string.run_command))
outState.putInt("downloadID", downloadID)
}
private fun initMenu() {

@ -25,9 +25,11 @@ import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.regex.Pattern
import kotlin.math.log
class DownloadLogFragment : Fragment() {
@ -78,7 +80,7 @@ class DownloadLogFragment : Fragment() {
logViewModel = ViewModelProvider(this)[LogViewModel::class.java]
logViewModel.items.map { it.find { log -> log.id == id } }.observe(viewLifecycleOwner) { logItem ->
logViewModel.getLogFlowByID(id!!).observe(viewLifecycleOwner){logItem ->
mainActivity.runOnUiThread{
content.text = logItem?.content
content.scrollTo(0, content.height)
@ -86,9 +88,8 @@ class DownloadLogFragment : Fragment() {
}
}
CoroutineScope(Dispatchers.IO).launch {
val logItem = logViewModel.getItemById(id!!)
val logItem = logViewModel.getItemById(id)
topAppBar.title = logItem.title
}

@ -26,12 +26,15 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.DownloadLogsAdapter
import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel
import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll
import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListener {
@ -69,6 +72,8 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
)
recyclerView = view.findViewById(R.id.logs_recyclerview)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.enableFastScroll()
recyclerView.forceFastScrollMode()
recyclerView.adapter = downloadLogAdapter
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){
@ -230,7 +235,10 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
val position = viewHolder.bindingAdapterPosition
when (direction) {
ItemTouchHelper.LEFT -> {
val deletedItem = items[position]
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
logViewModel.getItemById(items[position].id)
}
logViewModel.delete(deletedItem)
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
@ -239,6 +247,7 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
}
}.show()
}
}
}
}

@ -1,12 +1,11 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.os.Bundle
import android.view.View
import android.content.DialogInterface
import android.view.inputmethod.InputMethodManager
import android.widget.ListView
import androidx.appcompat.app.AppCompatActivity
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.deniscerri.ytdlnis.R
@ -46,6 +45,24 @@ abstract class BaseSettingsFragment : PreferenceFragmentCompat() {
.setNegativeButton(R.string.cancel, null)
.show()
}
is MultiSelectListPreference -> {
val selectedItems = preference.entryValues.map {
preference.values.contains(it)
}.toBooleanArray()
MaterialAlertDialogBuilder(requireContext())
.setTitle(preference.title)
.setMultiChoiceItems(preference.entries, selectedItems) { _, _, _ ->
val newValues = preference.entryValues
.filterIndexed { index, _ -> selectedItems[index] }
.map { it.toString() }
.toMutableSet()
if (preference.callChangeListener(newValues)) {
preference.values = newValues
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
is EditTextPreference -> {
val binding = TextinputBinding.inflate(layoutInflater)
binding.urlEdittext.setText(preference.text)

@ -1,43 +1,68 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.app.Activity
import android.content.Context.POWER_SERVICE
import android.app.PendingIntent
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import androidx.preference.Preference
import androidx.preference.SeekBarPreference
import com.deniscerri.ytdlnis.MainActivity
import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import com.deniscerri.ytdlnis.R
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.deniscerri.ytdlnis.receiver.AlarmStartReceiver
import com.deniscerri.ytdlnis.receiver.AlarmStopReceiver
import com.deniscerri.ytdlnis.util.UiUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler
import java.util.Calendar
class DownloadSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.downloads
private var concurrentDownloads: SeekBarPreference? = null
private var ignoreBatteryOptimization: Preference? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.downloading_preferences, rootKey)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val useScheduler = findPreference<SwitchPreferenceCompat>("use_scheduler")
val scheduleStart = findPreference<Preference>("schedule_start")
scheduleStart?.summary = preferences.getString("schedule_start", "00:00")
val scheduleEnd = findPreference<Preference>("schedule_end")
scheduleEnd?.summary = preferences.getString("schedule_end", "05:00")
val scheduler = AlarmScheduler(requireContext())
useScheduler?.setOnPreferenceChangeListener { preference, newValue ->
if (newValue as Boolean){
scheduler.schedule()
}else{
scheduler.cancel()
//start worker if there are leftover downloads waiting for scheduler
val intent = Intent(context, AlarmStartReceiver::class.java)
requireActivity().sendBroadcast(intent)
}
true
}
scheduleStart?.setOnPreferenceClickListener {
UiUtil.showTimePicker(parentFragmentManager){
val hr = it.get(Calendar.HOUR_OF_DAY)
val mn = it.get(Calendar.MINUTE)
val formattedTime = String.format("%02d", hr) + ":" + String.format("%02d", mn)
preferences.edit().putString("schedule_start",formattedTime).apply()
scheduleStart.summary = formattedTime
ignoreBatteryOptimization = findPreference("ignore_battery")
val packageName: String = requireContext().packageName
val pm = requireContext().applicationContext.getSystemService(POWER_SERVICE) as PowerManager
if (pm.isIgnoringBatteryOptimizations(packageName)) {
ignoreBatteryOptimization!!.isVisible = false
scheduler.schedule()
}
true
}
ignoreBatteryOptimization = findPreference("ignore_battery")
ignoreBatteryOptimization!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:" + requireContext().packageName)
startActivity(intent)
scheduleEnd?.setOnPreferenceClickListener {
UiUtil.showTimePicker(parentFragmentManager){
val hr = it.get(Calendar.HOUR_OF_DAY)
val mn = it.get(Calendar.MINUTE)
val formattedTime = String.format("%02d", hr) + ":" + String.format("%02d", mn)
preferences.edit().putString("schedule_end",formattedTime).apply()
scheduleEnd.summary = formattedTime
scheduler.schedule()
}
true
}
}

@ -29,6 +29,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
private var videoPath: Preference? = null
private var commandPath: Preference? = null
private var accessAllFiles : Preference? = null
private var cacheDownloads : Preference? = null
private var audioFilenameTemplate : EditTextPreference? = null
private var videoFilenameTemplate : EditTextPreference? = null
private var clearCache: Preference? = null
@ -46,6 +47,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
videoPath = findPreference("video_path")
commandPath = findPreference("command_path")
accessAllFiles = findPreference("access_all_files")
cacheDownloads = findPreference("cache_downloads")
videoFilenameTemplate = findPreference("file_name_template")
audioFilenameTemplate = findPreference("file_name_template_audio")
clearCache = findPreference("clear_cache")
@ -64,6 +66,10 @@ class FolderSettingsFragment : BaseSettingsFragment() {
if((VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) ||
VERSION.SDK_INT < 30) {
accessAllFiles!!.isVisible = false
cacheDownloads!!.isEnabled = true
}else{
editor.putBoolean("cache_downloads", false).apply()
cacheDownloads!!.isEnabled = false
}
editor.apply()

@ -1,11 +1,15 @@
package com.deniscerri.ytdlnis.ui.more.settings
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat
@ -126,5 +130,22 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
}
true
}
var ignoreBatteryOptimization = findPreference<Preference>("ignore_battery")
val packageName: String = requireContext().packageName
val pm = requireContext().applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager
if (pm.isIgnoringBatteryOptimizations(packageName)) {
ignoreBatteryOptimization!!.isVisible = false
}
ignoreBatteryOptimization = findPreference("ignore_battery")
ignoreBatteryOptimization!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val intent = Intent()
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
intent.data = Uri.parse("package:" + requireContext().packageName)
startActivity(intent)
true
}
}
}

@ -15,6 +15,8 @@ import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.copyFileTo
import com.anggrayudi.storage.file.copyFolderTo
import com.anggrayudi.storage.file.getAbsolutePath
import com.anggrayudi.storage.file.moveFileTo
import com.anggrayudi.storage.file.moveTo
import com.deniscerri.ytdlnis.App
import com.deniscerri.ytdlnis.R
import com.yausername.youtubedl_android.YoutubeDLRequest
@ -89,8 +91,47 @@ object FileUtil {
return@forEach
}
if(it.name.contains(".part-Frag")) return@forEach
runCatching {
if (File(formatPath(destDir)).canWrite()){
val files = it.listFiles()?.filter { fil -> !fil.isDirectory }?.toTypedArray() ?: arrayOf(it)
for (ff in files){
val newFile = File(dir.absolutePath + "/${ff.absolutePath.removePrefix(originDir.absolutePath)}")
runCatching {
newFile.parentFile?.mkdirs()
}
if (Build.VERSION.SDK_INT >= 26 ) {
var newFileName = newFile.absolutePath
var counter = 1
while (Files.exists(File(newFileName).toPath())) {
// If the file already exists in the destination directory, add a number to differentiate it
newFileName = newFile.absolutePath.replace(newFile.nameWithoutExtension, newFile.nameWithoutExtension+" ($counter)")
counter++
}
fileList.add(Files.move(
ff.toPath(),
File(newFileName).toPath(),
StandardCopyOption.REPLACE_EXISTING
).absolutePathString())
ff.delete()
fileList.add(newFileName)
}else{
var newFileName = newFile.absolutePath
var counter = 1
while (File(newFileName).exists()) {
// If the file already exists in the destination directory, add a number to differentiate it
newFileName = newFile.absolutePath.replace(newFile.nameWithoutExtension, newFile.nameWithoutExtension+" ($counter)")
counter++
}
ff.copyTo(File(newFileName),false)
ff.delete()
fileList.add(newFileName)
}
}
return@forEach
}
}
val curr = DocumentFile.fromFile(it)
val dst = DocumentFile.fromTreeUri(context, destDir.toUri())
@ -124,23 +165,7 @@ object FileUtil {
runCatching {
it.walkTopDown().forEach { f ->
if (f.isDirectory) return@forEach
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(f.extension) ?: "*/*"
val destUri = DocumentsContract.createDocument(
context.contentResolver,
dst.uri,
mimeType,
f.name
) ?: return@runCatching
val inputStream = f.inputStream()
val outputStream =
context.contentResolver.openOutputStream(destUri) ?: return@runCatching
inputStream.copyTo(outputStream)
inputStream.closeQuietly()
outputStream.closeQuietly()
val destUri = moveFileInputStream(it, context, dst) ?: return@forEach
fileList.add(DocumentFile.fromSingleUri(context, destUri)!!.getAbsolutePath(context))
}
@ -153,26 +178,11 @@ object FileUtil {
}
}else{
withContext(Dispatchers.IO){
curr.copyFileTo(context, dst!!, callback = object : FileCallback() {
curr.moveFileTo(context, dst!!, callback = object : FileCallback() {
override fun onFailed(errorCode: ErrorCode) {
//if its usb?
runCatching {
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*"
val destUri = DocumentsContract.createDocument(
context.contentResolver,
dst.uri,
mimeType,
it.name
) ?: return@runCatching
val inputStream = it.inputStream()
val outputStream =
context.contentResolver.openOutputStream(destUri) ?: return@runCatching
inputStream.copyTo(outputStream)
inputStream.closeQuietly()
outputStream.closeQuietly()
val destUri = moveFileInputStream(it, context, dst) ?: return
fileList.add(DocumentFile.fromSingleUri(context, destUri)!!.getAbsolutePath(context))
it.delete()
}
@ -205,42 +215,6 @@ object FileUtil {
}
}catch (e: Exception) {
Log.e("error", e.message.toString())
val files = it.listFiles()?.filter { fil -> !fil.isDirectory }?.toTypedArray() ?: arrayOf(it)
for (ff in files){
val newFile = File(dir.absolutePath + "/${ff.absolutePath.removePrefix(originDir.absolutePath)}")
runCatching {
newFile.parentFile?.mkdirs()
}
if (Build.VERSION.SDK_INT >= 26 ) {
var newFileName = newFile.absolutePath
var counter = 1
while (Files.exists(File(newFileName).toPath())) {
// If the file already exists in the destination directory, add a number to differentiate it
newFileName = newFile.absolutePath.replace(newFile.nameWithoutExtension, newFile.nameWithoutExtension+" ($counter)")
counter++
}
fileList.add(Files.move(
ff.toPath(),
File(newFileName).toPath(),
StandardCopyOption.REPLACE_EXISTING
).absolutePathString())
ff.delete()
fileList.add(newFileName)
}else{
var newFileName = newFile.absolutePath
var counter = 1
while (File(newFileName).exists()) {
// If the file already exists in the destination directory, add a number to differentiate it
newFileName = newFile.absolutePath.replace(newFile.nameWithoutExtension, newFile.nameWithoutExtension+" ($counter)")
counter++
}
ff.renameTo(File(newFileName))
fileList.add(newFileName)
}
}
return@forEach
}
}
@ -250,7 +224,28 @@ object FileUtil {
return@withContext scanMedia(fileList, context)
}
}
private fun scanMedia(files: List<String>, context: Context) : List<String> {
private fun moveFileInputStream(it: File, context: Context, dst: DocumentFile) : Uri? {
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*"
val destUri = DocumentsContract.createDocument(
context.contentResolver,
dst.uri,
mimeType,
it.name
) ?: return null
val inputStream = it.inputStream()
val outputStream =
context.contentResolver.openOutputStream(destUri) ?: return null
inputStream.copyTo(outputStream)
inputStream.closeQuietly()
outputStream.closeQuietly()
return destUri
}
fun scanMedia(files: List<String>, context: Context) : List<String> {
try {
val paths = files.sortedByDescending { File(it).length() }
runCatching {MediaScannerConnection.scanFile(context, paths.toTypedArray(), null, null) }
@ -259,7 +254,7 @@ object FileUtil {
e.printStackTrace()
}
return listOf(context.getString(R.string.unfound_file))
return listOf()
}
fun getCachePath(context: Context) : String {

@ -133,13 +133,14 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class)
fun getYoutubeVideo(url: String): List<ResultItem?> {
val theURL = url.replace("\\?list.*".toRegex(), "")
return try {
val id = getIDFromYoutubeURL(url)
val id = getIDFromYoutubeURL(theURL)
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) getFromYTDL(url) else listOf(createVideoFromPipedJSON(res, url))
if (res.length() == 0) getFromYTDL(theURL) else listOf(createVideoFromPipedJSON(res, theURL))
}catch (e: Exception){
val v = getFromYTDL(url)
v.forEach { it!!.url = url }
val v = getFromYTDL(theURL)
v.forEach { it!!.url = theURL }
v
}
}
@ -519,8 +520,11 @@ class InfoUtil(private val context: Context) {
YoutubeDL.getInstance().execute(request){ progress, _, line ->
try{
if (line.isNotBlank()){
val listOfStrings = JSONArray(line)
progress(parseYTDLFormats(listOfStrings))
}
}catch (e: Exception){
progress(emptyList())
}
@ -932,16 +936,38 @@ class InfoUtil(private val context: Context) {
}
}
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val cacheDir = FileUtil.getCachePath(context)
val tempFileDir = File(cacheDir, downloadItem.id.toString())
tempFileDir.delete()
tempFileDir.mkdirs()
fun getFilePaths(request: YoutubeDLRequest) : List<String>{
return try{
request.addOption("--print", "filename")
val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try {
val lineSeparator = System.getProperty("line.separator")
res.out.split(lineSeparator!!).toTypedArray()
} catch (e: Exception) {
arrayOf(res.out)
}
results.filter { !it.isNullOrBlank() }.map { it!!.substring(0, it.lastIndexOf(".")) }.map { it.substring(it.lastIndexOf("/") + 1, it.length) }
}catch (e: Exception){
listOf()
}
}
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{
val url = downloadItem.url
val request = YoutubeDLRequest(url)
val type = downloadItem.type
val downDir : File
if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){
downDir = File(FileUtil.formatPath(downloadItem.downloadPath))
}else{
val cacheDir = FileUtil.getCachePath(context)
downDir = File(cacheDir, downloadItem.id.toString())
downDir.delete()
downDir.mkdirs()
}
val aria2 = sharedPreferences.getBoolean("aria2", false)
if (aria2) {
request.addOption("--downloader", "libaria2c.so")
@ -1078,7 +1104,7 @@ class InfoUtil(private val context: Context) {
}
}
request.addOption("-P", tempFileDir.absolutePath)
request.addOption("-P", downDir.absolutePath)
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
@ -1270,7 +1296,7 @@ class InfoUtil(private val context: Context) {
request.addOption("--ppa", "ffmpeg:-an")
}
request.addOption("-P", tempFileDir.absolutePath)
request.addOption("-P", downDir.absolutePath)
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
@ -1283,13 +1309,13 @@ class InfoUtil(private val context: Context) {
}
DownloadViewModel.Type.command -> {
request.addOption("-P", downDir.absolutePath)
request.addOption(
"--config-locations",
File(context.cacheDir.absolutePath + "/config[${downloadItem.id}].txt").apply {
writeText(downloadItem.format.format_note)
}.absolutePath
)
request.addOption("-P", tempFileDir.absolutePath)
}
}

@ -35,6 +35,7 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager
@ -117,7 +118,7 @@ object UiUtil {
fun populateCommandCard(card: MaterialCardView, item: CommandTemplate){
card.findViewById<TextView>(R.id.title).text = item.title
card.findViewById<TextView>(R.id.content).text = item.content
card.findViewById<TextView>(R.id.id).text = item.id.toString()
card.tag = item.id
}
fun showCommandTemplateCreationOrUpdatingSheet(item: CommandTemplate?, context: Activity, lifeCycle: LifecycleOwner, commandTemplateViewModel: CommandTemplateViewModel, newTemplate: (newTemplate: CommandTemplate) -> Unit){
@ -279,51 +280,62 @@ object UiUtil {
}
fun openFileIntent(fragmentContext: Context, downloadPath: String) {
val file = File(downloadPath)
val uri = FileProvider.getUriForFile(
fragmentContext,
fragmentContext.packageName + ".fileprovider",
file
)
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension)
val i = Intent(Intent.ACTION_VIEW)
i.setDataAndType(uri, mime)
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
try {
fragmentContext.startActivity(i)
}catch (e: Exception){
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK
fragmentContext.startActivity(i)
fun openFileIntent(context: Context, downloadPath: String) {
val uri = downloadPath.runCatching {
DocumentFile.fromSingleUri(context, Uri.parse(downloadPath)).run{
if (this?.exists() == true){
this.uri
}else if (File(this@runCatching).exists()){
FileProvider.getUriForFile(context, context.packageName + ".fileprovider",
File(this@runCatching))
}else null
}
}.getOrNull()
if (uri == null){
Toast.makeText(context, "Error opening file!", Toast.LENGTH_SHORT).show()
}else{
Intent().apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = Intent.ACTION_VIEW
data = uri
}.run{
context.startActivity(this)
}
}
}
fun shareFileIntent(fragmentContext: Context, paths: List<String>){
fun shareFileIntent(context: Context, paths: List<String>){
val uris : ArrayList<Uri> = arrayListOf()
paths.forEach {
val file = File(it)
if (! file.exists()) return@forEach
val uri = FileProvider.getUriForFile(
fragmentContext,
fragmentContext.packageName + ".fileprovider",
file
)
uris.add(uri)
paths.runCatching {
this.forEach {path ->
val uri = DocumentFile.fromSingleUri(context, Uri.parse(path)).run{
if (this?.exists() == true){
this.uri
}else if (File(path).exists()){
FileProvider.getUriForFile(context, context.packageName + ".fileprovider",
File(path))
}else null
}
if (uri != null) uris.add(uri)
}
}
val shareIntent: Intent = Intent().apply {
if (uris.isEmpty()){
Toast.makeText(context, "Error sharing files!", Toast.LENGTH_SHORT).show()
}else{
Intent().apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
action = Intent.ACTION_SEND_MULTIPLE
putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
type = "*/*"
type = if (uris.size == 1) uris[0].let { context.contentResolver.getType(it) } ?: "media/*" else "*/*"
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
}.run{
context.startActivity(Intent.createChooser(this, context.getString(R.string.share)))
}
try {
fragmentContext.startActivity(Intent.createChooser(shareIntent, fragmentContext.getString(R.string.share)))
}catch (e: Exception){
val intent = Intent.createChooser(shareIntent, fragmentContext.getString(R.string.share))
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
fragmentContext.startActivity(intent)
}
}
@ -361,6 +373,24 @@ object UiUtil {
datePicker.show(fragmentManager, "datepicker")
}
fun showTimePicker(fragmentManager: FragmentManager , onSubmit : (chosenTime: Calendar) -> Unit ){
val currentDate = Calendar.getInstance()
val date = Calendar.getInstance()
val timepicker = MaterialTimePicker.Builder()
.setTimeFormat(TimeFormat.CLOCK_24H)
.setHour(currentDate.get(Calendar.HOUR_OF_DAY))
.setMinute(currentDate.get(Calendar.MINUTE))
.build()
timepicker.addOnPositiveButtonClickListener{
date[Calendar.HOUR_OF_DAY] = timepicker.hour
date[Calendar.MINUTE] = timepicker.minute
onSubmit(date)
}
timepicker.show(fragmentManager, "timepicker")
}
fun showDownloadItemDetailsCard(
item: DownloadItem,
context: Activity,

@ -0,0 +1,116 @@
package com.deniscerri.ytdlnis.work
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.receiver.AlarmStartReceiver
import com.deniscerri.ytdlnis.receiver.AlarmStopReceiver
import java.util.Calendar
class AlarmScheduler(private val context: Context) {
private val alarmManager = context.getSystemService(AlarmManager::class.java)
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
fun schedule() {
cancel()
//schedule starting alarm
val startingTime = preferences.getString("schedule_start", "00:00")!!
val sTime = Calendar.getInstance()
sTime.set(Calendar.HOUR_OF_DAY, startingTime.split(":")[0].toInt())
sTime.set(Calendar.MINUTE, startingTime.split(":")[1].toInt())
sTime.set(Calendar.SECOND, 0)
val time = calculateNextTime(sTime)
val intent = Intent(context, AlarmStartReceiver::class.java)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC,
time.timeInMillis,
PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE )
)
//schedule closing alarm
val endingTime = preferences.getString("schedule_end", "05:00")!!
val eTime = Calendar.getInstance()
eTime.set(Calendar.HOUR_OF_DAY, endingTime.split(":")[0].toInt())
eTime.set(Calendar.MINUTE, endingTime.split(":")[1].toInt())
sTime.set(Calendar.SECOND, 0)
val calendar = calculateNextTime(eTime)
val intent2 = Intent(context, AlarmStopReceiver::class.java)
alarmManager.setExactAndAllowWhileIdle(
AlarmManager.RTC,
calendar.timeInMillis + 60000,
PendingIntent.getBroadcast(context, 1, intent2, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE )
)
}
fun cancel() {
alarmManager.cancel(
PendingIntent.getBroadcast(
context,
0,
Intent(context, AlarmStartReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
alarmManager.cancel(
PendingIntent.getBroadcast(
context,
1,
Intent(context, AlarmStopReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
}
fun calculateNextTime(c: Calendar) : Calendar {
val calendar = Calendar.getInstance()
if (c.get(Calendar.HOUR_OF_DAY) < calendar.get(Calendar.HOUR_OF_DAY)){
c.add(Calendar.DATE, 1)
}else if (
c.get(Calendar.HOUR_OF_DAY) == calendar.get(Calendar.HOUR_OF_DAY) &&
c.get(Calendar.MINUTE) < calendar.get(Calendar.MINUTE)
){
c.add(Calendar.DATE, 1)
}
return c
}
fun isDuringTheScheduledTime(): Boolean{
val now = Calendar.getInstance()
val currentHour = now.get(Calendar.HOUR_OF_DAY)
val currentMinute = now.get(Calendar.MINUTE)
val startingTime = preferences.getString("schedule_start", "00:00")!!
val startingHour = startingTime.split(":")[0].toInt()
val startingMinute = startingTime.split(":")[1].toInt()
val endingTime = preferences.getString("schedule_end", "05:00")!!
var endingHour = endingTime.split(":")[0].toInt()
if (endingHour < 12 && endingHour < startingHour){
endingHour += 24
}
val endingMinute = endingTime.split(":")[1].toInt()
if (currentHour in startingHour..endingHour){
if (currentHour == endingHour){
if (currentMinute > endingMinute) {
return false
}
}else if(currentHour == startingHour){
if (currentMinute < startingMinute){
return false
}
}
return true
}
return false
}
}

@ -35,6 +35,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Calendar
import kotlin.random.Random
@ -53,6 +54,7 @@ class DownloadWorker(
val resultDao = dbManager.resultDao
val logRepo = LogRepository(dbManager.logDao)
val handler = Handler(Looper.getMainLooper())
val alarmScheduler = AlarmScheduler(context)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val time = System.currentTimeMillis() + 6000
val queuedItems = dao.getQueuedDownloadsThatAreNotScheduledChunked(time)
@ -75,8 +77,14 @@ class DownloadWorker(
}
val running = ArrayList(runningYTDLInstances)
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (items.isEmpty() && running.isEmpty()) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
if (useScheduler){
if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
}
}
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running }
@ -114,7 +122,7 @@ class DownloadWorker(
"Format: ${downloadItem.format}\n\n"
}
else { "" } +
"Command: ${infoUtil.parseYTDLRequestString(request)} ${downloadItem.extraCommands}\n\n",
"Command:\n ${infoUtil.parseYTDLRequestString(request)} ${downloadItem.extraCommands}\n\n",
downloadItem.format,
downloadItem.type,
System.currentTimeMillis(),
@ -147,11 +155,23 @@ class DownloadWorker(
}
}.onSuccess {
runningYTDLInstances.remove(downloadItem.id)
FileUtil.deleteConfigFiles(request)
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
runBlocking {
var finalPaths : List<String>?
//if there was no cache used
if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){
setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id))
val p = infoUtil.getFilePaths(request)
finalPaths = File(FileUtil.formatPath(downloadLocation))
.walkTopDown()
.filter { it.isFile && p.contains(it.nameWithoutExtension) }
.sortedByDescending { it.length() }
.map { it.absolutePath }
.toList()
FileUtil.scanMedia(finalPaths, context)
}else{
//move file from internal to set download directory
setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id))
try {
@ -173,7 +193,9 @@ class DownloadWorker(
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}, 1000)
}
}
FileUtil.deleteConfigFiles(request)
//put download in history
val incognito = sharedPreferences.getBoolean("incognito", false)

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M21,12.4V7l-4,-4H5C3.89,3 3,3.9 3,5v14c0,1.1 0.89,2 2,2h7.4L21,12.4zM15,15c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3s1.34,-3 3,-3S15,13.34 15,15zM6,6h9v4H6V6zM19.99,16.25l1.77,1.77L16.77,23H15v-1.77L19.99,16.25zM23.25,16.51l-0.85,0.85l-1.77,-1.77l0.85,-0.85c0.2,-0.2 0.51,-0.2 0.71,0l1.06,1.06C23.45,16 23.45,16.32 23.25,16.51z"/>
</vector>

@ -19,13 +19,6 @@
android:layout_height="match_parent"
android:padding="15dp">
<TextView
android:layout_width="0dp"
android:visibility="gone"
android:id="@+id/id"
android:layout_height="0dp"
tools:ignore="MissingConstraints" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"

@ -57,9 +57,9 @@
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
app:icon="@drawable/ic_clock"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toBottomOf="@id/bottomsheet_download_button"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_download_button"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toTopOf="@id/bottomsheet_download_button" />
<Button
android:id="@+id/bottomsheet_download_button"

@ -37,11 +37,9 @@
android:paddingBottom="150dp"
>
<LinearLayout
<HorizontalScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusableInTouchMode="true"
android:orientation="vertical">
android:layout_height="wrap_content">
<TextView
android:id="@+id/content"
@ -49,11 +47,12 @@
android:fontFamily="monospace"
android:textSize="15sp"
android:gravity="bottom"
android:layout_width="match_parent"
android:scrollbars="vertical|horizontal"
android:layout_width="wrap_content"
android:textIsSelectable="true"
android:layout_height="wrap_content" />
</LinearLayout>
</HorizontalScrollView>
</ScrollView>

@ -2,9 +2,24 @@
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:label="@string/more"
android:id="@+id/main_nav"
app:startDestination="@id/homeFragment">
<action
android:id="@+id/action_mainSettingsFragment_to_folderSettingsFragment"
app:destination="@id/settingsNav" />
<action
android:id="@+id/action_mainSettingsFragment_to_updateSettingsFragment"
app:destination="@id/settingsNav" />
<action
android:id="@+id/action_mainSettingsFragment_to_processingSettingsFragment"
app:destination="@id/settingsNav" />
<action
android:id="@+id/action_mainSettingsFragment_to_appearanceSettingsFragment"
app:destination="@id/settingsNav" />
<action
android:id="@+id/action_mainSettingsFragment_to_downloadSettingsFragment"
app:destination="@id/settingsNav" />
<fragment
android:id="@+id/homeFragment"
android:name="com.deniscerri.ytdlnis.ui.HomeFragment"
@ -46,17 +61,15 @@
<action
android:id="@+id/action_moreFragment_to_downloadLogListFragment2"
app:destination="@id/downloadLogListFragment" />
<action
android:id="@+id/action_moreFragment_to_settingsNav"
app:destination="@id/settingsNav" />
</fragment>
<activity
android:id="@+id/terminalActivity"
android:name="com.deniscerri.ytdlnis.ui.more.TerminalActivity"
android:label="activity_terminal"
tools:layout="@layout/activity_terminal" />
<activity
android:id="@+id/activitySettings"
android:name="com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity"
android:label="activity_settings"
tools:layout="@layout/activity_settings" />
<fragment
android:id="@+id/cookiesFragment"
android:name="com.deniscerri.ytdlnis.ui.more.CookiesFragment"
@ -88,4 +101,52 @@
android:id="@+id/resultCardDetailsDialog"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog"
android:label="ResultCardDetailsDialog" />
<navigation android:id="@+id/settingsNav"
app:startDestination="@id/settingsFrame">
<fragment
android:id="@+id/settingsFrame"
android:name="com.deniscerri.ytdlnis.ui.more.settings.SettingsFragmentFrame"
android:label="settings_frame"
tools:layout="@layout/fragment_settings" />
<fragment
android:id="@+id/folderSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.FolderSettingsFragment"
android:label="FolderSettingsFragment" />
<fragment
android:id="@+id/processingSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.ProcessingSettingsFragment"
android:label="ProcessingSettingsFragment" />
<fragment
android:id="@+id/downloadSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.DownloadSettingsFragment"
android:label="DownloadSettingsFragment" />
<fragment
android:id="@+id/updateSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.UpdateSettingsFragment"
android:label="UpdateSettingsFragment" />
<fragment
android:id="@+id/mainSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.MainSettingsFragment"
android:label="MainSettingsFragment">
<action
android:id="@+id/action_mainSettingsFragment_to_appearanceSettingsFragment"
app:destination="@id/appearanceSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_folderSettingsFragment"
app:destination="@id/folderSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_downloadSettingsFragment"
app:destination="@id/downloadSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_processingSettingsFragment"
app:destination="@id/processingSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_updateSettingsFragment"
app:destination="@id/updateSettingsFragment" />
</fragment>
<fragment
android:id="@+id/appearanceSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.GeneralSettingsFragment"
android:label="AppearanceSettingsFragment" />
</navigation>
</navigation>

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">YTDLnis</string>
<string name="search">Arama</string>
<string name="remove_all">Hepsini Kaldır</string>
<string name="remove_results">Sonuçları Temizle</string>

@ -735,6 +735,7 @@
<item>@string/in_queue</item>
<item>@string/cancelled</item>
<item>@string/errored</item>
<item>@string/saved</item>
<item>@string/cookies</item>
<item>@string/command_templates</item>
<item>@string/shortcuts</item>
@ -747,6 +748,7 @@
<item>queued</item>
<item>cancelled</item>
<item>errored</item>
<item>saved</item>
<item>cookies</item>
<item>templates</item>
<item>shortcuts</item>

@ -2,7 +2,6 @@
<resources>
<string name="worst_quality">Worst Quality</string>
<string name="best_quality">Best Quality</string>
<string name="app_name" translatable="false">YTDLnis</string>
<string name="search">Search</string>
<string name="remove_all">Remove All</string>
<string name="remove_results">Clear Results</string>
@ -304,4 +303,8 @@
<string name="update_formats_background">Update Formats in Background</string>
<string name="auto_update_ytdlp">Auto-Update yt-dlp</string>
<string name="use_sponsorblock">Use SponsorBlock</string>
<string name="cache_first">Cache Downloads First</string>
<string name="cache_first_summary">Download in the internal storage then transfer afterwards. Use this when you can\'t write directly to storage</string>
<string name="scheduling">Scheduling</string>
<string name="use_scheduler">Download On Schedule</string>
</resources>

@ -3,6 +3,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory app:title="@string/downloading">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
@ -104,10 +105,32 @@
<EditTextPreference
app:key="fragment_retries"
app:title="@string/fragment_retries" />
</PreferenceCategory>
<PreferenceCategory app:title="@string/scheduling">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_baseline_file_open_24"
android:key="use_scheduler"
app:title="@string/use_scheduler" />
<Preference
android:dependency="use_scheduler"
app:title="@string/start"
app:defaultValue="00:00"
app:summary="12:00"
android:key="schedule_start"
/>
<Preference
app:icon="@drawable/ic_battery"
app:key="ignore_battery"
app:title="@string/ignore_battery_optimization" />
android:dependency="use_scheduler"
app:title="@string/end"
app:defaultValue="05:00"
app:summary="05:00"
android:key="schedule_end"
/>
</PreferenceCategory>
</PreferenceScreen>

@ -23,6 +23,25 @@
android:summary="@string/access_all_directories_summary"
app:title="@string/access_all_directories" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/if_file_rename"
android:enabled="false"
app:key="cache_downloads"
app:summary="@string/cache_first_summary"
app:title="@string/cache_first" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_save_24"
android:key="keep_cache"
app:summary="@string/keep_cache_summary"
app:title="@string/keep_cache" />
<EditTextPreference
android:icon="@drawable/ic_textformat"
@ -58,12 +77,4 @@
android:summary="@string/clear_temporary_files_summary"
app:title="@string/clear_temporary_files" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_save_24"
android:key="keep_cache"
app:summary="@string/keep_cache_summary"
app:title="@string/keep_cache" />
</PreferenceScreen>

@ -116,5 +116,11 @@
android:icon="@drawable/baseline_share_24"
android:key="show_terminal"
app:title="@string/show_terminal" />
<Preference
app:icon="@drawable/ic_battery"
app:key="ignore_battery"
app:title="@string/ignore_battery_optimization" />
</PreferenceCategory>
</PreferenceScreen>

@ -23,7 +23,7 @@
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
android:defaultValue="false"
android:defaultValue="true"
app:icon="@drawable/ic_nightly"
app:key="nightly_ytdl"
android:title="@string/update_ytdl_nightly"

@ -25,6 +25,9 @@ buildscript {
media3_version = "1.1.1"
agp_version = '8.1.0'
agp_version1 = '7.4.2'
RELEASE_NAME = 'YTDLnis'
BETA_NAME = 'YTDLnis [BETA]'
}
repositories {
mavenCentral()

Loading…
Cancel
Save