feat: support for multiple audio/subtitle downloads
This commit is contained in:
parent
fd8dd26435
commit
f1d16ba16a
10 changed files with 137 additions and 117 deletions
|
@ -137,7 +137,7 @@ suspend fun <T> tryWithSuspend(
|
||||||
* **/
|
* **/
|
||||||
data class FileUrl(
|
data class FileUrl(
|
||||||
var url: String,
|
var url: String,
|
||||||
val headers: Map<String, String> = mapOf()
|
var headers: Map<String, String> = mapOf()
|
||||||
) : Serializable {
|
) : Serializable {
|
||||||
companion object {
|
companion object {
|
||||||
operator fun get(url: String?, headers: Map<String, String> = mapOf()): FileUrl? {
|
operator fun get(url: String?, headers: Map<String, String> = mapOf()): FileUrl? {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import ani.dantotsu.addons.download.DownloadAddon
|
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.DownloadAddonManager
|
||||||
import ani.dantotsu.addons.download.DownloadLoadResult
|
import ani.dantotsu.addons.download.DownloadLoadResult
|
||||||
import ani.dantotsu.addons.torrent.TorrentAddon
|
import ani.dantotsu.addons.torrent.TorrentAddon
|
||||||
|
@ -101,8 +101,8 @@ class AddonLoader {
|
||||||
}
|
}
|
||||||
|
|
||||||
AddonType.DOWNLOAD -> {
|
AddonType.DOWNLOAD -> {
|
||||||
val extension = instance as? DownloadAddonApi
|
val extension = instance as? DownloadAddonApiV2
|
||||||
?: throw IllegalStateException("Extension is not a DownloadAddonApi")
|
?: throw IllegalStateException("Extension is not a DownloadAddonApiV2")
|
||||||
DownloadLoadResult.Success(
|
DownloadLoadResult.Success(
|
||||||
DownloadAddon.Installed(
|
DownloadAddon.Installed(
|
||||||
name = extName,
|
name = extName,
|
||||||
|
|
|
@ -10,7 +10,7 @@ sealed class DownloadAddon : Addon() {
|
||||||
override val pkgName: String,
|
override val pkgName: String,
|
||||||
override val versionName: String,
|
override val versionName: String,
|
||||||
override val versionCode: Long,
|
override val versionCode: Long,
|
||||||
val extension: DownloadAddonApi,
|
val extension: DownloadAddonApiV2,
|
||||||
val icon: Drawable?,
|
val icon: Drawable?,
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
) : Addon.Installed(name, pkgName, versionName, versionCode)
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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<String, String> = emptyMap(),
|
||||||
|
logCallback: (String) -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun executeFFMpeg(
|
||||||
|
videoUrl: String,
|
||||||
|
downloadPath: String,
|
||||||
|
headers: Map<String, String> = emptyMap(),
|
||||||
|
subtitleUrls: List<Pair<String, String>> = emptyList(),
|
||||||
|
audioUrls: List<Pair<String, String>> = emptyList(),
|
||||||
|
statCallback: (Double) -> Unit
|
||||||
|
): Long
|
||||||
|
|
||||||
|
fun getState(sessionId: Long): String
|
||||||
|
|
||||||
|
fun getStackTrace(sessionId: Long): String?
|
||||||
|
|
||||||
|
fun hadError(sessionId: Long): Boolean
|
||||||
|
}
|
|
@ -28,9 +28,7 @@ import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory
|
||||||
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
|
import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName
|
||||||
import ani.dantotsu.media.Media
|
import ani.dantotsu.media.Media
|
||||||
import ani.dantotsu.media.MediaType
|
import ani.dantotsu.media.MediaType
|
||||||
import ani.dantotsu.media.SubtitleDownloader
|
|
||||||
import ani.dantotsu.media.anime.AnimeWatchFragment
|
import ani.dantotsu.media.anime.AnimeWatchFragment
|
||||||
import ani.dantotsu.parsers.Subtitle
|
|
||||||
import ani.dantotsu.parsers.Video
|
import ani.dantotsu.parsers.Video
|
||||||
import ani.dantotsu.settings.saving.PrefManager
|
import ani.dantotsu.settings.saving.PrefManager
|
||||||
import ani.dantotsu.snackString
|
import ani.dantotsu.snackString
|
||||||
|
@ -227,7 +225,8 @@ class AnimeDownloaderService : Service() {
|
||||||
) ?: throw Exception("Failed to create output directory")
|
) ?: throw Exception("Failed to create output directory")
|
||||||
|
|
||||||
outputDir.findFile("${task.getTaskName()}.mkv")?.delete()
|
outputDir.findFile("${task.getTaskName()}.mkv")?.delete()
|
||||||
val outputFile = outputDir.createFile("video/x-matroska", "${task.getTaskName()}.mkv")
|
val outputFile =
|
||||||
|
outputDir.createFile("video/x-matroska", "${task.getTaskName()}.mkv")
|
||||||
?: throw Exception("Failed to create output file")
|
?: throw Exception("Failed to create output file")
|
||||||
|
|
||||||
var percent = 0
|
var percent = 0
|
||||||
|
@ -236,33 +235,30 @@ class AnimeDownloaderService : Service() {
|
||||||
this@AnimeDownloaderService,
|
this@AnimeDownloaderService,
|
||||||
outputFile.uri
|
outputFile.uri
|
||||||
)
|
)
|
||||||
val headersStringBuilder = StringBuilder()
|
if (!task.video.file.headers.containsKey("User-Agent")
|
||||||
task.video.file.headers.forEach {
|
&& !task.video.file.headers.containsKey("user-agent")
|
||||||
headersStringBuilder.append("\"${it.key}: ${it.value}\"\'\r\n\'")
|
) {
|
||||||
|
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(
|
ffExtension.executeFFProbe(
|
||||||
probeRequest
|
task.video.file.url,
|
||||||
|
task.video.file.headers
|
||||||
) {
|
) {
|
||||||
if (it.toDoubleOrNull() != null) {
|
if (it.toDoubleOrNull() != null) {
|
||||||
totalLength = it.toDouble()
|
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 =
|
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
|
// CALLED WHEN SESSION GENERATES STATISTICS
|
||||||
val timeInMilliseconds = it
|
val timeInMilliseconds = it
|
||||||
if (timeInMilliseconds > 0 && totalLength > 0) {
|
if (timeInMilliseconds > 0 && totalLength > 0) {
|
||||||
|
@ -275,17 +271,6 @@ class AnimeDownloaderService : Service() {
|
||||||
ffTask
|
ffTask
|
||||||
|
|
||||||
saveMediaInfo(task)
|
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
|
// periodically check if the download is complete
|
||||||
while (ffExtension.getState(ffTask) != "COMPLETED") {
|
while (ffExtension.getState(ffTask) != "COMPLETED") {
|
||||||
|
@ -559,7 +544,8 @@ class AnimeDownloaderService : Service() {
|
||||||
val title: String,
|
val title: String,
|
||||||
val episode: String,
|
val episode: String,
|
||||||
val video: Video,
|
val video: Video,
|
||||||
val subtitle: Subtitle? = null,
|
val subtitle: List<Pair<String, String>> = emptyList(),
|
||||||
|
val audio: List<Pair<String, String>> = emptyList(),
|
||||||
val sourceMedia: Media? = null,
|
val sourceMedia: Media? = null,
|
||||||
val episodeImage: String? = null,
|
val episodeImage: String? = null,
|
||||||
val retries: Int = 2,
|
val retries: Int = 2,
|
||||||
|
|
|
@ -47,7 +47,8 @@ object Helper {
|
||||||
title: String,
|
title: String,
|
||||||
episode: String,
|
episode: String,
|
||||||
video: Video,
|
video: Video,
|
||||||
subtitle: Subtitle? = null,
|
subtitle: List<Pair<String, String>> = emptyList(),
|
||||||
|
audio: List<Pair<String, String>> = emptyList(),
|
||||||
sourceMedia: Media? = null,
|
sourceMedia: Media? = null,
|
||||||
episodeImage: String? = null
|
episodeImage: String? = null
|
||||||
) {
|
) {
|
||||||
|
@ -66,6 +67,7 @@ object Helper {
|
||||||
episode,
|
episode,
|
||||||
video,
|
video,
|
||||||
subtitle,
|
subtitle,
|
||||||
|
audio,
|
||||||
sourceMedia,
|
sourceMedia,
|
||||||
episodeImage
|
episodeImage
|
||||||
)
|
)
|
||||||
|
|
|
@ -51,6 +51,7 @@ class SubtitleDownloader {
|
||||||
}
|
}
|
||||||
|
|
||||||
//actually downloads lol
|
//actually downloads lol
|
||||||
|
@Deprecated("handled externally")
|
||||||
suspend fun downloadSubtitle(
|
suspend fun downloadSubtitle(
|
||||||
context: Context,
|
context: Context,
|
||||||
url: String,
|
url: String,
|
||||||
|
|
|
@ -515,7 +515,8 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||||
val selectedVideo =
|
val selectedVideo =
|
||||||
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null
|
||||||
val subtitleNames = subtitles.map { it.language }
|
val subtitleNames = subtitles.map { it.language }
|
||||||
var subtitleToDownload: Subtitle? = null
|
var selectedSubtitles: MutableList<Pair<String, String>> = mutableListOf()
|
||||||
|
var selectedAudioTracks: MutableList<Pair<String, String>> = mutableListOf()
|
||||||
val activity = currActivity() ?: requireActivity()
|
val activity = currActivity() ?: requireActivity()
|
||||||
selectedVideo?.file?.url?.let { url ->
|
selectedVideo?.file?.url?.let { url ->
|
||||||
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
|
if (url.startsWith("magnet:") || url.endsWith(".torrent")) {
|
||||||
|
@ -552,24 +553,16 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (subtitles.isNotEmpty()) {
|
val currContext = currContext() ?: requireContext()
|
||||||
val alertDialog = AlertDialog.Builder(context, R.style.MyPopup)
|
fun go() {
|
||||||
.setTitle(R.string.download_subtitle)
|
|
||||||
.setSingleChoiceItems(
|
|
||||||
subtitleNames.toTypedArray(),
|
|
||||||
-1
|
|
||||||
) { _, which ->
|
|
||||||
subtitleToDownload = subtitles[which]
|
|
||||||
}
|
|
||||||
.setPositiveButton(R.string.download) { _, _ ->
|
|
||||||
dialog?.dismiss()
|
|
||||||
if (selectedVideo != null) {
|
if (selectedVideo != null) {
|
||||||
Helper.startAnimeDownloadService(
|
Helper.startAnimeDownloadService(
|
||||||
activity,
|
activity,
|
||||||
media!!.mainName(),
|
media!!.mainName(),
|
||||||
episode.number,
|
episode.number,
|
||||||
selectedVideo,
|
selectedVideo,
|
||||||
subtitleToDownload,
|
selectedSubtitles,
|
||||||
|
selectedAudioTracks,
|
||||||
media,
|
media,
|
||||||
episode.thumb?.url ?: media!!.banner ?: media!!.cover
|
episode.thumb?.url ?: media!!.banner ?: media!!.cover
|
||||||
)
|
)
|
||||||
|
@ -578,46 +571,72 @@ class SelectorDialogFragment : BottomSheetDialogFragment() {
|
||||||
snackString(R.string.no_video_selected)
|
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, _ ->
|
.setNegativeButton(R.string.skip) { dialog, _ ->
|
||||||
subtitleToDownload = null
|
selectedAudioTracks = mutableListOf()
|
||||||
if (selectedVideo != null) {
|
go()
|
||||||
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()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.setNeutralButton(R.string.cancel) { dialog, _ ->
|
.setNeutralButton(R.string.cancel) { dialog, _ ->
|
||||||
subtitleToDownload = null
|
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()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
.show()
|
.show()
|
||||||
alertDialog.window?.setDimAmount(0.8f)
|
alertDialog.window?.setDimAmount(0.8f)
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
if (selectedVideo != null) {
|
checkAudioTracks()
|
||||||
Helper.startAnimeDownloadService(
|
|
||||||
requireActivity(),
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|
|
@ -977,6 +977,7 @@ Non quae tempore quo provident laudantium qui illo dolor vel quia dolor et exerc
|
||||||
<string name="would_you_like_to_install">Would you like to install it?</string>
|
<string name="would_you_like_to_install">Would you like to install it?</string>
|
||||||
<string name="torrent_addon_not_available">Torrent Add-on not available</string>
|
<string name="torrent_addon_not_available">Torrent Add-on not available</string>
|
||||||
<string name="download_subtitle">Download Subtitle</string>
|
<string name="download_subtitle">Download Subtitle</string>
|
||||||
|
<string name="download_audio_tracks">Download Audio Tracks</string>
|
||||||
<string name="no_video_selected">No video selected</string>
|
<string name="no_video_selected">No video selected</string>
|
||||||
<string name="no_subtitles_available">No subtitles available</string>
|
<string name="no_subtitles_available">No subtitles available</string>
|
||||||
<string name="vote_out_of_total">(%1$s out of %2$s liked this review)</string>
|
<string name="vote_out_of_total">(%1$s out of %2$s liked this review)</string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue