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 poTokens: MutableList<YoutubePoTokenItem>,
var enabled: Boolean = true, var enabled: Boolean = true,
var useOnlyPoToken: Boolean = false, var useOnlyPoToken: Boolean = false,
var urlRegex: MutableList<String> = mutableListOf(), var urlRegex: MutableList<String> = mutableListOf()
var autoGenerated: Boolean = false
) )
data class YoutubePoTokenItem( 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.title).text = item!!.title
card.findViewById<TextView>(R.id.author).text = item.author card.findViewById<TextView>(R.id.author).text = item.author
card.findViewById<TextView>(R.id.duration).text = item.duration card.findViewById<TextView>(R.id.duration).text = item.duration
card.findViewById<TextView>(R.id.index).text = ((item.playlistIndex ?: (position + 1))).toString()
// CHECKBOX ---------------------------------- // CHECKBOX ----------------------------------
val check = card.findViewById<CheckBox>(R.id.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 title.alpha = if (item.enabled) 1f else 0.3f
content.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) val switch = card.findViewById<MaterialSwitch>(R.id.materialSwitch)
switch.isChecked = item.enabled switch.isChecked = item.enabled
switch.setOnClickListener { 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 = view.findViewById(R.id.from_textinput_edittext)
startTextInput.keyListener = DigitsKeyListener.getInstance("0123456789:.") startTextInput.keyListener = DigitsKeyListener.getInstance("0123456789:.")
startTextInput.imeOptions = EditorInfo.IME_ACTION_DONE startTextInput.imeOptions = EditorInfo.IME_ACTION_DONE
startTextInput.inputType = EditorInfo.TYPE_NUMBER_FLAG_DECIMAL startTextInput.inputType = EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS
startTextInput.maxLines = 1 startTextInput.maxLines = 1
endTextInput = view.findViewById(R.id.to_textinput_edittext) endTextInput = view.findViewById(R.id.to_textinput_edittext)
endTextInput.keyListener = DigitsKeyListener.getInstance("0123456789:.") endTextInput.keyListener = DigitsKeyListener.getInstance("0123456789:.")
endTextInput.imeOptions = EditorInfo.IME_ACTION_DONE endTextInput.imeOptions = EditorInfo.IME_ACTION_DONE
endTextInput.inputType = EditorInfo.TYPE_NUMBER_FLAG_DECIMAL endTextInput.inputType = EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS
endTextInput.maxLines = 1 endTextInput.maxLines = 1
cancelBtn = view.findViewById(R.id.cancelButton) cancelBtn = view.findViewById(R.id.cancelButton)

View file

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

View file

@ -39,51 +39,9 @@ class AdvancedSettingsFragment : BaseSettingsFragment() {
false false
} }
val newPipeUtil = NewPipeUtil(requireContext()) findPreference<Preference>("generate_po_tokens")?.setOnPreferenceClickListener {
findNavController().navigate(R.id.generateYoutubePoTokensFragment)
findPreference<SwitchPreferenceCompat>("use_newpipe_potoken")?.apply { false
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
}
} }
val formatImportanceAudio: Preference? = findPreference("format_importance_audio") 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.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.navigation.fragment.findNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
@ -94,7 +95,7 @@ class YoutubePlayerClientFragment : Fragment(), YoutubePlayerClientAdapter.OnIte
} }
) )
} }
checkNoResults() checkNoResults()
dragHandle.setOnClickListener { dragHandle.setOnClickListener {

View file

@ -575,4 +575,21 @@ object Extensions {
else "%(section_title&{} - |)s$this" 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.enableTextHighlight
import com.deniscerri.ytdl.util.Extensions.getMediaDuration import com.deniscerri.ytdl.util.Extensions.getMediaDuration
import com.deniscerri.ytdl.util.Extensions.toStringDuration import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.deniscerri.ytdl.util.extractors.PipedApiUtil
import com.deniscerri.ytdl.util.extractors.YTDLPUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil
import com.google.android.material.badge.BadgeDrawable import com.google.android.material.badge.BadgeDrawable
import com.google.android.material.badge.BadgeUtils 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) { private fun showGeneratedCommand(context: Activity, preferences: SharedPreferences, command: String) {
val builder = MaterialAlertDialogBuilder(context) val builder = MaterialAlertDialogBuilder(context)
builder.setTitle(context.getString(R.string.command)) 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.DownloadItem
import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.ResultItem 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.models.YoutubePlayerClientItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
@ -635,6 +636,7 @@ class YTDLPUtil(private val context: Context, private val commandTemplateDao: Co
if (aria2) { if (aria2) {
request.addOption("--downloader", "libaria2c.so") request.addOption("--downloader", "libaria2c.so")
//request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"") //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) 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 generatedPoTokensRaw = sharedPreferences.getString("youtube_generated_po_tokens", "[]")
val visitorData = sharedPreferences.getString("newpipe_visitordata", "")!! val generatedPoTokens = Gson().fromJson(generatedPoTokensRaw,Array<YoutubeGeneratePoTokenItem>::class.java).toMutableList()
if (visitorData.isNotBlank()) { if (generatedPoTokens.isNotEmpty()) {
playerClients.add("web") for (value in generatedPoTokens) {
sharedPreferences.getString("newpipe_gvs_potoken", "")?.apply { if (value.enabled) {
if (this.isNotBlank()) poTokens.add("web.gvs+$this") 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.Format
import com.deniscerri.ytdl.database.models.ResultItem import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.util.Extensions.getIDFromYoutubeURL
import com.deniscerri.ytdl.util.Extensions.toStringDuration 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 com.google.gson.Gson
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.json.JSONException import org.json.JSONException
@ -35,16 +36,10 @@ class NewPipeUtil(context: Context) {
private var sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) private var sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val countryCode = sharedPreferences.getString("locale", "")!!.ifEmpty { "US" } private val countryCode = sharedPreferences.getString("locale", "")!!.ifEmpty { "US" }
private val language = sharedPreferences.getString("app_language", "")!!.ifEmpty { "en" } private val language = sharedPreferences.getString("app_language", "")!!.ifEmpty { "en" }
private val testURL = "https://www.youtube.com/watch?v=aqz-KE-bpKQ" //bbb
init { init {
NewPipe.init(NewPipeDownloaderImpl(OkHttpClient.Builder()), Localization(language, countryCode)) NewPipe.init(NewPipeDownloaderImpl(OkHttpClient.Builder()), Localization(language, countryCode))
YoutubeStreamExtractor.setPoTokenProvider(NewPipePoTokenGenerator()) YoutubeStreamExtractor.setPoTokenProvider(PoTokenGenerator())
}
fun testRun() {
getVideoData(testURL)
return
} }
fun getVideoData(url : String) : Result<List<ResultItem>> { fun getVideoData(url : String) : Result<List<ResultItem>> {
@ -288,7 +283,7 @@ class NewPipeUtil(context: Context) {
private fun createVideoFromStreamInfoItem(stream: StreamInfoItem, url: String) : ResultItem? { private fun createVideoFromStreamInfoItem(stream: StreamInfoItem, url: String) : ResultItem? {
var video: ResultItem? = null var video: ResultItem? = null
try { try {
val id = getIDFromYoutubeURL(url) val id = url.getIDFromYoutubeURL()
val title = stream.name val title = stream.name
val author = stream.uploaderName.removeSuffix(" - Topic") val author = stream.uploaderName.removeSuffix(" - Topic")
val duration = stream.duration.toInt().toStringDuration(Locale.US) 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? { private fun createVideoFromStream(stream: StreamInfo, url: String, ignoreFormatPreference : Boolean = false): ResultItem? {
var video: ResultItem? = null var video: ResultItem? = null
try { try {
val id = getIDFromYoutubeURL(url) val id = url.getIDFromYoutubeURL()
val title = stream.name val title = stream.name
val author = stream.uploaderName.removeSuffix(" - Topic") val author = stream.uploaderName.removeSuffix(" - Topic")
val duration = stream.duration.toInt().toStringDuration(Locale.US) val duration = stream.duration.toInt().toStringDuration(Locale.US)
@ -420,27 +415,4 @@ class NewPipeUtil(context: Context) {
} }
return video 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) 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.Handler
import android.os.Looper import android.os.Looper
@ -7,22 +7,16 @@ import android.webkit.CookieManager
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdl.App import com.deniscerri.ytdl.App
import com.deniscerri.ytdl.BuildConfig import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.database.models.YoutubePlayerClientItem import com.deniscerri.ytdl.util.extractors.potoken.webview.PoTokenWebView
import com.deniscerri.ytdl.database.models.YoutubePoTokenItem
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking 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.NewPipe
import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo import org.schabi.newpipe.extractor.services.youtube.InnertubeClientRequestInfo
import org.schabi.newpipe.extractor.services.youtube.PoTokenProvider import org.schabi.newpipe.extractor.services.youtube.PoTokenProvider
import org.schabi.newpipe.extractor.services.youtube.PoTokenResult import org.schabi.newpipe.extractor.services.youtube.PoTokenResult
import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper import org.schabi.newpipe.extractor.services.youtube.YoutubeParsingHelper
class NewPipePoTokenGenerator : PoTokenProvider { class PoTokenGenerator : PoTokenProvider {
val TAG = NewPipePoTokenGenerator::class.simpleName val TAG = PoTokenGenerator::class.simpleName
private val supportsWebView by lazy { runCatching { CookieManager.getInstance() }.isSuccess } private val supportsWebView by lazy { runCatching { CookieManager.getInstance() }.isSuccess }
private object WebPoTokenGenLock private object WebPoTokenGenLock
@ -39,15 +33,6 @@ class NewPipePoTokenGenerator : PoTokenProvider {
val result = kotlin.runCatching { val result = kotlin.runCatching {
getWebClientPoToken(videoId, false) 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() return result.getOrNull()
} }
@ -84,8 +69,7 @@ class NewPipePoTokenGenerator : PoTokenProvider {
webPoTokenGenerator?.let { Handler(Looper.getMainLooper()).post { it.close() } } webPoTokenGenerator?.let { Handler(Looper.getMainLooper()).post { it.close() } }
// create a new webPoTokenGenerator // create a new webPoTokenGenerator
webPoTokenGenerator = PoTokenWebView webPoTokenGenerator = PoTokenWebView.getNewPoTokenGenerator(App.instance)
.getNewPoTokenGenerator(App.instance)
// The streaming poToken needs to be generated exactly once before generating // The streaming poToken needs to be generated exactly once before generating
// any other (player) tokens. // 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.Json
import kotlinx.serialization.json.JsonNull import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject 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.content.Context
import android.os.Build import android.os.Build
@ -18,10 +18,13 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import com.deniscerri.ytdl.BuildConfig import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.util.extractors.newpipe.potoken.JavascriptUtil.parseChallengeData import com.deniscerri.ytdl.util.extractors.potoken.BadWebViewException
import com.deniscerri.ytdl.util.extractors.newpipe.potoken.JavascriptUtil.parseIntegrityTokenData import com.deniscerri.ytdl.util.extractors.potoken.PoTokenException
import com.deniscerri.ytdl.util.extractors.newpipe.potoken.JavascriptUtil.stringToU8 import com.deniscerri.ytdl.util.extractors.potoken.buildExceptionForJsError
import com.deniscerri.ytdl.util.extractors.newpipe.potoken.JavascriptUtil.u8ToBase64 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.Headers.Companion.toHeaders
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
@ -39,8 +42,7 @@ class PoTokenWebView private constructor(
) { ) {
private val webView = WebView(context) private val webView = WebView(context)
private val scope = MainScope() private val scope = MainScope()
private val poTokenContinuations = private val poTokenContinuations = Collections.synchronizedMap(ArrayMap<String, Continuation<String>>())
Collections.synchronizedMap(ArrayMap<String, Continuation<String>>())
private val exceptionHandler = CoroutineExceptionHandler { _, t -> private val exceptionHandler = CoroutineExceptionHandler { _, t ->
onInitializationErrorCloseAndCancel(t) onInitializationErrorCloseAndCancel(t)
} }
@ -60,13 +62,6 @@ class PoTokenWebView private constructor(
// so that we can run async functions and get back the result // so that we can run async functions and get back the result
webView.addJavascriptInterface(this, JS_INTERFACE) 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() { webView.webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(m: ConsoleMessage): Boolean { override fun onConsoleMessage(m: ConsoleMessage): Boolean {
if (m.message().contains("Uncaught")) { if (m.message().contains("Uncaught")) {
@ -330,7 +325,7 @@ class PoTokenWebView private constructor(
companion object { companion object {
private const val TAG = "PoTokenWebView" 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 var GOOGLE_API_KEY = "AIzaSyDyT5W0Jh49F30Pqqtyfdf7pDLFKLJoAnw"
private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo" private const val REQUEST_KEY = "O43z0dpjhgX20SCx4KAo"
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " + 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"?> <?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:scrollbars="none"
android:orientation="vertical" android:orientation="vertical"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -200,7 +201,8 @@
android:paddingHorizontal="10dp" android:paddingHorizontal="10dp"
android:text="-" android:text="-"
android:textSize="20sp" android:textSize="20sp"
android:textStyle="bold" /> android:textStyle="bold"
tools:ignore="HardcodedText" />
<com.google.android.material.textfield.TextInputLayout <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_width="0dp"
android:layout_height="wrap_content"> 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 <TextView
android:id="@+id/urlRegex" android:id="@+id/urlRegex"
style="@style/Widget.Material3.FloatingActionButton.Large.Secondary" style="@style/Widget.Material3.FloatingActionButton.Large.Secondary"

View file

@ -32,6 +32,26 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Avatar" /> 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 <TextView
android:id="@+id/duration" android:id="@+id/duration"
android:layout_width="wrap_content" android:layout_width="wrap_content"

View file

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

View file

@ -1541,4 +1541,13 @@
<item>all</item> <item>all</item>
</string-array> </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> </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="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">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="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="generate_potokens">Generate PO Tokens</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="regenerate">Re-generate</string>
</resources> </resources>

View file

@ -38,6 +38,12 @@
<item name="cornerSize">7dp</item> <item name="cornerSize">7dp</item>
</style> </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"> <style name="ShapeAppearanceOverlay.Chip.Rounded" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item> <item name="cornerFamily">rounded</item>
<item name="cornerSizeTopLeft">12dp</item> <item name="cornerSizeTopLeft">12dp</item>

View file

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

View file

@ -102,15 +102,6 @@
app:useSimpleSummaryProvider="true" app:useSimpleSummaryProvider="true"
app:title="@string/format_source" /> 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 <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"