소스 검색

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
arkon 2 년 전
부모
커밋
7e40680af0

+ 84 - 51
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<Long> = 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
-
-        sourceDirs.values.forEach { sourceDir ->
-            val mangaDirs = sourceDir.dir.listFiles()
-                .orEmpty()
-                .associateNotNullKeys { it.name to MangaDirectory(it) }
-
-            sourceDir.mangaDirs = mangaDirs
+            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
+                }
 
-            mangaDirs.values.forEach { mangaDir ->
-                val chapterDirs = mangaDir.dir.listFiles()
-                    .orEmpty()
-                    .mapNotNull { chapterDir ->
-                        chapterDir.name
-                            ?.replace(".cbz", "")
-                            ?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
+            rootDownloadsDir.sourceDirs = sourceDirs
+
+            sourceDirs.values
+                .map { sourceDir ->
+                    async {
+                        val mangaDirs = sourceDir.dir.listFiles().orEmpty()
+                            .filterNot { it.name.isNullOrBlank() }
+                            .associate { it.name!! to MangaDirectory(it) }
+                            .toMutableMap()
+
+                        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<Source> {
+        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 <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): Map<R, V> {
+    private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): MutableMap<R, V> {
         val destination = LinkedHashMap<R, V>()
         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 <T, K, V> Array<T>.associateNotNullKeys(transform: (T) -> Pair<K?, V>): Map<K, V> {
-        val destination = LinkedHashMap<K, V>()
-        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<Long, SourceDirectory> = hashMapOf(),
+    var sourceDirs: MutableMap<Long, SourceDirectory> = mutableMapOf(),
 )
 
 /**
@@ -291,7 +324,7 @@ private class RootDirectory(
  */
 private class SourceDirectory(
     val dir: UniFile,
-    var mangaDirs: Map<String, MangaDirectory> = hashMapOf(),
+    var mangaDirs: MutableMap<String, MangaDirectory> = mutableMapOf(),
 )
 
 /**
@@ -299,5 +332,5 @@ private class SourceDirectory(
  */
 private class MangaDirectory(
     val dir: UniFile,
-    var chapterDirs: Set<String> = hashSetOf(),
+    var chapterDirs: MutableSet<String> = mutableSetOf(),
 )

+ 5 - 0
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<LoadResult.Untrusted>()
             .map { it.extension }
+
+        isInitialized = true
     }
 
     /**

+ 4 - 1
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<SManga> {
             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<List<SChapter>> {
             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<List<Page>> {
             return Observable.error(getSourceNotInstalledException())
         }

+ 4 - 1
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<LibraryController>(), 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

+ 2 - 1
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)

+ 2 - 0
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)) }