mirror of https://github.com/deniscerri/ytdlnis
prep 1.7.9.1
parent
65462a3704
commit
f5d154a724
@ -0,0 +1,21 @@
|
||||
package com.deniscerri.ytdl.util.extractors
|
||||
|
||||
import java.util.ArrayList
|
||||
|
||||
object GoogleApiUtil {
|
||||
fun getSearchSuggestions(query: String): ArrayList<String> {
|
||||
val url = "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&q=$query"
|
||||
val res = NetworkUtil.genericArrayRequest(url)
|
||||
if (res.length() == 0) return ArrayList()
|
||||
val suggestionList = ArrayList<String>()
|
||||
try {
|
||||
for (i in 0 until res.getJSONArray(1).length()) {
|
||||
val item = res.getJSONArray(1).getString(i)
|
||||
suggestionList.add(item)
|
||||
}
|
||||
} catch (ignored: Exception) {
|
||||
ignored.printStackTrace()
|
||||
}
|
||||
return suggestionList
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.deniscerri.ytdl.util.extractors
|
||||
|
||||
import android.util.Log
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.BufferedReader
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
object NetworkUtil {
|
||||
|
||||
fun genericRequest(url: String): JSONObject {
|
||||
Log.e(NetworkUtil.toString(), url)
|
||||
val reader: BufferedReader
|
||||
var line: String?
|
||||
val responseContent = StringBuilder()
|
||||
val conn: HttpURLConnection
|
||||
var json = JSONObject()
|
||||
try {
|
||||
val req = URL(url)
|
||||
conn = req.openConnection() as HttpURLConnection
|
||||
conn.requestMethod = "GET"
|
||||
conn.connectTimeout = 3000
|
||||
conn.readTimeout = 5000
|
||||
if (conn.responseCode < 300) {
|
||||
reader = BufferedReader(InputStreamReader(conn.inputStream))
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
responseContent.append(line)
|
||||
}
|
||||
reader.close()
|
||||
json = JSONObject(responseContent.toString())
|
||||
if (json.has("error")) {
|
||||
throw Exception()
|
||||
}
|
||||
}
|
||||
conn.disconnect()
|
||||
} catch (e: Exception) {
|
||||
Log.e(NetworkUtil.toString(), e.toString())
|
||||
}
|
||||
return json
|
||||
}
|
||||
|
||||
fun genericArrayRequest(url: String): JSONArray {
|
||||
Log.e(NetworkUtil.toString(), url)
|
||||
val reader: BufferedReader
|
||||
var line: String?
|
||||
val responseContent = StringBuilder()
|
||||
val conn: HttpURLConnection
|
||||
var json = JSONArray()
|
||||
try {
|
||||
val req = URL(url)
|
||||
conn = req.openConnection() as HttpURLConnection
|
||||
conn.requestMethod = "GET"
|
||||
conn.connectTimeout = 3000
|
||||
conn.readTimeout = 5000
|
||||
if (conn.responseCode < 300) {
|
||||
reader = BufferedReader(InputStreamReader(conn.inputStream))
|
||||
while (reader.readLine().also { line = it } != null) {
|
||||
responseContent.append(line)
|
||||
}
|
||||
reader.close()
|
||||
json = JSONArray(responseContent.toString())
|
||||
}
|
||||
conn.disconnect()
|
||||
} catch (e: Exception) {
|
||||
Log.e(NetworkUtil.toString(), e.toString())
|
||||
}
|
||||
return json
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,330 @@
|
||||
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.ArrayList
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
class PipedApiUtil(private val 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®ion=${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®ion=${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
|
||||
}
|
||||
|
||||
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!!
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,139 @@
|
||||
package com.deniscerri.ytdl.util.extractors
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.deniscerri.ytdl.database.models.ResultItem
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.util.ArrayList
|
||||
import java.util.Locale
|
||||
|
||||
class YoutubeApiUtil(context: Context) {
|
||||
private var sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
private val countryCode = sharedPreferences.getString("locale", "")!!.ifEmpty { "US" }
|
||||
|
||||
@Throws(JSONException::class)
|
||||
fun getTrending(): ArrayList<ResultItem> {
|
||||
val items = arrayListOf<ResultItem>()
|
||||
val key = sharedPreferences.getString("api_key", "")!!
|
||||
val url = "https://www.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&videoCategoryId=10®ionCode=${countryCode}&maxResults=25&key=$key"
|
||||
//short data
|
||||
val res = NetworkUtil.genericRequest(url)
|
||||
//extra data from the same videos
|
||||
val contentDetails =
|
||||
NetworkUtil.genericRequest("https://www.googleapis.com/youtube/v3/videos?part=contentDetails&chart=mostPopular&videoCategoryId=10®ionCode=${countryCode}&maxResults=25&key=$key")
|
||||
if (!contentDetails.has("items")) return ArrayList()
|
||||
val dataArray = res.getJSONArray("items")
|
||||
val extraDataArray = contentDetails.getJSONArray("items")
|
||||
for (i in 0 until dataArray.length()) {
|
||||
val element = dataArray.getJSONObject(i)
|
||||
val snippet = element.getJSONObject("snippet")
|
||||
var duration = extraDataArray.getJSONObject(i).getJSONObject("contentDetails")
|
||||
.getString("duration")
|
||||
duration = formatDuration(duration)
|
||||
snippet.put("videoID", element.getString("id"))
|
||||
snippet.put("duration", duration)
|
||||
fixThumbnail(snippet)
|
||||
val v = createVideofromJSON(snippet)
|
||||
if (v == null || v.thumb.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
items.add(v)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
private fun createVideofromJSON(obj: JSONObject): ResultItem? {
|
||||
var video: ResultItem? = null
|
||||
try {
|
||||
val id = obj.getString("videoID")
|
||||
val title = obj.getString("title").toString()
|
||||
val author = obj.getString("channelTitle").toString()
|
||||
val duration = obj.getString("duration")
|
||||
val thumb = obj.getString("thumb")
|
||||
val url = "https://www.youtube.com/watch?v=$id"
|
||||
video = ResultItem(0,
|
||||
url,
|
||||
title,
|
||||
author,
|
||||
duration,
|
||||
thumb,
|
||||
"youtube",
|
||||
"",
|
||||
ArrayList(),
|
||||
"",
|
||||
ArrayList()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("YoutubeApiUtil", e.toString())
|
||||
}
|
||||
return video
|
||||
}
|
||||
|
||||
private fun formatDuration(dur: String): String {
|
||||
var badDur = dur
|
||||
if (dur == "P0D") {
|
||||
return "LIVE"
|
||||
}
|
||||
var hours = false
|
||||
var duration = ""
|
||||
badDur = badDur.substring(2)
|
||||
if (badDur.contains("H")) {
|
||||
hours = true
|
||||
duration += String.format(
|
||||
Locale.getDefault(),
|
||||
"%02d",
|
||||
badDur.substring(0, badDur.indexOf("H")).toInt()
|
||||
) + ":"
|
||||
badDur = badDur.substring(badDur.indexOf("H") + 1)
|
||||
}
|
||||
if (badDur.contains("M")) {
|
||||
duration += String.format(
|
||||
Locale.getDefault(),
|
||||
"%02d",
|
||||
badDur.substring(0, badDur.indexOf("M")).toInt()
|
||||
) + ":"
|
||||
badDur = badDur.substring(badDur.indexOf("M") + 1)
|
||||
} else if (hours) duration += "00:"
|
||||
if (badDur.contains("S")) {
|
||||
if (duration.isEmpty()) duration = "00:"
|
||||
duration += String.format(
|
||||
Locale.getDefault(),
|
||||
"%02d",
|
||||
badDur.substring(0, badDur.indexOf("S")).toInt()
|
||||
)
|
||||
} else {
|
||||
duration += "00"
|
||||
}
|
||||
if (duration == "00:00") {
|
||||
duration = ""
|
||||
}
|
||||
return duration
|
||||
}
|
||||
|
||||
private fun fixThumbnail(o: JSONObject): JSONObject {
|
||||
var imageURL = ""
|
||||
try {
|
||||
val thumbs = o.getJSONObject("thumbnails")
|
||||
imageURL = thumbs.getJSONObject("maxres").getString("url")
|
||||
} catch (e: Exception) {
|
||||
try {
|
||||
val thumbs = o.getJSONObject("thumbnails")
|
||||
imageURL = thumbs.getJSONObject("high").getString("url")
|
||||
} catch (u: Exception) {
|
||||
try {
|
||||
val thumbs = o.getJSONObject("thumbnails")
|
||||
imageURL = thumbs.getJSONObject("default").getString("url")
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
o.put("thumb", imageURL)
|
||||
} catch (ignored: Exception) {
|
||||
}
|
||||
return o
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,57 @@
|
||||
package com.deniscerri.ytdl.util.extractors.newpipe
|
||||
|
||||
import com.google.common.net.HttpHeaders.USER_AGENT
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody
|
||||
import org.schabi.newpipe.extractor.downloader.Downloader
|
||||
import org.schabi.newpipe.extractor.downloader.Request
|
||||
import org.schabi.newpipe.extractor.downloader.Response
|
||||
import org.schabi.newpipe.extractor.exceptions.ReCaptchaException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
class NewPipeDownloaderImpl(builder: OkHttpClient.Builder) : Downloader() {
|
||||
private var client: OkHttpClient = builder.readTimeout(30, TimeUnit.SECONDS).build()
|
||||
|
||||
override fun execute(request: Request): Response {
|
||||
val httpMethod = request.httpMethod()
|
||||
val url = request.url()
|
||||
val headers = request.headers()
|
||||
val dataToSend = request.dataToSend()
|
||||
|
||||
var requestBody: RequestBody? = null
|
||||
if (dataToSend != null) {
|
||||
requestBody = RequestBody.create(null, dataToSend)
|
||||
}
|
||||
|
||||
val requestBuilder: okhttp3.Request.Builder = okhttp3.Request.Builder()
|
||||
.method(httpMethod, requestBody).url(url)
|
||||
.addHeader("User-Agent", USER_AGENT)
|
||||
|
||||
for ((headerName, headerValueList) in headers) {
|
||||
if (headerValueList.size > 1) {
|
||||
requestBuilder.removeHeader(headerName)
|
||||
for (headerValue in headerValueList) {
|
||||
requestBuilder.addHeader(headerName, headerValue)
|
||||
}
|
||||
} else if (headerValueList.size == 1) {
|
||||
requestBuilder.header(headerName, headerValueList[0])
|
||||
}
|
||||
}
|
||||
|
||||
val response: okhttp3.Response = client.newCall(requestBuilder.build()).execute()
|
||||
|
||||
if (response.code == 429) {
|
||||
response.close()
|
||||
throw ReCaptchaException("reCaptcha Challenge requested", url)
|
||||
}
|
||||
|
||||
val body = response.body
|
||||
val responseBodyToReturn = body.string()
|
||||
val latestUrl = response.request.url.toString()
|
||||
return Response(
|
||||
response.code, response.message, response.headers.toMultimap(),
|
||||
responseBodyToReturn, latestUrl
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,440 @@
|
||||
package com.deniscerri.ytdl.util.extractors.newpipe
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
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 kotlinx.serialization.Serializer
|
||||
import okhttp3.OkHttpClient
|
||||
import org.json.JSONException
|
||||
import org.schabi.newpipe.extractor.NewPipe
|
||||
import org.schabi.newpipe.extractor.Page
|
||||
import org.schabi.newpipe.extractor.ServiceList
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfo
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItem
|
||||
import org.schabi.newpipe.extractor.channel.ChannelInfoItemExtractor
|
||||
import org.schabi.newpipe.extractor.channel.tabs.ChannelTabInfo
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskInfo
|
||||
import org.schabi.newpipe.extractor.kiosk.KioskList
|
||||
import org.schabi.newpipe.extractor.linkhandler.LinkHandler
|
||||
import org.schabi.newpipe.extractor.linkhandler.ListLinkHandler
|
||||
import org.schabi.newpipe.extractor.localization.ContentCountry
|
||||
import org.schabi.newpipe.extractor.localization.Localization
|
||||
import org.schabi.newpipe.extractor.playlist.PlaylistInfo
|
||||
import org.schabi.newpipe.extractor.search.SearchExtractor
|
||||
import org.schabi.newpipe.extractor.search.SearchInfo
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeMusicSearchExtractor
|
||||
import org.schabi.newpipe.extractor.services.youtube.extractors.YoutubeSearchExtractor
|
||||
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfo
|
||||
import org.schabi.newpipe.extractor.stream.StreamInfoItem
|
||||
import org.schabi.newpipe.extractor.utils.ExtractorHelper
|
||||
import java.util.Locale
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
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" }
|
||||
init {
|
||||
NewPipe.init(NewPipeDownloaderImpl(OkHttpClient.Builder()), Localization(language, countryCode))
|
||||
}
|
||||
|
||||
fun getVideoData(url : String) : Result<List<ResultItem>> {
|
||||
try {
|
||||
val streamInfo = StreamInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId), url)
|
||||
val vid = createVideoFromStream(streamInfo, url) ?: return Result.failure(Throwable())
|
||||
return Result.success(listOf(vid))
|
||||
}catch (e: Exception) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFormats(url: String) : Result<List<Format> > {
|
||||
try {
|
||||
val streamInfo = StreamInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId), url)
|
||||
val vid = createVideoFromStream(streamInfo, url)
|
||||
return Result.success(vid!!.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 streamInfo = StreamInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId), url)
|
||||
createVideoFromStream(streamInfo, 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>> {
|
||||
try {
|
||||
val items = arrayListOf<ResultItem>()
|
||||
val res = SearchInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId),
|
||||
NewPipe.getService(ServiceList.YouTube.serviceId)
|
||||
.searchQHFactory
|
||||
.fromQuery(query, listOf(YoutubeSearchQueryHandlerFactory.VIDEOS), ""))
|
||||
|
||||
if (res.relatedItems.isEmpty()) return Result.failure(Throwable())
|
||||
|
||||
for (i in 0 until res.relatedItems.size) {
|
||||
val element = res.relatedItems[i]
|
||||
if (element is StreamInfoItem) {
|
||||
if (element.duration <= 0) continue
|
||||
val v = createVideoFromStreamInfoItem(element, element.url) ?: continue
|
||||
items.add(v)
|
||||
}
|
||||
}
|
||||
return Result.success(items)
|
||||
|
||||
}catch (e: Exception){
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(JSONException::class)
|
||||
fun searchMusic(query: String): Result<ArrayList<ResultItem>> {
|
||||
try {
|
||||
val items = arrayListOf<ResultItem>()
|
||||
val res = SearchInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId),
|
||||
NewPipe.getService(ServiceList.YouTube.serviceId)
|
||||
.searchQHFactory
|
||||
.fromQuery(query, listOf(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS), ""))
|
||||
if (res.relatedItems.isEmpty()) return Result.failure(Throwable())
|
||||
|
||||
for (i in 0 until res.relatedItems.size) {
|
||||
val element = res.relatedItems[i]
|
||||
if (element is StreamInfoItem) {
|
||||
if (element.duration <= 0) continue
|
||||
val v = createVideoFromStreamInfoItem(element, element.url) ?: continue
|
||||
items.add(v)
|
||||
}
|
||||
}
|
||||
return Result.success(items)
|
||||
|
||||
}catch (e: Exception){
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun getStreamingUrlAndChapters(url: String) : Result<Pair<List<String>, List<ChapterItem>?>> {
|
||||
try {
|
||||
val streamInfo = StreamInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId), url)
|
||||
val item = createVideoFromStream(streamInfo, url)
|
||||
if (item!!.urls.isBlank()) return Result.failure(Throwable())
|
||||
val urls = item.urls.split(",")
|
||||
val chapters = item.chapters
|
||||
return Result.success(Pair(urls, chapters))
|
||||
}catch (e: Exception) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun getChannelData(url: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> {
|
||||
try {
|
||||
val req = ChannelInfo.getInfo(ServiceList.YouTube, url)
|
||||
println(Gson().toJson(req))
|
||||
val items = mutableListOf<ResultItem>()
|
||||
for (tab in req.tabs) {
|
||||
if (listOf("videos", "shorts", "livestreams").contains(tab.contentFilters[0])) {
|
||||
val tabInfo = ChannelTabInfo.getInfo(ServiceList.YouTube, tab)
|
||||
val tmp = getChannelTabData(tab, tabInfo, req.name, "${url}/${tabInfo.url.split("/").last()}") {
|
||||
progress(it)
|
||||
}
|
||||
if (tmp.isFailure) continue
|
||||
else items.addAll(tmp.getOrNull()!!)
|
||||
}
|
||||
}
|
||||
return Result.success(items)
|
||||
}catch (e: Exception) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChannelTabData(linkHandler: ListLinkHandler, tabInfo: ChannelTabInfo, channelName: String, playlistURL: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> {
|
||||
try {
|
||||
val totalItems = mutableListOf<ResultItem>()
|
||||
var nextPage : Page? = null
|
||||
var playlistName = ""
|
||||
|
||||
while (true) {
|
||||
val items = mutableListOf<ResultItem>()
|
||||
val req = if (nextPage == null) {
|
||||
if (tabInfo.hasNextPage()) {
|
||||
nextPage = tabInfo.nextPage
|
||||
}
|
||||
playlistName = "$channelName - ${tabInfo.name}"
|
||||
tabInfo.relatedItems.toList()
|
||||
} else {
|
||||
val tmp = ChannelTabInfo.getMoreItems(ServiceList.YouTube, linkHandler, nextPage)
|
||||
nextPage = if (tmp.hasNextPage()) tmp.nextPage else null
|
||||
tmp.items.toList()
|
||||
}
|
||||
|
||||
if (req.isEmpty()) return Result.failure(Throwable())
|
||||
|
||||
for (element in req) {
|
||||
if (element is StreamInfoItem) {
|
||||
if (element.duration <= 0) continue
|
||||
val v = createVideoFromStreamInfoItem(element, element.url) ?: continue
|
||||
v.apply {
|
||||
playlistTitle = playlistName
|
||||
this.playlistURL = playlistURL
|
||||
items.add(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalItems.addAll(items)
|
||||
progress(items)
|
||||
if (nextPage == null || items.isEmpty()) break
|
||||
}
|
||||
|
||||
return Result.success(totalItems)
|
||||
}catch (e: Exception) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun getPlaylistData(playlistURL: String, progress: (pagedResults: MutableList<ResultItem>) -> Unit) : Result<List<ResultItem>> {
|
||||
try {
|
||||
val totalItems = mutableListOf<ResultItem>()
|
||||
var nextPage : Page? = null
|
||||
var playlistName = ""
|
||||
|
||||
while (true) {
|
||||
val items = mutableListOf<ResultItem>()
|
||||
val req = if (nextPage == null) {
|
||||
val tmp = PlaylistInfo.getInfo(ServiceList.YouTube, playlistURL)
|
||||
if (tmp.hasNextPage()) {
|
||||
nextPage = tmp.nextPage
|
||||
}
|
||||
playlistName = tmp.name
|
||||
tmp.relatedItems.toList()
|
||||
} else {
|
||||
val tmp = PlaylistInfo.getMoreItems(ServiceList.YouTube, playlistURL, nextPage)
|
||||
nextPage = if (tmp.hasNextPage()) tmp.nextPage else null
|
||||
tmp.items.toList()
|
||||
}
|
||||
|
||||
if (req.isEmpty()) return Result.failure(Throwable())
|
||||
|
||||
for (element in req) {
|
||||
if (element is StreamInfoItem) {
|
||||
if (element.duration <= 0) continue
|
||||
val v = createVideoFromStreamInfoItem(element, element.url) ?: continue
|
||||
v.apply {
|
||||
playlistTitle = playlistName
|
||||
this.playlistURL = playlistURL
|
||||
items.add(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
totalItems.addAll(items)
|
||||
progress(items)
|
||||
if (nextPage == null || items.isEmpty()) break
|
||||
}
|
||||
|
||||
return Result.success(totalItems)
|
||||
}catch (e: Exception) {
|
||||
return Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getTrending(): ArrayList<ResultItem> {
|
||||
try {
|
||||
val items = arrayListOf<ResultItem>()
|
||||
val info = KioskInfo.getInfo(NewPipe.getService(ServiceList.YouTube.serviceId), "https://www.youtube.com/feed/trending")
|
||||
if (info.relatedItems.isEmpty()) return arrayListOf()
|
||||
|
||||
for (i in 0 until info.relatedItems.size) {
|
||||
val element = info.relatedItems[i]
|
||||
if (element is StreamInfoItem) {
|
||||
if (element.duration <= 0) continue
|
||||
val v = createVideoFromStreamInfoItem(element, element.url) ?: continue
|
||||
items.add(v)
|
||||
}
|
||||
}
|
||||
|
||||
return items
|
||||
}catch (err: Exception) {
|
||||
return arrayListOf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun createVideoFromStreamInfoItem(stream: StreamInfoItem, url: String) : ResultItem? {
|
||||
var video: ResultItem? = null
|
||||
try {
|
||||
val id = getIDFromYoutubeURL(url)
|
||||
val title = stream.name
|
||||
val author = stream.uploaderName.removeSuffix(" - Topic")
|
||||
val duration = stream.duration.toInt().toStringDuration(Locale.US)
|
||||
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
|
||||
|
||||
video = ResultItem(0,
|
||||
url,
|
||||
title,
|
||||
author,
|
||||
duration,
|
||||
thumb,
|
||||
"youtube",
|
||||
"",
|
||||
ArrayList(),
|
||||
"",
|
||||
ArrayList()
|
||||
)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e("NewPipeUtil", e.toString())
|
||||
}
|
||||
return video
|
||||
}
|
||||
|
||||
private fun createVideoFromStream(stream: StreamInfo, url: String, ignoreFormatPreference : Boolean = false): ResultItem? {
|
||||
var video: ResultItem? = null
|
||||
try {
|
||||
val id = getIDFromYoutubeURL(url)
|
||||
val title = stream.name
|
||||
val author = stream.uploaderName.removeSuffix(" - Topic")
|
||||
val duration = stream.duration.toInt().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 (stream.audioStreams.isNotEmpty()){
|
||||
for (f in 0 until stream.audioStreams.size){
|
||||
val it = stream.audioStreams[f]
|
||||
if (it.bitrate == 0) continue
|
||||
|
||||
val formatObj = Format(
|
||||
format_id = it.itag.toString(),
|
||||
container = it.format!!.name,
|
||||
acodec = it.codec,
|
||||
filesize = it.itagItem!!.contentLength,
|
||||
format_note = (it.audioTrackName ?: (it.itagItem?.getResolutionString() ?: ((it.bitrate / 1000).toString() + "k"))) + " Audio",
|
||||
lang = it.audioLocale?.language,
|
||||
asr = it.itagItem!!.sampleRate.toString(),
|
||||
url = it.content,
|
||||
tbr = (it.bitrate / 1000).toString() + "k"
|
||||
)
|
||||
|
||||
formats.add(formatObj)
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.videoStreams.isNotEmpty()){
|
||||
for (f in 0 until stream.videoStreams.size){
|
||||
val it = stream.videoStreams[f]
|
||||
if (it.bitrate == 0) continue
|
||||
|
||||
val formatObj = Format(
|
||||
format_id = it.itag.toString(),
|
||||
container = it.format!!.name,
|
||||
vcodec = it.codec,
|
||||
format_note = it.itagItem!!.getResolutionString() ?: it.quality,
|
||||
filesize = it.itagItem!!.contentLength,
|
||||
url = it.content,
|
||||
tbr = (it.bitrate / 1000).toString() + "k"
|
||||
)
|
||||
formats.add(formatObj)
|
||||
}
|
||||
}
|
||||
|
||||
if (stream.videoOnlyStreams.isNotEmpty()){
|
||||
for (f in 0 until stream.videoOnlyStreams.size){
|
||||
val it = stream.videoOnlyStreams[f]
|
||||
if (it.bitrate == 0) continue
|
||||
|
||||
val formatObj = Format(
|
||||
format_id = it.itag.toString(),
|
||||
container = it.format!!.name,
|
||||
vcodec = it.codec,
|
||||
format_note = it.itagItem!!.getResolutionString() ?: it.quality,
|
||||
filesize = it.itagItem!!.contentLength,
|
||||
url = it.content,
|
||||
tbr = (it.bitrate / 1000).toString() + "k"
|
||||
)
|
||||
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 (stream.streamSegments.isNotEmpty()){
|
||||
for (c in 0 until stream.streamSegments.size){
|
||||
val chapter = stream.streamSegments[c]
|
||||
val end = if (c == stream.streamSegments.size - 1) stream.duration.toInt() else stream.streamSegments[c+1].startTimeSeconds
|
||||
val item = ChapterItem(chapter.startTimeSeconds.toLong(), end.toLong(), chapter.title)
|
||||
chapters.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
video = ResultItem(0,
|
||||
url,
|
||||
title,
|
||||
author,
|
||||
duration,
|
||||
thumb,
|
||||
"youtube",
|
||||
"",
|
||||
formats,
|
||||
if (stream.hlsUrl.isNotBlank() && stream.hlsUrl != "null") stream.hlsUrl else "",
|
||||
chapters
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Log.e("NewPipeUtil", 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!!
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
# What's Changed
|
||||
|
||||
## Newpipe Extractor
|
||||
|
||||
Since Piped is currently broken and unusable, i implemented the NEWPIPE EXTRACTOR to replace it. This includes:
|
||||
- video queries
|
||||
- playlist queries
|
||||
- trending
|
||||
- searching
|
||||
- youtube channel queries
|
||||
- formats
|
||||
The speed is as fast as piped and seems to be working. Will use that for the forseeable future, until further notice
|
||||
|
||||
## Home screen filtering
|
||||
|
||||
This is a feature request that is long overdue. When implementing newpipe extractor i also coded in proper youtube channel parsing. Now items have their respective channel playlist name. So the app will show you filter chips if you have multiple playlist available.
|
||||
So you can filter between videos, shorts, livestreams and choose to just download those by long pressing etc etc.
|
||||
In case newpipe fails and it defaults to yt-dlp, proper channel playlist parsing is not yet possible.
|
||||
You can track the issue i have created here:
|
||||
https://github.com/yt-dlp/yt-dlp/issues/10827
|
||||
|
||||
## Other fixes
|
||||
|
||||
- Fixed app selecting all playlist items even though user selected a few, if the user is parsing an instagram post. (they all had the same url)
|
||||
- Fixed app not changing container label in the multiple download card when switching download type
|
||||
- Fixed app not being able to return to home after clicking the download notification
|
||||
- Added ability for the app to parse multiple urls at the same time. For now it will do 10 items at max
|
||||
- Added ability to show "Continue Anyway" in the error dialog so it will keep showing the download card in case you want to save it for later or schedule it
|
||||
- Added general subtitle variant codes with .*
|
||||
- Fixed app not applying the subtitle language label in the subtitle configuration dialog
|
||||
- Fixed app not showing the total format size when being in audio download type in the multiple downlod card
|
||||
- Fixed app not navigating to the download queue fragment to adjust the errored download? Couldnt reprod but reworked logic lmk
|
||||
- Fixed app still using original title for metadata even though the user changed it in the textfield
|
||||
- Slight fixes for artist parsing for ytm videos
|
||||
- Improved and simplified uploader parsing when quick downloading
|
||||
- Ignored the recode command when using avi container, because it doesnt support it
|
||||
- Prevented app selecting bad formats 233 and 234 when quick downloading
|
||||
- Fixed app not showing best quality and worst quality labels depending on the user language
|
||||
- Fixed app crashing when selecting formats of an instagram post with the same url
|
||||
|
||||
## Note
|
||||
|
||||
Some people are asking for the ability to not use cache while downloading in usb storage. This is an android limitation. Even though you might have all files access, android still doesnt consider usb storage as main storage and cant directly write to it. So i have to use caching and then transfer the file.
|
||||
If anyone is knowledgeable to pull this off, dm me on telegram and we can make it happen. :)
|
||||
|
||||
I could've released this sooner but i have been busy :/
|
||||
Loading…
Reference in New Issue