mirror of
https://git.mihon.tech/mihonapp/mihon
synced 2024-11-26 23:28:58 +03: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:
parent
93925a7286
commit
7e40680af0
6 changed files with 99 additions and 52 deletions
|
@ -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)) }
|
||||
|
|
Loading…
Reference in a new issue