ytdlnis/app/src/main/java/com/deniscerri/ytdl/util/InfoUtil.kt
2024-07-13 15:37:36 +02:00

1651 lines
No EOL
69 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.deniscerri.ytdl.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.SharedPreferences
import android.content.res.Resources
import android.os.Handler
import android.os.Looper
import android.text.Html
import android.util.Log
import android.util.Patterns
import android.widget.Toast
import androidx.preference.PreferenceManager
import com.afollestad.materialdialogs.utils.MDUtil.getStringArray
import com.anggrayudi.storage.extension.count
import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.ChapterItem
import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.ResultItem
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.util.Extensions.getIntByAny
import com.deniscerri.ytdl.util.Extensions.getStringByAny
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.Extensions.toStringDuration
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
import java.lang.reflect.Type
import java.net.HttpURLConnection
import java.net.URL
import java.util.*
import java.util.regex.Pattern
class InfoUtil(private val context: Context) {
private lateinit var sharedPreferences: SharedPreferences
private lateinit var handler: Handler
init {
try {
handler = Handler(Looper.getMainLooper())
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
countryCODE = sharedPreferences.getString("locale", "")!!
if (countryCODE.isEmpty()) countryCODE = "US"
} catch (e: Exception) {
e.printStackTrace()
}
}
fun search(query: String): ArrayList<ResultItem> {
return runCatching {
when(sharedPreferences.getString("search_engine", "ytsearch")){
"ytsearch" -> searchFromPiped(query)
"ytsearchmusic" -> searchFromPipedMusic(query)
else -> throw Exception()
}
}.getOrElse {
getFromYTDL(query)
}
}
@Throws(JSONException::class)
fun searchFromPiped(query: String): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val data = genericRequest("$pipedURL/search?q=$query&filter=videos&region=${countryCODE}")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return getFromYTDL(query)
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 items
}
@Throws(JSONException::class)
fun searchFromPipedMusic(query: String): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val data = genericRequest("$pipedURL/search?q=$query=&filter=music_songs&region=${countryCODE}")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return getFromYTDL(query)
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 items
}
@Throws(JSONException::class)
fun getPlaylist(id: String, nextPageToken: String, playlistName: String): PlaylistTuple {
try{
val items = arrayListOf<ResultItem>()
// -------------- PIPED API FUNCTION -------------------
var url = ""
url = if (nextPageToken.isBlank()) "$pipedURL/playlists/$id"
else """$pipedURL/nextpage/playlists/$id?nextpage=${nextPageToken.replace("&prettyPrint", "%26prettyPrint")}"""
val res = genericRequest(url)
if (!res.has("relatedStreams")) throw Exception()
val dataArray = res.getJSONArray("relatedStreams")
var nextpage = res.getString("nextpage")
for (i in 0 until dataArray.length()){
kotlin.runCatching {
val obj = dataArray.getJSONObject(i)
createVideoFromPipedJSON(obj, "https://youtube.com" + obj.getString("url"))?.apply {
playlistTitle = runCatching { res.getString("name") }.getOrElse { playlistName }
playlistURL = "https://www.youtube.com/playlist?list=$id"
items.add(this)
}
}
}
if (nextpage == "null") nextpage = ""
return PlaylistTuple(nextpage, items)
}catch (e: Exception){
Log.e("PLST", e.toString())
return PlaylistTuple(
"",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
)
}
}
@Throws(JSONException::class)
fun getYoutubeVideo(url: String): List<ResultItem>? {
val theURL = url.replace("\\?list.*".toRegex(), "")
try{
val id = getIDFromYoutubeURL(theURL)
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
return getFromYTDL(theURL)
}
createVideoFromPipedJSON(res, theURL)?.apply {
return listOf(this)
}
}catch (e: Exception){
val v = getFromYTDL(theURL)
v.forEach { it.url = theURL }
return v
}
return null
}
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(TAG, e.toString())
}
return video
}
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()
}
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(TAG, e.toString())
}
return video
}
private fun genericRequest(url: String): JSONObject {
Log.e(TAG, 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(TAG, e.toString())
}
return json
}
private fun genericArrayRequest(url: String): JSONArray {
Log.e(TAG, 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(TAG, e.toString())
}
return json
}
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
}
@Throws(JSONException::class)
fun getTrending(): ArrayList<ResultItem> {
if (sharedPreferences.getString("api_key", "")!!.isNotBlank()){
return getTrendingFromYoutubeAPI()
}
return getTrendingFromPiped()
}
@Throws(JSONException::class)
fun getTrendingFromYoutubeAPI(): 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&regionCode=$countryCODE&maxResults=25&key=$key"
//short data
val res = genericRequest(url)
//extra data from the same videos
val contentDetails =
genericRequest("https://www.googleapis.com/youtube/v3/videos?part=contentDetails&chart=mostPopular&videoCategoryId=10&regionCode=$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 getTrendingFromPiped(): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val url = "$pipedURL/trending?region=$countryCODE"
val res = 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 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!!
}
fun getFormats(url: String, source : String? = null) : List<Format> {
val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
if (url.isYoutubeURL() && formatSource == "piped"){
try {
val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
return if (source != null) {
listOf()
}else {
getFormatsFromYTDL(url)
}
}else {
val item = createVideoFromPipedJSON(res, "https://youtube.com/watch?v=$id", true)
return item!!.formats
}
}catch(e: Exception) {
println(e)
if (e is CancellationException) throw e
return getFormatsFromYTDL(url)
}
}else{
return getFormatsFromYTDL(url)
}
}
private fun getFormatsFromYTDL(url: String) : List<Format> {
val request = YoutubeDLRequest(url)
request.addOption("--print", "%(formats)s")
request.addOption("--print", "%(duration)s")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--compat-options", "manifest-filesize-approx")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()) {
request.addOption("--proxy", proxy)
}
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
val res = YoutubeDL.getInstance().execute(request)
val results: Array<String?> = try {
res.out.split(System.lineSeparator()).toTypedArray()
} catch (e: Exception) {
arrayOf(res.out)
}
val json = results[0]
val jsonArray = kotlin.runCatching { JSONArray(json) }.getOrElse { JSONArray() }
return parseYTDLFormats(jsonArray)
}
data class MultipleFormatProgress(
val url: String,
val formats: List<Format>,
val unavailable : Boolean = false,
val unavailableMessage: String = ""
)
suspend fun getFormatsMultiple(urls: List<String>, source: String? = null, progress: (progress: MultipleFormatProgress) -> Unit) : MutableList<MutableList<Format>> {
val formatCollection = mutableListOf<MutableList<Format>>()
val formatSource = source ?: sharedPreferences.getString("formats_source", "yt-dlp")
val allYoutubeLinks = urls.all { it.isYoutubeURL() }
if (allYoutubeLinks && formatSource == "piped") {
urls.forEach { url ->
val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
createVideoFromPipedJSON(res, url).apply {
formatCollection.add(this!!.formats)
progress(
MultipleFormatProgress(url, this.formats)
)
}
}
}
else {
val urlsFile = File(context.cacheDir, "urls.txt")
urlsFile.delete()
withContext(Dispatchers.IO) {
urlsFile.createNewFile()
}
urls.forEach {
urlsFile.appendText(it+"\n")
}
try {
val request = YoutubeDLRequest(emptyList())
request.addOption("--print", "formats")
request.addOption("-a", urlsFile.absolutePath)
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
val txt = parseYTDLRequestString(request)
println(txt)
var urlIdx = 0
YoutubeDL.getInstance().execute(request){ progress, _, line ->
try{
if (line.isNotBlank()){
val url = urls[urlIdx]
println(line)
println(url)
if (line.contains("unavailable")) {
progress(MultipleFormatProgress(url, listOf(), true, line))
}else{
val formatsJSON = JSONArray(line)
val formats = parseYTDLFormats(formatsJSON)
formatCollection.add(formats)
progress(MultipleFormatProgress(url, formats))
}
}
}catch (e: Exception){
Log.e("GET MULTIPLE FORMATS", e.toString())
}
urlIdx++
}
} catch (e: Exception) {
e.message?.split(System.lineSeparator())?.apply {
this.forEach { line ->
println(line)
if (line.contains("unavailable")) {
kotlin.runCatching {
val id = Regex("""\[.*?\] (\w+):""").find(line)!!.groupValues[1]
val url = urls.first { it.contains(id) }
progress(MultipleFormatProgress(url, listOf(), true, line))
delay(500)
}
}
}
}
handler.post {
Toast.makeText(context, e.message, Toast.LENGTH_LONG).show()
}
e.printStackTrace()
}
urlsFile.delete()
}
return formatCollection
}
@SuppressLint("RestrictedApi")
fun getFromYTDL(query: String, singleItem: Boolean = false): ArrayList<ResultItem> {
val items = arrayListOf<ResultItem>()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
val request : YoutubeDLRequest
if (query.contains("http")){
request = YoutubeDLRequest(query)
}else{
request = YoutubeDLRequest(emptyList())
when (searchEngine){
"ytsearchmusic" -> {
request.addOption("--default-search", "https://music.youtube.com/search?q=")
request.addOption("ytsearch25:\"${query}\"")
}
else -> {
request.addOption("${searchEngine}25:\"${query}\"")
}
}
}
val lang = sharedPreferences.getString("app_language", "en")
if (searchEngine == "ytsearch" && context.getStringArray(R.array.subtitle_langs).contains(lang)){
request.addOption("--extractor-args", "youtube:lang=$lang")
}
request.addOption("--flat-playlist")
request.addOption(if (singleItem) "-J" else "-j")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--compat-options", "manifest-filesize-approx")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val results: List<String?> = try {
val lineSeparator = System.getProperty("line.separator")
youtubeDLResponse.out.split(lineSeparator!!)
} catch (e: Exception) {
listOf(youtubeDLResponse.out)
}.filter { it.isNotBlank() }.apply {
if (this.isEmpty()) throw Exception("Command Used: \n yt-dlp ${parseYTDLRequestString(request)}")
}
for (result in results) {
if (result.isNullOrBlank()) continue
val jsonObject = JSONObject(result)
val title = jsonObject.getStringByAny("alt_title", "title", "webpage_url_basename")
if (title == "[Private video]" || title == "[Deleted video]") continue
var author = jsonObject.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
var duration = jsonObject.getIntByAny("duration").toString()
if (duration != "-1"){
duration = jsonObject.getInt("duration").toStringDuration(Locale.US)
}
var thumb: String? = ""
if (jsonObject.has("thumbnail")) {
thumb = jsonObject.getString("thumbnail")
} else if (jsonObject.has("thumbnails")) {
val thumbs = jsonObject.getJSONArray("thumbnails")
if (thumbs.length() > 0){
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
}
}
if(author.isEmpty()){
runCatching {
val firstEntry = jsonObject.getJSONArray("entries").getJSONObject(0)
author = firstEntry.getStringByAny("uploader", "channel", "playlist_uploader", "uploader_id")
val thumbs = firstEntry.getJSONArray("thumbnails")
if (thumbs.length() > 0){
thumb = thumbs.getJSONObject(thumbs.length() - 1).getString("url")
}
}
}
var website = jsonObject.getStringByAny("extractor_key", "extractor","ie_key")
if (website == "Generic" || website == "HTML5MediaEmbed") website = jsonObject.getStringByAny("webpage_url_domain")
var playlistTitle = jsonObject.getStringByAny("playlist_title")
var playlistURL: String? = ""
var playlistIndex: Int? = null
if(playlistTitle.removeSurrounding("\"") == query) playlistTitle = ""
if (playlistTitle.isNotBlank()){
playlistURL = query
kotlin.runCatching { playlistIndex = jsonObject.getInt("playlist_index") }
}
val formatsInJSON = if (jsonObject.has("formats") && jsonObject.get("formats") is JSONArray) jsonObject.getJSONArray("formats") else null
val formats : ArrayList<Format> = parseYTDLFormats(formatsInJSON)
val chaptersInJSON = if (jsonObject.has("chapters") && jsonObject.get("chapters") is JSONArray) jsonObject.getJSONArray("chapters") else null
val listType: Type = object : TypeToken<List<ChapterItem>>() {}.type
var chapters : ArrayList<ChapterItem> = arrayListOf()
if (chaptersInJSON != null){
chapters = Gson().fromJson(chaptersInJSON.toString(), listType)
}
var urls = "";
if(jsonObject.has("requested_formats")) {
val requestedFormats = jsonObject.getJSONArray("requested_formats")
val urlList = mutableListOf<String>()
val length = requestedFormats.length()-1
for (i in length downTo 0) {
urlList.add(requestedFormats.getJSONObject(i).getString("url"))
}
urls = urlList.joinToString("\n")
}
val url = if (jsonObject.has("url") && results.size > 1){
jsonObject.getString("url")
}else{
if (Patterns.WEB_URL.matcher(query).matches() && playlistURL?.isEmpty() == true){
query
}else{
jsonObject.getStringByAny("webpage_url", "original_url", "url", )
jsonObject.getString("webpage_url")
}
}
val type = jsonObject.getStringByAny("_type")
if (type == "playlist" && playlistTitle.isEmpty()) {
playlistTitle = title
}
val res = ResultItem(0,
url,
title,
author,
duration,
thumb!!,
website,
playlistTitle,
formats,
urls,
chapters,
playlistURL,
playlistIndex
)
items.add(res)
}
return items
}
private fun parseYTDLFormats(formatsInJSON: JSONArray?) : ArrayList<Format> {
val formats = arrayListOf<Format>()
if (formatsInJSON != null) {
for (f in formatsInJSON.length() - 1 downTo 0){
val format = formatsInJSON.getJSONObject(f)
kotlin.runCatching {
if (format.get("filesize").toString() == "None") {
format.remove("filesize")
}
}
kotlin.runCatching {
if (format.get("filesize_approx").toString() == "None") {
format.remove("filesize_approx")
}
}
kotlin.runCatching {
if (!format.has("filesize")) {
format.put("filesize", 0)
}
}
kotlin.runCatching {
if(format.get("format_note").toString() == "null"){
format.remove("format_note")
}
}
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if (formatProper.format_note == null) continue
if (format.has("format_note")){
if (!formatProper!!.format_note.contains("audio only", true)) {
formatProper.format_note = format.getString("format_note")
}else{
if (!formatProper.format_note.endsWith("audio", true)){
formatProper.format_note = format.getString("format_note").uppercase().removeSuffix("AUDIO") + " AUDIO"
}
}
}
if (formatProper.format_note == "storyboard") continue
formatProper.container = format.getString("ext")
if (formatProper.tbr == "None") formatProper.tbr = ""
if (!formatProper.tbr.isNullOrBlank()){
formatProper.tbr = formatProper.tbr + "k"
}
if(formatProper.vcodec.isNullOrEmpty() || formatProper.vcodec == "null"){
if(formatProper.acodec.isNullOrEmpty() || formatProper.acodec == "null"){
formatProper.vcodec = format.getStringByAny("video_ext", "ext").ifEmpty { "unknown" }
}
}
formats.add(formatProper)
}
}
return formats
}
fun getSearchSuggestions(query: String): ArrayList<String> {
val url = "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&q=$query"
val res = genericArrayRequest(url)
Log.e("aa", res.toString())
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
}
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
}
fun getStreamingUrlAndChapters(url: String) : Pair<List<String>, List<ChapterItem>?> {
try {
if (url.isYoutubeURL()) {
val id = getIDFromYoutubeURL(url)
val res = genericRequest("$pipedURL/streams/$id")
if (res.length() == 0) {
throw Exception()
}else{
val item = createVideoFromPipedJSON(res, url)
if (item!!.urls.isBlank()) throw Exception()
val urls = item.urls.split(",")
val chapters = item.chapters
return Pair(urls, chapters)
}
}else{
throw Exception()
}
}catch (e: Exception) {
try {
val request = YoutubeDLRequest(url)
request.addOption("--get-url")
request.addOption("--print", "%(.{urls,chapters})s")
request.addOption("--skip-download")
request.addOption("-R", "1")
request.addOption("--socket-timeout", "5")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
val useHeader = sharedPreferences.getBoolean("use_header", false)
val header = sharedPreferences.getString("useragent_header", "")
if (useHeader && !header.isNullOrBlank()){
request.addOption("--add-header","User-Agent:${header}")
}
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
request.addOption("-P", FileUtil.getCachePath(context) + "/tmp")
val youtubeDLResponse = YoutubeDL.getInstance().execute(request)
val json = JSONObject(youtubeDLResponse.out)
val urls = if (json.has("urls")) {
json.getString("urls").split("\n")
}else{
listOf()
}
val chapters = if (json.has("chapters")) {
val arr = json.getJSONArray("chapters")
val list = mutableListOf<ChapterItem>()
for (i in 0 until arr.length()) {
list.add(
Gson().fromJson(arr.getJSONObject(i).toString(), ChapterItem::class.java)
)
}
list
}else{
listOf()
}
return Pair(urls, chapters)
} catch (e: Exception) {
return Pair(listOf(), listOf())
}
}
}
@SuppressLint("RestrictedApi")
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest {
val useItemURL = sharedPreferences.getBoolean("use_itemurl_instead_playlisturl", false)
val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank() || useItemURL){
YoutubeDLRequest(downloadItem.url)
}else{
YoutubeDLRequest(downloadItem.playlistURL!!).apply {
if(downloadItem.playlistIndex == null){
val matchPortion = downloadItem.url.split("/").last().split("=").last().split("&").first()
addOption("--match-filter", "id~='${matchPortion}'")
}else{
addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}")
}
}
}
if (downloadItem.playlistIndex != null && useItemURL) {
request.addOption("--parse-metadata", " ${downloadItem.playlistIndex}: %(playlist_index)s")
}
val type = downloadItem.type
val downDir : File
val canWrite = File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite() || sharedPreferences.getBoolean("access_all_files", false)
if (!sharedPreferences.getBoolean("cache_downloads", true) && canWrite){
downDir = File(FileUtil.formatPath(downloadItem.downloadPath))
request.addOption("--no-quiet")
request.addOption("--no-simulate")
request.addOption("--print", "after_move:'%(filepath,_filename)s'")
}else{
val cacheDir = FileUtil.getCachePath(context)
downDir = File(cacheDir, downloadItem.id.toString())
downDir.delete()
downDir.mkdirs()
}
val aria2 = sharedPreferences.getBoolean("aria2", false)
if (aria2) {
request.addOption("--downloader", "libaria2c.so")
request.addOption("--external-downloader-args", "aria2c:\"--summary-interval=1\"")
}
val concurrentFragments = sharedPreferences.getInt("concurrent_fragments", 1)
if (concurrentFragments > 1) request.addOption("-N", concurrentFragments)
val retries = sharedPreferences.getString("retries", "")!!
val fragmentRetries = sharedPreferences.getString("fragment_retries", "")!!
if(retries.isNotEmpty()) request.addOption("--retries", retries)
if(fragmentRetries.isNotEmpty()) request.addOption("--fragment-retries", fragmentRetries)
val limitRate = sharedPreferences.getString("limit_rate", "")!!
if (limitRate.isNotBlank()) request.addOption("-r", limitRate)
val bufferSize = sharedPreferences.getString("buffer_size", "")!!
if (bufferSize.isNotBlank()){
request.addOption("--buffer-size", bufferSize)
request.addOption("--no-resize-buffer")
}
val sponsorblockURL = sharedPreferences.getString("sponsorblock_url", "")!!
if (sponsorblockURL.isNotBlank()) request.addOption("--sponsorblock-api", sponsorblockURL)
if (sharedPreferences.getBoolean("restrict_filenames", true)) request.addOption("--restrict-filenames")
if (sharedPreferences.getBoolean("force_ipv4", false)){
request.addOption("-4")
}
if (sharedPreferences.getBoolean("use_cookies", false)){
FileUtil.getCookieFile(context){
request.addOption("--cookies", it)
}
}
val proxy = sharedPreferences.getString("proxy", "")
if (proxy!!.isNotBlank()){
request.addOption("--proxy", proxy)
}
val keepCache = sharedPreferences.getBoolean("keep_cache", false)
if(keepCache){
request.addOption("--keep-fragments")
}
val embedMetadata = sharedPreferences.getBoolean("embed_metadata", true)
val thumbnailFormat = sharedPreferences.getString("thumbnail_format", "jpg")
var filenameTemplate = downloadItem.customFileNameTemplate
if(downloadItem.type != DownloadViewModel.Type.command){
if (sharedPreferences.getBoolean("no_part", false)){
request.addOption("--no-part")
}
request.addOption("--trim-filenames", 254 - downDir.absolutePath.length)
if (downloadItem.SaveThumb) {
request.addOption("--write-thumbnail")
request.addOption("--convert-thumbnails", thumbnailFormat!!)
}
if (!sharedPreferences.getBoolean("mtime", false)){
request.addOption("--no-mtime")
}
val sponsorBlockFilters : ArrayList<String> = when(downloadItem.type) {
DownloadViewModel.Type.audio -> {
downloadItem.audioPreferences.sponsorBlockFilters
}
//video
else -> {
downloadItem.videoPreferences.sponsorBlockFilters
}
}
if (sharedPreferences.getBoolean("use_sponsorblock", true)){
if (sponsorBlockFilters.isNotEmpty()) {
val filters = java.lang.String.join(",", sponsorBlockFilters.filter { it.isNotBlank() })
if (filters.isNotBlank()) {
request.addOption("--sponsorblock-remove", filters)
if (sharedPreferences.getBoolean("force_keyframes", false)){
request.addOption("--force-keyframes-at-cuts")
}
}
}
}
if(downloadItem.title.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "video:title", ".+", downloadItem.title.take(180)))
}
if (downloadItem.author.isNotBlank()){
request.addCommands(listOf("--replace-in-metadata", "video:uploader", ".+", downloadItem.author.take(30)))
}
request.addOption("--parse-metadata", "uploader:^(?P<uploader>.*?)(?:(?= - Topic)|$)")
if (embedMetadata){
request.addOption("--parse-metadata", "%(uploader,channel,creator,artist|null)s:%(uploader)s")
}
if (downloadItem.downloadSections.isNotBlank()){
downloadItem.downloadSections.split(";").forEach {
if (it.isBlank()) return@forEach
request.addOption("--download-sections", "*${it.split(" ")[0]}")
if (sharedPreferences.getBoolean("force_keyframes", false) && !request.hasOption("--force-keyframes-at-cuts")){
request.addOption("--force-keyframes-at-cuts")
}
}
filenameTemplate = if (filenameTemplate.isBlank()){
"%(section_title&{} |)s%(title).170B"
}else{
"%(section_title&{} |)s $filenameTemplate"
}
if (downloadItem.downloadSections.split(";").size > 1){
filenameTemplate = "%(autonumber)d. $filenameTemplate [%(section_start>%H%M%S)s]"
}
}
if (sharedPreferences.getBoolean("use_audio_quality", false)){
request.addOption("--audio-quality", sharedPreferences.getInt("audio_quality", 0))
}
if (sharedPreferences.getBoolean("write_description", false)){
request.addOption("--write-description")
}
}
if (sharedPreferences.getString("prevent_duplicate_downloads", "")!! == "download_archive"){
request.addOption("--download-archive", FileUtil.getDownloadArchivePath(context))
}
val preferredAudioCodec = sharedPreferences.getString("audio_codec", "")!!
val aCodecPrefIndex = context.getStringArray(R.array.audio_codec_values).indexOf(preferredAudioCodec)
val aCodecPref = runCatching { context.getStringArray(R.array.audio_codec_values_ytdlp)[aCodecPrefIndex] }.getOrElse { "" }
when(type){
DownloadViewModel.Type.audio -> {
val supportedContainers = context.resources.getStringArray(R.array.audio_containers)
var abrSort = ""
var audioQualityId : String = downloadItem.format.format_id
if (audioQualityId.isBlank() || listOf("0", context.getString(R.string.best_quality), "ba", "best", "").contains(audioQualityId)){
audioQualityId = "ba/b"
}else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){
audioQualityId = "wa/w"
}else if(audioQualityId.contains("kbps_ytdlnisgeneric")){
abrSort = audioQualityId.split("kbps")[0]
audioQualityId = ""
}else{
audioQualityId += "/ba/b"
}
val ext = downloadItem.container
if (audioQualityId.isNotBlank()) {
if(audioQualityId.contains("-")){
audioQualityId = if(!downloadItem.format.lang.isNullOrBlank() && downloadItem.format.lang != "None"){
"ba[format_id~='^(${audioQualityId.split("-")[0]})'][language^=${downloadItem.format.lang}]/ba/b"
}else{
"$audioQualityId/${audioQualityId.split("-")[0]}"
}
}
request.addOption("-f", audioQualityId)
}else{
//enters here if generic or quick downloaded with ba format
val preferredLanguage = sharedPreferences.getString("audio_language","")!!
if (preferredLanguage.isNotBlank()){
request.addOption("-f", "ba[language^=$preferredLanguage]/ba/b")
}
}
request.addOption("-x")
val formatSorting = StringBuilder("hasaud")
if (aCodecPref.isNotBlank()){
formatSorting.append(",acodec:$aCodecPref")
}
if (abrSort.isNotBlank()){
formatSorting.append(",abr~${abrSort}")
}
if(ext.isNotBlank()){
if(!ext.matches("(webm)|(Default)|(${context.getString(R.string.defaultValue)})".toRegex()) && supportedContainers.contains(ext)){
request.addOption("--audio-format", ext)
formatSorting.append(",aext:$ext")
}
}
request.addOption("-P", downDir.absolutePath)
request.addOption("-S", formatSorting.toString())
if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
request.addOption("-o", "chapter:%(section_title)s.%(ext)s")
}else{
if (embedMetadata){
request.addOption("--embed-metadata")
request.addOption("--parse-metadata", "%(artist,uploader)s:^(?P<meta_album_artist>[^,]*)")
request.addOption("--parse-metadata", "%(album_artist,meta_album_artist|)s:%(album_artist)s")
request.addOption("--parse-metadata", "description:(?:Released on: )(?P<dscrptn_year>\\d{4})")
request.addOption("--parse-metadata", "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:%(meta_date)s")
if (downloadItem.playlistTitle.isNotEmpty()) {
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
request.addOption("--parse-metadata", "%(track_number,playlist_index)d:%(track_number)s")
} else {
request.addOption("--parse-metadata", "%(album,title)s:%(meta_album)s")
}
}
val cropThumb = downloadItem.audioPreferences.cropThumb ?: sharedPreferences.getBoolean("crop_thumbnail", true)
if (downloadItem.audioPreferences.embedThumb){
request.addOption("--embed-thumbnail")
if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!)
val thumbnailConfig = StringBuilder("")
val cropConfig = """-vf crop=\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\"""""
if (thumbnailFormat == "jpg") thumbnailConfig.append("""--ppa "ThumbnailsConvertor:-qmin 1 -q:v 1"""")
if (cropThumb){
if (thumbnailFormat == "jpg") {
thumbnailConfig.deleteCharAt(thumbnailConfig.length - 1)
thumbnailConfig.append(""" $cropConfig""")
}
else thumbnailConfig.append("""--ppa "ThumbnailsConvertor:$cropConfig""")
}
if (thumbnailConfig.isNotBlank()){
runCatching {
val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.id + "##ffmpegCrop.txt")
config.writeText(thumbnailConfig.toString())
request.addOption("--config", config.absolutePath)
}
}
}
if (filenameTemplate.isNotBlank()){
request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
}
}
}
DownloadViewModel.Type.video -> {
val supportedContainers = context.resources.getStringArray(R.array.video_containers)
if (downloadItem.videoPreferences.addChapters) {
if (sharedPreferences.getBoolean("use_sponsorblock", true)){
request.addOption("--sponsorblock-mark", "all")
}
request.addOption("--embed-chapters")
}
var cont = ""
val outputContainer = downloadItem.container
if(
outputContainer.isNotEmpty() &&
outputContainer != "Default" &&
outputContainer != context.getString(R.string.defaultValue) &&
supportedContainers.contains(outputContainer)
){
cont = outputContainer
request.addOption("--merge-output-format", outputContainer.lowercase())
if (outputContainer != "webm") {
val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false)
if (embedThumb) {
request.addOption("--embed-thumbnail")
if (!request.hasOption("--convert-thumbnails")) request.addOption("--convert-thumbnails", thumbnailFormat!!)
}
}
}
var acont = sharedPreferences.getString("audio_format", "")!!
if (acont == "Default") acont = ""
//format logic
var videoF = downloadItem.format.format_id
var altAudioF = ""
var audioF = downloadItem.videoPreferences.audioFormatIDs.map { f ->
val format = downloadItem.allFormats.find { it.format_id == f }
format?.run {
if (this.format_id.contains("-")) {
if (!this.lang.isNullOrBlank() && this.lang != "None") {
"ba[format_id~='^(${this.format_id.split("-")[0]})'][language^=${this.lang}]"
} else {
altAudioF = this.format_id.split("-")[0]
this.format_id
}
} else this.format_id
} ?: f
}.joinToString("+").ifBlank { "ba" }
val preferredAudioLanguage = sharedPreferences.getString("audio_language", "")!!
if (downloadItem.videoPreferences.removeAudio) audioF = ""
var abrSort = ""
if(audioF.contains("kbps_ytdlnisgeneric")){
abrSort = audioF.split("kbps")[0]
audioF = ""
}
val f = StringBuilder()
val preferredCodec = sharedPreferences.getString("video_codec", "")
val preferredQuality = sharedPreferences.getString("video_quality", "best")
val vCodecPrefIndex = context.getStringArray(R.array.video_codec_values).indexOf(preferredCodec)
val vCodecPref = context.getStringArray(R.array.video_codec_values_ytdlp)[vCodecPrefIndex]
val defaultFormats = context.resources.getStringArray(R.array.video_formats_values)
val usingGenericFormat = defaultFormats.contains(videoF) || downloadItem.allFormats.isEmpty() || downloadItem.allFormats == getGenericVideoFormats(context.resources)
var hasGenericResulutionFormat = ""
if (!usingGenericFormat){
// with audio removed
if (audioF.isBlank()){
f.append("$videoF/bv/b")
}else{
//with audio
f.append("$videoF+$audioF/")
if (altAudioF.isNotBlank()){
f.append("$videoF+$altAudioF/")
}
f.append("$videoF+ba/$videoF/b")
if (audioF.count("+") > 0){
request.addOption("--audio-multistreams")
}
}
}else{
if (videoF == context.resources.getString(R.string.best_quality) || videoF == "best") {
videoF = "bv"
}else if (videoF == context.resources.getString(R.string.worst_quality) || videoF == "worst") {
videoF = "wv"
if (audioF == "ba") audioF = "wa"
}else if (defaultFormats.contains(videoF)) {
hasGenericResulutionFormat = videoF.split("_")[0].dropLast(1)
videoF = "bv"
}
val preferredFormatIDs = sharedPreferences.getString("format_id", "").toString()
.split(",")
.filter { it.isNotEmpty() }
.ifEmpty { listOf(videoF) }.toMutableList()
if (!preferredFormatIDs.contains(videoF)){
preferredFormatIDs.add(0, videoF)
}
// ^ [videoF, preferredID1, preferredID2...]
val preferredAudioFormatIDs = sharedPreferences.getString("format_id_audio", "")
.toString()
.split(",")
.filter { it.isNotBlank() }
.ifEmpty {
val list = mutableListOf<String>()
if (preferredAudioLanguage.isNotEmpty() && !downloadItem.videoPreferences.removeAudio) list.add("ba[language^=$preferredAudioLanguage]")
list.add(audioF)
list
}.apply {
}.toMutableList()
if (!preferredAudioFormatIDs.contains(audioF) && audioF != "ba"){
preferredAudioFormatIDs.add(0, audioF)
}
// ^ [audioF, preferredAID1, preferredAID2, ....]
if (preferredAudioFormatIDs.any{it.contains("+")}){
request.addOption("--audio-multistreams")
}
preferredFormatIDs.forEach { v ->
preferredAudioFormatIDs.forEach { a ->
val aa = if (a.isNotBlank()) "+$a" else ""
f.append("$v$aa/")
}
//build format with just videoformatid and audio if remove audio is not checked
if (!downloadItem.videoPreferences.removeAudio){
//build format with audio with preferred language
if (preferredAudioLanguage.isNotBlank()){
val al = if (v == "wv"){
"$v+wa[language^=$preferredAudioLanguage]/"
}else{
"$v+ba[language^=$preferredAudioLanguage]/"
}
if (!f.contains(al)) f.append(al)
}
//build format with best audio
if (!f.contains("$v+ba/") && !f.contains("wa")) f.append("$v+ba/")
//build format with standalone video
if (v == "wv"){
f.append("w/")
}else{
if (v != "bv") f.append("$v/")
}
}
}
if(!f.endsWith("b/")){
//last fallback
f.append("b")
}
}
val genericFormats = getGenericVideoFormats(context.resources).map { it.format_id }
StringBuilder().apply {
if (hasGenericResulutionFormat.isNotBlank()) {
append(",res:${hasGenericResulutionFormat}")
}else if (genericFormats.contains(videoF) && preferredQuality!!.contains("p_")){
append(",res:${preferredQuality.split("_")[0]}")
}
if (sharedPreferences.getBoolean("prefer_smaller_formats", false)) append(",+size")
if (vCodecPref.isNotBlank()) append(",vcodec:$vCodecPref")
if (aCodecPref.isNotBlank()) append(",acodec:$aCodecPref")
if (cont.isNotBlank()) append(",vext:$cont")
if (acont.isNotBlank()) append(",aext:$acont")
if (abrSort.isNotBlank()) append(",abr~${abrSort}")
if (this.isNotBlank()){
request.addOption("-S", "+hasaud$this")
}
}
request.addOption("-f", f.toString().replace("/$".toRegex(), ""))
if (downloadItem.videoPreferences.writeSubs){
request.addOption("--write-subs")
}
if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs")
}
if(downloadItem.videoPreferences.writeAutoSubs){
request.addOption("--write-auto-subs")
}
if (downloadItem.videoPreferences.embedSubs || downloadItem.videoPreferences.writeSubs || downloadItem.videoPreferences.writeAutoSubs){
val subFormat = sharedPreferences.getString("sub_format", "srt")
if(subFormat!!.isNotBlank()){
request.addOption("--sub-format", "${subFormat}/best")
request.addOption("--convert-subtitles", subFormat)
}
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages.ifEmpty { "en.*,.*-orig" })
}
if (downloadItem.videoPreferences.removeAudio){
request.addOption("--use-postprocessor", "FFmpegCopyStream")
request.addOption("--ppa", "CopyStream:-c copy -an")
}
request.addOption("-P", downDir.absolutePath)
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters")
request.addOption("-o", "chapter:%(section_number)d - %(section_title)s.%(ext)s")
}else{
if (filenameTemplate.isNotBlank()){
request.addOption("-o", "${filenameTemplate.removeSuffix(".%(ext)s")}.%(ext)s")
}
}
}
DownloadViewModel.Type.command -> {
request.addOption("-P", downDir.absolutePath)
request.addOption(
"--config-locations",
File(context.cacheDir.absolutePath + "/config[${downloadItem.id}].txt").apply {
writeText(downloadItem.format.format_note)
}.absolutePath
)
}
else -> {}
}
if (downloadItem.extraCommands.isNotBlank() && downloadItem.type != DownloadViewModel.Type.command){
val cache = File(FileUtil.getCachePath(context))
cache.mkdirs()
val conf = File(cache.absolutePath + "/${System.currentTimeMillis()}${UUID.randomUUID()}.txt")
conf.createNewFile()
conf.writeText(downloadItem.extraCommands)
request.addOption(
"--config-locations",
conf.absolutePath
)
}
return request
}
fun getPipedInstances() : List<String> {
kotlin.runCatching {
val res = 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 parseYTDLRequestString(request : YoutubeDLRequest) : String {
val arr = request.buildCommand().toMutableList()
for (i in arr.indices) {
if (!arr[i].startsWith("-")) {
arr[i] = "\"${arr[i]}\""
}
}
var final = java.lang.String.join(" ", arr).replace("\"\"", "\" \"")
val ppas = "--config(-locations)? \"(.*?)\"".toRegex().findAll(final)
ppas.forEach {res ->
val path = "\"(.*?)\"".toRegex().find(res.value)?.value?.replace("\"", "")
val newVal = runCatching {
File(path ?: "").readText()
}.onFailure {
res.value
}.getOrDefault("")
final = final.replace(res.value, newVal)
}
return final
}
fun getGenericAudioFormats(resources: Resources) : MutableList<Format>{
val audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",").filter { it.isNotEmpty() }
val audioFormats = resources.getStringArray(R.array.audio_formats)
val audioFormatsValues = resources.getStringArray(R.array.audio_formats_values)
val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("audio_format", "")
val acodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
if (this.isEmpty()){
resources.getString(R.string.defaultValue)
}else{
val audioCodecs = resources.getStringArray(R.array.audio_codec)
val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values)
audioCodecs[audioCodecsValues.indexOf(this)]
}
}
audioFormats.forEachIndexed { idx, it -> formats.add(Format(audioFormatsValues[idx], containerPreference!!,"",acodecPreference!!, "",0, it)) }
audioFormatIDPreference.forEach { formats.add(Format(it, containerPreference!!,"",resources.getString(R.string.preferred_format_id), "",1, it)) }
return formats
}
fun getGenericVideoFormats(resources: Resources) : MutableList<Format>{
val formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",").filter { it.isNotEmpty() }
val videoFormatsValues = resources.getStringArray(R.array.video_formats_values)
val videoFormats = resources.getStringArray(R.array.video_formats)
val formats = mutableListOf<Format>()
val containerPreference = sharedPreferences.getString("video_format", "")
val audioCodecPreference = sharedPreferences.getString("audio_codec", "")!!.run {
if (this.isNotEmpty()){
val audioCodecs = resources.getStringArray(R.array.audio_codec)
val audioCodecsValues = resources.getStringArray(R.array.audio_codec_values)
audioCodecs[audioCodecsValues.indexOf(this)]
}else this
}
val videoCodecPreference = sharedPreferences.getString("video_codec", "")!!.run {
if (this.isEmpty()){
resources.getString(R.string.defaultValue)
}else{
val videoCodecs = resources.getStringArray(R.array.video_codec)
val videoCodecsValues = resources.getStringArray(R.array.video_codec_values)
videoCodecs[videoCodecsValues.indexOf(this)]
}
}
videoFormatsValues.forEachIndexed { index, it -> formats.add(Format(it, containerPreference!!,videoCodecPreference,audioCodecPreference, "",0, videoFormats[index])) }
formatIDPreference.forEach { formats.add(Format(it, containerPreference!!,resources.getString(R.string.preferred_format_id),"", "",1, it)) }
return formats
}
private val pipedURL = sharedPreferences.getString("piped_instance", defaultPipedURL)?.ifEmpty { defaultPipedURL }?.removeSuffix("/")
class PlaylistTuple internal constructor(
var nextPageToken: String,
var videos: ArrayList<ResultItem>,
)
companion object {
private const val TAG = "API MANAGER"
private const val defaultPipedURL = "https://pipedapi.kavin.rocks/"
private var countryCODE: String = ""
}
}