more features

pull/648/head
deniscerri 2 years ago
parent 926e6ec73d
commit 4f04b039ad
No known key found for this signature in database
GPG Key ID: 95C43D517D830350

@ -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())
YoutubeDL.getInstance().version(context)?.let { setYTDLPVersion()
editor.putString("ytdl-version", it)
editor.apply() ytdlSource?.apply {
ytdlVersion!!.summary = it summary = preferences.getString("ytdlp_source_label", "")
} setOnPreferenceClickListener {
UiUtil.showYTDLSourceBottomSheet(requireActivity(), preferences) { t, r ->
ytdlSource?.setOnPreferenceChangeListener { preference, newValue -> summary = t
initYTDLUpdate(editor) preferences.edit().putString("ytdlp_source", r).apply()
true preferences.edit().putString("ytdlp_source_label", t).apply()
initYTDLUpdate(r)
}
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

@ -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>

@ -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"

@ -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>

@ -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…
Cancel
Save