From 297cc537108f5d5f528c16b8c9a5ab3ffd54c824 Mon Sep 17 00:00:00 2001 From: deniscerri <64997243+deniscerri@users.noreply.github.com> Date: Sun, 11 Jun 2023 23:37:47 +0200 Subject: [PATCH] added playlist support from piped and fixed some format bugs --- README.MD | 4 +- app/build.gradle | 1 - .../ytdlnis/adapter/ActiveDownloadAdapter.kt | 36 +--- .../database/models/VideoPreferences.kt | 2 +- .../database/repository/ResultRepository.kt | 15 +- .../database/viewmodel/DownloadViewModel.kt | 22 ++- .../database/viewmodel/ResultViewModel.kt | 9 +- .../com/deniscerri/ytdlnis/ui/HomeFragment.kt | 30 ++-- .../downloadcard/CutVideoBottomSheetDialog.kt | 2 +- .../more/settings/FolderSettingsFragment.kt | 7 +- .../com/deniscerri/ytdlnis/util/FileUtil.kt | 13 +- .../com/deniscerri/ytdlnis/util/InfoUtil.kt | 167 ++++++------------ .../deniscerri/ytdlnis/work/DownloadWorker.kt | 6 +- .../main/res/layout/active_download_card.xml | 31 +--- .../res/layout/fragment_download_command.xml | 1 + app/src/main/res/values/strings.xml | 2 +- app/src/main/res/values/styles.xml | 12 +- .../main/res/xml/processing_preferences.xml | 4 +- 18 files changed, 127 insertions(+), 237 deletions(-) diff --git a/README.MD b/README.MD index ce7c8127..a9bbc3b9 100644 --- a/README.MD +++ b/README.MD @@ -1,5 +1,5 @@

-
+
YTDLnis

@@ -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) diff --git a/app/build.gradle b/app/build.gradle index e4c0f495..e69f6d4a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt index 0304fe65..4678e731 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/ActiveDownloadAdapter.kt @@ -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(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(R.id.format_note) - formatNote.text = item.format.format_note.uppercase() + val formatDetailsChip = card.findViewById(R.id.format_note) + val formatDetailsText = StringBuilder(item.format.format_note.uppercase()+"\t") - val codec = card.findViewById(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(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(R.id.output) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt index 3661993a..2dd351ae 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt @@ -6,7 +6,7 @@ data class VideoPreferences ( var splitByChapters: Boolean = false, var sponsorBlockFilters: ArrayList = arrayListOf(), var writeSubs: Boolean = false, - var subsLanguages: String = "en.*,.*-orig", + var subsLanguages: String = "all", var audioFormatIDs : ArrayList = arrayListOf(), var removeAudio: Boolean = false ) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt index 21ba7047..3c19d8b7 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt @@ -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> = 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() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt index 98abfee6..a4360095 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt @@ -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 ) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt index 0c965902..8edee5dd 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt @@ -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> val loadingItems = MutableLiveData() - var itemCount : LiveData 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) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt index 2a989faf..8ebf2480 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/HomeFragment.kt @@ -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 } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/CutVideoBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/CutVideoBottomSheetDialog.kt index ff001f45..9c052d88 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/CutVideoBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/CutVideoBottomSheetDialog.kt @@ -198,8 +198,8 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls try{ val listType: Type = object : TypeToken>() {}.type chapters = Gson().fromJson(data.first().toString(), listType) + data.removeFirst() }catch (ignored: Exception) {} - data.removeFirst() } if (data.isEmpty()) throw Exception("No Streaming URL found!") diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt index 00d6267d..31358427 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt @@ -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()) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt index 2f94c292..c2f4af2c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt @@ -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]}" } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt index af4c7b50..69d0b81a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt @@ -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 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 { - 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 { - 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®ionCode=$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 = 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 = 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() 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 { - init() + items = ArrayList() return if (key!!.isEmpty()) { getTrendingFromPiped() @@ -580,7 +514,7 @@ class InfoUtil(private val context: Context) { } fun getFormats(url: String) : List { - 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, progress: (progress: List) -> 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 { - 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 = "" } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt index 3de4c937..72b925d7 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt @@ -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 && diff --git a/app/src/main/res/layout/active_download_card.xml b/app/src/main/res/layout/active_download_card.xml index 1fce7ca5..84477d8f 100644 --- a/app/src/main/res/layout/active_download_card.xml +++ b/app/src/main/res/layout/active_download_card.xml @@ -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" /> - - - - - + app:shapeAppearance="@style/ShapeAppearanceOverlay.Chip.Rounded" /> diff --git a/app/src/main/res/layout/fragment_download_command.xml b/app/src/main/res/layout/fragment_download_command.xml index afae490b..2694df1c 100644 --- a/app/src/main/res/layout/fragment_download_command.xml +++ b/app/src/main/res/layout/fragment_download_command.xml @@ -55,6 +55,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4ff143df..358d698d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -277,6 +277,6 @@ Transfer cached files to the downloads folder All items must either be audio or video to use this option Piped Instance - Write a PIPED API Server, the app can use for youtube queries and formats + Write a PIPED API Server, the app can use for YouTube queries and formats Use Custom Audio Quality \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a75b93fb..364aa517 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -31,19 +31,11 @@ 5dp - - - diff --git a/app/src/main/res/xml/processing_preferences.xml b/app/src/main/res/xml/processing_preferences.xml index c66e7bfa..26fb92ee 100644 --- a/app/src/main/res/xml/processing_preferences.xml +++ b/app/src/main/res/xml/processing_preferences.xml @@ -39,10 +39,8 @@