1.7.9.3 pre

This commit is contained in:
zaednasr 2024-10-17 22:41:07 +02:00
parent 3307118d7f
commit 56b503a46b
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
49 changed files with 1016 additions and 553 deletions

View file

@ -1,11 +1,13 @@
# YTDLnis Changelog
> # 1.7.9.2
> # 1.7.9.2 (2024-09)
# What's Changed
- newpipe extractor general fixes
- fixed newpipe formats not working
- removed piped because its more annoying than anything, it doesnt work currently
*shortest changelog wr*
> # 1.7.9.1 (2024-09)

View file

@ -24,7 +24,7 @@ if (isBeta) {
android {
namespace 'com.deniscerri.ytdl'
compileSdk 34
compileSdk 35
try {
def propertiesFile = rootProject.file("keystore.properties")
@ -44,8 +44,8 @@ android {
}catch(IOException ignored){}
defaultConfig {
applicationId "com.deniscerri.ytdl"
minSdk 23
targetSdk 33
minSdk 24
targetSdk 35
versionCode versionMajor * 1000000 + versionMinor * 10000 + versionPatch * 100 + versionBuild
versionName "${versionMajor}.${versionMinor}.${versionPatch}${versionExt}"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@ -135,14 +135,15 @@ android {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs_nio:2.1.0"
implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs_nio:2.1.2"
// 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.1"
// implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.16.1"
// implementation "io.github.junkfood02.youtubedl-android:aria2c:0.16.1"
implementation "io.github.junkfood02.youtubedl-android:library:0.17.1"
implementation "io.github.junkfood02.youtubedl-android:ffmpeg:0.17.1"
implementation "io.github.junkfood02.youtubedl-android:aria2c:0.17.1"
implementation "androidx.appcompat:appcompat:$appCompatVer"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
@ -154,7 +155,7 @@ dependencies {
implementation "androidx.navigation:navigation-ui-ktx:$navVer"
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.compose.ui:ui-android:1.7.4'
implementation 'androidx.preference:preference-ktx:1.2.1'
testImplementation "junit:junit:$junitVer"
androidTestImplementation "junit:junit:$junitVer"
@ -176,10 +177,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.3.0'
implementation 'androidx.paging:paging-runtime-ktx:3.3.2'
implementation "androidx.room:room-paging:$roomVer"
androidTestImplementation "androidx.room:room-testing:$roomVer"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.6'
implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation 'com.google.truth:truth:1.1.5'
@ -191,7 +192,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.4"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.6"
implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer
@ -207,8 +208,8 @@ dependencies {
implementation "com.anggrayudi:storage:1.5.5"
implementation 'me.zhanghai.android.fastscroll:library:1.3.0'
implementation 'com.google.accompanist:accompanist-webview:0.30.1'
implementation 'androidx.compose.material3:material3-android:1.2.1'
implementation 'androidx.compose.material3:material3-android:1.3.0'
implementation "io.noties.markwon:core:4.6.2"
implementation("org.greenrobot:eventbus:3.3.1")
implementation("com.github.teamnewpipe:NewPipeExtractor:0.24.2")
implementation("com.github.teamnewpipe:newpipeextractor:v0.24.2")
}

View file

@ -16,6 +16,7 @@ import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.util.Patterns
import android.view.View
import android.view.WindowInsets
import android.widget.CheckBox
@ -51,6 +52,7 @@ import com.deniscerri.ytdl.ui.downloads.DownloadQueueMainFragment
import com.deniscerri.ytdl.ui.downloads.HistoryFragment
import com.deniscerri.ytdl.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.util.CrashListener
import com.deniscerri.ytdl.util.Extensions.extractURL
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle
@ -183,7 +185,7 @@ class MainActivity : BaseActivity() {
(navigationView as NavigationBarView).getOrCreateBadge(R.id.historyFragment)
}
lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest {
downloadViewModel.activePausedDownloadsCount.collectLatest {
if (it == 0) {
activeDownloadsBadge.isVisible = false
activeDownloadsBadge.clearNumber()
@ -438,7 +440,7 @@ class MainActivity : BaseActivity() {
textBuilder.append(c.toChar())
}
val bundle = Bundle()
bundle.putString("url", textBuilder.toString())
bundle.putString("url", textBuilder.toString().extractURL())
navController.popBackStack(R.id.homeFragment, true)
navController.navigate(
R.id.homeFragment,

View file

@ -26,6 +26,15 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status='Active'")
fun getActiveDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Active' or status = 'Paused' ORDER BY CASE WHEN status = 'Active' THEN 0 ELSE 1 END")
fun getActiveAndPausedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Paused'")
fun getPausedDownloads() : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Paused'")
fun getPausedDownloadsList() : List<DownloadItem>
@RewriteQueriesToDropUnusedColumns
@Query("SELECT * FROM downloads WHERE status = 'Processing'")
fun getProcessingDownloads() : Flow<List<DownloadItemConfigureMultiple>>
@ -79,10 +88,11 @@ interface DownloadDao {
@Query("SELECT * FROM downloads WHERE status in('Active','Queued', 'Scheduled')")
fun getActiveAndQueuedDownloadsList() : List<DownloadItem>
@Query("UPDATE downloads SET status='Queued' where status = 'Active'")
suspend fun resetActiveToQueued()
@Query("SELECT id FROM downloads WHERE status in('Active','Queued')")
@Query("UPDATE downloads SET status='Queued', downloadStartTime = -1 where status in ('Paused')")
suspend fun resetPausedToQueued()
@Query("SELECT id FROM downloads WHERE status in('Active','Queued', 'Paused')")
fun getActiveAndQueuedDownloadIDs() : List<Long>
@Query("SELECT * FROM downloads WHERE status in('Active','Queued')")
@ -95,7 +105,11 @@ interface DownloadDao {
@Query("SELECT format FROM downloads WHERE status='Queued'")
fun getSelectedFormatFromQueued() : List<Format>
@Query("SELECT * FROM downloads WHERE downloadStartTime <= :currentTime and status in ('Queued', 'Scheduled') ORDER BY downloadStartTime, id LIMIT 20")
@Query("""
SELECT * FROM downloads
WHERE downloadStartTime <= :currentTime and status in ('Queued', 'Scheduled')
ORDER BY downloadStartTime, id
""")
fun getQueuedScheduledDownloadsUntil(currentTime: Long) : Flow<List<DownloadItem>>
@Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id")
@ -187,7 +201,7 @@ interface DownloadDao {
@Query("DELETE FROM downloads WHERE id in (:list)")
suspend fun deleteAllWithIDs(list: List<Long>)
@Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','Active', 'Scheduled')")
@Query("UPDATE downloads SET status='Cancelled' WHERE status in('Queued','Active', 'Scheduled', 'Paused')")
suspend fun cancelActiveQueued()
@Query("DELETE FROM downloads WHERE status='Processing' AND id=:id")
@ -200,7 +214,11 @@ interface DownloadDao {
suspend fun updateAll(list: List<DownloadItem>) : List<DownloadItem> {
val toReturn = mutableListOf<DownloadItem>()
list.forEach {
it.id = update(it)
if (it.id > 0) {
update(it)
}else{
it.id = insert(it)
}
toReturn.add(it)
}

View file

@ -11,23 +11,23 @@ import kotlinx.coroutines.flow.Flow
@Dao
interface HistoryDao {
@Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:format||'%' AND website LIKE '%'||:site||'%' ORDER BY " +
@Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:type||'%' AND website LIKE '%'||:site||'%' ORDER BY " +
"CASE WHEN :sort = 'ASC' THEN id END ASC," +
"CASE WHEN :sort = 'DESC' THEN id END DESC," +
"CASE WHEN :sort = '' THEN id END DESC ")
fun getHistorySortedByID(query : String, format : String, site : String, sort : String) : List<HistoryItem>
fun getHistorySortedByID(query : String, type : String, site : String, sort : String) : List<HistoryItem>
@Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:format||'%' AND website LIKE '%'||:site||'%' ORDER BY " +
@Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:type||'%' AND website LIKE '%'||:site||'%' ORDER BY " +
"CASE WHEN :sort = 'ASC' THEN title END ASC," +
"CASE WHEN :sort = 'DESC' THEN title END DESC," +
"CASE WHEN :sort = '' THEN title END DESC ")
fun getHistorySortedByTitle(query : String, format : String, site : String, sort : String) : List<HistoryItem>
fun getHistorySortedByTitle(query : String, type : String, site : String, sort : String) : List<HistoryItem>
@Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:format||'%' AND website LIKE '%'||:site||'%' ORDER BY " +
@Query("SELECT * FROM history WHERE (title LIKE '%'||:query||'%' OR author LIKE '%'||:query||'%') AND type LIKE '%'||:type||'%' AND website LIKE '%'||:site||'%' ORDER BY " +
"CASE WHEN :sort = 'ASC' THEN author END ASC," +
"CASE WHEN :sort = 'DESC' THEN author END DESC," +
"CASE WHEN :sort = '' THEN author END DESC ")
fun getHistorySortedByAuthor(query : String, format : String, site : String, sort : String) : List<HistoryItem>
fun getHistorySortedByAuthor(query : String, type : String, site : String, sort : String) : List<HistoryItem>
@Query("SELECT * FROM history")

View file

@ -44,6 +44,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
pagingSourceFactory = {downloadDao.getAllDownloads()}
)
val activeDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveDownloads().distinctUntilChanged()
val activePausedDownloads : Flow<List<DownloadItem>> = downloadDao.getActiveAndPausedDownloads().distinctUntilChanged()
val pausedDownloads : Flow<List<DownloadItem>> = downloadDao.getPausedDownloads().distinctUntilChanged()
val processingDownloads : Flow<List<DownloadItemConfigureMultiple>> = downloadDao.getProcessingDownloads().distinctUntilChanged()
val queuedDownloads : Pager<Int, DownloadItemSimple> = Pager(
config = PagingConfig(pageSize = 20, initialLoadSize = 20, prefetchDistance = 1),
@ -67,15 +69,16 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
)
val activeDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active).toListString())
val activePausedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.Paused).toListString())
val queuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Queued).toListString())
val activeQueuedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Active, Status.Queued).toListString())
val pausedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Paused).toListString())
val cancelledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Cancelled).toListString())
val erroredDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Error).toListString())
val savedDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Saved).toListString())
val scheduledDownloadsCount : Flow<Int> = downloadDao.getDownloadsCountByStatusFlow(listOf(Status.Scheduled).toListString())
enum class Status {
Active, Queued, Error, Cancelled, Saved, Processing, Scheduled, Duplicate
Active, Paused, Queued, Error, Cancelled, Saved, Processing, Scheduled, Duplicate
}
suspend fun insert(item: DownloadItem) : Long {
@ -222,48 +225,47 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
}
@SuppressLint("RestrictedApi")
suspend fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, inputData: Data.Builder = Data.Builder()) : Result<String> {
fun startDownloadWorker(queuedItems: List<DownloadItem>, context: Context, continueAfterPriorityItems: Boolean = true) : Result<String> {
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workManager = WorkManager.getInstance(context)
val currentWork = workManager.getWorkInfosByTag("download").await()
if (currentWork.size == 0 || currentWork.none{ it.state == WorkInfo.State.RUNNING } || (queuedItems.isNotEmpty() && queuedItems[0].downloadStartTime != 0L)){
val currentTime = System.currentTimeMillis()
var delay = 0L
if (queuedItems.isNotEmpty()){
val earliestStart = queuedItems.minBy { it.downloadStartTime }
delay = if (earliestStart.downloadStartTime != 0L){
earliestStart.downloadStartTime.minus(currentTime)
} else 0
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 Result.success("")
}
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
.setInputData(inputData.build())
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
val inputData = Data.Builder()
val currentTime = System.currentTimeMillis()
var delay = 0L
if (queuedItems.isNotEmpty()){
val earliestStart = queuedItems.minBy { it.downloadStartTime }
delay = if (earliestStart.downloadStartTime != 0L){
earliestStart.downloadStartTime.minus(currentTime)
} else 0
if (delay <= 60000L) delay = 0L
inputData.putLongArray("priority_item_ids", queuedItems.map { it.id }.toLongArray())
}
val useAlarmForScheduling = sharedPreferences.getBoolean("use_alarm_for_scheduling", false)
if (delay > 0L && useAlarmForScheduling) {
AlarmScheduler(context).scheduleAt(queuedItems.minBy { it.downloadStartTime }.downloadStartTime)
return Result.success("")
}
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
inputData.putBoolean("continue_after_priority_ids", continueAfterPriorityItems)
val workRequest = OneTimeWorkRequestBuilder<DownloadWorker>()
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(delay, TimeUnit.MILLISECONDS)
.setInputData(inputData.build())
workManager.enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
val message = StringBuilder()
val isCurrentNetworkMetered = (context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager).isActiveNetworkMetered

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdl.database.repository
import com.deniscerri.ytdl.database.DBManager.SORTING
import com.deniscerri.ytdl.database.dao.HistoryDao
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.util.FileUtil
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collectLatest
@ -26,41 +27,28 @@ class HistoryRepository(private val historyDao: HistoryDao) {
return historyDao.getAllHistoryByURL(url)
}
fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) : List<HistoryItem> {
fun getFiltered(query : String, type : String, site : String, sortType: HistorySortType, sort: SORTING, statusFilter: HistoryViewModel.HistoryStatus) : List<HistoryItem> {
var filtered = when(sortType){
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString())
HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, format, site, sort.toString())
HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthor(query, format, site, sort.toString())
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, type, site, sort.toString())
HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, type, site, sort.toString())
HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthor(query, type, site, sort.toString())
HistorySortType.FILESIZE -> {
val items = historyDao.getHistorySortedByID(query, format, site, sort.toString())
val items = historyDao.getHistorySortedByID(query, type, site, sort.toString())
when(sort){
SORTING.DESC -> items.sortedByDescending { it.format.filesize }
SORTING.ASC -> items.sortedBy { it.format.filesize }
}
}
}
if(notDeleted){
filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } }
}
return filtered
}
fun getRecordsBetweenTwoItems(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) : List<HistoryItem> {
var filtered = when(sortType){
HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString())
HistorySortType.TITLE -> historyDao.getHistorySortedByTitle(query, format, site, sort.toString())
HistorySortType.AUTHOR -> historyDao.getHistorySortedByAuthor(query, format, site, sort.toString())
HistorySortType.FILESIZE -> {
val items = historyDao.getHistorySortedByID(query, format, site, sort.toString())
when(sort){
SORTING.DESC -> items.sortedByDescending { it.format.filesize }
SORTING.ASC -> items.sortedBy { it.format.filesize }
}
when(statusFilter) {
HistoryViewModel.HistoryStatus.DELETED -> {
filtered = filtered.filter { it.downloadPath.any { it2 -> !FileUtil.exists(it2) } }
}
}
if(notDeleted){
filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } }
HistoryViewModel.HistoryStatus.NOT_DELETED -> {
filtered = filtered.filter { it.downloadPath.any { it2 -> FileUtil.exists(it2) } }
}
else -> {}
}
return filtered

View file

@ -14,12 +14,14 @@ import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeWatchVideosURL
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.extractors.GoogleApiUtil
import com.deniscerri.ytdl.util.extractors.PipedApiUtil
import com.deniscerri.ytdl.util.extractors.newpipe.NewPipeUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.util.extractors.YoutubeApiUtil
import com.yausername.youtubedl_android.YoutubeDLException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
@ -46,6 +48,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
enum class SourceType {
YOUTUBE_VIDEO,
YOUTUBE_WATCHVIDEOS,
YOUTUBE_PLAYLIST,
YOUTUBE_CHANNEL,
SEARCH_QUERY,
@ -111,6 +114,40 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return items
}
private suspend fun getYoutubeWatchVideos(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem> {
if (resetResults) deleteAll()
//throw YoutubeDLException("Youtube Watch Videos is not yet supported in data fetching. You can download it directly by clicking Continue Anyway or by Quick Downloading it!")
//TODO use below code after youtubedl-android has fixed issue #295 in their repo
val items = mutableListOf<ResultItem>()
val ytExtractorResult = newPipeUtil?.getPlaylistData(inputQuery) {
if (addToResults){
runBlocking {
val ids = resultDao.insertMultiple(it)
ids.forEachIndexed { index, id ->
it[index].id = id
}
}
}
items.addAll(it)
}
val response = if (ytExtractorResult?.isSuccess == true){
ytExtractorResult.getOrElse { items }
}else{
val res = ytdlpUtil.getFromYTDL(inputQuery)
if (addToResults) {
val ids = resultDao.insertMultiple(res)
ids.forEachIndexed { index, id ->
res[index].id = id
}
}
res
}
itemCount.value = response.size
return response
}
private suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
val theURL = inputQuery.replace("\\?list.*".toRegex(), "")
val ytExtractorRes = newPipeUtil?.getVideoData(theURL)
@ -157,9 +194,11 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
ytExtractorResult.getOrElse { items }
}else{
val res = ytdlpUtil.getFromYTDL(playlistURL)
val ids = resultDao.insertMultiple(res)
ids.forEachIndexed { index, id ->
res[index].id = id
if (addToResults) {
val ids = resultDao.insertMultiple(res)
ids.forEachIndexed { index, id ->
res[index].id = id
}
}
res
}
@ -313,6 +352,9 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
SourceType.YOUTUBE_VIDEO -> {
getYoutubeVideo(inputQuery, resetResults, addToResults)
}
SourceType.YOUTUBE_WATCHVIDEOS -> {
getYoutubeWatchVideos(inputQuery, resetResults, addToResults)
}
SourceType.YOUTUBE_PLAYLIST -> {
if (singleItem){
getFromYTDLP(inputQuery, resetResults, addToResults, true)
@ -345,6 +387,8 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
type = SourceType.YOUTUBE_PLAYLIST
}else if (inputQuery.isYoutubeChannelURL()) {
type = SourceType.YOUTUBE_CHANNEL
}else if (inputQuery.isYoutubeWatchVideosURL()) {
type = SourceType.YOUTUBE_WATCHVIDEOS
}
} else if (Patterns.WEB_URL.matcher(inputQuery).matches()) {
type = SourceType.YT_DLP

View file

@ -9,9 +9,13 @@ import android.os.Parcelable
import android.util.DisplayMetrics
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.filter
import androidx.paging.map
import androidx.preference.PreferenceManager
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
@ -38,16 +42,23 @@ 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.FormatUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker
import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDL
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.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ -62,12 +73,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao
private val formatUtil = FormatUtil(application)
private val notificationUtil = NotificationUtil(application)
private val ytdlpUtil = YTDLPUtil(application)
private val resources : Resources
val allDownloads : Flow<PagingData<DownloadItem>>
val queuedDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloads : Flow<List<DownloadItem>>
val activePausedDownloads : Flow<List<DownloadItem>>
val processingDownloads : Flow<List<DownloadItemConfigureMultiple>>
val cancelledDownloads : Flow<PagingData<DownloadItemSimple>>
val erroredDownloads : Flow<PagingData<DownloadItemSimple>>
@ -75,13 +88,21 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val scheduledDownloads : Flow<PagingData<DownloadItemSimple>>
val activeDownloadsCount : Flow<Int>
val activePausedDownloadsCount : Flow<Int>
val queuedDownloadsCount : Flow<Int>
val activeQueuedDownloadsCount : Flow<Int>
val pausedDownloadsCount : Flow<Int>
val cancelledDownloadsCount : Flow<Int>
val erroredDownloadsCount : Flow<Int>
val savedDownloadsCount : Flow<Int>
val scheduledDownloadsCount : Flow<Int>
val pausedAllDownloads = MediatorLiveData(PausedAllDownloadsState.HIDDEN)
private val pausedAllDownloadsFlow : Flow<PausedAllDownloadsState>
private var isPausingResuming = false
enum class PausedAllDownloadsState {
PAUSE, RESUME, PROCESSING, HIDDEN
}
@Parcelize
data class AlreadyExistsIDs(
var downloadItemID: Long,
@ -122,8 +143,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
activeDownloadsCount = repository.activeDownloadsCount
activePausedDownloadsCount = repository.activePausedDownloadsCount
queuedDownloadsCount = repository.queuedDownloadsCount
activeQueuedDownloadsCount = repository.activeQueuedDownloadsCount
pausedDownloadsCount = repository.pausedDownloadsCount
cancelledDownloadsCount = repository.cancelledDownloadsCount
erroredDownloadsCount = repository.erroredDownloadsCount
savedDownloadsCount = repository.savedDownloadsCount
@ -132,6 +154,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
allDownloads = repository.allDownloads.flow
queuedDownloads = repository.queuedDownloads.flow
activeDownloads = repository.activeDownloads
activePausedDownloads = repository.activePausedDownloads
processingDownloads = repository.processingDownloads
savedDownloads = repository.savedDownloads.flow
scheduledDownloads = repository.scheduledDownloads.flow
@ -144,6 +167,25 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
}
pausedAllDownloadsFlow = combine(activeDownloadsCount, queuedDownloadsCount, pausedDownloadsCount) { active, queued, paused ->
if (isPausingResuming) {
return@combine PausedAllDownloadsState.PROCESSING
}
if (active == 0 && queued == 0 && paused == 0) {
return@combine PausedAllDownloadsState.HIDDEN
}else if (paused > 1 || (active == 0 && queued > 0) || (paused > 0 && active > 0)) {
return@combine PausedAllDownloadsState.RESUME
}else if (active > 1) {
return@combine PausedAllDownloadsState.PAUSE
}else{
return@combine PausedAllDownloadsState.HIDDEN
}
}
pausedAllDownloads.addSource(pausedAllDownloadsFlow.asLiveData()) {
pausedAllDownloads.value = it
}
val confTmp = Configuration(application.resources.configuration)
val locale = if (Build.VERSION.SDK_INT < 33) {
@ -496,13 +538,20 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
)
}
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)
val preferredTemplateContent =
sharedPreferences.getString("preferred_command_template", "")!!.ifEmpty {
sharedPreferences.getString("lastCommandTemplateUsed", "")!!
}
val c = if (preferredTemplateContent.isNotBlank()) {
commandTemplateDao.getTemplateByContent(preferredTemplateContent)
}else {
commandTemplateDao.getFirst()
}
return generateCommandFormat(c)
return generateCommandFormat(
c ?: CommandTemplate(0, "", preferredTemplateContent, useAsExtraCommand = false, useAsExtraCommandAudio = false, useAsExtraCommandVideo = false)
)
}
}
}
@ -656,7 +705,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.deleteAllWithIDs(ids)
}
fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
private fun cancelActiveQueued() = viewModelScope.launch(Dispatchers.IO) {
processingItemsJob?.apply { cancel(CancellationException()) }
repository.cancelActiveQueued()
}
@ -705,14 +754,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
repository.startDownloadWorker(emptyList(), application)
}
suspend fun resetActiveToQueued() {
dbManager.downloadDao.resetActiveToQueued()
}
suspend fun startDownloadWorker(list: List<DownloadItem>){
repository.startDownloadWorker(list, application)
}
suspend fun putAtTopOfQueue(ids: List<Long>) = CoroutineScope(Dispatchers.IO).launch{
val downloads = dao.getQueuedDownloadsListIDs()
val lastID = ids.maxOf { it }
@ -809,11 +850,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
items.forEach {
if (it.downloadStartTime > 0) {
it.status = DownloadRepository.Status.Scheduled.toString()
}else{
}else {
it.status = DownloadRepository.Status.Queued.toString()
}
//CHECK DUPLICATES
var isDuplicate = false
if (checkDuplicate.isNotEmpty() && !ignoreDuplicates){
@ -939,11 +979,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
}else{
val queued = repository.updateAll(queuedItems)
if (sharedPreferences.getBoolean("paused_downloads", true)) {
sharedPreferences.edit().putBoolean("paused_downloads", false).apply()
resetActiveToQueued()
}
println(queued.size)
result.message = repository.startDownloadWorker(queued, context).getOrElse { "" }
@ -1173,4 +1209,69 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
}
fun pauseAllDownloads() = viewModelScope.launch {
pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING
isPausingResuming = true
WorkManager.getInstance(application).cancelAllWorkByTag("download")
val activeDownloadsList = withContext(Dispatchers.IO){
getActiveDownloads()
}
activeDownloadsList.forEach {
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
}
delay(1000)
isPausingResuming = false
activeDownloadsList.forEach {
repository.setDownloadStatus(it.id, DownloadRepository.Status.Paused)
}
pausedAllDownloads.value = PausedAllDownloadsState.RESUME
}
fun resumeAllDownloads() = viewModelScope.launch {
pausedAllDownloads.value = PausedAllDownloadsState.PROCESSING
isPausingResuming = true
WorkManager.getInstance(application).cancelAllWorkByTag("download")
val paused = withContext(Dispatchers.IO) {
dao.getPausedDownloadsList()
}
withContext(Dispatchers.IO){
dbManager.downloadDao.resetPausedToQueued()
repository.startDownloadWorker(paused, application)
}
delay(1000)
isPausingResuming = false
withContext(Dispatchers.Main) {
pausedAllDownloads.value = PausedAllDownloadsState.PAUSE
}
}
fun cancelAllDownloads() = viewModelScope.launch {
WorkManager.getInstance(application).cancelAllWorkByTag("download")
val activeAndQueued = withContext(Dispatchers.IO){
repository.getActiveAndQueuedDownloadIDs()
}
activeAndQueued.forEach { id ->
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id.toInt())
}
cancelActiveQueued()
}
fun resumeDownload(itemID: Long) = viewModelScope.launch {
kotlin.runCatching {
val item = withContext(Dispatchers.IO){
repository.getItemByID(itemID)
}
item.status = DownloadRepository.Status.Queued.toString()
withContext(Dispatchers.IO){
updateDownload(item)
}
putAtTopOfQueue(listOf(itemID))
withContext(Dispatchers.IO){
repository.startDownloadWorker(listOf(item), application, false)
}
}
}
}

View file

@ -10,9 +10,17 @@ import androidx.lifecycle.viewModelScope
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.DBManager.SORTING
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType
import com.deniscerri.ytdl.util.Extensions
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
class HistoryViewModel(application: Application) : AndroidViewModel(application) {
@ -20,36 +28,40 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
val sortOrder = MutableLiveData(SORTING.DESC)
val sortType = MutableLiveData(HistorySortType.DATE)
val websiteFilter = MutableLiveData("")
val statusFilter = MutableLiveData(HistoryStatus.UNSET)
private val queryFilter = MutableLiveData("")
private val formatFilter = MutableLiveData("")
private val notDeletedFilter = MutableLiveData(false)
private val typeFilter = MutableLiveData("")
enum class HistoryStatus {
UNSET, DELETED, NOT_DELETED, ALL
}
val allItems : LiveData<List<HistoryItem>>
private var _items = MediatorLiveData<List<HistoryItem>>()
init {
val dao = DBManager.getInstance(application).historyDao
repository = HistoryRepository(dao)
allItems = repository.items.asLiveData()
_items.addSource(allItems){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
}
_items.addSource(formatFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
_items.addSource(typeFilter){
filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
}
_items.addSource(sortType){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
}
_items.addSource(websiteFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
}
_items.addSource(queryFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
}
_items.addSource(notDeletedFilter){
filter(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
_items.addSource(statusFilter){
filter(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
}
}
@ -77,22 +89,26 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
queryFilter.value = filter
}
fun setFormatFilter(filter: String){
formatFilter.value = filter
fun setTypeFilter(filter: String){
typeFilter.value = filter
}
fun setNotDeleted(filter: Boolean){
notDeletedFilter.value = filter
fun setStatusFilter(status: HistoryStatus) {
statusFilter.value = status
}
private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, notDeleted: Boolean) = viewModelScope.launch(Dispatchers.IO){
_items.postValue(repository.getFiltered(query, format, site, sortType, sort, notDeleted))
private fun filter(query : String, format : String, site : String, sortType: HistorySortType, sort: SORTING, statusFilter: HistoryStatus) = viewModelScope.launch(Dispatchers.IO){
_items.postValue(repository.getFiltered(query, format, site, sortType, sort, statusFilter))
}
fun getAll() : List<HistoryItem> {
return repository.getAll()
}
fun getByID(id: Long) : HistoryItem {
return repository.getItem(id)
}
fun insert(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO){
repository.insert(item)
}
@ -118,13 +134,14 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application)
}
fun getRecordsBetweenTwoItems(item1: Long, item2: Long) : List<HistoryItem> {
val filtered = repository.getFiltered(queryFilter.value!!, formatFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, notDeletedFilter.value!!)
val filtered = repository.getFiltered(queryFilter.value!!, typeFilter.value!!, websiteFilter.value!!, sortType.value!!, sortOrder.value!!, statusFilter.value!!)
val firstIndex = filtered.indexOfFirst { it.id == item1 }
val secondIndex = filtered.indexOfFirst { it.id == item2 }
if(firstIndex > secondIndex) {
return filtered.filterIndexed { index, _ -> index in (secondIndex + 1) until firstIndex }
return if(firstIndex > secondIndex) {
filtered.filterIndexed { index, _ -> index in (secondIndex + 1) until firstIndex }
}else{
return filtered.filterIndexed { index, _ -> index in (firstIndex + 1) until secondIndex }
filtered.filterIndexed { index, _ -> index in (firstIndex + 1) until secondIndex }
}
}

View file

@ -33,6 +33,7 @@ import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
import java.util.concurrent.CancellationException
@ -96,6 +97,14 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
playlistFilter.value = p
}
private fun resetPlaylistFilter() = viewModelScope.launch(Dispatchers.Main) {
_items.removeSource(playlistFilter)
playlistFilter.value = ""
_items.addSource(playlistFilter) {
filter()
}
}
fun getFilteredList() : LiveData<List<ResultItem>> {
return _items
}
@ -133,7 +142,6 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
repository.itemCount.value = inputQueries.size
}
val resetResults = inputQueries.size == 1
uiState.update {it.copy(processing = true, errorMessage = null)}
val res = mutableListOf<ResultItem?>()
@ -195,6 +203,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
suspend fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
repository.deleteAll()
resetPlaylistFilter()
}
fun update(item: ResultItem) = viewModelScope.launch(Dispatchers.IO){

View file

@ -39,6 +39,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.util.Extensions.extractURL
import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -170,16 +171,12 @@ class ShareActivity : BaseActivity() {
runCatching { supportFragmentManager.popBackStack() }
quickDownload = intent.getBooleanExtra("quick_download", sharedPreferences.getBoolean("quick_download", false) || sharedPreferences.getString("preferred_download_type", "video") == "command")
val url = when(action){
val data = when(action){
Intent.ACTION_SEND -> intent.getStringExtra(Intent.EXTRA_TEXT)!!
else -> intent.dataString!!
}
val matcher = Patterns.WEB_URL.matcher(url)
val inputQuery = if (matcher.find()){
matcher.group()
}else{
url
}
val inputQuery = data.extractURL()
val type = intent.getStringExtra("TYPE")
val background = intent.getBooleanExtra("BACKGROUND", false)

View file

@ -981,12 +981,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchView!!.editText.setSelection(searchView!!.editText.length())
}
private fun updateMultiplePlaylistResults(playlistTitles: List<String>) {
playlistNameFilterChipGroup.children.filter { it.tag != "all" }.forEach {
playlistNameFilterChipGroup.removeView(it)
}
if (playlistTitles.isEmpty() || playlistTitles.size == 1) {
playlistNameFilterScrollView.isVisible = false
playlistNameFilterChipGroup.children.filter { it.tag != "all" }.forEach {
playlistNameFilterChipGroup.removeView(it)
}
return
}
@ -995,9 +997,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}
for (t in playlistTitles) {
val exists = playlistNameFilterChipGroup.children.any { it.tag == t }
if (exists) continue
val tmp = layoutinflater!!.inflate(R.layout.filter_chip, playlistNameFilterChipGroup, false) as Chip
tmp.text = t
tmp.tag = t
@ -1008,6 +1007,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
playlistNameFilterChipGroup.addView(tmp)
}
if (playlistNameFilterChipGroup.children.all { !(it as Chip).isChecked }) {
(playlistNameFilterChipGroup.children.first() as Chip).isChecked = true
}
playlistNameFilterScrollView.isVisible = true
}
}

View file

@ -71,8 +71,6 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
// PROGRESS BAR ----------------------------------------------------
val progressBar = card.findViewById<LinearProgressIndicator>(R.id.progress)
progressBar.tag = "${item.id}##progress"
progressBar.progress = 0
progressBar.isIndeterminate = true
// TITLE ----------------------------------
val itemTitle = card.findViewById<TextView>(R.id.title)
@ -122,39 +120,39 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
if (cancelButton.hasOnClickListeners()) cancelButton.setOnClickListener(null)
cancelButton.setOnClickListener {onItemClickListener.onCancelClick(item.id)}
// val resumeButton = card.findViewById<MaterialButton>(R.id.active_download_resume)
// resumeButton.isEnabled = true
// if (resumeButton.hasOnClickListeners()) resumeButton.setOnClickListener(null)
// if (activePaused) {
// resumeButton.setIconResource(R.drawable.exomedia_ic_play_arrow_white)
// resumeButton.setOnClickListener {
// resumeButton.isEnabled = false
// onItemClickListener.onResumeClick(item.id)
// }
// }else {
// resumeButton.setIconResource(R.drawable.exomedia_ic_pause_white)
// resumeButton.setOnClickListener {
// resumeButton.isEnabled = false
// onItemClickListener.onPauseClick(item.id)
// }
// }
val resumeButton = card.findViewById<MaterialButton>(R.id.active_download_resume)
resumeButton.isEnabled = true
if (resumeButton.hasOnClickListeners()) resumeButton.setOnClickListener(null)
val isPaused = item.status == DownloadRepository.Status.Paused.toString()
if (isPaused) {
resumeButton.setIconResource(R.drawable.exomedia_ic_play_arrow_white)
resumeButton.setOnClickListener {
resumeButton.isEnabled = false
onItemClickListener.onResumeClick(item.id)
}
}else {
resumeButton.setIconResource(R.drawable.exomedia_ic_pause_white)
resumeButton.setOnClickListener {
resumeButton.isEnabled = false
onItemClickListener.onPauseClick(item.id)
}
}
if (sharedPreferences.getBoolean("paused_downloads", false)) {
if (isPaused) {
progressBar.isIndeterminate = false
progressBar.progress = 0
cancelButton.isEnabled = true
output.text = activity.getString(R.string.exo_download_paused)
}else{
progressBar.isIndeterminate = true
progressBar.isIndeterminate = progressBar.progress <= 0
cancelButton.isEnabled = true
}
}
interface OnItemClickListener {
fun onCancelClick(itemID: Long)
fun onOutputClick(item: DownloadItem)
}
enum class ActiveDownloadAction {
Resume, Pause
fun onPauseClick(itemID: Long)
fun onResumeClick(itemID: Long)
}
companion object {

View file

@ -129,35 +129,35 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
else fileSize.text = fileSizeReadable
val menu = card.findViewById<View>(R.id.options)
//val paused = sharedPreferences.getBoolean("paused_downloads", false) || item.status == DownloadRepository.Status.ActivePaused.toString()
val paused = item.status == DownloadRepository.Status.Paused.toString()
menu.setOnClickListener {
val popup = PopupMenu(activity, it)
popup.menuInflater.inflate(R.menu.active_downloads_minified, popup.menu)
if (Build.VERSION.SDK_INT > 27) popup.menu.setGroupDividerEnabled(true)
// val pause = popup.menu[0]
// val resume = popup.menu[1]
//
// if (paused){
// pause.isVisible = false
// resume.isVisible = true
// }else{
// pause.isVisible = true
// resume.isVisible = false
// }
val pause = popup.menu[0]
val resume = popup.menu[1]
if (paused){
pause.isVisible = false
resume.isVisible = true
}else{
pause.isVisible = true
resume.isVisible = false
}
popup.setOnMenuItemClickListener { m ->
when(m.itemId){
// R.id.pause -> {
// onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Pause, position)
// if (progressBar.progress == 0) progressBar.isIndeterminate = false
// popup.dismiss()
// }
// R.id.resume -> {
// onItemClickListener.onPauseClick(item.id, ActiveDownloadAdapter.ActiveDownloadAction.Resume, position)
// progressBar.isIndeterminate = true
// popup.dismiss()
// }
R.id.pause -> {
onItemClickListener.onPauseClick(item.id, position)
if (progressBar.progress == 0) progressBar.isIndeterminate = false
popup.dismiss()
}
R.id.resume -> {
onItemClickListener.onResumeClick(item.id, position)
progressBar.isIndeterminate = true
popup.dismiss()
}
R.id.cancel -> {
onItemClickListener.onCancelClick(item.id)
popup.dismiss()
@ -170,7 +170,7 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
}
progressBar.isIndeterminate = !sharedPreferences.getBoolean("paused_downloads", false)
progressBar.isIndeterminate = !paused
card.setOnClickListener {
onItemClickListener.onCardClick()
@ -178,6 +178,8 @@ class ActiveDownloadMinifiedAdapter(onItemClickListener: OnItemClickListener, ac
}
interface OnItemClickListener {
fun onCancelClick(itemID: Long)
fun onPauseClick(itemID: Long, position: Int)
fun onResumeClick(itemID: Long, position: Int)
fun onCardClick()
}

View file

@ -2,11 +2,16 @@ package com.deniscerri.ytdl.ui.adapter
import android.annotation.SuppressLint
import android.app.Activity
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.graphics.Color
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.core.view.isVisible
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
@ -22,10 +27,12 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
private val onItemClickListener: OnItemClickListener
private val activity: Activity
private val checkedItems: ArrayList<Long> = ArrayList()
private val sharedPreferences: SharedPreferences
init {
this.onItemClickListener = onItemClickListener
this.activity = activity
this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity)
}
class ViewHolder(itemView: View, onItemClickListener: OnItemClickListener?) : RecyclerView.ViewHolder(itemView) {
@ -43,18 +50,18 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = getItem(position)
val item = getItem(position)!!
val card = holder.item
card.popup()
val title = card.findViewById<TextView>(R.id.title)
title.text = item?.title
title.text = item.title
val content = card.findViewById<TextView>(R.id.content)
content.text = item?.content
content.text = item.content
card.findViewById<TextView>(R.id.useInExtraCommands).apply {
isVisible = item!!.useAsExtraCommand
isVisible = item.useAsExtraCommand
val extraAudio = if (item.useAsExtraCommandAudio) context.getString(R.string.audio) else null
val extraVideo = if (item.useAsExtraCommandVideo) context.getString(R.string.video) else null
val finalText = context.getString(R.string.extra_command) + " " + listOfNotNull(
@ -64,7 +71,12 @@ class TemplatesAdapter(onItemClickListener: OnItemClickListener, activity: Activ
text = finalText
}
if (checkedItems.contains(item!!.id)) {
card.findViewById<TextView>(R.id.preferredTemplate).apply {
val preferred = sharedPreferences.getString("preferred_command_template", "")
isVisible = preferred == item.content
}
if (checkedItems.contains(item.id)) {
card.isChecked = true
card.strokeWidth = 5
} else {

View file

@ -20,8 +20,10 @@ import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem
@ -240,16 +242,21 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
true
}
downloadThumb.isVisible = item.thumb.isNotBlank()
downloadThumb.setOnClickListener {
downloadManager.enqueue(
DownloadManager.Request(item.thumb.toUri())
.setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_WIFI or
DownloadManager.Request.NETWORK_MOBILE
)
.setAllowedOverRoaming(true)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/" + item.title + ".jpg"))
runCatching {
downloadManager.enqueue(
DownloadManager.Request(item.thumb.toUri())
.setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_WIFI or
DownloadManager.Request.NETWORK_MOBILE
)
.setAllowedOverRoaming(true)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "YTDLnis/" + item.title + ".jpg"))
}.onFailure {
Toast.makeText(requireContext(), getString(R.string.failed_download), Toast.LENGTH_SHORT).show()
}
}
title.text = item.title
@ -479,33 +486,24 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
val activeCount = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloadsCount()
}
if (activeCount == 0){
val queuedCount = withContext(Dispatchers.IO){
downloadViewModel.getQueuedDownloadsCount()
}
if (queuedCount == 0) {
sharedPreferences.edit().putBoolean("paused_downloads", false).apply()
}
}
if (activeCount == 1){
val queue = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
if (!sharedPreferences.getBoolean("paused_downloads", false)){
runBlocking {
downloadViewModel.queueDownloads(queue)
}
}
}
}
}
override fun onPauseClick(itemID: Long, position: Int) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.updateToStatus(itemID, DownloadRepository.Status.Paused)
}
activeAdapter.notifyItemChanged(position)
}
}
override fun onResumeClick(itemID: Long, position: Int) {
downloadViewModel.resumeDownload(itemID)
}
override fun onCardClick() {
this.dismiss()
findNavController().navigate(

View file

@ -5,9 +5,11 @@ import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.content.SharedPreferences
import android.content.res.ColorStateList
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
@ -23,6 +25,8 @@ import androidx.annotation.OptIn
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.bundleOf
import androidx.core.view.isVisible
import androidx.core.view.marginBottom
@ -36,6 +40,7 @@ import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.room.util.getColumnIndexOrThrow
import androidx.work.WorkManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
@ -86,8 +91,8 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
private lateinit var activeDownloads : ActiveDownloadAdapter
private lateinit var notificationUtil: NotificationUtil
private lateinit var pause: ExtendedFloatingActionButton
private lateinit var resume: ExtendedFloatingActionButton
private lateinit var pauseBtn: ExtendedFloatingActionButton
private lateinit var resumeBtn: ExtendedFloatingActionButton
private lateinit var noResults: RelativeLayout
private lateinit var workManager: WorkManager
private lateinit var preferences: SharedPreferences
@ -119,80 +124,64 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
activeRecyclerView.adapter = activeDownloads
activeRecyclerView.layoutManager = GridLayoutManager(context, resources.getInteger(R.integer.grid_size))
pause = view.findViewById(R.id.pause)
pause.isVisible = false
resume = view.findViewById(R.id.resume)
pauseBtn = view.findViewById(R.id.pause)
pauseBtn.isVisible = false
resumeBtn = view.findViewById(R.id.resume)
noResults = view.findViewById(R.id.no_results)
pause.setOnClickListener {
downloadViewModel.pausedAllDownloads.observe(viewLifecycleOwner) { value ->
lifecycleScope.launch {
pause.isEnabled = false
delay(1000)
workManager.cancelAllWorkByTag("download")
val activeDownloadsList = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloads()
}
activeDownloadsList.forEach {
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
}
preferences.edit().putBoolean("paused_downloads", true).apply()
pause.isVisible = false
resume.isEnabled = false
resume.isVisible = true
activeDownloads.notifyDataSetChanged()
resume.isEnabled = true
}
}
resume.setOnClickListener {
lifecycleScope.launch {
resume.isEnabled = false
delay(1000)
preferences.edit().putBoolean("paused_downloads", false).apply()
resume.isVisible = false
pause.isEnabled = false
pause.isVisible = true
withContext(Dispatchers.IO) {
downloadViewModel.resetActiveToQueued()
downloadViewModel.startDownloadWorker(listOf())
withContext(Dispatchers.Main){
when(value) {
DownloadViewModel.PausedAllDownloadsState.PAUSE -> {
resumeBtn.isVisible = false
pauseBtn.isClickable = true
val typedValue = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorPrimaryContainer, typedValue, true)
pauseBtn.backgroundTintList = ColorStateList.valueOf(typedValue.data)
pauseBtn.isVisible = true
activeDownloads.notifyDataSetChanged()
pause.isEnabled = true
}
DownloadViewModel.PausedAllDownloadsState.RESUME -> {
pauseBtn.isVisible = false
resumeBtn.isClickable = true
val typedValue = TypedValue()
requireContext().theme.resolveAttribute(R.attr.colorPrimaryContainer, typedValue, true)
resumeBtn.backgroundTintList = ColorStateList.valueOf(typedValue.data)
resumeBtn.isVisible = true
activeDownloads.notifyDataSetChanged()
}
DownloadViewModel.PausedAllDownloadsState.PROCESSING -> {
pauseBtn.isClickable = false
pauseBtn.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.grey))
resumeBtn.isClickable = false
resumeBtn.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.grey))
}
else -> {
pauseBtn.isVisible = false
resumeBtn.isVisible = false
}
}
}
}
lifecycleScope.launch {
val activeCount = withContext(Dispatchers.IO) {
downloadViewModel.getActiveDownloadsCount()
pauseBtn.setOnClickListener {
lifecycleScope.launch {
downloadViewModel.pauseAllDownloads()
}
val queuedCount = withContext(Dispatchers.IO) {
downloadViewModel.getQueuedDownloadsCount()
}
val pausedDownloads = preferences.getBoolean("paused_downloads", false)
pause.isVisible = (queuedCount > 0 || activeCount > 0) && !pausedDownloads
resume.isVisible = (queuedCount > 0 || activeCount > 0) && pausedDownloads
}
lifecycleScope.launch {
downloadViewModel.activeQueuedDownloadsCount.collectLatest {
if (it == 0) {
pause.isVisible = false
resume.isVisible = false
}
resumeBtn.setOnClickListener {
lifecycleScope.launch {
downloadViewModel.resumeAllDownloads()
}
}
lifecycleScope.launch {
downloadViewModel.activeDownloads.collectLatest {
downloadViewModel.activePausedDownloads.collectLatest {
noResults.isVisible = it.isEmpty()
activeDownloads.submitList(it)
activeRecyclerView.scrollTo(0,0)
}
}
@ -235,32 +224,6 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
withContext(Dispatchers.IO){
downloadViewModel.updateDownload(item)
}
val activeCount = withContext(Dispatchers.IO){
downloadViewModel.getActiveDownloadsCount()
}
if (activeCount == 0){
val queuedCount = withContext(Dispatchers.IO){
downloadViewModel.getQueuedDownloadsCount()
}
if (queuedCount == 0) {
preferences.edit().putBoolean("paused_downloads", false).apply()
}
}
if (activeCount == 1){
val queue = withContext(Dispatchers.IO){
downloadViewModel.getQueued()
}
if (!preferences.getBoolean("paused_downloads", false)){
runBlocking {
downloadViewModel.queueDownloads(queue)
}
}
}
}
}
@ -275,4 +238,18 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
}
}
override fun onPauseClick(itemID: Long) {
lifecycleScope.launch {
YoutubeDL.getInstance().destroyProcessById(itemID.toString())
notificationUtil.cancelDownloadNotification(itemID.toInt())
withContext(Dispatchers.IO){
downloadViewModel.updateToStatus(itemID, DownloadRepository.Status.Paused)
}
}
}
override fun onResumeClick(itemID: Long) {
downloadViewModel.resumeDownload(itemID)
}
}

View file

@ -209,7 +209,7 @@ class DownloadQueueMainFragment : Fragment(){
when(m.itemId){
R.id.clear_queue -> {
showDeleteDialog {
cancelAllDownloads()
downloadViewModel.cancelAllDownloads()
}
}
R.id.clear_cancelled -> {
@ -270,24 +270,6 @@ class DownloadQueueMainFragment : Fragment(){
deleteDialog.show()
}
@SuppressLint("NotifyDataSetChanged")
private fun cancelAllDownloads() {
sharedPreferences.edit().putBoolean("paused_downloads", false).apply()
fragmentAdapter.notifyDataSetChanged()
workManager.cancelAllWorkByTag("download")
lifecycleScope.launch {
val notificationUtil = NotificationUtil(requireContext())
val activeAndQueued = withContext(Dispatchers.IO){
downloadViewModel.getActiveAndQueuedDownloadIDs()
}
activeAndQueued.forEach { id ->
YoutubeDL.getInstance().destroyProcessById(id.toString())
notificationUtil.cancelDownloadNotification(id.toInt())
}
downloadViewModel.cancelActiveQueued()
}
}
fun scrollToActive(){
tabLayout.getTabAt(0)!!.select()
viewPager2.setCurrentItem(0, true)

View file

@ -29,7 +29,9 @@ import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.core.view.children
import androidx.core.view.forEach
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -47,7 +49,9 @@ import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.ui.adapter.HistoryAdapter
import com.deniscerri.ytdl.ui.downloadcard.FormatSelectionBottomSheetDialog.FormatCategory
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.UiUtil
@ -85,9 +89,9 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
private var uiHandler: Handler? = null
private var noResults: RelativeLayout? = null
private var selectionChips: LinearLayout? = null
private var websiteGroup: ChipGroup? = null
private var historyList: List<HistoryItem?>? = null
private var allhistoryList: List<HistoryItem?>? = null
private var websiteList: MutableList<String> = mutableListOf()
private lateinit var selectedObjects: ArrayList<HistoryItem>
private var actionMode : ActionMode? = null
@ -112,7 +116,6 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
topAppBar = view.findViewById(R.id.history_toolbar)
noResults = view.findViewById(R.id.no_results)
selectionChips = view.findViewById(R.id.history_selection_chips)
websiteGroup = view.findViewById(R.id.website_chip_group)
uiHandler = Handler(Looper.getMainLooper())
sortChip = view.findViewById(R.id.sortChip)
selectedObjects = arrayListOf()
@ -127,7 +130,6 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyList = mutableListOf()
allhistoryList = mutableListOf()
historyAdapter =
HistoryAdapter(
@ -149,14 +151,14 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
historyViewModel.allItems.observe(viewLifecycleOwner) {
allhistoryList = it
if(it.isEmpty()){
noResults!!.visibility = VISIBLE
selectionChips!!.visibility = GONE
websiteGroup!!.removeAllViews()
topAppBar!!.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = false
}else{
noResults!!.visibility = GONE
selectionChips!!.visibility = VISIBLE
topAppBar!!.menu.children.firstOrNull { m -> m.itemId == R.id.filters }?.isVisible = true
updateWebsiteChips(it)
}
}
@ -306,6 +308,106 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
deleteDialog.show()
}
}
R.id.filters -> {
val filterSheet = BottomSheetDialog(requireContext())
filterSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
filterSheet.setContentView(R.layout.history_other_filters_sheet)
//format status
val notDeleted = filterSheet.findViewById<TextView>(R.id.not_deleted)!!
val deleted = filterSheet.findViewById<TextView>(R.id.deleted)!!
when(historyViewModel.statusFilter.value) {
HistoryViewModel.HistoryStatus.ALL -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
}
HistoryViewModel.HistoryStatus.DELETED -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
}
HistoryViewModel.HistoryStatus.NOT_DELETED -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
}
else -> {}
}
notDeleted.setOnClickListener {
val status = historyViewModel.statusFilter.value
when (status) {
HistoryViewModel.HistoryStatus.ALL -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.DELETED)
}
HistoryViewModel.HistoryStatus.NOT_DELETED -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.UNSET)
}
HistoryViewModel.HistoryStatus.DELETED -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.ALL)
}
else -> {
notDeleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.NOT_DELETED)
}
}
}
deleted.setOnClickListener {
val status = historyViewModel.statusFilter.value
when (status) {
HistoryViewModel.HistoryStatus.ALL -> {
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.NOT_DELETED)
}
HistoryViewModel.HistoryStatus.NOT_DELETED -> {
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.ALL)
}
HistoryViewModel.HistoryStatus.DELETED -> {
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.empty,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.UNSET)
}
else -> {
deleted.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_check,0,0,0)
historyViewModel.setStatusFilter(HistoryViewModel.HistoryStatus.DELETED)
}
}
}
if (websiteList.size < 2) {
filterSheet.findViewById<View>(R.id.websiteFilters)?.isVisible = false
}else{
val websiteGroup = filterSheet.findViewById<ChipGroup>(R.id.websitesChipGroup)
val websiteFilter = historyViewModel.websiteFilter.value
for (i in websiteList.indices) {
val w = websiteList[i]
val tmp = layoutinflater!!.inflate(R.layout.filter_chip, websiteGroup, false) as Chip
tmp.text = w
tmp.id = i
tmp.setOnClickListener {
Log.e(TAG, tmp.isChecked.toString())
if (tmp.isChecked) {
historyViewModel.setWebsiteFilter(tmp.text as String)
tmp.isChecked = true
} else {
historyViewModel.setWebsiteFilter("")
tmp.isChecked = false
}
}
if (w == websiteFilter){
tmp.isChecked = true
}
websiteGroup!!.addView(tmp)
}
}
val displayMetrics = DisplayMetrics()
requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics)
filterSheet.behavior.peekHeight = displayMetrics.heightPixels
filterSheet.show()
}
}
true
}
@ -374,80 +476,43 @@ class HistoryFragment : Fragment(), HistoryAdapter.OnItemClickListener{
val audio = fragmentView!!.findViewById<Chip>(R.id.audio_chip)
audio.setOnClickListener {
if (audio.isChecked) {
historyViewModel.setFormatFilter("audio")
historyViewModel.setTypeFilter(DownloadViewModel.Type.audio.toString())
audio.isChecked = true
} else {
historyViewModel.setFormatFilter("")
historyViewModel.setTypeFilter("")
audio.isChecked = false
}
}
val video = fragmentView!!.findViewById<Chip>(R.id.video_chip)
video.setOnClickListener {
if (video.isChecked) {
historyViewModel.setFormatFilter("video")
historyViewModel.setTypeFilter(DownloadViewModel.Type.video.toString())
video.isChecked = true
} else {
historyViewModel.setFormatFilter("")
historyViewModel.setTypeFilter("")
video.isChecked = false
}
}
val command = fragmentView!!.findViewById<Chip>(R.id.command_chip)
command.setOnClickListener {
if (command.isChecked) {
historyViewModel.setFormatFilter("command")
historyViewModel.setTypeFilter(DownloadViewModel.Type.command.toString())
command.isChecked = true
} else {
historyViewModel.setFormatFilter("")
historyViewModel.setTypeFilter("")
command.isChecked = false
}
}
val notDeleted = fragmentView!!.findViewById<Chip>(R.id.not_deleted_chip)
notDeleted.setOnClickListener {
if (notDeleted.isChecked) {
historyViewModel.setNotDeleted(true)
notDeleted.isChecked = true
} else {
historyViewModel.setNotDeleted(false)
notDeleted.isChecked = false
}
}
}
private fun updateWebsiteChips(list : List<HistoryItem>) {
val websites = mutableListOf<String>()
val websiteFilter = historyViewModel.websiteFilter.value
websiteList = mutableListOf()
for (item in list){
if (!websites.contains(item.website)) websites.add(item.website)
if (item.website == "null" || item.website.isEmpty()) continue
if (!websiteList.contains(item.website)) websiteList.add(item.website)
}
websiteGroup!!.removeAllViews()
if (websites.size <= 1) {
requireView().findViewById<View>(R.id.website_divider).visibility = GONE
return
}
//val websites = historyRecyclerViewAdapter!!.websites
for (i in websites.indices) {
val w = websites[i]
if (w == "null" || w.isEmpty()) continue
val tmp = layoutinflater!!.inflate(R.layout.filter_chip, websiteGroup, false) as Chip
tmp.text = w
tmp.id = i
tmp.setOnClickListener {
Log.e(TAG, tmp.isChecked.toString())
if (tmp.isChecked) {
historyViewModel.setWebsiteFilter(tmp.text as String)
tmp.isChecked = true
} else {
historyViewModel.setWebsiteFilter("")
tmp.isChecked = false
}
}
if (w == websiteFilter){
tmp.isChecked = true
}
websiteGroup!!.addView(tmp)
}
requireView().findViewById<View>(R.id.website_divider).visibility = VISIBLE
}
override fun onCardClick(itemID: Long, isPresent: Boolean) {

View file

@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
import android.graphics.Canvas
import android.graphics.Color
import android.os.Bundle
@ -23,6 +24,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -43,6 +45,7 @@ import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
@ -64,6 +67,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
private lateinit var mainActivity: MainActivity
private lateinit var sortChip: Chip
private lateinit var selectedObjects: ArrayList<CommandTemplate>
private lateinit var preferences: SharedPreferences
private var actionMode : ActionMode? = null
private val jsonFormat = Json { prettyPrint = true }
@ -95,7 +99,7 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
recyclerView = view.findViewById(R.id.template_recyclerview)
recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.adapter = templatesAdapter
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getStringSet("swipe_gesture", requireContext().getStringArray(R.array.swipe_gestures_values).toSet())!!.toList().contains("templates")){
val itemTouchHelper = ItemTouchHelper(simpleCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
@ -117,8 +121,8 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
commandTemplateViewModel.sortOrder.observe(viewLifecycleOwner){
if (it != null){
when(it){
SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
SORTING.ASC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_up)
SORTING.DESC -> sortChip.chipIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_down)
}
}
}
@ -264,9 +268,10 @@ class CommandTemplatesFragment : Fragment(), TemplatesAdapter.OnItemClickListene
}
}
@SuppressLint("NotifyDataSetChanged")
override fun onItemClick(commandTemplate: CommandTemplate, index: Int) {
UiUtil.showCommandTemplateCreationOrUpdatingSheet(commandTemplate,mainActivity, this, commandTemplateViewModel, newTemplate = {
templatesAdapter.notifyItemChanged(index)
templatesAdapter.notifyDataSetChanged()
}, {})
}

View file

@ -145,14 +145,13 @@ class MoreFragment : Fragment() {
}
activeDownloadsList.forEach {
it.status = DownloadRepository.Status.Queued.toString()
YoutubeDL.getInstance().destroyProcessById(it.id.toString())
notificationUtil.cancelDownloadNotification(it.id.toInt())
it.status = DownloadRepository.Status.Paused.toString()
withContext(Dispatchers.IO) {
downloadViewModel.updateDownload(it)
}
}
mainSharedPreferencesEditor.putBoolean("paused_downloads", true).apply()
}
if (doNotShowAgain){

View file

@ -148,6 +148,14 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
backup = findPreference("backup")
restore = findPreference("restore")
findPreference<Preference>("package_name")?.apply {
summary = BuildConfig.APPLICATION_ID
setOnPreferenceClickListener {
UiUtil.copyToClipboard(summary.toString(), requireActivity())
true
}
}
backup!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
val builder = MaterialAlertDialogBuilder(requireContext())

View file

@ -20,6 +20,7 @@ import android.net.Uri
import android.text.Html
import android.text.Spanned
import android.util.DisplayMetrics
import android.util.Patterns
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
@ -45,16 +46,22 @@ 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
import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.tabs.TabLayout
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.neo.highlight.core.Highlight
import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme
import com.squareup.picasso.Picasso
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import me.zhanghai.android.fastscroll.FastScrollerBuilder
import org.json.JSONObject
import java.io.File
@ -212,7 +219,13 @@ object Extensions {
tags.forEach {
runCatching {
val tmp = this.getString(it)
if (tmp != "null") return tmp
if (tmp != "null") {
return if (tmp.startsWith("[")) {
Json.decodeFromString<List<String>>(tmp).joinToString(", ")
}else{
tmp
}
}
}
}
@ -508,4 +521,16 @@ object Extensions {
return Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?(be.com))/@[a-zA-Z]+").matcher(this).find()
}
fun String.isYoutubeWatchVideosURL() : Boolean {
return Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?(be.com))/watch_videos\\?video_ids=.*").matcher(this).find()
}
fun String.extractURL() : String {
val res = Pattern.compile("(http|ftp|https)://([\\w_-]+(?:\\.[\\w_-]+)+)([\\w.,@?^=%&:/~+#-]*[\\w@?^=%&/~+#-])").matcher(this)
return if (res.find()){
res.group()
}else{
this
}
}
}

View file

@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.Format
import java.util.Collections
class FormatUtil(private var context: Context) {
private val sharedPreferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
@ -19,8 +20,13 @@ class FormatUtil(private var context: Context) {
private val videoCodecPreference : String = sharedPreferences.getString("video_codec", "").toString()
private val audioContainerPreference : String = sharedPreferences.getString("audio_format", "").toString()
private val videoContainerPreference : String = sharedPreferences.getString("video_format", "").toString()
@SuppressLint("RestrictedApi")
private val videoResolutionOrder = context.getStringArray(R.array.video_formats_values).filter { it.contains("_") }.map{ it.split("_")[0].dropLast(1) }.reversed()
private val videoResolutionOrder = context.getStringArray(R.array.video_formats_values)
.filter { it.contains("_") }
.map{ it.split("_")[0].dropLast(1) }.toMutableList().apply {
this.reverse()
}
private val preferSmallerFormats = sharedPreferences.getBoolean("prefer_smaller_formats", false)
@SuppressLint("RestrictedApi")

View file

@ -182,9 +182,12 @@ object UiUtil {
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.create_command_template)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val ok : Button = bottomSheet.findViewById(R.id.template_create)!!
val title : TextInputLayout = bottomSheet.findViewById(R.id.title)!!
val content : TextInputLayout = bottomSheet.findViewById(R.id.content)!!
val preferredCommandSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.preferredCommandTemplateSwitch)!!
val extraCommandsSwitch : MaterialSwitch = bottomSheet.findViewById(R.id.extraCommandsSwitch)!!
val extraCommandsAudio : CheckBox = bottomSheet.findViewById(R.id.checkbox_audio)!!
val extraCommandsVideo : CheckBox = bottomSheet.findViewById(R.id.checkbox_video)!!
@ -248,6 +251,8 @@ object UiUtil {
}
if (item != null){
preferredCommandSwitch.isChecked = item.content == sharedPreferences.getString("preferred_command_template", "")
extraCommandsSwitch.isChecked = item.useAsExtraCommand
if (item.useAsExtraCommand){
extraCommandsAudio.isVisible = true
@ -294,6 +299,13 @@ object UiUtil {
}
ok.setOnClickListener {
if (preferredCommandSwitch.isChecked) {
sharedPreferences.edit().putString("preferred_command_template", content.editText!!.text.toString()).apply()
}else if (sharedPreferences.getString("preferred_command_template", "") == item?.content){
sharedPreferences.edit().putString("preferred_command_template", "").apply()
}
if (item == null){
val t = CommandTemplate(0, title.editText!!.text.toString(), content.editText!!.text.toString(), extraCommandsSwitch.isChecked, extraCommandsAudio.isChecked, extraCommandsVideo.isChecked)
commandTemplateViewModel.insert(t)
@ -304,7 +316,6 @@ object UiUtil {
item.useAsExtraCommand = extraCommandsSwitch.isChecked
item.useAsExtraCommandAudio = extraCommandsAudio.isChecked
item.useAsExtraCommandVideo = extraCommandsVideo.isChecked
Log.e("aa", item.toString())
commandTemplateViewModel.update(item)
newTemplate(item)
}
@ -1037,7 +1048,7 @@ object UiUtil {
}
suspend fun showCommandTemplates(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, itemSelected: (itemSelected: List<CommandTemplate>) -> Unit) {
suspend fun showCommandTemplates(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, onlyOne: Boolean = false, itemSelected: (itemSelected: List<CommandTemplate>) -> Unit) {
val bottomSheet = BottomSheetDialog(activity)
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
bottomSheet.setContentView(R.layout.command_template_list)
@ -1050,6 +1061,7 @@ object UiUtil {
linearLayout!!.removeAllViews()
val selectedItems = mutableListOf<CommandTemplate>()
val ok = bottomSheet.findViewById<MaterialButton>(R.id.command_ok)
ok?.isVisible = !onlyOne
ok?.isEnabled = list.size == 1
list.forEach {template ->
@ -1065,6 +1077,11 @@ object UiUtil {
(it as MaterialCardView).isChecked = true
}
if (onlyOne) {
itemSelected(listOf(template))
bottomSheet.cancel()
}
ok?.isEnabled = selectedItems.isNotEmpty()
}
linearLayout.addView(item)

View file

@ -21,6 +21,7 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
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.isYoutubeWatchVideosURL
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatUtil
@ -51,7 +52,14 @@ class YTDLPUtil(private val context: Context) {
val request : YoutubeDLRequest
if (query.contains("http")){
request = YoutubeDLRequest(query)
if (query.isYoutubeWatchVideosURL()) {
request = YoutubeDLRequest(emptyList())
val config = File(context.cacheDir.absolutePath + "/config" + System.currentTimeMillis() + "##url.txt")
config.writeText(query)
request.addOption("--config", config.absolutePath)
}else{
request = YoutubeDLRequest(query)
}
}else{
request = YoutubeDLRequest(emptyList())
when (searchEngine){
@ -64,13 +72,8 @@ class YTDLPUtil(private val context: Context) {
}
}
}
val lang = sharedPreferences.getString("app_language", "en")
if (searchEngine == "ytsearch" || query.isYoutubeURL()) {
var extractorArgs = "player_client=default,mediaconnect"
if (context.getStringArray(R.array.subtitle_langs).contains(lang)) {
extractorArgs += ";lang=$lang"
}
request.addOption("--extractor-args", "youtube:$extractorArgs")
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
}
request.addOption("--flat-playlist")
@ -103,7 +106,7 @@ class YTDLPUtil(private val context: Context) {
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
println(parseYTDLRequestString(request))
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val results: List<String?> = try {
val lineSeparator = System.getProperty("line.separator")
@ -119,7 +122,7 @@ class YTDLPUtil(private val context: Context) {
val title = jsonObject.getStringByAny("alt_title", "title", "webpage_url_basename")
if (title == "[Private video]" || title == "[Deleted video]") continue
var author = jsonObject.getStringByAny("artist", "uploader", "channel", "playlist_uploader", "uploader_id")
var author = jsonObject.getStringByAny("artists", "artist", "uploader", "channel", "playlist_uploader", "uploader_id")
var duration = jsonObject.getIntByAny("duration").toString()
if (duration != "-1"){
duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
@ -689,12 +692,7 @@ class YTDLPUtil(private val context: Context) {
}
if (downloadItem.url.isYoutubeURL()) {
var extractorArgs = "player_client=default,mediaconnect"
val lang = sharedPreferences.getString("app_language", "en")
if (context.getStringArray(R.array.subtitle_langs).contains(lang)) {
extractorArgs += ";lang=$lang"
}
request.addOption("--extractor-args", "youtube:$extractorArgs")
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
}
}
@ -1098,4 +1096,18 @@ class YTDLPUtil(private val context: Context) {
}
return request
}
private fun getYoutubeExtractorArgs() : String {
var extractorArgs = "player_client=default,mediaconnect"
val lang = sharedPreferences.getString("app_language", "en")
if (context.getStringArray(R.array.subtitle_langs).contains(lang)) {
extractorArgs += ";lang=$lang"
}
val poToken = sharedPreferences.getString("youtube_po_token", "")!!
if (poToken.isNotBlank()) {
extractorArgs += ";po_token=web+$poToken"
}
return extractorArgs
}
}

View file

@ -151,6 +151,8 @@ class NewPipeUtil(context: Context) {
fun getChannelData(url: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> {
try {
//TODO BROKEN FOR NOW
return Result.failure(Throwable())
val req = ChannelInfo.getInfo(ServiceList.YouTube, url)
println(Gson().toJson(req))
val items = mutableListOf<ResultItem>()
@ -326,7 +328,7 @@ class NewPipeUtil(context: Context) {
if (stream.audioStreams.isNotEmpty()){
for (f in 0 until stream.audioStreams.size){
val it = stream.audioStreams[f]
if (it.bitrate == 0) continue
if (it.bitrate == 0 || listOf(599, 600).contains(it.itag)) continue
val formatObj = Format(
format_id = it.itag.toString(),

View file

@ -18,7 +18,6 @@ import androidx.work.ForegroundInfo
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.await
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.MainActivity
@ -38,6 +37,7 @@ import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ -53,6 +53,13 @@ class DownloadWorker(
@SuppressLint("RestrictedApi")
override suspend fun doWork(): Result {
if (isStopped) return Result.success()
val workManager = WorkManager.getInstance(context)
val currentWork = withContext(Dispatchers.IO) {
workManager.getWorkInfosByTag("download").get()
}
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) {
return Result.success()
}
val notificationUtil = NotificationUtil(App.instance)
val ytdlpUtil = YTDLPUtil(context)
@ -66,10 +73,11 @@ class DownloadWorker(
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val time = System.currentTimeMillis() + 6000
val queuedItems = dao.getQueuedScheduledDownloadsUntil(time)
val currentWork = WorkManager.getInstance(context).getWorkInfosByTag("download").await()
if (currentWork.count{it.state == WorkInfo.State.RUNNING} > 1) return Result.success()
val priorityItemIDs = inputData.getLongArray("priority_item_ids")!!.toMutableList()
val continueAfterPriorityIds = inputData.getBoolean("continue_after_priority_ids", true)
// this is needed for observe sources call, so it wont create result items
// [removed]
//val createResultItem = inputData.getBoolean("createResultItem", true)
val confTmp = Configuration(context.resources.configuration)
@ -100,8 +108,9 @@ class DownloadWorker(
val foregroundInfo = ForegroundInfo(1000000000, workNotif)
setForegroundAsync(foregroundInfo)
queuedItems.collect { items ->
if (isStopped) return@collect
queuedItems.collectLatest { items ->
if (this@DownloadWorker.isStopped) return@collectLatest
runningYTDLInstances.clear()
val activeDownloads = dao.getActiveDownloadsList()
activeDownloads.forEach {
@ -112,17 +121,28 @@ class DownloadWorker(
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (items.isEmpty() && running.isEmpty()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collect
return@collectLatest
}
if (useScheduler){
if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collect
return@collectLatest
}
}
if (priorityItemIDs.isEmpty() && !continueAfterPriorityIds) {
WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id)
return@collectLatest
}
val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running }
val eligibleDownloads = if (priorityItemIDs.isNotEmpty()) {
val tmp = priorityItemIDs.take(concurrentDownloads)
items.filter { it.id !in running && tmp.contains(it.id) }
}else{
items.take(concurrentDownloads).filter { it.id !in running }
}
eligibleDownloads.forEach{downloadItem ->
val notification = notificationUtil.createDownloadServiceNotification(openDownloadQueue, downloadItem.title.ifEmpty { downloadItem.url })
@ -365,7 +385,10 @@ class DownloadWorker(
}
if (eligibleDownloads.isNotEmpty()){
eligibleDownloads.forEach { it.status = DownloadRepository.Status.Active.toString() }
eligibleDownloads.forEach {
it.status = DownloadRepository.Status.Active.toString()
priorityItemIDs.remove(it.id)
}
dao.updateMultiple(eligibleDownloads)
}
}

View file

@ -180,7 +180,7 @@ class ObserveSourceWorker(
}
if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){
downloadRepo.startDownloadWorker(queuedItems, context, Data.Builder().putBoolean("createResultItem", false))
downloadRepo.startDownloadWorker(queuedItems, context)
if(!useScheduler){
queuedItems.filter { it.downloadStartTime != 0L || (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach {

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="?attr/colorAccent" android:pathData="M10.09,15.59L11.5,17l5,-5 -5,-5 -1.41,1.41L12.67,11H3v2h9.67l-2.58,2.59zM19,3H5c-1.11,0 -2,0.9 -2,2v4h2V5h14v14H5v-4H3v4c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
</vector>

View file

@ -128,13 +128,14 @@
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="wrap_content"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:hint="@string/search_hint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
android:layout_marginStart="60dp"
app:layout_constraintStart_toEndOf="@+id/home_toolbar"
app:layout_constraintTop_toTopOf="parent"
app:menu="@menu/main_menu">

View file

@ -166,21 +166,21 @@
app:layout_constraintVertical_bias="0.51"
>
<!-- <com.google.android.material.button.MaterialButton-->
<!-- android:id="@+id/active_download_resume"-->
<!-- style="?attr/materialIconButtonFilledStyle"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:contentDescription="@string/resume"-->
<!-- android:layout_height="wrap_content"-->
<!-- app:backgroundTint="?attr/colorSurface"-->
<!-- app:cornerRadius="15dp"-->
<!-- app:icon="@drawable/exomedia_ic_play_arrow_white"-->
<!-- app:iconSize="30dp"-->
<!-- app:iconTint="?android:textColorPrimary"-->
<!-- app:layout_constraintBottom_toTopOf="@+id/output"-->
<!-- app:layout_constraintEnd_toEndOf="parent"-->
<!-- app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview"-->
<!-- app:layout_constraintVertical_bias="0.51" />-->
<com.google.android.material.button.MaterialButton
android:id="@+id/active_download_resume"
style="?attr/materialIconButtonFilledStyle"
android:layout_width="wrap_content"
android:contentDescription="@string/resume"
android:layout_height="wrap_content"
app:backgroundTint="?attr/colorSurface"
app:cornerRadius="15dp"
app:icon="@drawable/exomedia_ic_play_arrow_white"
app:iconSize="30dp"
app:iconTint="?android:textColorPrimary"
app:layout_constraintBottom_toTopOf="@+id/output"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/linearlayout2_horizontalscrollview"
app:layout_constraintVertical_bias="0.51" />
<com.google.android.material.button.MaterialButton
android:id="@+id/active_download_delete"

View file

@ -41,26 +41,66 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
<TextView
android:id="@+id/useInExtraCommands"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
<HorizontalScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondary"
android:clickable="false"
android:visibility="gone"
android:ellipsize="end"
android:text="@string/extra_command"
android:gravity="center"
android:maxLines="1"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
android:scrollbars="none"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/content" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/content"
app:layout_constraintHorizontal_bias="0.0"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="match_parent">
<TextView
android:id="@+id/preferredTemplate"
style="@style/Widget.Material3.FloatingActionButton.Large.Primary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorPrimaryContainer"
android:clickable="false"
android:gravity="center"
android:visibility="gone"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
android:text="@string/preferred_command_template"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/useInExtraCommands"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
android:layout_marginEnd="5dp"
android:text="@string/extra_command"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorSecondaryContainer"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</HorizontalScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -102,54 +102,60 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/preferredCommandTemplateSwitch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="10dp"
android:checked="false"
android:text="@string/preferred_command_template"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" />
<com.google.android.material.card.MaterialCardView
<View
style="@style/Divider.Horizontal"
android:layout_marginVertical="10dp" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardCornerRadius="20dp">
android:layout_marginHorizontal="10dp"
android:orientation="vertical">
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/extraCommandsSwitch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:checked="false"
android:text="@string/extra_command"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="10dp"
android:orientation="vertical">
android:orientation="horizontal">
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/extraCommandsSwitch"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:checked="false"
android:text="@string/extra_command"
android:textSize="15sp"
app:layout_constraintEnd_toEndOf="parent" />
<LinearLayout
android:layout_width="match_parent"
<CheckBox
android:id="@+id/checkbox_audio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:layout_marginEnd="20dp"
android:visibility="gone"
android:checked="true"
android:text="@string/audio" />
<CheckBox
android:id="@+id/checkbox_audio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="20dp"
android:visibility="gone"
android:checked="true"
android:text="@string/audio" />
<CheckBox
android:id="@+id/checkbox_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:visibility="gone"
android:text="@string/video" />
</LinearLayout>
<CheckBox
android:id="@+id/checkbox_video"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checked="true"
android:visibility="gone"
android:text="@string/video" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
<View
style="@style/Divider.Horizontal"

View file

@ -19,8 +19,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="20dp"
android:orientation="horizontal"
android:paddingTop="20dp">
android:layout_marginTop="20dp"
android:orientation="horizontal">
<com.facebook.shimmer.ShimmerFrameLayout
android:visibility="gone"

View file

@ -17,9 +17,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:text="@string/pause"
android:text="@string/pause_all"
android:layout_margin="16dp"
android:contentDescription="@string/pause"
android:contentDescription="@string/pause_all"
app:icon="@drawable/exomedia_ic_pause_white"
app:useCompatPadding="true" />
@ -29,9 +29,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:text="@string/resume"
android:text="@string/resume_all"
android:layout_margin="16dp"
android:contentDescription="@string/resume"
android:contentDescription="@string/resume_all"
app:icon="@drawable/exomedia_ic_play_arrow_white"
app:useCompatPadding="true" />

View file

@ -94,7 +94,6 @@
</androidx.recyclerview.widget.RecyclerView>
<include layout="@layout/no_results"
android:visibility="gone" />

View file

@ -37,7 +37,6 @@
<LinearLayout
android:id="@+id/history_selection_chips"
android:layout_width="wrap_content"
android:visibility="gone"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
@ -58,16 +57,6 @@
<View style="@style/Divider.Vertical"/>
<com.google.android.material.chip.Chip
android:id="@+id/not_deleted_chip"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/not_deleted"/>
<View android:id="@+id/notdeleted_divider" style="@style/Divider.Vertical"/>
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:selectionRequired="false"
@ -97,17 +86,6 @@
</com.google.android.material.chip.ChipGroup>
<View android:id="@+id/website_divider" style="@style/Divider.Vertical"/>
<com.google.android.material.chip.ChipGroup
android:id="@+id/website_chip_group"
app:selectionRequired="false"
app:singleSelection="true"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</HorizontalScrollView>

View file

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<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:layout_width="wrap_content"
android:text="@string/status"
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/not_deleted"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:drawablePadding="30dp"
android:focusable="true"
android:paddingHorizontal="20dp"
android:paddingVertical="10dp"
android:text="@string/not_deleted"
android:textSize="15sp"
app:drawableStartCompat="@drawable/empty" />
<TextView
android:id="@+id/deleted"
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="@string/deleted"
app:drawableStartCompat="@drawable/empty" />
</LinearLayout>
<LinearLayout
android:id="@+id/websiteFilters"
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/websites"
android:textSize="14sp"
android:textStyle="bold" />
<com.google.android.material.chip.ChipGroup
android:id="@+id/websitesChipGroup"
android:layout_width="wrap_content"
app:chipSpacingVertical="-10dp"
app:chipSpacing="0dp"
android:layout_marginHorizontal="20dp"
android:layout_marginBottom="20dp"
app:singleSelection="true"
android:layout_height="wrap_content"
app:singleLine="false">
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</LinearLayout>

View file

@ -40,6 +40,7 @@
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLength="9"
android:inputType="number" />
</com.google.android.material.textfield.TextInputLayout>
@ -57,6 +58,7 @@
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLength="9"
android:inputType="text" />
</com.google.android.material.textfield.TextInputLayout>

View file

@ -3,19 +3,19 @@
xmlns:tools="http://schemas.android.com/tools"
tools:context=".MainActivity" >
<!-- <group>-->
<!-- <item-->
<!-- android:id="@+id/pause"-->
<!-- android:title="@string/pause"-->
<!-- android:icon="@drawable/exomedia_ic_pause_white"-->
<!-- app:showAsAction="ifRoom" />-->
<group>
<item
android:id="@+id/pause"
android:title="@string/pause"
android:icon="@drawable/exomedia_ic_pause_white"
app:showAsAction="ifRoom" />
<!-- <item-->
<!-- android:id="@+id/resume"-->
<!-- android:title="@string/resume"-->
<!-- android:icon="@drawable/exomedia_ic_play_arrow_white"-->
<!-- app:showAsAction="ifRoom" />-->
<!-- </group>-->
<item
android:id="@+id/resume"
android:title="@string/resume"
android:icon="@drawable/exomedia_ic_play_arrow_white"
app:showAsAction="ifRoom" />
</group>
<item
android:id="@+id/cancel"

View file

@ -10,6 +10,12 @@
app:showAsAction="collapseActionView|always"
app:actionViewClass="androidx.appcompat.widget.SearchView"/>
<item
android:id="@+id/filters"
android:title="Filters"
android:icon="@drawable/baseline_filter_alt_24"
app:showAsAction="always"/>
<item
android:id="@+id/download_queue"
android:title="@string/download_queue"

View file

@ -411,7 +411,7 @@
<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>
<string name="use_item_url_not_playlist">Use Item URL instead of Playlist URL</string>
<string name="use_item_url_not_playlist_summary">Useful for Libretube offline playlists that are not recognised by yt-dlp. Playlist metadata wont be embedded with this enabled</string>
<string name="use_item_url_not_playlist_summary">Playlist metadata wont be embedded with this enabled</string>
<string name="recode_video">Recode Video</string>
<string name="recode_video_summary">Recodes the video file to the specified video format</string>
<string name="daily">Daily</string>
@ -422,4 +422,11 @@
<string name="playlist_as_album">Use Playlist Name as Album Metadata</string>
<string name="playlist_as_album_summary">If Album Metadata is not present, use playlist name instead</string>
<string name="data_fetching_extractor_youtube">Data Fetching Extractor (YouTube)</string>
<string name="pause_all">Pause All</string>
<string name="resume_all">Resume All</string>
<string name="package_name">Package Name</string>
<string name="preferred_command_template">Preferred Command Template</string>
<string name="status">Status</string>
<string name="deleted">Deleted</string>
<string name="websites">Websites</string>
</resources>

View file

@ -102,6 +102,13 @@
app:useSimpleSummaryProvider="true"
app:title="@string/format_source" />
<EditTextPreference
android:defaultValue=""
android:icon="@drawable/ic_code"
android:key="youtube_po_token"
app:useSimpleSummaryProvider="true"
android:title="PO Token" />
<Preference
android:icon="@drawable/ic_dns"
app:key="piped_instance"

View file

@ -39,6 +39,10 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/about" >
<Preference
app:icon="@drawable/baseline_exit_to_app_24"
app:key="package_name"
app:title="@string/package_name" />
<Preference
app:icon="@drawable/ic_discord"
app:key="discord"

View file

@ -15,7 +15,8 @@ buildscript {
// supports java 1.6
commonsCompressVer = '1.12'
youtubedlAndroidVer = "ed652962e5"
youtubedlAndroidVerNoPyCrypto = "97bee6c0e8"
youtubedlAndroidVerNoPyCrypto = "97bee6c0e8"
youtubedlAndroidVerMine = "8831745ea3"
oldytdlAndroidVer = "0.14.0"
workVer = "2.9.1"
composeVer = '1.4.2'
@ -23,8 +24,8 @@ buildscript {
coroutineVer = '1.7.3'
retrofitVer = "2.9.0"
kodeinVer = "7.16.0"
navVer = "2.7.7"
media3_version = "1.4.0"
navVer = "2.8.3"
media3_version = "1.4.1"
agp_version = '8.1.0'
agp_version1 = '7.4.2'
roomVer = '2.5.2'
@ -38,16 +39,15 @@ buildscript {
}
plugins {
id 'com.android.application' version '8.4.0' apply false
id 'com.android.library' version '8.4.0' apply false
id 'com.android.application' version '8.5.2' apply false
id 'com.android.library' version '8.5.2' apply false
id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
id "org.jetbrains.kotlin.plugin.serialization" version "1.8.10" apply false
id "org.jetbrains.kotlin.plugin.parcelize" version "1.8.10" apply false
id 'com.android.test' version '8.4.0' apply false
id 'com.android.test' version '8.5.2' apply false
id 'com.google.devtools.ksp' version '1.8.10-1.0.9' apply false
}
tasks.register('clean', Delete) {
delete rootProject.buildDir
}

View file

@ -1,4 +1,6 @@
# What's Changed
- newpipe extractor general fixes
- fixed newpipe formats not working
- removed piped because its more annoying than anything, it doesnt work currently
*shortest changelog wr*

View file

@ -1,6 +1,6 @@
#Wed Oct 18 18:36:39 CEST 2023
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists