mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	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
This commit is contained in:
		| @@ -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 | ||||
|             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<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(), | ||||
| ) | ||||
|   | ||||
| @@ -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 | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -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()) | ||||
|         } | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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) | ||||
|   | ||||
| @@ -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)) } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user