diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt index c82e4f2ee9..873484ff91 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreService.kt @@ -43,6 +43,7 @@ import uy.kohesive.injekt.injectLazy import java.io.File import java.text.SimpleDateFormat import java.util.Locale +import java.util.concurrent.TimeUnit /** * Restores backup from json file @@ -117,7 +118,7 @@ class BackupRestoreService : Service() { startForeground(Notifications.ID_RESTORE_PROGRESS, progressNotification.build()) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock") - wakeLock.acquire() + wakeLock.acquire(TimeUnit.HOURS.toMillis(3)) } /** 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 9061a997ef..c54d8db8f9 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 @@ -217,6 +217,17 @@ class DownloadCache( } } + 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 + } + } + } + + /** * Removes a manga that has 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 f2470be65a..4e6622ea21 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -219,16 +219,21 @@ class DownloadManager(val context: Context) { * @param manga the manga of the chapters. * @param source the source of the chapters. */ - fun cleanupChapters(allChapters: List, manga: Manga, source: Source) { + 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 } /** 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 2cce3e479d..c12aa1c639 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadProvider.kt @@ -142,13 +142,11 @@ class DownloadProvider(private val context: Context) { 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) + "${getChapterDirName(chp)}_tmp").any { dir -> - mangaDir.findFile( - dir - ) != null + (chapters.find { chp -> + getValidChapterDirNames(chp).any { dir -> + mangaDir.findFile(dir) != null } - } == null + } == null) || it.name?.endsWith("_tmp") == true } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 8b9c8e635b..9029ea5510 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -40,9 +40,6 @@ import eu.kanade.tachiyomi.util.notification import eu.kanade.tachiyomi.util.notificationManager import eu.kanade.tachiyomi.util.syncChaptersWithSource import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch import rx.Observable import rx.Subscription import rx.schedulers.Schedulers @@ -52,6 +49,7 @@ import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.util.ArrayList import java.util.Date +import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger /** @@ -80,8 +78,6 @@ class LibraryUpdateService( */ private var subscription: Subscription? = null - var job: Job? = null - /** * Pending intent of action that cancels the library update @@ -116,8 +112,7 @@ class LibraryUpdateService( enum class Target { CHAPTERS, // Manga chapters DETAILS, // Manga metadata - TRACKING, // Tracking metadata - CLEANUP // Clean up downloads + TRACKING // Tracking metadata } companion object { @@ -184,7 +179,7 @@ class LibraryUpdateService( startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build()) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock") - wakeLock.acquire() + wakeLock.acquire(TimeUnit.MINUTES.toMillis(30)) } /** @@ -222,22 +217,16 @@ class LibraryUpdateService( // Unsubscribe from any previous subscription if needed. subscription?.unsubscribe() - val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() - // Update favorite manga. Destroy service when completed or in case of an error. - val mangaList = getMangaToUpdate(intent, target) - .sortedWith(rankingScheme[selectedScheme]) val handler = CoroutineExceptionHandler { _, exception -> Timber.e(exception) stopSelf(startId) } // Update either chapter list or manga details. - if (target == Target.CLEANUP) { - job = GlobalScope.launch(handler) { - cleanupDownloads() - } - job?.invokeOnCompletion { stopSelf(startId) } - } else { + val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault() + // Update favorite manga. Destroy service when completed or in case of an error. + val mangaList = getMangaToUpdate(intent, target) + .sortedWith(rankingScheme[selectedScheme]) subscription = Observable.defer { when (target) { Target.CHAPTERS -> updateChapterList(mangaList) @@ -250,7 +239,6 @@ class LibraryUpdateService( }, { stopSelf(startId) }) - } return START_REDELIVER_INTENT } @@ -360,11 +348,13 @@ class LibraryUpdateService( private fun cleanupDownloads() { val mangaList = db.getMangas().executeAsBlocking() + var foldersCleared = 0 for (manga in mangaList) { val chapterList = db.getChapters(manga).executeAsBlocking() val source = sourceManager.getOrStub(manga.source) - downloadManager.cleanupChapters(chapterList, manga, source) + foldersCleared += downloadManager.cleanupChapters(chapterList, manga, source) } + } fun downloadChapters(manga: Manga, chapters: List) { 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 0e48f2edce..421693fff6 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 @@ -4,21 +4,33 @@ import android.app.Dialog import android.os.Bundle import androidx.preference.PreferenceScreen import android.view.View +import android.widget.Toast import com.afollestad.materialdialogs.MaterialDialog import com.bluelinelabs.conductor.RouterTransaction import com.bluelinelabs.conductor.changehandler.FadeChangeHandler 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.PreferencesHelper import eu.kanade.tachiyomi.network.NetworkHelper +import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.library.LibraryController +import eu.kanade.tachiyomi.util.launchUI import eu.kanade.tachiyomi.util.toast +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() { @@ -74,10 +86,33 @@ class SettingsAdvancedController : SettingsController() { summaryRes = R.string.pref_clean_downloads_summary - onClick { LibraryUpdateService.start(context, target = Target.CLEANUP) } + onClick { cleanupDownloads() } } } + 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_cleanup_done) + else resources!!.getQuantityString(R.plurals.cleanup_done, foldersCleared, foldersCleared) + activity.toast(cleanupString, Toast.LENGTH_LONG) + } + } + + } + private fun clearChapterCache() { if (activity == null) return val files = chapterCache.cacheDir.listFiles() ?: return @@ -128,5 +163,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.xml b/app/src/main/res/values/strings.xml index 3e32aba0c8..6376cf9897 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -355,9 +355,16 @@ Refresh tracking metadata Updates status, score and last chapter read from the tracking services Clean up downloaded chapters - Deletes non-existent, partially downloaded, + Delete non-existent, partially downloaded, and read chapter folders + Starting cleanup + No folders to cleanup + + Cleanup done. Removed %d folder + Cleanup done. Removed %d folders + + Version Build time