diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt index 749fbf5ac..ae5c71987 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt @@ -200,6 +200,18 @@ class DownloadCache( } } + // SY --> + fun removeFolders(folders: List, manga: Manga) { + val sourceDir = rootDir.files[manga.source] ?: return + val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return + for (chapter in folders) { + if (chapter in mangaDir.files) { + mangaDir.files -= chapter + } + } + } + // SY <-- + /** * Removes a list of chapters that have been deleted from this cache. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index 714071eac..41cdb502d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -209,6 +209,32 @@ class DownloadManager(/* SY private */ val context: Context) { } } + // SY --> + /** + * Deletes the directories of chapters that were read or have no match + * + * @param chapters the list of chapters to delete. + * @param manga the manga of the chapters. + * @param source the source of the chapters. + */ + fun cleanupChapters(allChapters: List, manga: Manga, source: Source): Int { + var cleaned = 0 + val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source) + cleaned += filesWithNoChapter.size + cache.removeFolders(filesWithNoChapter.mapNotNull { it.name }, manga) + filesWithNoChapter.forEach { it.delete() } + val readChapters = allChapters.filter { it.read } + val readChapterDirs = provider.findChapterDirs(readChapters, manga, source) + readChapterDirs.forEach { it.delete() } + cleaned += readChapterDirs.size + cache.removeChapters(readChapters, manga) + if (cache.getDownloadCount(manga) == 0) { + provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete() // Delete manga directory if empty + } + return cleaned + } + // SY <-- + /** * Deletes the directory of a downloaded manga. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt index ee16f1337..edc3f9d70 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -109,6 +109,32 @@ class DownloadProvider(private val context: Context) { } } + // SY --> + /** + * Returns a list of all files in manga directory + * + * @param chapters the chapters to query. + * @param manga the manga of the chapter. + * @param source the source of the chapter. + */ + fun findUnmatchedChapterDirs( + chapters: List, + manga: Manga, + source: Source + ): List { + val mangaDir = findMangaDir(manga, source) ?: return emptyList() + return mangaDir.listFiles()!!.asList().filter { + ( + chapters.find { chp -> + getValidChapterDirNames(chp).any { dir -> + mangaDir.findFile(dir) != null + } + } == null + ) || it.name?.endsWith("_tmp") == true + } + } + // SY <-- + /** * Returns the download directory name for a source. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 96ad689cc..d805ad414 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.os.Build import android.os.Bundle import android.provider.Settings +import android.widget.Toast import androidx.core.net.toUri import androidx.core.text.HtmlCompat import androidx.preference.PreferenceScreen @@ -14,13 +15,16 @@ import com.afollestad.materialdialogs.MaterialDialog import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.preference.defaultValue import eu.kanade.tachiyomi.util.preference.intListPreference import eu.kanade.tachiyomi.util.preference.onChange @@ -42,9 +46,16 @@ import exh.PERV_EDEN_IT_SOURCE_ID import exh.debug.SettingsDebugController import exh.log.EHLogLevel import exh.source.BlacklistedSources +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import rx.Observable import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy class SettingsAdvancedController : SettingsController() { @@ -141,6 +152,17 @@ class SettingsAdvancedController : SettingsController() { } // --> EXH + preferenceCategory { + titleRes = R.string.group_downloader + + preference { + titleRes = R.string.clean_up_downloaded_chapters + summaryRes = R.string.delete_unused_chapters + + onClick { cleanupDownloads() } + } + } + preferenceCategory { titleRes = R.string.developer_tools isPersistent = false @@ -237,6 +259,35 @@ class SettingsAdvancedController : SettingsController() { // <-- EXH } + // SY --> + private fun cleanupDownloads() { + if (job?.isActive == true) return + activity?.toast(R.string.starting_cleanup) + job = GlobalScope.launch(Dispatchers.IO, CoroutineStart.DEFAULT) { + val mangaList = db.getMangas().executeAsBlocking() + val sourceManager: SourceManager = Injekt.get() + val downloadManager: DownloadManager = Injekt.get() + var foldersCleared = 0 + for (manga in mangaList) { + val chapterList = db.getChapters(manga).executeAsBlocking() + val source = sourceManager.getOrStub(manga.source) + foldersCleared += downloadManager.cleanupChapters(chapterList, manga, source) + } + launchUI { + val activity = activity ?: return@launchUI + val cleanupString = + if (foldersCleared == 0) activity.getString(R.string.no_folders_to_cleanup) + else resources!!.getQuantityString( + R.plurals.cleanup_done, + foldersCleared, + foldersCleared + ) + activity.toast(cleanupString, Toast.LENGTH_LONG) + } + } + } + // SY <-- + private fun clearChapterCache() { if (activity == null) return val files = chapterCache.cacheDir.listFiles() ?: return @@ -281,5 +332,7 @@ class SettingsAdvancedController : SettingsController() { private companion object { const val CLEAR_CACHE_KEY = "pref_clear_cache_key" + + private var job: Job? = null } } diff --git a/app/src/main/res/values/strings_sy.xml b/app/src/main/res/values/strings_sy.xml index 56382ddca..5add633e7 100644 --- a/app/src/main/res/values/strings_sy.xml +++ b/app/src/main/res/values/strings_sy.xml @@ -126,6 +126,14 @@ Hide extensions/sources that are incompatible with %1$s. Force-restart app after changing. Open debug menu IT CAN CORRUPT YOUR LIBRARY!]]> + Starting cleanup + Clean up downloaded chapters + Delete non-existent, partially downloaded, and read chapter folders + No folders to cleanup + + Cleanup done. Removed %d folder + Cleanup done. Removed %d folders + Minimal @@ -178,6 +186,7 @@ Preserve reading position on read manga Auto Webtoon Mode Use auto webtoon mode for manga that are detected to likely use the long strip format + Enable zoom out