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 {
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
@ -126,7 +127,7 @@ android {
dependencies {
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:ffmpeg:$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.room:room-paging:$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"
androidTestImplementation 'com.google.truth:truth:1.1.5'
@ -182,7 +183,7 @@ dependencies {
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0'
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.3"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.4"
implementation "com.github.Irineu333:Highlight-KT:1.0.4"
// For media playback using ExoPlayer
@ -201,5 +202,5 @@ dependencies {
implementation 'androidx.compose.material3:material3-android:1.2.1'
implementation "io.noties.markwon:core:4.6.2"
implementation("org.greenrobot:eventbus:3.3.1")
//implementation("com.github.TeamNewPipe:NewPipeExtractor:0.24.2")
implementation("com.github.TeamNewPipe:NewPipeExtractor:0.24.2")
}

View file

@ -88,11 +88,16 @@
static <1>$Companion Companion;
}
### Rules for NewPipeExtractor
#-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
#-keep class org.mozilla.javascript.** { *; }
#-keep class org.mozilla.classfile.ClassFileWriter
#-dontwarn org.mozilla.javascript.tools.**
# Rules for NewPipeExtractor
-keep class org.schabi.newpipe.extractor.timeago.patterns.** { *; }
-keep class org.mozilla.javascript.** { *; }
-keep class org.mozilla.classfile.ClassFileWriter
-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
-dontobfuscate

View file

@ -441,14 +441,30 @@ class MainActivity : BaseActivity() {
e.printStackTrace()
}
}else if (action == Intent.ACTION_VIEW){
navController.popBackStack(navController.graph.startDestinationId, true)
val navbarItems = NavbarUtil.getNavBarItems(this)
when(intent.getStringExtra("destination")){
"Downloads" -> {
if (navbarItems.any { n -> n.itemId == R.id.historyFragment && n.isVisible }) {
navController.popBackStack(navController.graph.startDestinationId, true)
}
navController.navigate(R.id.historyFragment)
}
"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" -> {
val bundle = Bundle()

View file

@ -14,6 +14,9 @@ interface ResultDao {
@Query("SELECT * FROM results order by id")
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")
fun getCount() : Flow<Int>

View file

@ -2,23 +2,49 @@ package com.deniscerri.ytdl.database.repository
import android.content.Context
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.models.ChapterItem
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.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.InfoUtil
import kotlinx.coroutines.delay
import com.deniscerri.ytdl.util.FileUtil
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.MutableStateFlow
import java.util.regex.Pattern
import kotlinx.coroutines.runBlocking
class ResultRepository(private val resultDao: ResultDao, private val context: Context) {
val YTDLNIS_SEARCH = "YTDLNIS_SEARCH"
val allResults : Flow<List<ResultItem>> = resultDao.getResults()
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 {
YOUTUBE_VIDEO, YOUTUBE_PLAYLIST, SEARCH_QUERY, YT_DLP
YOUTUBE_VIDEO,
YOUTUBE_PLAYLIST,
YOUTUBE_CHANNEL,
SEARCH_QUERY,
YT_DLP
}
suspend fun insert(it: ResultItem){
@ -31,17 +57,71 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
suspend fun updateTrending(){
deleteAll()
val infoUtil = InfoUtil(context)
val items = infoUtil.getTrending()
val items = if (sharedPreferences.getString("api_key", "")!!.isNotBlank()) {
youtubeApiUtil.getTrending()
}else{
newPipeUtil.getTrending()
}
itemCount.value = items.size
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>{
val infoUtil = InfoUtil(context)
if (resetResults) deleteAll()
val res = infoUtil.search(inputQuery)
itemCount.value = res.size
val res = when(sharedPreferences.getString("search_engine", "ytsearch")) {
"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){
val ids = resultDao.insertMultiple(res)
ids.forEachIndexed { index, id ->
@ -51,53 +131,70 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return res
}
suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{
val infoUtil = InfoUtil(context)
val v = infoUtil.getYoutubeVideo(inputQuery) ?: return arrayListOf()
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 = ""
private suspend fun getYoutubePlaylist(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
val id = inputQuery.split("list=".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[1].split("&").first()
val playlistURL = "https://youtube.com/playlist?list=${id}"
if (resetResults) deleteAll()
val infoUtil = InfoUtil(context)
val items = arrayListOf<ResultItem>()
do {
val tmp = infoUtil.getPlaylist(query, nextPageToken, if (items.isNotEmpty()) items[0].playlistTitle else "")
val items = mutableListOf<ResultItem>()
val newPipeResult = newPipeUtil.getPlaylistData(playlistURL) {
if (addToResults){
val ids = resultDao.insertMultiple(tmp.videos.toList())
ids.forEachIndexed { index, id ->
tmp.videos[index].id = id
runBlocking {
val ids = resultDao.insertMultiple(it)
ids.forEachIndexed { index, id ->
it[index].id = id
}
}
}
items.addAll(tmp.videos)
val tmpToken = tmp.nextPageToken
if (tmpToken.isEmpty()) break
if (tmpToken == nextPageToken) break
nextPageToken = tmpToken
delay(1000)
} while (true)
itemCount.value = items.size
return items
items.addAll(it)
}
val response = if (newPipeResult.isSuccess){
newPipeResult.getOrElse { items }
}else{
val res = ytdlpUtil.getFromYTDL(playlistURL)
val ids = resultDao.insertMultiple(res)
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> {
val infoUtil = InfoUtil(context)
val items = infoUtil.getFromYTDL(inputQuery, singleItem)
private suspend fun getYoutubeChannel(url: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
if (resetResults) deleteAll()
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) {
deleteAll()
itemCount.value = items.size
@ -115,6 +212,43 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
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){
resultDao.delete(item.id)
}
@ -148,23 +282,30 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
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)){
SourceType.YOUTUBE_VIDEO -> {
getYoutubeVideo(inputQuery, resetResults, addToResults)
}
SourceType.YOUTUBE_PLAYLIST -> {
if (singleItem){
getDefault(inputQuery, resetResults, addToResults, true)
getFromYTDLP(inputQuery, resetResults, addToResults, true)
}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 -> {
search(inputQuery, resetResults, addToResults)
}
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
if (inputQuery.contains("playlist?list=")) {
type = SourceType.YOUTUBE_PLAYLIST
}else if (inputQuery.isYoutubeChannelURL()) {
type = SourceType.YOUTUBE_CHANNEL
}
} else if (Patterns.WEB_URL.matcher(inputQuery).matches()) {
type = SourceType.YT_DLP

View file

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

View file

@ -7,6 +7,8 @@ import android.content.SharedPreferences
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import androidx.preference.PreferenceManager
@ -14,18 +16,17 @@ import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.DBManager
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.HistoryItem
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.models.SearchHistoryItem
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.repository.SearchHistoryRepository
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.yausername.youtubedl_android.YoutubeDLException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.joinAll
@ -40,7 +41,9 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
val repository : ResultRepository
private val searchHistoryRepository : SearchHistoryRepository
val items : LiveData<List<ResultItem>>
private val infoUtil: InfoUtil
private var _items = MediatorLiveData<List<ResultItem>>()
val playlistFilter = MutableLiveData("")
private val notificationUtil: NotificationUtil
private val dao: ResultDao
data class ResultsUiState(
@ -72,12 +75,31 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
dao = DBManager.getInstance(application).resultDao
repository = ResultRepository(dao, getApplication<Application>().applicationContext)
searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao)
items = repository.allResults.asLiveData()
_items.addSource(items){
filter()
}
_items.addSource(playlistFilter) {
filter()
}
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
infoUtil = InfoUtil(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){
try {
val item = repository.getFirstResult()
@ -271,7 +293,7 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
updateFormatsResultDataJob = viewModelScope.launch(Dispatchers.IO) {
updatingFormats.emit(true)
try {
val formats = infoUtil.getFormats(result.url)
val formats = getFormats(result.url)
updatingFormats.emit(false)
formats.apply {
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>{
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.Handler
import android.os.Looper
import android.text.Editable
import android.util.Log
import android.util.Patterns
import android.view.*
@ -19,7 +18,6 @@ import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.os.bundleOf
import androidx.core.view.children
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.SearchSuggestionItem
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.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.adapter.HomeAdapter
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.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.ThemeUtil
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.MaterialToolbar
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.ChipGroup
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
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.SearchView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
@ -84,10 +80,12 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
private var downloadAllFab: ExtendedFloatingActionButton? = null
private var clipboardFab: ExtendedFloatingActionButton? = null
private var homeFabs: LinearLayout? = null
private var infoUtil: InfoUtil? = null
private var notificationUtil: NotificationUtil? = 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 downloadViewModel : DownloadViewModel
private lateinit var historyViewModel : HistoryViewModel
@ -123,7 +121,6 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
activity = getActivity()
mainActivity = activity as MainActivity?
quickLaunchSheet = false
infoUtil = InfoUtil(requireContext())
notificationUtil = NotificationUtil(requireContext())
selectedObjects = arrayListOf()
return fragmentView
@ -157,6 +154,8 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
downloadSelectedFab = homeFabs!!.findViewById(R.id.download_selected_fab)
downloadAllFab = homeFabs!!.findViewById(R.id.download_all_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()) }
@ -186,7 +185,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val progressBar = view.findViewById<View>(R.id.progress)
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
resultViewModel.items.observe(requireActivity()) {
resultViewModel.getFilteredList().observe(requireActivity()) {
kotlin.runCatching {
homeAdapter!!.submitList(it)
resultsList = it
@ -197,6 +196,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
}else{
downloadAllFab!!.visibility = GONE
}
}else if (resultViewModel.repository.itemCount.value == 1){
if (sharedPreferences!!.getBoolean("download_card", true)){
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()
downloadSelectedFab?.tag = "downloadSelected"
downloadSelectedFab?.setOnClickListener(this)
@ -523,13 +531,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
val combinedList = mutableListOf<SearchSuggestionItem>()
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 {
SearchSuggestionItem(it, SearchSuggestionType.HISTORY)
}
val suggestions = if (sharedPreferences!!.getBoolean("search_suggestions", false)){
withContext(Dispatchers.IO){
infoUtil!!.getSearchSuggestions(searchQuery)
resultViewModel.getSearchSuggestions(searchQuery)
}
}else{
emptyList()
@ -780,6 +788,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
mode!!.menuInflater.inflate(R.menu.main_menu_context, menu)
mode.title = "${selectedObjects.size} ${getString(R.string.selected)}"
searchBar!!.isEnabled = false
playlistNameFilterChipGroup.children.forEach { it.isEnabled = false }
searchBar!!.menu.forEach { it.isEnabled = false }
(activity as MainActivity).disableBottomNavigation()
return true
@ -877,6 +886,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
(activity as MainActivity).enableBottomNavigation()
clearCheckedItems()
searchBar!!.isEnabled = true
playlistNameFilterChipGroup.children.forEach { it.isEnabled = true }
searchBar!!.menu.forEach { it.isEnabled = true }
searchBar?.expand(appBarLayout!!)
}
@ -970,4 +980,34 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
searchView!!.editText.setText(text)
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.TextView
import androidx.core.view.isVisible
import androidx.paging.PagingDataAdapter
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.AsyncDifferConfig
import androidx.recyclerview.widget.DiffUtil
@ -19,12 +18,10 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.deniscerri.ytdl.R
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.util.Extensions.loadThumbnail
import com.deniscerri.ytdl.util.Extensions.popup
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.google.android.material.button.MaterialButton
import java.util.Locale

View file

@ -15,14 +15,13 @@ import android.view.inputmethod.InputMethodManager
import android.widget.*
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel
import com.deniscerri.ytdl.util.Extensions.enableTextHighlight
import com.deniscerri.ytdl.util.InfoUtil
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.BottomSheetDialogFragment
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() {
private lateinit var infoUtil: InfoUtil
private lateinit var ytdlpUtil: YTDLPUtil
private lateinit var sharedPreferences: SharedPreferences
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity().applicationContext)
ytdlpUtil = YTDLPUtil(requireActivity().applicationContext)
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
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)
if (item != null){
val currentCommand = infoUtil.parseYTDLRequestString(infoUtil.buildYoutubeDLRequest(item))
val currentCommand = ytdlpUtil.parseYTDLRequestString(ytdlpUtil.buildYoutubeDLRequest(item))
currentText?.text = currentCommand
}else{
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.MotionEvent
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.*
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem.fromUri
import androidx.media3.common.Player
@ -31,17 +30,15 @@ import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.ChapterItem
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.setTextAndRecalculateWidth
import com.deniscerri.ytdl.util.Extensions.toStringDuration
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.VideoPlayerUtil
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.button.MaterialButton
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.materialswitch.MaterialSwitch
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.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.lang.reflect.Type
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() {
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var infoUtil: InfoUtil
private lateinit var player: ExoPlayer
private lateinit var resultViewModel: ResultViewModel
private lateinit var cutSection : ConstraintLayout
private lateinit var durationText: TextView
private lateinit var progress : ProgressBar
@ -99,8 +89,8 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
private lateinit var selectedCuts: MutableList<String>
override fun onCreate(savedInstanceState: Bundle?) {
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity().applicationContext)
}
@ -182,7 +172,7 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
try {
val data = withContext(Dispatchers.IO) {
if (urls.isNullOrEmpty()) {
infoUtil.getStreamingUrlAndChapters(item.url)
resultViewModel.getStreamingUrlAndChapters(item.url)
}else{
Pair(urls.split("\n"), chapters)
}

View file

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

View file

@ -7,7 +7,6 @@ import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.util.Patterns
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.receiver.ShareActivity
import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomsheet.BottomSheetBehavior
@ -60,8 +58,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
@ -73,7 +69,6 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
private lateinit var resultViewModel: ResultViewModel
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var commandTemplateViewModel : CommandTemplateViewModel
private lateinit var infoUtil: InfoUtil
private lateinit var sharedPreferences : SharedPreferences
private lateinit var updateItem : Button
private lateinit var view: View
@ -96,7 +91,6 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() {
historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java]
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
infoUtil = InfoUtil(requireContext())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val res: ResultItem?
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.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.UiUtil
import com.facebook.shimmer.ShimmerFrameLayout
import com.google.android.material.bottomappbar.BottomAppBar
@ -73,7 +72,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
private lateinit var resultViewModel: ResultViewModel
private lateinit var listAdapter : ConfigureMultipleDownloadsAdapter
private lateinit var recyclerView: RecyclerView
private lateinit var infoUtil: InfoUtil
private lateinit var behavior: BottomSheetBehavior<View>
private lateinit var bottomAppBar: BottomAppBar
private lateinit var filesize : TextView
@ -100,7 +98,6 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java]
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
infoUtil = InfoUtil(requireContext())
currentDownloadIDs = arguments?.getLongArray("currentDownloadIDs")?.toList() ?: listOf()
processingItemsCount = currentDownloadIDs.size

View file

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

View file

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

View file

@ -13,7 +13,6 @@ import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.text.format.DateFormat
import android.util.DisplayMetrics
import android.view.LayoutInflater
import android.view.View
@ -21,8 +20,6 @@ import android.view.ViewGroup
import android.view.Window
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.lifecycle.ViewModelProvider
@ -48,8 +45,6 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.adapter.ActiveDownloadMinifiedAdapter
import com.deniscerri.ytdl.ui.adapter.GenericDownloadAdapter
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.UiUtil
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.BottomSheetDialogFragment
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.dialog.MaterialAlertDialogBuilder
import com.google.android.material.elevation.SurfaceColors
@ -76,13 +70,9 @@ import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdapter.OnItemClickListener, ActiveDownloadMinifiedAdapter.OnItemClickListener {
private lateinit var infoUtil: InfoUtil
private lateinit var notificationUtil: NotificationUtil
private lateinit var videoView: PlayerView
private lateinit var downloadViewModel: DownloadViewModel
@ -99,7 +89,6 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
infoUtil = InfoUtil(requireActivity())
notificationUtil = NotificationUtil(requireActivity())
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
@ -283,7 +272,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap
try {
val data = withContext(Dispatchers.IO) {
if (item.urls.isEmpty()) {
infoUtil.getStreamingUrlAndChapters(item.url)
resultViewModel.getStreamingUrlAndChapters(item.url)
}else{
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.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import com.google.gson.Gson
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
@ -71,7 +72,9 @@ class DownloadQueueMainFragment : Fragment(){
}else{
mainActivity.hideBottomNavigation()
}
topAppBar.setNavigationOnClickListener { mainActivity.onBackPressedDispatcher.onBackPressed() }
topAppBar.setNavigationOnClickListener {
mainActivity.onBackPressedDispatcher.onBackPressed()
}
tabLayout = view.findViewById(R.id.download_tablayout)
viewPager2 = view.findViewById(R.id.download_viewpager)
@ -148,6 +151,8 @@ class DownloadQueueMainFragment : Fragment(){
}, 200)
}
arguments?.clear()
if (sharedPreferences.getBoolean("show_count_downloads", false)){
lifecycleScope.launch {
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.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository.EveryCategory
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.toTimePeriodsArray
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
@ -498,4 +499,8 @@ object Extensions {
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.content.Context
import android.content.SharedPreferences
import androidx.core.content.ContextCompat
import androidx.core.content.contentValuesOf
import android.content.res.Resources
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
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 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() }
@ -170,6 +163,53 @@ class FormatSorter(private var context: Context) {
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
/*

View file

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

View file

@ -9,7 +9,6 @@ import android.app.TaskStackBuilder
import android.content.Context
import android.content.Intent
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
@ -17,7 +16,6 @@ import android.os.Bundle
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.FileProvider
import androidx.core.os.bundleOf
import androidx.documentfile.provider.DocumentFile
import androidx.navigation.NavDeepLinkBuilder
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.util.Extensions.toBitmap
import java.io.File
import kotlin.random.Random
class NotificationUtil(var context: Context) {
@ -339,22 +338,28 @@ class NotificationUtil(var context: Context) {
.setArguments(bundle)
.createPendingIntent()
val errorTabPendingIntent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment)
.setArguments(bundleOf(Pair("tab", "error")))
.createPendingIntent()
val intent = Intent(context, MainActivity::class.java)
intent.setAction(Intent.ACTION_VIEW)
intent.putExtra("destination", "Queue")
intent.putExtra("tab", "error")
val errorTabPendingIntent = PendingIntent.getActivity(
context,
Random.nextInt(),
intent,
PendingIntent.FLAG_IMMUTABLE
)
val reconfigurePendingItent = NavDeepLinkBuilder(context)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.downloadQueueMainFragment)
.setArguments(
bundleOf(
Pair("tab", "error"),
Pair("reconfigure", id)
)
)
.createPendingIntent()
val intent2 = Intent(context, MainActivity::class.java)
intent2.setAction(Intent.ACTION_VIEW)
intent2.putExtra("reconfigure", id)
intent2.putExtra("tab", "error")
intent2.putExtra("destination", "Queue")
val reconfigurePendingItent = PendingIntent.getActivity(
context,
Random.nextInt(),
intent2,
PendingIntent.FLAG_IMMUTABLE
)
notificationBuilder
.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.getMediaDuration
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.BadgeUtils
import com.google.android.material.badge.ExperimentalBadgeUtils
@ -567,10 +569,10 @@ object UiUtil {
if (fileSizeReadable == "?") fileSize!!.visibility = View.GONE
else fileSize!!.text = fileSizeReadable
val infoUtil = InfoUtil(context)
val ytdlpUtil = YTDLPUtil(context)
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)
@ -1867,7 +1869,7 @@ object UiUtil {
CoroutineScope(Dispatchers.IO).launch {
val chipGroup = view.findViewById<ChipGroup>(R.id.filename_suggested_chipgroup)
val chips = mutableListOf<Chip>()
val instances = InfoUtil(context).getPipedInstances().ifEmpty { return@launch }
val instances = PipedApiUtil(context).getPipedInstances().ifEmpty { return@launch }
instances.forEach { s ->
val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
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.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.ForegroundInfo
import androidx.work.WorkInfo
import androidx.work.WorkManager
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.models.HistoryItem
import com.deniscerri.ytdl.database.models.LogItem
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 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(

View file

@ -12,7 +12,6 @@ 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.ForegroundInfo
@ -20,7 +19,6 @@ import androidx.work.WorkInfo
import androidx.work.WorkManager
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.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.toStringDuration
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.InfoUtil
import com.deniscerri.ytdl.util.NotificationUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -46,7 +44,6 @@ import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import java.io.File
import java.util.Locale
import kotlin.random.Random
class DownloadWorker(
@ -58,11 +55,10 @@ class DownloadWorker(
if (isStopped) return Result.success()
val notificationUtil = NotificationUtil(App.instance)
val infoUtil = InfoUtil(context)
val ytdlpUtil = YTDLPUtil(context)
val dbManager = DBManager.getInstance(context)
val dao = dbManager.downloadDao
val historyDao = dbManager.historyDao
val resultDao = dbManager.resultDao
val logRepo = LogRepository(dbManager.logDao)
val resultRepo = ResultRepository(dbManager.resultDao, context)
val handler = Handler(Looper.getMainLooper())
@ -135,7 +131,7 @@ class DownloadWorker(
CoroutineScope(Dispatchers.IO).launch {
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()
CoroutineScope(Dispatchers.IO).launch {
delay(1500)
@ -158,7 +154,7 @@ class DownloadWorker(
val logDownloads = sharedPreferences.getBoolean("log_downloads", false) && !downloadItem.incognito
val commandString = infoUtil.parseYTDLRequestString(request)
val commandString = ytdlpUtil.parseYTDLRequestString(request)
val initialLogDetails = "Downloading:\n" +
"Title: ${downloadItem.title}\n" +
"URL: ${downloadItem.url}\n" +
@ -258,9 +254,6 @@ class DownloadWorker(
add("txt")
}
finalPaths = finalPaths.filter { path -> !nonMediaExtensions.any { path.endsWith(it) } }.toMutableList()
if (finalPaths.isEmpty()){
finalPaths = mutableListOf(context.getString(R.string.unfound_file))
}
FileUtil.deleteConfigFiles(request)
//put download in history
@ -270,39 +263,40 @@ class DownloadWorker(
Toast.makeText(context, resources.getString(R.string.download_already_exists), Toast.LENGTH_LONG).show()
}, 100)
}else{
val unixTime = System.currentTimeMillis() / 1000
finalPaths.first().apply {
val file = File(this)
var duration = downloadItem.duration
val d = file.getMediaDuration(context)
if (d > 0) duration = d.toStringDuration(Locale.US)
if (finalPaths.isNotEmpty()) {
val unixTime = System.currentTimeMillis() / 1000
finalPaths.first().apply {
val file = File(this)
var duration = downloadItem.duration
val d = file.getMediaDuration(context)
if (d > 0) duration = d.toStringDuration(Locale.US)
downloadItem.format.filesize = file.length()
downloadItem.format.container = file.extension
downloadItem.duration = duration
downloadItem.format.filesize = file.length()
downloadItem.format.container = file.extension
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.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){

View file

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

View file

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

View file

@ -146,23 +146,62 @@
</com.google.android.material.appbar.AppBarLayout>
<RelativeLayout
app:layout_behavior="@string/appbar_scrolling_view_behavior"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
android:orientation="vertical"
android:layout_height="wrap_content"
<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: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:orientation="vertical"
android:paddingBottom="200dp"
app:spanCount="2"
android:scrollbars="none"
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
/>
app:spanCount="2" />
<com.facebook.shimmer.ShimmerFrameLayout
android:layout_width="match_parent"

View file

@ -142,12 +142,53 @@
android:layout_width="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
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
android:layout_below="@id/playlist_selection_chips_scrollview"
android:clipToPadding="false"
android:paddingBottom="200dp"
app:spanCount="3"

View file

@ -150,12 +150,54 @@
android:layout_width="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
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
android:layout_below="@id/playlist_selection_chips_scrollview"
android:clipToPadding="false"
android:paddingBottom="200dp"
app:spanCount="2"

View file

@ -142,12 +142,53 @@
android:layout_width="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
android:layout_width="match_parent"
android:id="@+id/recyclerViewHome"
android:orientation="vertical"
android:layout_height="wrap_content"
android:scrollbars="none"
android:layout_below="@id/playlist_selection_chips_scrollview"
android:clipToPadding="false"
android:paddingBottom="200dp"
app:spanCount="4"

View file

@ -179,6 +179,47 @@
</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>

View file

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

View file

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