prep 1.7.9.1
This commit is contained in:
parent
65462a3704
commit
f5d154a724
43 changed files with 2063 additions and 1158 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
15
app/proguard-rules.pro
vendored
15
app/proguard-rules.pro
vendored
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
/*
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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®ion=${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®ion=${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!!
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -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®ionCode=${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®ionCode=${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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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!!
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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){
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -235,7 +235,7 @@
|
|||
</array>
|
||||
|
||||
<array name="formats_source">
|
||||
<item>PIPED [Youtube]</item>
|
||||
<item>NewPipe</item>
|
||||
<item>yt-dlp</item>
|
||||
</array>
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
46
fastlane/metadata/android/en-US/changelogs/10791.txt
Normal file
46
fastlane/metadata/android/en-US/changelogs/10791.txt
Normal 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 :/
|
||||
Loading…
Reference in a new issue