multiple download card fixes

This commit is contained in:
zaednasr 2024-07-07 20:48:22 +02:00
parent fa2bd2ed14
commit 80266888e3
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
48 changed files with 1421 additions and 1972 deletions

View file

@ -130,10 +130,10 @@ dependencies {
implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer"
//
// implementation "io.github.junkfood02.youtubedl-android:library:0.16.0"
// implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.16.0"
// implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.0"
// implementation "io.github.junkfood02.youtubedl-android:library:0.16.1"
// implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.16.1"
// implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.1"
implementation "androidx.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
@ -143,10 +143,10 @@ dependencies {
implementation 'androidx.preference:preference-ktx:1.2.1'
implementation "androidx.navigation:navigation-fragment-ktx:$navVer"
implementation "androidx.navigation:navigation-ui-ktx:$navVer"
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.test.ext:junit-ktx:1.1.5'
implementation 'androidx.compose.ui:ui-android:1.6.5'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.core:core-ktx:1.13.1'
implementation 'androidx.test.ext:junit-ktx:1.2.1'
implementation 'androidx.compose.ui:ui-android:1.6.8'
implementation 'androidx.preference:preference-ktx:1.2.1'
testImplementation "junit:junit:$junitVer"
androidTestImplementation "junit:junit:$junitVer"
androidTestImplementation "androidx.test.ext:junit:$androidJunitVer"
@ -167,10 +167,10 @@ dependencies {
implementation "androidx.room:room-runtime:$roomVer"
implementation "androidx.room:room-ktx:$roomVer"
ksp "androidx.room:room-compiler:$roomVer"
implementation 'androidx.paging:paging-runtime-ktx:3.2.1'
implementation 'androidx.paging:paging-runtime-ktx:3.3.0'
implementation "androidx.room:room-paging:$roomVer"
androidTestImplementation "androidx.room:room-testing:$roomVer"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.3'
implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation 'com.google.truth:truth:1.1.5'
@ -182,7 +182,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.2"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.3"
implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer

View file

@ -258,13 +258,10 @@ class MainActivity : BaseActivity() {
navigationView.visibilityChanged {
if (it.isVisible){
val curr = navController.currentDestination?.id
if (curr != R.id.homeFragment && curr != R.id.historyFragment && curr != R.id.moreFragment) hideBottomNavigation()
if (!showingNavbarItems.contains(curr)) hideBottomNavigation()
}
}
when(preferences.getString("start_destination", "")) {
"Queue" -> if (savedInstanceState == null) navController.navigate(R.id.downloadQueueMainFragment)
}
cookieViewModel.updateCookiesFile()
val intent = intent

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdl.database.dao
import android.util.Log
import androidx.paging.PagingSource
import androidx.room.Dao
import androidx.room.Insert
@ -25,7 +26,7 @@ interface DownloadDao {
fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloads() : Flow<List<DownloadItem>>
fun getProcessingDownloads() : Flow<List<DownloadItemSimple>>
@Query("SELECT COUNT(*) FROM downloads WHERE status in (:statuses)")
fun getDownloadsCountFlow(statuses: List<String>) : Flow<Int>
@ -54,15 +55,18 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads set downloadStartTime=:time, status='Scheduled' WHERE status ='Processing'")
suspend fun updateProcessingDownloadTime(time: Long)
@Query("UPDATE downloads set downloadPath=:path WHERE status ='Processing'")
suspend fun updateProcessingDownloadPath(path: String)
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloadsList() : List<DownloadItem>
@Query("SELECT * FROM downloads WHERE url=:url AND status='Processing'")
fun getProcessingDownloadsByUrl(url: String) : List<DownloadItem>
@Query("DELETE from downloads where status = 'Processing' AND url=:url")
suspend fun deleteProcessingByUrl(url: String)
@Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads SET status='Queued' where status = 'Active'")
@ -179,6 +183,13 @@ interface DownloadDao {
@Upsert
suspend fun update(item: DownloadItem)
@Transaction
suspend fun updateAll(list: List<DownloadItem>){
list.forEach {
update(it)
}
}
@Query("UPDATE downloads set status=:status where id=:id")
suspend fun setStatus(id: Long, status: String)

View file

@ -49,9 +49,15 @@ interface ResultDao {
@Query("DELETE FROM results WHERE id=:id")
suspend fun delete(id: Long)
@Query("DELETE FROM results WHERE url=:url")
suspend fun deleteByUrl(url: String)
@Query("SELECT * FROM results WHERE url=:url LIMIT 1")
fun getResultByURL(url: String) : ResultItem?
@Query("SELECT * FROM results WHERE url=:url")
fun getAllByURL(url: String) : List<ResultItem>
@Query("SELECT * FROM results where id=:id LIMIT 1")
fun getResultByID(id: Long): ResultItem?

View file

@ -26,6 +26,7 @@ import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.DownloadItemSimple
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.DownloadWorker
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
@ -42,7 +43,7 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
pagingSourceFactory = {downloadDao.getAllDownloads()}
)
val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads().distinctUntilChanged()
val processingDownloads : Flow<List<DownloadItem>> = downloadDao.getProcessingDownloads().distinctUntilChanged()
val processingDownloads : Flow<List<DownloadItemSimple>> = downloadDao.getProcessingDownloads().distinctUntilChanged()
val queuedDownloads : Pager<Int, DownloadItemSimple> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
pagingSourceFactory = {downloadDao.getQueuedDownloads()}
@ -101,6 +102,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.update(item)
}
suspend fun updateAll(list: List<DownloadItem>) {
downloadDao.updateAll(list)
}
suspend fun updateWithoutUpsert(item: DownloadItem){
kotlin.runCatching { downloadDao.updateWithoutUpsert(item) }
}
@ -122,6 +127,14 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getActiveDownloadsList()
}
fun getProcessingDownloadsByUrl(url: String) : List<DownloadItem> {
return downloadDao.getProcessingDownloadsByUrl(url)
}
suspend fun deleteProcessingByUrl(url: String) {
downloadDao.deleteProcessingByUrl(url)
}
fun getProcessingDownloads() : List<DownloadItem> {
return downloadDao.getProcessingDownloadsList()
}
@ -130,10 +143,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getActiveAndQueuedDownloadsList()
}
suspend fun updateProcessingDownloadTime(time: Long) {
downloadDao.updateProcessingDownloadTime(time)
}
fun getActiveAndQueuedDownloadIDs() : List<Long> {
return downloadDao.getActiveAndQueuedDownloadIDs()
}
@ -203,14 +212,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
downloadDao.removeAllLogID()
}
fun getFilteredProcessingDownloads(ids: List<Long>) : Flow<List<DownloadItem>> {
return downloadDao.getProcessingDownloads()
.map {
it.filter { ids.contains(it.id) }
}
}
@SuppressLint("RestrictedApi")
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -230,12 +231,16 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
if (delay <= 60000L) delay = 0L
}
val useAlarmForScheduling = sharedPreferences.getBoolean("use_alarm_for_scheduling", false)
if (delay > 0L && useAlarmForScheduling) {
AlarmScheduler(context).scheduleAt(queuedItems.minBy { it.downloadStartTime }.downloadStartTime)
return
}
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
//workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
@ -243,10 +248,6 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
.setInputData(inputData.build())
queuedItems.forEach {
workRequest.addTag(it.id.toString())
}
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,

View file

@ -5,7 +5,9 @@ import android.util.Patterns
import com.deniscerri.ytdl.database.dao.ResultDao
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.InfoUtil
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import java.util.regex.Pattern
@ -88,6 +90,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
if (tmpToken.isEmpty()) break
if (tmpToken == nextPageToken) break
nextPageToken = tmpToken
delay(1000)
} while (true)
itemCount.value = items.size
return items
@ -117,6 +120,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
resultDao.delete(item.id)
}
suspend fun deleteByUrl(url: String) {
resultDao.deleteByUrl(url)
}
suspend fun deleteAll(){
itemCount.value = 0
resultDao.deleteAll()
@ -134,6 +141,10 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return resultDao.getResultByURL(url)
}
fun getAllByURL(url: String) : List<ResultItem> {
return resultDao.getAllByURL(url)
}
fun getAllByIDs(ids: List<Long>) : List<ResultItem> {
return resultDao.getAllByIDs(ids)
}
@ -162,9 +173,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
private fun getQueryType(inputQuery: String) : SourceType {
var type = SourceType.SEARCH_QUERY
val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val m = p.matcher(inputQuery)
if (m.find()) {
if (inputQuery.isYoutubeURL()) {
type = SourceType.YOUTUBE_VIDEO
if (inputQuery.contains("playlist?list=")) {
type = SourceType.YOUTUBE_PLAYLIST

View file

@ -5,7 +5,13 @@ import android.app.Application
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.DisplayMetrics
import android.util.Log
import android.widget.Toast
import androidx.core.content.res.ResourcesCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
@ -34,18 +40,26 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.ui.downloadcard.FormatTuple
import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdatePlaylistFormatsWorker
import com.google.gson.Gson
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import okhttp3.internal.format
import java.io.File
import java.util.Locale
@ -53,14 +67,15 @@ import java.util.Locale
class DownloadViewModel(private val application: Application) : AndroidViewModel(application) {
private val dbManager: DBManager
val repository : DownloadRepository
private val sharedDownloadViewModel: SharedDownloadViewModel
private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao
private val infoUtil : InfoUtil
private val resources : Resources
val allDownloads : Flow<PagingData<DownloadItem>>
val queuedDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloads : Flow<List<DownloadItem>>
val processingDownloads : Flow<List<DownloadItem>>
val processingDownloads : Flow<List<DownloadItemSimple>>
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
val savedDownloads : Flow<PagingData<DownloadItemSimple>>
@ -74,7 +89,15 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val savedDownloadsCount : Flow<Int>
val scheduledDownloadsCount : Flow<Int>
val alreadyExistsUiState: MutableStateFlow<List<SharedDownloadViewModel.AlreadyExistsIDs>>
@Parcelize
data class AlreadyExistsIDs(
var downloadItemID: Long,
var historyItemID : Long?
) : Parcelable
val alreadyExistsUiState: MutableStateFlow<List<AlreadyExistsIDs>> = MutableStateFlow(
mutableListOf()
)
private var extraCommandsForAudio: String = ""
private var extraCommandsForVideo: String = ""
@ -87,15 +110,19 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
auto, audio, video, command
}
var processingItemsFlow : ProcessingItemsJob? = null
private val urlsForAudioType = listOf(
"music",
"audio",
"soundcloud"
)
var processingItems = MutableStateFlow(false)
var processingItemsJob : Job? = null
init {
dbManager = DBManager.getInstance(application)
dao = dbManager.downloadDao
repository = DownloadRepository(dao)
sharedDownloadViewModel = SharedDownloadViewModel(application)
alreadyExistsUiState = sharedDownloadViewModel.alreadyExistsUiState
historyRepository = HistoryRepository(dbManager.historyDao)
resultRepository = ResultRepository(dbManager.resultDao, application)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
@ -125,6 +152,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
}
val confTmp = Configuration(application.resources.configuration)
confTmp.setLocale(Locale(sharedPreferences.getString("app_language", "en")!!))
val metrics = DisplayMetrics()
resources = Resources(application.assets, metrics, confTmp)
}
fun deleteDownload(id: Long) = viewModelScope.launch(Dispatchers.IO) {
@ -155,15 +187,130 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun getDownloadType(t: Type? = null, url: String) : Type {
return sharedDownloadViewModel.getDownloadType(t, url)
var type = t
if (type == null){
val preferredDownloadType = sharedPreferences.getString("preferred_download_type", Type.auto.toString())
type = if (sharedPreferences.getBoolean("remember_download_type", false)){
Type.valueOf(sharedPreferences.getString("last_used_download_type",
preferredDownloadType)!!)
}else{
Type.valueOf(preferredDownloadType!!)
}
}
return when(type){
Type.auto -> {
if (urlsForAudioType.any { url.contains(it) }){
Type.audio
}else{
Type.video
}
}
else -> type
}
}
fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem {
return sharedDownloadViewModel.createDownloadItemFromResult(result, url, givenType)
val resultItem = result ?: createEmptyResultItem(url)
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false)
var type = getDownloadType(givenType, resultItem.url)
if(type == Type.command && commandTemplateDao.getTotalNumber() == 0) type = Type.video
val customFileNameTemplate = when(type) {
Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader).30B - %(title).170B")
Type.video -> sharedPreferences.getString("file_name_template", "%(uploader).30B - %(title).170B")
else -> ""
}
val downloadPath = when(type){
Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())
Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())
else -> sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
}
val container = when(type){
Type.audio -> sharedPreferences.getString("audio_format", "")
else -> sharedPreferences.getString("video_format", "")
}
val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!))
val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats)
val videoPreferences = VideoPreferences(
embedSubs,
addChapters, false,
ArrayList(sponsorblock),
saveSubs,
saveAutoSubs,
audioFormatIDs = preferredAudioFormats
)
val extraCommands = when(type){
Type.audio -> extraCommandsForAudio
Type.video -> extraCommandsForVideo
else -> ""
}
return DownloadItem(0,
resultItem.url,
resultItem.title,
resultItem.author,
resultItem.thumb,
resultItem.duration,
type,
getFormat(resultItem.formats, type),
container!!,
"",
resultItem.formats,
downloadPath!!, resultItem.website,
"",
if (resultItem.playlistTitle == resultRepository.YTDLNIS_SEARCH) "" else resultItem.playlistTitle,
audioPreferences,
videoPreferences,
extraCommands,
customFileNameTemplate!!,
saveThumb,
DownloadRepository.Status.Queued.toString(),
0,
null,
playlistURL = resultItem.playlistURL,
playlistIndex = resultItem.playlistIndex,
incognito = sharedPreferences.getBoolean("incognito", false)
)
}
fun createResultItemFromDownload(downloadItem: DownloadItem) : ResultItem {
return sharedDownloadViewModel.createResultItemFromDownload(downloadItem)
return ResultItem(
0,
downloadItem.url,
downloadItem.title,
downloadItem.author,
downloadItem.duration,
downloadItem.thumb,
downloadItem.website,
downloadItem.playlistTitle,
downloadItem.allFormats,
"",
arrayListOf(),
downloadItem.playlistURL,
downloadItem.playlistIndex,
System.currentTimeMillis()
)
}
fun createResultItemFromHistory(downloadItem: HistoryItem) : ResultItem {
@ -187,7 +334,22 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun createEmptyResultItem(url: String) : ResultItem {
return sharedDownloadViewModel.createEmptyResultItem(url)
return ResultItem(
0,
url,
"",
"",
"",
"",
"",
"",
arrayListOf(),
"",
arrayListOf(),
"",
null,
System.currentTimeMillis()
)
}
fun switchDownloadType(list: List<DownloadItem>, type: Type) : List<DownloadItem>{
@ -300,73 +462,112 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
return sharedDownloadViewModel.getPreferredAudioRequirements()
}
//requirement and importance
@SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
return sharedDownloadViewModel.getPreferredVideoRequirements()
}
fun getFormat(formats: List<Format>, type: Type) : Format {
return sharedDownloadViewModel.getFormat(formats, type)
when(type) {
Type.audio -> {
return cloneFormat (
try {
val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
FormatSorter(application).sortAudioFormats(theFormats).first()
}catch (e: Exception){
infoUtil.getGenericAudioFormats(resources).first()
}
)
}
Type.video -> {
return cloneFormat(
try {
val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty {
infoUtil.getGenericVideoFormats(resources).sortedByDescending { it.filesize }
}
FormatSorter(application).sortVideoFormats(theFormats).first()
}catch (e: Exception){
infoUtil.getGenericVideoFormats(resources).first()
}
)
}
else -> {
val lastUsedCommandTemplate = sharedPreferences.getString("lastCommandTemplateUsed", "")!!
val c = if (lastUsedCommandTemplate.isBlank()){
commandTemplateDao.getFirst() ?: CommandTemplate(0,"","", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
}else{
commandTemplateDao.getTemplateByContent(lastUsedCommandTemplate) ?: CommandTemplate(0, "", lastUsedCommandTemplate, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
}
return generateCommandFormat(c)
}
}
}
private fun cloneFormat(item: Format) : Format {
val string = Gson().toJson(item, Format::class.java)
return Gson().fromJson(string, Format::class.java)
}
fun getPreferredAudioFormats(formats: List<Format>) : ArrayList<String>{
return sharedDownloadViewModel.getPreferredAudioFormats(formats)
val preferredAudioFormats = arrayListOf<String>()
val audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
for (f in formats.sortedBy { it.format_id }){
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
if (fId != null) {
if (fId.split("+").all { formats.map { f-> f.format_id }.contains(it) }){
preferredAudioFormats.addAll(fId.split("+"))
break
}
}
}
if (preferredAudioFormats.isEmpty()){
val audioF = getFormat(formats, Type.audio)
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
preferredAudioFormats.add(audioF.format_id)
}
}
return preferredAudioFormats
}
fun generateCommandFormat(c: CommandTemplate) : Format {
return sharedDownloadViewModel.generateCommandFormat(c)
return Format(
c.title,
c.id.toString(),
"",
"",
"",
0,
c.content.replace("\n", " ")
)
}
data class ProcessingItemsJob(
var job: Job? = null,
var originItemType: String,
var originItemIDs: List<Long>,
var processingDownloadItemIDs: MutableList<Long> = mutableListOf()
)
private fun updateProcessingJobData(item: ProcessingItemsJob?) {
processingItemsFlow = item
}
fun turnDownloadItemsToProcessingDownloads(itemIDs: List<Long>) = viewModelScope.launch(Dispatchers.IO){
updateProcessingJobData(ProcessingItemsJob(null, DownloadItem::class.java.toString(), itemIDs))
val job = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing()
processingItems.emit(true)
try {
itemIDs.forEachIndexed { idx, it ->
val item = repository.getItemByID(it)
if (processingItemsFlow?.job?.isCancelled == true) throw CancellationException()
if (processingItemsJob?.isCancelled == true) throw CancellationException()
item.id = 0
item.status = DownloadRepository.Status.Processing.toString()
processingItemsFlow?.apply {
val id = repository.insert(item)
processingDownloadItemIDs.add(id)
updateProcessingJobData(this)
}
repository.insert(item)
}
processingItems.emit(false)
} catch (e: Exception) {
deleteProcessing()
updateProcessingJobData(null)
processingItems.emit(false)
}
}
updateProcessingJobData(ProcessingItemsJob(job, DownloadItem::class.java.toString(), itemIDs))
processingItemsJob = job
}
fun turnResultItemsToProcessingDownloads(itemIDs: List<Long>, downloadNow: Boolean = false) = viewModelScope.launch(Dispatchers.IO) {
updateProcessingJobData(ProcessingItemsJob(null, ResultItem::class.java.toString(), itemIDs))
val job = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessing()
processingItems.emit(true)
try {
val toInsert = mutableListOf<DownloadItem>()
itemIDs.forEach { id ->
val item = resultRepository.getItemByID(id) ?: return@forEach
val preferredType = getDownloadType(url = item.url).toString()
@ -375,7 +576,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
))
downloadItem.status = DownloadRepository.Status.Processing.toString()
if (processingItemsFlow?.job?.isCancelled == true) {
if (processingItemsJob?.isCancelled == true) {
throw CancellationException()
}
@ -383,21 +584,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
downloadItem.status = DownloadRepository.Status.Queued.toString()
queueDownloads(listOf(downloadItem))
}else{
processingItemsFlow?.apply {
processingDownloadItemIDs.add(repository.insert(downloadItem))
updateProcessingJobData(this)
}
toInsert.add(downloadItem)
//repository.insert(downloadItem)
}
}
repository.insertAll(toInsert)
processingItems.emit(false)
}catch (e: Exception) {
deleteProcessing()
updateProcessingJobData(null)
processingItems.emit(false)
}
}
updateProcessingJobData(ProcessingItemsJob(job, ResultItem::class.java.toString(), itemIDs))
processingItemsJob = job
}
fun insert(item: DownloadItem) = viewModelScope.launch(Dispatchers.IO){
@ -423,10 +623,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun deleteScheduled() = viewModelScope.launch(Dispatchers.IO) {
val scheduledIds = repository.getScheduledDownloadIDs()
scheduledIds.forEach {
WorkManager.getInstance(application).cancelAllWorkByTag(it.toString())
}
repository.deleteScheduled()
}
@ -451,7 +647,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
processingItemsFlow?.apply { this.job?.cancel(CancellationException()) }
processingItemsJob?.apply { cancel(CancellationException()) }
repository.cancelActiveQueued()
}
@ -574,9 +770,193 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
queueDownloads(processingItems)
}
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<SharedDownloadViewModel.AlreadyExistsIDs> {
val ids = sharedDownloadViewModel.queueDownloads(items, ign)
return ids
suspend fun queueDownloads(items: List<DownloadItem>, ignoreDuplicates : Boolean = false) {
val context = App.instance
val alarmScheduler = AlarmScheduler(context)
val queuedItems = mutableListOf<DownloadItem>()
//download id, history item id
//history item id if the existing item is already downloaded
//if history id is empty, it just found an existing item in the queue/active list
val existingItemIDs = mutableListOf<AlreadyExistsIDs>()
val downloadArchive = runCatching {
File(FileUtil.getDownloadArchivePath(context)).useLines { it.toList() }
}
.getOrElse { listOf() }
.map { it.split(" ")[1] }
val checkDuplicate = sharedPreferences.getString("prevent_duplicate_downloads", "")!!
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads()
}
items.forEach {
if (it.status != DownloadRepository.Status.Scheduled.toString()) {
it.status = DownloadRepository.Status.Queued.toString()
}
//CHECK DUPLICATES
var alreadyExists = false
if (checkDuplicate.isNotEmpty() && !ignoreDuplicates){
when(checkDuplicate){
"download_archive" -> {
if (downloadArchive.any { d -> it.url.contains(d) }){
alreadyExists = true
if (it.id == 0L) {
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
it.id = id
}
existingItemIDs.add(
AlreadyExistsIDs(
it.id,
null
)
)
}
}
"url_type" -> {
val existingDownload = activeAndQueuedDownloads.firstOrNull { a -> a.type == it.type && a.url == it.url }
if (existingDownload != null){
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
it.id = id
alreadyExists = true
existingItemIDs.add(
AlreadyExistsIDs(
it.id,
null
)
)
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistoryItem = history.firstOrNull {
h -> h.type == it.type
}
if (existingHistoryItem != null){
alreadyExists = true
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
existingItemIDs.add(
AlreadyExistsIDs(
id,
existingHistoryItem.id
)
)
}
}
}
"config" -> {
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null){
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
alreadyExists = true
existingItemIDs.add(AlreadyExistsIDs(id, null))
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistoryItem = history.firstOrNull {
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
}
if (existingHistoryItem != null){
alreadyExists = true
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
existingItemIDs.add(
AlreadyExistsIDs(
id,
existingHistoryItem.id
)
)
}
}
}
}
}
if (!alreadyExists){
queuedItems.add(it)
}
}
repository.updateAll(queuedItems)
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
if (alarmScheduler.canSchedule()){
alarmScheduler.schedule()
}else{
sharedPreferences.edit().putBoolean("use_scheduler", false).apply()
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, context.getString(R.string.enable_alarm_permission), Toast.LENGTH_LONG).show()
}
}
}else{
if (!sharedPreferences.getBoolean("paused_downloads", false)) {
repository.startDownloadWorker(queuedItems, context)
}
if(!useScheduler){
CoroutineScope(Dispatchers.IO).launch {
queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
kotlin.runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}else{
CoroutineScope(Dispatchers.IO).launch {
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
kotlin.runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}
}
if (existingItemIDs.isNotEmpty()){
alreadyExistsUiState.value = existingItemIDs.toList()
}
}
fun getQueuedCollectedFileSize() : Long {
@ -599,17 +979,25 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateProcessingtoSavedStatus()
}
suspend fun updateProcessingFormat(selectedFormats: List<FormatTuple>): List<Long> {
fun updateAllProcessingFormats(formatTuples : List<MultipleItemFormatTuple>) = viewModelScope.launch(Dispatchers.IO) {
val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i ->
selectedFormats[index].format?.apply {
i.format = this
items.forEach {
val ft = formatTuples.first { ft -> ft.url == it.url }.formatTuple
ft.format?.apply {
it.format = this
}
if (i.type == Type.video) selectedFormats[index].audioFormats?.map { it.format_id }?.let { i.videoPreferences.audioFormatIDs.addAll(it) }
repository.update(i)
if (it.type == Type.video) {
ft.audioFormats?.map { a -> a.format_id }?.let { list ->
it.videoPreferences.audioFormatIDs.clear()
it.videoPreferences.audioFormatIDs.addAll(list)
}
}
repository.update(it)
}
return items.map { itm -> itm.format.filesize }
}
suspend fun updateProcessingCommandFormat(format: Format){
@ -624,37 +1012,50 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
dao.updateProcessingDownloadPath(path)
}
fun getProcessingDownloadsCount() : Int {
return dao.getDownloadsCountByStatus(listOf(DownloadRepository.Status.Processing.toString()))
}
fun getProcessingDownloads() : List<DownloadItem> {
return repository.getProcessingDownloads()
}
fun updateDownloadItemFormats(id: Long, list: List<Format>) = viewModelScope.launch(Dispatchers.IO) {
val item = repository.getItemByID(id)
item.allFormats.clear()
item.allFormats.addAll(list)
item.format = getFormat(list, item.type)
suspend fun updateProcessingAllFormats(formatCollection: List<List<Format>>) {
val items = repository.getProcessingDownloads()
items.forEachIndexed { index, i ->
i.allFormats.clear()
if (formatCollection.size == items.size && formatCollection[index].isNotEmpty()) {
runCatching {
i.allFormats.addAll(formatCollection[index])
}
runCatching {
resultRepository.getAllByURL(item.url).forEach {
it.formats.clear()
it.formats.addAll(list)
resultRepository.update(it)
}
i.format = getFormat(i.allFormats, i.type)
kotlin.runCatching {
dbManager.resultDao.getResultByURL(i.url)?.apply {
this.formats = formatCollection[index].toMutableList()
dbManager.resultDao.update(this)
}
}
repository.update(i)
}
}
fun updateProcessingFormatByUrl(url: String, list: List<Format>) = viewModelScope.launch(Dispatchers.IO) {
val items = repository.getProcessingDownloadsByUrl(url)
items.forEach { item ->
item.allFormats.clear()
item.allFormats.addAll(list)
item.format = getFormat(list, item.type)
repository.update(item)
}
kotlin.runCatching {
resultRepository.getAllByURL(url).forEach {
it.formats.clear()
it.formats.addAll(list)
resultRepository.update(it)
}
}
}
fun removeUnavailableDownloadAndResultByURL(url: String) = viewModelScope.launch(Dispatchers.IO) {
repository.deleteProcessingByUrl(url)
resultRepository.deleteByUrl(url)
}
suspend fun continueUpdatingFormatsOnBackground(){
val ids = dao.getProcessingDownloadsList().map { it.id }
val ids = repository.getProcessingDownloads().map { it.id }
dao.updateProcessingtoSavedStatus()
val id = System.currentTimeMillis().toInt()
@ -685,12 +1086,21 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
}
suspend fun updateProcessingDownloadTime(time: Long) {
repository.updateProcessingDownloadTime(time)
suspend fun updateProcessingDownloadTimeAndQueueScheduled(time: Long) {
val processing = repository.getProcessingDownloads()
processing.forEach {
it.downloadStartTime = time
it.status = DownloadRepository.Status.Scheduled.toString()
}
queueDownloads(processing)
}
fun checkIfAllProcessingItemsHaveSameType() : Pair<Boolean, Type> {
val types = dao.getProcessingDownloadTypes()
if (types.isEmpty()) {
return Pair(false, Type.command)
}
return Pair(types.size == 1, Type.valueOf(types.first()))
}

View file

@ -1,665 +0,0 @@
package com.deniscerri.ytdl.database.viewmodel
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Handler
import android.os.Looper
import android.os.Parcelable
import android.util.DisplayMetrics
import android.widget.Toast
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.dao.CommandTemplateDao
import com.deniscerri.ytdl.database.dao.DownloadDao
import com.deniscerri.ytdl.database.models.AudioPreferences
import com.deniscerri.ytdl.database.models.CommandTemplate
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.models.VideoPreferences
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.parcelize.Parcelize
import okhttp3.internal.immutableListOf
import java.io.File
import java.util.Locale
class SharedDownloadViewModel(private val context: Context) {
private val dbManager: DBManager = DBManager.getInstance(context)
val repository : DownloadRepository
private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao
private val infoUtil : InfoUtil
private var bestVideoFormat : Format
private var bestAudioFormat : Format
private var defaultVideoFormats : MutableList<Format>
private val videoQualityPreference: String
private val formatIDPreference: List<String>
private val audioFormatIDPreference: List<String>
private val resources : Resources
private var extraCommandsForAudio: String = ""
private var extraCommandsForVideo: String = ""
private var audioContainer: String?
private var videoContainer: String?
private var videoCodec: String?
private var audioCodec: String?
private val dao: DownloadDao
private val historyRepository: HistoryRepository
private val resultRepository: ResultRepository
@Parcelize
data class AlreadyExistsIDs(
var downloadItemID: Long,
var historyItemID : Long?
) : Parcelable
val alreadyExistsUiState: MutableStateFlow<List<AlreadyExistsIDs>> = MutableStateFlow(
mutableListOf()
)
private val urlsForAudioType = listOf(
"music",
"audio",
"soundcloud"
)
init {
dao = dbManager.downloadDao
repository = DownloadRepository(dao)
historyRepository = HistoryRepository(dbManager.historyDao)
resultRepository = ResultRepository(dbManager.resultDao, context)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
commandTemplateDao = DBManager.getInstance(context).commandTemplateDao
infoUtil = InfoUtil(context)
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
if (sharedPreferences.getBoolean("use_extra_commands", false)){
extraCommandsForAudio = commandTemplateDao.getAllTemplatesAsExtraCommandsForAudio().joinToString(" ")
extraCommandsForVideo = commandTemplateDao.getAllTemplatesAsExtraCommandsForVideo().joinToString(" ")
}
}
videoQualityPreference = sharedPreferences.getString("video_quality", "best").toString()
formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
val confTmp = Configuration(context.resources.configuration)
confTmp.setLocale(Locale(sharedPreferences.getString("app_language", "en")!!))
val metrics = DisplayMetrics()
resources = Resources(context.assets, metrics, confTmp)
videoContainer = sharedPreferences.getString("video_format", "Default")
defaultVideoFormats = infoUtil.getGenericVideoFormats(resources)
bestVideoFormat = defaultVideoFormats.first()
audioContainer = sharedPreferences.getString("audio_format", "mp3")
bestAudioFormat = if (audioFormatIDPreference.isEmpty()){
infoUtil.getGenericAudioFormats(resources).first()
}else{
Format(
audioFormatIDPreference.first().split("+").first(),
audioContainer!!,
"",
"",
"",
0,
audioFormatIDPreference.first().split("+").first()
)
}
videoCodec = sharedPreferences.getString("video_codec", "")
audioCodec = sharedPreferences.getString("audio_codec", "")
}
fun getDownloadType(t: Type? = null, url: String) : Type {
var type = t
if (type == null){
val preferredDownloadType = sharedPreferences.getString("preferred_download_type", Type.auto.toString())
type = if (sharedPreferences.getBoolean("remember_download_type", false)){
Type.valueOf(sharedPreferences.getString("last_used_download_type",
preferredDownloadType)!!)
}else{
Type.valueOf(preferredDownloadType!!)
}
}
return when(type){
Type.auto -> {
if (urlsForAudioType.any { url.contains(it) }){
Type.audio
}else{
Type.video
}
}
else -> type
}
}
fun createDownloadItemFromResult(result: ResultItem?, url: String = "", givenType: Type) : DownloadItem {
val resultItem = result ?: createEmptyResultItem(url)
val embedSubs = sharedPreferences.getBoolean("embed_subtitles", false)
val saveSubs = sharedPreferences.getBoolean("write_subtitles", false)
val saveAutoSubs = sharedPreferences.getBoolean("write_auto_subtitles", false)
val addChapters = sharedPreferences.getBoolean("add_chapters", false)
val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false)
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
val cropThumb = sharedPreferences.getBoolean("crop_thumbnail", false)
var type = getDownloadType(givenType, resultItem.url)
if(type == Type.command && commandTemplateDao.getTotalNumber() == 0) type = Type.video
val customFileNameTemplate = when(type) {
Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader).30B - %(title).170B")
Type.video -> sharedPreferences.getString("file_name_template", "%(uploader).30B - %(title).170B")
else -> ""
}
val downloadPath = when(type){
Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())
Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())
else -> sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
}
val container = when(type){
Type.audio -> sharedPreferences.getString("audio_format", "")
else -> sharedPreferences.getString("video_format", "")
}
val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
val audioPreferences = AudioPreferences(embedThumb, cropThumb,false, ArrayList(sponsorblock!!))
val preferredAudioFormats = getPreferredAudioFormats(resultItem.formats)
val videoPreferences = VideoPreferences(
embedSubs,
addChapters, false,
ArrayList(sponsorblock),
saveSubs,
saveAutoSubs,
audioFormatIDs = preferredAudioFormats
)
val extraCommands = when(type){
Type.audio -> extraCommandsForAudio
Type.video -> extraCommandsForVideo
else -> ""
}
return DownloadItem(0,
resultItem.url,
resultItem.title,
resultItem.author,
resultItem.thumb,
resultItem.duration,
type,
getFormat(resultItem.formats, type),
container!!,
"",
resultItem.formats,
downloadPath!!, resultItem.website,
"",
if (resultItem.playlistTitle == resultRepository.YTDLNIS_SEARCH) "" else resultItem.playlistTitle,
audioPreferences,
videoPreferences,
extraCommands,
customFileNameTemplate!!,
saveThumb,
DownloadRepository.Status.Queued.toString(),
0,
null,
playlistURL = resultItem.playlistURL,
playlistIndex = resultItem.playlistIndex,
incognito = sharedPreferences.getBoolean("incognito", false)
)
}
fun createResultItemFromDownload(downloadItem: DownloadItem) : ResultItem {
return ResultItem(
0,
downloadItem.url,
downloadItem.title,
downloadItem.author,
downloadItem.duration,
downloadItem.thumb,
downloadItem.website,
downloadItem.playlistTitle,
downloadItem.allFormats,
"",
arrayListOf(),
downloadItem.playlistURL,
downloadItem.playlistIndex,
System.currentTimeMillis()
)
}
fun createEmptyResultItem(url: String) : ResultItem {
return ResultItem(
0,
url,
"",
"",
"",
"",
"",
"",
arrayListOf(),
"",
arrayListOf(),
"",
null,
System.currentTimeMillis()
)
}
fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
val itemValues = resources.getStringArray(R.array.format_importance_audio_values).toSet()
val prefAudio = sharedPreferences.getString("format_importance_audio", itemValues.joinToString(","))!!
prefAudio.split(",").forEachIndexed { idx, s ->
val importance = (itemValues.size - idx) * 10
when(s) {
"id" -> {
requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) importance else 0}
}
"language" -> {
sharedPreferences.getString("audio_language", "")?.apply {
if (this.isNotBlank()){
requirements.add { it: Format -> if (it.lang?.contains(this) == true) importance else 0 }
}
}
}
"codec" -> {
requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) importance else 0}
}
"container" -> {
requirements.add {it: Format -> if (it.container == audioContainer) importance else 0 }
}
}
}
return requirements
}
//requirement and importance
@SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
val itemValues = resources.getStringArray(R.array.format_importance_video_values).toSet()
val prefVideo = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!!
prefVideo.split(",").forEachIndexed { idx, s ->
var importance = (itemValues.size - idx) * 10
when(s) {
"id" -> {
requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) importance else 0 }
}
"resolution" -> {
context.getStringArray(R.array.video_formats_values)
.filter { it.contains("_") }
.map{ it.split("_")[0].dropLast(1)
}.toMutableList().apply {
when(videoQualityPreference) {
"worst" -> {
requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (importance) else 0 }
}
"best" -> {
requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (importance) else 0 }
}
else -> {
val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) }
val preference = this[preferenceIndex]
for(i in 0..preferenceIndex){
removeAt(0)
}
add(0, preference)
forEachIndexed { index, res ->
requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (importance - index - 1) else 0 }
}
}
}
}
}
"codec" -> {
requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) importance else 0 }
}
"no_audio" -> {
requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") importance else 0 }
}
"container" -> {
requirements.add { it: Format ->
if (videoContainer == "mp4")
if (it.container.equals("mpeg_4", true)) importance else 0
else
if (it.container.equals(videoContainer, true)) importance else 0
}
}
}
}
return requirements
}
fun getFormat(formats: List<Format>, type: Type) : Format {
when(type) {
Type.audio -> {
return cloneFormat (
try {
val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
FormatSorter(context).sortAudioFormats(theFormats).first()
//
// val requirements = getPreferredAudioRequirements()
// theFormats.maxByOrNull { f -> requirements.sumOf{ req -> req(f)} } ?: throw Exception()
}catch (e: Exception){
bestAudioFormat
}
)
}
Type.video -> {
return cloneFormat(
try {
val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty {
defaultVideoFormats.sortedByDescending { it.filesize }
}
FormatSorter(context).sortVideoFormats(theFormats).first()
//
// when (videoQualityPreference) {
// "worst" -> {
// theFormats.last()
// }
// else /*best*/ -> {
// val requirements = getPreferredVideoRequirements()
// theFormats.run {
// if (sharedPreferences.getBoolean("prefer_smaller_formats", false)){
// sortedBy { it.filesize }.maxByOrNull { f -> requirements.sumOf { req -> req(f) } } ?: throw Exception()
// }else{
// sortedByDescending { it.filesize }.maxByOrNull { f ->
// val summ = requirements.sumOf { req -> req(f) }
// summ
// } ?: throw Exception()
// }
// }
// }
// }
}catch (e: Exception){
bestVideoFormat
}
)
}
else -> {
val lastUsedCommandTemplate = sharedPreferences.getString("lastCommandTemplateUsed", "")!!
val c = if (lastUsedCommandTemplate.isBlank()){
commandTemplateDao.getFirst() ?: CommandTemplate(0,"","", useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
}else{
commandTemplateDao.getTemplateByContent(lastUsedCommandTemplate) ?: CommandTemplate(0, "", lastUsedCommandTemplate, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
}
return generateCommandFormat(c)
}
}
}
fun getPreferredAudioFormats(formats: List<Format>) : ArrayList<String>{
val preferredAudioFormats = arrayListOf<String>()
for (f in formats.sortedBy { it.format_id }){
val fId = audioFormatIDPreference.sorted().find { it.contains(f.format_id) }
if (fId != null) {
if (fId.split("+").all { formats.map { f-> f.format_id }.contains(it) }){
preferredAudioFormats.addAll(fId.split("+"))
break
}
}
}
if (preferredAudioFormats.isEmpty()){
val audioF = getFormat(formats, Type.audio)
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){
preferredAudioFormats.add(audioF.format_id)
}
}
return preferredAudioFormats
}
fun generateCommandFormat(c: CommandTemplate) : Format {
return Format(
c.title,
c.id.toString(),
"",
"",
"",
0,
c.content.replace("\n", " ")
)
}
private fun cloneFormat(item: Format) : Format {
val string = Gson().toJson(item, Format::class.java)
return Gson().fromJson(string, Format::class.java)
}
suspend fun queueDownloads(items: List<DownloadItem>, ign : Boolean = false) : List<AlreadyExistsIDs> {
val context = App.instance
val alarmScheduler = AlarmScheduler(context)
val queuedItems = mutableListOf<DownloadItem>()
//download id, history item id
//history item id if the existing item is already downloaded
val existingItemIDs = mutableListOf<AlreadyExistsIDs>()
if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){
items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" }
}
val downloadArchive = runCatching { File(FileUtil.getDownloadArchivePath(context)).useLines { it.toList() } }.getOrElse { listOf() }
.map { it.split(" ")[1] }
items.forEach {
if (it.status != DownloadRepository.Status.Scheduled.toString())
it.status = DownloadRepository.Status.Queued.toString()
var alreadyExists = false
val checkDuplicate = sharedPreferences.getString("prevent_duplicate_downloads", "")!!
if (checkDuplicate.isNotEmpty() && !ign){
when(checkDuplicate){
"download_archive" -> {
if (downloadArchive.any { d -> it.url.contains(d) }){
alreadyExists = true
if (it.id == 0L) {
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
it.id = id
}
existingItemIDs.add(AlreadyExistsIDs(it.id, null))
}
}
"url_type" -> {
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads()
}
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null){
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
it.id = id
alreadyExists = true
existingItemIDs.add(AlreadyExistsIDs(it.id, null))
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistoryItem = history.firstOrNull {
h -> h.type == it.type
}
if (existingHistoryItem != null){
alreadyExists = true
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
existingItemIDs.add(AlreadyExistsIDs(id, existingHistoryItem.id))
}
}
}
"config" -> {
val currentCommand = infoUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand)
val activeAndQueuedDownloads = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloads()
}
val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0
d.logID = null
d.customFileNameTemplate = it.customFileNameTemplate
d.status = DownloadRepository.Status.Queued.toString()
d.toString() == it.toString()
}
if (existingDownload != null){
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
alreadyExists = true
existingItemIDs.add(AlreadyExistsIDs(id, null))
}else{
//check if downloaded and file exists
val history = withContext(Dispatchers.IO){
historyRepository.getAllByURL(it.url).filter { item -> item.downloadPath.any { path -> FileUtil.exists(path) } }
}
val existingHistoryItem = history.firstOrNull {
h -> h.command.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "") == parsedCurrentCommand.replace("(-P \"(.*?)\")|(--trim-filenames \"(.*?)\")".toRegex(), "")
}
if (existingHistoryItem != null){
alreadyExists = true
it.status = DownloadRepository.Status.Processing.toString()
val id = runBlocking {
repository.insert(it)
}
existingItemIDs.add(AlreadyExistsIDs(id, existingHistoryItem.id))
}
}
}
}
}
if (!alreadyExists){
if (it.id == 0L){
val id = runBlocking {
repository.insert(it)
}
it.id = id
}else if (listOf(DownloadRepository.Status.Queued, DownloadRepository.Status.Scheduled).toListString().contains(it.status)){
withContext(Dispatchers.IO){
repository.update(it)
}
}
queuedItems.add(it)
}
}
if (existingItemIDs.isNotEmpty()){
alreadyExistsUiState.value = existingItemIDs.toList()
}
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
if (alarmScheduler.canSchedule()){
alarmScheduler.schedule()
}else{
sharedPreferences.edit().putBoolean("use_scheduler", false).apply()
Handler(Looper.getMainLooper()).post {
Toast.makeText(context, context.getString(R.string.enable_alarm_permission), Toast.LENGTH_LONG).show()
}
}
}else{
if (queuedItems.isNotEmpty()){
if (!sharedPreferences.getBoolean("paused_downloads", false)) {
repository.startDownloadWorker(queuedItems, context)
}
if(!useScheduler){
queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {
CoroutineScope(Dispatchers.IO).launch {
runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}else{
queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach {
CoroutineScope(Dispatchers.IO).launch {
runCatching {
resultRepository.updateDownloadItem(it)?.apply {
repository.updateWithoutUpsert(this)
}
}
}
}
}
}
}
return existingItemIDs
}
}

View file

@ -11,13 +11,14 @@ import android.widget.FrameLayout
import android.widget.ImageView
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.paging.PagingDataAdapter
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.DownloadItemSimple
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.loadThumbnail
import com.deniscerri.ytdl.util.Extensions.popup
@ -25,7 +26,8 @@ import com.deniscerri.ytdl.util.FileUtil
import com.google.android.material.button.MaterialButton
import java.util.Locale
class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ConfigureMultipleDownloadsAdapter.ViewHolder>(AsyncDifferConfig.Builder(
class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItemSimple?, ConfigureMultipleDownloadsAdapter.ViewHolder>(
AsyncDifferConfig.Builder(
DIFF_CALLBACK
).build()) {
private val onItemClickListener: OnItemClickListener
@ -155,12 +157,12 @@ class ConfigureMultipleDownloadsAdapter(onItemClickListener: OnItemClickListener
}
companion object {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<DownloadItem> = object : DiffUtil.ItemCallback<DownloadItem>() {
override fun areItemsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
private val DIFF_CALLBACK: DiffUtil.ItemCallback<DownloadItemSimple> = object : DiffUtil.ItemCallback<DownloadItemSimple>() {
override fun areItemsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean {
return oldItem.url == newItem.url
}
override fun areContentsTheSame(oldItem: DownloadItem, newItem: DownloadItem): Boolean {
override fun areContentsTheSame(oldItem: DownloadItemSimple, newItem: DownloadItemSimple): Boolean {
return oldItem.title == newItem.title &&
oldItem.author == newItem.author &&
oldItem.type == newItem.type &&

View file

@ -41,6 +41,9 @@ class NavBarOptionsAdapter(
val noHome = listOf(R.id.terminalActivity)
holder.binding.apply {
title.text = item.title
title.contentDescription = item.title
checkbox.isChecked = item.isVisible || essential.contains(item.itemId)
checkbox.isEnabled = !essential.contains(item.itemId)
home.setImageResource(
@ -52,11 +55,8 @@ class NavBarOptionsAdapter(
if (!checkbox.isChecked || selectedHomeTabId == item.itemId) {
return@setOnClickListener
}
val oldSelection = items.indexOfFirst { it.itemId == selectedHomeTabId }
selectedHomeTabId = item.itemId
listOf(position, oldSelection).forEach {
notifyItemChanged(it)
}
notifyDataSetChanged()
}
checkbox.setOnClickListener {
item.isVisible = checkbox.isChecked

View file

@ -113,26 +113,28 @@ class ConfigureDownloadBottomSheetDialog(private val currentDownloadItem: Downlo
viewPager2.adapter = fragmentAdapter
viewPager2.isSaveFromParentEnabled = false
when(currentDownloadItem.type) {
Type.audio -> {
tabLayout.selectTab(tabLayout.getTabAt(0))
viewPager2.setCurrentItem(0, false)
}
Type.video -> {
if (isAudioOnly){
tabLayout.getTabAt(0)!!.select()
view.post {
when(currentDownloadItem.type) {
Type.audio -> {
tabLayout.selectTab(tabLayout.getTabAt(0))
viewPager2.setCurrentItem(0, false)
Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
}else{
tabLayout.getTabAt(1)!!.select()
viewPager2.setCurrentItem(1, false)
}
}
else -> {
tabLayout.selectTab(tabLayout.getTabAt(2))
viewPager2.postDelayed( {
viewPager2.setCurrentItem(2, false)
}, 200)
Type.video -> {
if (isAudioOnly){
tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, false)
Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
}else{
tabLayout.getTabAt(1)!!.select()
viewPager2.setCurrentItem(1, false)
}
}
else -> {
tabLayout.selectTab(tabLayout.getTabAt(2))
viewPager2.postDelayed( {
viewPager2.setCurrentItem(2, false)
}, 200)
}
}
}

View file

@ -226,26 +226,30 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null)
val listener = object : OnFormatClickListener {
override fun onFormatClick(item: List<FormatTuple>) {
item.first().format?.apply {
override fun onFormatClick(formatTuple: FormatTuple) {
formatTuple.format?.apply {
downloadItem.format = this
UiUtil.populateFormatCard(requireContext(), formatCard, this, null)
}
}
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
override fun onFormatsUpdated(allFormats: List<Format>) {
lifecycleScope.launch(Dispatchers.IO) {
resultItem?.apply {
this.formats.removeAll(formats.toSet())
this.formats.addAll(allFormats.first().filter { !genericAudioFormats.contains(it) })
this.formats.addAll(allFormats.filter { !genericAudioFormats.contains(it) })
resultViewModel.update(this)
kotlin.runCatching {
val f1 = fragmentManager?.findFragmentByTag("f1") as DownloadVideoFragment
f1.updateUI(this)
}
}
currentDownloadItem?.apply {
downloadViewModel.updateDownloadItemFormats(this.id, allFormats.filter { !genericAudioFormats.contains(it) })
}
}
formats = allFormats.first().filter { !genericAudioFormats.contains(it) }.toMutableList()
formats = allFormats.filter { !genericAudioFormats.contains(it) }.toMutableList()
formats.removeAll(genericAudioFormats)
val preferredFormat = downloadViewModel.getFormat(formats, Type.audio)
downloadItem.format = preferredFormat
@ -255,7 +259,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
}
formatCard.setOnClickListener{
if (parentFragmentManager.findFragmentByTag("formatSheet") == null){
val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listOf(formats.ifEmpty { genericAudioFormats }), listener)
val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listener)
bottomSheet.show(parentFragmentManager, "formatSheet")
}
}

View file

@ -184,7 +184,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
//check if the item has formats and its audio-only
val formats = result.formats
val isAudioOnly = formats.isNotEmpty() && formats.none { !it.format_note.contains("audio") }
var isAudioOnly = formats.isNotEmpty() && formats.none { !it.format_note.contains("audio") }
if (isAudioOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
@ -216,26 +216,28 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
viewPager2.adapter = fragmentAdapter
viewPager2.isSaveFromParentEnabled = false
when(type) {
Type.audio -> {
tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, false)
}
Type.video -> {
if (isAudioOnly){
view.post {
when(type) {
Type.audio -> {
tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, false)
Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
}else{
tabLayout.getTabAt(1)!!.select()
viewPager2.setCurrentItem(1, false)
}
}
else -> {
tabLayout.getTabAt(2)!!.select()
viewPager2.postDelayed( {
viewPager2.setCurrentItem(2, false)
}, 200)
Type.video -> {
if (isAudioOnly){
tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, false)
Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
}else{
tabLayout.getTabAt(1)!!.select()
viewPager2.setCurrentItem(1, false)
}
}
else -> {
tabLayout.getTabAt(2)!!.select()
viewPager2.postDelayed( {
viewPager2.setCurrentItem(2, false)
}, 200)
}
}
}
@ -592,6 +594,15 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
resultViewModel.updateFormatsResultData.collectLatest { formats ->
if (formats == null) return@collectLatest
kotlin.runCatching {
isAudioOnly = formats.isNotEmpty() && formats.none { !it.format_note.contains("audio") }
if (isAudioOnly){
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.isClickable = true
(tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(1)?.alpha = 0.3f
Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show()
tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, false)
}
lifecycleScope.launch {
withContext(Dispatchers.Main){
runCatching {

View file

@ -201,9 +201,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
toggleLoading(true)
lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.updateProcessingDownloadTime(cal.timeInMillis)
downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.queueProcessingDownloads()
downloadViewModel.updateProcessingDownloadTimeAndQueueScheduled(cal.timeInMillis)
}
dismiss()
}
@ -231,10 +230,10 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
downloadViewModel.deleteAllWithID(currentDownloadIDs)
downloadViewModel.moveProcessingToSavedCategory()
}
getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException())
this.job = null
}
downloadViewModel.processingItemsJob?.cancel(CancellationException())
downloadViewModel.processingItemsJob = null
dismiss()
}
}
@ -242,29 +241,27 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
true
}
val formatListener = object : OnFormatClickListener {
override fun onFormatClick(selectedFormats: List<FormatTuple>) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingFormat(selectedFormats)
}
val formatListener = object : OnMultipleFormatClickListener {
override fun onFormatClick(formatTuple: List<MultipleItemFormatTuple>) {
downloadViewModel.updateAllProcessingFormats(formatTuple)
}
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
CoroutineScope(Dispatchers.IO).launch {
downloadViewModel.updateProcessingAllFormats(allFormats)
}
override fun onFormatUpdated(url: String, formats: List<Format>) {
downloadViewModel.updateProcessingFormatByUrl(url, formats)
}
override fun onItemUnavailable(url: String) {
downloadViewModel.removeUnavailableDownloadAndResultByURL(url)
}
override fun onContinueOnBackground() {
requireActivity().lifecycleScope.launch {
withContext(Dispatchers.IO){
downloadViewModel.continueUpdatingFormatsOnBackground()
}
getProcessingItemsData()?.apply {
this.job?.cancel(CancellationException())
this.job = null
}
downloadViewModel.processingItemsJob?.cancel(CancellationException())
downloadViewModel.processingItemsJob = null
dismiss()
}
}
@ -421,20 +418,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
val items = withContext(Dispatchers.IO){
downloadViewModel.getProcessingDownloads()
}
val flatFormatCollection = items.map { it.allFormats }.flatten()
val commonFormats = withContext(Dispatchers.IO){
flatFormatCollection.groupingBy { it.format_id }.eachCount().filter { it.value == items.size }.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }.map { it.value }
}
val formats = if (commonFormats.isNotEmpty() && items.none{it.allFormats.isEmpty()}) {
items.map { it.allFormats }
}else{
when(items.first().type){
DownloadViewModel.Type.audio -> listOf<List<Format>>(infoUtil.getGenericAudioFormats(requireContext().resources))
else -> listOf<List<Format>>(infoUtil.getGenericVideoFormats(requireContext().resources))
}
}
val bottomSheet = FormatSelectionBottomSheetDialog(items, formats, formatListener)
val bottomSheet = FormatSelectionBottomSheetDialog(items, _multipleFormatsListener = formatListener)
bottomSheet.show(parentFragmentManager, "formatSheet")
}
}
@ -661,10 +645,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
bottomAppBar.menu.children.forEach { m -> m.isEnabled = !loading }
}
private fun toggleLoadingShimmerTitle(show: Boolean) {
}
private fun updateFileSize(items: List<Long>){
if (items.all { it > 5L }){
val size = FileUtil.convertFileSize(items.sum())
@ -822,12 +802,8 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
override fun onDismiss(dialog: DialogInterface) {
lifecycleScope.launch {
withContext(Dispatchers.IO){
getProcessingItemsData()?.apply {
if (this.job?.isActive == true){
this.job?.cancel(CancellationException())
downloadViewModel.deleteAllWithID(this.processingDownloadItemIDs)
}
}
downloadViewModel.processingItemsJob?.cancel(CancellationException())
downloadViewModel.processingItemsJob = null
downloadViewModel.deleteProcessing()
}
}
@ -912,8 +888,5 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
}
}
private fun getProcessingItemsData() : DownloadViewModel.ProcessingItemsJob? {
return downloadViewModel.processingItemsFlow
}
}

View file

@ -60,7 +60,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
private lateinit var saveDir : TextInputLayout
private lateinit var freeSpace : TextView
private lateinit var infoUtil: InfoUtil
private lateinit var genericVideoFormats: MutableList<Format>
private lateinit var genericAudioFormats: MutableList<Format>
lateinit var downloadItem: DownloadItem
@ -77,6 +79,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
infoUtil = InfoUtil(requireContext())
genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources)
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList()
return fragmentView
@ -234,34 +237,38 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) })
val listener = object : OnFormatClickListener {
override fun onFormatClick(item: List<FormatTuple>) {
item.first().format?.apply {
override fun onFormatClick(formatTuple: FormatTuple) {
formatTuple.format?.apply {
downloadItem.format = this
}
downloadItem.videoPreferences.audioFormatIDs.clear()
item.first().audioFormats?.map { it.format_id }?.let {
formatTuple.audioFormats?.map { it.format_id }?.let {
downloadItem.videoPreferences.audioFormatIDs.addAll(it)
}
UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format,
if(downloadItem.videoPreferences.removeAudio) listOf() else item.first().audioFormats
if(downloadItem.videoPreferences.removeAudio) listOf() else formatTuple.audioFormats
)
}
override fun onFormatsUpdated(allFormats: List<List<Format>>) {
override fun onFormatsUpdated(allFormats: List<Format>) {
lifecycleScope.launch {
withContext(Dispatchers.IO){
resultItem?.apply {
this.formats.removeAll(formats)
this.formats.addAll(allFormats.first().filter { !genericVideoFormats.contains(it) })
this.formats.addAll(allFormats.filter { !genericVideoFormats.contains(it) })
resultViewModel.update(this)
kotlin.runCatching {
val f1 = fragmentManager?.findFragmentByTag("f0") as DownloadAudioFragment
f1.updateUI(this)
}
}
currentDownloadItem?.apply {
downloadViewModel.updateDownloadItemFormats(this.id, allFormats.filter { !genericVideoFormats.contains(it) })
}
}
}
formats = allFormats.first().filter { !genericVideoFormats.contains(it) }.toMutableList()
formats = allFormats.filter { !genericVideoFormats.contains(it) }.toMutableList()
val preferredFormat = downloadViewModel.getFormat(formats, Type.video)
val preferredAudioFormats = downloadViewModel.getPreferredAudioFormats(formats)
downloadItem.format = preferredFormat
@ -274,7 +281,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
}
formatCard.setOnClickListener{
if (parentFragmentManager.findFragmentByTag("formatSheet") == null){
val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listOf(formats.ifEmpty { genericVideoFormats }), listener)
val bottomSheet = FormatSelectionBottomSheetDialog(listOf(downloadItem), listener)
bottomSheet.show(parentFragmentManager, "formatSheet")
}
}
@ -413,6 +420,10 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
@SuppressLint("RestrictedApi")
fun updateSelectedAudioFormat(format: Format){
if (genericAudioFormats.contains(format)) {
return
}
downloadItem.videoPreferences.audioFormatIDs.clear()
downloadItem.videoPreferences.audioFormatIDs.addAll(arrayListOf(format.format_id))
val formatCard = requireView().findViewById<MaterialCardView>(R.id.format_card_constraintLayout)

View file

@ -18,9 +18,9 @@ import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.AlreadyExistsItem
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.AlreadyExistsIDs
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.database.viewmodel.SharedDownloadViewModel.AlreadyExistsIDs
import com.deniscerri.ytdl.ui.adapter.AlreadyExistsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.UiUtil

View file

@ -7,8 +7,10 @@ import android.os.Bundle
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.Window
import android.widget.*
import androidx.compose.foundation.layout.PaddingValues
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.isVisible
@ -21,6 +23,7 @@ import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.FormatSorter
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil
@ -31,46 +34,57 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.internal.format
import java.util.regex.Pattern
class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>? = null, private var _formats: List<List<Format>>? = null, private val _listener: OnFormatClickListener? = null) : BottomSheetDialogFragment() {
class FormatSelectionBottomSheetDialog(
private val _items: List<DownloadItem?>? = null,
private val _listener: OnFormatClickListener? = null,
private val _multipleFormatsListener: OnMultipleFormatClickListener? = null
) : BottomSheetDialogFragment() {
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var infoUtil: InfoUtil
private lateinit var view: View
private lateinit var continueInBackgroundSnackBar : Snackbar
private lateinit var downloadViewModel: DownloadViewModel
private lateinit var sharedPreferences: SharedPreferences
private lateinit var formatCollection: MutableList<List<Format>>
private lateinit var chosenFormats: List<Format>
private var selectedVideo : Format? = null
private lateinit var selectedAudios : MutableList<Format>
private lateinit var videoFormatList : LinearLayout
private lateinit var audioFormatList : LinearLayout
private lateinit var okBtn : Button
private lateinit var refreshBtn: Button
private lateinit var videoTitle : TextView
private lateinit var audioTitle : TextView
private lateinit var chosenFormats: List<Format>
private var selectedVideo : Format? = null
private lateinit var selectedAudios : MutableList<Format>
private lateinit var sortBy : FormatSorting
private lateinit var filterBy : FormatCategory
private lateinit var filterBtn : Button
private lateinit var continueInBackgroundSnackBar : Snackbar
private lateinit var view: View
private var updateFormatsJob: Job? = null
private var isMissingFormats: Boolean = false
private var hasGenericFormats: Boolean = false
private lateinit var items: List<DownloadItem?>
private lateinit var formats: List<List<Format>>
private lateinit var items: MutableList<DownloadItem?>
private lateinit var formats: MutableList<Format>
private lateinit var listener: OnFormatClickListener
private lateinit var multipleFormatsListener: OnMultipleFormatClickListener
private var currentFormatSource : String? = null
private lateinit var genericAudioFormats : List<Format>
private lateinit var genericVideoFormats : List<Format>
enum class FormatSorting {
filesize, container, codec, id
}
@ -82,7 +96,6 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity().applicationContext)
formatCollection = mutableListOf()
chosenFormats = listOf()
selectedAudios = mutableListOf()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
@ -101,9 +114,27 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
return
}
items = _items
formats = _formats!!
listener = _listener!!
items = _items.distinctBy { it!!.url }.toMutableList()
if (items.size == 1) {
formats = items.first()!!.allFormats
}else{
val flatFormatCollection = items.map { it!!.allFormats }.flatten()
formats = flatFormatCollection.groupingBy { it.format_id }.eachCount()
.filter { it.value == items.size }
.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }
.map { it.value }.toMutableList()
}
_listener?.apply {
listener = this
}
_multipleFormatsListener?.apply {
multipleFormatsListener = this
}
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources)
genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources)
sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!)
filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!)
@ -126,32 +157,25 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
okBtn = view.findViewById(R.id.format_ok)
shimmers.visibility = View.GONE
hasGenericFormats = formats.first().isEmpty() || formats.last().any { it.format_id == "best" || it.format_id == "ba" }
filterBtn.isVisible = !hasGenericFormats
isMissingFormats = formats.isEmpty() && items.any { it!!.allFormats.isEmpty() }
if (items.size > 1){
if (!hasGenericFormats){
formatCollection.addAll(formats)
val flattenFormats = formats.flatten()
val commonFormats = flattenFormats.groupingBy { it.format_id }.eachCount().filter { it.value == items.size }.mapValues { flattenFormats.first { f -> f.format_id == it.key } }.map { it.value }
chosenFormats = commonFormats.mapTo(mutableListOf()) {it.copy()}
if (!isMissingFormats){
chosenFormats = formats.mapTo(mutableListOf()) {it.copy()}
chosenFormats = when(items.first()?.type){
Type.audio -> chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
else -> chosenFormats
}
chosenFormats.forEach {
it.filesize =
flattenFormats.filter { f -> f.format_id == it.format_id }
.sumOf { itt -> itt.filesize }
it.filesize = items.map { itm -> itm!!.allFormats }.flatten().filter { f -> f.format_id == it.format_id }.sumOf { itt -> itt.filesize }
}
}else{
chosenFormats = formats.flatten()
chosenFormats = formats
}
addFormatsToView()
}else{
chosenFormats = formats.flatten()
if(!hasGenericFormats){
chosenFormats = formats
if(!isMissingFormats){
if(items.first()?.type == Type.audio){
chosenFormats = chosenFormats.filter { it.format_note.contains("audio", ignoreCase = true) }
}
@ -159,41 +183,22 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
addFormatsToView()
}
val refreshBtn = view.findViewById<Button>(R.id.format_refresh)
if (!hasGenericFormats || items.isEmpty() || items.first()?.url?.isEmpty() == true) refreshBtn.visibility = View.GONE
val formatSourceLinear = view.findViewById<MaterialCardView>(R.id.formatSourceLinear)
val formatSourceGroup = view.findViewById<RadioGroup>(R.id.format_source_group)
val youtubeURLMatcher = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val canSwitch = items.all { youtubeURLMatcher.matcher(it!!.url).find() }
if (canSwitch) {
val availableSources = resources.getStringArray(R.array.formats_source)
val availableSourcesValues = resources.getStringArray(R.array.formats_source_values)
val currentSource = sharedPreferences.getString("formats_source", "yt-dlp")
formatSourceLinear.isVisible = true
availableSources.forEachIndexed { idx, it ->
val radio = RadioButton(requireContext())
val tag = availableSourcesValues[idx]
radio.text = it
radio.tag = tag
radio.setOnClickListener { compoundButton ->
formatSourceGroup.clearCheck()
formatSourceGroup.check(compoundButton.id)
currentFormatSource = compoundButton.tag.toString()
refreshBtn.performClick()
}
formatSourceGroup.addView(radio)
}
formatSourceGroup.check(formatSourceGroup.children.firstOrNull { it.tag == currentSource }!!.id)
refreshBtn = view.findViewById(R.id.format_refresh)
filterBtn.isVisible = chosenFormats.isNotEmpty() || items.all { it!!.url.isYoutubeURL() }
if (!isMissingFormats || items.isEmpty() || items.first()?.url?.isEmpty() == true) {
refreshBtn.visibility = View.GONE
}
refreshBtn.setOnClickListener {
lifecycleScope.launch {
if (items.size > 10){
val itemsThatHaveFormats = items.filter { it!!.allFormats.isNotEmpty() }
val itemsWithMissingFormats = items.filter { it!!.allFormats.isEmpty() }.ifEmpty { items }
if (itemsWithMissingFormats.size > 10){
continueInBackgroundSnackBar = Snackbar.make(view, R.string.update_formats_background, Snackbar.LENGTH_LONG)
continueInBackgroundSnackBar.setAction(R.string.ok) {
listener.onContinueOnBackground()
_multipleFormatsListener!!.onContinueOnBackground()
this@FormatSelectionBottomSheetDialog.dismiss()
}
continueInBackgroundSnackBar.show()
@ -202,19 +207,19 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
chosenFormats = emptyList()
refreshBtn.isEnabled = false
formatSourceLinear.isVisible = false
refreshBtn.isVisible = true
okBtn.isVisible = false
okBtn.isEnabled = false
filterBtn.isEnabled = false
formatListLinearLayout.visibility = View.GONE
shimmers.visibility = View.VISIBLE
shimmers.startShimmer()
updateFormatsJob = launch {
updateFormatsJob = launch(Dispatchers.IO) {
try{
//simple download
if (items.size == 1) {
formatCollection.clear()
kotlin.runCatching {
val res = withContext(Dispatchers.IO){
infoUtil.getFormats(items.first()!!.url, currentFormatSource)
}
val res = infoUtil.getFormats(items.first()!!.url, currentFormatSource)
res.filter { it.format_note != "storyboard" }
chosenFormats = if (items.first()?.type == Type.audio) {
res.filter { it.format_note.contains("audio", ignoreCase = true) }
@ -223,11 +228,11 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
}
if (chosenFormats.isEmpty()) throw Exception()
formats = listOf(res)
formats.clear()
formats.addAll(res)
withContext(Dispatchers.Main){
listener.onFormatsUpdated(formats)
listener.onFormatsUpdated(res)
}
formatCollection.add(res)
}.onFailure { err ->
withContext(Dispatchers.Main){
UiUtil.handleResultResponse(requireActivity(), ResultViewModel.ResultsUiState(
@ -240,26 +245,44 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
//list format filtering
}else{
var progress = "0/${items.size}"
formatCollection.clear()
formats.clear()
var progressInt = 0
val formatCollection = itemsThatHaveFormats.map { it!!.allFormats }.toMutableList()
var progress = "0/${itemsWithMissingFormats.size}"
withContext(Dispatchers.Main) {
refreshBtn.text = progress
}
withContext(Dispatchers.IO){
infoUtil.getFormatsMultiple(items.map { it!!.url }, currentFormatSource) {
if (isActive) {
lifecycleScope.launch(Dispatchers.Main) {
formatCollection.add(it)
progress = "${formatCollection.size}/${items.size}"
refreshBtn.text = progress
val res = infoUtil.getFormatsMultiple(itemsWithMissingFormats.map { it!!.url }, currentFormatSource) {
if (!isActive) return@getFormatsMultiple
if (it.unavailable) {
lifecycleScope.launch {
multipleFormatsListener.onItemUnavailable(it.url)
items.removeAt(items.indexOfFirst { item -> item!!.url == it.url })
withContext(Dispatchers.Main) {
Snackbar.make(view, it.unavailableMessage, Snackbar.LENGTH_SHORT).show()
}
}
}else{
multipleFormatsListener.onFormatUpdated(it.url, it.formats)
items.firstOrNull { item -> item!!.url == it.url }?.apply {
allFormats.clear()
allFormats.addAll(it.formats)
}
progressInt++
lifecycleScope.launch(Dispatchers.Main) {
progress = "${progressInt}/${itemsWithMissingFormats.size}"
refreshBtn.text = progress
}
}
}
formats = formatCollection
withContext(Dispatchers.Main){
listener.onFormatsUpdated(formats)
}
formatCollection.addAll(res)
if (!isActive) return@launch
val flatFormatCollection = formatCollection.flatten()
val commonFormats =
@ -267,14 +290,13 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
.filter { it.value == items.size }
.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }
.map { it.value }
formats.addAll(commonFormats)
chosenFormats = commonFormats.filter { it.filesize != 0L }
.mapTo(mutableListOf()) { it.copy() }
chosenFormats = when (items.first()?.type) {
Type.audio -> chosenFormats.filter {
it.format_note.contains(
"audio",
ignoreCase = true
)
it.vcodec.isBlank() || it.vcodec == "none"
}
else -> chosenFormats
@ -286,24 +308,25 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
.sumOf { itt -> itt.filesize }
}
}
hasGenericFormats = formatCollection.size == 0
isMissingFormats = formats.isEmpty()
withContext(Dispatchers.Main){
shimmers.visibility = View.GONE
shimmers.stopShimmer()
formatSourceLinear.isVisible = canSwitch
filterBtn.isVisible = !hasGenericFormats
addFormatsToView()
refreshBtn.isVisible = hasGenericFormats
refreshBtn.isEnabled = hasGenericFormats
refreshBtn.isVisible = isMissingFormats
refreshBtn.isEnabled = isMissingFormats
filterBtn.isEnabled = true
okBtn.isEnabled = true
formatListLinearLayout.visibility = View.VISIBLE
}
}catch (e: Exception){
withContext(Dispatchers.Main) {
refreshBtn.isEnabled = true
refreshBtn.text = getString(R.string.update_formats)
filterBtn.isEnabled = true
okBtn.isEnabled = true
refreshBtn.text = getString(R.string.update)
formatListLinearLayout.visibility = View.VISIBLE
shimmers.visibility = View.GONE
formatSourceLinear.isVisible = canSwitch
shimmers.stopShimmer()
e.printStackTrace()
@ -334,54 +357,92 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
filterSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
filterSheet.setContentView(R.layout.format_category_sheet)
val all = filterSheet.findViewById<TextView>(R.id.all)
val suggested = filterSheet.findViewById<TextView>(R.id.suggested)
val smallest = filterSheet.findViewById<TextView>(R.id.smallest)
val generic = filterSheet.findViewById<TextView>(R.id.generic)
//format filter
filterSheet.findViewById<LinearLayout>(R.id.format_filter_linear)?.isVisible = !isMissingFormats
if (!isMissingFormats) {
val all = filterSheet.findViewById<TextView>(R.id.all)
val suggested = filterSheet.findViewById<TextView>(R.id.suggested)
val smallest = filterSheet.findViewById<TextView>(R.id.smallest)
val generic = filterSheet.findViewById<TextView>(R.id.generic)
val filterOptions = listOf(all!!, suggested!!,smallest!!, generic!!)
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
when(filterBy) {
FormatCategory.ALL -> all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
FormatCategory.SUGGESTED -> {
val filterOptions = listOf(all!!, suggested!!,smallest!!, generic!!)
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
when(filterBy) {
FormatCategory.ALL -> all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
FormatCategory.SUGGESTED -> {
suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
}
FormatCategory.SMALLEST -> {
smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
}
FormatCategory.GENERIC -> {
generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
}
}
all.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.ALL
all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
}
suggested.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.SUGGESTED
suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
}
FormatCategory.SMALLEST -> {
smallest.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.SMALLEST
smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
}
FormatCategory.GENERIC -> {
generic.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.GENERIC
generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
}
}
all.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.ALL
all.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
}
suggested.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.SUGGESTED
suggested.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
}
smallest.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.SMALLEST
smallest.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
}
generic.setOnClickListener {
filterOptions.forEach { it.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0) }
filterBy = FormatCategory.GENERIC
generic.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
addFormatsToView()
filterSheet.dismiss()
//format source
val formatSourceLinear = filterSheet.findViewById<LinearLayout>(R.id.format_source_linear)!!
val canSwitch = items.all { it!!.url.isYoutubeURL() }
formatSourceLinear.isVisible = canSwitch
if (canSwitch) {
val formatSourceOptions = mutableListOf<TextView>()
val availableSources = resources.getStringArray(R.array.formats_source)
val availableSourcesValues = resources.getStringArray(R.array.formats_source_values)
val currentSource = currentFormatSource ?: sharedPreferences.getString("formats_source", "yt-dlp")
formatSourceLinear.isVisible = true
availableSources.forEachIndexed { idx, it ->
val txt = requireActivity().layoutInflater.inflate(R.layout.selectable_textview_filter, null) as TextView
val tag = availableSourcesValues[idx]
txt.text = it
txt.tag = tag
txt.setOnClickListener {
currentFormatSource = it.tag.toString()
refreshBtn.performClick()
filterSheet.dismiss()
}
formatSourceOptions.add(txt)
formatSourceLinear.addView(txt)
}
formatSourceOptions.firstOrNull { it.tag == currentSource }?.apply {
setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check, 0,0,0)
}
}
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
filterSheet.behavior.peekHeight = displayMetrics.heightPixels
@ -390,21 +451,26 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
}
private fun returnFormats(){
//simple video format selection
if (items.size == 1){
listener.onFormatClick(listOf(FormatTuple(selectedVideo, selectedAudios.ifEmpty { listOf(downloadViewModel.getFormat(chosenFormats, Type.audio)) })))
//simple video format selection
listener.onFormatClick(FormatTuple(selectedVideo, selectedAudios.ifEmpty { listOf(downloadViewModel.getFormat(chosenFormats, Type.audio)) }))
}else{
//playlist format selection
val selectedFormats = mutableListOf<Format?>()
formatCollection.forEach {
selectedFormats.add(it.first{ f -> f.format_id == selectedVideo?.format_id})
val formatsToReturn = mutableListOf<MultipleItemFormatTuple>()
items.forEach {
formatsToReturn.add(
MultipleItemFormatTuple(
it!!.url,
FormatTuple(
it.allFormats.firstOrNull { f -> f.format_id == selectedVideo?.format_id },
selectedAudios.map { sa ->
it.allFormats.first { a -> a.format_id == sa.format_id }
}.ifEmpty { null }
)
)
)
}
if (selectedFormats.isEmpty()) {
items.forEach { _ ->
selectedFormats.add(selectedVideo)
}
}
listener.onFormatClick(selectedFormats.map { FormatTuple(it, selectedAudios) })
multipleFormatsListener.onFormatClick(formatsToReturn)
}
}
@ -466,7 +532,9 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
}
}
val canMultiSelectAudio = items.first()?.type == Type.video && finalFormats.find { it.format_note.contains("audio", ignoreCase = true) } != null
val canMultiSelectAudio = items.first()?.type == Type.video && finalFormats.find { it.vcodec.isBlank() || it.vcodec == "none" } != null
videoFormatList.removeAllViews()
audioFormatList.removeAllViews()
@ -495,9 +563,9 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
if (finalFormats.isEmpty()){
finalFormats = if (items.first()?.type == Type.audio){
infoUtil.getGenericAudioFormats(requireContext().resources)
genericAudioFormats
}else{
infoUtil.getGenericVideoFormats(requireContext().resources)
genericVideoFormats
}
}
@ -533,18 +601,22 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
}
}else{
if (items.size == 1){
listener.onFormatClick(listOf(FormatTuple(format, null)))
listener.onFormatClick(FormatTuple(format, null))
}else{
val selectedFormats = mutableListOf<Format>()
formatCollection.forEach {
selectedFormats.add(it.firstOrNull{ f -> f.format_id == format.format_id} ?: format)
val formatsToReturn = mutableListOf<MultipleItemFormatTuple>()
val f = if (genericAudioFormats.contains(format) || genericVideoFormats.contains(format)) format else null
items.forEach {
formatsToReturn.add(
MultipleItemFormatTuple(
it!!.url,
FormatTuple(
f ?: it.allFormats.firstOrNull { af -> af.format_id == format.format_id },
null
)
)
)
}
if (selectedFormats.isEmpty()) {
items.forEach { _ ->
selectedFormats.add(format)
}
}
listener.onFormatClick(selectedFormats.map { FormatTuple(it, null) })
multipleFormatsListener.onFormatClick(formatsToReturn)
}
dismiss()
}
@ -594,25 +666,36 @@ class FormatSelectionBottomSheetDialog(private val _items: List<DownloadItem?>?
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
cleanUp()
super.onDismiss(dialog)
}
private fun cleanUp(){
kotlin.runCatching {
updateFormatsJob?.cancel()
updateFormatsJob?.cancel(CancellationException())
parentFragmentManager.beginTransaction().remove(parentFragmentManager.findFragmentByTag("formatSheet")!!).commit()
}
}
}
interface OnFormatClickListener{
fun onFormatClick(selectedFormats: List<FormatTuple>)
fun onContinueOnBackground() {}
fun onFormatsUpdated(allFormats: List<List<Format>>)
fun onFormatClick(formatTuple: FormatTuple)
fun onFormatsUpdated(allFormats: List<Format>)
}
interface OnMultipleFormatClickListener{
fun onFormatClick(formatTuple: List<MultipleItemFormatTuple>)
fun onContinueOnBackground() {}
fun onFormatUpdated(url: String, formats: List<Format>)
fun onItemUnavailable(url: String)
}
class MultipleItemFormatTuple internal constructor(
var url: String,
var formatTuple: FormatTuple
)
class FormatTuple internal constructor(
var format: Format?,
var audioFormats: List<Format>?

View file

@ -24,6 +24,7 @@ import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
@ -39,11 +40,11 @@ import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadMinifiedAdapter
import com.deniscerri.ytdl.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.setFullScreen
@ -452,127 +453,28 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
downloadViewModel.getItemByID(itemID)
}
val bottomSheet = BottomSheetDialog(requireContext())
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.history_item_details_bottom_sheet)
val title = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_title)
title!!.text = item.title.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
val author = bottomSheet.findViewById<TextView>(R.id.bottom_sheet_author)
author!!.text = item.author.ifEmpty { "`${requireContext().getString(R.string.defaultValue)}`" }
// BUTTON ----------------------------------
val btn = bottomSheet.findViewById<MaterialButton>(R.id.downloads_download_button_type)
when (item.type) {
DownloadViewModel.Type.audio -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_music)
}
DownloadViewModel.Type.video -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_video)
}
else -> {
btn!!.icon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_terminal)
}
}
val time = bottomSheet.findViewById<Chip>(R.id.time)
val formatNote = bottomSheet.findViewById<Chip>(R.id.format_note)
val container = bottomSheet.findViewById<Chip>(R.id.container_chip)
val codec = bottomSheet.findViewById<Chip>(R.id.codec)
val fileSize = bottomSheet.findViewById<Chip>(R.id.file_size)
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) time!!.visibility = View.GONE
else {
val calendar = Calendar.getInstance()
calendar.timeInMillis = item.downloadStartTime
time!!.text = SimpleDateFormat(DateFormat.getBestDateTimePattern(Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(calendar.time)
time.setOnClickListener {
UiUtil.showDatePicker(parentFragmentManager) {
bottomSheet.dismiss()
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(item.id)
item.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
UiUtil.showDownloadItemDetailsCard(
item,
requireActivity(),
DownloadRepository.Status.valueOf(item.status),
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
sheet.hide()
removeQueuedItem(itemID)
},
downloadItem = {
runBlocking{
downloadViewModel.queueDownloads(listOf(it))
}
}
}
if (item.format.format_note == "?" || item.format.format_note == "") formatNote!!.visibility =
View.GONE
else formatNote!!.text = item.format.format_note
if (item.format.container != "") container!!.text = item.format.container.uppercase()
else container!!.visibility = View.GONE
val codecText =
if (item.format.encoding != "") {
item.format.encoding.uppercase()
}else if (item.format.vcodec != "none" && item.format.vcodec != ""){
item.format.vcodec.uppercase()
} else {
item.format.acodec.uppercase()
}
if (codecText == "" || codecText == "none"){
codec!!.visibility = View.GONE
}else{
codec!!.visibility = View.VISIBLE
codec.text = codecText
}
val fileSizeReadable = FileUtil.convertFileSize(item.format.filesize)
if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE
else fileSize!!.text = fileSizeReadable
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
val url = item.url
link!!.text = url
link.tag = itemID
link.setOnClickListener{
bottomSheet.dismiss()
UiUtil.openLinkIntent(requireContext(), item.url)
}
link.setOnLongClickListener{
bottomSheet.dismiss()
UiUtil.copyLinkToClipBoard(requireContext(), item.url)
true
}
val remove = bottomSheet.findViewById<Button>(R.id.bottomsheet_remove_button)
remove!!.tag = itemID
remove.setOnClickListener{
bottomSheet.hide()
removeQueuedItem(itemID)
}
val openFile = bottomSheet.findViewById<Button>(R.id.bottomsheet_open_file_button)
openFile!!.visibility = View.GONE
val downloadNow = bottomSheet.findViewById<Button>(R.id.bottomsheet_redownload_button)
if (item.downloadStartTime <= System.currentTimeMillis() / 1000) downloadNow!!.visibility = View.GONE
else{
downloadNow!!.text = getString(R.string.download_now)
downloadNow.setOnClickListener {
bottomSheet.dismiss()
downloadViewModel.deleteDownload(item.id)
item.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(item.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(item))
}
}
}
bottomSheet.show()
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
bottomSheet.behavior.peekHeight = displayMetrics.heightPixels
bottomSheet.window!!.setLayout(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
},
longClickDownloadButton = {
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
Pair("downloadItem", it),
Pair("result", downloadViewModel.createResultItemFromDownload(it)),
Pair("type", it.type)
)
)
},
scheduleButtonClick = {}
)
}
}
@ -608,7 +510,6 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
lifecycleScope.launch {
val id = itemID.toInt()
YoutubeDL.getInstance().destroyProcessById(id.toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toString())
notificationUtil.cancelDownloadNotification(id)
withContext(Dispatchers.IO){

View file

@ -15,14 +15,19 @@ import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.widget.LinearLayout
import android.widget.ListView
import android.widget.RelativeLayout
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
import androidx.core.view.setMargins
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -49,6 +54,7 @@ import com.deniscerri.ytdl.work.DownloadWorker
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
@ -70,27 +76,15 @@ import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener, QueuedDownloadAdapter.OnItemClickListener {
class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickListener {
private var fragmentView: View? = null
private var activity: Activity? = null
private lateinit var downloadViewModel : DownloadViewModel
private lateinit var activeDownloadsLinear: LinearLayout
private lateinit var activeDownloadsNoResults: View
//private lateinit var activeDownloadsLinear: LinearLayout
private lateinit var activeRecyclerView : RecyclerView
private lateinit var activeDownloads : ActiveDownloadAdapter
private lateinit var queuedDownloadsLinear: LinearLayout
private lateinit var queuedRecyclerView : RecyclerView
private lateinit var queuedDownloads : QueuedDownloadAdapter
private lateinit var queuedFilesizeCount : TextView
private var queuedTotalSize = 0
private lateinit var queuedDragHandleToggle : TextView
private var queuedActionMode : ActionMode? = null
private lateinit var inQueueLabel : MaterialButton
private lateinit var activeQueuedLinear: LinearLayout
private lateinit var notificationUtil: NotificationUtil
private lateinit var pause: ExtendedFloatingActionButton
private lateinit var resume: ExtendedFloatingActionButton
@ -119,34 +113,12 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
workManager = WorkManager.getInstance(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
activeDownloadsLinear = view.findViewById(R.id.active_linear)
activeDownloadsNoResults = view.findViewById(R.id.active_no_results)
activeDownloads = ActiveDownloadAdapter(this,requireActivity())
activeRecyclerView = view.findViewById(R.id.download_recyclerview)
activeRecyclerView.forceFastScrollMode()
activeRecyclerView.adapter = activeDownloads
activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
queuedDownloadsLinear = view.findViewById(R.id.queue_linear)
val itemTouchHelper = ItemTouchHelper(queuedDragDropHelper)
queuedDownloads = QueuedDownloadAdapter(this, requireActivity(), itemTouchHelper)
queuedRecyclerView = view.findViewById(R.id.queued_recyclerview)
queuedRecyclerView.forceFastScrollMode()
queuedRecyclerView.adapter = queuedDownloads
queuedRecyclerView.enableFastScroll()
itemTouchHelper.attachToRecyclerView(queuedRecyclerView)
queuedFilesizeCount = view.findViewById(R.id.filesize)
queuedDragHandleToggle = view.findViewById(R.id.drag)
inQueueLabel = view.findViewById(R.id.in_queue_label)
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("queued")){
val swipeItemTouchHelper = ItemTouchHelper(queuedSwipeCallback)
swipeItemTouchHelper.attachToRecyclerView(queuedRecyclerView)
}
queuedRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
activeQueuedLinear = view.findViewById(R.id.active_queued_linear)
pause = view.findViewById(R.id.pause)
pause.isVisible = false
resume = view.findViewById(R.id.resume)
@ -164,13 +136,13 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
notificationUtil.cancelDownloadNotification(it.id.toInt())
}
preferences.edit().putBoolean("paused_downloads", true).apply()
pause.isEnabled = false
pause.isVisible = false
resume.isEnabled = false
resume.isVisible = true
activeDownloads.notifyDataSetChanged()
withContext(Dispatchers.Main){
delay(1000)
pause.isVisible = false
pause.isEnabled = true
resume.isVisible = true
resume.isEnabled = true
}
}
@ -179,7 +151,9 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
resume.setOnClickListener {
lifecycleScope.launch {
preferences.edit().putBoolean("paused_downloads", false).apply()
resume.isEnabled = false
resume.isVisible = false
pause.isEnabled = false
pause.isVisible = true
withContext(Dispatchers.IO) {
downloadViewModel.resetActiveToQueued()
downloadViewModel.startDownloadWorker(listOf())
@ -189,16 +163,14 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
withContext(Dispatchers.Main){
delay(1000)
resume.isVisible = false
resume.isEnabled = true
pause.isVisible = true
pause.isEnabled = true
}
}
}
lifecycleScope.launch {
val activeQueuedDownloadsCount = withContext(Dispatchers.IO) {
downloadViewModel.getActiveDownloadsCount()
downloadViewModel.getActiveQueuedDownloadsCount()
}
val pausedDownloads = preferences.getBoolean("paused_downloads", false)
pause.isVisible = activeQueuedDownloadsCount > 0 && !pausedDownloads
@ -207,8 +179,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
lifecycleScope.launch {
downloadViewModel.activeQueuedDownloadsCount.collectLatest {
activeQueuedLinear.isVisible = it > 0
noResults.isVisible = it == 0
if (it == 0) {
pause.isVisible = false
resume.isVisible = false
@ -218,57 +188,11 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
lifecycleScope.launch {
downloadViewModel.activeDownloads.collectLatest {
activeDownloadsNoResults.isVisible = it.isEmpty()
noResults.isVisible = it.isEmpty()
activeDownloads.submitList(it)
}
}
lifecycleScope.launch {
downloadViewModel.queuedDownloads.collectLatest {
queuedDownloads.submitData(it)
}
}
queuedDownloads.addLoadStateListener { loadState ->
lifecycleScope.launch {
if (loadState.append.endOfPaginationReached )
{
if (queuedDownloads.itemCount < 1){
queuedFilesizeCount.visibility = View.GONE
}else{
val size = withContext(Dispatchers.IO){
FileUtil.convertFileSize(downloadViewModel.getQueuedCollectedFileSize())
}
if (size == "?") queuedFilesizeCount.visibility = View.GONE
else {
queuedFilesizeCount.visibility = View.VISIBLE
queuedFilesizeCount.text = "${getString(R.string.file_size)}: ~ $size"
}
}
}
}
}
lifecycleScope.launch {
downloadViewModel.getTotalSize(listOf(DownloadRepository.Status.Queued)).observe(viewLifecycleOwner){
queuedDownloadsLinear.isVisible = it > 0
queuedTotalSize = it
queuedDragHandleToggle.isVisible = it > 1
if (it > 0) {
val badgeDrawable = BadgeDrawable.create(requireContext())
badgeDrawable.number = it
badgeDrawable.isVisible = true
badgeDrawable.verticalOffset = 25
requireView().post {
BadgeUtils.attachBadgeDrawable(badgeDrawable, inQueueLabel)
}
}
}
}
queuedDragHandleToggle.setOnClickListener {
queuedDownloads.toggleShowDragHandle()
}
}
//dont remove
@ -280,14 +204,14 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
requireActivity().runOnUiThread {
try {
progressBar?.setProgressCompat(event.progress, true)
if (event.progress > 95) {
pause.isEnabled = false
}
outputText?.text = event.output
}catch (ignored: Exception) {}
}
}
fun hidePauseBtn(){
pause.isVisible = false
}
override fun onStart() {
super.onStart()
@ -351,398 +275,4 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
}
//QUEUED ITEMS FUNCTIONS
private val queuedContextualActionBar = object : ActionMode.Callback {
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
mode!!.menuInflater.inflate(R.menu.queued_menu_context, menu)
mode.title = "${queuedDownloads.getSelectedObjectsCount(queuedTotalSize)} ${getString(R.string.selected)}"
return true
}
override fun onPrepareActionMode(
mode: ActionMode?,
menu: Menu?
): Boolean {
return false
}
override fun onActionItemClicked(
mode: ActionMode?,
item: MenuItem?
): Boolean {
return when (item!!.itemId) {
R.id.select_between -> {
lifecycleScope.launch {
val selectedIDs = getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(
DownloadRepository.Status.Queued).toListString())
}.toMutableList()
idsInMiddle.addAll(selectedIDs)
if (idsInMiddle.isNotEmpty()){
queuedDownloads.checkMultipleItems(idsInMiddle)
queuedActionMode?.title = "${idsInMiddle.count()} ${getString(R.string.selected)}"
}
mode?.menu?.findItem(R.id.select_between)?.isVisible = false
}
true
}
R.id.delete_results -> {
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete_multiple_items))
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
queuedDownloads.clearCheckedItems()
for (id in selectedObjects){
YoutubeDL.getInstance().destroyProcessById(id.toInt().toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
notificationUtil.cancelDownloadNotification(id.toInt())
}
downloadViewModel.deleteAllWithID(selectedObjects)
queuedActionMode?.finish()
}
}
deleteDialog.show()
true
}
R.id.select_all -> {
queuedDownloads.checkAll()
mode?.title = "(${queuedDownloads.getSelectedObjectsCount(queuedTotalSize)}) ${resources.getString(R.string.all_items_selected)}"
true
}
R.id.invert_selected -> {
queuedDownloads.invertSelected()
val selectedObjects = queuedDownloads.getSelectedObjectsCount(queuedTotalSize)
queuedActionMode!!.title = "$selectedObjects ${getString(R.string.selected)}"
if (selectedObjects == 0) queuedActionMode?.finish()
true
}
R.id.up -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
queuedDownloads.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.putAtTopOfQueue(selectedObjects)
}
queuedActionMode?.finish()
}
true
}
R.id.down -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
queuedDownloads.clearCheckedItems()
withContext(Dispatchers.IO){
downloadViewModel.putAtBottomOfQueue(selectedObjects)
}
queuedActionMode?.finish()
}
true
}
R.id.copy_urls -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
val urls = withContext(Dispatchers.IO){
downloadViewModel.getURLsByIds(selectedObjects)
}
UiUtil.copyToClipboard(urls.joinToString("\n"), requireActivity())
queuedActionMode?.finish()
}
true
}
else -> false
}
}
override fun onDestroyActionMode(mode: ActionMode?) {
queuedActionMode = null
queuedDownloads.clearCheckedItems()
}
suspend fun getSelectedIDs() : List<Long>{
return if (queuedDownloads.inverted || queuedDownloads.checkedItems.isEmpty()){
withContext(Dispatchers.IO){
downloadViewModel.getItemIDsNotPresentIn(queuedDownloads.checkedItems.toList(), listOf(
DownloadRepository.Status.Queued))
}
}else{
queuedDownloads.checkedItems.toList()
}
}
}
var movedToNewPositionID = 0L
private val queuedDragDropHelper: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val fromPosition = viewHolder.bindingAdapterPosition
val toPosition = target.bindingAdapterPosition
movedToNewPositionID = target.itemView.tag.toString().toLong()
queuedDownloads.notifyItemMoved(fromPosition, toPosition)
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
}
override fun onSelectedChanged(
viewHolder: RecyclerView.ViewHolder?,
actionState: Int
) {
super.onSelectedChanged(viewHolder, actionState)
if (ItemTouchHelper.ACTION_STATE_DRAG == actionState) {
/**
* Change alpha, scale and elevation on drag.
*/
(viewHolder?.itemView as? MaterialCardView)?.also {
AnimatorSet().apply {
this.duration = 100L
this.interpolator = AccelerateDecelerateInterpolator()
playTogether(
UiUtil.getAlphaAnimator(it, 0.7f),
UiUtil.getScaleXAnimator(it, 1.02f),
UiUtil.getScaleYAnimator(it, 1.02f),
UiUtil.getElevationAnimator(it, R.dimen.elevation_6dp)
)
}.start()
}
}
}
override fun clearView(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
) {
super.clearView(recyclerView, viewHolder)
/**
* Clear alpha, scale and elevation after drag/swipe
*/
(viewHolder.itemView as? MaterialCardView)?.also {
AnimatorSet().apply {
this.duration = 100L
this.interpolator = AccelerateDecelerateInterpolator()
playTogether(
UiUtil.getAlphaAnimator(it, 1f),
UiUtil.getScaleXAnimator(it, 1f),
UiUtil.getScaleYAnimator(it, 1f),
UiUtil.getElevationAnimator(it, R.dimen.elevation_2dp)
)
}.start()
}
downloadViewModel.putAtPosition(viewHolder.itemView.tag.toString().toLong(), movedToNewPositionID)
}
override fun isLongPressDragEnabled(): Boolean {
return false
}
}
private var queuedSwipeCallback: ItemTouchHelper.SimpleCallback =
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
override fun onMove(recyclerView: RecyclerView,viewHolder: RecyclerView.ViewHolder,target: RecyclerView.ViewHolder
): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val itemID = viewHolder.itemView.tag.toString().toLong()
when (direction) {
ItemTouchHelper.LEFT -> {
lifecycleScope.launch {
val deletedItem = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
queuedDownloads.notifyItemChanged(viewHolder.bindingAdapterPosition)
removeQueuedItem(deletedItem.id)
}
}
}
}
override fun onChildDraw(
c: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
dX: Float,
dY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
RecyclerViewSwipeDecorator.Builder(
requireContext(),
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
.addSwipeLeftBackgroundColor(Color.RED)
.addSwipeLeftActionIcon(R.drawable.baseline_delete_24)
.addSwipeRightBackgroundColor(
MaterialColors.getColor(
requireContext(),
R.attr.colorOnSurfaceInverse, Color.TRANSPARENT
)
)
.create()
.decorate()
super.onChildDraw(
c,
recyclerView,
viewHolder,
dX,
dY,
actionState,
isCurrentlyActive
)
}
}
override fun onQueuedCancelClick(itemID: Long) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toInt().toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}.let {
it.status = DownloadRepository.Status.Cancelled.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(it)
}
}
}
}
override fun onMoveQueuedItemToTop(itemID: Long) {
lifecycleScope.launch {
downloadViewModel.putAtTopOfQueue(listOf(itemID))
}
}
override fun onMoveQueuedItemToBottom(itemID: Long) {
lifecycleScope.launch {
downloadViewModel.putAtBottomOfQueue(listOf(itemID))
}
}
override fun onQueuedCardClick(itemID: Long) {
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(itemID)
}
UiUtil.showDownloadItemDetailsCard(
item,
requireActivity(),
DownloadRepository.Status.valueOf(item.status),
removeItem = { it: DownloadItem, sheet: BottomSheetDialog ->
sheet.hide()
removeQueuedItem(it.id)
},
downloadItem = {
downloadViewModel.deleteDownload(it.id)
it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(it))
}
},
longClickDownloadButton = {
lifecycleScope.launch {
it.status = DownloadRepository.Status.Saved.toString()
withContext(Dispatchers.IO){
downloadViewModel.updateToStatus(it.id, DownloadRepository.Status.Saved)
}
findNavController().navigate(R.id.downloadBottomSheetDialog, bundleOf(
Pair("downloadItem", it),
Pair("result", downloadViewModel.createResultItemFromDownload(it)),
Pair("type", it.type)
)
)
}
},
scheduleButtonClick = {downloadItem ->
UiUtil.showDatePicker(parentFragmentManager) {
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(downloadItem.id)
downloadItem.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem))
}
}
}
)
}
}
override fun onQueuedCardSelect(isChecked: Boolean, position: Int) {
lifecycleScope.launch {
val selectedObjects = queuedDownloads.getSelectedObjectsCount(queuedTotalSize)
if (queuedActionMode == null) queuedActionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(queuedContextualActionBar)
val now = System.currentTimeMillis()
queuedActionMode?.apply {
if (selectedObjects == 0){
this.finish()
}else{
this.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.up).isVisible = position > 0
this.menu.findItem(R.id.down).isVisible = position < queuedTotalSize
this.menu.findItem(R.id.select_between).isVisible = false
if(selectedObjects == 2){
val selectedIDs = queuedContextualActionBar.getSelectedIDs().sortedBy { it }
val idsInMiddle = withContext(Dispatchers.IO){
downloadViewModel.getIDsBetweenTwoItems(selectedIDs.first(), selectedIDs.last(), listOf(DownloadRepository.Status.Queued).toListString())
}
this.menu.findItem(R.id.select_between).isVisible = idsInMiddle.isNotEmpty()
}
}
}
}
}
private fun removeQueuedItem(id: Long){
lifecycleScope.launch {
val item = withContext(Dispatchers.IO){
downloadViewModel.getItemByID(id)
}
val deleteDialog = MaterialAlertDialogBuilder(requireContext())
deleteDialog.setTitle(getString(R.string.you_are_going_to_delete) + " \"" + item.title + "\"!")
deleteDialog.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() }
deleteDialog.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int ->
item.status = DownloadRepository.Status.Cancelled.toString()
lifecycleScope.launch(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
Snackbar.make(queuedRecyclerView, getString(R.string.cancelled) + ": " + item.title, Snackbar.LENGTH_LONG)
.setAction(getString(R.string.undo)) {
lifecycleScope.launch(Dispatchers.IO) {
downloadViewModel.deleteDownload(item.id)
downloadViewModel.queueDownloads(listOf(item))
}
}.show()
}
deleteDialog.show()
}
}
}

View file

@ -79,6 +79,8 @@ class DownloadQueueMainFragment : Fragment(){
val isInNavBar = NavbarUtil.getNavBarItems(requireActivity()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
if (isInNavBar) {
topAppBar.navigationIcon = null
}else{
mainActivity.hideBottomNavigation()
}
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
@ -90,7 +92,7 @@ class DownloadQueueMainFragment : Fragment(){
overScrollMode = View.OVER_SCROLL_NEVER
}
val fragments = mutableListOf(ActiveDownloadsFragment(), /*QueuedDownloadsFragment(),*/ ScheduledDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment())
val fragments = mutableListOf(ActiveDownloadsFragment(), QueuedDownloadsFragment(), ScheduledDownloadsFragment(), CancelledDownloadsFragment(), ErroredDownloadsFragment(), SavedDownloadsFragment())
fragmentAdapter = DownloadListFragmentAdapter(
childFragmentManager,
@ -104,11 +106,11 @@ class DownloadQueueMainFragment : Fragment(){
TabLayoutMediator(tabLayout, viewPager2) { tab, position ->
when (position) {
0 -> tab.text = getString(R.string.running)
// 1 -> tab.text = getString(R.string.in_queue)
1 -> tab.text = getString(R.string.scheduled)
2 -> tab.text = getString(R.string.cancelled)
3 -> tab.text = getString(R.string.errored)
4 -> tab.text = getString(R.string.saved)
1 -> tab.text = getString(R.string.in_queue)
2 -> tab.text = getString(R.string.scheduled)
3 -> tab.text = getString(R.string.cancelled)
4 -> tab.text = getString(R.string.errored)
5 -> tab.text = getString(R.string.saved)
}
}.attach()
@ -130,13 +132,12 @@ class DownloadQueueMainFragment : Fragment(){
initMenu()
}
})
mainActivity.hideBottomNavigation()
initMenu()
if (arguments?.getString("tab") != null){
tabLayout.getTabAt(3)!!.select()
tabLayout.getTabAt(4)!!.select()
viewPager2.postDelayed( {
viewPager2.setCurrentItem(3, false)
viewPager2.setCurrentItem(4, false)
val reconfigureID = arguments?.getLong("reconfigure")
reconfigureID?.apply {
notificationUtil.cancelErroredNotification(this.toInt())
@ -166,30 +167,30 @@ class DownloadQueueMainFragment : Fragment(){
}
}
}
// lifecycleScope.launch {
// downloadViewModel.queuedDownloadsCount.collectLatest {
// tabLayout.getTabAt(1)?.apply {
// createBadge(it)
// }
// }
// }
lifecycleScope.launch {
downloadViewModel.scheduledDownloadsCount.collectLatest {
downloadViewModel.queuedDownloadsCount.collectLatest {
tabLayout.getTabAt(1)?.apply {
createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.cancelledDownloadsCount.collectLatest {
downloadViewModel.scheduledDownloadsCount.collectLatest {
tabLayout.getTabAt(2)?.apply {
createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.erroredDownloadsCount.collectLatest {
downloadViewModel.cancelledDownloadsCount.collectLatest {
tabLayout.getTabAt(3)?.apply {
createBadge(it)
}
}
}
lifecycleScope.launch {
downloadViewModel.erroredDownloadsCount.collectLatest {
tabLayout.getTabAt(4)?.apply {
removeBadge()
if (it > 0) createBadge(it)
}
@ -197,7 +198,7 @@ class DownloadQueueMainFragment : Fragment(){
}
lifecycleScope.launch {
downloadViewModel.savedDownloadsCount.collectLatest {
tabLayout.getTabAt(4)?.apply {
tabLayout.getTabAt(5)?.apply {
removeBadge()
if (it > 0) createBadge(it)
}

View file

@ -116,6 +116,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
sortChip = view.findViewById(R.id.sortChip)
selectedObjects = arrayListOf()
val isInNavBar = NavbarUtil.getNavBarItems(requireActivity()).any { n -> n.itemId == R.id.historyFragment && n.isVisible }
if (isInNavBar) {
topAppBar?.navigationIcon = null
}else{
mainActivity?.hideBottomNavigation()
}
topAppBar?.setNavigationOnClickListener { mainActivity?.onBackPressedDispatcher?.onBackPressed() }
historyList = mutableListOf()
allhistoryList = mutableListOf()

View file

@ -211,7 +211,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
adapter.clearCheckedItems()
for (id in selectedObjects){
YoutubeDL.getInstance().destroyProcessById(id.toInt().toString())
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
notificationUtil.cancelDownloadNotification(id.toInt())
}
downloadViewModel.deleteAllWithID(selectedObjects)
@ -222,20 +221,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
deleteDialog.show()
true
}
R.id.download -> {
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
adapter.clearCheckedItems()
for (id in selectedObjects){
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
}
withContext(Dispatchers.IO) {
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(selectedObjects)
}
actionMode?.finish()
}
true
}
R.id.select_all -> {
adapter.checkAll()
mode?.title = "(${adapter.getSelectedObjectsCount(totalSize)}) ${resources.getString(R.string.all_items_selected)}"
@ -469,7 +454,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
downloadItem = {
downloadViewModel.deleteDownload(it.id)
it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(it))
}
@ -493,7 +477,6 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(downloadItem.id)
downloadItem.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem))
}
@ -507,16 +490,11 @@ class QueuedDownloadsFragment : Fragment(), QueuedDownloadAdapter.OnItemClickLis
lifecycleScope.launch {
val selectedObjects = adapter.getSelectedObjectsCount(totalSize)
if (actionMode == null) actionMode = (getActivity() as AppCompatActivity?)!!.startSupportActionMode(contextualActionBar)
val now = System.currentTimeMillis()
actionMode?.apply {
if (selectedObjects == 0){
this.finish()
}else{
this.title = "$selectedObjects ${getString(R.string.selected)}"
this.menu.findItem(R.id.download).isVisible = withContext(Dispatchers.IO){
downloadViewModel.checkAllQueuedItemsAreScheduledAfterNow(adapter.checkedItems.toList(), adapter.inverted, now)
}
this.menu.findItem(R.id.up).isVisible = position > 0
this.menu.findItem(R.id.down).isVisible = position < totalSize
this.menu.findItem(R.id.select_between).isVisible = false

View file

@ -37,6 +37,7 @@ import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.forceFastScrollMode
import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -138,7 +139,6 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
downloadItem = {
downloadViewModel.deleteDownload(it.id)
it.downloadStartTime = 0
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(it.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(it))
}
@ -156,7 +156,6 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
Toast.makeText(context, getString(R.string.download_rescheduled_to) + " " + it.time, Toast.LENGTH_LONG).show()
downloadViewModel.deleteDownload(downloadItem.id)
downloadItem.downloadStartTime = it.timeInMillis
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(downloadItem.id.toString())
runBlocking {
downloadViewModel.queueDownloads(listOf(downloadItem))
adapter.notifyItemChanged(position)
@ -269,9 +268,6 @@ class ScheduledDownloadsFragment : Fragment(), ScheduledDownloadAdapter.OnItemCl
lifecycleScope.launch {
val selectedObjects = getSelectedIDs()
adapter.clearCheckedItems()
for (id in selectedObjects){
WorkManager.getInstance(requireContext()).cancelAllWorkByTag(id.toInt().toString())
}
withContext(Dispatchers.IO) {
downloadViewModel.resetScheduleTimeForItemsAndStartDownload(selectedObjects)
}

View file

@ -36,6 +36,7 @@ class MoreFragment : Fragment() {
private lateinit var logs: TextView
private lateinit var commandTemplates: TextView
private lateinit var downloadQueue: TextView
private lateinit var downloads: TextView
private lateinit var cookies: TextView
private lateinit var observeSources: TextView
private lateinit var terminateApp: TextView
@ -59,14 +60,26 @@ class MoreFragment : Fragment() {
terminal = view.findViewById(R.id.terminal)
logs = view.findViewById(R.id.logs)
commandTemplates = view.findViewById(R.id.command_templates)
downloads = view.findViewById(R.id.downloads)
downloadQueue = view.findViewById(R.id.download_queue)
cookies = view.findViewById(R.id.cookies)
observeSources = view.findViewById(R.id.observe_sources)
terminateApp = view.findViewById(R.id.terminate)
settings = view.findViewById(R.id.settings)
val showingTerminal = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.terminalActivity && n.isVisible }
var showingTerminal = false
var showingDownloads = false
var showingDownloadQueue = false
NavbarUtil.getNavBarItems(requireContext()).apply {
showingTerminal = any { n -> n.itemId == R.id.terminalActivity && n.isVisible }
showingDownloads = any { n -> n.itemId == R.id.historyFragment && n.isVisible }
showingDownloadQueue = any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
}
terminal.isVisible = !showingTerminal
downloads.isVisible = !showingDownloads
downloadQueue.isVisible = !showingDownloadQueue
terminal.setOnClickListener {
val intent = Intent(context, TerminalActivity::class.java)
@ -81,8 +94,9 @@ class MoreFragment : Fragment() {
findNavController().navigate(R.id.commandTemplatesFragment)
}
val showingDownloadQueue = NavbarUtil.getNavBarItems(requireContext()).any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }
downloadQueue.isVisible = !showingDownloadQueue
downloads.setOnClickListener {
findNavController().navigate(R.id.historyFragment)
}
downloadQueue.setOnClickListener {
findNavController().navigate(R.id.downloadQueueMainFragment)

View file

@ -34,6 +34,7 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DownloadLogFragment : Fragment() {
@ -96,7 +97,9 @@ class DownloadLogFragment : Fragment() {
CoroutineScope(Dispatchers.IO).launch {
val logItem = logViewModel.getItemById(id!!)
topAppBar.title = logItem.title
withContext(Dispatchers.Main){
topAppBar.title = logItem.title
}
}
content.isFocusable = true

View file

@ -69,13 +69,29 @@ class DownloadSettingsFragment : BaseSettingsFragment() {
archivePathResultLauncher.launch(intent)
true
}
val scheduler = AlarmScheduler(requireContext())
val useAlarmManagerInsteadOfWorkManager = findPreference<SwitchPreferenceCompat>("use_alarm_for_scheduling")
useAlarmManagerInsteadOfWorkManager?.setOnPreferenceChangeListener { preference, newValue ->
var allowChange = true
if (newValue as Boolean){
if (!scheduler.canSchedule() && Build.VERSION.SDK_INT >= 31){
Intent().also { intent ->
intent.action = Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM
requireContext().startActivity(intent)
}
allowChange = false
}
}
allowChange
}
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 ->
var allowChange = true

View file

@ -40,6 +40,7 @@ import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory
@ -476,4 +477,8 @@ object Extensions {
"<small>" + subtitle + "</small>" + "<br />", HtmlCompat.FROM_HTML_MODE_LEGACY)
}
fun String.isYoutubeURL() : Boolean {
return Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)").matcher(this).find()
}
}

View file

@ -57,33 +57,53 @@ class FormatSorter(private var context: Context) {
fun sortAudioFormats(formats: List<Format>) : List<Format> {
val orderPreferences = getAudioFormatImportance()
val fieldSorter: Comparator<Format> = Comparator { a, b ->
for (order in orderPreferences) {
when(order) {
"smallsize" -> {
(a.filesize).compareTo(b.filesize)
}
"id" -> {
(audioFormatIDPreference.contains(b.format_id)).compareTo(audioFormatIDPreference.contains(a.format_id))
}
"language" -> {
if (audioLanguagePreference.isBlank()) {
0
val fieldSorter: Comparator<Format> = object : Comparator<Format> {
override fun compare(a: Format, b: Format): Int {
for (order in orderPreferences) {
val comparison = when (order) {
"smallsize" -> {
(a.filesize).compareTo(b.filesize)
}
else {
(b.lang?.contains(audioLanguagePreference) == true).compareTo(a.lang?.contains(audioLanguagePreference) == true)
"id" -> {
(audioFormatIDPreference.contains(b.format_id)).compareTo(
audioFormatIDPreference.contains(a.format_id)
)
}
"language" -> {
if (audioLanguagePreference.isBlank()) {
0
} else {
(b.lang?.contains(audioLanguagePreference) == true).compareTo(
a.lang?.contains(
audioLanguagePreference
) == true
)
}
}
"codec" -> {
("^(${audioCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE)
.matches(b.acodec))
.compareTo(
"^(${audioCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE)
.matches(a.acodec)
)
}
"container" -> {
(audioContainerPreference == b.container).compareTo(
audioContainerPreference == a.container
)
}
else -> 0
}
"codec" -> {
("^(${audioCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE).matches(b.acodec))
.compareTo("^(${audioCodecPreference}).+$".toRegex(RegexOption.IGNORE_CASE).matches(a.acodec))
}
"container" -> {
(audioContainerPreference == b.container).compareTo(audioContainerPreference == a.container)
}
if (comparison != 0) return comparison
}
return 0
}
0
}
return formats.sortedWith(fieldSorter)
}
@ -152,4 +172,101 @@ class FormatSorter(private var context: Context) {
return formats.sortedWith(fieldSorter)
}
//OLD CODE JUST FOR STORAGE / REFERENCE
/*
* fun getPreferredAudioRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
val itemValues = resources.getStringArray(R.array.format_importance_audio_values).toSet()
val prefAudio = sharedPreferences.getString("format_importance_audio", itemValues.joinToString(","))!!
prefAudio.split(",").forEachIndexed { idx, s ->
val importance = (itemValues.size - idx) * 10
when(s) {
"id" -> {
requirements.add {it: Format -> if (audioFormatIDPreference.contains(it.format_id)) importance else 0}
}
"language" -> {
sharedPreferences.getString("audio_language", "")?.apply {
if (this.isNotBlank()){
requirements.add { it: Format -> if (it.lang?.contains(this) == true) importance else 0 }
}
}
}
"codec" -> {
requirements.add {it: Format -> if ("^(${audioCodec}).+$".toRegex(RegexOption.IGNORE_CASE).matches(it.acodec)) importance else 0}
}
"container" -> {
requirements.add {it: Format -> if (it.container == audioContainer) importance else 0 }
}
}
}
return requirements
}
//requirement and importance
@SuppressLint("RestrictedApi")
fun getPreferredVideoRequirements(): MutableList<(Format) -> Int> {
val requirements: MutableList<(Format) -> Int> = mutableListOf()
val itemValues = resources.getStringArray(R.array.format_importance_video_values).toSet()
val prefVideo = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!!
prefVideo.split(",").forEachIndexed { idx, s ->
var importance = (itemValues.size - idx) * 10
when(s) {
"id" -> {
requirements.add { it: Format -> if (formatIDPreference.contains(it.format_id)) importance else 0 }
}
"resolution" -> {
context.getStringArray(R.array.video_formats_values)
.filter { it.contains("_") }
.map{ it.split("_")[0].dropLast(1)
}.toMutableList().apply {
when(videoQualityPreference) {
"worst" -> {
requirements.add { it: Format -> if (it.format_note.contains("worst", ignoreCase = true)) (importance) else 0 }
}
"best" -> {
requirements.add { it: Format -> if (it.format_note.contains("best", ignoreCase = true)) (importance) else 0 }
}
else -> {
val preferenceIndex = this.indexOfFirst { videoQualityPreference.contains(it) }
val preference = this[preferenceIndex]
for(i in 0..preferenceIndex){
removeAt(0)
}
add(0, preference)
forEachIndexed { index, res ->
requirements.add { it: Format -> if (it.format_note.contains(res, ignoreCase = true)) (importance - index - 1) else 0 }
}
}
}
}
}
"codec" -> {
requirements.add { it: Format -> if ("^(${videoCodec})(.+)?$".toRegex(RegexOption.IGNORE_CASE).matches(it.vcodec)) importance else 0 }
}
"no_audio" -> {
requirements.add { it: Format -> if (it.acodec == "none" || it.acodec == "") importance else 0 }
}
"container" -> {
requirements.add { it: Format ->
if (videoContainer == "mp4")
if (it.container.equals("mpeg_4", true)) importance else 0
else
if (it.container.equals(videoContainer, true)) importance else 0
}
}
}
}
return requirements
}
* */
}

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Handler
import android.os.Looper
import android.text.Html
import android.util.Log
@ -20,13 +21,16 @@ import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.getIntByAny
import com.deniscerri.ytdl.util.Extensions.getStringByAny
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.work.TerminalDownloadWorker
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
@ -42,9 +46,11 @@ import java.util.regex.Pattern
class InfoUtil(private val context: Context) {
private lateinit var sharedPreferences: SharedPreferences
private lateinit var handler: Handler
init {
try {
handler = Handler(Looper.getMainLooper())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
countryCODE = sharedPreferences.getString("locale", "")!!
if (countryCODE.isEmpty()) countryCODE = "US"
@ -131,6 +137,7 @@ class InfoUtil(private val context: Context) {
if (nextpage == "null") nextpage = ""
return PlaylistTuple(nextpage, items)
}catch (e: Exception){
Log.e("PLST", e.toString())
return PlaylistTuple(
"",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
@ -186,7 +193,7 @@ class InfoUtil(private val context: Context) {
}
return video
}
private fun createVideoFromPipedJSON(obj: JSONObject, url: String): ResultItem? {
private fun createVideoFromPipedJSON(obj: JSONObject, url: String, ignoreFormatPreference : Boolean = false): ResultItem? {
var video: ResultItem? = null
try {
val id = getIDFromYoutubeURL(url)
@ -201,7 +208,7 @@ class InfoUtil(private val context: Context) {
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val formats : ArrayList<Format> = ArrayList()
if(sharedPreferences.getString("formats_source", "yt-dlp") == "piped"){
if(sharedPreferences.getString("formats_source", "yt-dlp") == "piped" || ignoreFormatPreference){
if (obj.has("audioStreams")){
val formatsInJSON = obj.getJSONArray("audioStreams")
for (f in 0 until formatsInJSON.length()){
@ -440,11 +447,8 @@ class InfoUtil(private val context: Context) {
}
fun getFormats(url: String, source : String? = null) : List<Format> {
val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val m = p.matcher(url)
val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
if (m.find() && formatSource == "piped"){
if (url.isYoutubeURL() && formatSource == "piped"){
try {
val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
@ -455,11 +459,12 @@ class InfoUtil(private val context: Context) {
getFormatsFromYTDL(url)
}
}else {
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id")
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id", true)
return item!!.formats
}
}catch(e: Exception) {
println(e)
if (e is CancellationException) throw e
return getFormatsFromYTDL(url)
}
@ -502,32 +507,53 @@ class InfoUtil(private val context: Context) {
val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try {
val lineSeparator = System.getProperty("line.separator")
res.out.split(lineSeparator!!).toTypedArray()
res.out.split(System.lineSeparator()).toTypedArray()
} catch (e: Exception) {
arrayOf(res.out)
}
val json = results[0]
val jsonArray = JSONArray(json)
val jsonArray = kotlin.runCatching { JSONArray(json) }.getOrElse { JSONArray() }
return parseYTDLFormats(jsonArray)
}
fun getFormatsMultiple(urls: List<String>, source: String? = null, progress: (progress: List<Format>) -> Unit){
val urlsFile = File(context.cacheDir, "urls.txt")
urlsFile.delete()
urlsFile.createNewFile()
urls.forEach {
urlsFile.appendText(it+"\n")
}
data class MultipleFormatProgress(
val url: String,
val formats: List<Format>,
val unavailable : Boolean = false,
val unavailableMessage: String = ""
)
suspend fun getFormatsMultiple(urls: List<String>, source: String? = null, progress: (progress: MultipleFormatProgress) -> Unit) : MutableList<MutableList<Format>> {
val formatCollection = mutableListOf<MutableList<Format>>()
val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
val p = Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)")
val allYoutubeLinks = urls.any {p.matcher(it).find() }
if (formatSource == "yt-dlp" || !allYoutubeLinks){
val allYoutubeLinks = urls.all { it.isYoutubeURL() }
if (allYoutubeLinks && formatSource == "piped") {
urls.forEach { url ->
val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
createVideoFromPipedJSON(res, url).apply {
formatCollection.add(this!!.formats)
progress(
MultipleFormatProgress(url, this.formats)
)
}
}
}
else {
val urlsFile = File(context.cacheDir, "urls.txt")
urlsFile.delete()
withContext(Dispatchers.IO) {
urlsFile.createNewFile()
}
urls.forEach {
urlsFile.appendText(it+"\n")
}
try {
val request = YoutubeDLRequest(emptyList())
request.addOption("--print", "%(formats)s")
request.addOption("--print", "formats")
request.addOption("-a", urlsFile.absolutePath)
request.addOption("--skip-download")
request.addOption("-R", "1")
@ -537,7 +563,6 @@ class InfoUtil(private val context: Context) {
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
@ -557,35 +582,59 @@ class InfoUtil(private val context: Context) {
}
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
val txt = parseYTDLRequestString(request)
println(txt)
var urlIdx = 0
YoutubeDL.getInstance().execute(request){ progress, _, line ->
try{
if (line.isNotBlank()){
val listOfStrings = JSONArray(line)
progress(parseYTDLFormats(listOfStrings))
val url = urls[urlIdx]
println(line)
println(url)
if (line.contains("unavailable")) {
progress(MultipleFormatProgress(url, listOf(), true, line))
}else{
val formatsJSON = JSONArray(line)
val formats = parseYTDLFormats(formatsJSON)
formatCollection.add(formats)
progress(MultipleFormatProgress(url, formats))
}
}
}catch (e: Exception){
progress(emptyList())
Log.e("GET MULTIPLE FORMATS", e.toString())
}
urlIdx++
}
} catch (e: Exception) {
Looper.prepare().run {
e.message?.split(System.lineSeparator())?.apply {
this.forEach { line ->
println(line)
if (line.contains("unavailable")) {
kotlin.runCatching {
val id = Regex("""\[.*?\] (\w+):""").find(line)!!.groupValues[1]
val url = urls.first { it.contains(id) }
progress(MultipleFormatProgress(url, listOf(), true, line))
delay(500)
}
}
}
}
handler.post {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
e.printStackTrace()
}
}else{
urls.forEach {
val id = getIDFromYoutubeURL(it)
val res = genericRequest("$pipedURL/streams/$id")
createVideoFromPipedJSON(res, it)?.apply {
progress(this.formats)
}
}
urlsFile.delete()
}
urlsFile.delete()
return formatCollection
}
@SuppressLint("RestrictedApi")
@ -656,7 +705,7 @@ class InfoUtil(private val context: Context) {
for (result in results) {
if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result)
var title = jsonObject.getStringByAny("alt_title", "title", "webpage_url_basename")
val title = jsonObject.getStringByAny("alt_title", "title", "webpage_url_basename")
if (title == "[Private video]" || title == "[Deleted video]") continue
var author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
@ -789,7 +838,6 @@ class InfoUtil(private val context: Context) {
}
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
Log.e("NOTE", format.toString())
if (formatProper.format_note == null) continue
if (format.has("format_note")){
@ -956,16 +1004,13 @@ class InfoUtil(private val context: Context) {
@SuppressLint("RestrictedApi")
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest {
lateinit var request : YoutubeDLRequest
val matchFilter = mutableListOf<String>()
request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank()){
YoutubeDLRequest(downloadItem.url)
}else{
YoutubeDLRequest(downloadItem.playlistURL!!).apply {
if(downloadItem.playlistIndex == null){
val matchPortion = downloadItem.url.split("/").last().split("=").last().split("&").first()
matchFilter.add("id~='${matchPortion}'")
addOption("--match-filter", "id~='${matchPortion}'")
}else{
addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}")
}
@ -1131,6 +1176,7 @@ class InfoUtil(private val context: Context) {
when(type){
DownloadViewModel.Type.audio -> {
val supportedContainers = context.resources.getStringArray(R.array.audio_containers)
var abrSort = ""
var audioQualityId : String = downloadItem.format.format_id
if (audioQualityId.isBlank() || listOf("0", context.getString(R.string.best_quality), "ba", "best", "").contains(audioQualityId)){
@ -1138,7 +1184,8 @@ class InfoUtil(private val context: Context) {
}else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){
audioQualityId = "wa/w"
}else if(audioQualityId.contains("kbps_ytdlnisgeneric")){
matchFilter.add("abr<=${audioQualityId.split("kbps")[0]}")
abrSort = audioQualityId.split("kbps")[0]
audioQualityId = ""
}else{
audioQualityId += "/ba/b"
@ -1170,6 +1217,10 @@ class InfoUtil(private val context: Context) {
formatSorting.append(",acodec:$aCodecPref")
}
if (abrSort.isNotBlank()){
formatSorting.append(",abr~${abrSort}")
}
if(ext.isNotBlank()){
if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){
request.addOption("--audio-format", ext)
@ -1284,8 +1335,9 @@ class InfoUtil(private val context: Context) {
val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!!
if (downloadItem.videoPreferences.removeAudio) audioF = ""
var abrSort = ""
if(audioF.contains("kbps_ytdlnisgeneric")){
matchFilter.add("abr<=${audioF.split("kbps")[0]}")
abrSort = audioF.split("kbps")[0]
audioF = ""
}
@ -1403,6 +1455,7 @@ class InfoUtil(private val context: Context) {
if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref")
if (cont.isNotBlank()) append(",vext:$cont")
if (acont.isNotBlank()) append(",aext:$acont")
if (abrSort.isNotBlank()) append(",abr~${abrSort}")
if (this.isNotBlank()){
request.addOption("-S", "+hasaud$this")
}
@ -1466,10 +1519,6 @@ class InfoUtil(private val context: Context) {
else -> {}
}
if (matchFilter.isNotEmpty()) {
request.addOption("--match-filter", matchFilter.joinToString(" & "))
}
if (downloadItem.extraCommands.isNotBlank() && downloadItem.type != DownloadViewModel.Type.command){
val cache = File(FileUtil.getCachePath(context))
cache.mkdirs()
@ -1546,7 +1595,8 @@ class InfoUtil(private val context: Context) {
fun getGenericVideoFormats(resources: Resources) : MutableList<Format>{
val formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
val videoFormats = resources.getStringArray(R.array.video_formats_values)
val videoFormatsValues = resources.getStringArray(R.array.video_formats_values)
val videoFormats = resources.getStringArray(R.array.video_formats)
val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("video_format", "")
val audioCodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
@ -1565,7 +1615,7 @@ class InfoUtil(private val context: Context) {
videoCodecs[videoCodecsValues.indexOf(this)]
}
}
videoFormats.forEach { formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, it.split("_")[0])) }
videoFormatsValues.forEachIndexed { index, it -> formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, videoFormats[index])) }
formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.preferred_format_id),"", "",1, it)) }
return formats
}
@ -1574,7 +1624,7 @@ class InfoUtil(private val context: Context) {
class PlaylistTuple internal constructor(
var nextPageToken: String,
var videos: ArrayList<ResultItem>
var videos: ArrayList<ResultItem>,
)
companion object {

View file

@ -9,12 +9,9 @@ import android.view.MenuItem
import android.widget.PopupMenu
import androidx.core.view.forEach
import androidx.core.view.get
import androidx.core.view.isGone
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R
import com.google.android.material.navigation.NavigationBarView
import com.google.android.material.navigation.NavigationView
import java.util.*
object NavbarUtil {
@ -28,7 +25,7 @@ object NavbarUtil {
private var navItems = mapOf(
"Home" to R.id.homeFragment,
"History" to R.id.historyFragment,
"Downloads" to R.id.downloadQueueMainFragment,
"Queue" to R.id.downloadQueueMainFragment,
"Terminal" to R.id.terminalActivity,
"More" to R.id.moreFragment
)
@ -36,10 +33,11 @@ object NavbarUtil {
fun getStartFragmentId(context: Context) : Int {
val pref = settings.getString("start_destination", "")!!
val items = getDefaultNavBarItems(context)
val navBarHasPref = getNavBarPrefs().contains(items.indexOfFirst { it.itemId == navItems[pref] }.toString())
return if (pref == "") {
R.id.homeFragment
}else {
items.firstOrNull { it.itemId == navItems[pref] }!!.itemId
items.firstOrNull { it.itemId == navItems[pref] && navBarHasPref }?.itemId ?: R.id.homeFragment
}
}
@ -83,7 +81,7 @@ object NavbarUtil {
private fun getNavBarPrefs(): List<String> {
return settings
.getString("navigation_bar", "Home,History,More")!!
.getString("navigation_bar", "0,1,2,-3,-4")!!
.split(",")
}

View file

@ -22,6 +22,21 @@ class AlarmScheduler(private val context: Context) {
private val preferences = PreferenceManager.getDefaultSharedPreferences(context)
private val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
fun scheduleAt(at: Long) {
alarmManager?.setExactAndAllowWhileIdle(
AlarmManager.RTC_WAKEUP,
at,
PendingIntent.getBroadcast(
context,
1,
Intent(context, ScheduleAlarmReceiver::class.java),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
)
}
@SuppressLint("ScheduleExactAlarm")
fun schedule() {
cancel()

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="?android:colorAccent" android:pathData="M22,5.72l-4.6,-3.86 -1.29,1.53 4.6,3.86L22,5.72zM7.88,3.39L6.6,1.86 2,5.71l1.29,1.53 4.59,-3.85zM12.5,8L11,8v6l4.75,2.85 0.75,-1.23 -4,-2.37L12.5,8zM12,4c-4.97,0 -9,4.03 -9,9s4.02,9 9,9c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,20c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
</vector>

View file

@ -58,6 +58,7 @@
app:icon="@drawable/ic_music"
app:iconGravity="textStart"
app:iconPadding="0dp"
android:contentDescription="@string/preferred_download_type"
app:iconSize="11dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View file

@ -1,9 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
<androidx.core.widget.NestedScrollView android:layout_width="match_parent"
android:scrollbars="none"
android:orientation="vertical"
android:layout_height="match_parent">
android:layout_height="wrap_content"
android:fillViewport="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
@ -127,4 +132,4 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -1,19 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout style="@style/Widget.Material3.BottomSheet"
<LinearLayout style="@style/Widget.Material3.BottomSheet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:id="@+id/format_filter_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/all"
android:layout_width="wrap_content"
android:text="@string/format_filter"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="20dp"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/all"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
@ -73,4 +84,24 @@
</LinearLayout>
</FrameLayout>
<LinearLayout
android:id="@+id/format_source_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="20dp"
android:text="@string/format_source"
android:textSize="14sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>

View file

@ -75,7 +75,8 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/update_formats"
android:text="@string/update"
android:contentDescription="@string/update_formats"
android:layout_marginStart="10dp"
app:icon="@drawable/ic_refresh"
app:layout_constraintBottom_toBottomOf="parent"
@ -166,44 +167,6 @@
android:orientation="vertical" />
</LinearLayout>
<com.google.android.material.card.MaterialCardView
android:id="@+id/formatSourceLinear"
android:orientation="vertical"
android:layout_width="match_parent"
android:visibility="gone"
android:layout_margin="10dp"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/format_source_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="2"
android:layout_marginHorizontal="20dp"
android:layout_marginTop="10dp"
android:singleLine="false"
android:text="@string/format_source"
android:textSize="12sp" />
<RadioGroup
android:id="@+id/format_source_group"
android:layout_marginHorizontal="20dp"
android:checkedButton="@+id/radio_button_1"
android:layout_width="match_parent"
android:layout_marginBottom="10dp"
android:layout_height="wrap_content">
</RadioGroup>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -4,109 +4,13 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android">
<androidx.core.widget.NestedScrollView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/active_queued_linear"
android:orientation="vertical"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:id="@+id/active_linear"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<include
android:id="@+id/active_no_results"
layout="@layout/no_results" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/download_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pause_resume" />
</LinearLayout>
<LinearLayout
android:id="@+id/queue_linear"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content"
android:minHeight="200dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_marginTop="20dp"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/in_queue_label"
style="@style/Widget.Material3.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="5dp"
android:textColor="?android:colorPrimary"
android:text="@string/in_queue"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/drag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
app:drawableStartCompat="@drawable/ic_drag_handle"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:id="@+id/filesize"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="20dp"
android:paddingBottom="10dp"
android:text="@string/file_size"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/queued_recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
android:scrollbars="none"
android:paddingBottom="100dp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/pause_resume">
</androidx.recyclerview.widget.RecyclerView>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
android:layout_height="wrap_content"
android:scrollbars="none"
android:clipToPadding="false"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"/>
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/pause"

View file

@ -16,6 +16,7 @@
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/history_toolbar"
app:navigationIcon="@drawable/ic_back"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:title="@string/downloads"

View file

@ -69,6 +69,22 @@
android:paddingHorizontal="15dp"
app:drawableStartCompat="@drawable/ic_baseline_keyboard_arrow_right_24" />
<TextView
android:id="@+id/downloads"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="35dp"
android:textSize="16sp"
android:paddingVertical="15dp"
android:text="@string/downloads"
android:textStyle="bold"
android:clickable="true"
android:focusable="true"
android:background="?android:attr/selectableItemBackground"
android:paddingHorizontal="15dp"
app:drawableTint="?android:colorAccent"
app:drawableStartCompat="@drawable/ic_downloads" />
<TextView
android:id="@+id/download_queue"
android:layout_width="match_parent"

View file

@ -14,8 +14,11 @@
<TextView
android:layout_width="wrap_content"
android:text="@string/sort_by"
android:textSize="12sp"
android:layout_margin="20dp"
android:textSize="14sp"
android:textStyle="bold"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="10dp"
android:layout_marginTop="20dp"
android:layout_height="wrap_content"/>
<TextView

View file

@ -188,6 +188,7 @@
android:padding="0dp"
app:cornerRadius="10dp"
app:icon="@drawable/ic_video"
android:contentDescription="@string/preferred_download_type"
app:iconTint="?attr/colorAccent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"

View file

@ -17,6 +17,7 @@
android:layout_height="match_parent"
android:background="?attr/selectableItemBackgroundBorderless"
android:paddingHorizontal="10dp"
android:contentDescription="@string/home"
android:src="@drawable/ic_home_outlined"
tools:ignore="ContentDescription" />
@ -39,6 +40,7 @@
android:gravity="center_vertical"
android:paddingHorizontal="16dp"
android:singleLine="true"
android:textDirection="locale"
android:textColor="?android:attr/textColorPrimary"
android:textSize="18sp"
tools:text="Item" />

View file

@ -86,13 +86,15 @@
android:paddingHorizontal="10dp"
android:paddingBottom="10dp"
android:hint="@string/url"
app:endIconContentDescription="@string/import_from_clipboard"
app:endIconMode="custom"
app:endIconDrawable="@drawable/ic_clipboard"
style="@style/Widget.Material3.TextInputLayout.FilledBox">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="text"
android:inputType="textMultiLine"
android:maxLines="3"
android:layout_height="wrap_content"
/>

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/source_template"
android:clickable="true"
android:focusable="true"
android:background="?attr/selectableItemBackground"
android:paddingVertical="10dp"
android:paddingHorizontal="20dp"
android:drawablePadding="30dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="15sp"
android:text="TEST"
app:drawableStartCompat="@drawable/empty"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:ignore="HardcodedText" />

View file

@ -43,13 +43,13 @@
<string-array name="video_formats">
<item>@string/best_quality</item>
<item>2160p</item>
<item>1440p</item>
<item>1080p</item>
<item>720p</item>
<item>480p</item>
<item>360p</item>
<item>240p</item>
<item>~2160p</item>
<item>~1440p</item>
<item>~1080p</item>
<item>~720p</item>
<item>~480p</item>
<item>~360p</item>
<item>~240p</item>
<item>@string/worst_quality</item>
</string-array>
@ -66,13 +66,13 @@
</string-array>
<string-array name="audio_formats">
<item>best</item>
<item>192kbps</item>
<item>160kbps</item>
<item>128kbps</item>
<item>96kbps</item>
<item>64kbps</item>
<item>worst</item>
<item>@string/best_quality</item>
<item>~192kbps</item>
<item>~160kbps</item>
<item>~128kbps</item>
<item>~96kbps</item>
<item>~64kbps</item>
<item>@string/worst_quality</item>
</string-array>
<string-array name="audio_formats_values">

View file

@ -408,4 +408,6 @@
<string name="queue">Queue</string>
<string name="reset">Reset</string>
<string name="restore_info">Press \'Restore\' to merge the saved data with your current data.\nPress \'Reset\' to erase and only use the saved data in the file.</string>
<string name="use_alarm_manager">Use AlarmManager instead of WorkManager for Scheduling</string>
<string name="use_alarm_manager_summary">Enable this if WorkManager is restricted by your device vendor or scheduled jobs are not too precise</string>
</resources>

View file

@ -161,6 +161,15 @@
</PreferenceCategory>
<PreferenceCategory app:title="@string/scheduling">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_access_alarm_24"
android:key="use_alarm_for_scheduling"
android:summary="@string/use_alarm_manager_summary"
app:title="@string/use_alarm_manager" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"

View file

@ -8,7 +8,7 @@ buildscript {
appCompatVer = '1.6.1'
junitVer = '4.13.2'
androidJunitVer = '1.1.5'
espressoVer = '3.5.1'
espressoVer = '3.6.1'
jacksonVer = '2.9.8'
// supports java 1.6
commonsIoVer = '2.5'