added playlist support from piped and fixed some format bugs

This commit is contained in:
deniscerri 2023-06-11 23:37:47 +02:00
parent b7c2ae0100
commit 297cc53710
No known key found for this signature in database
GPG key ID: 95C43D517D830350
18 changed files with 127 additions and 237 deletions

View file

@ -1,5 +1,5 @@
<h1 align="center">
<img src="fastlane/metadata/android/en-US/images/icon.png" width="30%" /> <br>
<img src="fastlane/metadata/android/en-US/images/icon.png" width="25%" /> <br>
YTDLnis
</h1>
@ -11,7 +11,7 @@
[![Github Download](https://custom-icon-badges.herokuapp.com/badge/Download-blue?style=for-the-badge&logo=download&logoColor=white)](https://github.com/deniscerri/ytdlnis/releases/latest)
[![IzzyOnDroid Repo](https://custom-icon-badges.herokuapp.com/badge/IzzyOnDroid%20Repo-red?style=for-the-badge&logo=download&logoColor=white)](https://android.izzysoft.de/repo/apk/com.deniscerri.ytdl)
[![UpToDown](https://custom-icon-badges.herokuapp.com/badge/UpToDown-green?style=for-the-badge&logo=download&logoColor=white)]([https://android.izzysoft.de/repo/apk/com.deniscerri.ytdl](https://ytdlnis.en.uptodown.com/android/download))
![CI](https://github.com/deniscerri/ytdlnis/actions/workflows/android.yml/badge.svg?branch=main&event=pull)
[![preview release](https://img.shields.io/github/release/deniscerri/ytdlnis.svg?maxAge=3600&include_prereleases&label=preview)](https://github.com/deniscerri/ytdlnis/releases)

View file

@ -181,5 +181,4 @@ dependencies {
implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4'
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1"
implementation "com.github.Irineu333:Highlight-KT:1.0.4"
implementation "com.anggrayudi:storage:1.5.4"
}

View file

@ -24,6 +24,7 @@ import com.google.android.material.progressindicator.LinearProgressIndicator
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.ShapeAppearanceModel
import com.squareup.picasso.Picasso
import java.lang.StringBuilder
class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity: Activity) : ListAdapter<DownloadItem?, ActiveDownloadAdapter.ViewHolder>(AsyncDifferConfig.Builder(DIFF_CALLBACK).build()) {
private val onItemClickListener: OnItemClickListener
@ -91,10 +92,9 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
DownloadViewModel.Type.command -> type.setIconResource(R.drawable.ic_terminal)
}
val formatNote = card.findViewById<Chip>(R.id.format_note)
formatNote.text = item.format.format_note.uppercase()
val formatDetailsChip = card.findViewById<Chip>(R.id.format_note)
val formatDetailsText = StringBuilder(item.format.format_note.uppercase()+"\t")
val codec = card.findViewById<Chip>(R.id.codec)
val codecText =
if (item.format.encoding != "") {
item.format.encoding.uppercase()
@ -103,34 +103,14 @@ class ActiveDownloadAdapter(onItemClickListener: OnItemClickListener, activity:
} else {
item.format.acodec.uppercase()
}
if (codecText == "" || codecText == "none"){
codec.visibility = View.GONE
}else{
codec.visibility = View.VISIBLE
codec.text = codecText
if (codecText != "" && codecText != "none"){
formatDetailsText.append("\t|\t$codecText")
}
val fileSize = card.findViewById<Chip>(R.id.file_size)
fileSize.text = FileUtil.convertFileSize(item.format.filesize)
if (fileSize.text == "?") fileSize.visibility = View.GONE
val fileSize = FileUtil.convertFileSize(item.format.filesize)
if (fileSize != "?") formatDetailsText.append("\t|\t$fileSize")
if (fileSize.visibility == View.VISIBLE && codec.visibility == View.GONE){
fileSize.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setTopLeftCorner(CornerFamily.ROUNDED, 0F)
.setBottomLeftCorner(CornerFamily.ROUNDED, 0F)
.setTopRightCorner(CornerFamily.ROUNDED, 20F)
.setBottomRightCorner(CornerFamily.ROUNDED, 20F)
.build()
}
if (fileSize.visibility == View.GONE && codec.visibility == View.GONE){
formatNote.shapeAppearanceModel = ShapeAppearanceModel.builder()
.setTopLeftCorner(CornerFamily.ROUNDED, 40F)
.setBottomLeftCorner(CornerFamily.ROUNDED, 40F)
.setTopRightCorner(CornerFamily.ROUNDED, 40F)
.setBottomRightCorner(CornerFamily.ROUNDED, 40F)
.build()
}
formatDetailsChip.text = formatDetailsText
//OUTPUT
val output = card.findViewById<TextView>(R.id.output)

View file

@ -6,7 +6,7 @@ data class VideoPreferences (
var splitByChapters: Boolean = false,
var sponsorBlockFilters: ArrayList<String> = arrayListOf(),
var writeSubs: Boolean = false,
var subsLanguages: String = "en.*,.*-orig",
var subsLanguages: String = "all",
var audioFormatIDs : ArrayList<String> = arrayListOf(),
var removeAudio: Boolean = false
)

View file

@ -9,11 +9,12 @@ import com.deniscerri.ytdlnis.database.dao.ResultDao
import com.deniscerri.ytdlnis.database.models.CommandTemplate
import com.deniscerri.ytdlnis.database.models.ResultItem
import com.deniscerri.ytdlnis.util.InfoUtil
import kotlinx.coroutines.flow.MutableStateFlow
class ResultRepository(private val resultDao: ResultDao, private val commandTemplateDao: CommandTemplateDao, private val context: Context) {
private val tag: String = "ResultRepository"
val allResults : LiveData<List<ResultItem>> = resultDao.getResults()
var itemCount = MutableLiveData(-1)
var itemCount = MutableStateFlow(-1)
suspend fun insert(it: ResultItem){
resultDao.insert(it)
@ -27,7 +28,7 @@ class ResultRepository(private val resultDao: ResultDao, private val commandTemp
deleteAll()
val infoUtil = InfoUtil(context)
val items = infoUtil.getTrending(context)
itemCount.postValue(items.size)
itemCount.value = items.size
for (i in items){
resultDao.insert(i!!)
}
@ -38,7 +39,7 @@ class ResultRepository(private val resultDao: ResultDao, private val commandTemp
try{
if (resetResults) deleteAll()
val res = infoUtil.search(inputQuery)
itemCount.postValue(res.size)
itemCount.value = res.size
res.forEach {
resultDao.insert(it!!)
}
@ -54,7 +55,7 @@ class ResultRepository(private val resultDao: ResultDao, private val commandTemp
val v = infoUtil.getVideo(query)
if (resetResults) {
deleteAll()
itemCount.postValue(1)
itemCount.value = 1
}else{
v!!.playlistTitle = "ytdlnis-Search"
}
@ -81,10 +82,10 @@ class ResultRepository(private val resultDao: ResultDao, private val commandTemp
if (tmpToken == nextPageToken) break
nextPageToken = tmpToken
} while (true)
itemCount.postValue(items.size)
items.forEach {
resultDao.insert(it!!)
}
itemCount.value = items.size
return items
}
@ -94,7 +95,7 @@ class ResultRepository(private val resultDao: ResultDao, private val commandTemp
val items = infoUtil.getFromYTDL(inputQuery)
if (resetResults) {
deleteAll()
itemCount.postValue(items.size)
itemCount.value = items.size
}else{
items.forEach { it!!.playlistTitle = "ytdlnis-Search" }
}
@ -113,7 +114,7 @@ class ResultRepository(private val resultDao: ResultDao, private val commandTemp
}
suspend fun deleteAll(){
itemCount.postValue(0)
itemCount.value = 0
resultDao.deleteAll()
}

View file

@ -94,13 +94,14 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
val videoFormat = resources.getStringArray(R.array.video_formats)
val videoFormatValues = resources.getStringArray(R.array.video_formats_values)
var videoContainer = sharedPreferences.getString("video_format", "Default")
if (videoContainer == "Default") videoContainer = App.instance.getString(R.string.defaultValue)
defaultVideoFormats = mutableListOf()
videoFormat.forEach {
videoFormat.forEachIndexed { index , it ->
val tmp = Format(
it,
videoFormatValues[index],
videoContainer!!,
"",
"",
@ -116,7 +117,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
var audioContainer = sharedPreferences.getString("audio_format", "mp3")
if (audioContainer == "Default") audioContainer = App.instance.getString(R.string.defaultValue)
bestAudioFormat = Format(
resources.getString(R.string.best_quality),
"best",
audioContainer!!,
"",
"",
@ -155,8 +156,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
}
val downloadPath = when(type){
Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefautAudioPath())
Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefautVideoPath())
Type.audio -> sharedPreferences.getString("music_path", FileUtil.getDefaultAudioPath())
Type.video -> sharedPreferences.getString("video_path", FileUtil.getDefaultVideoPath())
else -> sharedPreferences.getString("command_path", FileUtil.getDefaultCommandPath())
}
@ -263,11 +264,18 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
else -> ""
}
val defaultPath = when(historyItem.type) {
Type.audio -> FileUtil.getDefaultAudioPath()
Type.video -> FileUtil.getDefaultVideoPath()
Type.command -> FileUtil.getDefaultCommandPath()
}
val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet())
val audioPreferences = AudioPreferences(embedThumb, false, ArrayList(sponsorblock!!))
val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs)
val downloadPath = File(historyItem.downloadPath)
val path = if (downloadPath.exists()) downloadPath.parent else defaultPath
return DownloadItem(0,
historyItem.url,
historyItem.title,
@ -279,7 +287,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
container,
"",
ArrayList(),
File(historyItem.downloadPath).parent!!, historyItem.website, "", "", audioPreferences, videoPreferences,customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0
path, historyItem.website, "", "", audioPreferences, videoPreferences,customFileNameTemplate!!, saveThumb, DownloadRepository.Status.Processing.toString(), 0
)
}

View file

@ -3,6 +3,7 @@ package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application
import android.content.SharedPreferences
import android.util.Log
import androidx.compose.runtime.MutableState
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
@ -16,17 +17,17 @@ import com.deniscerri.ytdlnis.database.models.SearchHistoryItem
import com.deniscerri.ytdlnis.database.repository.ResultRepository
import com.deniscerri.ytdlnis.database.repository.SearchHistoryRepository
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.regex.Pattern
class ResultViewModel(application: Application) : AndroidViewModel(application) {
private val tag: String = "ResultViewModel"
private val repository : ResultRepository
val repository : ResultRepository
private val searchHistoryRepository : SearchHistoryRepository
val items : LiveData<List<ResultItem>>
val loadingItems = MutableLiveData<Boolean>()
var itemCount : LiveData<Int>
private val sharedPreferences: SharedPreferences
init {
@ -36,7 +37,6 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao)
items = repository.allResults
loadingItems.postValue(false)
itemCount = repository.itemCount
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
}
@ -65,7 +65,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
if (inputQueries.size == 1){
parseQuery(inputQueries[0], true)
}else {
repository.itemCount.postValue(inputQueries.size)
repository.itemCount.value = inputQueries.size
loadingItems.postValue(true)
inputQueries.forEach {
parseQuery(it, false)
@ -90,7 +90,6 @@ class ResultViewModel(application: Application) : AndroidViewModel(application)
}
"Playlist" -> {
res = repository.getPlaylist(inputQuery, resetResults)
}
"Default" -> {
res = repository.getDefault(inputQuery, resetResults)

View file

@ -148,13 +148,13 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
resultViewModel.items.observe(viewLifecycleOwner) {
homeAdapter!!.submitList(it)
resultsList = it
if(resultViewModel.itemCount.value!! > 1 || resultViewModel.itemCount.value!! == -1){
if(resultViewModel.repository.itemCount.value > 1 || resultViewModel.repository.itemCount.value == -1){
if (it.size > 1 && it[0].playlistTitle.isNotEmpty() && it[0].playlistTitle != getString(R.string.trendingPlaylist) && !loadingItems){
downloadAllFabCoordinator!!.visibility = VISIBLE
}else{
downloadAllFabCoordinator!!.visibility = GONE
}
}else if (resultViewModel.itemCount.value!! == 1){
}else if (resultViewModel.repository.itemCount.value == 1){
if (sharedPreferences!!.getBoolean("download_card", true)){
if(it.size == 1 && quickLaunchSheet && parentFragmentManager.findFragmentByTag("downloadSingleSheet") == null){
showSingleDownloadSheet(it[0], DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), false)
@ -626,18 +626,26 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, OnClickListene
}
R.id.download -> {
lifecycleScope.launch {
val downloadList = withContext(Dispatchers.IO){
downloadViewModel.turnResultItemsToDownloadItems(selectedObjects!!)
if (sharedPreferences!!.getBoolean("download_card", true) && selectedObjects!!.size == 1) {
showSingleDownloadSheet(selectedObjects!![0], DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), false)
}else{
val downloadList = withContext(Dispatchers.IO){
downloadViewModel.turnResultItemsToDownloadItems(selectedObjects!!)
}
if (sharedPreferences!!.getBoolean("download_card", true)) {
if (selectedObjects!!.size == 1){
showSingleDownloadSheet(selectedObjects!![0], DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), false)
}else{
val bottomSheet = DownloadMultipleBottomSheetDialog(selectedObjects!!, downloadList.toMutableList())
bottomSheet.show(parentFragmentManager, "downloadMultipleSheet")
}
} else {
downloadViewModel.queueDownloads(downloadList)
}
}
clearCheckedItems()
actionMode?.finish()
if (sharedPreferences!!.getBoolean("download_card", true)) {
val bottomSheet = DownloadMultipleBottomSheetDialog(selectedObjects!!, downloadList.toMutableList())
bottomSheet.show(parentFragmentManager, "downloadMultipleSheet")
} else {
downloadViewModel.queueDownloads(downloadList)
}
}
true
}

View file

@ -198,8 +198,8 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls
try{
val listType: Type = object : TypeToken<List<ChapterItem>>() {}.type
chapters = Gson().fromJson(data.first().toString(), listType)
data.removeFirst()
}catch (ignored: Exception) {}
data.removeFirst()
}
if (data.isEmpty()) throw Exception("No Streaming URL found!")

View file

@ -11,18 +11,15 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.preference.EditTextPreference
import androidx.preference.Preference
import androidx.preference.PreferenceManager
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.WorkManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.work.DownloadWorker
import com.deniscerri.ytdlnis.work.MoveCacheFilesWorker
import com.google.android.material.snackbar.Snackbar
import java.io.File
import java.util.concurrent.TimeUnit
class FolderSettingsFragment : BaseSettingsFragment() {
@ -55,10 +52,10 @@ class FolderSettingsFragment : BaseSettingsFragment() {
moveCache = findPreference("move_cache")
if (preferences.getString("music_path", "")!!.isEmpty()) {
editor.putString("music_path", FileUtil.getDefautAudioPath())
editor.putString("music_path", FileUtil.getDefaultAudioPath())
}
if (preferences.getString("video_path", "")!!.isEmpty()) {
editor.putString("video_path", FileUtil.getDefautVideoPath())
editor.putString("video_path", FileUtil.getDefaultVideoPath())
}
if (preferences.getString("command_path", "")!!.isEmpty()) {
editor.putString("command_path", FileUtil.getDefaultCommandPath())

View file

@ -8,8 +8,6 @@ import android.os.Environment
import android.provider.DocumentsContract
import android.util.Log
import android.webkit.MimeTypeMap
import com.anggrayudi.storage.callback.FileCallback
import com.anggrayudi.storage.file.moveTo
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.DownloadItem
import okhttp3.internal.closeQuietly
@ -19,12 +17,14 @@ import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
import kotlin.math.log10
import kotlin.math.pow
object FileUtil {
fun deleteFile(path: String){
val file = File(path)
if (file.exists()) {
@ -134,7 +134,7 @@ object FileUtil {
if (Build.VERSION.SDK_INT >= 26 ){
Files.move(it.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING)
}else{
it.moveTo(context, destFile.parent!!, destFile.name, FileCallback.ConflictResolution.REPLACE)
it.renameTo(destFile)
}
fileList.add(destFile)
}
@ -183,11 +183,11 @@ object FileUtil {
return File(context.filesDir.absolutePath + """/logs/Terminal - ${titleRegex.replace(command.take(30), "")}##terminal.log""")
}
fun getDefautAudioPath() : String{
fun getDefaultAudioPath() : String{
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis/Audio"
}
fun getDefautVideoPath() : String{
fun getDefaultVideoPath() : String{
return Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + File.separator + "YTDLnis/Video"
}
@ -199,6 +199,7 @@ object FileUtil {
if (s <= 0) return "?"
val units = arrayOf("B", "kB", "MB", "GB", "TB")
val digitGroups = (log10(s.toDouble()) / log10(1024.0)).toInt()
return DecimalFormat("#,##0.#").format(s / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]
val symbols = DecimalFormatSymbols(Locale.US)
return "${DecimalFormat("#,##0.#", symbols).format(s / 1024.0.pow(digitGroups.toDouble()))} ${units[digitGroups]}"
}
}

View file

@ -6,7 +6,6 @@ import android.os.Looper
import android.text.Html
import android.util.Log
import android.widget.Toast
import androidx.core.text.HtmlCompat
import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.database.models.ChapterItem
@ -16,9 +15,13 @@ import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.yausername.youtubedl_android.YoutubeDL
import com.yausername.youtubedl_android.YoutubeDLRequest
import okhttp3.ResponseBody
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.http.GET
import java.io.BufferedReader
import java.io.File
import java.io.InputStreamReader
@ -32,32 +35,23 @@ import java.util.regex.Pattern
class InfoUtil(private val context: Context) {
private var items: ArrayList<ResultItem?>
private lateinit var sharedPreferences: SharedPreferences
private lateinit var retrofit : Retrofit
private var key: String? = null
private var useInvidous = false
init {
try {
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
key = sharedPreferences.getString("api_key", "")
countryCODE = sharedPreferences.getString("locale", "")!!
if (countryCODE.isEmpty()) countryCODE = "US"
} catch (e: Exception) {
e.printStackTrace()
}
items = ArrayList()
}
private fun init(){
if (countryCODE.isEmpty()){
countryCODE = "US"
}
if (!useInvidous){
val res = genericRequest(invidousURL + "stats")
useInvidous = res.length() != 0
}
}
fun search(query: String): ArrayList<ResultItem?> {
init()
items = ArrayList()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
return if (searchEngine == "ytsearch"){
@ -122,7 +116,7 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class)
fun searchFromPiped(query: String): ArrayList<ResultItem?> {
val data = genericRequest(pipedURL + "/search?q=" + query + "?filter=videos")
val data = genericRequest("$pipedURL/search?q=$query?filter=videos")
val dataArray = data.getJSONArray("items")
if (dataArray.length() == 0) return getFromYTDL(query)
for (i in 0 until dataArray.length()) {
@ -141,14 +135,27 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class)
fun getPlaylist(id: String, nextPageToken: String): PlaylistTuple {
try{
init()
items = ArrayList()
if (key!!.isEmpty()) {
return if (useInvidous) getPlaylistFromInvidous(id) else PlaylistTuple(
"",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
)
// -------------- 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()){
val obj = dataArray.getJSONObject(i)
items.add(createVideoFromPipedJSON(obj, obj.getString("url").removePrefix("/watch?v=")))
}
if (nextpage == "null") nextpage = ""
return PlaylistTuple(nextpage, items)
}
//---------- YOUTUBE API FUNCTION --------------------------------
val url = "https://youtube.googleapis.com/youtube/v3/playlistItems?part=snippet&pageToken=$nextPageToken&maxResults=50&regionCode=$countryCODE&playlistId=$id&key=$key"
//short data
val res = genericRequest(url)
@ -206,34 +213,12 @@ class InfoUtil(private val context: Context) {
}
}
private fun getPlaylistFromInvidous(id: String): PlaylistTuple {
val url = invidousURL + "playlists/" + id
val res = genericRequest(url)
if (res.length() == 0) return PlaylistTuple(
"",
getFromYTDL("https://www.youtube.com/playlist?list=$id")
)
try {
val vids = res.getJSONArray("videos")
for (i in 0 until vids.length()) {
val element = vids.getJSONObject(i)
val v = createVideoFromInvidiousJSON(element)
if (v == null || v.thumb.isEmpty()) continue
v.playlistTitle = res.getString("title")
items.add(v)
}
} catch (e: Exception) {
e.printStackTrace()
}
return PlaylistTuple("", items)
}
@Throws(JSONException::class)
fun getVideo(id: String): ResultItem? {
try {
init()
if (key!!.isEmpty()) {
val res = genericRequest(pipedURL + "/streams/" + id)
val res = genericRequest("$pipedURL/streams/$id")
return if (res.length() == 0) getFromYTDL("https://www.youtube.com/watch?v=$id")[0] else createVideoFromPipedJSON(
res, id
)
@ -282,71 +267,17 @@ class InfoUtil(private val context: Context) {
}
return video
}
private fun createVideoFromInvidiousJSON(obj: JSONObject): ResultItem? {
var video: ResultItem? = null
try {
val id = obj.getString("videoId")
val title = Html.fromHtml(obj.getString("title").toString()).toString()
val author = Html.fromHtml(obj.getString("author").toString()).toString()
if (author.isBlank()) throw Exception()
val duration = formatIntegerDuration(obj.getInt("lengthSeconds"), Locale.getDefault())
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val url = "https://www.youtube.com/watch?v=$id"
val formats : ArrayList<Format> = ArrayList()
if (obj.has("adaptiveFormats")){
val formatsInJSON = obj.getJSONArray("adaptiveFormats")
for (f in 0 until formatsInJSON.length()){
val format = formatsInJSON.getJSONObject(f)
if(!format.has("container")) continue
val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{
if (!formatObj.format_note.contains("audio", ignoreCase = true)){
val codecs = "\"([^\"]*)\"".toRegex().find(format.getString("type").split(";")[1])!!.value.split(",")
if (codecs.size > 1){
formatObj.vcodec = codecs[0].replace("\"", "")
formatObj.acodec = codecs[1].replace("\"", "")
}else if (codecs.size == 1){
formatObj.vcodec = codecs[0].replace("\"", "")
formatObj.acodec = "none"
}
}
}catch (e: Exception) {
e.printStackTrace()
}
formats.add(formatObj)
}
}
video = ResultItem(0,
url,
title,
author,
duration,
thumb,
"youtube",
"",
formats,
"",
ArrayList()
)
} catch (e: Exception) {
Log.e(TAG, e.toString())
}
return video
}
private fun createVideoFromPipedJSON(obj: JSONObject, id: String): ResultItem? {
var video: ResultItem? = null
try {
val title = Html.fromHtml(obj.getString("title").toString()).toString()
val author = Html.fromHtml(obj.getString("uploader").toString()).toString()
if (author.isBlank()) throw Exception()
val author = try {
Html.fromHtml(obj.getString("uploader").toString()).toString()
}catch (e: Exception){
Html.fromHtml(obj.getString("uploaderName").toString()).toString()
}
val duration = formatIntegerDuration(obj.getInt("duration"), Locale.getDefault())
val duration = formatIntegerDuration(obj.getInt("duration"), Locale.US)
val thumb = "https://i.ytimg.com/vi/$id/hqdefault.jpg"
val url = "https://www.youtube.com/watch?v=$id"
val formats : ArrayList<Format> = ArrayList()
@ -358,8 +289,6 @@ class InfoUtil(private val context: Context) {
val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{
formatObj.acodec = format.getString("codec")
val streamURL = format.getString("url")
formatObj.format_id = streamURL.substringAfter("itag=").substringBefore("&")
formatObj.asr = format.getString("quality")
if (! format.getString("audioTrackName").equals("null", ignoreCase = true)){
formatObj.format_note = format.getString("audioTrackName") + " Audio, " + formatObj.format_note
@ -382,8 +311,6 @@ class InfoUtil(private val context: Context) {
val formatObj = Gson().fromJson(format.toString(), Format::class.java)
try{
formatObj.vcodec = format.getString("codec")
val streamURL = format.getString("url")
formatObj.format_id = streamURL.substringAfter("itag=").substringBefore("&")
}catch (e: Exception) {
e.printStackTrace()
}
@ -391,6 +318,13 @@ class InfoUtil(private val context: Context) {
}
}
formats.sortBy { it.filesize }
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 engDefault = it.value.find { f -> f.format_note.contains("original", true) }
engDefault?.format_id = (engDefault?.format_id?.split("-")?.get(0) ?: "") + "-${it.value.size-1}"
}
}
val chapters = ArrayList<ChapterItem>()
if (obj.has("chapters") && obj.getJSONArray("chapters").length() > 0){
@ -506,7 +440,7 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class)
fun getTrending(context: Context): ArrayList<ResultItem?> {
init()
items = ArrayList()
return if (key!!.isEmpty()) {
getTrendingFromPiped()
@ -580,7 +514,7 @@ class InfoUtil(private val context: Context) {
}
fun getFormats(url: String) : List<Format> {
init()
val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?")
val m = p.matcher(url)
val formatSource = sharedPreferences.getString("formats_source", "yt-dlp")
@ -653,7 +587,7 @@ class InfoUtil(private val context: Context) {
}
fun getFormatsMultiple(urls: List<String>, progress: (progress: List<Format>) -> Unit){
init()
val urlsFile = File(context.cacheDir, "urls.txt")
urlsFile.delete()
urlsFile.createNewFile()
@ -729,7 +663,6 @@ class InfoUtil(private val context: Context) {
}
fun getFromYTDL(query: String): ArrayList<ResultItem?> {
init()
items = ArrayList()
val searchEngine = sharedPreferences.getString("search_engine", "ytsearch")
try {
@ -774,7 +707,7 @@ class InfoUtil(private val context: Context) {
var duration = ""
runCatching {
if (jsonObject.has("duration")) {
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.getDefault())
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
}
}
val url = jsonObject.getString("webpage_url")
@ -801,11 +734,14 @@ class InfoUtil(private val context: Context) {
format.put("filesize", 0)
}
val formatProper = Gson().fromJson(format.toString(), Format::class.java)
if (format.has("format_note") && formatProper.format_note != null){
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{
formatProper.format_note = "${format.getString("format_note")} audio"
if (!formatProper.format_note.endsWith("audio", true)){
formatProper.format_note = "${format.getString("format_note")} audio"
}
}
}
if (formatProper.format_note == "storyboard") continue
@ -825,7 +761,6 @@ class InfoUtil(private val context: Context) {
var urls = "";
if(jsonObject.has("urls")) urls = jsonObject.getString("urls");
Log.e("aa", formats.toString())
items.add(ResultItem(0,
url,
title,
@ -846,7 +781,6 @@ class InfoUtil(private val context: Context) {
}
e.printStackTrace()
}
Log.e("aa", items.toString())
return items
}
@ -880,7 +814,7 @@ class InfoUtil(private val context: Context) {
var duration = ""
runCatching {
if (jsonObject.has("duration")) {
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.getDefault())
duration = formatIntegerDuration(jsonObject.getInt("duration"), Locale.US)
}
}
@ -1037,7 +971,6 @@ class InfoUtil(private val context: Context) {
companion object {
private const val TAG = "API MANAGER"
private const val invidousURL = "https://invidious.baczek.me/api/v1/"
private const val defaultPipedURL = "https://pipedapi.kavin.rocks/"
private var countryCODE: String = ""
}

View file

@ -226,7 +226,7 @@ class DownloadWorker(
}
if (downloadItem.videoPreferences.embedSubs) {
request.addOption("--embed-subs")
request.addOption("--sub-langs", "en.*,.*-orig")
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
}
val defaultFormats = context.resources.getStringArray(R.array.video_formats)
@ -265,7 +265,9 @@ class DownloadWorker(
request.addOption("--write-auto-subs")
request.addOption("--sub-format", "str/ass/best")
request.addOption("--convert-subtitles", "srt")
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
if (!downloadItem.videoPreferences.embedSubs) {
request.addOption("--sub-langs", downloadItem.videoPreferences.subsLanguages)
}
}
if (downloadItem.videoPreferences.removeAudio &&

View file

@ -96,36 +96,7 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Chip.RoundedStart" />
<com.google.android.material.chip.Chip
android:id="@+id/codec"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:textSize="11sp"
app:chipBackgroundColor="?attr/colorSurface"
app:chipMinTouchTargetSize="0dp"
app:chipStrokeWidth="0.5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Chip.Middle" />
<com.google.android.material.chip.Chip
android:id="@+id/file_size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:textSize="11sp"
app:chipBackgroundColor="?attr/colorSurface"
app:chipMinTouchTargetSize="0dp"
app:chipStrokeWidth="0.5dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Chip.RoundedEnd" />
app:shapeAppearance="@style/ShapeAppearanceOverlay.Chip.Rounded" />
</com.google.android.material.chip.ChipGroup>

View file

@ -55,6 +55,7 @@
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:inputType="text"
android:maxLines="2"
android:layout_height="wrap_content"/>
</com.google.android.material.textfield.TextInputLayout>

View file

@ -277,6 +277,6 @@
<string name="move_temporary_files_summary">Transfer cached files to the downloads folder</string>
<string name="format_filtering_hint_2">All items must either be audio or video to use this option</string>
<string name="piped_instance">Piped Instance</string>
<string name="piped_instance_summary">Write a PIPED API Server, the app can use for youtube queries and formats</string>
<string name="piped_instance_summary">Write a PIPED API Server, the app can use for YouTube queries and formats</string>
<string name="custom_audio_quality">Use Custom Audio Quality</string>
</resources>

View file

@ -31,19 +31,11 @@
<item name="cornerSize">5dp</item>
</style>
<style name="ShapeAppearanceOverlay.Chip.RoundedStart" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<style name="ShapeAppearanceOverlay.Chip.Rounded" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSizeTopLeft">12dp</item>
<item name="cornerSizeTopRight">0dp</item>
<item name="cornerSizeBottomLeft">12dp</item>
<item name="cornerSizeBottomRight">0dp</item>
</style>
<style name="ShapeAppearanceOverlay.Chip.RoundedEnd" parent="ShapeAppearance.MaterialComponents.SmallComponent">
<item name="cornerFamily">rounded</item>
<item name="cornerSizeTopLeft">0dp</item>
<item name="cornerSizeTopRight">12dp</item>
<item name="cornerSizeBottomLeft">0dp</item>
<item name="cornerSizeBottomLeft">12dp</item>
<item name="cornerSizeBottomRight">12dp</item>
</style>

View file

@ -39,10 +39,8 @@
<EditTextPreference
android:icon="@drawable/baseline_subject_24"
app:key="subs_lang"
android:dependency="write_subtitles"
app:useSimpleSummaryProvider="true"
app:defaultValue="en.*,.*-orig"
app:defaultValue="all"
app:title="@string/subtitle_languages" />
<SwitchPreferenceCompat