prep 1.7.9.1

This commit is contained in:
zaednasr 2024-08-25 19:51:39 +02:00
parent 65462a3704
commit f5d154a724
No known key found for this signature in database
GPG key ID: 92B1DE23AD3D0E9E
43 changed files with 2063 additions and 1158 deletions

View file

@ -95,6 +95,7 @@ android {
} }
compileOptions { compileOptions {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17
} }
@ -126,7 +127,7 @@ android {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar']) implementation fileTree(dir: 'libs', include: ['*.jar'])
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer" implementation "com.github.yausername.youtubedl-android:library:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer" implementation "com.github.yausername.youtubedl-android:ffmpeg:$youtubedlAndroidVer"
implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer" implementation "com.github.yausername.youtubedl-android:aria2c:$youtubedlAndroidVer"
@ -170,7 +171,7 @@ dependencies {
implementation 'androidx.paging:paging-runtime-ktx:3.3.0' implementation 'androidx.paging:paging-runtime-ktx:3.3.0'
implementation "androidx.room:room-paging:$roomVer" implementation "androidx.room:room-paging:$roomVer"
androidTestImplementation "androidx.room:room-testing:$roomVer" androidTestImplementation "androidx.room:room-testing:$roomVer"
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.3' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
implementation "androidx.compose.runtime:runtime:$composeVer" implementation "androidx.compose.runtime:runtime:$composeVer"
androidTestImplementation 'com.google.truth:truth:1.1.5' androidTestImplementation 'com.google.truth:truth:1.1.5'
@ -182,7 +183,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4' implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.3" implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.4"
implementation "com.github.Irineu333:Highlight-KT:1.0.4" implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer // For media playback using ExoPlayer
@ -201,5 +202,5 @@ dependencies {
implementation 'androidx.compose.material3:material3-android:1.2.1' implementation 'androidx.compose.material3:material3-android:1.2.1'
implementation "io.noties.markwon:core:4.6.2" implementation "io.noties.markwon:core:4.6.2"
implementation("org.greenrobot:eventbus:3.3.1") implementation("org.greenrobot:eventbus:3.3.1")
//implementation("com.github.TeamNewPipe:NewPipeExtractor:0.24.2") implementation("com.github.TeamNewPipe:NewPipeExtractor:0.24.2")
} }

View file

@ -88,11 +88,16 @@
static <1>$Companion Companion; static <1>$Companion Companion;
} }
### Rules for NewPipeExtractor # Rules for NewPipeExtractor
#-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; } -keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
#-keep class org.mozilla.javascript.** { *; } -keep class org.mozilla.javascript.** { *; }
#-keep class org.mozilla.classfile.ClassFileWriter -keep class org.mozilla.classfile.ClassFileWriter
#-dontwarn org.mozilla.javascript.tools.** -dontwarn org.mozilla.javascript.tools.**
-dontwarn java.beans.BeanDescriptor
-dontwarn java.beans.BeanInfo
-dontwarn java.beans.IntrospectionException
-dontwarn java.beans.Introspector
-dontwarn java.beans.PropertyDescriptor
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault -keepattributes RuntimeVisibleAnnotations,AnnotationDefault
-dontobfuscate -dontobfuscate

View file

@ -441,14 +441,30 @@ class MainActivity : BaseActivity() {
e.printStackTrace() e.printStackTrace()
} }
}else if (action == Intent.ACTION_VIEW){ }else if (action == Intent.ACTION_VIEW){
navController.popBackStack(navController.graph.startDestinationId, true)
val navbarItems = NavbarUtil.getNavBarItems(this)
when(intent.getStringExtra("destination")){ when(intent.getStringExtra("destination")){
"Downloads" -> { "Downloads" -> {
if (navbarItems.any { n -> n.itemId == R.id.historyFragment && n.isVisible }) {
navController.popBackStack(navController.graph.startDestinationId, true)
}
navController.navigate(R.id.historyFragment) navController.navigate(R.id.historyFragment)
} }
"Queue" -> { "Queue" -> {
navController.navigate(R.id.downloadQueueMainFragment) if (navbarItems.any { n -> n.itemId == R.id.downloadQueueMainFragment && n.isVisible }) {
navController.popBackStack(navController.graph.startDestinationId, true)
}
val bundle = Bundle()
intent.getStringExtra("tab")?.apply {
bundle.putString("tab", this)
}
intent.getLongExtra("reconfigure", 0L).apply {
if (this != 0L){
bundle.putLong("reconfigure", this)
}
}
navController.navigate(R.id.downloadQueueMainFragment, bundle)
} }
"Search" -> { "Search" -> {
val bundle = Bundle() val bundle = Bundle()

View file

@ -14,6 +14,9 @@ interface ResultDao {
@Query("SELECT * FROM results order by id") @Query("SELECT * FROM results order by id")
fun getResults() : Flow<List<ResultItem>> fun getResults() : Flow<List<ResultItem>>
@Query("SELECT * FROM results WHERE playlistTitle LIKE '%' || :playlistName || '%' order by id")
fun getResultsWithPlaylistName(playlistName: String) : List<ResultItem>
@Query("SELECT COUNT(id) FROM results") @Query("SELECT COUNT(id) FROM results")
fun getCount() : Flow<Int> fun getCount() : Flow<Int>

View file

@ -2,23 +2,49 @@ package com.deniscerri.ytdl.database.repository
import android.content.Context import android.content.Context
import android.util.Patterns import android.util.Patterns
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.database.DBManager.SORTING
import com.deniscerri.ytdl.database.dao.ResultDao import com.deniscerri.ytdl.database.dao.ResultDao
import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.models.ResultItem import com.deniscerri.ytdl.database.models.ResultItem
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.isYoutubeURL
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.FileUtil
import kotlinx.coroutines.delay import com.deniscerri.ytdl.util.extractors.GoogleApiUtil
import com.deniscerri.ytdl.util.extractors.newpipe.NewPipeUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.util.extractors.YoutubeApiUtil
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import java.util.regex.Pattern import kotlinx.coroutines.runBlocking
class ResultRepository(private val resultDao: ResultDao, private val context: Context) { class ResultRepository(private val resultDao: ResultDao, private val context: Context) {
val YTDLNIS_SEARCH = "YTDLNIS_SEARCH" val YTDLNIS_SEARCH = "YTDLNIS_SEARCH"
val allResults : Flow<List<ResultItem>> = resultDao.getResults() val allResults : Flow<List<ResultItem>> = resultDao.getResults()
var itemCount = MutableStateFlow(-1) var itemCount = MutableStateFlow(-1)
fun getFiltered(playlistName : String = "") : List<ResultItem> {
return resultDao.getResultsWithPlaylistName(playlistName)
}
private val youtubeApiUtil = YoutubeApiUtil(context)
//private val pipedApiUtil = PipedApiUtil(context)
private val newPipeUtil = NewPipeUtil(context)
private val ytdlpUtil = YTDLPUtil(context)
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
enum class SourceType { enum class SourceType {
YOUTUBE_VIDEO, YOUTUBE_PLAYLIST, SEARCH_QUERY, YT_DLP YOUTUBE_VIDEO,
YOUTUBE_PLAYLIST,
YOUTUBE_CHANNEL,
SEARCH_QUERY,
YT_DLP
} }
suspend fun insert(it: ResultItem){ suspend fun insert(it: ResultItem){
@ -31,17 +57,71 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
suspend fun updateTrending(){ suspend fun updateTrending(){
deleteAll() deleteAll()
val infoUtil = InfoUtil(context) val items = if (sharedPreferences.getString("api_key", "")!!.isNotBlank()) {
val items = infoUtil.getTrending() youtubeApiUtil.getTrending()
}else{
newPipeUtil.getTrending()
}
itemCount.value = items.size itemCount.value = items.size
resultDao.insertMultiple(items) resultDao.insertMultiple(items)
} }
fun getSearchSuggestions(searchQuery: String) : ArrayList<String> {
return GoogleApiUtil.getSearchSuggestions(searchQuery)
}
fun getStreamingUrlAndChapters(url: String) : Pair<List<String>, List<ChapterItem>?> {
val newPipeTrial = newPipeUtil.getStreamingUrlAndChapters(url)
if (newPipeTrial.isFailure){
val res = ytdlpUtil.getStreamingUrlAndChapters(url)
return res.getOrDefault(Pair(listOf(""), null))
}
return newPipeTrial.getOrNull()!!
}
suspend fun search(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{ suspend fun search(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{
val infoUtil = InfoUtil(context)
if (resetResults) deleteAll() if (resetResults) deleteAll()
val res = infoUtil.search(inputQuery) val res = when(sharedPreferences.getString("search_engine", "ytsearch")) {
itemCount.value = res.size "ytsearch" -> newPipeUtil.search(inputQuery)
"ytsearchmusic" -> newPipeUtil.searchMusic(inputQuery)
else -> Result.failure(Throwable())
}
val items = if (res.isSuccess) {
res.getOrNull()!!
}else{
//fallback if newpipe failed
ytdlpUtil.getFromYTDL(inputQuery)
}
itemCount.value = items.size
if (addToResults){
val ids = resultDao.insertMultiple(items)
ids.forEachIndexed { index, id ->
items[index].id = id
}
}
return items
}
private suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
val theURL = inputQuery.replace("\\?list.*".toRegex(), "")
val newPipeRes = newPipeUtil.getVideoData(theURL)
val res = if (newPipeRes.isSuccess) {
newPipeRes.getOrNull()!!
}else{
ytdlpUtil.getFromYTDL(inputQuery)
}
if (resetResults) {
deleteAll()
itemCount.value = res.size
}else{
res.filter { it.playlistTitle.isBlank() }.forEach { it.playlistTitle = YTDLNIS_SEARCH }
}
if (addToResults){ if (addToResults){
val ids = resultDao.insertMultiple(res) val ids = resultDao.insertMultiple(res)
ids.forEachIndexed { index, id -> ids.forEachIndexed { index, id ->
@ -51,53 +131,70 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return res return res
} }
suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{ private suspend fun getYoutubePlaylist(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
val infoUtil = InfoUtil(context) val id = inputQuery.split("list=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].split("&").first()
val v = infoUtil.getYoutubeVideo(inputQuery) ?: return arrayListOf() val playlistURL = "https://youtube.com/playlist?list=${id}"
if (resetResults) {
deleteAll()
itemCount.value = v.size
}else{
v.filter { it.playlistTitle.isBlank() }.forEach { it.playlistTitle = YTDLNIS_SEARCH }
}
if (addToResults){
val ids = resultDao.insertMultiple(v)
ids.forEachIndexed { index, id ->
v[index].id = id
}
}
return ArrayList(v)
}
suspend fun getPlaylist(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{
val query = inputQuery.split("list=".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()[1].split("&").first()
var nextPageToken = ""
if (resetResults) deleteAll() if (resetResults) deleteAll()
val infoUtil = InfoUtil(context) val items = mutableListOf<ResultItem>()
val items = arrayListOf<ResultItem>() val newPipeResult = newPipeUtil.getPlaylistData(playlistURL) {
do {
val tmp = infoUtil.getPlaylist(query, nextPageToken, if (items.isNotEmpty()) items[0].playlistTitle else "")
if (addToResults){ if (addToResults){
val ids = resultDao.insertMultiple(tmp.videos.toList()) runBlocking {
ids.forEachIndexed { index, id -> val ids = resultDao.insertMultiple(it)
tmp.videos[index].id = id ids.forEachIndexed { index, id ->
it[index].id = id
}
} }
} }
items.addAll(tmp.videos) items.addAll(it)
val tmpToken = tmp.nextPageToken }
if (tmpToken.isEmpty()) break
if (tmpToken == nextPageToken) break val response = if (newPipeResult.isSuccess){
nextPageToken = tmpToken newPipeResult.getOrElse { items }
delay(1000) }else{
} while (true) val res = ytdlpUtil.getFromYTDL(playlistURL)
itemCount.value = items.size val ids = resultDao.insertMultiple(res)
return items ids.forEachIndexed { index, id ->
res[index].id = id
}
res
}
itemCount.value = response.size
return response
} }
suspend fun getDefault(inputQuery: String, resetResults: Boolean, addToResults: Boolean, singleItem: Boolean = false) : ArrayList<ResultItem> { private suspend fun getYoutubeChannel(url: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
val infoUtil = InfoUtil(context) if (resetResults) deleteAll()
val items = infoUtil.getFromYTDL(inputQuery, singleItem) val items = mutableListOf<ResultItem>()
val newPipeResult = newPipeUtil.getChannelData(url) {
if (addToResults){
runBlocking {
val ids = resultDao.insertMultiple(it)
ids.forEachIndexed { index, id ->
it[index].id = id
}
}
}
items.addAll(it)
}
val response = if (newPipeResult.isSuccess){
newPipeResult.getOrElse { items }
}else{
val res = ytdlpUtil.getFromYTDL(url)
val ids = resultDao.insertMultiple(res)
ids.forEachIndexed { index, id ->
res[index].id = id
}
res
}
itemCount.value = response.size
return response
}
private suspend fun getFromYTDLP(inputQuery: String, resetResults: Boolean, addToResults: Boolean, singleItem: Boolean = false) : ArrayList<ResultItem> {
val items = ytdlpUtil.getFromYTDL(inputQuery, singleItem)
if (resetResults) { if (resetResults) {
deleteAll() deleteAll()
itemCount.value = items.size itemCount.value = items.size
@ -115,6 +212,43 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return items return items
} }
fun getFormats(url: String, source : String? = null) : List<Format> {
val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
val res = if (url.isYoutubeURL() && formatSource == "piped") {
val tmpRes = newPipeUtil.getFormats(url)
if (tmpRes.isFailure && source != null) {
Result.success(listOf())
}else{
tmpRes
}
}else{
Result.failure(Throwable())
}
return if (res.isSuccess){
res.getOrNull()!!
}else{
ytdlpUtil.getFormats(url)
}
}
suspend fun getFormatsMultiple(urls: List<String>, source: String? = null, progress: (progress: ResultViewModel.MultipleFormatProgress) -> Unit) : MutableList<MutableList<Format>> {
val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
val allYoutubeLinks = urls.all { it.isYoutubeURL() }
if (allYoutubeLinks && formatSource == "piped") {
val res = newPipeUtil.getFormatsForAll(urls) {
progress(it)
}
return res.getOrElse { mutableListOf() }
}
else {
val res = ytdlpUtil.getFormatsForAll(urls) {
progress(it)
}
return res.getOrElse { mutableListOf() }
}
}
suspend fun delete(item: ResultItem){ suspend fun delete(item: ResultItem){
resultDao.delete(item.id) resultDao.delete(item.id)
} }
@ -148,23 +282,30 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return resultDao.getAllByIDs(ids) return resultDao.getAllByIDs(ids)
} }
suspend fun getResultsFromSource(inputQuery: String, resetResults: Boolean, addToResults: Boolean = true, singleItem: Boolean = false) : ArrayList<ResultItem> { suspend fun getResultsFromSource(inputQuery: String, resetResults: Boolean, addToResults: Boolean = true, singleItem: Boolean = false) : List<ResultItem> {
return when(getQueryType(inputQuery)){ return when(getQueryType(inputQuery)){
SourceType.YOUTUBE_VIDEO -> { SourceType.YOUTUBE_VIDEO -> {
getYoutubeVideo(inputQuery, resetResults, addToResults) getYoutubeVideo(inputQuery, resetResults, addToResults)
} }
SourceType.YOUTUBE_PLAYLIST -> { SourceType.YOUTUBE_PLAYLIST -> {
if (singleItem){ if (singleItem){
getDefault(inputQuery, resetResults, addToResults, true) getFromYTDLP(inputQuery, resetResults, addToResults, true)
}else{ }else{
getPlaylist(inputQuery, resetResults, addToResults) getYoutubePlaylist(inputQuery, resetResults, addToResults)
}
}
SourceType.YOUTUBE_CHANNEL -> {
if (singleItem) {
getFromYTDLP(inputQuery, resetResults, addToResults, true)
}else{
getYoutubeChannel(inputQuery, resetResults, addToResults)
} }
} }
SourceType.SEARCH_QUERY -> { SourceType.SEARCH_QUERY -> {
search(inputQuery, resetResults, addToResults) search(inputQuery, resetResults, addToResults)
} }
SourceType.YT_DLP -> { SourceType.YT_DLP -> {
getDefault(inputQuery, resetResults, addToResults, singleItem) getFromYTDLP(inputQuery, resetResults, addToResults, singleItem)
} }
} }
@ -176,6 +317,8 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
type = SourceType.YOUTUBE_VIDEO type = SourceType.YOUTUBE_VIDEO
if (inputQuery.contains("playlist?list=")) { if (inputQuery.contains("playlist?list=")) {
type = SourceType.YOUTUBE_PLAYLIST type = SourceType.YOUTUBE_PLAYLIST
}else if (inputQuery.isYoutubeChannelURL()) {
type = SourceType.YOUTUBE_CHANNEL
} }
} else if (Patterns.WEB_URL.matcher(inputQuery).matches()) { } else if (Patterns.WEB_URL.matcher(inputQuery).matches()) {
type = SourceType.YT_DLP type = SourceType.YT_DLP

View file

@ -5,11 +5,8 @@ import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources
import android.os.Build import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Parcelable import android.os.Parcelable
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.widget.Toast
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
@ -40,8 +37,8 @@ import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple
import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.Extensions.toListString
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatSorter import com.deniscerri.ytdl.util.FormatUtil
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.deniscerri.ytdl.work.AlarmScheduler import com.deniscerri.ytdl.work.AlarmScheduler
import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker
import com.google.gson.Gson import com.google.gson.Gson
@ -64,7 +61,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
val repository : DownloadRepository val repository : DownloadRepository
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
private val commandTemplateDao: CommandTemplateDao private val commandTemplateDao: CommandTemplateDao
private val infoUtil : InfoUtil private val formatUtil = FormatUtil(application)
private val ytdlpUtil = YTDLPUtil(application)
private val resources : Resources private val resources : Resources
val allDownloads : Flow<PagingData<DownloadItem>> val allDownloads : Flow<PagingData<DownloadItem>>
@ -122,7 +120,6 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
resultRepository = ResultRepository(dbManager.resultDao, application) resultRepository = ResultRepository(dbManager.resultDao, application)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
commandTemplateDao = DBManager.getInstance(application).commandTemplateDao commandTemplateDao = DBManager.getInstance(application).commandTemplateDao
infoUtil = InfoUtil(application)
activeDownloadsCount = repository.activeDownloadsCount activeDownloadsCount = repository.activeDownloadsCount
queuedDownloadsCount = repository.queuedDownloadsCount queuedDownloadsCount = repository.queuedDownloadsCount
@ -478,9 +475,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return cloneFormat ( return cloneFormat (
try { try {
val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" } val theFormats = formats.filter { it.vcodec.isBlank() || it.vcodec == "none" }
FormatSorter(application).sortAudioFormats(theFormats).first() FormatUtil(application).sortAudioFormats(theFormats).first()
}catch (e: Exception){ }catch (e: Exception){
infoUtil.getGenericAudioFormats(resources).first() formatUtil.getGenericAudioFormats(resources).first()
} }
) )
@ -489,12 +486,12 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
return cloneFormat( return cloneFormat(
try { try {
val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty { val theFormats = formats.filter { it.vcodec.isNotBlank() && it.vcodec != "none" }.ifEmpty {
infoUtil.getGenericVideoFormats(resources).sortedByDescending { it.filesize } formatUtil.getGenericVideoFormats(resources).sortedByDescending { it.filesize }
} }
FormatSorter(application).sortVideoFormats(theFormats).first() FormatUtil(application).sortVideoFormats(theFormats).first()
}catch (e: Exception){ }catch (e: Exception){
infoUtil.getGenericVideoFormats(resources).first() formatUtil.getGenericVideoFormats(resources).first()
} }
) )
} }
@ -529,7 +526,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
if (preferredAudioFormats.isEmpty()){ if (preferredAudioFormats.isEmpty()){
val audioF = getFormat(formats, Type.audio) val audioF = getFormat(formats, Type.audio)
if (!infoUtil.getGenericAudioFormats(resources).contains(audioF)){ if (!formatUtil.getGenericAudioFormats(resources).contains(audioF)){
preferredAudioFormats.add(audioF.format_id) preferredAudioFormats.add(audioF.format_id)
} }
} }
@ -873,8 +870,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
} }
"config" -> { "config" -> {
val currentCommand = infoUtil.buildYoutubeDLRequest(it) val currentCommand = ytdlpUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand) val parsedCurrentCommand = ytdlpUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d -> val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0 d.id = 0
d.logID = null d.logID = null

View file

@ -7,6 +7,8 @@ import android.content.SharedPreferences
import android.util.Log import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -14,18 +16,17 @@ import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.dao.ResultDao import com.deniscerri.ytdl.database.dao.ResultDao
import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.models.ResultItem import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.models.SearchHistoryItem import com.deniscerri.ytdl.database.models.SearchHistoryItem
import com.deniscerri.ytdl.database.repository.ResultRepository import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.repository.SearchHistoryRepository import com.deniscerri.ytdl.database.repository.SearchHistoryRepository
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDLException import com.yausername.youtubedl_android.YoutubeDLException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.joinAll import kotlinx.coroutines.joinAll
@ -40,7 +41,9 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
val repository : ResultRepository val repository : ResultRepository
private val searchHistoryRepository : SearchHistoryRepository private val searchHistoryRepository : SearchHistoryRepository
val items : LiveData<List<ResultItem>> val items : LiveData<List<ResultItem>>
private val infoUtil: InfoUtil private var _items = MediatorLiveData<List<ResultItem>>()
val playlistFilter = MutableLiveData("")
private val notificationUtil: NotificationUtil private val notificationUtil: NotificationUtil
private val dao: ResultDao private val dao: ResultDao
data class ResultsUiState( data class ResultsUiState(
@ -72,12 +75,31 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
dao = DBManager.getInstance(application).resultDao dao = DBManager.getInstance(application).resultDao
repository = ResultRepository(dao, getApplication<Application>().applicationContext) repository = ResultRepository(dao, getApplication<Application>().applicationContext)
searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao) searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao)
items = repository.allResults.asLiveData() items = repository.allResults.asLiveData()
_items.addSource(items){
filter()
}
_items.addSource(playlistFilter) {
filter()
}
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
infoUtil = InfoUtil(application)
notificationUtil = NotificationUtil(application) notificationUtil = NotificationUtil(application)
} }
fun filter() = viewModelScope.launch(Dispatchers.IO){
_items.postValue(repository.getFiltered(playlistFilter.value ?: ""))
}
fun setPlaylistFilter(p: String){
playlistFilter.value = p
}
fun getFilteredList() : LiveData<List<ResultItem>> {
return _items
}
fun checkTrending() = viewModelScope.launch(Dispatchers.IO){ fun checkTrending() = viewModelScope.launch(Dispatchers.IO){
try { try {
val item = repository.getFirstResult() val item = repository.getFirstResult()
@ -271,7 +293,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
updateFormatsResultDataJob = viewModelScope.launch(Dispatchers.IO) { updateFormatsResultDataJob = viewModelScope.launch(Dispatchers.IO) {
updatingFormats.emit(true) updatingFormats.emit(true)
try { try {
val formats = infoUtil.getFormats(result.url) val formats = getFormats(result.url)
updatingFormats.emit(false) updatingFormats.emit(false)
formats.apply { formats.apply {
if (formats.isNotEmpty() && updateFormatsResultDataJob?.isCancelled == false) { if (formats.isNotEmpty() && updateFormatsResultDataJob?.isCancelled == false) {
@ -309,7 +331,34 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
} }
} }
data class MultipleFormatProgress(
val url: String,
val formats: List<Format>,
val unavailable : Boolean = false,
val unavailableMessage: String = ""
)
fun getFormats(url: String, source: String? = null) : List<Format> {
return repository.getFormats(url, source)
}
suspend fun getFormatsMultiple(urls: List<String>, source: String? = null, progress: (progress: MultipleFormatProgress) -> Unit) : MutableList<MutableList<Format>> {
val res = repository.getFormatsMultiple(urls, source) {
progress(it)
}
return res
}
fun getResultsBetweenTwoItems(item1: Long, item2: Long) : List<ResultItem>{ fun getResultsBetweenTwoItems(item1: Long, item2: Long) : List<ResultItem>{
return dao.getResultsBetweenTwoItems(item1, item2) return dao.getResultsBetweenTwoItems(item1, item2)
} }
fun getSearchSuggestions(searchQuery: String) : ArrayList<String> {
return repository.getSearchSuggestions(searchQuery)
}
fun getStreamingUrlAndChapters(url: String) : Pair<List<String>, List<ChapterItem>?> {
return repository.getStreamingUrlAndChapters(url)
}
} }

View file

@ -9,7 +9,6 @@ import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.text.Editable
import android.util.Log import android.util.Log
import android.util.Patterns import android.util.Patterns
import android.view.* import android.view.*
@ -19,7 +18,6 @@ import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.forEach import androidx.core.view.forEach
@ -38,13 +36,15 @@ import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.ResultItem import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.models.SearchSuggestionItem import com.deniscerri.ytdl.database.models.SearchSuggestionItem
import com.deniscerri.ytdl.database.models.SearchSuggestionType import com.deniscerri.ytdl.database.models.SearchSuggestionType
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.adapter.HomeAdapter import com.deniscerri.ytdl.ui.adapter.HomeAdapter
import com.deniscerri.ytdl.ui.adapter.SearchSuggestionsAdapter import com.deniscerri.ytdl.ui.adapter.SearchSuggestionsAdapter
import com.deniscerri.ytdl.ui.downloads.HistoryFragment
import com.deniscerri.ytdl.ui.downloads.HistoryFragment.Companion
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
@ -52,19 +52,15 @@ import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.google.android.material.chip.ChipGroup import com.google.android.material.chip.ChipGroup
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.search.SearchBar import com.google.android.material.search.SearchBar
import com.google.android.material.search.SearchView import com.google.android.material.search.SearchView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -84,10 +80,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
private var downloadAllFab: ExtendedFloatingActionButton? = null private var downloadAllFab: ExtendedFloatingActionButton? = null
private var clipboardFab: ExtendedFloatingActionButton? = null private var clipboardFab: ExtendedFloatingActionButton? = null
private var homeFabs: LinearLayout? = null private var homeFabs: LinearLayout? = null
private var infoUtil: InfoUtil? = null
private var notificationUtil: NotificationUtil? = null private var notificationUtil: NotificationUtil? = null
private var downloadQueue: ArrayList<ResultItem>? = null private var downloadQueue: ArrayList<ResultItem>? = null
private lateinit var playlistNameFilterScrollView: HorizontalScrollView
private lateinit var playlistNameFilterChipGroup: ChipGroup
private lateinit var resultViewModel : ResultViewModel private lateinit var resultViewModel : ResultViewModel
private lateinit var downloadViewModel : DownloadViewModel private lateinit var downloadViewModel : DownloadViewModel
private lateinit var historyViewModel : HistoryViewModel private lateinit var historyViewModel : HistoryViewModel
@ -123,7 +121,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
activity = getActivity() activity = getActivity()
mainActivity = activity as MainActivity? mainActivity = activity as MainActivity?
quickLaunchSheet = false quickLaunchSheet = false
infoUtil = InfoUtil(requireContext())
notificationUtil = NotificationUtil(requireContext()) notificationUtil = NotificationUtil(requireContext())
selectedObjects = arrayListOf() selectedObjects = arrayListOf()
return fragmentView return fragmentView
@ -157,6 +154,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
downloadSelectedFab = homeFabs!!.findViewById(R.id.download_selected_fab) downloadSelectedFab = homeFabs!!.findViewById(R.id.download_selected_fab)
downloadAllFab = homeFabs!!.findViewById(R.id.download_all_fab) downloadAllFab = homeFabs!!.findViewById(R.id.download_all_fab)
clipboardFab = homeFabs!!.findViewById(R.id.copied_url_fab) clipboardFab = homeFabs!!.findViewById(R.id.copied_url_fab)
playlistNameFilterScrollView = view.findViewById(R.id.playlist_selection_chips_scrollview)
playlistNameFilterChipGroup = view.findViewById(R.id.playlist_selection_chips)
runCatching { materialToolbar!!.title = ThemeUtil.getStyledAppName(requireContext()) } runCatching { materialToolbar!!.title = ThemeUtil.getStyledAppName(requireContext()) }
@ -186,7 +185,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val progressBar = view.findViewById<View>(R.id.progress) val progressBar = view.findViewById<View>(R.id.progress)
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
resultViewModel.items.observe(requireActivity()) { resultViewModel.getFilteredList().observe(requireActivity()) {
kotlin.runCatching { kotlin.runCatching {
homeAdapter!!.submitList(it) homeAdapter!!.submitList(it)
resultsList = it resultsList = it
@ -197,6 +196,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}else{ }else{
downloadAllFab!!.visibility = GONE downloadAllFab!!.visibility = GONE
} }
}else if (resultViewModel.repository.itemCount.value == 1){ }else if (resultViewModel.repository.itemCount.value == 1){
if (sharedPreferences!!.getBoolean("download_card", true)){ if (sharedPreferences!!.getBoolean("download_card", true)){
if(it.size == 1 && quickLaunchSheet && parentFragmentManager.findFragmentByTag("downloadSingleSheet") == null){ if(it.size == 1 && quickLaunchSheet && parentFragmentManager.findFragmentByTag("downloadSingleSheet") == null){
@ -213,6 +213,14 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
} }
} }
resultViewModel.items.observe(requireActivity()) {
updateMultiplePlaylistResults(it
.filter { it2 -> it2.playlistTitle != "" && it2.playlistTitle != "YTDLNIS_SEARCH" }
.map { it.playlistTitle }
.distinct()
)
}
initMenu() initMenu()
downloadSelectedFab?.tag = "downloadSelected" downloadSelectedFab?.tag = "downloadSelected"
downloadSelectedFab?.setOnClickListener(this) downloadSelectedFab?.setOnClickListener(this)
@ -523,13 +531,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val combinedList = mutableListOf<SearchSuggestionItem>() val combinedList = mutableListOf<SearchSuggestionItem>()
val history = withContext(Dispatchers.IO){ val history = withContext(Dispatchers.IO){
resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery) } resultViewModel.getSearchHistory().map { it.query }.filter { it.contains(searchQuery, ignoreCase = true) }
}.map { }.map {
SearchSuggestionItem(it, SearchSuggestionType.HISTORY) SearchSuggestionItem(it, SearchSuggestionType.HISTORY)
} }
val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){ val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
infoUtil!!.getSearchSuggestions(searchQuery) resultViewModel.getSearchSuggestions(searchQuery)
} }
}else{ }else{
emptyList() emptyList()
@ -780,6 +788,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
mode!!.menuInflater.inflate(R.menu.main_menu_context, menu) mode!!.menuInflater.inflate(R.menu.main_menu_context, menu)
mode.title = "${selectedObjects.size} ${getString(R.string.selected)}" mode.title = "${selectedObjects.size} ${getString(R.string.selected)}"
searchBar!!.isEnabled = false searchBar!!.isEnabled = false
playlistNameFilterChipGroup.children.forEach { it.isEnabled = false }
searchBar!!.menu.forEach { it.isEnabled = false } searchBar!!.menu.forEach { it.isEnabled = false }
(activity as MainActivity).disableBottomNavigation() (activity as MainActivity).disableBottomNavigation()
return true return true
@ -877,6 +886,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
(activity as MainActivity).enableBottomNavigation() (activity as MainActivity).enableBottomNavigation()
clearCheckedItems() clearCheckedItems()
searchBar!!.isEnabled = true searchBar!!.isEnabled = true
playlistNameFilterChipGroup.children.forEach { it.isEnabled = true }
searchBar!!.menu.forEach { it.isEnabled = true } searchBar!!.menu.forEach { it.isEnabled = true }
searchBar?.expand(appBarLayout!!) searchBar?.expand(appBarLayout!!)
} }
@ -970,4 +980,34 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchView!!.editText.setText(text) searchView!!.editText.setText(text)
searchView!!.editText.setSelection(searchView!!.editText.length()) searchView!!.editText.setSelection(searchView!!.editText.length())
} }
private fun updateMultiplePlaylistResults(playlistTitles: List<String>) {
if (playlistTitles.isEmpty() || playlistTitles.size == 1) {
playlistNameFilterScrollView.isVisible = false
playlistNameFilterChipGroup.children.filter { it.tag != "all" }.forEach {
playlistNameFilterChipGroup.removeView(it)
}
return
}
playlistNameFilterChipGroup.children.first().setOnClickListener {
resultViewModel.setPlaylistFilter("")
}
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
tmp.setOnClickListener {
resultViewModel.setPlaylistFilter(t)
}
playlistNameFilterChipGroup.addView(tmp)
}
playlistNameFilterScrollView.isVisible = true
}
} }

View file

@ -11,7 +11,6 @@ import android.widget.FrameLayout
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.paging.PagingDataAdapter
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -19,12 +18,10 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItemConfigureMultiple import com.deniscerri.ytdl.database.models.DownloadItemConfigureMultiple
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.loadThumbnail import com.deniscerri.ytdl.util.Extensions.loadThumbnail
import com.deniscerri.ytdl.util.Extensions.popup import com.deniscerri.ytdl.util.Extensions.popup
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import java.util.Locale import java.util.Locale

View file

@ -15,14 +15,13 @@ import android.view.inputmethod.InputMethodManager
import android.widget.* import android.widget.*
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdl.util.Extensions.enableTextHighlight import com.deniscerri.ytdl.util.Extensions.enableTextHighlight
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
@ -32,13 +31,13 @@ import kotlinx.coroutines.withContext
class AddExtraCommandsDialog(private val item: DownloadItem? = null, private val callback: ExtraCommandsListener? = null) : BottomSheetDialogFragment() { class AddExtraCommandsDialog(private val item: DownloadItem? = null, private val callback: ExtraCommandsListener? = null) : BottomSheetDialogFragment() {
private lateinit var infoUtil: InfoUtil private lateinit var ytdlpUtil: YTDLPUtil
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var commandTemplateViewModel: CommandTemplateViewModel private lateinit var commandTemplateViewModel: CommandTemplateViewModel
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity().applicationContext) ytdlpUtil = YTDLPUtil(requireActivity().applicationContext)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java] commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
} }
@ -87,7 +86,7 @@ class AddExtraCommandsDialog(private val item: DownloadItem? = null, private val
val currentText = view.findViewById<TextView>(R.id.currentText) val currentText = view.findViewById<TextView>(R.id.currentText)
if (item != null){ if (item != null){
val currentCommand = infoUtil.parseYTDLRequestString(infoUtil.buildYoutubeDLRequest(item)) val currentCommand = ytdlpUtil.parseYTDLRequestString(ytdlpUtil.buildYoutubeDLRequest(item))
currentText?.text = currentCommand currentText?.text = currentCommand
}else{ }else{
view.findViewById<View>(R.id.current).visibility = View.GONE view.findViewById<View>(R.id.current).visibility = View.GONE

View file

@ -13,12 +13,11 @@ import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.* import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem.fromUri import androidx.media3.common.MediaItem.fromUri
import androidx.media3.common.Player import androidx.media3.common.Player
@ -31,17 +30,15 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.ChapterItem import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.util.Extensions import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.convertToTimestamp import com.deniscerri.ytdl.util.Extensions.convertToTimestamp
import com.deniscerri.ytdl.util.Extensions.setTextAndRecalculateWidth import com.deniscerri.ytdl.util.Extensions.setTextAndRecalculateWidth
import com.deniscerri.ytdl.util.Extensions.toStringDuration import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.Extensions.toStringTimeStamp import com.deniscerri.ytdl.util.Extensions.toStringTimeStamp
import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.VideoPlayerUtil import com.deniscerri.ytdl.util.VideoPlayerUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
@ -52,26 +49,19 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.materialswitch.MaterialSwitch import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.slider.RangeSlider import com.google.android.material.slider.RangeSlider
import com.google.android.material.textfield.TextInputLayout
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.lang.reflect.Type
import java.util.* import java.util.*
import kotlin.math.min
import kotlin.properties.Delegates
class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private val urls : String? = null, private var chapters: List<ChapterItem>? = null, private val listener: VideoCutListener? = null) : BottomSheetDialogFragment() { class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private val urls : String? = null, private var chapters: List<ChapterItem>? = null, private val listener: VideoCutListener? = null) : BottomSheetDialogFragment() {
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var infoUtil: InfoUtil
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var resultViewModel: ResultViewModel
private lateinit var cutSection : ConstraintLayout private lateinit var cutSection : ConstraintLayout
private lateinit var durationText: TextView private lateinit var durationText: TextView
private lateinit var progress : ProgressBar private lateinit var progress : ProgressBar
@ -99,8 +89,8 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
private lateinit var selectedCuts: MutableList<String> private lateinit var selectedCuts: MutableList<String>
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity().applicationContext)
} }
@ -182,7 +172,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
try { try {
val data = withContext(Dispatchers.IO) { val data = withContext(Dispatchers.IO) {
if (urls.isNullOrEmpty()) { if (urls.isNullOrEmpty()) {
infoUtil.getStreamingUrlAndChapters(item.url) resultViewModel.getStreamingUrlAndChapters(item.url)
}else{ }else{
Pair(urls.split("\n"), chapters) Pair(urls.split("\n"), chapters)
} }

View file

@ -6,7 +6,6 @@ import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextUtils
import android.text.TextWatcher import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -16,10 +15,8 @@ import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.ViewCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.setPadding import androidx.core.view.setPadding
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
@ -35,7 +32,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.FormatUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -48,7 +45,6 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.Locale
class DownloadAudioFragment(private var resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "", private var nonSpecific: Boolean = false) : Fragment(), GUISync { class DownloadAudioFragment(private var resultItem: ResultItem? = null, private var currentDownloadItem: DownloadItem? = null, private var url: String = "", private var nonSpecific: Boolean = false) : Fragment(), GUISync {
@ -58,7 +54,6 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
private lateinit var resultViewModel : ResultViewModel private lateinit var resultViewModel : ResultViewModel
private lateinit var saveDir : TextInputLayout private lateinit var saveDir : TextInputLayout
private lateinit var freeSpace : TextView private lateinit var freeSpace : TextView
private lateinit var infoUtil: InfoUtil
private lateinit var genericAudioFormats: MutableList<Format> private lateinit var genericAudioFormats: MutableList<Format>
lateinit var downloadItem : DownloadItem lateinit var downloadItem : DownloadItem
@ -76,8 +71,7 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
activity = getActivity() activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
infoUtil = InfoUtil(requireContext()) genericAudioFormats = FormatUtil(requireContext()).getGenericAudioFormats(requireContext().resources)
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList() shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList()
return fragmentView return fragmentView

View file

@ -7,7 +7,6 @@ import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Patterns import android.util.Patterns
import android.view.LayoutInflater import android.view.LayoutInflater
@ -40,7 +39,6 @@ import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.receiver.ShareActivity import com.deniscerri.ytdl.receiver.ShareActivity
import com.deniscerri.ytdl.ui.BaseActivity import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -60,8 +58,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
class DownloadBottomSheetDialog : BottomSheetDialogFragment() { class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
@ -73,7 +69,6 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var commandTemplateViewModel : CommandTemplateViewModel private lateinit var commandTemplateViewModel : CommandTemplateViewModel
private lateinit var infoUtil: InfoUtil
private lateinit var sharedPreferences : SharedPreferences private lateinit var sharedPreferences : SharedPreferences
private lateinit var updateItem : Button private lateinit var updateItem : Button
private lateinit var view: View private lateinit var view: View
@ -96,7 +91,6 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java] historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java] commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
infoUtil = InfoUtil(requireContext())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val res: ResultItem? val res: ResultItem?
val dwl: DownloadItem? val dwl: DownloadItem?

View file

@ -44,7 +44,6 @@ import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter import com.deniscerri.ytdl.ui.adapter.ConfigureMultipleDownloadsAdapter
import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomappbar.BottomAppBar
@ -73,7 +72,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var listAdapter : ConfigureMultipleDownloadsAdapter private lateinit var listAdapter : ConfigureMultipleDownloadsAdapter
private lateinit var recyclerView: RecyclerView private lateinit var recyclerView: RecyclerView
private lateinit var infoUtil: InfoUtil
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var bottomAppBar: BottomAppBar private lateinit var bottomAppBar: BottomAppBar
private lateinit var filesize : TextView private lateinit var filesize : TextView
@ -100,7 +98,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java] commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
infoUtil = InfoUtil(requireContext())
currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf() currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf()
processingItemsCount = currentDownloadIDs.size processingItemsCount = currentDownloadIDs.size

View file

@ -15,7 +15,6 @@ import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -33,7 +32,7 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil import com.deniscerri.ytdl.util.FormatUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -60,7 +59,6 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
lateinit var author : TextInputLayout lateinit var author : TextInputLayout
private lateinit var saveDir : TextInputLayout private lateinit var saveDir : TextInputLayout
private lateinit var freeSpace : TextView private lateinit var freeSpace : TextView
private lateinit var infoUtil: InfoUtil
private lateinit var genericVideoFormats: MutableList<Format> private lateinit var genericVideoFormats: MutableList<Format>
private lateinit var genericAudioFormats: MutableList<Format> private lateinit var genericAudioFormats: MutableList<Format>
@ -78,9 +76,9 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
activity = getActivity() activity = getActivity()
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
infoUtil = InfoUtil(requireContext()) val formatUtil = FormatUtil(requireContext())
genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources) genericVideoFormats = formatUtil.getGenericVideoFormats(requireContext().resources)
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources) genericAudioFormats = formatUtil.getGenericAudioFormats(requireContext().resources)
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList() shownFields = preferences.getStringSet("modify_download_card", requireContext().getStringArray(R.array.modify_download_card_values).toSet())!!.toList()
return fragmentView return fragmentView

View file

@ -7,10 +7,8 @@ import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.* import android.widget.*
import androidx.compose.foundation.layout.PaddingValues
import androidx.core.view.children import androidx.core.view.children
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible import androidx.core.view.isVisible
@ -24,25 +22,20 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.FormatSorter import com.deniscerri.ytdl.util.FormatUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.chip.Chip
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.internal.format
import java.util.regex.Pattern
class FormatSelectionBottomSheetDialog( class FormatSelectionBottomSheetDialog(
@ -52,10 +45,11 @@ class FormatSelectionBottomSheetDialog(
) : BottomSheetDialogFragment() { ) : BottomSheetDialogFragment() {
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var infoUtil: InfoUtil private lateinit var formatUtil: FormatUtil
private lateinit var view: View private lateinit var view: View
private lateinit var continueInBackgroundSnackBar : Snackbar private lateinit var continueInBackgroundSnackBar : Snackbar
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var resultViewModel: ResultViewModel
private lateinit var sharedPreferences: SharedPreferences private lateinit var sharedPreferences: SharedPreferences
private lateinit var videoFormatList : LinearLayout private lateinit var videoFormatList : LinearLayout
private lateinit var audioFormatList : LinearLayout private lateinit var audioFormatList : LinearLayout
@ -95,11 +89,12 @@ class FormatSelectionBottomSheetDialog(
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity().applicationContext) formatUtil = FormatUtil(requireContext())
chosenFormats = listOf() chosenFormats = listOf()
selectedAudios = mutableListOf() selectedAudios = mutableListOf()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
} }
@ -133,8 +128,8 @@ class FormatSelectionBottomSheetDialog(
multipleFormatsListener = this multipleFormatsListener = this
} }
genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources) genericAudioFormats = formatUtil.getGenericAudioFormats(requireContext().resources)
genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources) genericVideoFormats = formatUtil.getGenericVideoFormats(requireContext().resources)
sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!) sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!)
filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!) filterBy = FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!)
@ -221,7 +216,7 @@ class FormatSelectionBottomSheetDialog(
//simple download //simple download
if (items.size == 1) { if (items.size == 1) {
kotlin.runCatching { kotlin.runCatching {
val res = infoUtil.getFormats(items.first()!!.url, currentFormatSource) val res = resultViewModel.getFormats(items.first()!!.url, currentFormatSource)
if (!isActive) return@launch if (!isActive) return@launch
res.filter { it.format_note != "storyboard" } res.filter { it.format_note != "storyboard" }
chosenFormats = if (items.first()?.type == Type.audio) { chosenFormats = if (items.first()?.type == Type.audio) {
@ -255,7 +250,7 @@ class FormatSelectionBottomSheetDialog(
refreshBtn.text = progress refreshBtn.text = progress
} }
val res = infoUtil.getFormatsMultiple(itemsWithMissingFormats.map { it!!.url }, currentFormatSource) { val res = resultViewModel.getFormatsMultiple(itemsWithMissingFormats.map { it!!.url }, currentFormatSource) {
if (!isActive) return@getFormatsMultiple if (!isActive) return@getFormatsMultiple
if (it.unavailable) { if (it.unavailable) {
@ -493,7 +488,7 @@ class FormatSelectionBottomSheetDialog(
FormatSorting.filesize -> chosenFormats FormatSorting.filesize -> chosenFormats
} }
val formatSorter = FormatSorter(requireContext()) val formatSorter = FormatUtil(requireContext())
//filter category //filter category
when(filterBy){ when(filterBy){

View file

@ -40,8 +40,6 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.TextWithSubtitle
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
@ -56,9 +54,7 @@ import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.hamcrest.core.Every
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.time.Month
import java.util.Calendar import java.util.Calendar
import java.util.Locale import java.util.Locale
@ -73,7 +69,6 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var behavior: BottomSheetBehavior<View> private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var commandTemplateViewModel : CommandTemplateViewModel private lateinit var commandTemplateViewModel : CommandTemplateViewModel
private lateinit var infoUtil: InfoUtil
private lateinit var sharedPreferences : SharedPreferences private lateinit var sharedPreferences : SharedPreferences
private lateinit var view: View private lateinit var view: View
@ -110,7 +105,6 @@ class ObserveSourcesBottomSheetDialog : BottomSheetDialogFragment() {
historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java] historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java] commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
infoUtil = InfoUtil(requireContext())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
currentItem = if (Build.VERSION.SDK_INT >= 33){ currentItem = if (Build.VERSION.SDK_INT >= 33){

View file

@ -13,7 +13,6 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.text.format.DateFormat
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -21,8 +20,6 @@ import android.view.ViewGroup
import android.view.Window import android.view.Window
import android.widget.Button import android.widget.Button
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -48,8 +45,6 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadMinifiedAdapter import com.deniscerri.ytdl.ui.adapter.ActiveDownloadMinifiedAdapter
import com.deniscerri.ytdl.ui.adapter.GenericDownloadAdapter import com.deniscerri.ytdl.ui.adapter.GenericDownloadAdapter
import com.deniscerri.ytdl.util.Extensions.setFullScreen import com.deniscerri.ytdl.util.Extensions.setFullScreen
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.VideoPlayerUtil import com.deniscerri.ytdl.util.VideoPlayerUtil
@ -58,7 +53,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.chip.Chip
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors import com.google.android.material.elevation.SurfaceColors
@ -76,13 +70,9 @@ import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdapter.OnItemClickListener, ActiveDownloadMinifiedAdapter.OnItemClickListener { class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdapter.OnItemClickListener, ActiveDownloadMinifiedAdapter.OnItemClickListener {
private lateinit var infoUtil: InfoUtil
private lateinit var notificationUtil: NotificationUtil private lateinit var notificationUtil: NotificationUtil
private lateinit var videoView: PlayerView private lateinit var videoView: PlayerView
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
@ -99,7 +89,6 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity())
notificationUtil = NotificationUtil(requireActivity()) notificationUtil = NotificationUtil(requireActivity())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
@ -283,7 +272,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
try { try {
val data = withContext(Dispatchers.IO) { val data = withContext(Dispatchers.IO) {
if (item.urls.isEmpty()) { if (item.urls.isEmpty()) {
infoUtil.getStreamingUrlAndChapters(item.url) resultViewModel.getStreamingUrlAndChapters(item.url)
}else{ }else{
Pair(item.urls.split("\n"), null) Pair(item.urls.split("\n"), null)
} }

View file

@ -30,6 +30,7 @@ import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator import com.google.android.material.tabs.TabLayoutMediator
import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
@ -71,7 +72,9 @@ class DownloadQueueMainFragment : Fragment(){
}else{ }else{
mainActivity.hideBottomNavigation() mainActivity.hideBottomNavigation()
} }
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() } topAppBar.setNavigationOnClickListener {
mainActivity.onBackPressedDispatcher.onBackPressed()
}
tabLayout = view.findViewById(R.id.download_tablayout) tabLayout = view.findViewById(R.id.download_tablayout)
viewPager2 = view.findViewById(R.id.download_viewpager) viewPager2 = view.findViewById(R.id.download_viewpager)
@ -148,6 +151,8 @@ class DownloadQueueMainFragment : Fragment(){
}, 200) }, 200)
} }
arguments?.clear()
if (sharedPreferences.getBoolean("show_count_downloads", false)){ if (sharedPreferences.getBoolean("show_count_downloads", false)){
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.activeDownloadsCount.collectLatest { downloadViewModel.activeDownloadsCount.collectLatest {

View file

@ -45,6 +45,7 @@ import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialog
@ -498,4 +499,8 @@ object Extensions {
return Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)").matcher(this).find() return Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)").matcher(this).find()
} }
fun String.isYoutubeChannelURL() : Boolean {
return Pattern.compile("((^(https?)://)?(www.)?(m.)?youtu(.be)?(be.com))/@[a-zA-Z]+").matcher(this).find()
}
} }

View file

@ -3,20 +3,13 @@ package com.deniscerri.ytdl.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import androidx.core.content.ContextCompat import android.content.res.Resources
import androidx.core.content.contentValuesOf
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.google.gson.Gson
import com.google.gson.JsonObject
import org.json.JSONObject
import java.text.Normalizer.Form
import java.util.regex.Pattern
class FormatSorter(private var context: Context) { class FormatUtil(private var context: Context) {
private val sharedPreferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) private val sharedPreferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val videoFormatIDPreference : List<String> = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() } private val videoFormatIDPreference : List<String> = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
private val audioFormatIDPreference : List<String> = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() } private val audioFormatIDPreference : List<String> = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
@ -170,6 +163,53 @@ class FormatSorter(private var context: Context) {
return formats.sortedWith(fieldSorter) return formats.sortedWith(fieldSorter)
} }
fun getGenericAudioFormats(resources: Resources) : MutableList<Format>{
val audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
val audioFormats = resources.getStringArray(R.array.audio_formats)
val audioFormatsValues = resources.getStringArray(R.array.audio_formats_values)
val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("audio_format", "")
val acodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
if (this.isEmpty()){
resources.getString(R.string.defaultValue)
}else{
val audioCodecs = resources.getStringArray(R.array.audio_codec)
val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values)
audioCodecs[audioCodecsValues.indexOf(this)]
}
}
audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) }
audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",resources.getString(R.string.preferred_format_id), "",1, it)) }
return formats
}
fun getGenericVideoFormats(resources: Resources) : MutableList<Format>{
val formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
val videoFormatsValues = resources.getStringArray(R.array.video_formats_values)
val videoFormats = resources.getStringArray(R.array.video_formats)
val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("video_format", "")
val audioCodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
if (this.isNotEmpty()){
val audioCodecs = resources.getStringArray(R.array.audio_codec)
val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values)
audioCodecs[audioCodecsValues.indexOf(this)]
}else this
}
val videoCodecPreference = sharedPreferences.getString("video_codec", "")!!.run {
if (this.isEmpty()){
resources.getString(R.string.defaultValue)
}else{
val videoCodecs = resources.getStringArray(R.array.video_codec)
val videoCodecsValues = resources.getStringArray(R.array.video_codec_values)
videoCodecs[videoCodecsValues.indexOf(this)]
}
}
videoFormatsValues.forEachIndexed { index, it -> formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, videoFormats[index])) }
formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.preferred_format_id),"", "",1, it)) }
return formats
}
//OLD CODE JUST FOR STORAGE / REFERENCE //OLD CODE JUST FOR STORAGE / REFERENCE
/* /*

View file

@ -35,7 +35,6 @@ object NavbarUtil {
val pref = settings.getString("start_destination", "")!! val pref = settings.getString("start_destination", "")!!
val items = getDefaultNavBarItems(context) val items = getDefaultNavBarItems(context)
val navBarHasPref = getNavBarPrefs().contains(items.indexOfFirst { it.itemId == navItems[pref] }.toString()) val navBarHasPref = getNavBarPrefs().contains(items.indexOfFirst { it.itemId == navItems[pref] }.toString())
println("START FRAGMENT: $pref")
return if (pref == "") { return if (pref == "") {
R.id.homeFragment R.id.homeFragment
}else { }else {

View file

@ -9,7 +9,6 @@ import android.app.TaskStackBuilder
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Resources import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
@ -17,7 +16,6 @@ import android.os.Bundle
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.os.bundleOf
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.navigation.NavDeepLinkBuilder import androidx.navigation.NavDeepLinkBuilder
import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.MainActivity
@ -29,6 +27,7 @@ import com.deniscerri.ytdl.receiver.PauseDownloadNotificationReceiver
import com.deniscerri.ytdl.receiver.ResumeActivity import com.deniscerri.ytdl.receiver.ResumeActivity
import com.deniscerri.ytdl.util.Extensions.toBitmap import com.deniscerri.ytdl.util.Extensions.toBitmap
import java.io.File import java.io.File
import kotlin.random.Random
class NotificationUtil(var context: Context) { class NotificationUtil(var context: Context) {
@ -339,22 +338,28 @@ class NotificationUtil(var context: Context) {
.setArguments(bundle) .setArguments(bundle)
.createPendingIntent() .createPendingIntent()
val errorTabPendingIntent = NavDeepLinkBuilder(context) val intent = Intent(context, MainActivity::class.java)
.setGraph(R.navigation.nav_graph) intent.setAction(Intent.ACTION_VIEW)
.setDestination(R.id.downloadQueueMainFragment) intent.putExtra("destination", "Queue")
.setArguments(bundleOf(Pair("tab", "error"))) intent.putExtra("tab", "error")
.createPendingIntent() val errorTabPendingIntent = PendingIntent.getActivity(
context,
Random.nextInt(),
intent,
PendingIntent.FLAG_IMMUTABLE
)
val reconfigurePendingItent = NavDeepLinkBuilder(context) val intent2 = Intent(context, MainActivity::class.java)
.setGraph(R.navigation.nav_graph) intent2.setAction(Intent.ACTION_VIEW)
.setDestination(R.id.downloadQueueMainFragment) intent2.putExtra("reconfigure", id)
.setArguments( intent2.putExtra("tab", "error")
bundleOf( intent2.putExtra("destination", "Queue")
Pair("tab", "error"), val reconfigurePendingItent = PendingIntent.getActivity(
Pair("reconfigure", id) context,
) Random.nextInt(),
) intent2,
.createPendingIntent() PendingIntent.FLAG_IMMUTABLE
)
notificationBuilder notificationBuilder
.setContentTitle("${res.getString(R.string.failed_download)}: $title") .setContentTitle("${res.getString(R.string.failed_download)}: $title")

View file

@ -70,6 +70,8 @@ import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.enableTextHighlight import com.deniscerri.ytdl.util.Extensions.enableTextHighlight
import com.deniscerri.ytdl.util.Extensions.getMediaDuration import com.deniscerri.ytdl.util.Extensions.getMediaDuration
import com.deniscerri.ytdl.util.Extensions.toStringDuration import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.extractors.PipedApiUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils import com.google.android.material.badge.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils import com.google.android.material.badge.ExperimentalBadgeUtils
@ -567,10 +569,10 @@ object UiUtil {
if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE
else fileSize!!.text = fileSizeReadable else fileSize!!.text = fileSizeReadable
val infoUtil = InfoUtil(context) val ytdlpUtil = YTDLPUtil(context)
command?.setOnClickListener { command?.setOnClickListener {
showGeneratedCommand(context, infoUtil.parseYTDLRequestString(infoUtil.buildYoutubeDLRequest(item))) showGeneratedCommand(context, ytdlpUtil.parseYTDLRequestString(ytdlpUtil.buildYoutubeDLRequest(item)))
} }
val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link) val link = bottomSheet.findViewById<Button>(R.id.bottom_sheet_link)
@ -1867,7 +1869,7 @@ object UiUtil {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val chipGroup = view.findViewById<ChipGroup>(R.id.filename_suggested_chipgroup) val chipGroup = view.findViewById<ChipGroup>(R.id.filename_suggested_chipgroup)
val chips = mutableListOf<Chip>() val chips = mutableListOf<Chip>()
val instances = InfoUtil(context).getPipedInstances().ifEmpty { return@launch } val instances = PipedApiUtil(context).getPipedInstances().ifEmpty { return@launch }
instances.forEach { s -> instances.forEach { s ->
val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
tmp.text = s tmp.text = s

View file

@ -0,0 +1,21 @@
package com.deniscerri.ytdl.util.extractors
import java.util.ArrayList
object GoogleApiUtil {
fun getSearchSuggestions(query: String): ArrayList<String> {
val url = "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&q=$query"
val res = NetworkUtil.genericArrayRequest(url)
if (res.length() == 0) return ArrayList()
val suggestionList = ArrayList<String>()
try {
for (i in 0 until res.getJSONArray(1).length()) {
val item = res.getJSONArray(1).getString(i)
suggestionList.add(item)
}
} catch (ignored: Exception) {
ignored.printStackTrace()
}
return suggestionList
}
}

View file

@ -0,0 +1,71 @@
package com.deniscerri.ytdl.util.extractors
import android.util.Log
import org.json.JSONArray
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
object NetworkUtil {
fun genericRequest(url: String): JSONObject {
Log.e(NetworkUtil.toString(), url)
val reader: BufferedReader
var line: String?
val responseContent = StringBuilder()
val conn: HttpURLConnection
var json = JSONObject()
try {
val req = URL(url)
conn = req.openConnection() as HttpURLConnection
conn.requestMethod = "GET"
conn.connectTimeout = 3000
conn.readTimeout = 5000
if (conn.responseCode < 300) {
reader = BufferedReader(InputStreamReader(conn.inputStream))
while (reader.readLine().also { line = it } != null) {
responseContent.append(line)
}
reader.close()
json = JSONObject(responseContent.toString())
if (json.has("error")) {
throw Exception()
}
}
conn.disconnect()
} catch (e: Exception) {
Log.e(NetworkUtil.toString(), e.toString())
}
return json
}
fun genericArrayRequest(url: String): JSONArray {
Log.e(NetworkUtil.toString(), url)
val reader: BufferedReader
var line: String?
val responseContent = StringBuilder()
val conn: HttpURLConnection
var json = JSONArray()
try {
val req = URL(url)
conn = req.openConnection() as HttpURLConnection
conn.requestMethod = "GET"
conn.connectTimeout = 3000
conn.readTimeout = 5000
if (conn.responseCode < 300) {
reader = BufferedReader(InputStreamReader(conn.inputStream))
while (reader.readLine().also { line = it } != null) {
responseContent.append(line)
}
reader.close()
json = JSONArray(responseContent.toString())
}
conn.disconnect()
} catch (e: Exception) {
Log.e(NetworkUtil.toString(), e.toString())
}
return json
}
}

View file

@ -0,0 +1,330 @@
package com.deniscerri.ytdl.util.extractors
import android.content.Context
import android.content.SharedPreferences
import android.text.Html
import android.util.Log
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDLException
import kotlinx.coroutines.delay
import org.json.JSONException
import org.json.JSONObject
import java.util.ArrayList
import java.util.Locale
import kotlin.coroutines.cancellation.CancellationException
class PipedApiUtil(private val context: Context) {
private var sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val countryCode = sharedPreferences.getString("locale", "")!!.ifEmpty { "US" }
private val defaultPipedURL = "https://pipedapi.kavin.rocks/"
private val pipedURL = sharedPreferences.getString("piped_instance", "")!!.ifEmpty { defaultPipedURL }.removeSuffix("/")
fun getPipedInstances() : List<String> {
kotlin.runCatching {
val res = NetworkUtil.genericArrayRequest("https://piped-instances.kavin.rocks/")
val list = mutableListOf<String>()
for (i in 0 until res.length()) {
val element = res.getJSONObject(i)
list.add(element.getString("api_url"))
}
return list
}
return listOf()
}
fun getVideoData(url : String) : Result<List<ResultItem>> {
val id = getIDFromYoutubeURL(url)
val res = NetworkUtil.genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
return Result.failure(Throwable())
}
val vid = createVideoFromPipedJSON(res, url) ?: return Result.failure(Throwable())
return Result.success(listOf(vid))
}
fun getFormats(url: String) : Result<List<Format> > {
try {
val id = getIDFromYoutubeURL(url)
val res = NetworkUtil.genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
return Result.failure(Throwable())
}else {
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id", true)
return Result.success(item!!.formats)
}
}catch(e: Exception) {
println(e)
if (e is CancellationException) throw e
return Result.failure(e)
}
}
fun getFormatsForAll(urls: List<String>, progress: (progress: ResultViewModel.MultipleFormatProgress) -> Unit) : Result<MutableList<MutableList<Format>>> {
return kotlin.runCatching {
val formatCollection = mutableListOf<MutableList<Format>>()
urls.forEach { url ->
val id = getIDFromYoutubeURL(url)
val res = NetworkUtil.genericRequest("$pipedURL/streams/$id")
createVideoFromPipedJSON(res, url).apply {
formatCollection.add(this!!.formats)
progress(
ResultViewModel.MultipleFormatProgress(url, this.formats)
)
}
}
return Result.success(formatCollection)
}.onFailure {
return Result.failure(it)
}
}
@Throws(JSONException::class)
fun search(query: String): Result<ArrayList<ResultItem>> {
val items = arrayListOf<ResultItem>()
val data = NetworkUtil.genericRequest("$pipedURL/search?q=$query&filter=videos&region=${countryCode}")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return Result.failure(Throwable())
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
if (element.getInt("duration") == -1) continue
element.put("uploader", element.getString("uploaderName"))
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
if (v == null || v.thumb.isEmpty()) {
continue
}
items.add(v)
}
return Result.success(items)
}
@Throws(JSONException::class)
fun searchMusic(query: String): Result<ArrayList<ResultItem>> {
val items = arrayListOf<ResultItem>()
val data = NetworkUtil.genericRequest("$pipedURL/search?q=$query=&filter=music_songs&region=${countryCode}")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return Result.failure(Throwable())
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
if (element.getInt("duration") == -1) continue
element.put("uploader", element.getString("uploaderName"))
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
if (v == null || v.thumb.isEmpty()) {
continue
}
items.add(v)
}
return Result.success(items)
}
fun getStreamingUrlAndChapters(url: String) : Result<Pair<List<String>, List<ChapterItem>?>> {
val id = getIDFromYoutubeURL(url)
val res = NetworkUtil.genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
throw Exception()
}else{
val item = createVideoFromPipedJSON(res, url)
if (item!!.urls.isBlank()) return Result.failure(Throwable())
val urls = item.urls.split(",")
val chapters = item.chapters
return Result.success(Pair(urls, chapters))
}
}
suspend fun getPlaylistData(id: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> {
val totalItems = mutableListOf<ResultItem>()
val nextPageToken = ""
var playlistName = ""
while (true) {
val items = mutableListOf<ResultItem>()
var url = ""
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
else """$pipedURL/nextpage/playlists/$id?nextpage=${
nextPageToken.replace(
"&prettyPrint",
"%26prettyPrint"
)
}"""
println(url)
val res = NetworkUtil.genericRequest(url)
if (!res.has("relatedStreams")) throw Exception()
val dataArray = res.getJSONArray("relatedStreams")
val nextpage = res.getString("nextpage")
val isMixPlaylist = nextPageToken.isBlank() && res.getInt("videos") < 0
if (isMixPlaylist) throw YoutubeDLException("This playlist type is unviewable.")
for (i in 0 until dataArray.length()) {
kotlin.runCatching {
val obj = dataArray.getJSONObject(i)
createVideoFromPipedJSON(
obj,
"https://youtube.com" + obj.getString("url")
)?.apply {
playlistTitle = playlistName.ifEmpty { runCatching { res.getString("name") }.getOrElse { "" } }
playlistURL = "https://www.youtube.com/playlist?list=$id"
items.add(this)
if (playlistTitle.isNotBlank() && playlistName.isNotBlank()) {
playlistName = playlistTitle
}
}
}
}
progress(items)
delay(1000)
totalItems.addAll(items)
if (nextpage == "null") break
}
return Result.success(totalItems)
}
fun getTrending(): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val url = "$pipedURL/trending?region=${countryCode}"
val res = NetworkUtil.genericArrayRequest(url)
for (i in 0 until res.length()) {
val element = res.getJSONObject(i)
if (element.getInt("duration") < 0) continue
element.put("uploader", element.getString("uploaderName"))
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
if (v == null || v.thumb.isEmpty()) continue
items.add(v)
}
return items
}
private fun createVideoFromPipedJSON(obj: JSONObject, url: String, ignoreFormatPreference : Boolean = false): ResultItem? {
var video: ResultItem? = null
try {
val id = getIDFromYoutubeURL(url)
val title = Html.fromHtml(obj.getString("title").toString()).toString()
val author = try {
Html.fromHtml(obj.getString("uploader").toString()).toString()
}catch (e: Exception){
Html.fromHtml(obj.getString("uploaderName").toString()).toString()
}.removeSuffix(" - Topic")
val duration = obj.getInt("duration").toStringDuration(Locale.US)
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val formats : ArrayList<Format> = ArrayList()
if(sharedPreferences.getString("formats_source", "yt-dlp") == "piped" || ignoreFormatPreference){
if (obj.has("audioStreams")){
val formatsInJSON = obj.getJSONArray("audioStreams")
for (f in 0 until formatsInJSON.length()){
val format = formatsInJSON.getJSONObject(f)
if (format.getInt("bitrate") == 0) continue
val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{
formatObj.acodec = format.getString("codec")
formatObj.asr = format.getString("quality")
if (! format.getString("audioTrackName").equals("null", ignoreCase = true)){
formatObj.format_note = format.getString("audioTrackName") + " Audio, " + formatObj.format_note
}else{
formatObj.format_note = formatObj.format_note + " Audio"
}
if (!formatObj.tbr.isNullOrBlank()){
formatObj.tbr = (formatObj.tbr!!.toInt() / 1000).toString() + "k"
}
}catch (e: Exception) {
e.printStackTrace()
}
formats.add(formatObj)
}
}
if (obj.has("videoStreams")){
val formatsInJSON = obj.getJSONArray("videoStreams")
for (f in 0 until formatsInJSON.length()){
val format = formatsInJSON.getJSONObject(f)
if (format.getInt("bitrate") == 0) continue
val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{
formatObj.vcodec = format.getString("codec")
if (!formatObj.tbr.isNullOrBlank()){
formatObj.tbr = (formatObj.tbr!!.toInt() / 1000).toString() + "k"
}
}catch (e: Exception) {
e.printStackTrace()
}
formats.add(formatObj)
}
}
formats.groupBy { it.format_id }.forEach {
if (it.value.count() > 1) {
it.value.filter { f-> !f.format_note.contains("original", true) }.forEachIndexed { index, format -> format.format_id = format.format_id.split("-")[0] + "-${index}" }
val defaultLang = it.value.find { f -> f.format_note.contains("original", true) }
defaultLang?.format_id = (defaultLang?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}"
}
}
formats.sortByDescending { it.filesize }
}
val chapters = ArrayList<ChapterItem>()
if (obj.has("chapters") && obj.getJSONArray("chapters").length() > 0){
val chaptersJArray = obj.getJSONArray("chapters")
for (c in 0 until chaptersJArray.length()){
val chapter = chaptersJArray.getJSONObject(c)
val end = if (c == chaptersJArray.length() - 1) obj.getInt("duration") else chaptersJArray.getJSONObject(c+1).getInt("start")
val item = ChapterItem(chapter.getInt("start").toLong(), end.toLong(), chapter.getString("title"))
chapters.add(item)
}
}
video = ResultItem(0,
url,
title,
author,
duration,
thumb,
"youtube",
"",
formats,
if (obj.has("hls") && obj.getString("hls") != "null") obj.getString("hls") else "",
chapters
)
} catch (e: Exception) {
Log.e("PipedAPIUtil", e.toString())
}
return video
}
private fun getIDFromYoutubeURL(inputQuery: String) : String {
var el: Array<String?> =
inputQuery.split("/".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
var query = el[el.size - 1]
if (query!!.contains("watch?v=")) {
query = query.substring(8)
}
el = query.split("&".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
query = el[0]
el = query!!.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
query = el[0]
return query!!
}
}

View file

@ -0,0 +1,139 @@
package com.deniscerri.ytdl.util.extractors
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.database.models.ResultItem
import org.json.JSONException
import org.json.JSONObject
import java.util.ArrayList
import java.util.Locale
class YoutubeApiUtil(context: Context) {
private var sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val countryCode = sharedPreferences.getString("locale", "")!!.ifEmpty { "US" }
@Throws(JSONException::class)
fun getTrending(): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val key = sharedPreferences.getString("api_key", "")!!
val url = "https://www.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&videoCategoryId=10&regionCode=${countryCode}&maxResults=25&key=$key"
//short data
val res = NetworkUtil.genericRequest(url)
//extra data from the same videos
val contentDetails =
NetworkUtil.genericRequest("https://www.googleapis.com/youtube/v3/videos?part=contentDetails&chart=mostPopular&videoCategoryId=10&regionCode=${countryCode}&maxResults=25&key=$key")
if (!contentDetails.has("items")) return ArrayList()
val dataArray = res.getJSONArray("items")
val extraDataArray = contentDetails.getJSONArray("items")
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
val snippet = element.getJSONObject("snippet")
var duration = extraDataArray.getJSONObject(i).getJSONObject("contentDetails")
.getString("duration")
duration = formatDuration(duration)
snippet.put("videoID", element.getString("id"))
snippet.put("duration", duration)
fixThumbnail(snippet)
val v = createVideofromJSON(snippet)
if (v == null || v.thumb.isEmpty()) {
continue
}
items.add(v)
}
return items
}
private fun createVideofromJSON(obj: JSONObject): ResultItem? {
var video: ResultItem? = null
try {
val id = obj.getString("videoID")
val title = obj.getString("title").toString()
val author = obj.getString("channelTitle").toString()
val duration = obj.getString("duration")
val thumb = obj.getString("thumb")
val url = "https://www.youtube.com/watch?v=$id"
video = ResultItem(0,
url,
title,
author,
duration,
thumb,
"youtube",
"",
ArrayList(),
"",
ArrayList()
)
} catch (e: Exception) {
Log.e("YoutubeApiUtil", e.toString())
}
return video
}
private fun formatDuration(dur: String): String {
var badDur = dur
if (dur == "P0D") {
return "LIVE"
}
var hours = false
var duration = ""
badDur = badDur.substring(2)
if (badDur.contains("H")) {
hours = true
duration += String.format(
Locale.getDefault(),
"%02d",
badDur.substring(0, badDur.indexOf("H")).toInt()
) + ":"
badDur = badDur.substring(badDur.indexOf("H") + 1)
}
if (badDur.contains("M")) {
duration += String.format(
Locale.getDefault(),
"%02d",
badDur.substring(0, badDur.indexOf("M")).toInt()
) + ":"
badDur = badDur.substring(badDur.indexOf("M") + 1)
} else if (hours) duration += "00:"
if (badDur.contains("S")) {
if (duration.isEmpty()) duration = "00:"
duration += String.format(
Locale.getDefault(),
"%02d",
badDur.substring(0, badDur.indexOf("S")).toInt()
)
} else {
duration += "00"
}
if (duration == "00:00") {
duration = ""
}
return duration
}
private fun fixThumbnail(o: JSONObject): JSONObject {
var imageURL = ""
try {
val thumbs = o.getJSONObject("thumbnails")
imageURL = thumbs.getJSONObject("maxres").getString("url")
} catch (e: Exception) {
try {
val thumbs = o.getJSONObject("thumbnails")
imageURL = thumbs.getJSONObject("high").getString("url")
} catch (u: Exception) {
try {
val thumbs = o.getJSONObject("thumbnails")
imageURL = thumbs.getJSONObject("default").getString("url")
} catch (ignored: Exception) {
}
}
}
try {
o.put("thumb", imageURL)
} catch (ignored: Exception) {
}
return o
}
}

View file

@ -0,0 +1,57 @@
package com.deniscerri.ytdl.util.extractors.newpipe
import com.google.common.net.HttpHeaders.USER_AGENT
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import org.schabi.newpipe.extractor.downloader.Downloader
import org.schabi.newpipe.extractor.downloader.Request
import org.schabi.newpipe.extractor.downloader.Response
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
import java.util.concurrent.TimeUnit
class NewPipeDownloaderImpl(builder: OkHttpClient.Builder) : Downloader() {
private var client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build()
override fun execute(request: Request): Response {
val httpMethod = request.httpMethod()
val url = request.url()
val headers = request.headers()
val dataToSend = request.dataToSend()
var requestBody: RequestBody? = null
if (dataToSend != null) {
requestBody = RequestBody.create(null, dataToSend)
}
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
.method(httpMethod, requestBody).url(url)
.addHeader("User-Agent", USER_AGENT)
for ((headerName, headerValueList) in headers) {
if (headerValueList.size > 1) {
requestBuilder.removeHeader(headerName)
for (headerValue in headerValueList) {
requestBuilder.addHeader(headerName, headerValue)
}
} else if (headerValueList.size == 1) {
requestBuilder.header(headerName, headerValueList[0])
}
}
val response: okhttp3.Response = client.newCall(requestBuilder.build()).execute()
if (response.code == 429) {
response.close()
throw ReCaptchaException("reCaptcha Challenge requested", url)
}
val body = response.body
val responseBodyToReturn = body.string()
val latestUrl = response.request.url.toString()
return Response(
response.code, response.message, response.headers.toMultimap(),
responseBodyToReturn, latestUrl
)
}
}

View file

@ -0,0 +1,440 @@
package com.deniscerri.ytdl.util.extractors.newpipe
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.google.gson.Gson
import kotlinx.serialization.Serializer
import okhttp3.OkHttpClient
import org.json.JSONException
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.Page
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
import org.schabi.newpipe.extractor.kiosk.KioskInfo
import org.schabi.newpipe.extractor.kiosk.KioskList
import org.schabi.newpipe.extractor.linkhandler.LinkHandler
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
import org.schabi.newpipe.extractor.localization.ContentCountry
import org.schabi.newpipe.extractor.localization.Localization
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
import org.schabi.newpipe.extractor.search.SearchExtractor
import org.schabi.newpipe.extractor.search.SearchInfo
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMusicSearchExtractor
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeSearchExtractor
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.utils.ExtractorHelper
import java.util.Locale
import kotlin.coroutines.cancellation.CancellationException
class NewPipeUtil(context: Context) {
private var sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val countryCode = sharedPreferences.getString("locale", "")!!.ifEmpty { "US" }
private val language = sharedPreferences.getString("app_language", "")!!.ifEmpty { "en" }
init {
NewPipe.init(NewPipeDownloaderImpl(OkHttpClient.Builder()), Localization(language, countryCode))
}
fun getVideoData(url : String) : Result<List<ResultItem>> {
try {
val streamInfo = StreamInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId), url)
val vid = createVideoFromStream(streamInfo, url) ?: return Result.failure(Throwable())
return Result.success(listOf(vid))
}catch (e: Exception) {
return Result.failure(e)
}
}
fun getFormats(url: String) : Result<List<Format> > {
try {
val streamInfo = StreamInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId), url)
val vid = createVideoFromStream(streamInfo, url)
return Result.success(vid!!.formats)
}catch(e: Exception) {
println(e)
if (e is CancellationException) throw e
return Result.failure(e)
}
}
fun getFormatsForAll(urls: List<String>, progress: (progress: ResultViewModel.MultipleFormatProgress) -> Unit) : Result<MutableList<MutableList<Format>>> {
return kotlin.runCatching {
val formatCollection = mutableListOf<MutableList<Format>>()
urls.forEach { url ->
val streamInfo = StreamInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId), url)
createVideoFromStream(streamInfo, url).apply {
formatCollection.add(this!!.formats)
progress(ResultViewModel.MultipleFormatProgress(url, this.formats))
}
}
return Result.success(formatCollection)
}.onFailure {
return Result.failure(it)
}
}
@Throws(JSONException::class)
fun search(query: String): Result<ArrayList<ResultItem>> {
try {
val items = arrayListOf<ResultItem>()
val res = SearchInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId),
NewPipe.getService(ServiceList.YouTube.serviceId)
.searchQHFactory
.fromQuery(query, listOf(YoutubeSearchQueryHandlerFactory.VIDEOS), ""))
if (res.relatedItems.isEmpty()) return Result.failure(Throwable())
for (i in 0 until res.relatedItems.size) {
val element = res.relatedItems[i]
if (element is StreamInfoItem) {
if (element.duration <= 0) continue
val v = createVideoFromStreamInfoItem(element, element.url) ?: continue
items.add(v)
}
}
return Result.success(items)
}catch (e: Exception){
return Result.failure(e)
}
}
@Throws(JSONException::class)
fun searchMusic(query: String): Result<ArrayList<ResultItem>> {
try {
val items = arrayListOf<ResultItem>()
val res = SearchInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId),
NewPipe.getService(ServiceList.YouTube.serviceId)
.searchQHFactory
.fromQuery(query, listOf(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS), ""))
if (res.relatedItems.isEmpty()) return Result.failure(Throwable())
for (i in 0 until res.relatedItems.size) {
val element = res.relatedItems[i]
if (element is StreamInfoItem) {
if (element.duration <= 0) continue
val v = createVideoFromStreamInfoItem(element, element.url) ?: continue
items.add(v)
}
}
return Result.success(items)
}catch (e: Exception){
return Result.failure(e)
}
}
fun getStreamingUrlAndChapters(url: String) : Result<Pair<List<String>, List<ChapterItem>?>> {
try {
val streamInfo = StreamInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId), url)
val item = createVideoFromStream(streamInfo, url)
if (item!!.urls.isBlank()) return Result.failure(Throwable())
val urls = item.urls.split(",")
val chapters = item.chapters
return Result.success(Pair(urls, chapters))
}catch (e: Exception) {
return Result.failure(e)
}
}
fun getChannelData(url: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> {
try {
val req = ChannelInfo.getInfo(ServiceList.YouTube, url)
println(Gson().toJson(req))
val items = mutableListOf<ResultItem>()
for (tab in req.tabs) {
if (listOf("videos", "shorts", "livestreams").contains(tab.contentFilters[0])) {
val tabInfo = ChannelTabInfo.getInfo(ServiceList.YouTube, tab)
val tmp = getChannelTabData(tab, tabInfo, req.name, "${url}/${tabInfo.url.split("/").last()}") {
progress(it)
}
if (tmp.isFailure) continue
else items.addAll(tmp.getOrNull()!!)
}
}
return Result.success(items)
}catch (e: Exception) {
return Result.failure(e)
}
}
private fun getChannelTabData(linkHandler: ListLinkHandler, tabInfo: ChannelTabInfo, channelName: String, playlistURL: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> {
try {
val totalItems = mutableListOf<ResultItem>()
var nextPage : Page? = null
var playlistName = ""
while (true) {
val items = mutableListOf<ResultItem>()
val req = if (nextPage == null) {
if (tabInfo.hasNextPage()) {
nextPage = tabInfo.nextPage
}
playlistName = "$channelName - ${tabInfo.name}"
tabInfo.relatedItems.toList()
} else {
val tmp = ChannelTabInfo.getMoreItems(ServiceList.YouTube, linkHandler, nextPage)
nextPage = if (tmp.hasNextPage()) tmp.nextPage else null
tmp.items.toList()
}
if (req.isEmpty()) return Result.failure(Throwable())
for (element in req) {
if (element is StreamInfoItem) {
if (element.duration <= 0) continue
val v = createVideoFromStreamInfoItem(element, element.url) ?: continue
v.apply {
playlistTitle = playlistName
this.playlistURL = playlistURL
items.add(this)
}
}
}
totalItems.addAll(items)
progress(items)
if (nextPage == null || items.isEmpty()) break
}
return Result.success(totalItems)
}catch (e: Exception) {
return Result.failure(e)
}
}
fun getPlaylistData(playlistURL: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> {
try {
val totalItems = mutableListOf<ResultItem>()
var nextPage : Page? = null
var playlistName = ""
while (true) {
val items = mutableListOf<ResultItem>()
val req = if (nextPage == null) {
val tmp = PlaylistInfo.getInfo(ServiceList.YouTube, playlistURL)
if (tmp.hasNextPage()) {
nextPage = tmp.nextPage
}
playlistName = tmp.name
tmp.relatedItems.toList()
} else {
val tmp = PlaylistInfo.getMoreItems(ServiceList.YouTube, playlistURL, nextPage)
nextPage = if (tmp.hasNextPage()) tmp.nextPage else null
tmp.items.toList()
}
if (req.isEmpty()) return Result.failure(Throwable())
for (element in req) {
if (element is StreamInfoItem) {
if (element.duration <= 0) continue
val v = createVideoFromStreamInfoItem(element, element.url) ?: continue
v.apply {
playlistTitle = playlistName
this.playlistURL = playlistURL
items.add(this)
}
}
}
totalItems.addAll(items)
progress(items)
if (nextPage == null || items.isEmpty()) break
}
return Result.success(totalItems)
}catch (e: Exception) {
return Result.failure(e)
}
}
fun getTrending(): ArrayList<ResultItem> {
try {
val items = arrayListOf<ResultItem>()
val info = KioskInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId), "https://www.youtube.com/feed/trending")
if (info.relatedItems.isEmpty()) return arrayListOf()
for (i in 0 until info.relatedItems.size) {
val element = info.relatedItems[i]
if (element is StreamInfoItem) {
if (element.duration <= 0) continue
val v = createVideoFromStreamInfoItem(element, element.url) ?: continue
items.add(v)
}
}
return items
}catch (err: Exception) {
return arrayListOf()
}
}
private fun createVideoFromStreamInfoItem(stream: StreamInfoItem, url: String) : ResultItem? {
var video: ResultItem? = null
try {
val id = getIDFromYoutubeURL(url)
val title = stream.name
val author = stream.uploaderName.removeSuffix(" - Topic")
val duration = stream.duration.toInt().toStringDuration(Locale.US)
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
video = ResultItem(0,
url,
title,
author,
duration,
thumb,
"youtube",
"",
ArrayList(),
"",
ArrayList()
)
} catch (e: Exception) {
Log.e("NewPipeUtil", e.toString())
}
return video
}
private fun createVideoFromStream(stream: StreamInfo, url: String, ignoreFormatPreference : Boolean = false): ResultItem? {
var video: ResultItem? = null
try {
val id = getIDFromYoutubeURL(url)
val title = stream.name
val author = stream.uploaderName.removeSuffix(" - Topic")
val duration = stream.duration.toInt().toStringDuration(Locale.US)
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val formats : ArrayList<Format> = ArrayList()
if(sharedPreferences.getString("formats_source", "yt-dlp") == "piped" || ignoreFormatPreference){
if (stream.audioStreams.isNotEmpty()){
for (f in 0 until stream.audioStreams.size){
val it = stream.audioStreams[f]
if (it.bitrate == 0) continue
val formatObj = Format(
format_id = it.itag.toString(),
container = it.format!!.name,
acodec = it.codec,
filesize = it.itagItem!!.contentLength,
format_note = (it.audioTrackName ?: (it.itagItem?.getResolutionString() ?: ((it.bitrate / 1000).toString() + "k"))) + " Audio",
lang = it.audioLocale?.language,
asr = it.itagItem!!.sampleRate.toString(),
url = it.content,
tbr = (it.bitrate / 1000).toString() + "k"
)
formats.add(formatObj)
}
}
if (stream.videoStreams.isNotEmpty()){
for (f in 0 until stream.videoStreams.size){
val it = stream.videoStreams[f]
if (it.bitrate == 0) continue
val formatObj = Format(
format_id = it.itag.toString(),
container = it.format!!.name,
vcodec = it.codec,
format_note = it.itagItem!!.getResolutionString() ?: it.quality,
filesize = it.itagItem!!.contentLength,
url = it.content,
tbr = (it.bitrate / 1000).toString() + "k"
)
formats.add(formatObj)
}
}
if (stream.videoOnlyStreams.isNotEmpty()){
for (f in 0 until stream.videoOnlyStreams.size){
val it = stream.videoOnlyStreams[f]
if (it.bitrate == 0) continue
val formatObj = Format(
format_id = it.itag.toString(),
container = it.format!!.name,
vcodec = it.codec,
format_note = it.itagItem!!.getResolutionString() ?: it.quality,
filesize = it.itagItem!!.contentLength,
url = it.content,
tbr = (it.bitrate / 1000).toString() + "k"
)
formats.add(formatObj)
}
}
formats.groupBy { it.format_id }.forEach {
if (it.value.count() > 1) {
it.value.filter { f-> !f.format_note.contains("original", true) }.forEachIndexed { index, format -> format.format_id = format.format_id.split("-")[0] + "-${index}" }
val defaultLang = it.value.find { f -> f.format_note.contains("original", true) }
defaultLang?.format_id = (defaultLang?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}"
}
}
formats.sortByDescending { it.filesize }
}
val chapters = ArrayList<ChapterItem>()
if (stream.streamSegments.isNotEmpty()){
for (c in 0 until stream.streamSegments.size){
val chapter = stream.streamSegments[c]
val end = if (c == stream.streamSegments.size - 1) stream.duration.toInt() else stream.streamSegments[c+1].startTimeSeconds
val item = ChapterItem(chapter.startTimeSeconds.toLong(), end.toLong(), chapter.title)
chapters.add(item)
}
}
video = ResultItem(0,
url,
title,
author,
duration,
thumb,
"youtube",
"",
formats,
if (stream.hlsUrl.isNotBlank() && stream.hlsUrl != "null") stream.hlsUrl else "",
chapters
)
} catch (e: Exception) {
Log.e("NewPipeUtil", e.toString())
}
return video
}
private fun getIDFromYoutubeURL(inputQuery: String) : String {
var el: Array<String?> =
inputQuery.split("/".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
var query = el[el.size - 1]
if (query!!.contains("watch?v=")) {
query = query.substring(8)
}
el = query.split("&".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
query = el[0]
el = query!!.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
query = el[0]
return query!!
}
}

View file

@ -2,46 +2,12 @@ package com.deniscerri.ytdl.work
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Handler
import android.os.Looper
import android.util.DisplayMetrics
import android.util.Log
import android.widget.Toast
import androidx.navigation.NavDeepLinkBuilder
import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo
import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.await
import androidx.work.workDataOf
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.models.LogItem
import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.LogRepository
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.util.Extensions.getMediaDuration
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Locale
import kotlin.random.Random
class CancelScheduledDownloadWorker( class CancelScheduledDownloadWorker(

View file

@ -12,7 +12,6 @@ import android.os.Looper
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.navigation.NavDeepLinkBuilder
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
@ -20,7 +19,6 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import androidx.work.await import androidx.work.await
import androidx.work.workDataOf
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.App import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.MainActivity import com.deniscerri.ytdl.MainActivity
@ -34,8 +32,8 @@ import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.util.Extensions.getMediaDuration import com.deniscerri.ytdl.util.Extensions.getMediaDuration
import com.deniscerri.ytdl.util.Extensions.toStringDuration import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -46,7 +44,6 @@ import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.EventBus
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale
import kotlin.random.Random
class DownloadWorker( class DownloadWorker(
@ -58,11 +55,10 @@ class DownloadWorker(
if (isStopped) return Result.success() if (isStopped) return Result.success()
val notificationUtil = NotificationUtil(App.instance) val notificationUtil = NotificationUtil(App.instance)
val infoUtil = InfoUtil(context) val ytdlpUtil = YTDLPUtil(context)
val dbManager = DBManager.getInstance(context) val dbManager = DBManager.getInstance(context)
val dao = dbManager.downloadDao val dao = dbManager.downloadDao
val historyDao = dbManager.historyDao val historyDao = dbManager.historyDao
val resultDao = dbManager.resultDao
val logRepo = LogRepository(dbManager.logDao) val logRepo = LogRepository(dbManager.logDao)
val resultRepo = ResultRepository(dbManager.resultDao, context) val resultRepo = ResultRepository(dbManager.resultDao, context)
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
@ -135,7 +131,7 @@ class DownloadWorker(
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite() val noCache = !sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()
val request = infoUtil.buildYoutubeDLRequest(downloadItem) val request = ytdlpUtil.buildYoutubeDLRequest(downloadItem)
downloadItem.status = DownloadRepository.Status.Active.toString() downloadItem.status = DownloadRepository.Status.Active.toString()
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
delay(1500) delay(1500)
@ -158,7 +154,7 @@ class DownloadWorker(
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !downloadItem.incognito val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !downloadItem.incognito
val commandString = infoUtil.parseYTDLRequestString(request) val commandString = ytdlpUtil.parseYTDLRequestString(request)
val initialLogDetails = "Downloading:\n" + val initialLogDetails = "Downloading:\n" +
"Title: ${downloadItem.title}\n" + "Title: ${downloadItem.title}\n" +
"URL: ${downloadItem.url}\n" + "URL: ${downloadItem.url}\n" +
@ -258,9 +254,6 @@ class DownloadWorker(
add("txt") add("txt")
} }
finalPaths = finalPaths.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }.toMutableList() finalPaths = finalPaths.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }.toMutableList()
if (finalPaths.isEmpty()){
finalPaths = mutableListOf(context.getString(R.string.unfound_file))
}
FileUtil.deleteConfigFiles(request) FileUtil.deleteConfigFiles(request)
//put download in history //put download in history
@ -270,39 +263,40 @@ class DownloadWorker(
Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show() Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
}, 100) }, 100)
}else{ }else{
val unixTime = System.currentTimeMillis() / 1000 if (finalPaths.isNotEmpty()) {
finalPaths.first().apply { val unixTime = System.currentTimeMillis() / 1000
val file = File(this) finalPaths.first().apply {
var duration = downloadItem.duration val file = File(this)
val d = file.getMediaDuration(context) var duration = downloadItem.duration
if (d > 0) duration = d.toStringDuration(Locale.US) val d = file.getMediaDuration(context)
if (d > 0) duration = d.toStringDuration(Locale.US)
downloadItem.format.filesize = file.length() downloadItem.format.filesize = file.length()
downloadItem.format.container = file.extension downloadItem.format.container = file.extension
downloadItem.duration = duration downloadItem.duration = duration
}
val historyItem = HistoryItem(0,
downloadItem.url,
downloadItem.title,
downloadItem.author,
downloadItem.duration,
downloadItem.thumb,
downloadItem.type,
unixTime,
finalPaths,
downloadItem.website,
downloadItem.format,
downloadItem.id,
commandString)
historyDao.insert(historyItem)
} }
val historyItem = HistoryItem(0,
downloadItem.url,
downloadItem.title,
downloadItem.author,
downloadItem.duration,
downloadItem.thumb,
downloadItem.type,
unixTime,
finalPaths,
downloadItem.website,
downloadItem.format,
downloadItem.id,
commandString)
historyDao.insert(historyItem)
} }
} }
notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) notificationUtil.cancelDownloadNotification(downloadItem.id.toInt())
notificationUtil.createDownloadFinished( notificationUtil.createDownloadFinished(
downloadItem.id, downloadItem.title, downloadItem.type, if (finalPaths.first().equals(context.getString(R.string.unfound_file))) null else finalPaths, resources downloadItem.id, downloadItem.title, downloadItem.type, if (finalPaths.isEmpty()) null else finalPaths, resources
) )
// if (wasQuickDownloaded && createResultItem){ // if (wasQuickDownloaded && createResultItem){

View file

@ -21,8 +21,8 @@ import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdl.database.repository.ResultRepository import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.util.Extensions.calculateNextTimeForObserving import com.deniscerri.ytdl.util.Extensions.calculateNextTimeForObserving
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -42,8 +42,8 @@ class ObserveSourceWorker(
val resultsRepo = ResultRepository(dbManager.resultDao, App.instance) val resultsRepo = ResultRepository(dbManager.resultDao, App.instance)
val historyRepo = HistoryRepository(dbManager.historyDao) val historyRepo = HistoryRepository(dbManager.historyDao)
val downloadRepo = DownloadRepository(dbManager.downloadDao) val downloadRepo = DownloadRepository(dbManager.downloadDao)
val ytdlpUtil = YTDLPUtil(context)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val infoUtil = InfoUtil(context)
val item = repo.getByID(sourceID) val item = repo.getByID(sourceID)
if (item.status == ObserveSourcesRepository.SourceStatus.STOPPED){ if (item.status == ObserveSourcesRepository.SourceStatus.STOPPED){
@ -56,7 +56,7 @@ class ObserveSourceWorker(
setForegroundAsync(foregroundInfo) setForegroundAsync(foregroundInfo)
val list = kotlin.runCatching { val list = kotlin.runCatching {
infoUtil.getFromYTDL(item.url) ytdlpUtil.getFromYTDL(item.url)
}.onFailure { }.onFailure {
Log.e("observe", it.toString()) Log.e("observe", it.toString())
}.getOrElse { listOf() } }.getOrElse { listOf() }
@ -142,8 +142,8 @@ class ObserveSourceWorker(
items.forEach { items.forEach {
it.status = DownloadRepository.Status.Queued.toString() it.status = DownloadRepository.Status.Queued.toString()
val currentCommand = infoUtil.buildYoutubeDLRequest(it) val currentCommand = ytdlpUtil.buildYoutubeDLRequest(it)
val parsedCurrentCommand = infoUtil.parseYTDLRequestString(currentCommand) val parsedCurrentCommand = ytdlpUtil.parseYTDLRequestString(currentCommand)
val existingDownload = activeAndQueuedDownloads.firstOrNull{d -> val existingDownload = activeAndQueuedDownloads.firstOrNull{d ->
d.id = 0 d.id = 0
d.logID = null d.logID = null

View file

@ -1,14 +1,13 @@
package com.deniscerri.ytdl.work package com.deniscerri.ytdl.work
import android.content.Context import android.content.Context
import android.util.Log
import androidx.work.ForegroundInfo import androidx.work.ForegroundInfo
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.deniscerri.ytdl.App import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.NotificationUtil
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -21,7 +20,7 @@ class UpdateMultipleDownloadsFormatsWorker(
val dbManager = DBManager.getInstance(context) val dbManager = DBManager.getInstance(context)
val dao = dbManager.downloadDao val dao = dbManager.downloadDao
val resDao = dbManager.resultDao val resDao = dbManager.resultDao
val infoUtil = InfoUtil(context) val resultRepo = ResultRepository(resDao, context)
val vm = DownloadViewModel(App.instance) val vm = DownloadViewModel(App.instance)
val notificationUtil = NotificationUtil(context) val notificationUtil = NotificationUtil(context)
val ids = inputData.getLongArray("ids")!!.toMutableList() val ids = inputData.getLongArray("ids")!!.toMutableList()
@ -47,7 +46,7 @@ class UpdateMultipleDownloadsFormatsWorker(
runCatching { runCatching {
d.allFormats.clear() d.allFormats.clear()
d.allFormats.addAll(infoUtil.getFormats(d.url)) d.allFormats.addAll(resultRepo.getFormats(d.url))
d.format = vm.getFormat(d.allFormats,d.type) d.format = vm.getFormat(d.allFormats,d.type)
r?.formats?.clear() r?.formats?.clear()

View file

@ -146,23 +146,62 @@
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<RelativeLayout <RelativeLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior" app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <HorizontalScrollView
android:layout_width="match_parent" android:id="@+id/playlist_selection_chips_scrollview"
android:id="@+id/recyclerViewHome" android:layout_width="wrap_content"
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:visibility="gone"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:paddingHorizontal="10dp"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:selectionRequired="false"
android:id="@+id/playlist_selection_chips"
app:singleSelection="true"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/all"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/all"
android:tag="all"
android:checked="true"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerViewHome"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/playlist_selection_chips_scrollview"
android:clipToPadding="false" android:clipToPadding="false"
android:orientation="vertical"
android:paddingBottom="200dp" android:paddingBottom="200dp"
app:spanCount="2" android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/> app:spanCount="2" />
<com.facebook.shimmer.ShimmerFrameLayout <com.facebook.shimmer.ShimmerFrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"

View file

@ -142,12 +142,53 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<HorizontalScrollView
android:id="@+id/playlist_selection_chips_scrollview"
android:layout_width="wrap_content"
android:scrollbars="none"
android:visibility="gone"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:paddingHorizontal="10dp"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:selectionRequired="false"
android:id="@+id/playlist_selection_chips"
app:singleSelection="true"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/all"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/all"
android:tag="all"
android:minWidth="30dp"
android:checked="true"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/recyclerViewHome" android:id="@+id/recyclerViewHome"
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:layout_below="@id/playlist_selection_chips_scrollview"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="200dp" android:paddingBottom="200dp"
app:spanCount="3" app:spanCount="3"

View file

@ -150,12 +150,54 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<HorizontalScrollView
android:id="@+id/playlist_selection_chips_scrollview"
android:layout_width="wrap_content"
android:scrollbars="none"
android:visibility="gone"
app:layout_scrollFlags="scroll|enterAlways"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:paddingHorizontal="10dp"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:selectionRequired="false"
android:id="@+id/playlist_selection_chips"
app:singleSelection="true"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/all"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/all"
android:tag="all"
android:minWidth="30dp"
android:checked="true"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/recyclerViewHome" android:id="@+id/recyclerViewHome"
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:layout_below="@id/playlist_selection_chips_scrollview"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="200dp" android:paddingBottom="200dp"
app:spanCount="2" app:spanCount="2"

View file

@ -142,12 +142,53 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<HorizontalScrollView
android:id="@+id/playlist_selection_chips_scrollview"
android:layout_width="wrap_content"
android:scrollbars="none"
android:visibility="gone"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:paddingHorizontal="10dp"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:selectionRequired="false"
android:id="@+id/playlist_selection_chips"
app:singleSelection="true"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/all"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/all"
android:tag="all"
android:minWidth="30dp"
android:checked="true"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</HorizontalScrollView>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent" android:layout_width="match_parent"
android:id="@+id/recyclerViewHome" android:id="@+id/recyclerViewHome"
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:scrollbars="none" android:scrollbars="none"
android:layout_below="@id/playlist_selection_chips_scrollview"
android:clipToPadding="false" android:clipToPadding="false"
android:paddingBottom="200dp" android:paddingBottom="200dp"
app:spanCount="4" app:spanCount="4"

View file

@ -179,6 +179,47 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<HorizontalScrollView
android:id="@+id/playlist_selection_chips_scrollview"
android:layout_width="wrap_content"
android:scrollbars="none"
android:visibility="gone"
app:layout_scrollFlags="scroll|enterAlways"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="wrap_content"
android:paddingHorizontal="20dp"
android:layout_height="wrap_content">
<com.google.android.material.chip.ChipGroup
android:layout_width="wrap_content"
app:selectionRequired="false"
android:id="@+id/playlist_selection_chips"
app:singleSelection="true"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/all"
style="@style/Widget.Material3.Chip.Filter"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/all"
android:tag="all"
android:checked="true"
android:minWidth="30dp"
app:cornerRadius="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</com.google.android.material.chip.ChipGroup>
</LinearLayout>
</HorizontalScrollView>
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>

View file

@ -235,7 +235,7 @@
</array> </array>
<array name="formats_source"> <array name="formats_source">
<item>PIPED [Youtube]</item> <item>NewPipe</item>
<item>yt-dlp</item> <item>yt-dlp</item>
</array> </array>

View file

@ -5,7 +5,7 @@ buildscript {
ext { ext {
abiCodes = ['armeabi-v7a': 1, 'x86': 2, 'x86_64': 3, 'arm64-v8a': 4] abiCodes = ['armeabi-v7a': 1, 'x86': 2, 'x86_64': 3, 'arm64-v8a': 4]
// dependency versions // dependency versions
appCompatVer = '1.6.1' appCompatVer = '1.7.0'
junitVer = '4.13.2' junitVer = '4.13.2'
androidJunitVer = '1.1.5' androidJunitVer = '1.1.5'
espressoVer = '3.6.1' espressoVer = '3.6.1'
@ -17,20 +17,21 @@ buildscript {
youtubedlAndroidVer = "ed652962e5" youtubedlAndroidVer = "ed652962e5"
youtubedlAndroidVerNoPyCrypto = "97bee6c0e8" youtubedlAndroidVerNoPyCrypto = "97bee6c0e8"
oldytdlAndroidVer = "0.14.0" oldytdlAndroidVer = "0.14.0"
workVer = "2.9.0" workVer = "2.9.1"
composeVer = '1.4.2' composeVer = '1.4.2'
kotlinVer = "1.7.21" kotlinVer = "1.7.21"
coroutineVer = '1.7.3' coroutineVer = '1.7.3'
retrofitVer = "2.9.0" retrofitVer = "2.9.0"
kodeinVer = "7.16.0" kodeinVer = "7.16.0"
navVer = "2.7.7" navVer = "2.7.7"
media3_version = "1.3.1" media3_version = "1.4.0"
agp_version = '8.1.0' agp_version = '8.1.0'
agp_version1 = '7.4.2' agp_version1 = '7.4.2'
roomVer = '2.5.2' roomVer = '2.5.2'
} }
repositories { repositories {
mavenCentral() mavenCentral()
maven { url 'https://jitpack.io' }
google() google()
} }

View file

@ -0,0 +1,46 @@
# What's Changed
## Newpipe Extractor
Since Piped is currently broken and unusable, i implemented the NEWPIPE EXTRACTOR to replace it. This includes:
- video queries
- playlist queries
- trending
- searching
- youtube channel queries
- formats
The speed is as fast as piped and seems to be working. Will use that for the forseeable future, until further notice
## Home screen filtering
This is a feature request that is long overdue. When implementing newpipe extractor i also coded in proper youtube channel parsing. Now items have their respective channel playlist name. So the app will show you filter chips if you have multiple playlist available.
So you can filter between videos, shorts, livestreams and choose to just download those by long pressing etc etc.
In case newpipe fails and it defaults to yt-dlp, proper channel playlist parsing is not yet possible.
You can track the issue i have created here:
https://github.com/yt-dlp/yt-dlp/issues/10827
## Other fixes
- Fixed app selecting all playlist items even though user selected a few, if the user is parsing an instagram post. (they all had the same url)
- Fixed app not changing container label in the multiple download card when switching download type
- Fixed app not being able to return to home after clicking the download notification
- Added ability for the app to parse multiple urls at the same time. For now it will do 10 items at max
- Added ability to show "Continue Anyway" in the error dialog so it will keep showing the download card in case you want to save it for later or schedule it
- Added general subtitle variant codes with .*
- Fixed app not applying the subtitle language label in the subtitle configuration dialog
- Fixed app not showing the total format size when being in audio download type in the multiple downlod card
- Fixed app not navigating to the download queue fragment to adjust the errored download? Couldnt reprod but reworked logic lmk
- Fixed app still using original title for metadata even though the user changed it in the textfield
- Slight fixes for artist parsing for ytm videos
- Improved and simplified uploader parsing when quick downloading
- Ignored the recode command when using avi container, because it doesnt support it
- Prevented app selecting bad formats 233 and 234 when quick downloading
- Fixed app not showing best quality and worst quality labels depending on the user language
- Fixed app crashing when selecting formats of an instagram post with the same url
## Note
Some people are asking for the ability to not use cache while downloading in usb storage. This is an android limitation. Even though you might have all files access, android still doesnt consider usb storage as main storage and cant directly write to it. So i have to use caching and then transfer the file.
If anyone is knowledgeable to pull this off, dm me on telegram and we can make it happen. :)
I could've released this sooner but i have been busy :/