many more changes lol

This commit is contained in:
deniscerri 2025-03-14 15:35:02 +01:00
parent 0c1c06bedb
commit cf9e584e33
No known key found for this signature in database
GPG key ID: 95C43D517D830350
29 changed files with 524 additions and 622 deletions

View file

@ -0,0 +1,8 @@
package com.deniscerri.ytdl.database.models
data class YoutubeGeneratePoTokenItem(
var enabled: Boolean,
var clients: MutableList<String>,
var poTokens: MutableList<YoutubePoTokenItem>,
var visitorData: String
)

View file

@ -5,8 +5,7 @@ data class YoutubePlayerClientItem(
var poTokens: MutableList<YoutubePoTokenItem>,
var enabled: Boolean = true,
var useOnlyPoToken: Boolean = false,
var urlRegex: MutableList<String> = mutableListOf(),
var autoGenerated: Boolean = false
var urlRegex: MutableList<String> = mutableListOf()
)
data class YoutubePoTokenItem(

View file

@ -67,6 +67,7 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi
card.findViewById<TextView>(R.id.title).text = item!!.title
card.findViewById<TextView>(R.id.author).text = item.author
card.findViewById<TextView>(R.id.duration).text = item.duration
card.findViewById<TextView>(R.id.index).text = ((item.playlistIndex ?: (position + 1))).toString()
// CHECKBOX ----------------------------------
val check = card.findViewById<CheckBox>(R.id.checkBox)

View file

@ -91,8 +91,6 @@ class YoutubePlayerClientAdapter(onItemClickListener: OnItemClickListener, activ
title.alpha = if (item.enabled) 1f else 0.3f
content.alpha = if (item.enabled) 1f else 0.3f
card.findViewById<MaterialSwitch>(R.id.autoGeneratedNewPipe).isVisible = item.autoGenerated
val switch = card.findViewById<MaterialSwitch>(R.id.materialSwitch)
switch.isChecked = item.enabled
switch.setOnClickListener {

View file

@ -150,12 +150,12 @@ class CutVideoBottomSheetDialog(private val _item: DownloadItem? = null, private
startTextInput = view.findViewById(R.id.from_textinput_edittext)
startTextInput.keyListener = DigitsKeyListener.getInstance("0123456789:.")
startTextInput.imeOptions = EditorInfo.IME_ACTION_DONE
startTextInput.inputType = EditorInfo.TYPE_NUMBER_FLAG_DECIMAL
startTextInput.inputType = EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS
startTextInput.maxLines = 1
endTextInput = view.findViewById(R.id.to_textinput_edittext)
endTextInput.keyListener = DigitsKeyListener.getInstance("0123456789:.")
endTextInput.imeOptions = EditorInfo.IME_ACTION_DONE
endTextInput.inputType = EditorInfo.TYPE_NUMBER_FLAG_DECIMAL
endTextInput.inputType = EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS
endTextInput.maxLines = 1
cancelBtn = view.findViewById(R.id.cancelButton)

View file

@ -1,27 +1,19 @@
package com.deniscerri.ytdl.ui.more.settings
import android.annotation.SuppressLint
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.content.res.Resources.Theme
import android.net.Uri
import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import android.view.MenuInflater
import android.view.MenuItem
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.PopupMenu
import androidx.core.content.res.ResourcesCompat
import androidx.core.os.LocaleListCompat
import androidx.core.text.HtmlCompat
import androidx.core.text.parseAsHtml
import androidx.core.view.forEach
import androidx.navigation.fragment.findNavController
import androidx.preference.EditTextPreference
import androidx.preference.ListPreference
@ -34,13 +26,11 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdl.MainActivity
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.databinding.NavOptionsItemBinding
import com.deniscerri.ytdl.ui.adapter.NavBarOptionsAdapter
import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.ThemeUtil.getThemeColor
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -54,6 +44,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
private var updateUtil: UpdateUtil? = null
private var activeDownloadCount = 0
@SuppressLint("BatteryLife")
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.general_preferences, rootKey)
NavbarUtil.init(requireContext())
@ -82,7 +73,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
findPreference<Preference>("label_visibility")?.apply {
isVisible = !resources.getBoolean(R.bool.uses_side_nav)
setOnPreferenceChangeListener { preference, newValue ->
setOnPreferenceChangeListener { _, _ ->
ThemeUtil.recreateMain()
true
}
@ -98,9 +89,9 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
val options = NavbarUtil.getNavBarItems(requireContext())
val optionsRecycler = binding.findViewById<RecyclerView>(R.id.options_recycler)
var adapter : NavBarOptionsAdapter? = null;
val adapter : NavBarOptionsAdapter?
val onitemClick = object: NavBarOptionsAdapter.OnItemClickListener {
val onItemClick = object: NavBarOptionsAdapter.OnItemClickListener {
override fun onNavBarOptionDeselected(item: NavOptionsItemBinding) {
optionsRecycler.findViewHolderForLayoutPosition(0)?.apply {
(this as NavBarOptionsAdapter.NavBarOptionsViewHolder).apply {
@ -113,7 +104,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
adapter = NavBarOptionsAdapter(
options.toMutableList(),
NavbarUtil.getStartFragmentId(requireContext()),
onitemClick
onItemClick
)
val itemTouchCallback = object : ItemTouchHelper.Callback() {
@ -257,20 +248,12 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
}
}
findPreference<Preference>("piped_instance")?.setOnPreferenceClickListener {
UiUtil.showPipedInstancesDialog(requireActivity(), preferences.getString("piped_instance", "")!!){
editor.putString("piped_instance", it)
editor.apply()
}
true
}
findPreference<MultiSelectListPreference>("hide_thumbnails")?.apply {
values.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
setOnPreferenceChangeListener { _, newValues ->
(newValues as Set<String>).filter { it.isNotBlank() }.apply {
(newValues as Set<*>).map { it as String }.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
true
@ -282,7 +265,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
setOnPreferenceChangeListener { _, newValues ->
(newValues as Set<String>).filter { it.isNotBlank() }.apply {
(newValues as Set<*>).map { it as String }.filter { it.isNotBlank() }.apply {
summary = joinToString(", ") { entries[entryValues.indexOf(it)] }
}
true
@ -360,7 +343,7 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
if (newValues.size == entries.size) {
summary = "${s}\n[${getString(R.string.all)}]"
}else if (newValues.isNotEmpty()) {
val indexes = newValues.mapIndexed { index, _ -> index }
val indexes = List(newValues.size) { index -> index }
summary = "${s}\n[${entries.filterIndexed { index, _ -> indexes.contains(index) }.joinToString(", ")}]"
}else{
summary = s
@ -390,18 +373,9 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
super.onResume()
}
private fun restartApp() {
val intent = Intent(requireContext(), MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
requireActivity().finishAffinity()
activity?.finishAffinity()
}
private var displayOverAppsResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
) { _ ->
findNavController().popBackStack(R.id.appearanceSettingsFragment, false)
}

View file

@ -39,51 +39,9 @@ class AdvancedSettingsFragment : BaseSettingsFragment() {
false
}
val newPipeUtil = NewPipeUtil(requireContext())
findPreference<SwitchPreferenceCompat>("use_newpipe_potoken")?.apply {
fun getValues() : Triple<String, String, String> {
val gvs = prefs.getString("newpipe_gvs_potoken", "")!!
val player = prefs.getString("newpipe_player_potoken", "")!!
val visitorData = prefs.getString("newpipe_visitordata", "")!!
return Triple(gvs, player, visitorData)
}
fun updateSummary() : String {
val values = getValues()
val gvs = values.first
val player = values.second
val visitorData = values.third
return if (visitorData.isNotBlank()) {
"PO Token (GVS): ${gvs.take(10)}...\nPO Token (Player): ${player.take(10)}...\nVisitor Data: ${visitorData.take(10)}..."
}else {
""
}
}
summary = updateSummary()
setOnPreferenceClickListener {
if (this.isChecked) {
this.isChecked = false
UiUtil.showGenericConfirmDialog(requireContext(), getString(R.string.use_newpipe_potoken), getString(R.string.use_newpipe_potoken_warning)) {
editor.putBoolean("use_cookies", false)
editor.putBoolean("use_newpipe_token", true)
editor.apply()
this.isChecked = true
lifecycleScope.launch {
summary = getString(R.string.loading)
withContext(Dispatchers.IO) {
newPipeUtil.testRun()
}
summary = updateSummary()
}
}
}
false
}
findPreference<Preference>("generate_po_tokens")?.setOnPreferenceClickListener {
findNavController().navigate(R.id.generateYoutubePoTokensFragment)
false
}
val formatImportanceAudio: Preference? = findPreference("format_importance_audio")

View file

@ -0,0 +1,152 @@
package com.deniscerri.ytdl.ui.more.settings.advanced
import android.content.SharedPreferences
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.YoutubeGeneratePoTokenItem
import com.deniscerri.ytdl.database.models.YoutubePoTokenItem
import com.deniscerri.ytdl.ui.more.settings.SettingsActivity
import com.deniscerri.ytdl.util.Extensions.getIDFromYoutubeURL
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.extractors.potoken.PoTokenGenerator
import com.google.android.material.button.MaterialButton
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.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class GenerateYoutubePoTokensFragment : Fragment() {
private lateinit var settingsActivity: SettingsActivity
private lateinit var preferences: SharedPreferences
private lateinit var configuration : MutableList<YoutubeGeneratePoTokenItem>
private lateinit var poTokenGenerator: PoTokenGenerator
private val sampleURL = "https://www.youtube.com/watch?v=aqz-KE-bpKQ" // Big Buck Bunny
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
settingsActivity = activity as SettingsActivity
settingsActivity.changeTopAppbarTitle(getString(R.string.generate_potokens))
preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
poTokenGenerator = PoTokenGenerator()
return inflater.inflate(R.layout.fragment_generate_youtube_po_token, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
configuration = Gson().fromJson(
preferences.getString("youtube_generated_po_tokens", "[]"),
Array<YoutubeGeneratePoTokenItem>::class.java
).toMutableList()
initWeb()
}
private fun initWeb() {
val conf = configuration.find { it.clients.any { it2 -> it2.contains("web") } }
?: YoutubeGeneratePoTokenItem(false, mutableListOf("mweb"), mutableListOf(), "")
val switch = requireView().findViewById<MaterialSwitch>(R.id.web_client_switch)
val gvs = requireView().findViewById<TextView>(R.id.content_gvs)
val player = requireView().findViewById<TextView>(R.id.content_player)
val visitorData = requireView().findViewById<TextView>(R.id.content_visitordata)
val playerClientDiv = requireView().findViewById<View>(R.id.playerclient_div)
val playerClientText = requireView().findViewById<TextView>(R.id.content_playerclient)
val regenerate = requireView().findViewById<MaterialButton>(R.id.regenerate_webview_potokens)
switch.isChecked = conf.enabled
fun setValues(conf: YoutubeGeneratePoTokenItem) {
gvs.text = conf.poTokens.find { it.context == "gvs" }?.token ?: ""
player.text = conf.poTokens.find { it.context == "player" }?.token ?: ""
visitorData.text = conf.visitorData
playerClientText.text = conf.clients.joinToString(", ")
}
setValues(conf)
val clicker = View.OnClickListener {
UiUtil.copyToClipboard((it as TextView).text.toString(), requireActivity())
}
gvs.setOnClickListener(clicker)
player.setOnClickListener(clicker)
visitorData.setOnClickListener(clicker)
playerClientDiv.setOnClickListener {
val webClients = requireContext().getStringArray(R.array.web_player_clients)
val selectedItems = webClients.map {
conf.clients.contains(it)
}.toBooleanArray()
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.player_client))
.setMultiChoiceItems(webClients, selectedItems) { _, which, isChecked ->
selectedItems[which] = isChecked
}
.setPositiveButton(R.string.ok) { _, _ ->
val newValues = webClients
.filterIndexed { index, _ -> selectedItems[index] }
.toMutableSet()
configuration.remove(conf)
conf.clients.clear()
conf.clients.addAll(newValues)
configuration.add(conf)
preferences.edit().putString("youtube_generated_po_tokens", Gson().toJson(configuration).toString()).apply()
setValues(conf)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
regenerate.setOnClickListener {
lifecycleScope.launch {
val res = withContext(Dispatchers.IO) {
poTokenGenerator.getWebClientPoToken(sampleURL.getIDFromYoutubeURL())
}
if (res == null) {
Snackbar.make(requireView(), getString(R.string.network_error), Snackbar.LENGTH_LONG).show()
}else{
configuration.remove(conf)
conf.poTokens.clear()
conf.poTokens.add(YoutubePoTokenItem("gvs", res.streamingDataPoToken ?: ""))
conf.poTokens.add(YoutubePoTokenItem("player", res.playerRequestPoToken))
conf.visitorData = res.visitorData
configuration.add(conf)
setValues(conf)
preferences.edit().putString("youtube_generated_po_tokens", Gson().toJson(configuration).toString()).apply()
}
}
}
switch.setOnClickListener {
configuration.remove(conf)
conf.enabled = switch.isEnabled
configuration.add(conf)
preferences.edit().putString("youtube_generated_po_tokens", Gson().toJson(configuration).toString()).apply()
regenerate.performClick()
}
}
}

View file

@ -22,6 +22,7 @@ import androidx.core.view.children
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
@ -94,7 +95,7 @@ class YoutubePlayerClientFragment : Fragment(), YoutubePlayerClientAdapter.OnIte
}
)
}
checkNoResults()
dragHandle.setOnClickListener {

View file

@ -575,4 +575,21 @@ object Extensions {
else "%(section_title&{} - |)s$this"
}
}
fun String.getIDFromYoutubeURL() : String {
var el: Array<String?> =
this.split("/".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
var query = el[el.size - 1]
if (query!!.contains("watch?v=")) {
query = query.substring(8)
}
el = query.split("&".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
query = el[0]
el = query!!.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
query = el[0]
return query!!
}
}

View file

@ -69,7 +69,6 @@ import com.deniscerri.ytdl.ui.downloadcard.VideoCutListener
import com.deniscerri.ytdl.util.Extensions.enableTextHighlight
import com.deniscerri.ytdl.util.Extensions.getMediaDuration
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.extractors.PipedApiUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils
@ -2030,81 +2029,6 @@ object UiUtil {
}
fun showPipedInstancesDialog(context: Activity, currentInstance: String, instanceSelected: (f: String) -> Unit){
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.piped_instance))
val view = context.layoutInflater.inflate(R.layout.filename_template_dialog, null)
val editText = view.findViewById<EditText>(R.id.filename_edittext)
view.findViewById<TextInputLayout>(R.id.filename).apply {
hint = context.getString(R.string.piped_instance)
endIconMode = END_ICON_NONE
}
editText.setText(currentInstance)
editText.setSelection(editText.text.length)
builder.setView(view)
builder.setPositiveButton(
context.getString(R.string.ok)
) { _: DialogInterface?, _: Int ->
instanceSelected(editText.text.toString())
}
// handle the negative button of the alert dialog
builder.setNegativeButton(
context.getString(R.string.cancel)
) { _: DialogInterface?, _: Int -> }
builder.setNeutralButton("?") { _: DialogInterface?, _: Int ->
val browserIntent =
Intent(Intent.ACTION_VIEW, Uri.parse("https://github.com/TeamPiped/Piped/wiki/Instances"))
context.startActivity(browserIntent)
}
view.findViewById<View>(R.id.suggested).visibility = View.GONE
val dialog = builder.create()
dialog.show()
val imm = context.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager
editText!!.postDelayed({
editText.requestFocus()
imm.showSoftInput(editText, 0)
}, 300)
//handle suggestion chips
CoroutineScope(Dispatchers.IO).launch {
val chipGroup = view.findViewById<ChipGroup>(R.id.filename_suggested_chipgroup)
val chips = mutableListOf<Chip>()
val instances = PipedApiUtil(context).getPipedInstances().ifEmpty { return@launch }
instances.forEach { s ->
val tmp = context.layoutInflater.inflate(R.layout.filter_chip, chipGroup, false) as Chip
tmp.text = s
tmp.setOnClickListener {
val c = it as Chip
c.toggle()
editText.setText(c.text.toString())
editText.setSelection(c.text.length)
}
chips.add(tmp)
}
withContext(Dispatchers.Main){
view.findViewById<View>(R.id.suggested).visibility = View.VISIBLE
chips.forEach {
it.isChecked = editText.text == it.text
chipGroup!!.addView(it)
}
editText.doOnTextChanged { text, start, before, count ->
chips.forEach {
it.isChecked = editText.text == it.text
}
}
}
}
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
}
private fun showGeneratedCommand(context: Activity, preferences: SharedPreferences, command: String) {
val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.command))

View file

@ -1,336 +0,0 @@
package com.deniscerri.ytdl.util.extractors
import android.content.Context
import android.content.SharedPreferences
import android.text.Html
import android.util.Log
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.Format
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 com.yausername.youtubedl_android.YoutubeDLException
import kotlinx.coroutines.delay
import org.json.JSONException
import org.json.JSONObject
import java.util.Locale
import kotlin.coroutines.cancellation.CancellationException
class PipedApiUtil(context: Context) {
private var sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val countryCode = sharedPreferences.getString("locale", "")!!.ifEmpty { "US" }
private val defaultPipedURL = "https://pipedapi.kavin.rocks/"
private val pipedURL = sharedPreferences.getString("piped_instance", "")!!.ifEmpty { defaultPipedURL }.removeSuffix("/")
fun getPipedInstances() : List<String> {
kotlin.runCatching {
val res = NetworkUtil.genericArrayRequest("https://piped-instances.kavin.rocks/")
val list = mutableListOf<String>()
for (i in 0 until res.length()) {
val element = res.getJSONObject(i)
list.add(element.getString("api_url"))
}
return list
}
return listOf()
}
fun getVideoData(url : String) : Result<List<ResultItem>> {
val id = getIDFromYoutubeURL(url)
val res = NetworkUtil.genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
return Result.failure(Throwable())
}
val vid = createVideoFromPipedJSON(res, url) ?: return Result.failure(Throwable())
return Result.success(listOf(vid))
}
fun getFormats(url: String) : Result<List<Format> > {
try {
val id = getIDFromYoutubeURL(url)
val res = NetworkUtil.genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
return Result.failure(Throwable())
}else {
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id", true)
return Result.success(item!!.formats)
}
}catch(e: Exception) {
println(e)
if (e is CancellationException) throw e
return Result.failure(e)
}
}
fun getFormatsForAll(urls: List<String>, progress: (progress: ResultViewModel.MultipleFormatProgress) -> Unit) : Result<MutableList<MutableList<Format>>> {
return kotlin.runCatching {
val formatCollection = mutableListOf<MutableList<Format>>()
urls.forEach { url ->
val id = getIDFromYoutubeURL(url)
val res = NetworkUtil.genericRequest("$pipedURL/streams/$id")
createVideoFromPipedJSON(res, url).apply {
formatCollection.add(this!!.formats)
progress(
ResultViewModel.MultipleFormatProgress(url, this.formats)
)
}
}
return Result.success(formatCollection)
}.onFailure {
return Result.failure(it)
}
}
@Throws(JSONException::class)
fun search(query: String): Result<ArrayList<ResultItem>> {
val items = arrayListOf<ResultItem>()
val data = NetworkUtil.genericRequest("$pipedURL/search?q=$query&filter=videos&region=${countryCode}")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return Result.failure(Throwable())
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
if (element.getInt("duration") == -1) continue
element.put("uploader", element.getString("uploaderName"))
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
if (v == null || v.thumb.isEmpty()) {
continue
}
items.add(v)
}
return Result.success(items)
}
@Throws(JSONException::class)
fun searchMusic(query: String): Result<ArrayList<ResultItem>> {
val items = arrayListOf<ResultItem>()
val data = NetworkUtil.genericRequest("$pipedURL/search?q=$query=&filter=music_songs&region=${countryCode}")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return Result.failure(Throwable())
for (i in 0 until dataArray.length()) {
val element = dataArray.getJSONObject(i)
if (element.getInt("duration") == -1) continue
element.put("uploader", element.getString("uploaderName"))
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
if (v == null || v.thumb.isEmpty()) {
continue
}
items.add(v)
}
return Result.success(items)
}
fun getStreamingUrlAndChapters(url: String) : Result<Pair<List<String>, List<ChapterItem>?>> {
val id = getIDFromYoutubeURL(url)
val res = NetworkUtil.genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
throw Exception()
}else{
val item = createVideoFromPipedJSON(res, url)
if (item!!.urls.isBlank()) return Result.failure(Throwable())
val urls = item.urls.split(",")
val chapters = item.chapters
return Result.success(Pair(urls, chapters))
}
}
suspend fun getPlaylistData(id: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> {
val totalItems = mutableListOf<ResultItem>()
val nextPageToken = ""
var playlistName = ""
while (true) {
val items = mutableListOf<ResultItem>()
var url = ""
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
else """$pipedURL/nextpage/playlists/$id?nextpage=${
nextPageToken.replace(
"&prettyPrint",
"%26prettyPrint"
)
}"""
println(url)
val res = NetworkUtil.genericRequest(url)
if (!res.has("relatedStreams")) throw Exception()
val dataArray = res.getJSONArray("relatedStreams")
val nextpage = res.getString("nextpage")
val isMixPlaylist = nextPageToken.isBlank() && res.getInt("videos") < 0
if (isMixPlaylist) throw YoutubeDLException("This playlist type is unviewable.")
for (i in 0 until dataArray.length()) {
kotlin.runCatching {
val obj = dataArray.getJSONObject(i)
createVideoFromPipedJSON(
obj,
"https://youtube.com" + obj.getString("url")
)?.apply {
playlistTitle = playlistName.ifEmpty { runCatching { res.getString("name") }.getOrElse { "" } }
playlistURL = "https://www.youtube.com/playlist?list=$id"
items.add(this)
if (playlistTitle.isNotBlank() && playlistName.isNotBlank()) {
playlistName = playlistTitle
}
}
}
}
progress(items)
delay(1000)
totalItems.addAll(items)
if (nextpage == "null") break
}
return Result.success(totalItems)
}
fun getTrending(): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val url = "$pipedURL/trending?region=${countryCode}"
val res = NetworkUtil.genericArrayRequest(url)
for (i in 0 until res.length()) {
val element = res.getJSONObject(i)
if (element.getInt("duration") < 0) continue
element.put("uploader", element.getString("uploaderName"))
val v = createVideoFromPipedJSON(element, "https://youtube.com" + element.getString("url"))
if (v == null || v.thumb.isEmpty()) continue
items.add(v)
}
return items
}
fun getChannelData(
url: String,
progress: (pagedResults: MutableList<ResultItem>) -> Unit
): Result<List<ResultItem>> {
return Result.failure(Throwable("Not yet implemented / supported?"))
}
private fun createVideoFromPipedJSON(obj: JSONObject, url: String, ignoreFormatPreference : Boolean = false): ResultItem? {
var video: ResultItem? = null
try {
val id = getIDFromYoutubeURL(url)
val title = Html.fromHtml(obj.getString("title").toString()).toString()
val author = try {
Html.fromHtml(obj.getString("uploader").toString()).toString()
}catch (e: Exception){
Html.fromHtml(obj.getString("uploaderName").toString()).toString()
}.removeSuffix(" - Topic")
val duration = obj.getInt("duration").toStringDuration(Locale.US)
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val formats : ArrayList<Format> = ArrayList()
if(sharedPreferences.getString("formats_source", "yt-dlp") == "piped" || ignoreFormatPreference){
if (obj.has("audioStreams")){
val formatsInJSON = obj.getJSONArray("audioStreams")
for (f in 0 until formatsInJSON.length()){
val format = formatsInJSON.getJSONObject(f)
if (format.getInt("bitrate") == 0) continue
val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{
formatObj.acodec = format.getString("codec")
formatObj.asr = format.getString("quality")
if (! format.getString("audioTrackName").equals("null", ignoreCase = true)){
formatObj.format_note = format.getString("audioTrackName") + " Audio, " + formatObj.format_note
}else{
formatObj.format_note = formatObj.format_note + " Audio"
}
if (!formatObj.tbr.isNullOrBlank()){
formatObj.tbr = (formatObj.tbr!!.toInt() / 1000).toString() + "k"
}
}catch (e: Exception) {
e.printStackTrace()
}
formats.add(formatObj)
}
}
if (obj.has("videoStreams")){
val formatsInJSON = obj.getJSONArray("videoStreams")
for (f in 0 until formatsInJSON.length()){
val format = formatsInJSON.getJSONObject(f)
if (format.getInt("bitrate") == 0) continue
val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{
formatObj.vcodec = format.getString("codec")
if (!formatObj.tbr.isNullOrBlank()){
formatObj.tbr = (formatObj.tbr!!.toInt() / 1000).toString() + "k"
}
}catch (e: Exception) {
e.printStackTrace()
}
formats.add(formatObj)
}
}
formats.groupBy { it.format_id }.forEach {
if (it.value.count() > 1) {
it.value.filter { f-> !f.format_note.contains("original", true) }.forEachIndexed { index, format -> format.format_id = format.format_id.split("-")[0] + "-${index}" }
val defaultLang = it.value.find { f -> f.format_note.contains("original", true) }
defaultLang?.format_id = (defaultLang?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}"
}
}
formats.sortByDescending { it.filesize }
}
val chapters = ArrayList<ChapterItem>()
if (obj.has("chapters") && obj.getJSONArray("chapters").length() > 0){
val chaptersJArray = obj.getJSONArray("chapters")
for (c in 0 until chaptersJArray.length()){
val chapter = chaptersJArray.getJSONObject(c)
val end = if (c == chaptersJArray.length() - 1) obj.getInt("duration") else chaptersJArray.getJSONObject(c+1).getInt("start")
val item = ChapterItem(chapter.getInt("start").toLong(), end.toLong(), chapter.getString("title"))
chapters.add(item)
}
}
video = ResultItem(0,
url,
title,
author,
duration,
thumb,
"youtube",
"",
formats,
if (obj.has("hls") && obj.getString("hls") != "null") obj.getString("hls") else "",
chapters
)
} catch (e: Exception) {
Log.e("PipedAPIUtil", e.toString())
}
return video
}
private fun getIDFromYoutubeURL(inputQuery: String) : String {
var el: Array<String?> =
inputQuery.split("/".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
var query = el[el.size - 1]
if (query!!.contains("watch?v=")) {
query = query.substring(8)
}
el = query.split("&".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
query = el[0]
el = query!!.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
query = el[0]
return query!!
}
}

View file

@ -17,6 +17,7 @@ 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.ResultItem
import com.deniscerri.ytdl.database.models.YoutubeGeneratePoTokenItem
import com.deniscerri.ytdl.database.models.YoutubePlayerClientItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
@ -635,6 +636,7 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
if (aria2) {
request.addOption("--downloader", "libaria2c.so")
//request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"")
request.addOption("--external-downloader-args", "aria2c:\"--check-certificate=false\"")
}
val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
@ -1296,18 +1298,23 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
}
}
if (sharedPreferences.getBoolean("use_newpipe_potoken", false)) {
val visitorData = sharedPreferences.getString("newpipe_visitordata", "")!!
if (visitorData.isNotBlank()) {
playerClients.add("web")
sharedPreferences.getString("newpipe_gvs_potoken", "")?.apply {
if (this.isNotBlank()) poTokens.add("web.gvs+$this")
val generatedPoTokensRaw = sharedPreferences.getString("youtube_generated_po_tokens", "[]")
val generatedPoTokens = Gson().fromJson(generatedPoTokensRaw,Array<YoutubeGeneratePoTokenItem>::class.java).toMutableList()
if (generatedPoTokens.isNotEmpty()) {
for (value in generatedPoTokens) {
if (value.enabled) {
for (cl in value.clients) {
playerClients.add(cl)
for (pt in value.poTokens) {
if (pt.token.isNotBlank()) {
poTokens.add("${cl}.${pt.context}+${pt.token}")
}
}
}
extractorArgs.add("player-skip=webpage,configs")
extractorArgs.add("visitor_data=${value.visitorData}")
}
sharedPreferences.getString("newpipe_player_potoken", "")?.apply {
if (this.isNotBlank()) poTokens.add("web.player+$this")
}
extractorArgs.add("player-skip=webpage,configs")
extractorArgs.add("visitor_data=$visitorData")
}
}

View file

@ -8,8 +8,9 @@ import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.getIDFromYoutubeURL
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.extractors.newpipe.potoken.NewPipePoTokenGenerator
import com.deniscerri.ytdl.util.extractors.potoken.PoTokenGenerator
import com.google.gson.Gson
import okhttp3.OkHttpClient
import org.json.JSONException
@ -35,16 +36,10 @@ class NewPipeUtil(context: Context) {
private var sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val countryCode = sharedPreferences.getString("locale", "")!!.ifEmpty { "US" }
private val language = sharedPreferences.getString("app_language", "")!!.ifEmpty { "en" }
private val testURL = "https://www.youtube.com/watch?v=aqz-KE-bpKQ" //bbb
init {
NewPipe.init(NewPipeDownloaderImpl(OkHttpClient.Builder()), Localization(language, countryCode))
YoutubeStreamExtractor.setPoTokenProvider(NewPipePoTokenGenerator())
}
fun testRun() {
getVideoData(testURL)
return
YoutubeStreamExtractor.setPoTokenProvider(PoTokenGenerator())
}
fun getVideoData(url : String) : Result<List<ResultItem>> {
@ -288,7 +283,7 @@ class NewPipeUtil(context: Context) {
private fun createVideoFromStreamInfoItem(stream: StreamInfoItem, url: String) : ResultItem? {
var video: ResultItem? = null
try {
val id = getIDFromYoutubeURL(url)
val id = url.getIDFromYoutubeURL()
val title = stream.name
val author = stream.uploaderName.removeSuffix(" - Topic")
val duration = stream.duration.toInt().toStringDuration(Locale.US)
@ -316,7 +311,7 @@ class NewPipeUtil(context: Context) {
private fun createVideoFromStream(stream: StreamInfo, url: String, ignoreFormatPreference : Boolean = false): ResultItem? {
var video: ResultItem? = null
try {
val id = getIDFromYoutubeURL(url)
val id = url.getIDFromYoutubeURL()
val title = stream.name
val author = stream.uploaderName.removeSuffix(" - Topic")
val duration = stream.duration.toInt().toStringDuration(Locale.US)
@ -420,27 +415,4 @@ class NewPipeUtil(context: Context) {
}
return video
}
private fun getIDFromYoutubeURL(inputQuery: String) : String {
var el: Array<String?> =
inputQuery.split("/".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
var query = el[el.size - 1]
if (query!!.contains("watch?v=")) {
query = query.substring(8)
}
el = query.split("&".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
query = el[0]
el = query!!.split("\\?".toRegex()).dropLastWhile { it.isEmpty() }
.toTypedArray()
query = el[0]
return query!!
}
private fun generatePoToken(url: String) : PoTokenResult? {
val generator = NewPipePoTokenGenerator()
val id = getIDFromYoutubeURL(url)
return generator.getWebClientPoToken(id)
}
}

View file

@ -1,4 +1,4 @@
package com.deniscerri.ytdl.util.extractors.newpipe.potoken
package com.deniscerri.ytdl.util.extractors.potoken
class PoTokenException(message: String) : Exception(message)

View file

@ -1,4 +1,4 @@
package com.deniscerri.ytdl.util.extractors.newpipe.potoken
package com.deniscerri.ytdl.util.extractors.potoken
import android.os.Handler
import android.os.Looper
@ -7,22 +7,16 @@ import android.webkit.CookieManager
import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.database.models.YoutubePlayerClientItem
import com.deniscerri.ytdl.database.models.YoutubePoTokenItem
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import com.deniscerri.ytdl.util.extractors.potoken.webview.PoTokenWebView
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import org.schabi.newpipe.extractor.NewPipe
import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo
import org.schabi.newpipe.extractor.services.youtube.PoTokenProvider
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
class NewPipePoTokenGenerator : PoTokenProvider {
val TAG = NewPipePoTokenGenerator::class.simpleName
class PoTokenGenerator : PoTokenProvider {
val TAG = PoTokenGenerator::class.simpleName
private val supportsWebView by lazy { runCatching { CookieManager.getInstance() }.isSuccess }
private object WebPoTokenGenLock
@ -39,15 +33,6 @@ class NewPipePoTokenGenerator : PoTokenProvider {
val result = kotlin.runCatching {
getWebClientPoToken(videoId, false)
}
result.getOrNull()?.apply {
val preferences = PreferenceManager.getDefaultSharedPreferences(App.instance)
val editor = preferences.edit()
editor.putString("newpipe_gvs_potoken", this.streamingDataPoToken ?: "")
editor.putString("newpipe_player_potoken", this.playerRequestPoToken)
editor.putString("newpipe_visitordata", this.visitorData)
editor.apply()
}
return result.getOrNull()
}
@ -84,8 +69,7 @@ class NewPipePoTokenGenerator : PoTokenProvider {
webPoTokenGenerator?.let { Handler(Looper.getMainLooper()).post { it.close() } }
// create a new webPoTokenGenerator
webPoTokenGenerator = PoTokenWebView
.getNewPoTokenGenerator(App.instance)
webPoTokenGenerator = PoTokenWebView.getNewPoTokenGenerator(App.instance)
// The streaming poToken needs to be generated exactly once before generating
// any other (player) tokens.

View file

@ -1,5 +1,6 @@
package com.deniscerri.ytdl.util.extractors.newpipe.potoken
package com.deniscerri.ytdl.util.extractors.potoken.webview
import com.deniscerri.ytdl.util.extractors.potoken.PoTokenException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject

View file

@ -1,4 +1,4 @@
package com.deniscerri.ytdl.util.extractors.newpipe.potoken
package com.deniscerri.ytdl.util.extractors.potoken.webview
import android.content.Context
import android.os.Build
@ -18,10 +18,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.util.extractors.newpipe.potoken.JavascriptUtil.parseChallengeData
import com.deniscerri.ytdl.util.extractors.newpipe.potoken.JavascriptUtil.parseIntegrityTokenData
import com.deniscerri.ytdl.util.extractors.newpipe.potoken.JavascriptUtil.stringToU8
import com.deniscerri.ytdl.util.extractors.newpipe.potoken.JavascriptUtil.u8ToBase64
import com.deniscerri.ytdl.util.extractors.potoken.BadWebViewException
import com.deniscerri.ytdl.util.extractors.potoken.PoTokenException
import com.deniscerri.ytdl.util.extractors.potoken.buildExceptionForJsError
import com.deniscerri.ytdl.util.extractors.potoken.webview.JavascriptUtil.parseChallengeData
import com.deniscerri.ytdl.util.extractors.potoken.webview.JavascriptUtil.parseIntegrityTokenData
import com.deniscerri.ytdl.util.extractors.potoken.webview.JavascriptUtil.stringToU8
import com.deniscerri.ytdl.util.extractors.potoken.webview.JavascriptUtil.u8ToBase64
import okhttp3.Headers.Companion.toHeaders
import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody
@ -39,8 +42,7 @@ class PoTokenWebView private constructor(
) {
private val webView = WebView(context)
private val scope = MainScope()
private val poTokenContinuations =
Collections.synchronizedMap(ArrayMap<String, Continuation<String>>())
private val poTokenContinuations = Collections.synchronizedMap(ArrayMap<String, Continuation<String>>())
private val exceptionHandler = CoroutineExceptionHandler { _, t ->
onInitializationErrorCloseAndCancel(t)
}
@ -60,13 +62,6 @@ class PoTokenWebView private constructor(
// so that we can run async functions and get back the result
webView.addJavascriptInterface(this, JS_INTERFACE)
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
preferences.getString("api_key", "")?.apply {
if (this.isNotBlank()) {
GOOGLE_API_KEY = this
}
}
webView.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(m: ConsoleMessage): Boolean {
if (m.message().contains("Uncaught")) {
@ -330,7 +325,7 @@ class PoTokenWebView private constructor(
companion object {
private const val TAG = "PoTokenWebView"
//libretube api key, if user has his own api key his will be used
//libretube api key
private var GOOGLE_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw"
private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo"
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " +

View file

@ -0,0 +1,5 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="?android:colorAccent" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:fillColor="@android:color/white" android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM16.23,18L12,15.45 7.77,18l1.12,-4.81 -3.73,-3.23 4.92,-0.42L12,5l1.92,4.53 4.92,0.42 -3.73,3.23L16.23,18z"/>
</vector>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView android:layout_width="match_parent"
<androidx.core.widget.NestedScrollView xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:scrollbars="none"
android:orientation="vertical"
android:layout_height="wrap_content"
@ -200,7 +201,8 @@
android:paddingHorizontal="10dp"
android:text="-"
android:textSize="20sp"
android:textStyle="bold" />
android:textStyle="bold"
tools:ignore="HardcodedText" />
<com.google.android.material.textfield.TextInputLayout

View file

@ -0,0 +1,228 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.deniscerri.ytdl.ui.more.terminal.TerminalActivity">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/web_client_switch"
android:layout_width="match_parent"
android:background="?attr/colorSurfaceContainer"
android:paddingHorizontal="20dp"
android:layout_height="70dp"
android:checked="false"
android:textSize="16sp"
android:textStyle="bold"
android:text="Web Client Po Token"
tools:ignore="HardcodedText" />
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
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"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="15dp">
<TextView
android:id="@+id/title_gvs"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textSize="15sp"
android:textStyle="bold"
android:text="PO Token (GVS)"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/content_gvs"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:maxLines="3"
android:ellipsize="end"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_gvs" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
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"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="15dp">
<TextView
android:id="@+id/title_player"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textSize="15sp"
android:textStyle="bold"
android:text="PO Token (Player)"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/content_player"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:maxLines="3"
android:ellipsize="end"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_player" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
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"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="15dp">
<TextView
android:id="@+id/title_visitordata"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textSize="15sp"
android:textStyle="bold"
android:text="Visitor Data"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/content_visitordata"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:maxLines="3"
android:ellipsize="end"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_visitordata" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
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"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/playerclient_div"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="15dp">
<TextView
android:id="@+id/title_playerclients"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:textSize="15sp"
android:textStyle="bold"
android:text="@string/player_client"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/content_playerclient"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:maxLines="3"
android:ellipsize="end"
android:textSize="12sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title_playerclients" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
<Button
android:id="@+id/regenerate_webview_potokens"
style="@style/Widget.Material3.Button.ElevatedButton.Icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:autoLink="all"
android:text="@string/regenerate"
android:outlineProvider="none"
android:layout_gravity="end"
android:layout_margin="20dp"
app:icon="@drawable/baseline_stars_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -61,27 +61,6 @@
android:layout_width="0dp"
android:layout_height="wrap_content">
<TextView
android:id="@+id/autoGeneratedNewPipe"
style="@style/Widget.Material3.FloatingActionButton.Large.Tertiary"
android:layout_width="wrap_content"
android:visibility="gone"
android:layout_height="wrap_content"
android:text="NewPipe Autogenerated"
android:background="@drawable/rounded_corner"
android:backgroundTint="?attr/colorAccent"
android:clickable="false"
android:gravity="center"
android:minWidth="30dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
android:textStyle="bold"
app:cornerRadius="10dp"
app:layout_constraintTop_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/urlRegex"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"

View file

@ -32,6 +32,26 @@
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" />
<TextView
android:id="@+id/index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:ellipsize="end"
android:layout_margin="5dp"
android:background="#80000000"
android:gravity="center"
android:textStyle="bold"
android:maxLength="17"
android:maxLines="1"
android:minWidth="10dp"
android:paddingHorizontal="5dp"
android:textSize="12sp"
app:cornerRadius="10dp"
app:layout_constraintTop_toTopOf="@+id/downloads_image_view"
app:layout_constraintStart_toStartOf="@+id/downloads_image_view" />
<TextView
android:id="@+id/duration"
android:layout_width="wrap_content"

View file

@ -56,6 +56,9 @@
<action
android:id="@+id/action_advancedSettingsFragment_to_youtubePlayerClientFragment"
app:destination="@id/youtubePlayerClientFragment" />
<action
android:id="@+id/action_advancedSettingsFragment_to_generateYoutubePoTokensFragment"
app:destination="@id/generateYoutubePoTokensFragment" />
</fragment>
<fragment
android:id="@+id/youtubePlayerClientFragment"
@ -65,4 +68,8 @@
android:id="@+id/changeLogFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.updating.ChangeLogFragment"
android:label="ChangeLogFragment" />
<fragment
android:id="@+id/generateYoutubePoTokensFragment"
android:name="com.deniscerri.ytdl.ui.more.settings.advanced.GenerateYoutubePoTokensFragment"
android:label="GenerateYoutubePoTokensFragment" />
</navigation>

View file

@ -1541,4 +1541,13 @@
<item>all</item>
</string-array>
<string-array name="web_player_clients">
<item>web</item>
<item>mweb</item>
<item>web_safari</item>
<item>web_embedded</item>
<item>web_creator</item>
<item>web_music</item>
</string-array>
</resources>

View file

@ -472,6 +472,6 @@
<string name="automatic_backup_summary">Automatically make a backup of everything when a new version of the application is found</string>
<string name="write_subs_when_embed_subs">Write subtitles when embedding</string>
<string name="write_subs_when_embed_subs_summary">This will apply Save Subtitles and Save Automatic Subtitles to download the sub files and then delete them after embedding</string>
<string name="use_newpipe_potoken">Use Autogenerated PO Tokens from NewPipe Extractor</string>
<string name="use_newpipe_potoken_warning">By enabling this, the app will disable cookies and use the generated po tokens and visitor data as YouTube Extractor Arguments</string>
<string name="generate_potokens">Generate PO Tokens</string>
<string name="regenerate">Re-generate</string>
</resources>

View file

@ -38,6 +38,12 @@
<item name="cornerSize">7dp</item>
</style>
<style name="ShapeAppearanceOverlay.RightCorner" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSizeBottomRight">10dp</item>
<item name="backgroundTint">#80000000</item>
</style>
<style name="ShapeAppearanceOverlay.Chip.Rounded" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSizeTopLeft">12dp</item>

View file

@ -10,12 +10,12 @@
android:summary="@string/player_client_summary"
android:title="@string/player_client" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
android:icon="@drawable/ic_language"
android:defaultValue="false"
android:key="use_newpipe_potoken"
android:title="@string/use_newpipe_potoken" />
<Preference
android:defaultValue=""
android:icon="@drawable/baseline_stars_24"
android:key="generate_po_tokens"
android:summary="Web Client PO Token"
android:title="@string/generate_potokens" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"

View file

@ -102,15 +102,6 @@
app:useSimpleSummaryProvider="true"
app:title="@string/format_source" />
<Preference
android:icon="@drawable/ic_dns"
app:key="piped_instance"
app:defaultValue=""
app:isPreferenceVisible="false"
android:summary="@string/piped_instance_summary"
app:title="@string/piped_instance" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"