diff --git a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt index fa01cc79..546137c9 100644 --- a/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt +++ b/app/src/main/java/ani/dantotsu/download/DownloadsManager.kt @@ -3,7 +3,7 @@ package ani.dantotsu.download import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile -import ani.dantotsu.download.DownloadsManager.Companion.findValidName +import ani.dantotsu.media.Media import ani.dantotsu.media.MediaType import ani.dantotsu.settings.saving.PrefManager import ani.dantotsu.settings.saving.PrefName @@ -12,7 +12,6 @@ import ani.dantotsu.util.Logger import com.anggrayudi.storage.callback.FolderCallback import com.anggrayudi.storage.file.deleteRecursively import com.anggrayudi.storage.file.findFolder -import com.anggrayudi.storage.file.moveFileTo import com.anggrayudi.storage.file.moveFolderTo import com.google.gson.Gson import com.google.gson.reflect.TypeToken @@ -20,6 +19,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import me.xdrop.fuzzywuzzy.FuzzySearch import java.io.Serializable class DownloadsManager(private val context: Context) { @@ -127,7 +127,12 @@ class DownloadsManager(private val context: Context) { } } - fun moveDownloadsDir(context: Context, oldUri: Uri, newUri: Uri, finished: (Boolean, String) -> Unit) { + fun moveDownloadsDir( + context: Context, + oldUri: Uri, + newUri: Uri, + finished: (Boolean, String) -> Unit + ) { try { if (oldUri == newUri) { finished(false, "Source and destination are the same") @@ -141,18 +146,41 @@ class DownloadsManager(private val context: Context) { DocumentFile.fromTreeUri(context, newUri) ?: throw Exception("New base is null") val folder = oldBase.findFolder(BASE_LOCATION) ?: throw Exception("Base folder not found") - folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object: + folder.moveFolderTo(context, newBase, false, BASE_LOCATION, object : FolderCallback() { override fun onFailed(errorCode: ErrorCode) { when (errorCode) { ErrorCode.CANCELED -> finished(false, "Move canceled") - ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished(false, "Cannot create file in target") - ErrorCode.INVALID_TARGET_FOLDER -> finished(true, "Invalid target folder") // seems to still work - ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished(false, "No space left on target path") + ErrorCode.CANNOT_CREATE_FILE_IN_TARGET -> finished( + false, + "Cannot create file in target" + ) + + ErrorCode.INVALID_TARGET_FOLDER -> finished( + true, + "Invalid target folder" + ) // seems to still work + ErrorCode.NO_SPACE_LEFT_ON_TARGET_PATH -> finished( + false, + "No space left on target path" + ) + ErrorCode.UNKNOWN_IO_ERROR -> finished(false, "Unknown IO error") - ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished(false, "Source folder not found") - ErrorCode.STORAGE_PERMISSION_DENIED -> finished(false, "Storage permission denied") - ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished(false, "Target folder cannot have same path with source folder") + ErrorCode.SOURCE_FOLDER_NOT_FOUND -> finished( + false, + "Source folder not found" + ) + + ErrorCode.STORAGE_PERMISSION_DENIED -> finished( + false, + "Storage permission denied" + ) + + ErrorCode.TARGET_FOLDER_CANNOT_HAVE_SAME_PATH_WITH_SOURCE_FOLDER -> finished( + false, + "Target folder cannot have same path with source folder" + ) + else -> finished(false, "Failed to move downloads: $errorCode") } Logger.log("Failed to move downloads: $errorCode") @@ -164,7 +192,7 @@ class DownloadsManager(private val context: Context) { super.onCompleted(result) } }) - } + } } catch (e: Exception) { snackString("Error: ${e.message}") @@ -189,7 +217,7 @@ class DownloadsManager(private val context: Context) { val baseDirectory = getBaseDirectory(context, downloadedType.type) val directory = baseDirectory?.findFolder(downloadedType.title)?.findFolder(downloadedType.chapter) - + downloadsList.remove(downloadedType) // Check if the directory exists and delete it recursively if (directory?.exists() == true) { val deleted = directory.deleteRecursively(context, false) @@ -226,11 +254,7 @@ class DownloadsManager(private val context: Context) { private const val MANGA_SUB_LOCATION = "Manga" private const val ANIME_SUB_LOCATION = "Anime" private const val NOVEL_SUB_LOCATION = "Novel" - private const val RESERVED_CHARS = "|\\?*<\":>+[]/'" - fun String?.findValidName(): String { - return this?.filterNot { RESERVED_CHARS.contains(it) } ?: "" - } /** * Get and create a base directory for the given type @@ -238,7 +262,6 @@ class DownloadsManager(private val context: Context) { * @param type the type of media * @return the base directory */ - private fun getBaseDirectory(context: Context, type: MediaType): DocumentFile? { val baseDirectory = Uri.parse(PrefManager.getVal(PrefName.DownloadsDir)) if (baseDirectory == Uri.EMPTY) return null @@ -283,7 +306,12 @@ class DownloadsManager(private val context: Context) { } } - fun getDirSize(context: Context, type: MediaType, title: String, chapter: String? = null): Long { + fun getDirSize( + context: Context, + type: MediaType, + title: String, + chapter: String? = null + ): Long { val directory = getSubDirectory(context, type, false, title, chapter) ?: return 0 var size = 0L directory.listFiles().forEach { @@ -303,9 +331,27 @@ class DownloadsManager(private val context: Context) { } } + private const val RATIO_THRESHOLD = 95 + fun Media.compareName(name: String): Boolean { + val mainName = mainName().findValidName().lowercase() + val ratio = FuzzySearch.ratio(mainName, name.lowercase()) + return ratio > RATIO_THRESHOLD + } + + fun String.compareName(name: String): Boolean { + val mainName = findValidName().lowercase() + val compareName = name.findValidName().lowercase() + val ratio = FuzzySearch.ratio(mainName, compareName) + return ratio > RATIO_THRESHOLD + } } } +private const val RESERVED_CHARS = "|\\?*<\":>+[]/'" +private fun String?.findValidName(): String { + return this?.filterNot { RESERVED_CHARS.contains(it) } ?: "" +} + data class DownloadedType( val pTitle: String, val pChapter: String, val type: MediaType ) : Serializable { 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 6c21cb86..6bffde6e 100644 --- a/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/anime/OfflineAnimeFragment.kt @@ -33,7 +33,7 @@ import ani.dantotsu.currActivity import ani.dantotsu.currContext import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager -import ani.dantotsu.download.DownloadsManager.Companion.findValidName +import ani.dantotsu.download.DownloadsManager.Companion.compareName import ani.dantotsu.initActivity import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity @@ -175,7 +175,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 == item.title.findValidName() } + downloadManager.animeDownloadedTypes.firstOrNull { it.title.compareName(item.title) } media?.let { lifecycleScope.launch { val mediaModel = getMedia(it) 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 1d912887..70aba7c6 100644 --- a/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt +++ b/app/src/main/java/ani/dantotsu/download/manga/OfflineMangaFragment.kt @@ -168,7 +168,7 @@ class OfflineMangaFragment : Fragment(), OfflineMangaSearchListener { // Get the OfflineMangaModel that was clicked val item = adapter.getItem(position) as OfflineMangaModel val media = - downloadManager.mangaDownloadedTypes.firstOrNull { it.title == item.title } + downloadManager.mangaDownloadedTypes.firstOrNull { it.title.contains(item.title) } ?: downloadManager.novelDownloadedTypes.firstOrNull { it.title == item.title } media?.let { lifecycleScope.launch { 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 2554f283..0954f1ff 100644 --- a/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/anime/AnimeWatchFragment.kt @@ -14,7 +14,6 @@ import android.view.ViewGroup import android.widget.FrameLayout import android.widget.Toast import androidx.annotation.OptIn -import androidx.appcompat.app.AppCompatActivity import androidx.cardview.widget.CardView import androidx.core.content.ContextCompat import androidx.core.math.MathUtils @@ -25,7 +24,6 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.offline.DownloadService import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -35,14 +33,14 @@ import ani.dantotsu.R import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager -import ani.dantotsu.download.DownloadsManager.Companion.findValidName +import ani.dantotsu.download.DownloadsManager.Companion.compareName import ani.dantotsu.download.anime.AnimeDownloaderService import ani.dantotsu.dp import ani.dantotsu.media.Media import ani.dantotsu.media.MediaDetailsActivity import ani.dantotsu.media.MediaDetailsViewModel -import ani.dantotsu.media.MediaType import ani.dantotsu.media.MediaNameAdapter +import ani.dantotsu.media.MediaType import ani.dantotsu.navBarHeight import ani.dantotsu.notifications.subscription.SubscriptionHelper import ani.dantotsu.notifications.subscription.SubscriptionHelper.Companion.saveSubscription @@ -425,17 +423,27 @@ class AnimeWatchFragment : Fragment() { } fun onAnimeEpisodeDownloadClick(i: String) { - activity?.let{ + activity?.let { if (!hasDirAccess(it)) { (it as MediaDetailsActivity).accessAlertDialog(it.launcher) { success -> if (success) { - model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true) + model.onEpisodeClick( + media, + i, + requireActivity().supportFragmentManager, + isDownload = true + ) } else { snackString("Permission is required to download") } } } else { - model.onEpisodeClick(media, i, requireActivity().supportFragmentManager, isDownload = true) + model.onEpisodeClick( + media, + i, + requireActivity().supportFragmentManager, + isDownload = true + ) } } } @@ -472,10 +480,6 @@ class AnimeWatchFragment : Fragment() { ) ) { val taskName = AnimeDownloaderService.AnimeDownloadTask.getTaskName(media.mainName(), i) - val id = PrefManager.getAnimeDownloadPreferences().getString( - taskName, - "" - ) ?: "" PrefManager.getAnimeDownloadPreferences().edit().remove(taskName).apply() episodeAdapter.deleteDownload(i) } @@ -542,7 +546,7 @@ class AnimeWatchFragment : Fragment() { episodeAdapter.updateType(style ?: PrefManager.getVal(PrefName.AnimeDefaultView)) episodeAdapter.notifyItemRangeInserted(0, arr.size) for (download in downloadManager.animeDownloadedTypes) { - if (download.title == media.mainName().findValidName()) { + if (media.compareName(download.title)) { episodeAdapter.stopDownload(download.chapter) } } 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 26573b6e..6fc64967 100644 --- a/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt +++ b/app/src/main/java/ani/dantotsu/media/manga/MangaReadFragment.kt @@ -16,7 +16,6 @@ import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.Toast -import androidx.appcompat.app.AppCompatActivity import androidx.cardview.widget.CardView import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat @@ -35,7 +34,7 @@ import ani.dantotsu.R import ani.dantotsu.databinding.FragmentAnimeWatchBinding import ani.dantotsu.download.DownloadedType import ani.dantotsu.download.DownloadsManager -import ani.dantotsu.download.DownloadsManager.Companion.findValidName +import ani.dantotsu.download.DownloadsManager.Companion.compareName import ani.dantotsu.download.manga.MangaDownloaderService import ani.dantotsu.download.manga.MangaServiceDataSingleton import ani.dantotsu.dp @@ -194,7 +193,7 @@ open class MangaReadFragment : Fragment(), ScanlatorSelectionListener { ) for (download in downloadManager.mangaDownloadedTypes) { - if (download.title == media.mainName().findValidName()) { + if (media.compareName(download.title)) { chapterAdapter.stopDownload(download.chapter) } } diff --git a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt index f4e30c0d..08f62446 100644 --- a/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt +++ b/app/src/main/java/ani/dantotsu/parsers/OfflineAnimeParser.kt @@ -1,8 +1,6 @@ package ani.dantotsu.parsers import android.app.Application -import android.net.Uri -import android.os.Environment import ani.dantotsu.currContext import ani.dantotsu.download.DownloadsManager import ani.dantotsu.download.DownloadsManager.Companion.getSubDirectory @@ -10,13 +8,13 @@ import ani.dantotsu.download.anime.AnimeDownloaderService.AnimeDownloadTask.Comp import ani.dantotsu.media.MediaType import ani.dantotsu.media.MediaNameAdapter import ani.dantotsu.tryWithSuspend +import ani.dantotsu.util.Logger import eu.kanade.tachiyomi.animesource.model.SAnime import eu.kanade.tachiyomi.animesource.model.SEpisode import eu.kanade.tachiyomi.animesource.model.SEpisodeImpl 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() { @@ -80,6 +78,7 @@ class OfflineAnimeParser : AnimeParser() { val titles = downloadManager.animeDownloadedTypes.map { it.title }.distinct() val returnTitles: MutableList = mutableListOf() for (title in titles) { + Logger.log("Comparing $title to $query") if (FuzzySearch.ratio(title.lowercase(), query.lowercase()) > 80) { returnTitles.add(title) } @@ -112,7 +111,7 @@ class OfflineAnimeParser : AnimeParser() { } -class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() { +class OfflineVideoExtractor(private val videoServer: VideoServer) : VideoExtractor() { override val server: VideoServer get() = videoServer @@ -132,7 +131,7 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() { private fun getSubtitle(title: String, episode: String): List? { currContext()?.let { - DownloadsManager.getSubDirectory( + getSubDirectory( it, MediaType.ANIME, false, @@ -144,7 +143,7 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() { Subtitle( "Downloaded Subtitle", file.uri.toString(), - determineSubtitletype(file.name ?: "") + determineSubtitleType(file.name ?: "") ) ) } @@ -153,7 +152,7 @@ class OfflineVideoExtractor(val videoServer: VideoServer) : VideoExtractor() { return null } - fun determineSubtitletype(url: String): SubtitleType { + private fun determineSubtitleType(url: String): SubtitleType { return when { url.lowercase(Locale.ROOT).endsWith("ass") -> SubtitleType.ASS url.lowercase(Locale.ROOT).endsWith("vtt") -> SubtitleType.VTT