more features
This commit is contained in:
parent
926e6ec73d
commit
4f04b039ad
23 changed files with 777 additions and 309 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -14,6 +14,12 @@
|
||||||
- Made the app remember the last used scheduled time so it can suggest you that time for the next download
|
- Made the app remember the last used scheduled time so it can suggest you that time for the next download
|
||||||
- #618, made all preferences with a description show their values
|
- #618, made all preferences with a description show their values
|
||||||
- Fixed bug that prevented app from loading all urls from text file
|
- Fixed bug that prevented app from loading all urls from text file
|
||||||
|
- Added ability to write info json when downloading so when you resume an
|
||||||
|
|
||||||
|
## Custom yt-dlp source
|
||||||
|
|
||||||
|
Now you can create your own yt-dlp source the app can update to.
|
||||||
|
The app is running yt-dlp --update-to user/repo@latest where user/repo is provided by you through the app. The default 3 sources are still there ofc.
|
||||||
|
|
||||||
# Advanced Settings
|
# Advanced Settings
|
||||||
|
|
||||||
|
|
@ -22,6 +28,22 @@ Added a new category for more advanced users and moved the extractor argument se
|
||||||
- When PO Token is set, the app now adds the web extractor argument for youtube. I forgor...
|
- When PO Token is set, the app now adds the web extractor argument for youtube. I forgor...
|
||||||
(If you want to set more PO tokens for other player clients i guess set them in the other extractor argument text preference, and also modify the player client. The separate PO Token preference applies it only for the web player client)
|
(If you want to set more PO tokens for other player clients i guess set them in the other extractor argument text preference, and also modify the player client. The separate PO Token preference applies it only for the web player client)
|
||||||
|
|
||||||
|
## Write Info Json
|
||||||
|
|
||||||
|
Added ability to write info json when downloading so when you resume and restart downloads, it will not download the webpage and player clients all over again. It will help prevent you making unnecessary calls to the server.
|
||||||
|
On advanced settings you can turn this off if you want.
|
||||||
|
I coded it be available for the next 5 hours. Considering formats tend to expire on some extractors, i dont want the download to be unusable. This is a rare use case, one of them being you starting a download, it saves the info json and you cancel it and you remember to start it again, maybe tomorrow lol. Formats most likely expired, download item is useless. So in that case it will redownload the the webpage and player clients again, and it will save a new info file aswell no problem.
|
||||||
|
|
||||||
|
## More Home recommendation options
|
||||||
|
|
||||||
|
Now you can select to have more youtube feeds in the home screen from yt-dlp
|
||||||
|
- watch later playlist
|
||||||
|
- recommendations
|
||||||
|
- liked videos
|
||||||
|
- watch history
|
||||||
|
|
||||||
|
Since the option has been overhauled to put every home recommendation option in a single list-view preference, the preference has been reset so u have to change it again if you had changed it
|
||||||
|
|
||||||
|
|
||||||
> # 1.8.0 (2024-10)
|
> # 1.8.0 (2024-10)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -215,5 +215,5 @@ dependencies {
|
||||||
implementation 'androidx.compose.material3:material3-android:1.3.0'
|
implementation 'androidx.compose.material3:material3-android:1.3.0'
|
||||||
implementation "io.noties.markwon:core:4.6.2"
|
implementation "io.noties.markwon:core:4.6.2"
|
||||||
implementation("org.greenrobot:eventbus:3.3.1")
|
implementation("org.greenrobot:eventbus:3.3.1")
|
||||||
implementation("com.github.teamnewpipe:newpipeextractor:v0.24.2")
|
implementation("com.github.teamnewpipe:newpipeextractor:0.24.2")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,23 @@
|
||||||
package com.deniscerri.ytdl
|
package com.deniscerri.ytdl
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.ActionBar.LayoutParams
|
import android.app.ActionBar.LayoutParams
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.DialogInterface
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.drawable.ColorDrawable
|
import android.graphics.drawable.ColorDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.provider.Settings
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Patterns
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.WindowInsets
|
import android.view.WindowInsets
|
||||||
import android.widget.CheckBox
|
import android.widget.CheckBox
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.forEach
|
import androidx.core.view.forEach
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
|
@ -35,7 +27,6 @@ import androidx.fragment.app.FragmentContainerView
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavOptions
|
|
||||||
import androidx.navigation.fragment.NavHostFragment
|
import androidx.navigation.fragment.NavHostFragment
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import androidx.navigation.ui.setupWithNavController
|
import androidx.navigation.ui.setupWithNavController
|
||||||
|
|
@ -68,13 +59,10 @@ import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.io.BufferedReader
|
import java.io.BufferedReader
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.io.Reader
|
import java.io.Reader
|
||||||
import java.nio.charset.Charset
|
import java.nio.charset.Charset
|
||||||
|
|
@ -278,7 +266,7 @@ class MainActivity : BaseActivity() {
|
||||||
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
|
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
if(DBManager.getInstance(this@MainActivity).downloadDao.getDownloadsCountByStatus(listOf("Active", "Queued")) == 0){
|
if(DBManager.getInstance(this@MainActivity).downloadDao.getDownloadsCountByStatus(listOf("Active", "Queued")) == 0){
|
||||||
if (UpdateUtil(this@MainActivity).updateYoutubeDL() == YoutubeDL.UpdateStatus.DONE) {
|
if (UpdateUtil(this@MainActivity).updateYoutubeDL().status == UpdateUtil.YTDLPUpdateStatus.DONE) {
|
||||||
val version = YoutubeDL.getInstance().version(context)
|
val version = YoutubeDL.getInstance().version(context)
|
||||||
val snack = Snackbar.make(findViewById(R.id.frame_layout),
|
val snack = Snackbar.make(findViewById(R.id.frame_layout),
|
||||||
this@MainActivity.getString(R.string.ytld_update_success) + " [${version}]",
|
this@MainActivity.getString(R.string.ytld_update_success) + " [${version}]",
|
||||||
|
|
|
||||||
|
|
@ -3,25 +3,19 @@ package com.deniscerri.ytdl.database.repository
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.Patterns
|
import android.util.Patterns
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import com.deniscerri.ytdl.database.DBManager.SORTING
|
|
||||||
import com.deniscerri.ytdl.database.dao.ResultDao
|
import com.deniscerri.ytdl.database.dao.ResultDao
|
||||||
import com.deniscerri.ytdl.database.models.ChapterItem
|
import com.deniscerri.ytdl.database.models.ChapterItem
|
||||||
import com.deniscerri.ytdl.database.models.DownloadItem
|
import com.deniscerri.ytdl.database.models.DownloadItem
|
||||||
import com.deniscerri.ytdl.database.models.Format
|
import com.deniscerri.ytdl.database.models.Format
|
||||||
import com.deniscerri.ytdl.database.models.HistoryItem
|
|
||||||
import com.deniscerri.ytdl.database.models.ResultItem
|
import com.deniscerri.ytdl.database.models.ResultItem
|
||||||
import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType
|
|
||||||
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
|
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
|
||||||
import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL
|
import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL
|
||||||
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
|
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
|
||||||
import com.deniscerri.ytdl.util.Extensions.isYoutubeWatchVideosURL
|
import com.deniscerri.ytdl.util.Extensions.isYoutubeWatchVideosURL
|
||||||
import com.deniscerri.ytdl.util.FileUtil
|
|
||||||
import com.deniscerri.ytdl.util.extractors.GoogleApiUtil
|
import com.deniscerri.ytdl.util.extractors.GoogleApiUtil
|
||||||
import com.deniscerri.ytdl.util.extractors.PipedApiUtil
|
|
||||||
import com.deniscerri.ytdl.util.extractors.newpipe.NewPipeUtil
|
import com.deniscerri.ytdl.util.extractors.newpipe.NewPipeUtil
|
||||||
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
|
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
|
||||||
import com.deniscerri.ytdl.util.extractors.YoutubeApiUtil
|
import com.deniscerri.ytdl.util.extractors.YoutubeApiUtil
|
||||||
import com.yausername.youtubedl_android.YoutubeDLException
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
|
@ -37,15 +31,9 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
||||||
|
|
||||||
private val youtubeApiUtil = YoutubeApiUtil(context)
|
private val youtubeApiUtil = YoutubeApiUtil(context)
|
||||||
private val ytdlpUtil = YTDLPUtil(context)
|
private val ytdlpUtil = YTDLPUtil(context)
|
||||||
private var newPipeUtil : NewPipeUtil? = null
|
private var newPipeUtil = NewPipeUtil(context)
|
||||||
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
|
||||||
init {
|
|
||||||
if (sharedPreferences.getString("youtube_data_fetching_extractor", "NEWPIPE") == "NEWPIPE") {
|
|
||||||
newPipeUtil = NewPipeUtil(context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class SourceType {
|
enum class SourceType {
|
||||||
YOUTUBE_VIDEO,
|
YOUTUBE_VIDEO,
|
||||||
YOUTUBE_WATCHVIDEOS,
|
YOUTUBE_WATCHVIDEOS,
|
||||||
|
|
@ -55,6 +43,8 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
||||||
YT_DLP
|
YT_DLP
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isUsingNewPipeExtractorDataFetching() = sharedPreferences.getString("youtube_data_fetching_extractor", "NEWPIPE") == "NEWPIPE"
|
||||||
|
|
||||||
suspend fun insert(it: ResultItem){
|
suspend fun insert(it: ResultItem){
|
||||||
resultDao.insert(it)
|
resultDao.insert(it)
|
||||||
}
|
}
|
||||||
|
|
@ -63,12 +53,17 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
||||||
return resultDao.getFirstResult()
|
return resultDao.getFirstResult()
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateTrending(){
|
suspend fun getHomeRecommendations(){
|
||||||
deleteAll()
|
deleteAll()
|
||||||
val items = if (sharedPreferences.getString("api_key", "")!!.isNotBlank()) {
|
val category = sharedPreferences.getString("youtube_home_recommendations", "")
|
||||||
youtubeApiUtil.getTrending()
|
val items = when(category) {
|
||||||
}else{
|
"newpipe" -> newPipeUtil.getTrending()
|
||||||
newPipeUtil?.getTrending() ?: listOf()
|
"yt_api" -> youtubeApiUtil.getTrending()
|
||||||
|
"yt_dlp_watch_later" -> ytdlpUtil.getYoutubeWatchLater()
|
||||||
|
"yt_dlp_recommendations" -> ytdlpUtil.getYoutubeRecommendations()
|
||||||
|
"yt_dlp_liked" -> ytdlpUtil.getYoutubeLikedVideos()
|
||||||
|
"yt_dlp_watch_history" -> ytdlpUtil.getYoutubeWatchHistory()
|
||||||
|
else -> arrayListOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
itemCount.value = items.size
|
itemCount.value = items.size
|
||||||
|
|
@ -80,24 +75,28 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getStreamingUrlAndChapters(url: String) : Pair<List<String>, List<ChapterItem>?> {
|
fun getStreamingUrlAndChapters(url: String) : Pair<List<String>, List<ChapterItem>?> {
|
||||||
val extractorsTrial = newPipeUtil?.getStreamingUrlAndChapters(url) ?: Result.failure(Throwable())
|
val newPipeTrial = if (isUsingNewPipeExtractorDataFetching()) {
|
||||||
if (extractorsTrial.isFailure){
|
newPipeUtil.getStreamingUrlAndChapters(url)
|
||||||
|
}else {
|
||||||
|
Result.failure(Throwable())
|
||||||
|
}
|
||||||
|
if (newPipeTrial.isFailure){
|
||||||
val res = ytdlpUtil.getStreamingUrlAndChapters(url)
|
val res = ytdlpUtil.getStreamingUrlAndChapters(url)
|
||||||
return res.getOrDefault(Pair(listOf(""), null))
|
return res.getOrDefault(Pair(listOf(""), null))
|
||||||
}
|
}
|
||||||
|
|
||||||
return extractorsTrial.getOrNull()!!
|
return newPipeTrial.getOrNull()!!
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun search(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{
|
suspend fun search(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{
|
||||||
if (resetResults) deleteAll()
|
if (resetResults) deleteAll()
|
||||||
val res = when(sharedPreferences.getString("search_engine", "ytsearch")) {
|
val res = when(sharedPreferences.getString("search_engine", "ytsearch")) {
|
||||||
"ytsearch" -> newPipeUtil?.search(inputQuery)
|
"ytsearch" -> newPipeUtil.search(inputQuery)
|
||||||
"ytsearchmusic" -> newPipeUtil?.searchMusic(inputQuery)
|
"ytsearchmusic" -> newPipeUtil.searchMusic(inputQuery)
|
||||||
else -> Result.failure(Throwable())
|
else -> Result.failure(Throwable())
|
||||||
}
|
}
|
||||||
|
|
||||||
val items = if (res?.isSuccess == true) {
|
val items = if (res.isSuccess) {
|
||||||
res.getOrNull()!!
|
res.getOrNull()!!
|
||||||
}else{
|
}else{
|
||||||
//fallback to yt-dlp
|
//fallback to yt-dlp
|
||||||
|
|
@ -119,19 +118,24 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
||||||
|
|
||||||
//throw YoutubeDLException("Youtube Watch Videos is not yet supported in data fetching. You can download it directly by clicking Continue Anyway or by Quick Downloading it!")
|
//throw YoutubeDLException("Youtube Watch Videos is not yet supported in data fetching. You can download it directly by clicking Continue Anyway or by Quick Downloading it!")
|
||||||
val items = mutableListOf<ResultItem>()
|
val items = mutableListOf<ResultItem>()
|
||||||
val ytExtractorResult = newPipeUtil?.getPlaylistData(inputQuery) {
|
val newpipeExtractorResult = if (isUsingNewPipeExtractorDataFetching()) {
|
||||||
if (addToResults){
|
newPipeUtil.getPlaylistData(inputQuery) {
|
||||||
runBlocking {
|
if (addToResults){
|
||||||
val ids = resultDao.insertMultiple(it)
|
runBlocking {
|
||||||
ids.forEachIndexed { index, id ->
|
val ids = resultDao.insertMultiple(it)
|
||||||
it[index].id = id
|
ids.forEachIndexed { index, id ->
|
||||||
|
it[index].id = id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
items.addAll(it)
|
||||||
}
|
}
|
||||||
items.addAll(it)
|
}else {
|
||||||
|
Result.failure(Throwable())
|
||||||
}
|
}
|
||||||
val response = if (ytExtractorResult?.isSuccess == true){
|
|
||||||
ytExtractorResult.getOrElse { items }
|
val response = if (newpipeExtractorResult.isSuccess){
|
||||||
|
newpipeExtractorResult.getOrElse { items }
|
||||||
}else{
|
}else{
|
||||||
val res = ytdlpUtil.getFromYTDL(inputQuery)
|
val res = ytdlpUtil.getFromYTDL(inputQuery)
|
||||||
if (addToResults) {
|
if (addToResults) {
|
||||||
|
|
@ -149,10 +153,14 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
||||||
|
|
||||||
private suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
|
private suspend fun getYoutubeVideo(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
|
||||||
val theURL = inputQuery.replace("\\?list.*".toRegex(), "")
|
val theURL = inputQuery.replace("\\?list.*".toRegex(), "")
|
||||||
val ytExtractorRes = newPipeUtil?.getVideoData(theURL)
|
val newpipeExtractorResult = if (isUsingNewPipeExtractorDataFetching()) {
|
||||||
|
newPipeUtil.getVideoData(theURL)
|
||||||
|
}else {
|
||||||
|
Result.failure(Throwable())
|
||||||
|
}
|
||||||
|
|
||||||
val res = if (ytExtractorRes?.isSuccess == true) {
|
val res = if (newpipeExtractorResult.isSuccess) {
|
||||||
ytExtractorRes.getOrNull()!!
|
newpipeExtractorResult.getOrNull()!!
|
||||||
}else{
|
}else{
|
||||||
ytdlpUtil.getFromYTDL(inputQuery)
|
ytdlpUtil.getFromYTDL(inputQuery)
|
||||||
}
|
}
|
||||||
|
|
@ -177,19 +185,23 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
||||||
val playlistURL = "https://youtube.com/playlist?list=${id}"
|
val playlistURL = "https://youtube.com/playlist?list=${id}"
|
||||||
if (resetResults) deleteAll()
|
if (resetResults) deleteAll()
|
||||||
val items = mutableListOf<ResultItem>()
|
val items = mutableListOf<ResultItem>()
|
||||||
val ytExtractorResult = newPipeUtil?.getPlaylistData(playlistURL) {
|
val ytExtractorResult = if (isUsingNewPipeExtractorDataFetching()){
|
||||||
if (addToResults){
|
newPipeUtil.getPlaylistData(playlistURL) {
|
||||||
runBlocking {
|
if (addToResults){
|
||||||
val ids = resultDao.insertMultiple(it)
|
runBlocking {
|
||||||
ids.forEachIndexed { index, id ->
|
val ids = resultDao.insertMultiple(it)
|
||||||
it[index].id = id
|
ids.forEachIndexed { index, id ->
|
||||||
|
it[index].id = id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
items.addAll(it)
|
||||||
}
|
}
|
||||||
items.addAll(it)
|
}else {
|
||||||
|
Result.failure(Throwable())
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = if (ytExtractorResult?.isSuccess == true){
|
val response = if (ytExtractorResult.isSuccess){
|
||||||
ytExtractorResult.getOrElse { items }
|
ytExtractorResult.getOrElse { items }
|
||||||
}else{
|
}else{
|
||||||
val res = ytdlpUtil.getFromYTDL(playlistURL)
|
val res = ytdlpUtil.getFromYTDL(playlistURL)
|
||||||
|
|
@ -209,30 +221,26 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
|
||||||
private suspend fun getYoutubeChannel(url: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
|
private suspend fun getYoutubeChannel(url: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
|
||||||
if (resetResults) deleteAll()
|
if (resetResults) deleteAll()
|
||||||
val items = mutableListOf<ResultItem>()
|
val items = mutableListOf<ResultItem>()
|
||||||
val ytExtractorResult = newPipeUtil?.getChannelData(url) {
|
val ytExtractorResult = if (isUsingNewPipeExtractorDataFetching()) {
|
||||||
if (addToResults){
|
newPipeUtil.getChannelData(url) {
|
||||||
runBlocking {
|
if (addToResults){
|
||||||
val ids = resultDao.insertMultiple(it)
|
runBlocking {
|
||||||
ids.forEachIndexed { index, id ->
|
val ids = resultDao.insertMultiple(it)
|
||||||
it[index].id = id
|
ids.forEachIndexed { index, id ->
|
||||||
|
it[index].id = id
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
items.addAll(it)
|
||||||
}
|
}
|
||||||
items.addAll(it)
|
}else {
|
||||||
|
Result.failure(Throwable())
|
||||||
}
|
}
|
||||||
|
|
||||||
val response = if (ytExtractorResult?.isSuccess == true){
|
val response = if (ytExtractorResult.isSuccess){
|
||||||
ytExtractorResult.getOrElse { items }
|
ytExtractorResult.getOrElse { items }
|
||||||
}else{
|
}else{
|
||||||
val res = ytdlpUtil.getFromYTDL(url)
|
val res = ytdlpUtil.getFromYTDL(url)
|
||||||
//TODO REMOVE THIS WHEN YT-DLP FIXES ISSUE #10827
|
|
||||||
res.forEach {
|
|
||||||
it.apply {
|
|
||||||
playlistTitle = ""
|
|
||||||
playlistURL = ""
|
|
||||||
playlistIndex = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val ids = resultDao.insertMultiple(res)
|
val ids = resultDao.insertMultiple(res)
|
||||||
ids.forEachIndexed { index, id ->
|
ids.forEachIndexed { index, id ->
|
||||||
res[index].id = id
|
res[index].id = id
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import com.deniscerri.ytdl.database.DBManager
|
||||||
import com.deniscerri.ytdl.database.dao.ResultDao
|
import com.deniscerri.ytdl.database.dao.ResultDao
|
||||||
import com.deniscerri.ytdl.database.models.ChapterItem
|
import com.deniscerri.ytdl.database.models.ChapterItem
|
||||||
import com.deniscerri.ytdl.database.models.Format
|
import com.deniscerri.ytdl.database.models.Format
|
||||||
import com.deniscerri.ytdl.database.models.HistoryItem
|
|
||||||
import com.deniscerri.ytdl.database.models.ResultItem
|
import com.deniscerri.ytdl.database.models.ResultItem
|
||||||
import com.deniscerri.ytdl.database.models.SearchHistoryItem
|
import com.deniscerri.ytdl.database.models.SearchHistoryItem
|
||||||
import com.deniscerri.ytdl.database.repository.ResultRepository
|
import com.deniscerri.ytdl.database.repository.ResultRepository
|
||||||
|
|
@ -33,7 +32,6 @@ import kotlinx.coroutines.joinAll
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import java.util.concurrent.CancellationException
|
import java.util.concurrent.CancellationException
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -116,16 +114,25 @@ class ResultViewModel(private val application: Application) : AndroidViewModel(a
|
||||||
item.playlistTitle == getApplication<App>().getString(R.string.trendingPlaylist)
|
item.playlistTitle == getApplication<App>().getString(R.string.trendingPlaylist)
|
||||||
&& item.creationTime < (System.currentTimeMillis() / 1000) - 86400
|
&& item.creationTime < (System.currentTimeMillis() / 1000) - 86400
|
||||||
){
|
){
|
||||||
getTrending()
|
getHomeRecommendations()
|
||||||
}
|
}
|
||||||
}catch (e : Exception){
|
}catch (e : Exception){
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
getTrending()
|
getHomeRecommendations()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun getTrending() = viewModelScope.launch(Dispatchers.IO){
|
fun getHomeRecommendations() = viewModelScope.launch(Dispatchers.IO){
|
||||||
if (sharedPreferences.getBoolean("home_recommendations", false)){
|
if (!sharedPreferences.getString("youtube_home_recommendations", "").isNullOrBlank()){
|
||||||
repository.updateTrending()
|
kotlin.runCatching {
|
||||||
|
uiState.update {it.copy(processing = true)}
|
||||||
|
repository.getHomeRecommendations()
|
||||||
|
uiState.update {it.copy(processing = false)}
|
||||||
|
}.onFailure { t ->
|
||||||
|
uiState.update {it.copy(
|
||||||
|
processing = false,
|
||||||
|
errorMessage = t.message.toString(),
|
||||||
|
)}
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
deleteAll()
|
deleteAll()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,14 +36,11 @@ import com.deniscerri.ytdl.R
|
||||||
import com.deniscerri.ytdl.database.models.ResultItem
|
import com.deniscerri.ytdl.database.models.ResultItem
|
||||||
import com.deniscerri.ytdl.database.models.SearchSuggestionItem
|
import com.deniscerri.ytdl.database.models.SearchSuggestionItem
|
||||||
import com.deniscerri.ytdl.database.models.SearchSuggestionType
|
import com.deniscerri.ytdl.database.models.SearchSuggestionType
|
||||||
import com.deniscerri.ytdl.database.repository.ResultRepository
|
|
||||||
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
|
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
|
||||||
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
|
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
|
||||||
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
|
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
|
||||||
import com.deniscerri.ytdl.ui.adapter.HomeAdapter
|
import com.deniscerri.ytdl.ui.adapter.HomeAdapter
|
||||||
import com.deniscerri.ytdl.ui.adapter.SearchSuggestionsAdapter
|
import com.deniscerri.ytdl.ui.adapter.SearchSuggestionsAdapter
|
||||||
import com.deniscerri.ytdl.ui.downloads.HistoryFragment
|
|
||||||
import com.deniscerri.ytdl.ui.downloads.HistoryFragment.Companion
|
|
||||||
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
|
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
|
||||||
import com.deniscerri.ytdl.util.Extensions.isURL
|
import com.deniscerri.ytdl.util.Extensions.isURL
|
||||||
import com.deniscerri.ytdl.util.NotificationUtil
|
import com.deniscerri.ytdl.util.NotificationUtil
|
||||||
|
|
@ -67,7 +64,6 @@ import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.*
|
|
||||||
import kotlin.collections.ArrayList
|
import kotlin.collections.ArrayList
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -497,7 +493,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
|
||||||
resultViewModel.cancelParsingQueries()
|
resultViewModel.cancelParsingQueries()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resultViewModel.getTrending()
|
resultViewModel.getHomeRecommendations()
|
||||||
selectedObjects = ArrayList()
|
selectedObjects = ArrayList()
|
||||||
searchBar!!.setText("")
|
searchBar!!.setText("")
|
||||||
downloadAllFab!!.visibility = GONE
|
downloadAllFab!!.visibility = GONE
|
||||||
|
|
|
||||||
|
|
@ -269,7 +269,29 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findPreference<ListPreference>("youtube_home_recommendations")?.apply {
|
||||||
|
val s = getString(R.string.video_recommendations_summary)
|
||||||
|
summary = if (value.isNullOrBlank()) {
|
||||||
|
s
|
||||||
|
}else {
|
||||||
|
"${s}\n[${entries[entryValues.indexOf(value)]}]"
|
||||||
|
}
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
summary = if ((newValue as String?).isNullOrBlank()) {
|
||||||
|
s
|
||||||
|
}else {
|
||||||
|
"${s}\n[${entries[entryValues.indexOf(newValue)]}]"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newValue == "yt_api") {
|
||||||
|
findPreference<EditTextPreference>("api_key")?.isVisible = true
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
findPreference<EditTextPreference>("api_key")?.apply {
|
findPreference<EditTextPreference>("api_key")?.apply {
|
||||||
|
isVisible = preferences.getString("youtube_home_recommendations", "") == "yt_api"
|
||||||
val s = getString(R.string.api_key_summary)
|
val s = getString(R.string.api_key_summary)
|
||||||
summary = if (text.isNullOrBlank()) {
|
summary = if (text.isNullOrBlank()) {
|
||||||
s
|
s
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,59 @@
|
||||||
package com.deniscerri.ytdl.ui.more.settings
|
package com.deniscerri.ytdl.ui.more.settings
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.TextWatcher
|
||||||
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.view.Window
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import android.widget.Button
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import android.widget.EditText
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.ListView
|
||||||
|
import android.widget.PopupMenu
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import androidx.core.view.get
|
||||||
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
|
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
|
||||||
import com.deniscerri.ytdl.BuildConfig
|
import com.deniscerri.ytdl.BuildConfig
|
||||||
import com.deniscerri.ytdl.R
|
import com.deniscerri.ytdl.R
|
||||||
|
import com.deniscerri.ytdl.database.models.CommandTemplate
|
||||||
import com.deniscerri.ytdl.util.UiUtil
|
import com.deniscerri.ytdl.util.UiUtil
|
||||||
|
import com.deniscerri.ytdl.util.UiUtil.showShortcutsSheet
|
||||||
import com.deniscerri.ytdl.util.UpdateUtil
|
import com.deniscerri.ytdl.util.UpdateUtil
|
||||||
|
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import com.google.android.material.chip.Chip
|
||||||
|
import com.google.android.material.chip.ChipGroup
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
import com.google.android.material.materialswitch.MaterialSwitch
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import com.google.android.material.textfield.TextInputLayout
|
||||||
import com.yausername.youtubedl_android.YoutubeDL
|
import com.yausername.youtubedl_android.YoutubeDL
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
|
||||||
class UpdateSettingsFragment : BaseSettingsFragment() {
|
class UpdateSettingsFragment : BaseSettingsFragment() {
|
||||||
|
|
@ -25,39 +63,41 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
|
||||||
private var ytdlSource: Preference? = null
|
private var ytdlSource: Preference? = null
|
||||||
private var updateUtil: UpdateUtil? = null
|
private var updateUtil: UpdateUtil? = null
|
||||||
private var version: Preference? = null
|
private var version: Preference? = null
|
||||||
|
private lateinit var preferences: SharedPreferences
|
||||||
|
private lateinit var ytdlpUtil: YTDLPUtil
|
||||||
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
setPreferencesFromResource(R.xml.updating_preferences, rootKey)
|
setPreferencesFromResource(R.xml.updating_preferences, rootKey)
|
||||||
updateUtil = UpdateUtil(requireContext())
|
updateUtil = UpdateUtil(requireContext())
|
||||||
val preferences =
|
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||||
PreferenceManager.getDefaultSharedPreferences(requireContext())
|
|
||||||
val editor = preferences.edit()
|
|
||||||
updateYTDL = findPreference("update_ytdl")
|
updateYTDL = findPreference("update_ytdl")
|
||||||
ytdlVersion = findPreference("ytdl-version")
|
ytdlVersion = findPreference("ytdl-version")
|
||||||
ytdlSource = findPreference("ytdlp_source")
|
ytdlSource = findPreference("ytdlp_source_label")
|
||||||
|
ytdlpUtil = YTDLPUtil(requireContext())
|
||||||
|
setYTDLPVersion()
|
||||||
|
|
||||||
YoutubeDL.getInstance().version(context)?.let {
|
ytdlSource?.apply {
|
||||||
editor.putString("ytdl-version", it)
|
summary = preferences.getString("ytdlp_source_label", "")
|
||||||
editor.apply()
|
setOnPreferenceClickListener {
|
||||||
ytdlVersion!!.summary = it
|
UiUtil.showYTDLSourceBottomSheet(requireActivity(), preferences) { t, r ->
|
||||||
|
summary = t
|
||||||
|
preferences.edit().putString("ytdlp_source", r).apply()
|
||||||
|
preferences.edit().putString("ytdlp_source_label", t).apply()
|
||||||
|
initYTDLUpdate(r)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ytdlSource?.setOnPreferenceChangeListener { preference, newValue ->
|
|
||||||
initYTDLUpdate(editor)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ytdlVersion!!.setOnPreferenceClickListener {
|
ytdlVersion!!.setOnPreferenceClickListener {
|
||||||
initYTDLUpdate(editor)
|
initYTDLUpdate()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
updateYTDL!!.onPreferenceClickListener =
|
updateYTDL!!.onPreferenceClickListener =
|
||||||
Preference.OnPreferenceClickListener {
|
Preference.OnPreferenceClickListener {
|
||||||
initYTDLUpdate(editor)
|
initYTDLUpdate()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,30 +132,49 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initYTDLUpdate(editor: SharedPreferences.Editor) = lifecycleScope.launch {
|
private fun setYTDLPVersion() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
ytdlVersion!!.summary = ""
|
||||||
|
val version = withContext(Dispatchers.IO){
|
||||||
|
ytdlpUtil.getVersion()
|
||||||
|
}
|
||||||
|
preferences.edit().apply {
|
||||||
|
putString("ytdl-version", version)
|
||||||
|
apply()
|
||||||
|
}
|
||||||
|
ytdlVersion!!.summary = version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initYTDLUpdate(channel: String? = null) = lifecycleScope.launch {
|
||||||
Snackbar.make(requireView(),
|
Snackbar.make(requireView(),
|
||||||
requireContext().getString(R.string.ytdl_updating_started),
|
requireContext().getString(R.string.ytdl_updating_started),
|
||||||
Snackbar.LENGTH_LONG).show()
|
Snackbar.LENGTH_LONG).show()
|
||||||
runCatching {
|
runCatching {
|
||||||
when (updateUtil!!.updateYoutubeDL()) {
|
val res = updateUtil!!.updateYoutubeDL(channel)
|
||||||
YoutubeDL.UpdateStatus.DONE -> {
|
when (res.status) {
|
||||||
Snackbar.make(requireView(),
|
UpdateUtil.YTDLPUpdateStatus.DONE -> {
|
||||||
requireContext().getString(R.string.ytld_update_success),
|
Snackbar.make(requireView(), res.message, Snackbar.LENGTH_LONG).show()
|
||||||
Snackbar.LENGTH_LONG).show()
|
setYTDLPVersion()
|
||||||
|
|
||||||
YoutubeDL.getInstance().version(context)?.let {
|
|
||||||
editor.putString("ytdl-version", it)
|
|
||||||
editor.apply()
|
|
||||||
ytdlVersion!!.summary = it
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
YoutubeDL.UpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(requireView(),
|
UpdateUtil.YTDLPUpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(requireView(),
|
||||||
requireContext().getString(R.string.you_are_in_latest_version),
|
requireContext().getString(R.string.you_are_in_latest_version),
|
||||||
Snackbar.LENGTH_LONG).show()
|
Snackbar.LENGTH_LONG).show()
|
||||||
|
UpdateUtil.YTDLPUpdateStatus.ERROR -> {
|
||||||
|
val msg = res.message
|
||||||
|
view?.apply {
|
||||||
|
val snackBar = Snackbar.make(this, msg, Snackbar.LENGTH_LONG)
|
||||||
|
snackBar.setAction(R.string.copy_log){
|
||||||
|
UiUtil.copyToClipboard(msg, requireActivity())
|
||||||
|
}
|
||||||
|
val snackbarView: View = snackBar.view
|
||||||
|
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
|
||||||
|
snackTextView.maxLines = 9999999
|
||||||
|
snackBar.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
else -> {
|
else -> {
|
||||||
Snackbar.make(requireView(),
|
|
||||||
requireContext().getString(R.string.errored),
|
|
||||||
Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,9 @@ object FileUtil {
|
||||||
request.getArguments("--config-locations")?.forEach {
|
request.getArguments("--config-locations")?.forEach {
|
||||||
if (it != null) File(it).delete()
|
if (it != null) File(it).delete()
|
||||||
}
|
}
|
||||||
|
request.getArguments("-o")?.firstOrNull { it?.startsWith("infojson:") == true }?.apply {
|
||||||
|
File(this.removePrefix("infojson:")).delete()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import android.text.format.DateFormat
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewTreeObserver
|
import android.view.ViewTreeObserver
|
||||||
|
|
@ -28,6 +29,8 @@ import android.widget.Button
|
||||||
import android.widget.CheckBox
|
import android.widget.CheckBox
|
||||||
import android.widget.EditText
|
import android.widget.EditText
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
|
import android.widget.PopupMenu
|
||||||
|
import android.widget.RadioButton
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.DimenRes
|
import androidx.annotation.DimenRes
|
||||||
|
|
@ -38,7 +41,9 @@ import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.core.text.parseAsHtml
|
import androidx.core.text.parseAsHtml
|
||||||
|
import androidx.core.view.children
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.core.widget.doAfterTextChanged
|
||||||
import androidx.core.widget.doOnTextChanged
|
import androidx.core.widget.doOnTextChanged
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
|
@ -2061,4 +2066,139 @@ object UiUtil {
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showAddEditCustomYTDLPSource(context: Activity, title: String = "", repo: String = "", created: (title: String, repo: String) -> Unit) {
|
||||||
|
val builder = MaterialAlertDialogBuilder(context)
|
||||||
|
builder.setTitle(context.getString(R.string.new_source))
|
||||||
|
val view = context.layoutInflater.inflate(R.layout.create_ytdlp_sources, null)
|
||||||
|
val titleTextInput = view.findViewById<TextInputLayout>(R.id.title_textinput).editText!!
|
||||||
|
val repoTextInput = view.findViewById<TextInputLayout>(R.id.repo_textinput).editText!!
|
||||||
|
titleTextInput.setText(title)
|
||||||
|
repoTextInput.setText(repo)
|
||||||
|
builder.setView(view)
|
||||||
|
builder.setPositiveButton(context.getString(R.string.add)) { _: DialogInterface?, _: Int ->
|
||||||
|
val t = titleTextInput.text.toString()
|
||||||
|
val r = repoTextInput.text.toString()
|
||||||
|
created(t, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle the negative button of the alert dialog
|
||||||
|
builder.setNegativeButton(
|
||||||
|
context.getString(R.string.cancel)
|
||||||
|
) { _: DialogInterface?, _: Int -> }
|
||||||
|
|
||||||
|
val dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
|
||||||
|
val createBtn = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||||
|
createBtn.isEnabled = titleTextInput.text.toString().isNotBlank() && repoTextInput.text.toString().isNotBlank()
|
||||||
|
titleTextInput.doAfterTextChanged {
|
||||||
|
createBtn.isEnabled = titleTextInput.text.toString().isNotBlank() && repoTextInput.text.toString().isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
repoTextInput.doAfterTextChanged {
|
||||||
|
createBtn.isEnabled = titleTextInput.text.toString().isNotBlank() && repoTextInput.text.toString().isNotBlank()
|
||||||
|
}
|
||||||
|
|
||||||
|
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||||
|
repoTextInput.postDelayed({
|
||||||
|
repoTextInput.requestFocus()
|
||||||
|
imm.showSoftInput(repoTextInput, 0)
|
||||||
|
}, 300)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun showYTDLSourceBottomSheet(context: Activity, preferences: SharedPreferences, selectedSource: (title: String, repo: String) -> Unit) {
|
||||||
|
val bottomSheet = BottomSheetDialog(context)
|
||||||
|
bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
bottomSheet.setContentView(R.layout.ytdlp_sources_list)
|
||||||
|
|
||||||
|
val list = preferences.getStringSet("custom_ytdlp_sources", emptySet())!!.toMutableList()
|
||||||
|
val parentView = bottomSheet.findViewById<LinearLayout>(R.id.sourcesList)!!
|
||||||
|
|
||||||
|
bottomSheet.findViewById<View>(R.id.add)?.apply {
|
||||||
|
setOnClickListener {
|
||||||
|
showAddEditCustomYTDLPSource(context) { title, repo ->
|
||||||
|
list.add("${title}___${repo}")
|
||||||
|
preferences.edit().putStringSet("custom_ytdlp_sources", list.toSet()).apply()
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
selectedSource(title, repo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val defaultSourceTitles = context.getStringArray(R.array.ytdlp_source)
|
||||||
|
val defaultSourceValues = context.getStringArray(R.array.ytdlp_source_values)
|
||||||
|
val tmp = list.toMutableList()
|
||||||
|
tmp.addAll(0, defaultSourceTitles.mapIndexed { index, s -> "${s}___${defaultSourceValues[index]}" })
|
||||||
|
tmp.forEach { s ->
|
||||||
|
val title = s.split("___")[0]
|
||||||
|
val source = s.split("___")[1]
|
||||||
|
val isEditable = !defaultSourceValues.contains(source)
|
||||||
|
val child = LayoutInflater.from(context).inflate(R.layout.custom_ytdlp_source, null)
|
||||||
|
child.findViewById<MaterialCardView>(R.id.sampleCustomSource).setOnClickListener {
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
selectedSource(title, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
child.findViewById<TextView>(R.id.sampleTitle).apply {
|
||||||
|
text = title
|
||||||
|
}
|
||||||
|
child.findViewById<RadioButton>(R.id.sampleRadioBtn).apply {
|
||||||
|
isChecked = preferences.getString("ytdlp_source", "stable") == source
|
||||||
|
setOnClickListener {
|
||||||
|
bottomSheet.dismiss()
|
||||||
|
selectedSource(title, source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
child.findViewById<TextView>(R.id.sampleRepo).text = source
|
||||||
|
child.findViewById<View>(R.id.options).apply {
|
||||||
|
isVisible = isEditable
|
||||||
|
setOnClickListener {
|
||||||
|
val popup = PopupMenu(context, it)
|
||||||
|
popup.menuInflater.inflate(R.menu.custom_ytdlp_source_menu, popup.menu)
|
||||||
|
popup.setOnMenuItemClickListener { m ->
|
||||||
|
when (m.itemId) {
|
||||||
|
R.id.edit -> {
|
||||||
|
popup.dismiss()
|
||||||
|
showAddEditCustomYTDLPSource(context, title, source) { nt, nr ->
|
||||||
|
child.findViewById<TextView>(R.id.sampleTitle).text = nt
|
||||||
|
child.findViewById<TextView>(R.id.sampleRepo).text = nr
|
||||||
|
val index = list.indexOf(s)
|
||||||
|
list[index] = "${nt}___${nr}"
|
||||||
|
preferences.edit().putStringSet("custom_ytdlp_sources", list.toSet()).apply()
|
||||||
|
if (child.findViewById<RadioButton>(R.id.sampleRadioBtn).isChecked) {
|
||||||
|
selectedSource(nt, nr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
R.id.remove -> {
|
||||||
|
popup.dismiss()
|
||||||
|
showGenericDeleteDialog(context, title) {
|
||||||
|
list.remove(s)
|
||||||
|
preferences.edit()
|
||||||
|
.putStringSet("custom_ytdlp_sources", list.toSet()).apply()
|
||||||
|
if (child.findViewById<RadioButton>(R.id.sampleRadioBtn).isChecked) {
|
||||||
|
parentView.children.first().performClick()
|
||||||
|
}
|
||||||
|
parentView.removeView(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
popup.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
parentView.addView(child)
|
||||||
|
}
|
||||||
|
|
||||||
|
bottomSheet.show()
|
||||||
|
bottomSheet.behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
bottomSheet.window!!.setLayout(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -14,44 +14,33 @@ import android.os.Bundle
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.text.format.DateFormat
|
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.ScrollView
|
|
||||||
import android.widget.TextView
|
import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.core.content.ContextCompat.startActivity
|
import androidx.core.content.ContextCompat.startActivity
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import androidx.recyclerview.widget.RecyclerView.LayoutManager
|
|
||||||
import com.deniscerri.ytdl.BuildConfig
|
import com.deniscerri.ytdl.BuildConfig
|
||||||
import com.deniscerri.ytdl.R
|
import com.deniscerri.ytdl.R
|
||||||
import com.deniscerri.ytdl.database.models.GithubRelease
|
import com.deniscerri.ytdl.database.models.GithubRelease
|
||||||
import com.deniscerri.ytdl.ui.adapter.ChangelogAdapter
|
import com.deniscerri.ytdl.ui.adapter.ChangelogAdapter
|
||||||
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
|
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
|
||||||
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.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.reflect.TypeToken
|
import com.google.gson.reflect.TypeToken
|
||||||
import com.yausername.youtubedl_android.YoutubeDL
|
import com.yausername.youtubedl_android.YoutubeDL
|
||||||
import com.yausername.youtubedl_android.YoutubeDL.UpdateStatus
|
import com.yausername.youtubedl_android.YoutubeDLRequest
|
||||||
import io.noties.markwon.AbstractMarkwonPlugin
|
import io.noties.markwon.AbstractMarkwonPlugin
|
||||||
import io.noties.markwon.Markwon
|
import io.noties.markwon.Markwon
|
||||||
import io.noties.markwon.MarkwonConfiguration
|
import io.noties.markwon.MarkwonConfiguration
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
|
|
||||||
class UpdateUtil(var context: Context) {
|
class UpdateUtil(var context: Context) {
|
||||||
|
|
@ -220,24 +209,35 @@ class UpdateUtil(var context: Context) {
|
||||||
return json
|
return json
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateYoutubeDL() : UpdateStatus? =
|
data class YTDLPUpdateResponse (
|
||||||
|
val status: YTDLPUpdateStatus,
|
||||||
|
val message: String = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class YTDLPUpdateStatus {
|
||||||
|
DONE, ALREADY_UP_TO_DATE, PROCESSING, ERROR
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun updateYoutubeDL(c: String? = null) : YTDLPUpdateResponse =
|
||||||
withContext(Dispatchers.IO){
|
withContext(Dispatchers.IO){
|
||||||
val sharedPreferences =
|
val sharedPreferences =
|
||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
if (updatingYTDL) {
|
if (updatingYTDL) {
|
||||||
UpdateStatus.ALREADY_UP_TO_DATE
|
YTDLPUpdateResponse(YTDLPUpdateStatus.PROCESSING)
|
||||||
}
|
}
|
||||||
|
|
||||||
updatingYTDL = true
|
updatingYTDL = true
|
||||||
|
|
||||||
val channelMap = mapOf(
|
val channel = if (c.isNullOrBlank()) sharedPreferences.getString("ytdlp_source", "nightly") else c
|
||||||
"stable" to YoutubeDL.UpdateChannel._STABLE,
|
val request = YoutubeDLRequest(emptyList())
|
||||||
"nightly" to YoutubeDL.UpdateChannel._NIGHTLY,
|
request.addOption("--update-to", "${channel}@latest")
|
||||||
"master" to YoutubeDL.UpdateChannel._MASTER,
|
|
||||||
)
|
val res = YoutubeDL.getInstance().execute(request)
|
||||||
val channel = sharedPreferences.getString("ytdlp_source", "nightly")
|
val out = res.out.split(System.getProperty("line.separator")).filter { it.isNotBlank() }.last()
|
||||||
YoutubeDL.getInstance().updateYoutubeDL(context, channelMap[channel] ?: YoutubeDL.UpdateChannel._NIGHTLY).apply {
|
|
||||||
updatingYTDL = false
|
if (out.contains("ERROR")) YTDLPUpdateResponse(YTDLPUpdateStatus.ERROR, out)
|
||||||
}
|
if (out.contains("yt-dlp is up to date")) YTDLPUpdateResponse(YTDLPUpdateStatus.ALREADY_UP_TO_DATE, out)
|
||||||
|
else YTDLPUpdateResponse(YTDLPUpdateStatus.DONE, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.lang.reflect.Type
|
import java.lang.reflect.Type
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.util.ArrayList
|
import java.util.ArrayList
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
@ -45,9 +46,43 @@ class YTDLPUtil(private val context: Context) {
|
||||||
private val formatUtil = FormatUtil(context)
|
private val formatUtil = FormatUtil(context)
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
private fun YoutubeDLRequest.applyDefaultOptionsForFetchingData() {
|
||||||
|
addOption("--skip-download")
|
||||||
|
addOption("-R", "1")
|
||||||
|
addOption("--compat-options", "manifest-filesize-approx")
|
||||||
|
val socketTimeout = sharedPreferences.getString("socket_timeout", "5")!!.ifEmpty { "5" }
|
||||||
|
addOption("--socket-timeout", socketTimeout)
|
||||||
|
|
||||||
|
if (sharedPreferences.getBoolean("force_ipv4", false)){
|
||||||
|
addOption("-4")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sharedPreferences.getBoolean("use_cookies", false)){
|
||||||
|
FileUtil.getCookieFile(context){
|
||||||
|
addOption("--cookies", it)
|
||||||
|
}
|
||||||
|
|
||||||
|
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
||||||
|
val header = sharedPreferences.getString("useragent_header", "")
|
||||||
|
if (useHeader && !header.isNullOrBlank()){
|
||||||
|
addOption("--add-header","User-Agent:${header}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val proxy = sharedPreferences.getString("proxy", "")
|
||||||
|
if (proxy!!.isNotBlank()){
|
||||||
|
addOption("--proxy", proxy)
|
||||||
|
}
|
||||||
|
addOption("-P", FileUtil.getCachePath(context) + "/tmp")
|
||||||
|
|
||||||
|
val extraCommands = sharedPreferences.getString("data_fetching_extra_commands", "")!!
|
||||||
|
if (extraCommands.isNotBlank()){
|
||||||
|
addConfig(extraCommands)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
fun getFromYTDL(query: String, singleItem: Boolean = false): ArrayList<ResultItem> {
|
fun getFromYTDL(query: String, singleItem: Boolean = false): ArrayList<ResultItem> {
|
||||||
val items = arrayListOf<ResultItem>()
|
|
||||||
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
|
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
|
||||||
|
|
||||||
val request : YoutubeDLRequest
|
val request : YoutubeDLRequest
|
||||||
|
|
@ -78,38 +113,7 @@ class YTDLPUtil(private val context: Context) {
|
||||||
|
|
||||||
request.addOption("--flat-playlist")
|
request.addOption("--flat-playlist")
|
||||||
request.addOption(if (singleItem) "-J" else "-j")
|
request.addOption(if (singleItem) "-J" else "-j")
|
||||||
request.addOption("--skip-download")
|
request.applyDefaultOptionsForFetchingData()
|
||||||
request.addOption("-R", "1")
|
|
||||||
request.addOption("--compat-options", "manifest-filesize-approx")
|
|
||||||
val socketTimeout = sharedPreferences.getString("socket_timeout", "5")!!.ifEmpty { "5" }
|
|
||||||
request.addOption("--socket-timeout", socketTimeout)
|
|
||||||
|
|
||||||
if (sharedPreferences.getBoolean("force_ipv4", false)){
|
|
||||||
request.addOption("-4")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
|
||||||
FileUtil.getCookieFile(context){
|
|
||||||
request.addOption("--cookies", it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
|
||||||
val header = sharedPreferences.getString("useragent_header", "")
|
|
||||||
if (useHeader && !header.isNullOrBlank()){
|
|
||||||
request.addOption("--add-header","User-Agent:${header}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val proxy = sharedPreferences.getString("proxy", "")
|
|
||||||
if (proxy!!.isNotBlank()){
|
|
||||||
request.addOption("--proxy", proxy)
|
|
||||||
}
|
|
||||||
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
|
|
||||||
|
|
||||||
val extraCommands = sharedPreferences.getString("data_fetching_extra_commands", "")!!
|
|
||||||
if (extraCommands.isNotBlank()){
|
|
||||||
request.addConfig(extraCommands)
|
|
||||||
}
|
|
||||||
|
|
||||||
println(parseYTDLRequestString(request))
|
println(parseYTDLRequestString(request))
|
||||||
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
||||||
|
|
@ -121,6 +125,13 @@ class YTDLPUtil(private val context: Context) {
|
||||||
}.filter { it.isNotBlank() }.apply {
|
}.filter { it.isNotBlank() }.apply {
|
||||||
if (this.isEmpty()) throw Exception("Command Used: \n yt-dlp ${parseYTDLRequestString(request)}")
|
if (this.isEmpty()) throw Exception("Command Used: \n yt-dlp ${parseYTDLRequestString(request)}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return parseYTDLPListResults(results, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseYTDLPListResults(results: List<String?>, query: String = "") : ArrayList<ResultItem> {
|
||||||
|
val items = arrayListOf<ResultItem>()
|
||||||
|
|
||||||
for (result in results) {
|
for (result in results) {
|
||||||
if (result.isNullOrBlank()) continue
|
if (result.isNullOrBlank()) continue
|
||||||
val jsonObject = JSONObject(result)
|
val jsonObject = JSONObject(result)
|
||||||
|
|
@ -163,7 +174,7 @@ class YTDLPUtil(private val context: Context) {
|
||||||
if(playlistTitle.removeSurrounding("\"") == query) playlistTitle = ""
|
if(playlistTitle.removeSurrounding("\"") == query) playlistTitle = ""
|
||||||
|
|
||||||
if (playlistTitle.isNotBlank()){
|
if (playlistTitle.isNotBlank()){
|
||||||
playlistURL = query
|
playlistURL = jsonObject.getStringByAny("playlist_webpage_url").ifEmpty { query }
|
||||||
kotlin.runCatching { playlistIndex = jsonObject.getInt("playlist_index") }
|
kotlin.runCatching { playlistIndex = jsonObject.getInt("playlist_index") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -223,9 +234,85 @@ class YTDLPUtil(private val context: Context) {
|
||||||
|
|
||||||
items.add(res)
|
items.add(res)
|
||||||
}
|
}
|
||||||
return items
|
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getYoutubeWatchLater() : ArrayList<ResultItem> {
|
||||||
|
val request = YoutubeDLRequest(listOf())
|
||||||
|
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
|
||||||
|
request.addOption( "-j")
|
||||||
|
request.addOption("--flat-playlist")
|
||||||
|
request.applyDefaultOptionsForFetchingData()
|
||||||
|
request.addOption(":ytwatchlater")
|
||||||
|
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
||||||
|
val results: List<String?> = try {
|
||||||
|
val lineSeparator = System.getProperty("line.separator")
|
||||||
|
youtubeDLResponse.out.split(lineSeparator!!)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
listOf(youtubeDLResponse.out)
|
||||||
|
}.filter { it.isNotBlank() }.apply {
|
||||||
|
if (this.isEmpty()) return arrayListOf()
|
||||||
|
}
|
||||||
|
return parseYTDLPListResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getYoutubeRecommendations() : ArrayList<ResultItem> {
|
||||||
|
val request = YoutubeDLRequest(listOf())
|
||||||
|
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
|
||||||
|
request.addOption( "-j")
|
||||||
|
request.addOption("--flat-playlist")
|
||||||
|
request.applyDefaultOptionsForFetchingData()
|
||||||
|
request.addOption(":ytrec")
|
||||||
|
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
||||||
|
val results: List<String?> = try {
|
||||||
|
val lineSeparator = System.getProperty("line.separator")
|
||||||
|
youtubeDLResponse.out.split(lineSeparator!!)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
listOf(youtubeDLResponse.out)
|
||||||
|
}.filter { it.isNotBlank() }.apply {
|
||||||
|
if (this.isEmpty()) return arrayListOf()
|
||||||
|
}
|
||||||
|
return parseYTDLPListResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getYoutubeLikedVideos() : ArrayList<ResultItem> {
|
||||||
|
val request = YoutubeDLRequest(listOf())
|
||||||
|
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
|
||||||
|
request.addOption( "-j")
|
||||||
|
request.addOption("--flat-playlist")
|
||||||
|
request.applyDefaultOptionsForFetchingData()
|
||||||
|
request.addOption(":ytfav")
|
||||||
|
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
||||||
|
val results: List<String?> = try {
|
||||||
|
val lineSeparator = System.getProperty("line.separator")
|
||||||
|
youtubeDLResponse.out.split(lineSeparator!!)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
listOf(youtubeDLResponse.out)
|
||||||
|
}.filter { it.isNotBlank() }.apply {
|
||||||
|
if (this.isEmpty()) return arrayListOf()
|
||||||
|
}
|
||||||
|
return parseYTDLPListResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getYoutubeWatchHistory() : ArrayList<ResultItem> {
|
||||||
|
val request = YoutubeDLRequest(listOf())
|
||||||
|
request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}")
|
||||||
|
request.addOption( "-j")
|
||||||
|
request.addOption("--flat-playlist")
|
||||||
|
request.applyDefaultOptionsForFetchingData()
|
||||||
|
request.addOption(":ythis")
|
||||||
|
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
||||||
|
val results: List<String?> = try {
|
||||||
|
val lineSeparator = System.getProperty("line.separator")
|
||||||
|
youtubeDLResponse.out.split(lineSeparator!!)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
listOf(youtubeDLResponse.out)
|
||||||
|
}.filter { it.isNotBlank() }.apply {
|
||||||
|
if (this.isEmpty()) return arrayListOf()
|
||||||
|
}
|
||||||
|
return parseYTDLPListResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getFormatsForAll(urls: List<String>, progress: (progress: ResultViewModel.MultipleFormatProgress) -> Unit) : Result<MutableList<MutableList<Format>>> {
|
suspend fun getFormatsForAll(urls: List<String>, progress: (progress: ResultViewModel.MultipleFormatProgress) -> Unit) : Result<MutableList<MutableList<Format>>> {
|
||||||
val formatCollection = mutableListOf<MutableList<Format>>()
|
val formatCollection = mutableListOf<MutableList<Format>>()
|
||||||
|
|
@ -243,38 +330,7 @@ class YTDLPUtil(private val context: Context) {
|
||||||
val request = YoutubeDLRequest(emptyList())
|
val request = YoutubeDLRequest(emptyList())
|
||||||
request.addOption("--print", "formats")
|
request.addOption("--print", "formats")
|
||||||
request.addOption("-a", urlsFile.absolutePath)
|
request.addOption("-a", urlsFile.absolutePath)
|
||||||
request.addOption("--skip-download")
|
request.applyDefaultOptionsForFetchingData()
|
||||||
request.addOption("-R", "1")
|
|
||||||
val socketTimeout = sharedPreferences.getString("socket_timeout", "5")!!.ifEmpty { "5" }
|
|
||||||
request.addOption("--socket-timeout", socketTimeout)
|
|
||||||
|
|
||||||
if (sharedPreferences.getBoolean("force_ipv4", false)){
|
|
||||||
request.addOption("-4")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
|
||||||
FileUtil.getCookieFile(context){
|
|
||||||
request.addOption("--cookies", it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
|
||||||
val header = sharedPreferences.getString("useragent_header", "")
|
|
||||||
if (useHeader && !header.isNullOrBlank()){
|
|
||||||
request.addOption("--add-header","User-Agent:${header}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val proxy = sharedPreferences.getString("proxy", "")
|
|
||||||
if (proxy!!.isNotBlank()){
|
|
||||||
request.addOption("--proxy", proxy)
|
|
||||||
}
|
|
||||||
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
|
|
||||||
|
|
||||||
val extraCommands = sharedPreferences.getString("data_fetching_extra_commands", "")!!
|
|
||||||
if (extraCommands.isNotBlank()){
|
|
||||||
request.addConfig(extraCommands)
|
|
||||||
}
|
|
||||||
|
|
||||||
val txt = parseYTDLRequestString(request)
|
val txt = parseYTDLRequestString(request)
|
||||||
println(txt)
|
println(txt)
|
||||||
|
|
@ -343,36 +399,7 @@ class YTDLPUtil(private val context: Context) {
|
||||||
val request = YoutubeDLRequest(url)
|
val request = YoutubeDLRequest(url)
|
||||||
request.addOption("--print", "%(formats)s")
|
request.addOption("--print", "%(formats)s")
|
||||||
request.addOption("--print", "%(duration)s")
|
request.addOption("--print", "%(duration)s")
|
||||||
request.addOption("--skip-download")
|
request.applyDefaultOptionsForFetchingData()
|
||||||
request.addOption("-R", "1")
|
|
||||||
request.addOption("--compat-options", "manifest-filesize-approx")
|
|
||||||
|
|
||||||
if (sharedPreferences.getBoolean("force_ipv4", false)){
|
|
||||||
request.addOption("-4")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
|
||||||
FileUtil.getCookieFile(context){
|
|
||||||
request.addOption("--cookies", it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
|
||||||
val header = sharedPreferences.getString("useragent_header", "")
|
|
||||||
if (useHeader && !header.isNullOrBlank()){
|
|
||||||
request.addOption("--add-header","User-Agent:${header}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val proxy = sharedPreferences.getString("proxy", "")
|
|
||||||
if (proxy!!.isNotBlank()) {
|
|
||||||
request.addOption("--proxy", proxy)
|
|
||||||
}
|
|
||||||
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
|
|
||||||
|
|
||||||
val extraCommands = sharedPreferences.getString("data_fetching_extra_commands", "")!!
|
|
||||||
if (extraCommands.isNotBlank()){
|
|
||||||
request.addConfig(extraCommands)
|
|
||||||
}
|
|
||||||
|
|
||||||
val res = YoutubeDL.getInstance().execute(request)
|
val res = YoutubeDL.getInstance().execute(request)
|
||||||
val results: Array<String?> = try {
|
val results: Array<String?> = try {
|
||||||
|
|
@ -447,37 +474,7 @@ class YTDLPUtil(private val context: Context) {
|
||||||
val request = YoutubeDLRequest(url)
|
val request = YoutubeDLRequest(url)
|
||||||
request.addOption("--get-url")
|
request.addOption("--get-url")
|
||||||
request.addOption("--print", "%(.{urls,chapters})s")
|
request.addOption("--print", "%(.{urls,chapters})s")
|
||||||
request.addOption("--skip-download")
|
request.applyDefaultOptionsForFetchingData()
|
||||||
request.addOption("-R", "1")
|
|
||||||
val socketTimeout = sharedPreferences.getString("socket_timeout", "5")!!.ifEmpty { "5" }
|
|
||||||
request.addOption("--socket-timeout", socketTimeout)
|
|
||||||
|
|
||||||
if (sharedPreferences.getBoolean("force_ipv4", false)){
|
|
||||||
request.addOption("-4")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sharedPreferences.getBoolean("use_cookies", false)){
|
|
||||||
FileUtil.getCookieFile(context){
|
|
||||||
request.addOption("--cookies", it)
|
|
||||||
}
|
|
||||||
|
|
||||||
val useHeader = sharedPreferences.getBoolean("use_header", false)
|
|
||||||
val header = sharedPreferences.getString("useragent_header", "")
|
|
||||||
if (useHeader && !header.isNullOrBlank()){
|
|
||||||
request.addOption("--add-header","User-Agent:${header}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val proxy = sharedPreferences.getString("proxy", "")
|
|
||||||
if (proxy!!.isNotBlank()){
|
|
||||||
request.addOption("--proxy", proxy)
|
|
||||||
}
|
|
||||||
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
|
|
||||||
|
|
||||||
val extraCommands = sharedPreferences.getString("data_fetching_extra_commands", "")!!
|
|
||||||
if (extraCommands.isNotBlank()){
|
|
||||||
request.addConfig(extraCommands)
|
|
||||||
}
|
|
||||||
|
|
||||||
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
|
||||||
val json = JSONObject(youtubeDLResponse.out)
|
val json = JSONObject(youtubeDLResponse.out)
|
||||||
|
|
@ -726,12 +723,15 @@ class YTDLPUtil(private val context: Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sharedPreferences.getBoolean("disable_write_info_json", false)) {
|
if (!sharedPreferences.getBoolean("disable_write_info_json", false)) {
|
||||||
val infoJsonFile = downDir.walkTopDown().firstOrNull { it.name == "${downloadItem.id}.info.json" }
|
val cachePath = "${FileUtil.getCachePath(context)}/infojsons"
|
||||||
|
val infoJsonName = downloadItem.url.replace("/", "")
|
||||||
|
|
||||||
|
val infoJsonFile = File(cachePath).walkTopDown().firstOrNull { it.name == "${infoJsonName}.info.json" }
|
||||||
//ignore info file if its older than 5 hours. puny measure to prevent expired formats in some cases
|
//ignore info file if its older than 5 hours. puny measure to prevent expired formats in some cases
|
||||||
if (infoJsonFile == null || System.currentTimeMillis() - infoJsonFile.lastModified() > (1000 * 60 * 60 * 5)) {
|
if (infoJsonFile == null || System.currentTimeMillis() - infoJsonFile.lastModified() > (1000 * 60 * 60 * 5)) {
|
||||||
request.addOption("--write-info-json")
|
request.addOption("--write-info-json")
|
||||||
request.addOption("--no-clean-info-json")
|
request.addOption("--no-clean-info-json")
|
||||||
request.addOption("-o", "infojson:${downloadItem.id}")
|
request.addOption("-o", "infojson:${cachePath}/${infoJsonName}")
|
||||||
}else {
|
}else {
|
||||||
request.addOption("--load-info-json", infoJsonFile.absolutePath)
|
request.addOption("--load-info-json", infoJsonFile.absolutePath)
|
||||||
}
|
}
|
||||||
|
|
@ -807,7 +807,7 @@ class YTDLPUtil(private val context: Context) {
|
||||||
request.addOption("-P", downDir.absolutePath)
|
request.addOption("-P", downDir.absolutePath)
|
||||||
request.addOption("-S", formatSorting.toString())
|
request.addOption("-S", formatSorting.toString())
|
||||||
|
|
||||||
metadataCommands.addOption("--parse-metadata", """%(uploader,artist,channel,creator|null)s:^(?P<uploader>.*?)(?:(?= - Topic)|$)""")
|
metadataCommands.addOption("--parse-metadata", """%(artists,artist,uploader,channel,creator|null)l:^(?P<uploader>.*?)(?:(?= - Topic)|$)""")
|
||||||
|
|
||||||
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
|
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
|
||||||
request.addOption("--split-chapters")
|
request.addOption("--split-chapters")
|
||||||
|
|
@ -1137,6 +1137,12 @@ class YTDLPUtil(private val context: Context) {
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getVersion() : String {
|
||||||
|
val req = YoutubeDLRequest(emptyList())
|
||||||
|
req.addOption("--version")
|
||||||
|
return YoutubeDL.getInstance().execute(req).out.trim()
|
||||||
|
}
|
||||||
|
|
||||||
private fun getYoutubeExtractorArgs() : String {
|
private fun getYoutubeExtractorArgs() : String {
|
||||||
val playerClient = sharedPreferences.getString("youtube_player_client", "default,mediaconnect")!!.split(",").filter { it.isNotBlank() }.toMutableList()
|
val playerClient = sharedPreferences.getString("youtube_player_client", "default,mediaconnect")!!.split(",").filter { it.isNotBlank() }.toMutableList()
|
||||||
val extractorArgs = mutableListOf<String>()
|
val extractorArgs = mutableListOf<String>()
|
||||||
|
|
|
||||||
|
|
@ -10,31 +10,21 @@ import com.deniscerri.ytdl.database.models.ResultItem
|
||||||
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
|
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
|
||||||
import com.deniscerri.ytdl.util.Extensions.toStringDuration
|
import com.deniscerri.ytdl.util.Extensions.toStringDuration
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import kotlinx.serialization.Serializer
|
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import org.json.JSONException
|
import org.json.JSONException
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
import org.schabi.newpipe.extractor.NewPipe
|
||||||
import org.schabi.newpipe.extractor.Page
|
import org.schabi.newpipe.extractor.Page
|
||||||
import org.schabi.newpipe.extractor.ServiceList
|
import org.schabi.newpipe.extractor.ServiceList
|
||||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
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.channel.tabs.ChannelTabInfo
|
||||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo
|
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.linkhandler.ListLinkHandler
|
||||||
import org.schabi.newpipe.extractor.localization.ContentCountry
|
|
||||||
import org.schabi.newpipe.extractor.localization.Localization
|
import org.schabi.newpipe.extractor.localization.Localization
|
||||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
|
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.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.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.coroutines.cancellation.CancellationException
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
|
|
||||||
50
app/src/main/res/layout/create_ytdlp_sources.xml
Normal file
50
app/src/main/res/layout/create_ytdlp_sources.xml
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/title_textinput"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
android:hint="@string/title"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:inputType="text"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
|
android:id="@+id/repo_textinput"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:paddingHorizontal="10dp"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
app:expandedHintEnabled="true"
|
||||||
|
app:hintEnabled="true"
|
||||||
|
android:hint="@string/source"
|
||||||
|
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||||
|
|
||||||
|
<com.google.android.material.textfield.TextInputEditText
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:inputType="text"
|
||||||
|
android:hint="owner/repository"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:ignore="HardcodedText" />
|
||||||
|
|
||||||
|
</com.google.android.material.textfield.TextInputLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
72
app/src/main/res/layout/custom_ytdlp_source.xml
Normal file
72
app/src/main/res/layout/custom_ytdlp_source.xml
Normal file
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.card.MaterialCardView android:id="@+id/sampleCustomSource"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="10dp"
|
||||||
|
android:checkable="true"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:backgroundTint="@android:color/transparent"
|
||||||
|
app:checkedIcon="@null"
|
||||||
|
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
|
||||||
|
app:strokeWidth="0dp"
|
||||||
|
app:cardPreventCornerOverlap="true"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<RadioButton
|
||||||
|
android:id="@+id/sampleRadioBtn"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="10dp"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sampleTitle"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/options"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/sampleRadioBtn"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/sampleRepo"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/options"
|
||||||
|
app:layout_constraintStart_toEndOf="@id/sampleRadioBtn"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/sampleTitle" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView
|
||||||
|
android:id="@+id/options"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:clickable="false"
|
||||||
|
android:focusable="false"
|
||||||
|
android:padding="16dp"
|
||||||
|
android:tintMode="src_in"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/baseline_more_vert_24"
|
||||||
|
app:tint="?attr/colorControlNormal"
|
||||||
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</com.google.android.material.card.MaterialCardView>
|
||||||
|
|
@ -29,13 +29,6 @@
|
||||||
app:strokeWidth="0dp"
|
app:strokeWidth="0dp"
|
||||||
android:layout_margin="10dp">
|
android:layout_margin="10dp">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageView
|
|
||||||
android:id="@+id/result_image_view"
|
|
||||||
android:adjustViewBounds="true"
|
|
||||||
android:scaleType="centerCrop"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||||
android:id="@+id/download_progress"
|
android:id="@+id/download_progress"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
@ -71,9 +64,6 @@
|
||||||
android:paddingTop="10dp"
|
android:paddingTop="10dp"
|
||||||
android:paddingEnd="10dp"
|
android:paddingEnd="10dp"
|
||||||
android:scrollbars="none"
|
android:scrollbars="none"
|
||||||
android:shadowColor="@color/black"
|
|
||||||
android:shadowDx="4"
|
|
||||||
android:shadowDy="4"
|
|
||||||
android:shadowRadius="2"
|
android:shadowRadius="2"
|
||||||
android:textColor="#FFF"
|
android:textColor="#FFF"
|
||||||
android:textSize="17sp"
|
android:textSize="17sp"
|
||||||
|
|
@ -89,9 +79,6 @@
|
||||||
android:paddingStart="10dp"
|
android:paddingStart="10dp"
|
||||||
android:paddingEnd="10dp"
|
android:paddingEnd="10dp"
|
||||||
android:scrollbars="none"
|
android:scrollbars="none"
|
||||||
android:shadowColor="@color/black"
|
|
||||||
android:shadowDx="4"
|
|
||||||
android:shadowDy="4"
|
|
||||||
android:shadowRadius="1.5"
|
android:shadowRadius="1.5"
|
||||||
android:textColor="#FFF"
|
android:textColor="#FFF"
|
||||||
android:textSize="12sp"
|
android:textSize="12sp"
|
||||||
|
|
|
||||||
79
app/src/main/res/layout/ytdlp_sources_list.xml
Normal file
79
app/src/main/res/layout/ytdlp_sources_list.xml
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="20dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="20dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bottom_sheet_title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/ytdl_source"
|
||||||
|
android:textSize="25sp"
|
||||||
|
app:layout_constraintEnd_toStartOf="@+id/add"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bottom_sheet_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/select"
|
||||||
|
android:textSize="12sp"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0.0"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/add"
|
||||||
|
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:autoLink="all"
|
||||||
|
android:text="@string/add"
|
||||||
|
app:icon="@drawable/ic_check"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
<androidx.core.widget.NestedScrollView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:padding="15dp"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/sourcesList"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</androidx.core.widget.NestedScrollView>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
19
app/src/main/res/menu/custom_ytdlp_source_menu.xml
Normal file
19
app/src/main/res/menu/custom_ytdlp_source_menu.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:context=".MainActivity" >
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/edit"
|
||||||
|
android:title="@string/edit"
|
||||||
|
android:icon="@drawable/ic_edit"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/remove"
|
||||||
|
android:title="@string/Remove"
|
||||||
|
android:icon="@drawable/baseline_delete_24"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
|
|
||||||
|
|
@ -1143,6 +1143,25 @@
|
||||||
<item>master</item>
|
<item>master</item>
|
||||||
</array>
|
</array>
|
||||||
|
|
||||||
|
<array name="video_recommendations">
|
||||||
|
<item>@string/disabled</item>
|
||||||
|
<item>NewPipe</item>
|
||||||
|
<item>Youtube API</item>
|
||||||
|
<item>@string/ytdlp_watch_later</item>
|
||||||
|
<item>@string/ytdlp_recommnedations</item>
|
||||||
|
<item>@string/ytdlp_liked</item>
|
||||||
|
<item>@string/ytdlp_watch_history</item>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<array name="video_recommnedations_values">
|
||||||
|
<item></item>
|
||||||
|
<item>newpipe</item>
|
||||||
|
<item>yt_api</item>
|
||||||
|
<item>yt_dlp_watch_later</item>
|
||||||
|
<item>yt_dlp_recommendations</item>
|
||||||
|
<item>yt_dlp_liked</item>
|
||||||
|
<item>yt_dlp_watch_history</item>
|
||||||
|
</array>
|
||||||
|
|
||||||
<!--below is for preferred audio language, not related to app's language-->
|
<!--below is for preferred audio language, not related to app's language-->
|
||||||
<string-array name="language_codes">
|
<string-array name="language_codes">
|
||||||
|
|
|
||||||
|
|
@ -204,7 +204,7 @@
|
||||||
<string name="subtitle_languages">Subtitle languages</string>
|
<string name="subtitle_languages">Subtitle languages</string>
|
||||||
<string name="format_source">Formats Source</string>
|
<string name="format_source">Formats Source</string>
|
||||||
<string name="video_recommendations">Video Recommendations</string>
|
<string name="video_recommendations">Video Recommendations</string>
|
||||||
<string name="video_recommendations_summary">Get recommended YouTube videos on the home screen from the NewPipe Extractor</string>
|
<string name="video_recommendations_summary">Get recommended YouTube videos on the home screen</string>
|
||||||
<string name="preferred_search_engine">Preferred Search Engine</string>
|
<string name="preferred_search_engine">Preferred Search Engine</string>
|
||||||
<string name="preferred_search_engine_summary">The search engine to use for in-app searches</string>
|
<string name="preferred_search_engine_summary">The search engine to use for in-app searches</string>
|
||||||
<string name="format_filtering_hint">All items must be of the same type to use this option</string>
|
<string name="format_filtering_hint">All items must be of the same type to use this option</string>
|
||||||
|
|
@ -439,4 +439,8 @@
|
||||||
<string name="data_fetching_extra_command_summary">Enable command templates to be used for data fetching as extra commands</string>
|
<string name="data_fetching_extra_command_summary">Enable command templates to be used for data fetching as extra commands</string>
|
||||||
<string name="disable_write_info_json">Disable Write Info Json</string>
|
<string name="disable_write_info_json">Disable Write Info Json</string>
|
||||||
<string name="disable_write_info_json_summary">(Not Recommended) Every time you restart / resume the download, yt-dlp will re-download json data from the servers.</string>
|
<string name="disable_write_info_json_summary">(Not Recommended) Every time you restart / resume the download, yt-dlp will re-download json data from the servers.</string>
|
||||||
|
<string name="ytdlp_watch_later">YT-DLP Youtube Watch Later (needs cookies)</string>
|
||||||
|
<string name="ytdlp_recommnedations">YT-DLP Youtube Recommendations (needs cookies)</string>
|
||||||
|
<string name="ytdlp_liked">YT-DLP Youtube Liked Videos (needs cookies)</string>
|
||||||
|
<string name="ytdlp_watch_history">YT-DLP Youtube Watch History (needs cookies)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -70,11 +70,12 @@
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="YouTube">
|
<PreferenceCategory android:title="YouTube">
|
||||||
<SwitchPreferenceCompat
|
<ListPreference
|
||||||
android:widgetLayout="@layout/preferece_material_switch"
|
|
||||||
app:defaultValue="true"
|
|
||||||
android:icon="@drawable/baseline_recommend_24"
|
android:icon="@drawable/baseline_recommend_24"
|
||||||
android:key="home_recommendations"
|
android:key="youtube_home_recommendations"
|
||||||
|
android:defaultValue="newpipe"
|
||||||
|
android:entries="@array/video_recommendations"
|
||||||
|
android:entryValues="@array/video_recommnedations_values"
|
||||||
app:summary="@string/video_recommendations_summary"
|
app:summary="@string/video_recommendations_summary"
|
||||||
app:title="@string/video_recommendations" />
|
app:title="@string/video_recommendations" />
|
||||||
|
|
||||||
|
|
@ -129,7 +130,7 @@
|
||||||
android:entryValues="@array/countries_values"
|
android:entryValues="@array/countries_values"
|
||||||
app:icon="@drawable/ic_language"
|
app:icon="@drawable/ic_language"
|
||||||
app:useSimpleSummaryProvider="true"
|
app:useSimpleSummaryProvider="true"
|
||||||
app:dependency="home_recommendations"
|
app:dependency="youtube_home_recommendations"
|
||||||
app:key="locale"
|
app:key="locale"
|
||||||
app:title="@string/preferred_locale" />
|
app:title="@string/preferred_locale" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,13 +20,9 @@
|
||||||
app:summary="@string/ytdl_update_hint"
|
app:summary="@string/ytdl_update_hint"
|
||||||
app:title="@string/update_ytdl" />
|
app:title="@string/update_ytdl" />
|
||||||
|
|
||||||
<ListPreference
|
<Preference
|
||||||
android:defaultValue="stable"
|
|
||||||
android:entries="@array/ytdlp_source"
|
|
||||||
android:entryValues="@array/ytdlp_source_values"
|
|
||||||
android:icon="@drawable/baseline_source_24"
|
android:icon="@drawable/baseline_source_24"
|
||||||
app:key="ytdlp_source"
|
app:key="ytdlp_source_label"
|
||||||
app:useSimpleSummaryProvider="true"
|
|
||||||
app:title="@string/ytdl_source" />
|
app:title="@string/ytdl_source" />
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,4 +16,4 @@ android.nonFinalResIds=false
|
||||||
android.nonTransitiveRClass=false
|
android.nonTransitiveRClass=false
|
||||||
android.suppressUnsupportedCompileSdk=35
|
android.suppressUnsupportedCompileSdk=35
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx1024M" -Dfile.encoding\=UTF-8
|
org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue