From 4f04b039ad47cbd18159ce37f09a8d2da704d27d Mon Sep 17 00:00:00 2001 From: deniscerri <64997243+deniscerri@users.noreply.github.com> Date: Sun, 24 Nov 2024 20:48:08 +0100 Subject: [PATCH] more features --- CHANGELOG.md | 22 ++ app/build.gradle | 2 +- .../java/com/deniscerri/ytdl/MainActivity.kt | 14 +- .../database/repository/ResultRepository.kt | 128 +++++---- .../database/viewmodel/ResultViewModel.kt | 21 +- .../com/deniscerri/ytdl/ui/HomeFragment.kt | 6 +- .../more/settings/GeneralSettingsFragment.kt | 22 ++ .../more/settings/UpdateSettingsFragment.kt | 127 ++++++--- .../java/com/deniscerri/ytdl/util/FileUtil.kt | 3 + .../java/com/deniscerri/ytdl/util/UiUtil.kt | 140 +++++++++ .../com/deniscerri/ytdl/util/UpdateUtil.kt | 46 +-- .../ytdl/util/extractors/YTDLPUtil.kt | 268 +++++++++--------- .../util/extractors/newpipe/NewPipeUtil.kt | 10 - .../main/res/layout/create_ytdlp_sources.xml | 50 ++++ .../main/res/layout/custom_ytdlp_source.xml | 72 +++++ .../main/res/layout/observe_sources_item.xml | 13 - .../main/res/layout/ytdlp_sources_list.xml | 79 ++++++ .../res/menu/custom_ytdlp_source_menu.xml | 19 ++ app/src/main/res/values/arrays.xml | 19 ++ app/src/main/res/values/strings.xml | 6 +- app/src/main/res/xml/general_preferences.xml | 11 +- app/src/main/res/xml/updating_preferences.xml | 8 +- gradle.properties | 2 +- 23 files changed, 778 insertions(+), 310 deletions(-) create mode 100644 app/src/main/res/layout/create_ytdlp_sources.xml create mode 100644 app/src/main/res/layout/custom_ytdlp_source.xml create mode 100644 app/src/main/res/layout/ytdlp_sources_list.xml create mode 100644 app/src/main/res/menu/custom_ytdlp_source_menu.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fdd821c..3942c647 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ - Made the app remember the last used scheduled time so it can suggest you that time for the next download - #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) diff --git a/app/build.gradle b/app/build.gradle index 8cc6b21b..d183a0a2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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") } diff --git a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt index 452cfeac..6a1a2375 100644 --- a/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/MainActivity.kt @@ -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}]", diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt index e1da36a8..81d1891e 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt @@ -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?> { - 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{ 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,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!") val items = mutableListOf() - val ytExtractorResult = newPipeUtil?.getPlaylistData(inputQuery) { - if (addToResults){ - runBlocking { - val ids = resultDao.insertMultiple(it) - ids.forEachIndexed { index, id -> - it[index].id = id + val newpipeExtractorResult = if (isUsingNewPipeExtractorDataFetching()) { + newPipeUtil.getPlaylistData(inputQuery) { + if (addToResults){ + runBlocking { + val ids = resultDao.insertMultiple(it) + 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{ 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{ 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,19 +185,23 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co val playlistURL = "https://youtube.com/playlist?list=${id}" if (resetResults) deleteAll() val items = mutableListOf() - val ytExtractorResult = newPipeUtil?.getPlaylistData(playlistURL) { - if (addToResults){ - runBlocking { - val ids = resultDao.insertMultiple(it) - ids.forEachIndexed { index, id -> - it[index].id = id + val ytExtractorResult = if (isUsingNewPipeExtractorDataFetching()){ + newPipeUtil.getPlaylistData(playlistURL) { + if (addToResults){ + runBlocking { + val ids = resultDao.insertMultiple(it) + 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 } }else{ 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{ if (resetResults) deleteAll() val items = mutableListOf() - val ytExtractorResult = newPipeUtil?.getChannelData(url) { - if (addToResults){ - runBlocking { - val ids = resultDao.insertMultiple(it) - ids.forEachIndexed { index, id -> - it[index].id = id + val ytExtractorResult = if (isUsingNewPipeExtractorDataFetching()) { + newPipeUtil.getChannelData(url) { + if (addToResults){ + runBlocking { + val ids = resultDao.insertMultiple(it) + 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 } }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 diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt index afe7720a..840dbf75 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/ResultViewModel.kt @@ -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().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() } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt index 48b7d27d..77af7b35 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt @@ -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 diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt index f4ce09cd..41395234 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/GeneralSettingsFragment.kt @@ -269,7 +269,29 @@ class GeneralSettingsFragment : BaseSettingsFragment() { } } + findPreference("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("api_key")?.isVisible = true + } + true + } + } + findPreference("api_key")?.apply { + isVisible = preferences.getString("youtube_home_recommendations", "") == "yt_api" val s = getString(R.string.api_key_summary) summary = if (text.isNullOrBlank()) { s diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt index 24e6531d..e6b33580 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/UpdateSettingsFragment.kt @@ -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?.setOnPreferenceChangeListener { preference, newValue -> - initYTDLUpdate(editor) - true + 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) + } + 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(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 { diff --git a/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt index 4294726a..06862ebd 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/FileUtil.kt @@ -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() + } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index 8d6d9b70..e9d95662 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -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(R.id.title_textinput).editText!! + val repoTextInput = view.findViewById(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(R.id.sourcesList)!! + + bottomSheet.findViewById(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(R.id.sampleCustomSource).setOnClickListener { + bottomSheet.dismiss() + selectedSource(title, source) + } + + child.findViewById(R.id.sampleTitle).apply { + text = title + } + child.findViewById(R.id.sampleRadioBtn).apply { + isChecked = preferences.getString("ytdlp_source", "stable") == source + setOnClickListener { + bottomSheet.dismiss() + selectedSource(title, source) + } + } + child.findViewById(R.id.sampleRepo).text = source + child.findViewById(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(R.id.sampleTitle).text = nt + child.findViewById(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(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(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 + ) + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt index 0698411f..64dbad88 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt @@ -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 { diff --git a/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt index a7fbf06b..4d32bb71 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt @@ -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 { - val items = arrayListOf() 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, query: String = "") : ArrayList { + val items = arrayListOf() + 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 { + 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 = 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 { + 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 = 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 { + 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 = 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 { + 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 = 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, progress: (progress: ResultViewModel.MultipleFormatProgress) -> Unit) : Result>> { val formatCollection = mutableListOf>() @@ -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 = 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.*?)(?:(?= - Topic)|$)""") + metadataCommands.addOption("--parse-metadata", """%(artists,artist,uploader,channel,creator|null)l:^(?P.*?)(?:(?= - 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() diff --git a/app/src/main/java/com/deniscerri/ytdl/util/extractors/newpipe/NewPipeUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/extractors/newpipe/NewPipeUtil.kt index 562392a2..d09dbf7f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/extractors/newpipe/NewPipeUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/extractors/newpipe/NewPipeUtil.kt @@ -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 diff --git a/app/src/main/res/layout/create_ytdlp_sources.xml b/app/src/main/res/layout/create_ytdlp_sources.xml new file mode 100644 index 00000000..ad31cf65 --- /dev/null +++ b/app/src/main/res/layout/create_ytdlp_sources.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/custom_ytdlp_source.xml b/app/src/main/res/layout/custom_ytdlp_source.xml new file mode 100644 index 00000000..ae546bf2 --- /dev/null +++ b/app/src/main/res/layout/custom_ytdlp_source.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/observe_sources_item.xml b/app/src/main/res/layout/observe_sources_item.xml index ae20f21c..10ff39ca 100644 --- a/app/src/main/res/layout/observe_sources_item.xml +++ b/app/src/main/res/layout/observe_sources_item.xml @@ -29,13 +29,6 @@ app:strokeWidth="0dp" android:layout_margin="10dp"> - - + + + + + + + + + + + +