more features
This commit is contained in:
parent
926e6ec73d
commit
4f04b039ad
23 changed files with 777 additions and 309 deletions
22
CHANGELOG.md
22
CHANGELOG.md
|
|
@ -14,6 +14,12 @@
|
|||
- Made the app remember the last used scheduled time so it can suggest you that time for the next download
|
||||
- #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,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<ResultItem>()
|
||||
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<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,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<ResultItem>()
|
||||
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<ResultItem>{
|
||||
if (resetResults) deleteAll()
|
||||
val items = mutableListOf<ResultItem>()
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
ytdlSource = findPreference("ytdlp_source_label")
|
||||
ytdlpUtil = YTDLPUtil(requireContext())
|
||||
setYTDLPVersion()
|
||||
|
||||
YoutubeDL.getInstance().version(context)?.let {
|
||||
editor.putString("ytdl-version", it)
|
||||
editor.apply()
|
||||
ytdlVersion!!.summary = it
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
50
app/src/main/res/layout/create_ytdlp_sources.xml
Normal file
50
app/src/main/res/layout/create_ytdlp_sources.xml
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/title_textinput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:hint="@string/title"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:inputType="text"
|
||||
android:layout_height="wrap_content"
|
||||
/>
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/repo_textinput"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingHorizontal="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
app:expandedHintEnabled="true"
|
||||
app:hintEnabled="true"
|
||||
android:hint="@string/source"
|
||||
style="@style/Widget.Material3.TextInputLayout.FilledBox">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:inputType="text"
|
||||
android:hint="owner/repository"
|
||||
android:layout_height="wrap_content"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
72
app/src/main/res/layout/custom_ytdlp_source.xml
Normal file
72
app/src/main/res/layout/custom_ytdlp_source.xml
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.material.card.MaterialCardView android:id="@+id/sampleCustomSource"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:checkable="true"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:backgroundTint="@android:color/transparent"
|
||||
app:checkedIcon="@null"
|
||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar"
|
||||
app:strokeWidth="0dp"
|
||||
app:cardPreventCornerOverlap="true"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/sampleRadioBtn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sampleTitle"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="18sp"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toStartOf="@+id/options"
|
||||
app:layout_constraintStart_toEndOf="@id/sampleRadioBtn"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sampleRepo"
|
||||
android:layout_width="wrap_content"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toStartOf="@+id/options"
|
||||
app:layout_constraintStart_toEndOf="@id/sampleRadioBtn"
|
||||
app:layout_constraintTop_toBottomOf="@id/sampleTitle" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/options"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:padding="16dp"
|
||||
android:tintMode="src_in"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/baseline_more_vert_24"
|
||||
app:tint="?attr/colorControlNormal"
|
||||
tools:ignore="ContentDescription" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
|
@ -29,13 +29,6 @@
|
|||
app:strokeWidth="0dp"
|
||||
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"
|
||||
|
|
|
|||
79
app/src/main/res/layout/ytdlp_sources_list.xml
Normal file
79
app/src/main/res/layout/ytdlp_sources_list.xml
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="vertical"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="20dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/ytdl_source"
|
||||
android:textSize="25sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/add"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/bottom_sheet_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/select"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/bottom_sheet_title" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/add"
|
||||
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoLink="all"
|
||||
android:text="@string/add"
|
||||
app:icon="@drawable/ic_check"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:padding="15dp"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/sourcesList"
|
||||
android:layout_width="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
19
app/src/main/res/menu/custom_ytdlp_source_menu.xml
Normal file
19
app/src/main/res/menu/custom_ytdlp_source_menu.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".MainActivity" >
|
||||
|
||||
<item
|
||||
android:id="@+id/edit"
|
||||
android:title="@string/edit"
|
||||
android:icon="@drawable/ic_edit"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/remove"
|
||||
android:title="@string/Remove"
|
||||
android:icon="@drawable/baseline_delete_24"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
</menu>
|
||||
|
||||
|
|
@ -1143,6 +1143,25 @@
|
|||
<item>master</item>
|
||||
</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…
Reference in a new issue