diff --git a/app/build.gradle b/app/build.gradle index bb3f0370..67f82c37 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ def properties = new Properties() def versionMajor = 1 def versionMinor = 6 def versionPatch = 3 -def versionBuild = 5 // bump for dogfood builds, public betas, etc. +def versionBuild = 9 // bump for dogfood builds, public betas, etc. def versionExt = "" if (versionBuild > 0){ @@ -206,4 +206,6 @@ dependencies { // For RTSP playback support with ExoPlayer implementation("androidx.media3:media3-exoplayer-rtsp:$media3_version") implementation("androidx.media3:media3-datasource-cronet:$media3_version") + + implementation "com.anggrayudi:storage:1.5.5" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 705a895b..91a929f8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,13 +2,6 @@ - - - @@ -19,7 +12,7 @@ - + @@ -38,8 +31,7 @@ android:supportsRtl="true" android:theme="@style/BaseTheme" tools:ignore="DataExtractionRules" - tools:targetApi="tiramisu" - android:banner="@mipmap/ic_launcher"> + tools:targetApi="tiramisu"> @@ -49,18 +41,7 @@ android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" android:exported="true" android:windowSoftInputMode="adjustPan"> - - - - - - - - - - - @@ -126,17 +107,20 @@ + android:windowSoftInputMode="adjustPan" + android:exported="true"> - - + + + + + @@ -152,9 +136,12 @@ android:windowSoftInputMode="adjustPan"> - - + + + + + @@ -169,9 +156,12 @@ android:windowSoftInputMode="adjustPan"> - - + + + + + diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt index 20966e99..b4a07c5d 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/CookieViewModel.kt @@ -71,8 +71,12 @@ class CookieViewModel(private val application: Application) : AndroidViewModel(a if (!hasCookies()) throw Exception("There is no cookies in the database!") flush() } + + var dbPath = "/data/data/com.deniscerri.ytdl/app_webview/Cookies" + if (!File(dbPath).exists()) dbPath = "/data/data/com.deniscerri.ytdl/app_webview/Default/Cookies" + SQLiteDatabase.openDatabase( - "/data/data/com.deniscerri.ytdl/app_webview/Default/Cookies", null, OPEN_READONLY + dbPath, null, OPEN_READONLY ).run { val projection = arrayOf( CookieObject.HOST, 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 87078cd9..b1ac71e9 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 @@ -68,8 +68,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application private var defaultVideoFormats : MutableList private val videoQualityPreference: String - private val formatIDPreference: String - private val audioFormatIDPreference: String + private val formatIDPreference: List + private val audioFormatIDPreference: List private val resources : Resources enum class Type { audio, video, command @@ -92,8 +92,8 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application erroredDownloads = repository.erroredDownloads.asLiveData() videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString() - formatIDPreference = sharedPreferences.getString("format_id", "").toString() - audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString() + formatIDPreference = sharedPreferences.getString("format_id", "").toString().split(",") + audioFormatIDPreference = sharedPreferences.getString("format_id_audio", "").toString().split(",") val confTmp = Configuration(application.resources.configuration) confTmp.locale = Locale(sharedPreferences.getString("app_language", "en")!!) @@ -170,7 +170,9 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val sponsorblock = sharedPreferences.getStringSet("sponsorblock_filters", emptySet()) val audioPreferences = AudioPreferences(embedThumb, false, ArrayList(sponsorblock!!)) - val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs, audioFormatIDs = ArrayList(resultItem.formats.filter { it.format_id == audioFormatIDPreference }.map { it.format_id })) + + val hasPreferredAudioFormats = resultItem.formats.filter { audioFormatIDPreference.contains(it.format_id) } + val videoPreferences = VideoPreferences(embedSubs, addChapters, false, ArrayList(sponsorblock), saveSubs, audioFormatIDs = if (hasPreferredAudioFormats.isEmpty()) arrayListOf() else arrayListOf(hasPreferredAudioFormats.map { it.format_id }.first())) return DownloadItem(0, resultItem.url, @@ -317,7 +319,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application return cloneFormat ( try { try{ - formats.first { it.format_note.contains("audio", ignoreCase = true) && it.format_id == audioFormatIDPreference } + formats.first { it.format_note.contains("audio", ignoreCase = true) && audioFormatIDPreference.contains(it.format_id)} }catch (e: Exception){ formats.last { it.format_note.contains("audio", ignoreCase = true) } } @@ -332,7 +334,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application try { val theFormats = formats.ifEmpty { defaultVideoFormats } try { - formats.first { !it.format_note.contains("audio", ignoreCase = true) && it.format_id == formatIDPreference } + formats.first { !it.format_note.contains("audio", ignoreCase = true) && formatIDPreference.contains(it.format_id)} }catch (e: Exception){ when (videoQualityPreference) { "worst" -> { @@ -397,7 +399,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application fun turnResultItemsToDownloadItems(items: List) : List { val list : MutableList = mutableListOf() - val preferredType = sharedPreferences.getString("preferred_download_type", "video"); + val preferredType = sharedPreferences.getString("preferred_download_type", "video") items.forEach { list.add(createDownloadItemFromResult(it!!, Type.valueOf(preferredType!!))) } @@ -454,12 +456,9 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads() val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true) val queuedItems = mutableListOf() - var lastDownloadId = repository.getLastDownloadId() - var exists = false items.forEach { - lastDownloadId++ it.status = DownloadRepository.Status.Queued.toString() if (activeAndQueuedDownloads.firstOrNull{d -> d.id = 0 @@ -469,7 +468,6 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application exists = true }else{ if (it.id == 0L){ - it.id = lastDownloadId val insert = async {repository.insert(it)} val id = insert.await() it.id = id @@ -492,7 +490,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application var delay = if (it.downloadStartTime != 0L){ it.downloadStartTime - currentTime } else 0 - if (delay < 0L) delay = 0L + if (delay < 0L) delay = 1L val workConstraints = Constraints.Builder() if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED) @@ -501,7 +499,7 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application .setInputData(Data.Builder().putLong("id", it.id).build()) .addTag("download") .setConstraints(workConstraints.build()) - .setInitialDelay(delay, TimeUnit.MILLISECONDS) + .setInitialDelay(delay, TimeUnit.SECONDS) .build() WorkManager.getInstance(context).beginUniqueWork( 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 416bbd99..0088aca4 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 @@ -103,7 +103,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) } private fun getQueryType(inputQuery: String) : String { var type = "Search" - val p = Pattern.compile("^(https?)://(www.)?(music.)?youtu(.be)?") + val p = Pattern.compile("(^(https?)://(www.)?(music.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)") val m = p.matcher(inputQuery) if (m.find()) { type = "YT_Video" diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index acf9b09a..d5ea6383 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -140,6 +140,9 @@ class FormatSelectionBottomSheetDialog(private val items: List, p res } if (chosenFormats.isEmpty()) throw Exception() + + formats = listOf(res) + //playlist format filtering }else{ var progress = "0/${items.size}" @@ -154,6 +157,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p formatCollection.add(it) } } + formats = formatCollection val flatFormatCollection = formatCollection.flatten() val commonFormats = flatFormatCollection.groupingBy { it.format_id }.eachCount().filter { it.value == items.size }.mapValues { flatFormatCollection.first { f -> f.format_id == it.key } }.map { it.value } chosenFormats = commonFormats.filter { it.filesize != 0L }.mapTo(mutableListOf()) {it.copy()} @@ -196,7 +200,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p //simple video format selection if (items.size == 1){ - listener.onFormatClick(List(items.size){chosenFormats}, listOf(FormatTuple(selectedVideo, selectedAudios))) + listener.onFormatClick(formats, listOf(FormatTuple(selectedVideo, selectedAudios))) }else{ //playlist format selection val selectedFormats = mutableListOf() @@ -208,7 +212,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p selectedFormats.add(selectedVideo) } } - listener.onFormatClick(formatCollection, selectedFormats.map { FormatTuple(it, selectedAudios) }) + listener.onFormatClick(formats, selectedFormats.map { FormatTuple(it, selectedAudios) }) } dismiss() @@ -260,7 +264,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p val clickedCard = (clickedformat as MaterialCardView) if (format.vcodec.isNotBlank() && format.vcodec != "none") { if (clickedCard.isChecked) { - listener.onFormatClick(List(items.size){finalFormats}, listOf(FormatTuple(format, null))) + listener.onFormatClick(formats, listOf(FormatTuple(format, null))) dismiss() } videoFormatList.forEach { (it as MaterialCardView).isChecked = false } @@ -279,7 +283,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p } }else{ if (items.size == 1){ - listener.onFormatClick(List(items.size){finalFormats}, listOf(FormatTuple(format, null))) + listener.onFormatClick(formats, listOf(FormatTuple(format, null))) }else{ val selectedFormats = mutableListOf() formatCollection.forEach { @@ -290,7 +294,7 @@ class FormatSelectionBottomSheetDialog(private val items: List, p selectedFormats.add(format) } } - listener.onFormatClick(formatCollection, selectedFormats.map { FormatTuple(it, null) }) + listener.onFormatClick(formats, selectedFormats.map { FormatTuple(it, null) }) } dismiss() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt index 23c8895f..f03240b4 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt @@ -447,10 +447,15 @@ class MainSettingsFragment : PreferenceFragmentCompat() { val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java) item.id = 0L cancelled.add(item) + + } + + cancelled.asReversed().forEach { f -> withContext(Dispatchers.IO){ - downloadViewModel.insert(item) + downloadViewModel.insert(f) } } + if(cancelled.isNotEmpty()){ finalMessage.append("${getString(R.string.cancelled)}: ${cancelled.count()}\n") } @@ -464,10 +469,15 @@ class MainSettingsFragment : PreferenceFragmentCompat() { val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java) item.id = 0L errored.add(item) + + } + + errored.asReversed().forEach { f -> withContext(Dispatchers.IO){ - downloadViewModel.insert(item) + downloadViewModel.insert(f) } } + if(errored.isNotEmpty()){ finalMessage.append("${getString(R.string.errored)}: ${errored.count()}\n") } @@ -491,13 +501,20 @@ class MainSettingsFragment : PreferenceFragmentCompat() { //command template restore if(json.has("templates")){ val items = json.getAsJsonArray("templates") + val templates = mutableListOf() items.forEach { val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CommandTemplate::class.java) item.id = 0L + templates.add(item) + + } + + templates.asReversed().forEach { t -> withContext(Dispatchers.IO){ - commandTemplateViewModel.insert(item) + commandTemplateViewModel.insert(t) } } + if(items.count() > 0){ finalMessage.append("${getString(R.string.command_templates)}: ${items.count()}\n") } 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 663a8f7e..97e4cb25 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt @@ -2,15 +2,21 @@ package com.deniscerri.ytdlnis.util import android.content.Context import android.media.MediaScannerConnection -import android.net.Uri import android.os.Build import android.os.Environment -import android.provider.DocumentsContract import android.util.Log -import android.webkit.MimeTypeMap +import androidx.core.net.toUri +import androidx.documentfile.provider.DocumentFile +import com.anggrayudi.storage.callback.FileCallback +import com.anggrayudi.storage.callback.FolderCallback +import com.anggrayudi.storage.file.copyFileTo +import com.anggrayudi.storage.file.copyFolderTo +import com.anggrayudi.storage.file.getAbsolutePath +import com.anggrayudi.storage.file.moveFileTo +import com.anggrayudi.storage.file.moveFolderTo import com.deniscerri.ytdlnis.R -import com.deniscerri.ytdlnis.database.models.DownloadItem -import okhttp3.internal.closeQuietly +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import java.io.File import java.net.URLDecoder import java.nio.charset.StandardCharsets @@ -70,81 +76,105 @@ object FileUtil { @Throws(Exception::class) - fun moveFile(originDir: File, context: Context, destDir: String, keepCache: Boolean, progress: (p: Int) -> Unit) : List { - val fileList = mutableListOf() - val dir = File(formatPath(destDir)) - if (!dir.exists()) dir.mkdirs() - var currentDirectory = dir - originDir.walk().forEach { - var destFile = File(dir.absolutePath + "/${it.absolutePath.removePrefix(originDir.absolutePath)}") - if (it.isDirectory) { - destFile.mkdirs() - currentDirectory = destFile - return@forEach - } - try { - if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(part-Frag)".toRegex())){ - it.delete() + suspend fun moveFile(originDir: File, context: Context, destDir: String, keepCache: Boolean, progress: (p: Int) -> Unit) : List { + return withContext(Dispatchers.Main){ + val fileList = mutableListOf() + val dir = File(formatPath(destDir)) + if (!dir.exists()) dir.mkdirs() + originDir.walk().forEach { + if (it.isDirectory && it.absolutePath == originDir.absolutePath) return@forEach + var destFile: DocumentFile + try { + if (it.name.matches("(^config.*.\\.txt\$)|(rList)|(part-Frag)".toRegex())){ + it.delete() + return@forEach + } + + if(it.name.contains(".part-Frag")) return@forEach + + + val curr = DocumentFile.fromFile(it) + val dst = DocumentFile.fromTreeUri(context, destDir.toUri()) + + if (it.isDirectory){ + withContext(Dispatchers.IO){ + curr.copyFolderTo(context, dst!!, skipEmptyFiles = false, callback = object : FolderCallback() { + override fun onStart(folder: DocumentFile, totalFilesToCopy: Int, workerThread: Thread): Long { + return 1000 // update progress every 1 second + } + + override fun onParentConflict(destinationFolder: DocumentFile, action: ParentFolderConflictAction, canMerge: Boolean) { + if (canMerge){ + action.confirmResolution(ConflictResolution.MERGE) + }else{ + action.confirmResolution(ConflictResolution.CREATE_NEW) + } + } + + override fun onReport(report: Report) { + progress(report.progress.toInt()) + } + + override fun onCompleted(result: Result) { + fileList.addAll(result.folder.listFiles().map { f -> f.getAbsolutePath(context) }) + it.deleteRecursively() + } + + }) + } + }else{ + withContext(Dispatchers.IO){ + curr.copyFileTo(context, dst!!, callback = object : FileCallback() { + override fun onStart(file: Any, workerThread: Thread): Long { + return 1000 // update progress every 1 second + } + + override fun onReport(report: Report) { + progress(report.progress.toInt()) + } + + override fun onCompleted(result: Any) { + destFile = (result as DocumentFile) + fileList.add(destFile.getAbsolutePath(context)) + it.deleteRecursively() + super.onCompleted(result) + } + }) + } + } + }catch (e: Exception) { + Log.e("error", e.message.toString()) + val files = it.listFiles()?.filter { fil -> !fil.isDirectory }?.toTypedArray() ?: arrayOf(it) + for (ff in files){ + val newFile = File(dir.absolutePath + "/${ff.absolutePath.removePrefix(originDir.absolutePath)}") + newFile.mkdirs() + if (Build.VERSION.SDK_INT >= 26 ) { + Files.move( + ff.toPath(), + newFile.toPath(), + StandardCopyOption.REPLACE_EXISTING + ) + }else{ + ff.renameTo(newFile) + } + fileList.add(newFile.absolutePath) + } return@forEach } - if(it.name.contains(".part-Frag")) return@forEach - - val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*" - destFile = File(currentDirectory.absolutePath + "/${it.name}") - - val dest = Uri.parse(destDir).run { - DocumentsContract.buildDocumentUriUsingTree( - this, - DocumentsContract.getTreeDocumentId(this) - ) - } - val destUri = DocumentsContract.createDocument( - context.contentResolver, - dest, - mimeType, - it.name - ) ?: return@forEach - - val inputStream = it.inputStream() - val outputStream = - context.contentResolver.openOutputStream(destUri) ?: return@forEach - inputStream.copyTo(outputStream) - inputStream.closeQuietly() - outputStream.closeQuietly() - - fileList.add(destFile) - }catch (e: java.lang.Exception) { - Log.e("error", e.message.toString()) - - if (destFile.absolutePath.contains("/storage/emulated/0/Download") - || destFile.absolutePath.contains("/storage/emulated/0/Documents") - ){ - if (Build.VERSION.SDK_INT >= 26 ){ - Files.move(it.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING) - }else{ - it.renameTo(destFile) - } - fileList.add(destFile) - } - return@forEach - }catch(e : Exception){ - Log.e("error", e.message.toString()) - return@forEach } - + if (!keepCache){ + originDir.deleteRecursively() + } + return@withContext scanMedia(fileList, context) } - if (!keepCache){ - originDir.deleteRecursively() - } - return scanMedia(fileList, context) } - private fun scanMedia(files: List, context: Context) : List { + private fun scanMedia(files: List, context: Context) : List { try { - val paths = files.map { it.absolutePath }.toTypedArray() - MediaScannerConnection.scanFile(context, paths, null, null) - return files.sortedByDescending { it.length() }.map { it.absolutePath } + val paths = files.sortedByDescending { File(it).length() } + MediaScannerConnection.scanFile(context, paths.toTypedArray(), null, null) + return paths }catch (e: Exception){ e.printStackTrace() } 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 04c58384..e42b7b9f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt @@ -68,7 +68,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®ion=${countryCODE}") val dataArray = data.getJSONArray("items") if (dataArray.length() == 0) return getFromYTDL(query) for (i in 0 until dataArray.length()) { @@ -86,7 +86,7 @@ class InfoUtil(private val context: Context) { @Throws(JSONException::class) fun searchFromPipedMusic(query: String): ArrayList { - val data = genericRequest("$pipedURL/search?q=$query=&filter=music_songs") + val data = genericRequest("$pipedURL/search?q=$query=&filter=music_songs®ion=${countryCODE}") val dataArray = data.getJSONArray("items") if (dataArray.length() == 0) return getFromYTDL(query) for (i in 0 until dataArray.length()) { @@ -350,9 +350,44 @@ class InfoUtil(private val context: Context) { @Throws(JSONException::class) fun getTrending(): ArrayList { items = ArrayList() + if (sharedPreferences.getString("api_key", "")!!.isNotBlank()){ + return getTrendingFromYoutubeAPI() + } return getTrendingFromPiped() } + @Throws(JSONException::class) + fun getTrendingFromYoutubeAPI(): ArrayList { + val key = sharedPreferences.getString("api_key", "")!! + + val url = "https://www.googleapis.com/youtube/v3/videos?part=snippet&chart=mostPopular&videoCategoryId=10®ionCode=$countryCODE&maxResults=25&key=$key" + //short data + val res = genericRequest(url) + //extra data from the same videos + val contentDetails = + genericRequest("https://www.googleapis.com/youtube/v3/videos?part=contentDetails&chart=mostPopular&videoCategoryId=10®ionCode=$countryCODE&maxResults=25&key=$key") + if (!contentDetails.has("items")) return ArrayList() + val dataArray = res.getJSONArray("items") + val extraDataArray = contentDetails.getJSONArray("items") + for (i in 0 until dataArray.length()) { + val element = dataArray.getJSONObject(i) + val snippet = element.getJSONObject("snippet") + var duration = extraDataArray.getJSONObject(i).getJSONObject("contentDetails") + .getString("duration") + duration = formatDuration(duration) + snippet.put("videoID", element.getString("id")) + snippet.put("duration", duration) + fixThumbnail(snippet) + val v = createVideofromJSON(snippet) + if (v == null || v.thumb.isEmpty()) { + continue + } + v.playlistTitle = context.getString(R.string.trendingPlaylist) + items.add(v) + } + return items + } + private fun getTrendingFromPiped(): ArrayList { val url = "$pipedURL/trending?region=$countryCODE" val res = genericArrayRequest(url) @@ -881,7 +916,10 @@ class InfoUtil(private val context: Context) { } fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ - val tempFileDir = File(context.cacheDir.absolutePath + "/downloads/" + downloadItem.id) + val cacheDir = File(context.cacheDir.absolutePath + "/downloads/") + val tempFileDir = File(cacheDir, downloadItem.id.toString()) + tempFileDir.delete() + tempFileDir.mkdirs() val url = downloadItem.url val request = YoutubeDLRequest(url) @@ -925,7 +963,12 @@ class InfoUtil(private val context: Context) { if (sponsorBlockFilters.isNotEmpty()) { val filters = java.lang.String.join(",", sponsorBlockFilters.filter { it.isNotBlank() }) - if (filters.isNotBlank()) request.addOption("--sponsorblock-remove", filters) + if (filters.isNotBlank()) { + request.addOption("--sponsorblock-remove", filters) + if (sharedPreferences.getBoolean("force_keyframes", false)){ + request.addOption("--force-keyframes-at-cuts") + } + } } if(downloadItem.title.isNotBlank()){ @@ -936,7 +979,7 @@ class InfoUtil(private val context: Context) { } request.addCommands(listOf("--replace-in-metadata","uploader"," - Topic$","")) if (downloadItem.customFileNameTemplate.isBlank()) downloadItem.customFileNameTemplate = "%(uploader|${downloadItem.author})s - %(title)s" - downloadItem.customFileNameTemplate.replace("%(uploader)s", "%(uploader|${downloadItem.author})s") + downloadItem.customFileNameTemplate = downloadItem.customFileNameTemplate.replace("%(uploader)s", "%(uploader|${downloadItem.author})s") if (downloadItem.downloadSections.isNotBlank()){ downloadItem.downloadSections.split(";").forEach { @@ -946,7 +989,7 @@ class InfoUtil(private val context: Context) { else request.addOption("--download-sections", it) - if (sharedPreferences.getBoolean("force_keyframes", false)){ + if (sharedPreferences.getBoolean("force_keyframes", false) && !request.hasOption("--force-keyframes-at-cuts")){ request.addOption("--force-keyframes-at-cuts") } } @@ -1019,7 +1062,9 @@ class InfoUtil(private val context: Context) { config.writeText(configData) request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1") request.addOption("--config", config.absolutePath) - } catch (ignored: Exception) {} + } catch (e: Exception) { + e.printStackTrace() + } } } request.addOption("--parse-metadata", "%(release_year,upload_date)s:%(meta_date)s") diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/ThemeUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/ThemeUtil.kt index 12218db8..0b9fb06e 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/ThemeUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/ThemeUtil.kt @@ -48,6 +48,7 @@ object ThemeUtil { "green" -> activity.setTheme(R.style.Theme_Green) "purple" -> activity.setTheme(R.style.Theme_Purple) "yellow" -> activity.setTheme(R.style.Theme_Yellow) + "orange" -> activity.setTheme(R.style.Theme_Orange) "monochrome" -> activity.setTheme(R.style.Theme_Monochrome) } @@ -71,37 +72,41 @@ object ThemeUtil { when (sharedPreferences.getString("ytdlnis_theme", "System")!!) { "System" -> { //set dynamic icon - val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.Default") - activity.packageManager.setComponentEnabledSetting(aliasComponentName, + activity.packageManager.setComponentEnabledSetting( + ComponentName(activity.packageName, "com.deniscerri.ytdlnis.Default"), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP) + PackageManager.DONT_KILL_APP + ) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) } "Light" -> { //set light icon - val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.LightIcon") - activity.packageManager.setComponentEnabledSetting(aliasComponentName, + activity.packageManager.setComponentEnabledSetting( + ComponentName(activity.packageName, "com.deniscerri.ytdlnis.LightIcon"), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP) + PackageManager.DONT_KILL_APP + ) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) } "Dark" -> { //set dark icon - val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.DarkIcon") - activity.packageManager.setComponentEnabledSetting(aliasComponentName, + activity.packageManager.setComponentEnabledSetting( + ComponentName(activity.packageName, "com.deniscerri.ytdlnis.DarkIcon"), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP) + PackageManager.DONT_KILL_APP + ) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES) } else -> { //set dynamic icon - val aliasComponentName = ComponentName(activity, "com.deniscerri.ytdlnis.Default") - activity.packageManager.setComponentEnabledSetting(aliasComponentName, + activity.packageManager.setComponentEnabledSetting( + ComponentName(activity.packageName, "com.deniscerri.ytdlnis.Default"), PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP) + PackageManager.DONT_KILL_APP + ) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) } @@ -109,7 +114,7 @@ object ThemeUtil { } - fun getThemeColor(context: Context, colorCode: Int): Int { + private fun getThemeColor(context: Context, colorCode: Int): Int { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val accent = sharedPreferences.getString("theme_accent", "blue") return if (accent == "Default" || accent == "blue"){ 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 50353729..3ea33b73 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import java.io.File @@ -133,44 +134,47 @@ class DownloadWorker( }.onSuccess { val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao) - var finalPaths : List? - //move file from internal to set download directory - setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads)) - try { - finalPaths = FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p -> - setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads)) - } - if (finalPaths.isNotEmpty()){ - setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id, "log" to logDownloads)) - }else{ + CoroutineScope(Dispatchers.IO).launch { + var finalPaths : List? + //move file from internal to set download directory + setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads)) + try { + finalPaths = withContext(Dispatchers.IO){ + FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p -> + setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id, "log" to logDownloads)) + } + } + + if (finalPaths.isNotEmpty()){ + setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id, "log" to logDownloads)) + }else{ + finalPaths = listOf(context.getString(R.string.unfound_file)) + } + }catch (e: Exception){ finalPaths = listOf(context.getString(R.string.unfound_file)) + e.printStackTrace() + handler.postDelayed({ + Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() + }, 1000) } - }catch (e: Exception){ - finalPaths = listOf(context.getString(R.string.unfound_file)) - e.printStackTrace() - handler.postDelayed({ - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() - }, 1000) - } - //put download in history - val incognito = sharedPreferences.getBoolean("incognito", false) - if (!incognito) { - val unixtime = System.currentTimeMillis() / 1000 - val file = File(finalPaths?.first()!!) - downloadItem.format.filesize = file.length() - val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id) - runBlocking { + //put download in history + val incognito = sharedPreferences.getBoolean("incognito", false) + if (!incognito) { + val unixtime = System.currentTimeMillis() / 1000 + val file = File(finalPaths?.first()!!) + downloadItem.format.filesize = file.length() + val historyItem = HistoryItem(0, downloadItem.url, downloadItem.title, downloadItem.author, downloadItem.duration, downloadItem.thumb, downloadItem.type, unixtime, finalPaths.first() , downloadItem.website, downloadItem.format, downloadItem.id) historyDao.insert(historyItem) } - } - notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) - notificationUtil.createDownloadFinished( - downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths, - NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID - ) + notificationUtil.cancelDownloadNotification(downloadItem.id.toInt()) + notificationUtil.createDownloadFinished( + downloadItem.title, if (finalPaths?.first().equals(context.getString(R.string.unfound_file))) null else finalPaths, + NotificationUtil.DOWNLOAD_FINISHED_CHANNEL_ID + ) + } if (wasQuickDownloaded){ runCatching { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt index e83de8ad..4f61f008 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/TerminalDownloadWorker.kt @@ -116,16 +116,18 @@ class TerminalDownloadWorker( } } }.onSuccess { - //move file from internal to set download directory - try { - FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation!!, false){ p -> - setProgressAsync(workDataOf("progress" to p)) + CoroutineScope(Dispatchers.IO).launch { + //move file from internal to set download directory + try { + FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation!!, false){ p -> + setProgressAsync(workDataOf("progress" to p)) + } + }catch (e: Exception){ + e.printStackTrace() + handler.postDelayed({ + Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() + }, 1000) } - }catch (e: Exception){ - e.printStackTrace() - handler.postDelayed({ - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() - }, 1000) } if (it.out.length > 200){ diff --git a/app/src/main/res/layout/fragment_generic_download_queue.xml b/app/src/main/res/layout/fragment_generic_download_queue.xml index ca57a94d..4f074f04 100644 --- a/app/src/main/res/layout/fragment_generic_download_queue.xml +++ b/app/src/main/res/layout/fragment_generic_download_queue.xml @@ -3,7 +3,7 @@ app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingBottom="200dp" + android:layout_marginBottom="200dp" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/app/src/main/res/navigation/settings_nav.xml b/app/src/main/res/navigation/settings_nav.xml index e7f76d99..edcdc179 100644 --- a/app/src/main/res/navigation/settings_nav.xml +++ b/app/src/main/res/navigation/settings_nav.xml @@ -28,23 +28,18 @@ android:label="MainSettingsFragment" > + app:destination="@id/appearanceSettingsFragment" /> + app:destination="@id/folderSettingsFragment" /> + app:destination="@id/downloadSettingsFragment" /> + app:destination="@id/processingSettingsFragment" /> + app:destination="@id/updateSettingsFragment" /> \ No newline at end of file diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index ac9155b3..441c80d1 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -203,6 +203,35 @@ + + + +