From 87a9df4c12195edd28cadf54ace4c7fc6019db5e Mon Sep 17 00:00:00 2001 From: rebelonion <87634197+rebelonion@users.noreply.github.com> Date: Fri, 19 Jan 2024 01:49:24 -0600 Subject: [PATCH] I FUCKING HATE EXOPLAYER SUBTITLES --- .../ani/dantotsu/download/DownloadsManager.kt | 56 +++++++++++++- .../download/anime/AnimeDownloaderService.kt | 13 ++++ .../ani/dantotsu/download/video/Helper.kt | 24 +----- .../ani/dantotsu/media/SubtitleDownloader.kt | 46 +++++++++++- .../ani/dantotsu/media/anime/ExoplayerView.kt | 26 +++++-- .../media/anime/SelectorDialogFragment.kt | 74 ++++++++++++++++--- .../media/anime/SubtitleDialogFragment.kt | 3 + .../dantotsu/parsers/OfflineAnimeParser.kt | 60 ++++++++++++++- .../ani/dantotsu/parsers/VideoExtractor.kt | 5 +- 9 files changed, 257 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt index d6247ec5c0..ef7765628c 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt @@ -71,7 +71,17 @@ class DownloadsManager(private val context: Context) { Toast.makeText(context, "Directory does not exist", Toast.LENGTH_SHORT).show() cleanDownloads() } - downloadsList.removeAll { it.title == title } + when (type) { + DownloadedType.Type.MANGA -> { + downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.MANGA } + } + DownloadedType.Type.ANIME -> { + downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.ANIME } + } + DownloadedType.Type.NOVEL -> { + downloadsList.removeAll { it.title == title && it.type == DownloadedType.Type.NOVEL } + } + } saveDownloads() } @@ -126,7 +136,7 @@ class DownloadsManager(private val context: Context) { { val jsonString = gson.toJson(downloadsList) val file = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/downloads.json" ) if (file.parentFile?.exists() == false) { @@ -199,7 +209,7 @@ class DownloadsManager(private val context: Context) { ) } val destination = File( - Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "Dantotsu/${downloadedType.title}/${downloadedType.chapter}" ) if (directory.exists()) { @@ -241,6 +251,46 @@ class DownloadsManager(private val context: Context) { const val novelLocation = "Dantotsu/Novel" const val mangaLocation = "Dantotsu/Manga" const val animeLocation = "Dantotsu/Anime" + + fun getDirectory(context: Context, type: DownloadedType.Type, title: String, chapter: String? = null): File { + return if (type == DownloadedType.Type.MANGA) { + if (chapter != null) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "$mangaLocation/$title/$chapter" + ) + } else { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "$mangaLocation/$title" + ) + } + } else if (type == DownloadedType.Type.ANIME) { + if (chapter != null) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "$animeLocation/$title/$chapter" + ) + } else { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "$animeLocation/$title" + ) + } + } else { + if (chapter != null) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "$novelLocation/$title/$chapter" + ) + } else { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "$novelLocation/$title" + ) + } + } + } } } 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 3bda220cf4..f0da25456e 100644 --- a/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt +++ b/app/src/main/java/ani/dantotsu/download/anime/AnimeDownloaderService.kt @@ -28,6 +28,7 @@ import ani.dantotsu.download.video.ExoplayerDownloadService import ani.dantotsu.download.video.Helper import ani.dantotsu.logger import ani.dantotsu.media.Media +import ani.dantotsu.media.SubtitleDownloader import ani.dantotsu.media.anime.AnimeWatchFragment import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.Video @@ -228,6 +229,17 @@ class AnimeDownloaderService : Service() { } saveMediaInfo(task) + task.subtitle?.let { + SubtitleDownloader.downloadSubtitle( + this@AnimeDownloaderService, + it.file.url, + DownloadedType( + task.title, + task.episode, + DownloadedType.Type.ANIME, + ) + ) + } val downloadStarted = hasDownloadStarted(downloadManager, task, 30000) // 30 seconds timeout @@ -313,6 +325,7 @@ class AnimeDownloaderService : Service() { } catch (e: Exception) { logger("Exception while downloading file: ${e.message}") snackString("Exception while downloading file: ${e.message}") + e.printStackTrace() FirebaseCrashlytics.getInstance().recordException(e) broadcastDownloadFailed(task.episode) } 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 af5d5cc091..25d0d47669 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat.getString import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.MimeTypes +import androidx.media3.common.TrackSelectionParameters import androidx.media3.common.util.UnstableApi import androidx.media3.database.StandaloneDatabaseProvider import androidx.media3.datasource.DataSource @@ -30,6 +31,7 @@ import androidx.media3.exoplayer.offline.DownloadHelper import androidx.media3.exoplayer.offline.DownloadManager import androidx.media3.exoplayer.offline.DownloadService import androidx.media3.exoplayer.scheduler.Requirements +import androidx.media3.ui.TrackSelectionDialogBuilder import ani.dantotsu.R import ani.dantotsu.defaultHeaders import ani.dantotsu.download.DownloadedType @@ -99,28 +101,6 @@ object Helper { ) downloadHelper.prepare(object : DownloadHelper.Callback { override fun onPrepared(helper: DownloadHelper) { - /*TrackSelectionDialogBuilder( TODO: use this for subtitles - context, "Select Source", helper.getTracks(0).groups - ) { _, overrides -> - val params = TrackSelectionParameters.Builder(context) - overrides.forEach { - params.addOverride(it.value) - } - helper.addTrackSelection(0, params.build()) - ExoplayerDownloadService - DownloadService.sendAddDownload( - context, - ExoplayerDownloadService::class.java, - helper.getDownloadRequest(null), - false - ) - }.apply { - setTheme(R.style.DialogTheme) - setTrackNameProvider { - if (it.frameRate > 0f) it.height.toString() + "p" else it.height.toString() + "p (fps : N/A)" - } - build().show() - }*/ helper.getDownloadRequest(null).let { DownloadService.sendAddDownload( context, diff --git a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt index 44d8b1590f..d74a89294d 100644 --- a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt +++ b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt @@ -1,19 +1,23 @@ package ani.dantotsu.media import android.content.Context +import ani.dantotsu.download.DownloadedType +import ani.dantotsu.download.DownloadsManager import ani.dantotsu.parsers.SubtitleType +import ani.dantotsu.snackString import eu.kanade.tachiyomi.network.NetworkHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import okhttp3.Request import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.io.File class SubtitleDownloader { companion object { //doesn't really download the subtitles -\_(o_o)_/- - suspend fun downloadSubtitles(context: Context, url: String): SubtitleType = + suspend fun loadSubtitleType(context: Context, url: String): SubtitleType = withContext(Dispatchers.IO) { // Initialize the NetworkHelper instance. Replace this line based on how you usually initialize it val networkHelper = Injekt.get() @@ -29,8 +33,8 @@ class SubtitleDownloader { val subtitleType = when { - responseBody.contains("[Script Info]") == true -> SubtitleType.ASS - responseBody.contains("WEBVTT") == true -> SubtitleType.VTT + responseBody.contains("[Script Info]") -> SubtitleType.ASS + responseBody.contains("WEBVTT") -> SubtitleType.VTT else -> SubtitleType.SRT } @@ -39,5 +43,41 @@ class SubtitleDownloader { return@withContext SubtitleType.UNKNOWN } } + + //actually downloads lol + suspend fun downloadSubtitle(context: Context, url: String, downloadedType: DownloadedType) { + try { + val directory = DownloadsManager.getDirectory(context, downloadedType.type, downloadedType.title, downloadedType.chapter) + if (!directory.exists()) { //just in case + directory.mkdirs() + } + val type = loadSubtitleType(context, url) + val subtiteFile = File(directory, "subtitle.${type}") + if (subtiteFile.exists()) { + subtiteFile.delete() + } + subtiteFile.createNewFile() + + val client = Injekt.get().client + val request = Request.Builder().url(url).build() + val reponse = client.newCall(request).execute() + + if (!reponse.isSuccessful) { + snackString("Failed to download subtitle") + return + } + + reponse.body.byteStream().use { input -> + subtiteFile.outputStream().use { output -> + input.copyTo(output) + } + } + } catch (e: Exception) { + snackString("Failed to download subtitle") + e.printStackTrace() + return + } + + } } } diff --git a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt index f5a1742a87..6405bf73c3 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -48,7 +48,9 @@ import androidx.media3.common.* import androidx.media3.common.C.AUDIO_CONTENT_TYPE_MOVIE import androidx.media3.common.C.TRACK_TYPE_VIDEO import androidx.media3.common.util.UnstableApi +import androidx.media3.common.util.Util import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DefaultDataSourceFactory import androidx.media3.datasource.HttpDataSource import androidx.media3.datasource.cache.CacheDataSource import androidx.media3.datasource.okhttp.OkHttpDataSource @@ -1284,7 +1286,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL if (subtitle?.type == SubtitleType.UNKNOWN) { val context = this runBlocking { - val type = SubtitleDownloader.downloadSubtitles(context, subtitle!!.file.url) + val type = SubtitleDownloader.loadSubtitleType(context, subtitle!!.file.url) val fileUri = Uri.parse(subtitle!!.file.url) sub = MediaItem.SubtitleConfiguration .Builder(fileUri) @@ -1302,8 +1304,9 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL } println("sub: $sub") } else { + val subUri = Uri.parse((subtitle!!.file.url)) sub = MediaItem.SubtitleConfiguration - .Builder(Uri.parse(subtitle!!.file.url)) + .Builder(subUri) .setSelectionFlags(C.SELECTION_FLAG_FORCED) .setMimeType( when (subtitle?.type) { @@ -1338,9 +1341,14 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL } dataSource } + val dafuckDataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, R.string.app_name.toString())) cacheFactory = CacheDataSource.Factory().apply { setCache(Helper.getSimpleCache(this@ExoplayerView)) - setUpstreamDataSourceFactory(dataSourceFactory) + if (ext.server.offline) { + setUpstreamDataSourceFactory(dafuckDataSourceFactory) + } else { + setUpstreamDataSourceFactory(dataSourceFactory) + } setCacheWriteDataSinkFactory(null) } @@ -1374,7 +1382,14 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL } builder.build() } else { - downloadedMediaItem + val addedSubsDownloadedMediaItem = downloadedMediaItem.buildUpon() + if (sub != null) { + val listofnotnullsubs = immutableListOf(sub).filterNotNull() + val addLanguage = listofnotnullsubs[0].buildUpon().setLanguage("en").build() + addedSubsDownloadedMediaItem.setSubtitleConfigurations(immutableListOf(addLanguage)) + episode.selectedSubtitle = 0 + } + addedSubsDownloadedMediaItem.build() } @@ -1635,7 +1650,7 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL if (isInitialized) { if (exoPlayer.currentPosition.toFloat() / exoPlayer.duration > settings.watchPercentage) { preloading = true - nextEpisode(false) { i -> //TODO: make sure this works for offline episodes + nextEpisode(false) { i -> val ep = episodes[episodeArr[currentEpisodeIndex + i]] ?: return@nextEpisode val selected = media.selected ?: return@nextEpisode lifecycleScope.launch(Dispatchers.IO) { @@ -1806,7 +1821,6 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL } super.onDestroy() - Glide.with(this).clear(exoPlay) finishAndRemoveTask() } 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 1a10315ce5..5ad07aacb4 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SelectorDialogFragment.kt @@ -1,6 +1,7 @@ package ani.dantotsu.media.anime import android.annotation.SuppressLint +import android.app.AlertDialog import android.content.DialogInterface import android.content.Intent import android.graphics.Color @@ -24,6 +25,7 @@ import ani.dantotsu.download.video.Helper import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsViewModel import ani.dantotsu.others.Download.download +import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.VideoExtractor import ani.dantotsu.parsers.VideoType import com.google.firebase.crashlytics.FirebaseCrashlytics @@ -302,18 +304,68 @@ class SelectorDialogFragment : BottomSheetDialogFragment() { val episode = media!!.anime!!.episodes!![media!!.anime!!.selectedEpisode!!]!! val selectedVideo = if (extractor.videos.size > episode.selectedVideo) extractor.videos[episode.selectedVideo] else null - if (selectedVideo != null) { - Helper.startAnimeDownloadService( - requireActivity(), - media!!.mainName(), - episode.number, - selectedVideo, - null, - media, - episode.thumb?.url ?: media!!.banner ?: media!!.cover - ) + + val subtitles = extractor.subtitles + val subtitleNames = subtitles.map { it.language } + var subtitleToDownload: Subtitle? = null + if (subtitles.isNotEmpty()) { + AlertDialog.Builder(context, R.style.MyPopup) + .setTitle("Download Subtitle") + .setSingleChoiceItems( + subtitleNames.toTypedArray(), + -1 + ) { dialog, which -> + subtitleToDownload = subtitles[which] + } + .setPositiveButton("Download") { _, _ -> + dialog?.dismiss() + if (selectedVideo != null) { + Helper.startAnimeDownloadService( + currActivity()!!, + media!!.mainName(), + episode.number, + selectedVideo, + subtitleToDownload, + media, + episode.thumb?.url ?: media!!.banner ?: media!!.cover + ) + } else { + snackString("No Video Selected") + } + } + .setNegativeButton("Cancel") { dialog, _ -> + subtitleToDownload = null + dialog.dismiss() + if (selectedVideo != null) { + Helper.startAnimeDownloadService( + currActivity()!!, + media!!.mainName(), + episode.number, + selectedVideo, + subtitleToDownload, + media, + episode.thumb?.url ?: media!!.banner ?: media!!.cover + ) + } else { + snackString("No Video Selected") + } + } + .show() + } else { - snackString("No Video Selected") + if (selectedVideo != null) { + Helper.startAnimeDownloadService( + requireActivity(), + media!!.mainName(), + episode.number, + selectedVideo, + subtitleToDownload, + media, + episode.thumb?.url ?: media!!.banner ?: media!!.cover + ) + } else { + snackString("No Video Selected") + } } dismiss() } diff --git a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt index 406efb3cb2..6cc971574f 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/SubtitleDialogFragment.kt @@ -6,7 +6,9 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.OptIn import androidx.fragment.app.activityViewModels +import androidx.media3.common.util.UnstableApi import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import ani.dantotsu.BottomSheetDialogFragment @@ -60,6 +62,7 @@ class SubtitleDialogFragment : BottomSheetDialogFragment() { ) ) + @OptIn(UnstableApi::class) override fun onBindViewHolder(holder: StreamViewHolder, position: Int) { val binding = holder.binding if (position == 0) { diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt index 2743251af1..0f8c79243e 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt @@ -1,9 +1,11 @@ package ani.dantotsu.parsers +import android.net.Uri import android.os.Environment import ani.dantotsu.currContext import ani.dantotsu.download.DownloadsManager import ani.dantotsu.media.anime.AnimeNameAdapter +import ani.dantotsu.tryWithSuspend import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl @@ -11,6 +13,7 @@ import me.xdrop.fuzzywuzzy.FuzzySearch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File +import java.util.Locale class OfflineAnimeParser : AnimeParser() { private val downloadManager = Injekt.get() @@ -34,6 +37,10 @@ class OfflineAnimeParser : AnimeParser() { val episodes = mutableListOf() if (directory.exists()) { directory.listFiles()?.forEach { + //put the title and episdode number in the extra data + val extraData = mutableMapOf() + extraData["title"] = animeLink + extraData["episode"] = it.name if (it.isDirectory) { val episode = Episode( it.name, @@ -41,6 +48,7 @@ class OfflineAnimeParser : AnimeParser() { it.name, null, null, + extra = extraData, sEpisode = SEpisodeImpl() ) episodes.add(episode) @@ -60,7 +68,8 @@ class OfflineAnimeParser : AnimeParser() { return listOf( VideoServer( episodeLink, - offline = true + offline = true, + extraData = extra ) ) } @@ -81,6 +90,21 @@ class OfflineAnimeParser : AnimeParser() { return returnList } + override suspend fun loadByVideoServers( + episodeUrl: String, + extra: Map?, + sEpisode: SEpisode, + callback: (VideoExtractor) -> Unit + ) { + val server = loadVideoServers(episodeUrl, extra, sEpisode).first() + OfflineVideoExtractor(server).apply { + tryWithSuspend { + load() + } + callback.invoke(this) + } + } + override suspend fun getVideoExtractor(server: VideoServer): VideoExtractor { return OfflineVideoExtractor(server) } @@ -92,7 +116,10 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() { get() = videoServer override suspend fun extract(): VideoContainer { - val sublist = emptyList() + val sublist = getSubtitle( + videoServer.extraData?.get("title") ?: "", + videoServer.extraData?.get("episode") ?: "" + )?: emptyList() //we need to return a "fake" video so that the app doesn't crash val video = Video( null, @@ -102,4 +129,33 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() { return VideoContainer(listOf(video), sublist) } + private fun getSubtitle(title: String, episode: String): List? { + currContext()?.let { + DownloadsManager.getDirectory( + it, + ani.dantotsu.download.DownloadedType.Type.ANIME, + title, + episode + ).listFiles()?.forEach { + if (it.name.contains("subtitle")) { + return listOf( + Subtitle( + "Downloaded Subtitle", + Uri.fromFile(it).toString(), + determineSubtitletype(it.absolutePath) + ) + ) + } + } + } + return null + } + + fun determineSubtitletype(url: String): SubtitleType { + return when { + url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS + url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT + else -> SubtitleType.SRT + } + } } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt b/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt index c98b8a4287..582a7cb4bf 100644 --- a/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt +++ b/app/src/main/java/ani/dantotsu/parsers/VideoExtractor.kt @@ -62,9 +62,8 @@ data class VideoServer( ) : Serializable { constructor(name: String, embedUrl: String, extraData: Map? = null) : this(name, FileUrl(embedUrl), extraData) - - constructor(name: String, offline: Boolean) - : this(name, FileUrl(""), null, null, offline) + constructor(name: String, offline: Boolean, extraData: Map?) + : this(name, FileUrl(""), extraData, null, offline) constructor( name: String,