From f1d16ba16a37c9aca5b80a0ad76071ead5a32440 Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Thu, 16 May 2024 14:51:35 -0500 Subject: [PATCH] feat: support for multiple audio/subtitle downloads --- app/src/main/java/ani/dantotsu/Network.kt | 2 +- .../java/ani/dantotsu/addons/AddonLoader.kt | 6 +- .../dantotsu/addons/download/DownloadAddon.kt | 2 +- .../addons/download/DownloadAddonApi.kt | 21 --- .../addons/download/DownloadAddonApiV2.kt | 32 +++++ .../download/anime/AnimeDownloaderService.kt | 56 +++----- .../ani/dantotsu/download/video/Helper.kt | 4 +- .../ani/dantotsu/media/SubtitleDownloader.kt | 1 + .../media/anime/SelectorDialogFragment.kt | 129 ++++++++++-------- app/src/main/res/values/strings.xml | 1 + 10 files changed, 137 insertions(+), 117 deletions(-) delete mode 100644 app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApi.kt create mode 100644 app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApiV2.kt diff --git a/app/src/main/java/ani/dantotsu/Network.kt b/app/src/main/java/ani/dantotsu/Network.kt index 1c26b3bb..fbfc399b 100644 --- a/app/src/main/java/ani/dantotsu/Network.kt +++ b/app/src/main/java/ani/dantotsu/Network.kt @@ -137,7 +137,7 @@ suspend fun tryWithSuspend( * **/ data class FileUrl( var url: String, - val headers: Map = mapOf() + var headers: Map = mapOf() ) : Serializable { companion object { operator fun get(url: String?, headers: Map = mapOf()): FileUrl? { diff --git a/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt b/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt index a6da3011..25681b94 100644 --- a/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt +++ b/app/src/main/java/ani/dantotsu/addons/AddonLoader.kt @@ -6,7 +6,7 @@ import android.content.pm.PackageManager import android.os.Build import androidx.core.content.pm.PackageInfoCompat import ani.dantotsu.addons.download.DownloadAddon -import ani.dantotsu.addons.download.DownloadAddonApi +import ani.dantotsu.addons.download.DownloadAddonApiV2 import ani.dantotsu.addons.download.DownloadAddonManager import ani.dantotsu.addons.download.DownloadLoadResult import ani.dantotsu.addons.torrent.TorrentAddon @@ -101,8 +101,8 @@ class AddonLoader { } AddonType.DOWNLOAD -> { - val extension = instance as? DownloadAddonApi - ?: throw IllegalStateException("Extension is not a DownloadAddonApi") + val extension = instance as? DownloadAddonApiV2 + ?: throw IllegalStateException("Extension is not a DownloadAddonApiV2") DownloadLoadResult.Success( DownloadAddon.Installed( name = extName, diff --git a/app/src/main/java/ani/dantotsu/addons/download/DownloadAddon.kt b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddon.kt index 4409206d..ee89d7c5 100644 --- a/app/src/main/java/ani/dantotsu/addons/download/DownloadAddon.kt +++ b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddon.kt @@ -10,7 +10,7 @@ sealed class DownloadAddon : Addon() { override val pkgName: String, override val versionName: String, override val versionCode: Long, - val extension: DownloadAddonApi, + val extension: DownloadAddonApiV2, val icon: Drawable?, val hasUpdate: Boolean = false, ) : Addon.Installed(name, pkgName, versionName, versionCode) diff --git a/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApi.kt b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApi.kt deleted file mode 100644 index c786387e..00000000 --- a/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApi.kt +++ /dev/null @@ -1,21 +0,0 @@ -package ani.dantotsu.addons.download - -import android.content.Context -import android.net.Uri - -interface DownloadAddonApi { - - fun cancelDownload(sessionId: Long) - - fun setDownloadPath(context: Context, uri: Uri): String - - suspend fun executeFFProbe(request: String, logCallback: (String) -> Unit) - - suspend fun executeFFMpeg(request: String, statCallback: (Double) -> Unit): Long - - fun getState(sessionId: Long): String - - fun getStackTrace(sessionId: Long): String? - - fun hadError(sessionId: Long): Boolean -} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApiV2.kt b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApiV2.kt new file mode 100644 index 00000000..b12f4bee --- /dev/null +++ b/app/src/main/java/ani/dantotsu/addons/download/DownloadAddonApiV2.kt @@ -0,0 +1,32 @@ +package ani.dantotsu.addons.download + +import android.content.Context +import android.net.Uri + +interface DownloadAddonApiV2 { + + fun cancelDownload(sessionId: Long) + + fun setDownloadPath(context: Context, uri: Uri): String + + suspend fun executeFFProbe( + videoUrl: String, + headers: Map = emptyMap(), + logCallback: (String) -> Unit + ) + + suspend fun executeFFMpeg( + videoUrl: String, + downloadPath: String, + headers: Map = emptyMap(), + subtitleUrls: List> = emptyList(), + audioUrls: List> = emptyList(), + statCallback: (Double) -> Unit + ): Long + + fun getState(sessionId: Long): String + + fun getStackTrace(sessionId: Long): String? + + fun hadError(sessionId: Long): Boolean +} diff --git a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt index 0c0e1fdd..ac631b90 100644 --- a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt @@ -28,9 +28,7 @@ import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName import ani.dantotsu.media.Media import ani.dantotsu.media.MediaType -import ani.dantotsu.media.SubtitleDownloader import ani.dantotsu.media.anime.AnimeWatchFragment -import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.Video import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.snackString @@ -227,8 +225,9 @@ class AnimeDownloaderService : Service() { ) ?: throw Exception("Failed to create output directory") outputDir.findFile("${task.getTaskName()}.mkv")?.delete() - val outputFile = outputDir.createFile("video/x-matroska", "${task.getTaskName()}.mkv") - ?: throw Exception("Failed to create output file") + val outputFile = + outputDir.createFile("video/x-matroska", "${task.getTaskName()}.mkv") + ?: throw Exception("Failed to create output file") var percent = 0 var totalLength = 0.0 @@ -236,33 +235,30 @@ class AnimeDownloaderService : Service() { this@AnimeDownloaderService, outputFile.uri ) - val headersStringBuilder = StringBuilder() - task.video.file.headers.forEach { - headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'") + if (!task.video.file.headers.containsKey("User-Agent") + && !task.video.file.headers.containsKey("user-agent") + ) { + val newHeaders = task.video.file.headers.toMutableMap() + newHeaders["User-Agent"] = defaultHeaders["User-Agent"]!! + task.video.file.headers = newHeaders } - if (!task.video.file.headers.containsKey("User-Agent")) { //headers should never be empty now - headersStringBuilder.append("\"").append("User-Agent: ") - .append(defaultHeaders["User-Agent"]).append("\"\'\r\n\'") - } - val probeRequest = - "-headers $headersStringBuilder -i \"${task.video.file.url}\" -show_entries format=duration -v quiet -of csv=\"p=0\"" + ffExtension.executeFFProbe( - probeRequest + task.video.file.url, + task.video.file.headers ) { if (it.toDoubleOrNull() != null) { totalLength = it.toDouble() } } - - val headers = headersStringBuilder.toString() - var request = "-headers $headers " - request += "-i \"${task.video.file.url}\" -c copy -map 0:v -map 0:a -map 0:s?" + - " -f matroska -timeout 600 -reconnect 1" + - " -reconnect_streamed 1 -allowed_extensions ALL " + - "-tls_verify 0 $path -v trace" - Logger.log("Request: $request") val ffTask = - ffExtension.executeFFMpeg(request) { + ffExtension.executeFFMpeg( + task.video.file.url, + path, + task.video.file.headers, + task.subtitle, + task.audio, + ) { // CALLED WHEN SESSION GENERATES STATISTICS val timeInMilliseconds = it if (timeInMilliseconds > 0 && totalLength > 0) { @@ -275,17 +271,6 @@ class AnimeDownloaderService : Service() { ffTask saveMediaInfo(task) - task.subtitle?.let { - SubtitleDownloader.downloadSubtitle( - this@AnimeDownloaderService, - it.file.url, - DownloadedType( - task.title, - task.episode, - MediaType.ANIME, - ) - ) - } // periodically check if the download is complete while (ffExtension.getState(ffTask) != "COMPLETED") { @@ -559,7 +544,8 @@ class AnimeDownloaderService : Service() { val title: String, val episode: String, val video: Video, - val subtitle: Subtitle? = null, + val subtitle: List> = emptyList(), + val audio: List> = emptyList(), val sourceMedia: Media? = null, val episodeImage: String? = null, val retries: Int = 2, diff --git a/app/src/main/java/ani/dantotsu/download/video/Helper.kt b/app/src/main/java/ani/dantotsu/download/video/Helper.kt index 463cfa91..b207d634 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -47,7 +47,8 @@ object Helper { title: String, episode: String, video: Video, - subtitle: Subtitle? = null, + subtitle: List> = emptyList(), + audio: List> = emptyList(), sourceMedia: Media? = null, episodeImage: String? = null ) { @@ -66,6 +67,7 @@ object Helper { episode, video, subtitle, + audio, sourceMedia, episodeImage ) diff --git a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt index 2519afb8..9d834a0c 100644 --- a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt +++ b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt @@ -51,6 +51,7 @@ class SubtitleDownloader { } //actually downloads lol + @Deprecated("handled externally") suspend fun downloadSubtitle( context: Context, url: String, diff --git a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt index 1519eb06..e93267e2 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt @@ -515,7 +515,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { val selectedVideo = if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null val subtitleNames = subtitles.map { it.language } - var subtitleToDownload: Subtitle? = null + var selectedSubtitles: MutableList> = mutableListOf() + var selectedAudioTracks: MutableList> = mutableListOf() val activity = currActivity() ?: requireActivity() selectedVideo?.file?.url?.let { url -> if (url.startsWith("magnet:") || url.endsWith(".torrent")) { @@ -552,65 +553,16 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { } } } - if (subtitles.isNotEmpty()) { - val alertDialog = AlertDialog.Builder(context, R.style.MyPopup) - .setTitle(R.string.download_subtitle) - .setSingleChoiceItems( - subtitleNames.toTypedArray(), - -1 - ) { _, which -> - subtitleToDownload = subtitles[which] - } - .setPositiveButton(R.string.download) { _, _ -> - dialog?.dismiss() - if (selectedVideo != null) { - Helper.startAnimeDownloadService( - activity, - media!!.mainName(), - episode.number, - selectedVideo, - subtitleToDownload, - media, - episode.thumb?.url ?: media!!.banner ?: media!!.cover - ) - broadcastDownloadStarted(episode.number, activity) - } else { - snackString(R.string.no_video_selected) - } - } - .setNegativeButton(R.string.skip) { dialog, _ -> - subtitleToDownload = null - if (selectedVideo != null) { - Helper.startAnimeDownloadService( - currActivity()!!, - media!!.mainName(), - episode.number, - selectedVideo, - subtitleToDownload, - media, - episode.thumb?.url ?: media!!.banner ?: media!!.cover - ) - broadcastDownloadStarted(episode.number, activity) - } else { - snackString(R.string.no_video_selected) - } - dialog.dismiss() - } - .setNeutralButton(R.string.cancel) { dialog, _ -> - subtitleToDownload = null - dialog.dismiss() - } - .show() - alertDialog.window?.setDimAmount(0.8f) - - } else { + val currContext = currContext() ?: requireContext() + fun go() { if (selectedVideo != null) { Helper.startAnimeDownloadService( - requireActivity(), + activity, media!!.mainName(), episode.number, selectedVideo, - subtitleToDownload, + selectedSubtitles, + selectedAudioTracks, media, episode.thumb?.url ?: media!!.banner ?: media!!.cover ) @@ -619,6 +571,73 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { snackString(R.string.no_video_selected) } } + fun checkAudioTracks() { + val audioTracks = extractor.audioTracks.map { it.lang } + if (audioTracks.isNotEmpty()) { + val audioNamesArray = audioTracks.toTypedArray() + val checkedItems = BooleanArray(audioNamesArray.size) { false } + val alertDialog = AlertDialog.Builder(currContext, R.style.MyPopup) + .setTitle(R.string.download_audio_tracks) + .setMultiChoiceItems(audioNamesArray, checkedItems) { _, which, isChecked -> + val audioPair = Pair(extractor.audioTracks[which].url, extractor.audioTracks[which].lang) + if (isChecked) { + selectedAudioTracks.add(audioPair) + } else { + selectedAudioTracks.remove(audioPair) + } + } + .setPositiveButton(R.string.download) { _, _ -> + dialog?.dismiss() + go() + } + .setNegativeButton(R.string.skip) { dialog, _ -> + selectedAudioTracks = mutableListOf() + go() + dialog.dismiss() + } + .setNeutralButton(R.string.cancel) { dialog, _ -> + selectedAudioTracks = mutableListOf() + dialog.dismiss() + } + .show() + alertDialog.window?.setDimAmount(0.8f) + } else { + go() + } + } + if (subtitles.isNotEmpty()) { + val subtitleNamesArray = subtitleNames.toTypedArray() + val checkedItems = BooleanArray(subtitleNamesArray.size) { false } + + val alertDialog = AlertDialog.Builder(currContext, R.style.MyPopup) + .setTitle(R.string.download_subtitle) + .setMultiChoiceItems(subtitleNamesArray, checkedItems) { _, which, isChecked -> + val subtitlePair = Pair(subtitles[which].file.url, subtitles[which].language) + if (isChecked) { + selectedSubtitles.add(subtitlePair) + } else { + selectedSubtitles.remove(subtitlePair) + } + } + .setPositiveButton(R.string.download) { _, _ -> + dialog?.dismiss() + checkAudioTracks() + } + .setNegativeButton(R.string.skip) { dialog, _ -> + selectedSubtitles = mutableListOf() + checkAudioTracks() + dialog.dismiss() + } + .setNeutralButton(R.string.cancel) { dialog, _ -> + selectedSubtitles = mutableListOf() + dialog.dismiss() + } + .show() + alertDialog.window?.setDimAmount(0.8f) + + } else { + checkAudioTracks() + } } dismiss() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index be1a853f..29ede87c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -977,6 +977,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc Would you like to install it? Torrent Add-on not available Download Subtitle + Download Audio Tracks No video selected No subtitles available (%1$s out of %2$s liked this review)