From 7e40680af02505f82fa3655d2d693092c6bd43a1 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 21 Oct 2022 15:00:41 -0400 Subject: [PATCH] Perform download cache renewal async Don't block on cache renewals, but notify library on updates so that the badges show up when ready. We skip the cache when checking if a chapter is downloaded for the reader assuming that it's a relatively low cost to check for a single chapter. (Probably) fixes #8254 / fixes #7847 --- .../tachiyomi/data/download/DownloadCache.kt | 131 +++++++++++------- .../tachiyomi/extension/ExtensionManager.kt | 5 + .../kanade/tachiyomi/source/SourceManager.kt | 5 +- .../tachiyomi/ui/library/LibraryPresenter.kt | 5 +- .../tachiyomi/ui/reader/ReaderPresenter.kt | 3 +- .../ui/reader/viewer/ReaderTransitionView.kt | 2 + 6 files changed, 99 insertions(+), 52 deletions(-) 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 34d549630..2bb4bd987 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 @@ -6,12 +6,21 @@ import com.hippo.unifile.UniFile import eu.kanade.domain.download.service.DownloadPreferences import eu.kanade.domain.manga.model.Manga import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.util.lang.launchIO import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withTimeout import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.TimeUnit @@ -26,9 +35,15 @@ class DownloadCache( private val context: Context, private val provider: DownloadProvider = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), + private val extensionManager: ExtensionManager = Injekt.get(), private val downloadPreferences: DownloadPreferences = Injekt.get(), ) { + // This is just a mechanism of notifying consumers of updates to the cache, the value itself + // is meaningless. + private val _state: MutableStateFlow = MutableStateFlow(0L) + val changes = _state.asStateFlow() + private val scope = CoroutineScope(Dispatchers.IO) /** @@ -41,6 +56,7 @@ class DownloadCache( * The last time the cache was refreshed. */ private var lastRenew = 0L + private var renewalJob: Job? = null private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference()) @@ -134,6 +150,8 @@ class DownloadCache( // Save the chapter directory mangaDir.chapterDirs += chapterDirName + + notifyChanges() } /** @@ -151,6 +169,8 @@ class DownloadCache( mangaDir.chapterDirs -= it } } + + notifyChanges() } /** @@ -170,6 +190,8 @@ class DownloadCache( } } } + + notifyChanges() } /** @@ -184,6 +206,8 @@ class DownloadCache( if (mangaDirName in sourceDir.mangaDirs) { sourceDir.mangaDirs -= mangaDirName } + + notifyChanges() } @Synchronized @@ -193,6 +217,8 @@ class DownloadCache( sourceDir.delete() rootDownloadsDir.sourceDirs -= source.id } + + notifyChanges() } /** @@ -206,76 +232,83 @@ class DownloadCache( /** * Renews the downloads cache. */ - @Synchronized private fun renewCache() { - if (lastRenew + renewInterval >= System.currentTimeMillis()) { + // Avoid renewing cache if in the process nor too often + if (lastRenew + renewInterval >= System.currentTimeMillis() || renewalJob?.isActive == true) { return } - val sources = sourceManager.getOnlineSources() + sourceManager.getStubSources() + renewalJob = scope.launchIO { + var sources = getSources() - // Ensure we try again later if no sources have been loaded - if (sources.isEmpty()) { - return - } + // Try to wait until extensions and sources have loaded + withTimeout(30000L) { + while (!extensionManager.isInitialized) { + delay(2000L) + } - val sourceDirs = rootDownloadsDir.dir.listFiles() - .orEmpty() - .associate { it.name to SourceDirectory(it) } - .mapNotNullKeys { entry -> - sources.find { - provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) - }?.id + while (sources.isEmpty()) { + delay(2000L) + sources = getSources() + } } - rootDownloadsDir.sourceDirs = sourceDirs + val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty() + .associate { it.name to SourceDirectory(it) } + .mapNotNullKeys { entry -> + sources.find { + provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) + }?.id + } - sourceDirs.values.forEach { sourceDir -> - val mangaDirs = sourceDir.dir.listFiles() - .orEmpty() - .associateNotNullKeys { it.name to MangaDirectory(it) } + rootDownloadsDir.sourceDirs = sourceDirs - sourceDir.mangaDirs = mangaDirs + sourceDirs.values + .map { sourceDir -> + async { + val mangaDirs = sourceDir.dir.listFiles().orEmpty() + .filterNot { it.name.isNullOrBlank() } + .associate { it.name!! to MangaDirectory(it) } + .toMutableMap() - mangaDirs.values.forEach { mangaDir -> - val chapterDirs = mangaDir.dir.listFiles() - .orEmpty() - .mapNotNull { chapterDir -> - chapterDir.name - ?.replace(".cbz", "") - ?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) } + sourceDir.mangaDirs = mangaDirs + + mangaDirs.values.forEach { mangaDir -> + val chapterDirs = mangaDir.dir.listFiles().orEmpty() + .mapNotNull { chapterDir -> + chapterDir.name + ?.replace(".cbz", "") + ?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) } + } + .toMutableSet() + + mangaDir.chapterDirs = chapterDirs + } } - .toHashSet() + } + .awaitAll() - mangaDir.chapterDirs = chapterDirs - } + lastRenew = System.currentTimeMillis() + notifyChanges() } + } - lastRenew = System.currentTimeMillis() + private fun getSources(): List { + return sourceManager.getOnlineSources() + sourceManager.getStubSources() + } + + private fun notifyChanges() { + _state.value += 1 } /** * Returns a new map containing only the key entries of [transform] that are not null. */ - private inline fun Map.mapNotNullKeys(transform: (Map.Entry) -> R?): Map { + private inline fun Map.mapNotNullKeys(transform: (Map.Entry) -> R?): MutableMap { val destination = LinkedHashMap() forEach { element -> transform(element)?.let { destination[it] = element.value } } return destination } - - /** - * Returns a map from a list containing only the key entries of [transform] that are not null. - */ - private inline fun Array.associateNotNullKeys(transform: (T) -> Pair): Map { - val destination = LinkedHashMap() - for (element in this) { - val (key, value) = transform(element) - if (key != null) { - destination[key] = value - } - } - return destination - } } /** @@ -283,7 +316,7 @@ class DownloadCache( */ private class RootDirectory( val dir: UniFile, - var sourceDirs: Map = hashMapOf(), + var sourceDirs: MutableMap = mutableMapOf(), ) /** @@ -291,7 +324,7 @@ private class RootDirectory( */ private class SourceDirectory( val dir: UniFile, - var mangaDirs: Map = hashMapOf(), + var mangaDirs: MutableMap = mutableMapOf(), ) /** @@ -299,5 +332,5 @@ private class SourceDirectory( */ private class MangaDirectory( val dir: UniFile, - var chapterDirs: Set = hashSetOf(), + var chapterDirs: MutableSet = mutableSetOf(), ) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt index cfd1bdd35..52e27bf8b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/ExtensionManager.kt @@ -42,6 +42,9 @@ class ExtensionManager( private val preferences: SourcePreferences = Injekt.get(), ) { + var isInitialized = false + private set + /** * API where all the available extensions can be found. */ @@ -102,6 +105,8 @@ class ExtensionManager( _untrustedExtensionsFlow.value = extensions .filterIsInstance() .map { it.extension } + + isInitialized = true } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt index 1f975b8b1..94f03372e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/SourceManager.kt @@ -113,7 +113,7 @@ class SourceManager( } @Suppress("OverridingDeprecatedMember") - open inner class StubSource(val sourceData: SourceData) : Source { + open inner class StubSource(private val sourceData: SourceData) : Source { override val id: Long = sourceData.id @@ -125,6 +125,7 @@ class SourceManager( throw getSourceNotInstalledException() } + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails")) override fun fetchMangaDetails(manga: SManga): Observable { return Observable.error(getSourceNotInstalledException()) } @@ -133,6 +134,7 @@ class SourceManager( throw getSourceNotInstalledException() } + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList")) override fun fetchChapterList(manga: SManga): Observable> { return Observable.error(getSourceNotInstalledException()) } @@ -141,6 +143,7 @@ class SourceManager( throw getSourceNotInstalledException() } + @Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList")) override fun fetchPageList(chapter: SChapter): Observable> { return Observable.error(getSourceNotInstalledException()) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 63c30495e..59c93a4a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -39,6 +39,7 @@ import eu.kanade.presentation.library.components.LibraryToolbarTitle import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.models.toDomainManga +import eu.kanade.tachiyomi.data.download.DownloadCache import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.source.SourceManager @@ -88,6 +89,7 @@ class LibraryPresenter( private val coverCache: CoverCache = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(), private val downloadManager: DownloadManager = Injekt.get(), + private val downloadCache: DownloadCache = Injekt.get(), private val trackManager: TrackManager = Injekt.get(), ) : BasePresenter(), LibraryState by state { @@ -338,7 +340,8 @@ class LibraryPresenter( val libraryMangasFlow = combine( getLibraryManga.subscribe(), libraryPreferences.downloadBadge().changes(), - ) { libraryMangaList, downloadBadgePref -> + downloadCache.changes, + ) { libraryMangaList, downloadBadgePref, _ -> libraryMangaList .map { libraryManga -> // Display mode based on user preference: take it from global library setting or category diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index 65df9c871..02589dbc5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -392,7 +392,7 @@ class ReaderPresenter( if (chapter.pageLoader is HttpPageLoader) { val manga = manga ?: return val dbChapter = chapter.chapter - val isDownloaded = downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source) + val isDownloaded = downloadManager.isChapterDownloaded(dbChapter.name, dbChapter.scanlator, manga.title, manga.source, skipCache = true) if (isDownloaded) { chapter.state = ReaderChapter.State.Wait } @@ -463,6 +463,7 @@ class ReaderPresenter( nextChapter.scanlator, manga.title, manga.source, + skipCache = true, ) || downloadManager.getChapterDownloadOrNull(nextChapter) != null if (isNextChapterDownloadedOrQueued) { downloadAutoNextChapters(chaptersNumberToDownload, nextChapter.id, nextChapter.read) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt index c46a1fa9c..8dfd3b6d0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt @@ -57,6 +57,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At prevChapter.scanlator, manga.title, manga.source, + skipCache = true, ) val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader binding.upperText.text = buildSpannedString { @@ -94,6 +95,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At nextChapter.scanlator, manga.title, manga.source, + skipCache = true, ) binding.upperText.text = buildSpannedString { bold { append(context.getString(R.string.transition_finished)) }