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
- #618, made all preferences with a description show their values
- 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
@ -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...
(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)

@ -215,5 +215,5 @@ dependencies {
implementation 'androidx.compose.material3:material3-android:1.3.0'
implementation "io.noties.markwon:core:4.6.2"
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
import android.Manifest
import android.app.ActionBar.LayoutParams
import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.provider.Settings
import android.util.Log
import android.util.Patterns
import android.view.View
import android.view.WindowInsets
import android.widget.CheckBox
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat
import androidx.core.os.bundleOf
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach
import androidx.core.view.isVisible
@ -35,7 +27,6 @@ import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController
import androidx.navigation.NavOptions
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import androidx.navigation.ui.setupWithNavController
@ -68,13 +59,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.io.Reader
import java.nio.charset.Charset
@ -278,7 +266,7 @@ class MainActivity : BaseActivity() {
CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) {
kotlin.runCatching {
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 snack = Snackbar.make(findViewById(R.id.frame_layout),
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.util.Patterns
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.database.DBManager.SORTING
import com.deniscerri.ytdl.database.dao.ResultDao
import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.repository.HistoryRepository.HistorySortType
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.isYoutubeWatchVideosURL
import com.deniscerri.ytdl.util.FileUtil
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.YTDLPUtil
import com.deniscerri.ytdl.util.extractors.YoutubeApiUtil
import com.yausername.youtubedl_android.YoutubeDLException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
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 ytdlpUtil = YTDLPUtil(context)
private var newPipeUtil : NewPipeUtil? = null
private var newPipeUtil = NewPipeUtil(context)
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
init {
if (sharedPreferences.getString("youtube_data_fetching_extractor", "NEWPIPE") == "NEWPIPE") {
newPipeUtil = NewPipeUtil(context)
}
}
enum class SourceType {
YOUTUBE_VIDEO,
YOUTUBE_WATCHVIDEOS,
@ -55,6 +43,8 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
YT_DLP
}
private fun isUsingNewPipeExtractorDataFetching() = sharedPreferences.getString("youtube_data_fetching_extractor", "NEWPIPE") == "NEWPIPE"
suspend fun insert(it: ResultItem){
resultDao.insert(it)
}
@ -63,12 +53,17 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
return resultDao.getFirstResult()
}
suspend fun updateTrending(){
suspend fun getHomeRecommendations(){
deleteAll()
val items = if (sharedPreferences.getString("api_key", "")!!.isNotBlank()) {
youtubeApiUtil.getTrending()
}else{
newPipeUtil?.getTrending() ?: listOf()
val category = sharedPreferences.getString("youtube_home_recommendations", "")
val items = when(category) {
"newpipe" -> newPipeUtil.getTrending()
"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
@ -80,24 +75,28 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
}
fun getStreamingUrlAndChapters(url: String) : Pair<List<String>, List<ChapterItem>?> {
val extractorsTrial = newPipeUtil?.getStreamingUrlAndChapters(url) ?: Result.failure(Throwable())
if (extractorsTrial.isFailure){
val newPipeTrial = if (isUsingNewPipeExtractorDataFetching()) {
newPipeUtil.getStreamingUrlAndChapters(url)
}else {
Result.failure(Throwable())
}
if (newPipeTrial.isFailure){
val res = ytdlpUtil.getStreamingUrlAndChapters(url)
return res.getOrDefault(Pair(listOf(""), null))
}
return extractorsTrial.getOrNull()!!
return newPipeTrial.getOrNull()!!
}
suspend fun search(inputQuery: String, resetResults: Boolean, addToResults: Boolean) : ArrayList<ResultItem>{
if (resetResults) deleteAll()
val res = when(sharedPreferences.getString("search_engine", "ytsearch")) {
"ytsearch" -> newPipeUtil?.search(inputQuery)
"ytsearchmusic" -> newPipeUtil?.searchMusic(inputQuery)
"ytsearch" -> newPipeUtil.search(inputQuery)
"ytsearchmusic" -> newPipeUtil.searchMusic(inputQuery)
else -> Result.failure(Throwable())
}
val items = if (res?.isSuccess == true) {
val items = if (res.isSuccess) {
res.getOrNull()!!
}else{
//fallback to yt-dlp
@ -119,7 +118,8 @@ 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!")
val items = mutableListOf<ResultItem>()
val ytExtractorResult = newPipeUtil?.getPlaylistData(inputQuery) {
val newpipeExtractorResult = if (isUsingNewPipeExtractorDataFetching()) {
newPipeUtil.getPlaylistData(inputQuery) {
if (addToResults){
runBlocking {
val ids = resultDao.insertMultiple(it)
@ -130,8 +130,12 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
}
items.addAll(it)
}
val response = if (ytExtractorResult?.isSuccess == true){
ytExtractorResult.getOrElse { items }
}else {
Result.failure(Throwable())
}
val response = if (newpipeExtractorResult.isSuccess){
newpipeExtractorResult.getOrElse { items }
}else{
val res = ytdlpUtil.getFromYTDL(inputQuery)
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>{
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) {
ytExtractorRes.getOrNull()!!
val res = if (newpipeExtractorResult.isSuccess) {
newpipeExtractorResult.getOrNull()!!
}else{
ytdlpUtil.getFromYTDL(inputQuery)
}
@ -177,7 +185,8 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
val playlistURL = "https://youtube.com/playlist?list=${id}"
if (resetResults) deleteAll()
val items = mutableListOf<ResultItem>()
val ytExtractorResult = newPipeUtil?.getPlaylistData(playlistURL) {
val ytExtractorResult = if (isUsingNewPipeExtractorDataFetching()){
newPipeUtil.getPlaylistData(playlistURL) {
if (addToResults){
runBlocking {
val ids = resultDao.insertMultiple(it)
@ -188,8 +197,11 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
}
items.addAll(it)
}
}else {
Result.failure(Throwable())
}
val response = if (ytExtractorResult?.isSuccess == true){
val response = if (ytExtractorResult.isSuccess){
ytExtractorResult.getOrElse { items }
}else{
val res = ytdlpUtil.getFromYTDL(playlistURL)
@ -209,7 +221,8 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
private suspend fun getYoutubeChannel(url: String, resetResults: Boolean, addToResults: Boolean) : List<ResultItem>{
if (resetResults) deleteAll()
val items = mutableListOf<ResultItem>()
val ytExtractorResult = newPipeUtil?.getChannelData(url) {
val ytExtractorResult = if (isUsingNewPipeExtractorDataFetching()) {
newPipeUtil.getChannelData(url) {
if (addToResults){
runBlocking {
val ids = resultDao.insertMultiple(it)
@ -220,19 +233,14 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co
}
items.addAll(it)
}
}else {
Result.failure(Throwable())
}
val response = if (ytExtractorResult?.isSuccess == true){
val response = if (ytExtractorResult.isSuccess){
ytExtractorResult.getOrElse { items }
}else{
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)
ids.forEachIndexed { index, 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.models.ChapterItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.models.SearchHistoryItem
import com.deniscerri.ytdl.database.repository.ResultRepository
@ -33,7 +32,6 @@ import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
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.creationTime < (System.currentTimeMillis() / 1000) - 86400
){
getTrending()
getHomeRecommendations()
}
}catch (e : Exception){
e.printStackTrace()
getTrending()
getHomeRecommendations()
}
}
fun getTrending() = viewModelScope.launch(Dispatchers.IO){
if (sharedPreferences.getBoolean("home_recommendations", false)){
repository.updateTrending()
fun getHomeRecommendations() = viewModelScope.launch(Dispatchers.IO){
if (!sharedPreferences.getString("youtube_home_recommendations", "").isNullOrBlank()){
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{
deleteAll()
}

@ -36,14 +36,11 @@ import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.models.SearchSuggestionItem
import com.deniscerri.ytdl.database.models.SearchSuggestionType
import com.deniscerri.ytdl.database.repository.ResultRepository
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.ui.adapter.HomeAdapter
import com.deniscerri.ytdl.ui.adapter.SearchSuggestionsAdapter
import com.deniscerri.ytdl.ui.downloads.HistoryFragment
import com.deniscerri.ytdl.ui.downloads.HistoryFragment.Companion
import com.deniscerri.ytdl.util.Extensions.enableFastScroll
import com.deniscerri.ytdl.util.Extensions.isURL
import com.deniscerri.ytdl.util.NotificationUtil
@ -67,7 +64,6 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import kotlin.collections.ArrayList
@ -497,7 +493,7 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti
resultViewModel.cancelParsingQueries()
}
}
resultViewModel.getTrending()
resultViewModel.getHomeRecommendations()
selectedObjects = ArrayList()
searchBar!!.setText("")
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 {
isVisible = preferences.getString("youtube_home_recommendations", "") == "yt_api"
val s = getString(R.string.api_key_summary)
summary = if (text.isNullOrBlank()) {
s

@ -1,21 +1,59 @@
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.net.Uri
import android.os.Build
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.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 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.preference.Preference
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.CommandTemplate
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UiUtil.showShortcutsSheet
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.textfield.TextInputLayout
import com.yausername.youtubedl_android.YoutubeDL
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.Locale
class UpdateSettingsFragment : BaseSettingsFragment() {
@ -25,39 +63,41 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
private var ytdlSource: Preference? = null
private var updateUtil: UpdateUtil? = null
private var version: Preference? = null
private lateinit var preferences: SharedPreferences
private lateinit var ytdlpUtil: YTDLPUtil
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.updating_preferences, rootKey)
updateUtil = UpdateUtil(requireContext())
val preferences =
PreferenceManager.getDefaultSharedPreferences(requireContext())
val editor = preferences.edit()
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
updateYTDL = findPreference("update_ytdl")
ytdlVersion = findPreference("ytdl-version")
ytdlSource = findPreference("ytdlp_source")
YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it)
editor.apply()
ytdlVersion!!.summary = it
ytdlSource = findPreference("ytdlp_source_label")
ytdlpUtil = YTDLPUtil(requireContext())
setYTDLPVersion()
ytdlSource?.apply {
summary = preferences.getString("ytdlp_source_label", "")
setOnPreferenceClickListener {
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)
}
ytdlSource?.setOnPreferenceChangeListener { preference, newValue ->
initYTDLUpdate(editor)
true
}
}
ytdlVersion!!.setOnPreferenceClickListener {
initYTDLUpdate(editor)
initYTDLUpdate()
true
}
updateYTDL!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener {
initYTDLUpdate(editor)
initYTDLUpdate()
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(),
requireContext().getString(R.string.ytdl_updating_started),
Snackbar.LENGTH_LONG).show()
runCatching {
when (updateUtil!!.updateYoutubeDL()) {
YoutubeDL.UpdateStatus.DONE -> {
Snackbar.make(requireView(),
requireContext().getString(R.string.ytld_update_success),
Snackbar.LENGTH_LONG).show()
YoutubeDL.getInstance().version(context)?.let {
editor.putString("ytdl-version", it)
editor.apply()
ytdlVersion!!.summary = it
}
val res = updateUtil!!.updateYoutubeDL(channel)
when (res.status) {
UpdateUtil.YTDLPUpdateStatus.DONE -> {
Snackbar.make(requireView(), res.message, Snackbar.LENGTH_LONG).show()
setYTDLPVersion()
}
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),
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 -> {
Snackbar.make(requireView(),
requireContext().getString(R.string.errored),
Snackbar.LENGTH_LONG).show()
}
}
}.onFailure {

@ -299,6 +299,9 @@ object FileUtil {
request.getArguments("--config-locations")?.forEach {
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.Log
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.ViewTreeObserver
@ -28,6 +29,8 @@ import android.widget.Button
import android.widget.CheckBox
import android.widget.EditText
import android.widget.LinearLayout
import android.widget.PopupMenu
import android.widget.RadioButton
import android.widget.TextView
import android.widget.Toast
import androidx.annotation.DimenRes
@ -38,7 +41,9 @@ import androidx.appcompat.content.res.AppCompatResources
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner
@ -2061,4 +2066,139 @@ object UiUtil {
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.Handler
import android.os.Looper
import android.text.format.DateFormat
import android.text.method.LinkMovementMethod
import android.util.Log
import android.widget.LinearLayout
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat.startActivity
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.GithubRelease
import com.deniscerri.ytdl.ui.adapter.ChangelogAdapter
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.gson.Gson
import com.google.gson.reflect.TypeToken
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.Markwon
import io.noties.markwon.MarkwonConfiguration
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Locale
class UpdateUtil(var context: Context) {
@ -220,24 +209,35 @@ class UpdateUtil(var context: Context) {
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){
val sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(context)
if (updatingYTDL) {
UpdateStatus.ALREADY_UP_TO_DATE
YTDLPUpdateResponse(YTDLPUpdateStatus.PROCESSING)
}
updatingYTDL = true
val channelMap = mapOf(
"stable" to YoutubeDL.UpdateChannel._STABLE,
"nightly" to YoutubeDL.UpdateChannel._NIGHTLY,
"master" to YoutubeDL.UpdateChannel._MASTER,
)
val channel = sharedPreferences.getString("ytdlp_source", "nightly")
YoutubeDL.getInstance().updateYoutubeDL(context, channelMap[channel] ?: YoutubeDL.UpdateChannel._NIGHTLY).apply {
updatingYTDL = false
}
val channel = if (c.isNullOrBlank()) sharedPreferences.getString("ytdlp_source", "nightly") else c
val request = YoutubeDLRequest(emptyList())
request.addOption("--update-to", "${channel}@latest")
val res = YoutubeDL.getInstance().execute(request)
val out = res.out.split(System.getProperty("line.separator")).filter { it.isNotBlank() }.last()
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 {

@ -36,6 +36,7 @@ import org.json.JSONArray
import org.json.JSONObject
import java.io.File
import java.lang.reflect.Type
import java.net.URLEncoder
import java.util.ArrayList
import java.util.Locale
import java.util.UUID
@ -45,9 +46,43 @@ class YTDLPUtil(private val context: Context) {
private val formatUtil = FormatUtil(context)
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")
fun getFromYTDL(query: String, singleItem: Boolean = false): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
val request : YoutubeDLRequest
@ -78,38 +113,7 @@ class YTDLPUtil(private val context: Context) {
request.addOption("--flat-playlist")
request.addOption(if (singleItem) "-J" else "-j")
request.addOption("--skip-download")
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)
}
request.applyDefaultOptionsForFetchingData()
println(parseYTDLRequestString(request))
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
@ -121,6 +125,13 @@ class YTDLPUtil(private val context: Context) {
}.filter { it.isNotBlank() }.apply {
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) {
if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result)
@ -163,7 +174,7 @@ class YTDLPUtil(private val context: Context) {
if(playlistTitle.removeSurrounding("\"") == query) playlistTitle = ""
if (playlistTitle.isNotBlank()){
playlistURL = query
playlistURL = jsonObject.getStringByAny("playlist_webpage_url").ifEmpty { query }
kotlin.runCatching { playlistIndex = jsonObject.getInt("playlist_index") }
}
@ -223,9 +234,85 @@ class YTDLPUtil(private val context: Context) {
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>>> {
val formatCollection = mutableListOf<MutableList<Format>>()
@ -243,38 +330,7 @@ class YTDLPUtil(private val context: Context) {
val request = YoutubeDLRequest(emptyList())
request.addOption("--print", "formats")
request.addOption("-a", urlsFile.absolutePath)
request.addOption("--skip-download")
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)
}
request.applyDefaultOptionsForFetchingData()
val txt = parseYTDLRequestString(request)
println(txt)
@ -343,36 +399,7 @@ class YTDLPUtil(private val context: Context) {
val request = YoutubeDLRequest(url)
request.addOption("--print", "%(formats)s")
request.addOption("--print", "%(duration)s")
request.addOption("--skip-download")
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)
}
request.applyDefaultOptionsForFetchingData()
val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try {
@ -447,37 +474,7 @@ class YTDLPUtil(private val context: Context) {
val request = YoutubeDLRequest(url)
request.addOption("--get-url")
request.addOption("--print", "%(.{urls,chapters})s")
request.addOption("--skip-download")
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)
}
request.applyDefaultOptionsForFetchingData()
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val json = JSONObject(youtubeDLResponse.out)
@ -726,12 +723,15 @@ class YTDLPUtil(private val context: Context) {
}
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
if (infoJsonFile == null || System.currentTimeMillis() - infoJsonFile.lastModified() > (1000 * 60 * 60 * 5)) {
request.addOption("--write-info-json")
request.addOption("--no-clean-info-json")
request.addOption("-o", "infojson:${downloadItem.id}")
request.addOption("-o", "infojson:${cachePath}/${infoJsonName}")
}else {
request.addOption("--load-info-json", infoJsonFile.absolutePath)
}
@ -807,7 +807,7 @@ class YTDLPUtil(private val context: Context) {
request.addOption("-P", downDir.absolutePath)
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()){
request.addOption("--split-chapters")
@ -1137,6 +1137,12 @@ class YTDLPUtil(private val context: Context) {
return request
}
fun getVersion() : String {
val req = YoutubeDLRequest(emptyList())
req.addOption("--version")
return YoutubeDL.getInstance().execute(req).out.trim()
}
private fun getYoutubeExtractorArgs() : String {
val playerClient = sharedPreferences.getString("youtube_player_client", "default,mediaconnect")!!.split(",").filter { it.isNotBlank() }.toMutableList()
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.util.Extensions.toStringDuration
import com.google.gson.Gson
import kotlinx.serialization.Serializer
import okhttp3.OkHttpClient
import org.json.JSONException
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.Page
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.channel.ChannelInfo
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
import org.schabi.newpipe.extractor.kiosk.KioskInfo
import org.schabi.newpipe.extractor.kiosk.KioskList
import org.schabi.newpipe.extractor.linkhandler.LinkHandler
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
import org.schabi.newpipe.extractor.localization.ContentCountry
import org.schabi.newpipe.extractor.localization.Localization
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
import org.schabi.newpipe.extractor.search.SearchExtractor
import org.schabi.newpipe.extractor.search.SearchInfo
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMusicSearchExtractor
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeSearchExtractor
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory
import org.schabi.newpipe.extractor.stream.StreamInfo
import org.schabi.newpipe.extractor.stream.StreamInfoItem
import org.schabi.newpipe.extractor.utils.ExtractorHelper
import java.util.Locale
import kotlin.coroutines.cancellation.CancellationException

@ -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"
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
android:id="@+id/download_progress"
android:layout_width="match_parent"
@ -71,9 +64,6 @@
android:paddingTop="10dp"
android:paddingEnd="10dp"
android:scrollbars="none"
android:shadowColor="@color/black"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="2"
android:textColor="#FFF"
android:textSize="17sp"
@ -89,9 +79,6 @@
android:paddingStart="10dp"
android:paddingEnd="10dp"
android:scrollbars="none"
android:shadowColor="@color/black"
android:shadowDx="4"
android:shadowDy="4"
android:shadowRadius="1.5"
android:textColor="#FFF"
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>
</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-->
<string-array name="language_codes">

@ -204,7 +204,7 @@
<string name="subtitle_languages">Subtitle languages</string>
<string name="format_source">Formats Source</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_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>
@ -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="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="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>

@ -70,11 +70,12 @@
</PreferenceCategory>
<PreferenceCategory android:title="YouTube">
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
<ListPreference
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:title="@string/video_recommendations" />
@ -129,7 +130,7 @@
android:entryValues="@array/countries_values"
app:icon="@drawable/ic_language"
app:useSimpleSummaryProvider="true"
app:dependency="home_recommendations"
app:dependency="youtube_home_recommendations"
app:key="locale"
app:title="@string/preferred_locale" />

@ -20,13 +20,9 @@
app:summary="@string/ytdl_update_hint"
app:title="@string/update_ytdl" />
<ListPreference
android:defaultValue="stable"
android:entries="@array/ytdlp_source"
android:entryValues="@array/ytdlp_source_values"
<Preference
android:icon="@drawable/baseline_source_24"
app:key="ytdlp_source"
app:useSimpleSummaryProvider="true"
app:key="ytdlp_source_label"
app:title="@string/ytdl_source" />

@ -16,4 +16,4 @@ android.nonFinalResIds=false
android.nonTransitiveRClass=false
android.suppressUnsupportedCompileSdk=35
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