From 01d96cce169a81cc90524151251e275ba8d4ccf3 Mon Sep 17 00:00:00 2001 From: Koen Date: Mon, 13 Nov 2023 15:49:55 +0100 Subject: [PATCH] Made export request folder to export to. --- .../java/com/futo/platformplayer/Settings.kt | 7 ++ .../java/com/futo/platformplayer/Utility.kt | 4 +- .../platformplayer/downloads/VideoExport.kt | 66 ++++++++++--------- .../services/ExportingService.kt | 8 +-- .../futo/platformplayer/states/StateApp.kt | 6 +- .../platformplayer/states/StateDownloads.kt | 5 +- .../viewholders/VideoDownloadViewHolder.kt | 16 ++++- app/src/main/res/values/strings.xml | 2 + 8 files changed, 67 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/Settings.kt b/app/src/main/java/com/futo/platformplayer/Settings.kt index 38380fb7..b3a2d35a 100644 --- a/app/src/main/java/com/futo/platformplayer/Settings.kt +++ b/app/src/main/java/com/futo/platformplayer/Settings.kt @@ -534,6 +534,13 @@ class Settings : FragmentedStorageFileJson() { StateApp.instance.changeExternalDownloadDirectory(it); } } + + @FormField(R.string.clear_external_downloads_directory, FieldForm.BUTTON, R.string.clear_the_external_storage_for_download_files, 5) + fun clearStorageDownload() { + Settings.instance.storage.storage_download = null; + Settings.instance.save(); + SettingsActivity.getActivity()?.let { UIDialogs.toast(it, "Cleared download storage directory") }; + } } diff --git a/app/src/main/java/com/futo/platformplayer/Utility.kt b/app/src/main/java/com/futo/platformplayer/Utility.kt index 3d93437c..0e57a220 100644 --- a/app/src/main/java/com/futo/platformplayer/Utility.kt +++ b/app/src/main/java/com/futo/platformplayer/Utility.kt @@ -164,9 +164,7 @@ fun Int.sp(resources: Resources): Int { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, this.toFloat(), resources.displayMetrics).toInt() } -fun File.share(context: Context) { - val uri = FileProvider.getUriForFile(context, context.resources.getString(R.string.authority), this); - +fun DocumentFile.share(context: Context) { val shareIntent = Intent(); shareIntent.action = Intent.ACTION_SEND; shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt index 987dd8c6..37f1395f 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt @@ -1,13 +1,18 @@ package com.futo.platformplayer.downloads +import android.content.Context +import android.net.Uri import android.os.Environment +import androidx.documentfile.provider.DocumentFile import com.arthenica.ffmpegkit.* import com.futo.platformplayer.api.media.models.streams.sources.* import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.logging.Logger +import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.toHumanBitrate import kotlinx.coroutines.* import java.io.* +import java.util.UUID import java.util.concurrent.CancellationException import java.util.concurrent.Executors import kotlin.coroutines.resumeWithException @@ -43,7 +48,7 @@ class VideoExport { this.subtitleSource = subtitleSource; } - suspend fun export(onProgress: ((Double) -> Unit)? = null): File = coroutineScope { + suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope { if(isCancelled) throw CancellationException("Export got cancelled"); val v = videoSource; @@ -55,34 +60,47 @@ class VideoExport { if (a != null) sourceCount++; if (s != null) sourceCount++; - var outputFile: File? = null; - val moviesRoot = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES); - val musicRoot = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC); - val moviesGrayjay = File(moviesRoot, "Grayjay"); - val musicGrayjay = File(musicRoot, "Grayjay"); - if(!moviesGrayjay.exists()) - moviesGrayjay.mkdirs(); - if(!musicGrayjay.exists()) - musicGrayjay.mkdirs(); - + val outputFile: DocumentFile?; + val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set"); if (sourceCount > 1) { val outputFileName = toSafeFileName(videoLocal.name) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container); - val f = File(moviesGrayjay, outputFileName); + val f = downloadRoot.createFile("video/mp4", outputFileName) + ?: throw Exception("Failed to create file in external directory."); Logger.i(TAG, "Combining video and audio through FFMPEG."); - combine(a?.filePath, v?.filePath, s?.filePath, f.absolutePath, videoLocal.duration.toDouble()) { progress -> onProgress?.invoke(progress) }; + val tempFile = File(context.cacheDir, "${UUID.randomUUID()}.mp4"); + try { + combine(a?.filePath, v?.filePath, s?.filePath, tempFile.absolutePath, videoLocal.duration.toDouble()) { progress -> onProgress?.invoke(progress) }; + context.contentResolver.openOutputStream(f.uri)?.use { outputStream -> + copy(tempFile.absolutePath, outputStream) { progress -> onProgress?.invoke(progress) }; + } + } finally { + tempFile.delete(); + } outputFile = f; } else if (v != null) { val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.videoContainerToExtension(v.container); - val f = File(moviesGrayjay, outputFileName); + val f = downloadRoot.createFile(v.container, outputFileName) + ?: throw Exception("Failed to create file in external directory."); + Logger.i(TAG, "Copying video."); - copy(v.filePath, f.absolutePath) { progress -> onProgress?.invoke(progress) }; + + context.contentResolver.openOutputStream(f.uri)?.use { outputStream -> + copy(v.filePath, outputStream) { progress -> onProgress?.invoke(progress) }; + } + outputFile = f; } else if (a != null) { val outputFileName = toSafeFileName(videoLocal.name) + "." + VideoDownload.audioContainerToExtension(a.container); - val f = File(musicGrayjay, outputFileName); + val f = downloadRoot.createFile(a.container, outputFileName) + ?: throw Exception("Failed to create file in external directory."); + Logger.i(TAG, "Copying audio."); - copy(a.filePath, f.absolutePath) { progress -> onProgress?.invoke(progress) }; + + context.contentResolver.openOutputStream(f.uri)?.use { outputStream -> + copy(a.filePath, outputStream) { progress -> onProgress?.invoke(progress) }; + } + outputFile = f; } else { throw Exception("Cannot export when no audio or video source is set."); @@ -179,10 +197,9 @@ class VideoExport { } } - private suspend fun copy(fromPath: String, toPath: String, bufferSize: Int = 8192, onProgress: ((Double) -> Unit)? = null) { + private suspend fun copy(fromPath: String, outputStream: OutputStream, bufferSize: Int = 8192, onProgress: ((Double) -> Unit)? = null) { withContext(Dispatchers.IO) { var inputStream: FileInputStream? = null - var outputStream: FileOutputStream? = null try { val srcFile = File(fromPath) @@ -190,17 +207,7 @@ class VideoExport { throw IOException("Source file not found.") } - val dstFile = File(toPath) - val parentDir = dstFile.parentFile ?: throw IOException("Non existent parent dir.") - - if (!parentDir.exists()) { - if (!parentDir.mkdirs()) { - throw IOException("Failed to create destination directory.") - } - } - inputStream = FileInputStream(srcFile) - outputStream = FileOutputStream(dstFile) val buffer = ByteArray(bufferSize) val totalBytes = srcFile.length() @@ -221,7 +228,6 @@ class VideoExport { throw IOException("Error occurred while copying file: ${e.message}", e) } finally { inputStream?.close() - outputStream?.close() } } } diff --git a/app/src/main/java/com/futo/platformplayer/services/ExportingService.kt b/app/src/main/java/com/futo/platformplayer/services/ExportingService.kt index 7593c8b0..659d222a 100644 --- a/app/src/main/java/com/futo/platformplayer/services/ExportingService.kt +++ b/app/src/main/java/com/futo/platformplayer/services/ExportingService.kt @@ -107,7 +107,7 @@ class ExportingService : Service() { { try{ notifyExport(currentExport); - doExport(currentExport); + doExport(applicationContext, currentExport); } catch(ex: Throwable) { Logger.e(TAG, "Failed export [${currentExport.videoLocal.name}]: ${ex.message}", ex); @@ -125,13 +125,13 @@ class ExportingService : Service() { stopService(this); } - private suspend fun doExport(export: VideoExport) { + private suspend fun doExport(context: Context, export: VideoExport) { Logger.i(TAG, "Exporting [${export.videoLocal.name}] started"); export.changeState(VideoExport.State.EXPORTING); var lastNotifyTime: Long = 0L; - val file = export.export { progress -> + val file = export.export(context) { progress -> export.progress = progress; val currentTime = System.currentTimeMillis(); @@ -146,7 +146,7 @@ class ExportingService : Service() { notifyExport(export); withContext(Dispatchers.Main) { - StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.path}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") { + StateAnnouncement.instance.registerAnnouncement(UUID.randomUUID().toString(), "File exported", "Exported [${file.uri}]", AnnouncementType.SESSION, time = null, category = "download", actionButton = "Open") { file.share(this@ExportingService); }; } diff --git a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt index e6d34609..c30a4311 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateApp.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateApp.kt @@ -111,17 +111,13 @@ class StateApp { return null; } fun changeExternalDownloadDirectory(context: IWithResultLauncher, onChanged: ((DocumentFile?)->Unit)? = null) { - - scopeOrNull?.launch(Dispatchers.Main) { - UIDialogs.toast("External download directory not yet used by export (WIP)"); - }; if(context is Context) requestDirectoryAccess(context, "Download Exports", "This directory is used to export downloads to for external usage.", null) { if(it != null) context.contentResolver.takePersistableUriPermission(it, Intent.FLAG_GRANT_WRITE_URI_PERMISSION.or(Intent.FLAG_GRANT_READ_URI_PERMISSION)); if(it != null && isValidStorageUri(context, it)) { Logger.i(TAG, "Changed external download directory: ${it}"); - Settings.instance.storage.storage_general = it.toString(); + Settings.instance.storage.storage_download = it.toString(); Settings.instance.save(); onChanged?.invoke(getExternalDownloadDirectory(context)); diff --git a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt index fa017213..f6cbd9b7 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt @@ -400,10 +400,7 @@ class StateDownloads { _exporting.save(videoExport); if(notify) { - if(videoSource == null) - UIDialogs.toast("Exporting [${shortName}]\nIn your music directory under Grayjay"); - else - UIDialogs.toast("Exporting [${shortName}]\nIn your movies directory under Grayjay"); + UIDialogs.toast("Exporting [${shortName}]"); StateApp.withContext { ExportingService.getOrCreateService(it) }; onExportsChanged.emit(); } diff --git a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt index 3fd9a3cd..a8d58d5b 100644 --- a/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt +++ b/app/src/main/java/com/futo/platformplayer/views/adapters/viewholders/VideoDownloadViewHolder.kt @@ -1,5 +1,6 @@ package com.futo.platformplayer.views.adapters.viewholders +import android.app.Activity import android.view.LayoutInflater import android.view.ViewGroup import android.widget.ImageButton @@ -7,9 +8,11 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import com.futo.platformplayer.* +import com.futo.platformplayer.activities.MainActivity import com.futo.platformplayer.constructs.Event1 import com.futo.platformplayer.downloads.VideoLocal import com.futo.platformplayer.images.GlideHelper.Companion.loadThumbnails +import com.futo.platformplayer.states.StateApp import com.futo.platformplayer.states.StateDownloads import com.futo.platformplayer.states.StatePlayer import com.futo.platformplayer.views.adapters.AnyAdapter @@ -47,7 +50,18 @@ class VideoDownloadViewHolder(_viewGroup: ViewGroup) : AnyAdapter.AnyViewHolder< } _videoExport.setOnClickListener { val v = _video ?: return@setOnClickListener; - StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); + if (StateApp.instance.getExternalDownloadDirectory(_view.context) == null) { + StateApp.instance.changeExternalDownloadDirectory(_view.context as MainActivity) { + if (it == null) { + UIDialogs.toast(_view.context, "Download directory must be set to export."); + return@changeExternalDownloadDirectory; + } + + StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); + }; + } else { + StateDownloads.instance.export(v, v.videoSource.firstOrNull(), v.audioSource.firstOrNull(), v.subtitlesSources.firstOrNull()); + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7aa634f2..f55cf32e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -278,9 +278,11 @@ Casting Change behavior of the player Change external Downloads directory + Clear external Downloads directory Change external General directory Change tabs visible on the home screen Change the external directory for general files + Clear the external storage for download files Change the external storage for download files Clear Cookies Clear Cookies on Logout