From 4dce8d6a803a2e720a28923f8e42cb279f784fa6 Mon Sep 17 00:00:00 2001 From: Kelvin Date: Tue, 11 Feb 2025 20:31:26 +0100 Subject: [PATCH] Export playlist support --- .../platformplayer/downloads/VideoDownload.kt | 4 +- .../platformplayer/downloads/VideoExport.kt | 4 +- .../mainactivity/main/PlaylistFragment.kt | 10 ++++ .../mainactivity/main/VideoListEditorView.kt | 10 ++++ .../platformplayer/states/StateDownloads.kt | 54 ++++++++++++++++++- .../res/layout/fragment_video_list_editor.xml | 16 ++++++ 6 files changed, 92 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt index cd4ae885..ede24707 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoDownload.kt @@ -375,8 +375,8 @@ class VideoDownload { else throw DownloadException("Could not find a valid video or audio source for download") if(asource is JSSource) { - this.hasVideoRequestExecutor = this.hasVideoRequestExecutor || asource.hasRequestExecutor; - this.requiresLiveVideoSource = this.hasVideoRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate); + this.hasAudioRequestExecutor = this.hasAudioRequestExecutor || asource.hasRequestExecutor; + this.requiresLiveAudioSource = this.hasAudioRequestExecutor || (asource is JSDashManifestRawSource && asource.hasGenerate); } if(asource == null) { 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 7c1c4e09..a4615822 100644 --- a/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt +++ b/app/src/main/java/com/futo/platformplayer/downloads/VideoExport.kt @@ -39,7 +39,7 @@ class VideoExport { this.subtitleSource = subtitleSource; } - suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null): DocumentFile = coroutineScope { + suspend fun export(context: Context, onProgress: ((Double) -> Unit)? = null, documentRoot: DocumentFile? = null): DocumentFile = coroutineScope { val v = videoSource; val a = audioSource; val s = subtitleSource; @@ -50,7 +50,7 @@ class VideoExport { if (s != null) sourceCount++; val outputFile: DocumentFile?; - val downloadRoot = StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set"); + val downloadRoot = documentRoot ?: StateApp.instance.getExternalDownloadDirectory(context) ?: throw Exception("External download directory is not set"); if (sourceCount > 1) { val outputFileName = videoLocal.name.sanitizeFileName(true) + ".mp4"// + VideoDownload.videoContainerToExtension(v.container); val f = downloadRoot.createFile("video/mp4", outputFileName) diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt index 81669c73..b58e3ee2 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/PlaylistFragment.kt @@ -8,6 +8,7 @@ import android.view.ViewGroup import androidx.core.app.ShareCompat import androidx.lifecycle.lifecycleScope import com.futo.platformplayer.* +import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylist import com.futo.platformplayer.api.media.models.playlists.IPlatformPlaylistDetails import com.futo.platformplayer.api.media.models.video.IPlatformVideo @@ -78,6 +79,14 @@ class PlaylistFragment : MainFragment() { val nameInput = SlideUpMenuTextInput(context, context.getString(R.string.name)); val editPlaylistOverlay = SlideUpMenuOverlay(context, overlayContainer, context.getString(R.string.edit_playlist), context.getString(R.string.ok), false, nameInput); + _buttonExport.setOnClickListener { + _playlist?.let { + val context = StateApp.instance.contextOrNull ?: return@let; + if(context is IWithResultLauncher) + StateDownloads.instance.exportPlaylist(context, it.id); + } + }; + _buttonDownload.visibility = View.VISIBLE; editPlaylistOverlay.onOK.subscribe { val text = nameInput.text; @@ -176,6 +185,7 @@ class PlaylistFragment : MainFragment() { setVideos(parameter.videos, true) setMetadata(parameter.videos.size, parameter.videos.sumOf { it.duration }) setButtonDownloadVisible(true) + setButtonExportVisible(false) setButtonEditVisible(true) if (!StatePlaylists.instance.playlistStore.hasItem { it.id == parameter.id }) { diff --git a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt index 2e355fa4..b458a093 100644 --- a/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt +++ b/app/src/main/java/com/futo/platformplayer/fragment/mainactivity/main/VideoListEditorView.kt @@ -34,6 +34,7 @@ abstract class VideoListEditorView : LinearLayout { protected var overlayContainer: FrameLayout private set; protected var _buttonDownload: ImageButton; + protected var _buttonExport: ImageButton; private var _buttonShare: ImageButton; private var _buttonEdit: ImageButton; @@ -54,6 +55,8 @@ abstract class VideoListEditorView : LinearLayout { _buttonEdit = findViewById(R.id.button_edit); _buttonDownload = findViewById(R.id.button_download); _buttonDownload.visibility = View.GONE; + _buttonExport = findViewById(R.id.button_export); + _buttonExport.visibility = View.GONE; _buttonShare = findViewById(R.id.button_share); val onShare = _onShare; @@ -68,6 +71,7 @@ abstract class VideoListEditorView : LinearLayout { buttonShuffle.setOnClickListener { onShuffleClick(); }; _buttonEdit.setOnClickListener { onEditClick(); }; + setButtonExportVisible(false); setButtonDownloadVisible(canEdit()); videoListEditorView.onVideoOrderChanged.subscribe(::onVideoOrderChanged); @@ -108,6 +112,7 @@ abstract class VideoListEditorView : LinearLayout { _buttonDownload.setBackgroundResource(R.drawable.background_button_round); if(isDownloading) { + setButtonExportVisible(false); _buttonDownload.setImageResource(R.drawable.ic_loader_animated); _buttonDownload.drawable.assume { it.start() }; _buttonDownload.setOnClickListener { @@ -117,6 +122,7 @@ abstract class VideoListEditorView : LinearLayout { } } else if(isDownloaded) { + setButtonExportVisible(true) _buttonDownload.setImageResource(R.drawable.ic_download_off); _buttonDownload.setOnClickListener { UIDialogs.showConfirmationDialog(context, context.getString(R.string.are_you_sure_you_want_to_delete_the_downloaded_videos), { @@ -125,6 +131,7 @@ abstract class VideoListEditorView : LinearLayout { } } else { + setButtonExportVisible(false); _buttonDownload.setImageResource(R.drawable.ic_download); _buttonDownload.setOnClickListener { onDownload(); @@ -171,6 +178,9 @@ abstract class VideoListEditorView : LinearLayout { protected fun setButtonDownloadVisible(isVisible: Boolean) { _buttonDownload.visibility = if (isVisible) View.VISIBLE else View.GONE; } + protected fun setButtonExportVisible(isVisible: Boolean) { + _buttonExport.visibility = if (isVisible) View.VISIBLE else View.GONE; + } protected fun setButtonEditVisible(isVisible: Boolean) { _buttonEdit.visibility = if (isVisible) View.VISIBLE else View.GONE; 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 4bfeae7b..5bba7e77 100644 --- a/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt +++ b/app/src/main/java/com/futo/platformplayer/states/StateDownloads.kt @@ -3,9 +3,11 @@ package com.futo.platformplayer.states import android.content.ContentResolver import android.content.Context import android.os.StatFs +import androidx.documentfile.provider.DocumentFile import com.futo.platformplayer.R import com.futo.platformplayer.Settings import com.futo.platformplayer.UIDialogs +import com.futo.platformplayer.activities.IWithResultLauncher import com.futo.platformplayer.api.http.ManagedHttpClient import com.futo.platformplayer.api.media.PlatformID import com.futo.platformplayer.api.media.models.streams.sources.IAudioSource @@ -466,6 +468,54 @@ class StateDownloads { return _downloadsDirectory; } + fun exportPlaylist(context: Context, playlistId: String) { + if(context is IWithResultLauncher) + StateApp.instance.requestDirectoryAccess(context, "Export Playlist", "To export playlist to directory", null) { + if (it == null) + return@requestDirectoryAccess; + + val root = DocumentFile.fromTreeUri(context, it!!); + + val localVideos = StateDownloads.instance.getDownloadedVideosPlaylist(playlistId) + + var lastNotifyTime = -1L; + + UIDialogs.showDialogProgress(context) { + StateApp.instance.scopeOrNull?.launch(Dispatchers.IO) { + it.setText("Exporting videos.."); + var i = 0; + for (video in localVideos) { + withContext(Dispatchers.Main) { + it.setText("Exporting videos...(${i}/${localVideos.size})"); + //it.setProgress(i.toDouble() / localVideos.size); + } + + try { + val export = VideoExport(video, video.videoSource.firstOrNull(), video.audioSource.firstOrNull(), video.subtitlesSources.firstOrNull()); + Logger.i(TAG, "Exporting [${export.videoLocal.name}] started"); + + val file = export.export(context, { progress -> + val now = System.currentTimeMillis(); + if (lastNotifyTime == -1L || now - lastNotifyTime > 100) { + it.setProgress(progress); + lastNotifyTime = now; + } + }, root); + } catch(ex: Throwable) { + Logger.e(TAG, "Failed export [${video.name}]: ${ex.message}", ex); + } + i++; + } + withContext(Dispatchers.Main) { + it.setProgress(1f); + it.dismiss(); + UIDialogs.appToast("Finished exporting playlist"); + } + }; + } + } + } + fun export(context: Context, videoLocal: VideoLocal, videoSource: LocalVideoSource?, audioSource: LocalAudioSource?, subtitleSource: LocalSubtitleSource?) { var lastNotifyTime = -1L; @@ -477,13 +527,13 @@ class StateDownloads { try { Logger.i(TAG, "Exporting [${export.videoLocal.name}] started"); - val file = export.export(context) { progress -> + val file = export.export(context, { progress -> val now = System.currentTimeMillis(); if (lastNotifyTime == -1L || now - lastNotifyTime > 100) { it.setProgress(progress); lastNotifyTime = now; } - } + }, null); withContext(Dispatchers.Main) { it.setProgress(100.0f) diff --git a/app/src/main/res/layout/fragment_video_list_editor.xml b/app/src/main/res/layout/fragment_video_list_editor.xml index 0e636c6f..a906421b 100644 --- a/app/src/main/res/layout/fragment_video_list_editor.xml +++ b/app/src/main/res/layout/fragment_video_list_editor.xml @@ -54,6 +54,22 @@ +