diff --git a/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt b/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt new file mode 100644 index 00000000..a003bb64 --- /dev/null +++ b/app/src/main/java/ani/dantotsu/download/DownloadCompat.kt @@ -0,0 +1,381 @@ +package ani.dantotsu.download + +import android.content.Context +import android.net.Uri +import android.os.Environment +import android.widget.Toast +import ani.dantotsu.R +import ani.dantotsu.connections.crashlytics.CrashlyticsInterface +import ani.dantotsu.currActivity +import ani.dantotsu.currContext +import ani.dantotsu.download.anime.OfflineAnimeModel +import ani.dantotsu.download.manga.OfflineMangaModel +import ani.dantotsu.media.Media +import ani.dantotsu.media.MediaNameAdapter +import ani.dantotsu.media.MediaType +import ani.dantotsu.parsers.Episode +import ani.dantotsu.parsers.MangaChapter +import ani.dantotsu.parsers.MangaImage +import ani.dantotsu.parsers.Subtitle +import ani.dantotsu.parsers.SubtitleType +import ani.dantotsu.util.Logger +import com.google.gson.GsonBuilder +import com.google.gson.InstanceCreator +import eu.kanade.tachiyomi.animesource.model.SAnime +import eu.kanade.tachiyomi.animesource.model.SAnimeImpl +import eu.kanade.tachiyomi.animesource.model.SEpisode +import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SChapterImpl +import eu.kanade.tachiyomi.source.model.SManga +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.io.File +import java.util.Locale + +@Deprecated("external storage is deprecated, use SAF instead") +class DownloadCompat { + companion object { + @Deprecated("external storage is deprecated, use SAF instead") + fun loadMediaCompat(downloadedType: DownloadedType): Media? { + val type = when (downloadedType.type) { + MediaType.MANGA -> "Manga" + MediaType.ANIME -> "Anime" + else -> "Novel" + } + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/$type/${downloadedType.titleName}" + ) + //load media.json and convert to media class with gson + return try { + val gson = GsonBuilder() + .registerTypeAdapter(SChapter::class.java, InstanceCreator { + SChapterImpl() // Provide an instance of SChapterImpl + }) + .registerTypeAdapter(SAnime::class.java, InstanceCreator { + SAnimeImpl() // Provide an instance of SAnimeImpl + }) + .registerTypeAdapter(SEpisode::class.java, InstanceCreator { + SEpisodeImpl() // Provide an instance of SEpisodeImpl + }) + .create() + val media = File(directory, "media.json") + val mediaJson = media.readText() + gson.fromJson(mediaJson, Media::class.java) + } catch (e: Exception) { + Logger.log("Error loading media.json: ${e.message}") + Logger.log(e) + Injekt.get().logException(e) + null + } + } + + @Deprecated("external storage is deprecated, use SAF instead") + fun loadOfflineAnimeModelCompat(downloadedType: DownloadedType): OfflineAnimeModel { + val type = when (downloadedType.type) { + MediaType.MANGA -> "Manga" + MediaType.ANIME -> "Anime" + else -> "Novel" + } + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/$type/${downloadedType.titleName}" + ) + //load media.json and convert to media class with gson + try { + val mediaModel = loadMediaCompat(downloadedType)!! + val cover = File(directory, "cover.jpg") + val coverUri: Uri? = if (cover.exists()) { + Uri.fromFile(cover) + } else null + val banner = File(directory, "banner.jpg") + val bannerUri: Uri? = if (banner.exists()) { + Uri.fromFile(banner) + } else null + val title = mediaModel.mainName() + val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore + ?: 0) else mediaModel.userScore) / 10.0).toString() + val isOngoing = + mediaModel.status == currActivity()!!.getString(R.string.status_releasing) + val isUserScored = mediaModel.userScore != 0 + val watchedEpisodes = (mediaModel.userProgress ?: "~").toString() + val totalEpisode = + if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString() + " | " + (mediaModel.anime.totalEpisodes + ?: "~").toString()) else (mediaModel.anime?.totalEpisodes ?: "~").toString() + val chapters = " Chapters" + val totalEpisodesList = + if (mediaModel.anime?.nextAiringEpisode != null) (mediaModel.anime.nextAiringEpisode.toString()) else (mediaModel.anime?.totalEpisodes + ?: "~").toString() + return OfflineAnimeModel( + title, + score, + totalEpisode, + totalEpisodesList, + watchedEpisodes, + type, + chapters, + isOngoing, + isUserScored, + coverUri, + bannerUri + ) + } catch (e: Exception) { + Logger.log("Error loading media.json: ${e.message}") + Logger.log(e) + Injekt.get().logException(e) + return OfflineAnimeModel( + "unknown", + "0", + "??", + "??", + "??", + "movie", + "hmm", + isOngoing = false, + isUserScored = false, + null, + null + ) + } + } + + @Deprecated("external storage is deprecated, use SAF instead") + fun loadOfflineMangaModelCompat(downloadedType: DownloadedType): OfflineMangaModel { + val type = when (downloadedType.type) { + MediaType.MANGA -> "Manga" + MediaType.ANIME -> "Anime" + else -> "Novel" + } + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/$type/${downloadedType.titleName}" + ) + //load media.json and convert to media class with gson + try { + val mediaModel = loadMediaCompat(downloadedType)!! + val cover = File(directory, "cover.jpg") + val coverUri: Uri? = if (cover.exists()) { + Uri.fromFile(cover) + } else null + val banner = File(directory, "banner.jpg") + val bannerUri: Uri? = if (banner.exists()) { + Uri.fromFile(banner) + } else null + val title = mediaModel.mainName() + val score = ((if (mediaModel.userScore == 0) (mediaModel.meanScore + ?: 0) else mediaModel.userScore) / 10.0).toString() + val isOngoing = + mediaModel.status == currActivity()!!.getString(R.string.status_releasing) + val isUserScored = mediaModel.userScore != 0 + val readchapter = (mediaModel.userProgress ?: "~").toString() + val totalchapter = "${mediaModel.manga?.totalChapters ?: "??"}" + val chapters = " Chapters" + return OfflineMangaModel( + title, + score, + totalchapter, + readchapter, + type, + chapters, + isOngoing, + isUserScored, + coverUri, + bannerUri + ) + } catch (e: Exception) { + Logger.log("Error loading media.json: ${e.message}") + Logger.log(e) + Injekt.get().logException(e) + return OfflineMangaModel( + "unknown", + "0", + "??", + "??", + "movie", + "hmm", + isOngoing = false, + isUserScored = false, + null, + null + ) + } + } + + @Deprecated("external storage is deprecated, use SAF instead") + suspend fun loadEpisodesCompat( + animeLink: String, + extra: Map?, + sAnime: SAnime + ): List { + + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "${animeLocation}/$animeLink" + ) + //get all of the folder names and add them to the list + 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, + "$animeLink - ${it.name}", + it.name, + null, + null, + extra = extraData, + sEpisode = SEpisodeImpl() + ) + episodes.add(episode) + } + } + episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) } + return episodes + } + return emptyList() + } + + @Deprecated("external storage is deprecated, use SAF instead") + suspend fun loadChaptersCompat( + mangaLink: String, + extra: Map?, + sManga: SManga + ): List { + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Manga/$mangaLink" + ) + //get all of the folder names and add them to the list + val chapters = mutableListOf() + if (directory.exists()) { + directory.listFiles()?.forEach { + if (it.isDirectory) { + val chapter = MangaChapter( + it.name, + "$mangaLink/${it.name}", + it.name, + null, + null, + SChapter.create() + ) + chapters.add(chapter) + } + } + chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) } + return chapters + } + return emptyList() + } + + @Deprecated("external storage is deprecated, use SAF instead") + suspend fun loadImagesCompat(chapterLink: String, sChapter: SChapter): List { + val directory = File( + currContext()?.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Manga/$chapterLink" + ) + val images = mutableListOf() + val imageNumberRegex = Regex("""(\d+)\.jpg$""") + if (directory.exists()) { + directory.listFiles()?.forEach { + if (it.isFile) { + val image = MangaImage(it.absolutePath, false, null) + images.add(image) + } + } + images.sortBy { image -> + val matchResult = imageNumberRegex.find(image.url.url) + matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE + } + for (image in images) { + Logger.log("imageNumber: ${image.url.url}") + } + return images + } + return emptyList() + } + + @Deprecated("external storage is deprecated, use SAF instead") + fun loadSubtitleCompat(title: String, episode: String): List? { + currContext()?.let { + File( + it.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "$animeLocation/$title/$episode" + ).listFiles()?.forEach { + if (it.name.contains("subtitle")) { + return listOf( + Subtitle( + "Downloaded Subtitle", + Uri.fromFile(it).toString(), + determineSubtitletype(it.absolutePath) + ) + ) + } + } + } + return null + } + + private 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 + } + } + + @Deprecated("external storage is deprecated, use SAF instead") + fun removeMediaCompat(context: Context, title: String, type: MediaType) { + val subDirectory = if (type == MediaType.MANGA) { + "Manga" + } else if (type == MediaType.ANIME) { + "Anime" + } else { + "Novel" + } + val directory = File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/$subDirectory/$title" + ) + if (directory.exists()) { + directory.deleteRecursively() + } + } + + @Deprecated("external storage is deprecated, use SAF instead") + fun removeDownloadCompat(context: Context, downloadedType: DownloadedType) { + val directory = if (downloadedType.type == MediaType.MANGA) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Manga/${downloadedType.titleName}/${downloadedType.chapterName}" + ) + } else if (downloadedType.type == MediaType.ANIME) { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Anime/${downloadedType.titleName}/${downloadedType.chapterName}" + ) + } else { + File( + context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), + "Dantotsu/Novel/${downloadedType.titleName}/${downloadedType.chapterName}" + ) + } + + // Check if the directory exists and delete it recursively + if (directory.exists()) { + val deleted = directory.deleteRecursively() + if (deleted) { + Toast.makeText(context, "Successfully deleted", Toast.LENGTH_SHORT).show() + } else { + Toast.makeText(context, "Failed to delete directory", Toast.LENGTH_SHORT).show() + } + } + } + + private val animeLocation = "Dantotsu/Anime" + } +} \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt index b2b9af38..222bdcbb 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt @@ -3,6 +3,8 @@ package ani.dantotsu.download import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile +import ani.dantotsu.download.DownloadCompat.Companion.removeDownloadCompat +import ani.dantotsu.download.DownloadCompat.Companion.removeMediaCompat import ani.dantotsu.media.Media import ani.dantotsu.media.MediaType import ani.dantotsu.settings.saving.PrefManager @@ -58,6 +60,7 @@ class DownloadsManager(private val context: Context) { toast: Boolean = true, onFinished: () -> Unit ) { + removeDownloadCompat(context, downloadedType) downloadsList.remove(downloadedType) CoroutineScope(Dispatchers.IO).launch { removeDirectory(downloadedType, toast) @@ -69,6 +72,7 @@ class DownloadsManager(private val context: Context) { } fun removeMedia(title: String, type: MediaType) { + removeMediaCompat(context, title, type) val baseDirectory = getBaseDirectory(context, type) val directory = baseDirectory?.findFolder(title) if (directory?.exists() == true) { @@ -84,15 +88,15 @@ class DownloadsManager(private val context: Context) { } when (type) { MediaType.MANGA -> { - downloadsList.removeAll { it.title == title && it.type == MediaType.MANGA } + downloadsList.removeAll { it.titleName == title && it.type == MediaType.MANGA } } MediaType.ANIME -> { - downloadsList.removeAll { it.title == title && it.type == MediaType.ANIME } + downloadsList.removeAll { it.titleName == title && it.type == MediaType.ANIME } } MediaType.NOVEL -> { - downloadsList.removeAll { it.title == title && it.type == MediaType.NOVEL } + downloadsList.removeAll { it.titleName == title && it.type == MediaType.NOVEL } } } saveDownloads() @@ -115,7 +119,7 @@ class DownloadsManager(private val context: Context) { if (directory?.exists() == true && directory.isDirectory) { val files = directory.listFiles() for (file in files) { - if (!downloadsSubLists.any { it.title == file.name }) { + if (!downloadsSubLists.any { it.titleName == file.name }) { file.deleteRecursively(context, false) } } @@ -124,8 +128,8 @@ class DownloadsManager(private val context: Context) { val iterator = downloadsList.iterator() while (iterator.hasNext()) { val download = iterator.next() - val downloadDir = directory?.findFolder(download.title) - if ((downloadDir?.exists() == false && download.type == type) || download.title.isBlank()) { + val downloadDir = directory?.findFolder(download.titleName) + if ((downloadDir?.exists() == false && download.type == type) || download.titleName.isBlank()) { iterator.remove() } } @@ -211,16 +215,17 @@ class DownloadsManager(private val context: Context) { fun queryDownload(title: String, chapter: String, type: MediaType? = null): Boolean { return if (type == null) { - downloadsList.any { it.title == title && it.chapter == chapter } + downloadsList.any { it.titleName == title && it.chapterName == chapter } } else { - downloadsList.any { it.title == title && it.chapter == chapter && it.type == type } + downloadsList.any { it.titleName == title && it.chapterName == chapter && it.type == type } } } private fun removeDirectory(downloadedType: DownloadedType, toast: Boolean) { val baseDirectory = getBaseDirectory(context, downloadedType.type) val directory = - baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter) + baseDirectory?.findFolder(downloadedType.titleName) + ?.findFolder(downloadedType.chapterName) downloadsList.remove(downloadedType) // Check if the directory exists and delete it recursively if (directory?.exists() == true) { @@ -369,10 +374,16 @@ private fun String?.findValidName(): String { } data class DownloadedType( - val pTitle: String, val pChapter: String, val type: MediaType + private val pTitle: String?, + private val pChapter: String?, + val type: MediaType, + @Deprecated("use pTitle instead") + private val title: String? = null, + @Deprecated("use pChapter instead") + private val chapter: String? = null ) : Serializable { - val title: String - get() = pTitle.findValidName() - val chapter: String - get() = pChapter.findValidName() + val titleName: String + get() = title?:pTitle.findValidName() + val chapterName: String + get() = chapter?:pChapter.findValidName() } diff --git a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt index 6bffde6e..3a3216c1 100644 --- a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt @@ -31,6 +31,8 @@ import ani.dantotsu.bottomBar import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.currActivity import ani.dantotsu.currContext +import ani.dantotsu.download.DownloadCompat.Companion.loadMediaCompat +import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineAnimeModelCompat import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.compareName @@ -175,7 +177,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { // Get the OfflineAnimeModel that was clicked val item = adapter.getItem(position) as OfflineAnimeModel val media = - downloadManager.animeDownloadedTypes.firstOrNull { it.title.compareName(item.title) } + downloadManager.animeDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) } media?.let { lifecycleScope.launch { val mediaModel = getMedia(it) @@ -287,10 +289,10 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { } downloadsJob = Job() CoroutineScope(Dispatchers.IO + downloadsJob).launch { - val animeTitles = downloadManager.animeDownloadedTypes.map { it.title }.distinct() + val animeTitles = downloadManager.animeDownloadedTypes.map { it.titleName }.distinct() val newAnimeDownloads = mutableListOf() for (title in animeTitles) { - val tDownloads = downloadManager.animeDownloadedTypes.filter { it.title == title } + val tDownloads = downloadManager.animeDownloadedTypes.filter { it.titleName == title } val download = tDownloads.first() val offlineAnimeModel = loadOfflineAnimeModel(download) newAnimeDownloads += offlineAnimeModel @@ -313,7 +315,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { return try { val directory = DownloadsManager.getSubDirectory( context ?: currContext()!!, downloadedType.type, - false, downloadedType.title + false, downloadedType.titleName ) val gson = GsonBuilder() .registerTypeAdapter(SChapter::class.java, InstanceCreator { @@ -327,7 +329,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { }) .create() val media = directory?.findFile("media.json") - ?: return null + ?: return loadMediaCompat(downloadedType) val mediaJson = media.openInputStream(context ?: currContext()!!)?.bufferedReader().use { it?.readText() @@ -352,7 +354,7 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { try { val directory = DownloadsManager.getSubDirectory( context ?: currContext()!!, downloadedType.type, - false, downloadedType.title + false, downloadedType.titleName ) val mediaModel = getMedia(downloadedType)!! val cover = directory?.findFile("cover.jpg") @@ -391,22 +393,26 @@ class OfflineAnimeFragment : Fragment(), OfflineAnimeSearchListener { bannerUri ) } catch (e: Exception) { - Logger.log("Error loading media.json: ${e.message}") - Logger.log(e) - Injekt.get().logException(e) - return OfflineAnimeModel( - "unknown", - "0", - "??", - "??", - "??", - "movie", - "hmm", - isOngoing = false, - isUserScored = false, - null, - null - ) + return try { + loadOfflineAnimeModelCompat(downloadedType) + } catch (e: Exception) { + Logger.log("Error loading media.json: ${e.message}") + Logger.log(e) + Injekt.get().logException(e) + OfflineAnimeModel( + "unknown", + "0", + "??", + "??", + "??", + "movie", + "hmm", + isOngoing = false, + isUserScored = false, + null, + null + ) + } } } } diff --git a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt index de93087b..f368a77a 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt @@ -28,6 +28,8 @@ import ani.dantotsu.bottomBar import ani.dantotsu.connections.crashlytics.CrashlyticsInterface import ani.dantotsu.currActivity import ani.dantotsu.currContext +import ani.dantotsu.download.DownloadCompat +import ani.dantotsu.download.DownloadCompat.Companion.loadOfflineMangaModelCompat import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.compareName @@ -169,8 +171,8 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { // Get the OfflineMangaModel that was clicked val item = adapter.getItem(position) as OfflineMangaModel val media = - downloadManager.mangaDownloadedTypes.firstOrNull { it.title.compareName(item.title) } - ?: downloadManager.novelDownloadedTypes.firstOrNull { it.title.compareName(item.title) } + downloadManager.mangaDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) } + ?: downloadManager.novelDownloadedTypes.firstOrNull { it.titleName.compareName(item.title) } media?.let { lifecycleScope.launch { ContextCompat.startActivity( @@ -190,7 +192,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { // Get the OfflineMangaModel that was clicked val item = adapter.getItem(position) as OfflineMangaModel val type: MediaType = - if (downloadManager.mangaDownloadedTypes.any { it.title == item.title }) { + if (downloadManager.mangaDownloadedTypes.any { it.titleName == item.title }) { MediaType.MANGA } else { MediaType.NOVEL @@ -278,19 +280,19 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { downloads = listOf() downloadsJob = Job() CoroutineScope(Dispatchers.IO + downloadsJob).launch { - val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct() + val mangaTitles = downloadManager.mangaDownloadedTypes.map { it.titleName }.distinct() val newMangaDownloads = mutableListOf() for (title in mangaTitles) { - val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.title == title } + val tDownloads = downloadManager.mangaDownloadedTypes.filter { it.titleName == title } val download = tDownloads.first() val offlineMangaModel = loadOfflineMangaModel(download) newMangaDownloads += offlineMangaModel } downloads = newMangaDownloads - val novelTitles = downloadManager.novelDownloadedTypes.map { it.title }.distinct() + val novelTitles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct() val newNovelDownloads = mutableListOf() for (title in novelTitles) { - val tDownloads = downloadManager.novelDownloadedTypes.filter { it.title == title } + val tDownloads = downloadManager.novelDownloadedTypes.filter { it.titleName == title } val download = tDownloads.first() val offlineMangaModel = loadOfflineMangaModel(download) newNovelDownloads += offlineMangaModel @@ -315,7 +317,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { return try { val directory = getSubDirectory( context ?: currContext()!!, downloadedType.type, - false, downloadedType.title + false, downloadedType.titleName ) val gson = GsonBuilder() .registerTypeAdapter(SChapter::class.java, InstanceCreator { @@ -323,7 +325,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { }) .create() val media = directory?.findFile("media.json") - ?: return null + ?: return DownloadCompat.loadMediaCompat(downloadedType) val mediaJson = media.openInputStream(context ?: currContext()!!)?.bufferedReader().use { it?.readText() @@ -343,7 +345,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { try { val directory = getSubDirectory( context ?: currContext()!!, downloadedType.type, - false, downloadedType.title + false, downloadedType.titleName ) val mediaModel = getMedia(downloadedType)!! val cover = directory?.findFile("cover.jpg") @@ -376,21 +378,25 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { bannerUri ) } catch (e: Exception) { - Logger.log("Error loading media.json: ${e.message}") - Logger.log(e) - Injekt.get().logException(e) - return OfflineMangaModel( - "unknown", - "0", - "??", - "??", - "movie", - "hmm", - isOngoing = false, - isUserScored = false, - null, - null - ) + return try { + loadOfflineMangaModelCompat(downloadedType) + } catch (e: Exception) { + Logger.log("Error loading media.json: ${e.message}") + Logger.log(e) + Injekt.get().logException(e) + return OfflineMangaModel( + "unknown", + "0", + "??", + "??", + "movie", + "hmm", + isOngoing = false, + isUserScored = false, + null, + null + ) + } } } } 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 67fb9e2f..463cfa91 100644 --- a/app/src/main/java/ani/dantotsu/download/video/Helper.kt +++ b/app/src/main/java/ani/dantotsu/download/video/Helper.kt @@ -12,7 +12,17 @@ import androidx.annotation.OptIn import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.media3.common.util.UnstableApi +import androidx.media3.database.StandaloneDatabaseProvider +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.HttpDataSource +import androidx.media3.datasource.cache.NoOpCacheEvictor +import androidx.media3.datasource.cache.SimpleCache +import androidx.media3.datasource.okhttp.OkHttpDataSource +import androidx.media3.exoplayer.offline.Download +import androidx.media3.exoplayer.offline.DownloadManager +import androidx.media3.exoplayer.scheduler.Requirements import ani.dantotsu.R +import ani.dantotsu.defaultHeaders import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.anime.AnimeDownloaderService @@ -22,8 +32,12 @@ import ani.dantotsu.media.MediaType import ani.dantotsu.parsers.Subtitle import ani.dantotsu.parsers.Video import ani.dantotsu.settings.saving.PrefManager +import ani.dantotsu.util.Logger +import eu.kanade.tachiyomi.network.NetworkHelper import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import java.io.File +import java.util.concurrent.Executors @SuppressLint("UnsafeOptInUsageError") object Helper { @@ -104,4 +118,92 @@ object Helper { } return true } + + @Synchronized + @UnstableApi + @Deprecated("exoplayer download manager is no longer used") + fun downloadManager(context: Context): DownloadManager { + return download ?: let { + val database = Injekt.get() + val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) + val dataSourceFactory = DataSource.Factory { + //val dataSource: HttpDataSource = OkHttpDataSource.Factory(okHttpClient).createDataSource() + val networkHelper = Injekt.get() + val okHttpClient = networkHelper.client + val dataSource: HttpDataSource = + OkHttpDataSource.Factory(okHttpClient).createDataSource() + defaultHeaders.forEach { + dataSource.setRequestProperty(it.key, it.value) + } + dataSource + } + val threadPoolSize = Runtime.getRuntime().availableProcessors() + val executorService = Executors.newFixedThreadPool(threadPoolSize) + val downloadManager = DownloadManager( + context, + database, + getSimpleCache(context), + dataSourceFactory, + executorService + ).apply { + requirements = + Requirements(Requirements.NETWORK or Requirements.DEVICE_STORAGE_NOT_LOW) + maxParallelDownloads = 3 + } + downloadManager.addListener( //for testing + object : DownloadManager.Listener { + override fun onDownloadChanged( + downloadManager: DownloadManager, + download: Download, + finalException: Exception? + ) { + if (download.state == Download.STATE_COMPLETED) { + Logger.log("Download Completed") + } else if (download.state == Download.STATE_FAILED) { + Logger.log("Download Failed") + } else if (download.state == Download.STATE_STOPPED) { + Logger.log("Download Stopped") + } else if (download.state == Download.STATE_QUEUED) { + Logger.log("Download Queued") + } else if (download.state == Download.STATE_DOWNLOADING) { + Logger.log("Download Downloading") + } + } + } + ) + + downloadManager + } + } + @Deprecated("exoplayer download manager is no longer used") + @OptIn(UnstableApi::class) + fun getSimpleCache(context: Context): SimpleCache { + return if (simpleCache == null) { + val downloadDirectory = File(getDownloadDirectory(context), DOWNLOAD_CONTENT_DIRECTORY) + val database = Injekt.get() + simpleCache = SimpleCache(downloadDirectory, NoOpCacheEvictor(), database) + simpleCache!! + } else { + simpleCache!! + } + } + @Synchronized + @Deprecated("exoplayer download manager is no longer used") + private fun getDownloadDirectory(context: Context): File { + if (downloadDirectory == null) { + downloadDirectory = context.getExternalFilesDir(null) + if (downloadDirectory == null) { + downloadDirectory = context.filesDir + } + } + return downloadDirectory!! + } + @Deprecated("exoplayer download manager is no longer used") + private var download: DownloadManager? = null + @Deprecated("exoplayer download manager is no longer used") + private const val DOWNLOAD_CONTENT_DIRECTORY = "Anime_Downloads" + @Deprecated("exoplayer download manager is no longer used") + private var simpleCache: SimpleCache? = null + @Deprecated("exoplayer download manager is no longer used") + private var downloadDirectory: File? = null } \ No newline at end of file diff --git a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt index 018b506b..503f1732 100644 --- a/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt +++ b/app/src/main/java/ani/dantotsu/media/SubtitleDownloader.kt @@ -55,8 +55,8 @@ class SubtitleDownloader { context, downloadedType.type, false, - downloadedType.title, - downloadedType.chapter + downloadedType.titleName, + downloadedType.chapterName ) ?: throw Exception("Could not create directory") val type = loadSubtitleType(url) directory.findFile("subtitle.${type}")?.delete() diff --git a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt index 830d47b0..7a3d993e 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt @@ -553,8 +553,8 @@ class AnimeWatchFragment : Fragment() { episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView)) episodeAdapter.notifyItemRangeInserted(0, arr.size) for (download in downloadManager.animeDownloadedTypes) { - if (media.compareName(download.title)) { - episodeAdapter.stopDownload(download.chapter) + if (media.compareName(download.titleName)) { + episodeAdapter.stopDownload(download.chapterName) } } } 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 f570b6f4..f3b8ffb6 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/ExoplayerView.kt @@ -111,6 +111,7 @@ import ani.dantotsu.connections.updateProgress import ani.dantotsu.databinding.ActivityExoplayerBinding import ani.dantotsu.defaultHeaders import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory +import ani.dantotsu.download.video.Helper import ani.dantotsu.dp import ani.dantotsu.getCurrentBrightnessValue import ani.dantotsu.hideSystemBars @@ -1481,26 +1482,38 @@ class ExoplayerView : AppCompatActivity(), Player.Listener, SessionAvailabilityL val downloadedMediaItem = if (ext.server.offline) { val titleName = ext.server.name.split("/").first() val episodeName = ext.server.name.split("/").last() + downloadId = PrefManager.getAnimeDownloadPreferences() + .getString("$titleName - $episodeName", null) ?: + PrefManager.getAnimeDownloadPreferences() + .getString(ext.server.name, null) + val exoItem = if (downloadId != null) { + Helper.downloadManager(this) + .downloadIndex.getDownload(downloadId!!)?.request?.toMediaItem() + } else null + if (exoItem != null) { + exoItem + } else { - val directory = getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName) - if (directory != null) { - val files = directory.listFiles() - println(files) - val docFile = directory.listFiles().firstOrNull { - it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true - } - if (docFile != null) { - val uri = docFile.uri - MediaItem.Builder().setUri(uri).setMimeType(mimeType).build() + val directory = + getSubDirectory(this, MediaType.ANIME, false, titleName, episodeName) + if (directory != null) { + val files = directory.listFiles() + println(files) + val docFile = directory.listFiles().firstOrNull { + it.name?.endsWith(".mp4") == true || it.name?.endsWith(".mkv") == true + } + if (docFile != null) { + val uri = docFile.uri + MediaItem.Builder().setUri(uri).setMimeType(mimeType).build() + } else { + snackString("File not found") + null + } } else { - snackString("File not found") + snackString("Directory not found") null } - } else { - snackString("Directory not found") - null } - } else null mediaItem = if (downloadedMediaItem == null) { diff --git a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt index 8e9bd757..b86a3654 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -194,8 +194,8 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { ) for (download in downloadManager.mangaDownloadedTypes) { - if (media.compareName(download.title)) { - chapterAdapter.stopDownload(download.chapter) + if (media.compareName(download.titleName)) { + chapterAdapter.stopDownload(download.chapterName) } } diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt index 2b00b1d3..e35e8a8c 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt @@ -2,6 +2,8 @@ package ani.dantotsu.parsers import android.app.Application import ani.dantotsu.currContext +import ani.dantotsu.download.DownloadCompat.Companion.loadEpisodesCompat +import ani.dantotsu.download.DownloadCompat.Companion.loadSubtitleCompat import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Companion.getTaskName @@ -53,8 +55,12 @@ class OfflineAnimeParser : AnimeParser() { episodes.add(episode) } } - episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) } - return episodes + return if (episodes.isNotEmpty()) { + episodes.sortBy { MediaNameAdapter.findEpisodeNumber(it.number) } + episodes + } else { + loadEpisodesCompat(animeLink, extra, sAnime) + } } return emptyList() } @@ -75,14 +81,16 @@ class OfflineAnimeParser : AnimeParser() { override suspend fun search(query: String): List { - val titles = downloadManager.animeDownloadedTypes.map { it.title }.distinct() - val returnTitles: MutableList = mutableListOf() + val titles = downloadManager.animeDownloadedTypes.map { it.titleName }.distinct() + val returnTitlesPair: MutableList> = mutableListOf() for (title in titles) { Logger.log("Comparing $title to $query") - if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { - returnTitles.add(title) + val score = FuzzySearch.ratio(title.lowercase(), query.lowercase()) + if (score > 80) { + returnTitlesPair.add(Pair(title, score)) } } + val returnTitles = returnTitlesPair.sortedByDescending { it.second }.map { it.first } val returnList: MutableList = mutableListOf() for (title in returnTitles) { returnList.add(ShowResponse(title, title, title)) @@ -148,6 +156,7 @@ class OfflineVideoExtractor(private val videoServer: VideoServer) : VideoExtract ) } } + loadSubtitleCompat(title, episode)?.let { return it } } return null } diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt index 4b132d60..b943a2fa 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineMangaParser.kt @@ -1,6 +1,8 @@ package ani.dantotsu.parsers import android.app.Application +import ani.dantotsu.download.DownloadCompat.Companion.loadChaptersCompat +import ani.dantotsu.download.DownloadCompat.Companion.loadImagesCompat import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory import ani.dantotsu.media.MediaNameAdapter @@ -41,8 +43,12 @@ class OfflineMangaParser : MangaParser() { chapters.add(chapter) } } - chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) } - return chapters + return if (chapters.isNotEmpty()) { + chapters.sortBy { MediaNameAdapter.findChapterNumber(it.number) } + chapters + } else { + loadChaptersCompat(mangaLink, extra, sManga) + } } return emptyList() } @@ -60,26 +66,32 @@ class OfflineMangaParser : MangaParser() { images.add(image) } } - images.sortBy { image -> - val matchResult = imageNumberRegex.find(image.url.url) - matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE - } for (image in images) { Logger.log("imageNumber: ${image.url.url}") } - return images + return if (images.isNotEmpty()) { + images.sortBy { image -> + val matchResult = imageNumberRegex.find(image.url.url) + matchResult?.groups?.get(1)?.value?.toIntOrNull() ?: Int.MAX_VALUE + } + images + } else { + loadImagesCompat(chapterLink, sChapter) + } } return emptyList() } override suspend fun search(query: String): List { - val titles = downloadManager.mangaDownloadedTypes.map { it.title }.distinct() - val returnTitles: MutableList = mutableListOf() + val titles = downloadManager.mangaDownloadedTypes.map { it.titleName }.distinct() + val returnTitlesPair: MutableList> = mutableListOf() for (title in titles) { - if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { - returnTitles.add(title) + val score = FuzzySearch.ratio(title.lowercase(), query.lowercase()) + if (score > 80) { + returnTitlesPair.add(Pair(title, score)) } } + val returnTitles = returnTitlesPair.sortedByDescending { it.second }.map { it.first } val returnList: MutableList = mutableListOf() for (title in returnTitles) { returnList.add(ShowResponse(title, title, title)) diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt index 682e8af2..8f0fa2d1 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineNovelParser.kt @@ -5,6 +5,7 @@ import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory import ani.dantotsu.media.MediaNameAdapter import ani.dantotsu.media.MediaType +import ani.dantotsu.util.Logger import me.xdrop.fuzzywuzzy.FuzzySearch import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get @@ -48,13 +49,16 @@ class OfflineNovelParser : NovelParser() { } override suspend fun search(query: String): List { - val titles = downloadManager.novelDownloadedTypes.map { it.title }.distinct() - val returnTitles: MutableList = mutableListOf() + val titles = downloadManager.novelDownloadedTypes.map { it.titleName }.distinct() + val returnTitlesPair: MutableList> = mutableListOf() for (title in titles) { - if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { - returnTitles.add(title) + Logger.log("Comparing $title to $query") + val score = FuzzySearch.ratio(title.lowercase(), query.lowercase()) + if (score > 80) { + returnTitlesPair.add(Pair(title, score)) } } + val returnTitles = returnTitlesPair.sortedByDescending { it.second }.map { it.first } val returnList: MutableList = mutableListOf() for (title in returnTitles) { //need to search the subdirectories for the ShowResponses