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,12 +6,21 @@ import com.hippo.unifile.UniFile
|
|||||||
import eu.kanade.domain.download.service.DownloadPreferences
|
import eu.kanade.domain.download.service.DownloadPreferences
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
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.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
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.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@ -26,9 +35,15 @@ class DownloadCache(
|
|||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val provider: DownloadProvider = Injekt.get(),
|
private val provider: DownloadProvider = Injekt.get(),
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
private val downloadPreferences: DownloadPreferences = 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)
|
private val scope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -41,6 +56,7 @@ class DownloadCache(
|
|||||||
* The last time the cache was refreshed.
|
* The last time the cache was refreshed.
|
||||||
*/
|
*/
|
||||||
private var lastRenew = 0L
|
private var lastRenew = 0L
|
||||||
|
private var renewalJob: Job? = null
|
||||||
|
|
||||||
private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
|
private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
|
||||||
|
|
||||||
@ -134,6 +150,8 @@ class DownloadCache(
|
|||||||
|
|
||||||
// Save the chapter directory
|
// Save the chapter directory
|
||||||
mangaDir.chapterDirs += chapterDirName
|
mangaDir.chapterDirs += chapterDirName
|
||||||
|
|
||||||
|
notifyChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -151,6 +169,8 @@ class DownloadCache(
|
|||||||
mangaDir.chapterDirs -= it
|
mangaDir.chapterDirs -= it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -170,6 +190,8 @@ class DownloadCache(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -184,6 +206,8 @@ class DownloadCache(
|
|||||||
if (mangaDirName in sourceDir.mangaDirs) {
|
if (mangaDirName in sourceDir.mangaDirs) {
|
||||||
sourceDir.mangaDirs -= mangaDirName
|
sourceDir.mangaDirs -= mangaDirName
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
@ -193,6 +217,8 @@ class DownloadCache(
|
|||||||
sourceDir.delete()
|
sourceDir.delete()
|
||||||
rootDownloadsDir.sourceDirs -= source.id
|
rootDownloadsDir.sourceDirs -= source.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
notifyChanges()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -206,21 +232,28 @@ class DownloadCache(
|
|||||||
/**
|
/**
|
||||||
* Renews the downloads cache.
|
* Renews the downloads cache.
|
||||||
*/
|
*/
|
||||||
@Synchronized
|
|
||||||
private fun renewCache() {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val sources = sourceManager.getOnlineSources() + sourceManager.getStubSources()
|
renewalJob = scope.launchIO {
|
||||||
|
var sources = getSources()
|
||||||
|
|
||||||
// Ensure we try again later if no sources have been loaded
|
// Try to wait until extensions and sources have loaded
|
||||||
if (sources.isEmpty()) {
|
withTimeout(30000L) {
|
||||||
return
|
while (!extensionManager.isInitialized) {
|
||||||
|
delay(2000L)
|
||||||
}
|
}
|
||||||
|
|
||||||
val sourceDirs = rootDownloadsDir.dir.listFiles()
|
while (sources.isEmpty()) {
|
||||||
.orEmpty()
|
delay(2000L)
|
||||||
|
sources = getSources()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceDirs = rootDownloadsDir.dir.listFiles().orEmpty()
|
||||||
.associate { it.name to SourceDirectory(it) }
|
.associate { it.name to SourceDirectory(it) }
|
||||||
.mapNotNullKeys { entry ->
|
.mapNotNullKeys { entry ->
|
||||||
sources.find {
|
sources.find {
|
||||||
@ -230,52 +263,52 @@ class DownloadCache(
|
|||||||
|
|
||||||
rootDownloadsDir.sourceDirs = sourceDirs
|
rootDownloadsDir.sourceDirs = sourceDirs
|
||||||
|
|
||||||
sourceDirs.values.forEach { sourceDir ->
|
sourceDirs.values
|
||||||
val mangaDirs = sourceDir.dir.listFiles()
|
.map { sourceDir ->
|
||||||
.orEmpty()
|
async {
|
||||||
.associateNotNullKeys { it.name to MangaDirectory(it) }
|
val mangaDirs = sourceDir.dir.listFiles().orEmpty()
|
||||||
|
.filterNot { it.name.isNullOrBlank() }
|
||||||
|
.associate { it.name!! to MangaDirectory(it) }
|
||||||
|
.toMutableMap()
|
||||||
|
|
||||||
sourceDir.mangaDirs = mangaDirs
|
sourceDir.mangaDirs = mangaDirs
|
||||||
|
|
||||||
mangaDirs.values.forEach { mangaDir ->
|
mangaDirs.values.forEach { mangaDir ->
|
||||||
val chapterDirs = mangaDir.dir.listFiles()
|
val chapterDirs = mangaDir.dir.listFiles().orEmpty()
|
||||||
.orEmpty()
|
|
||||||
.mapNotNull { chapterDir ->
|
.mapNotNull { chapterDir ->
|
||||||
chapterDir.name
|
chapterDir.name
|
||||||
?.replace(".cbz", "")
|
?.replace(".cbz", "")
|
||||||
?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
|
?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
|
||||||
}
|
}
|
||||||
.toHashSet()
|
.toMutableSet()
|
||||||
|
|
||||||
mangaDir.chapterDirs = chapterDirs
|
mangaDir.chapterDirs = chapterDirs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
|
||||||
lastRenew = System.currentTimeMillis()
|
lastRenew = System.currentTimeMillis()
|
||||||
|
notifyChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.
|
* 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>()
|
val destination = LinkedHashMap<R, V>()
|
||||||
forEach { element -> transform(element)?.let { destination[it] = element.value } }
|
forEach { element -> transform(element)?.let { destination[it] = element.value } }
|
||||||
return destination
|
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(
|
private class RootDirectory(
|
||||||
val dir: UniFile,
|
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(
|
private class SourceDirectory(
|
||||||
val dir: UniFile,
|
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(
|
private class MangaDirectory(
|
||||||
val dir: UniFile,
|
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(),
|
private val preferences: SourcePreferences = Injekt.get(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
var isInitialized = false
|
||||||
|
private set
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API where all the available extensions can be found.
|
* API where all the available extensions can be found.
|
||||||
*/
|
*/
|
||||||
@ -102,6 +105,8 @@ class ExtensionManager(
|
|||||||
_untrustedExtensionsFlow.value = extensions
|
_untrustedExtensionsFlow.value = extensions
|
||||||
.filterIsInstance<LoadResult.Untrusted>()
|
.filterIsInstance<LoadResult.Untrusted>()
|
||||||
.map { it.extension }
|
.map { it.extension }
|
||||||
|
|
||||||
|
isInitialized = true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -113,7 +113,7 @@ class SourceManager(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("OverridingDeprecatedMember")
|
@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
|
override val id: Long = sourceData.id
|
||||||
|
|
||||||
@ -125,6 +125,7 @@ class SourceManager(
|
|||||||
throw getSourceNotInstalledException()
|
throw getSourceNotInstalledException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return Observable.error(getSourceNotInstalledException())
|
return Observable.error(getSourceNotInstalledException())
|
||||||
}
|
}
|
||||||
@ -133,6 +134,7 @@ class SourceManager(
|
|||||||
throw getSourceNotInstalledException()
|
throw getSourceNotInstalledException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
return Observable.error(getSourceNotInstalledException())
|
return Observable.error(getSourceNotInstalledException())
|
||||||
}
|
}
|
||||||
@ -141,6 +143,7 @@ class SourceManager(
|
|||||||
throw getSourceNotInstalledException()
|
throw getSourceNotInstalledException()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use the 1.x API instead", replaceWith = ReplaceWith("getPageList"))
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
return Observable.error(getSourceNotInstalledException())
|
return Observable.error(getSourceNotInstalledException())
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ import eu.kanade.presentation.library.components.LibraryToolbarTitle
|
|||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.models.toDomainManga
|
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.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
@ -88,6 +89,7 @@ class LibraryPresenter(
|
|||||||
private val coverCache: CoverCache = Injekt.get(),
|
private val coverCache: CoverCache = Injekt.get(),
|
||||||
private val sourceManager: SourceManager = Injekt.get(),
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
private val downloadManager: DownloadManager = Injekt.get(),
|
||||||
|
private val downloadCache: DownloadCache = Injekt.get(),
|
||||||
private val trackManager: TrackManager = Injekt.get(),
|
private val trackManager: TrackManager = Injekt.get(),
|
||||||
) : BasePresenter<LibraryController>(), LibraryState by state {
|
) : BasePresenter<LibraryController>(), LibraryState by state {
|
||||||
|
|
||||||
@ -338,7 +340,8 @@ class LibraryPresenter(
|
|||||||
val libraryMangasFlow = combine(
|
val libraryMangasFlow = combine(
|
||||||
getLibraryManga.subscribe(),
|
getLibraryManga.subscribe(),
|
||||||
libraryPreferences.downloadBadge().changes(),
|
libraryPreferences.downloadBadge().changes(),
|
||||||
) { libraryMangaList, downloadBadgePref ->
|
downloadCache.changes,
|
||||||
|
) { libraryMangaList, downloadBadgePref, _ ->
|
||||||
libraryMangaList
|
libraryMangaList
|
||||||
.map { libraryManga ->
|
.map { libraryManga ->
|
||||||
// Display mode based on user preference: take it from global library setting or category
|
// 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) {
|
if (chapter.pageLoader is HttpPageLoader) {
|
||||||
val manga = manga ?: return
|
val manga = manga ?: return
|
||||||
val dbChapter = chapter.chapter
|
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) {
|
if (isDownloaded) {
|
||||||
chapter.state = ReaderChapter.State.Wait
|
chapter.state = ReaderChapter.State.Wait
|
||||||
}
|
}
|
||||||
@ -463,6 +463,7 @@ class ReaderPresenter(
|
|||||||
nextChapter.scanlator,
|
nextChapter.scanlator,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
|
skipCache = true,
|
||||||
) || downloadManager.getChapterDownloadOrNull(nextChapter) != null
|
) || downloadManager.getChapterDownloadOrNull(nextChapter) != null
|
||||||
if (isNextChapterDownloadedOrQueued) {
|
if (isNextChapterDownloadedOrQueued) {
|
||||||
downloadAutoNextChapters(chaptersNumberToDownload, nextChapter.id, nextChapter.read)
|
downloadAutoNextChapters(chaptersNumberToDownload, nextChapter.id, nextChapter.read)
|
||||||
|
@ -57,6 +57,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
prevChapter.scanlator,
|
prevChapter.scanlator,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
|
skipCache = true,
|
||||||
)
|
)
|
||||||
val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader
|
val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader
|
||||||
binding.upperText.text = buildSpannedString {
|
binding.upperText.text = buildSpannedString {
|
||||||
@ -94,6 +95,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
|
|||||||
nextChapter.scanlator,
|
nextChapter.scanlator,
|
||||||
manga.title,
|
manga.title,
|
||||||
manga.source,
|
manga.source,
|
||||||
|
skipCache = true,
|
||||||
)
|
)
|
||||||
binding.upperText.text = buildSpannedString {
|
binding.upperText.text = buildSpannedString {
|
||||||
bold { append(context.getString(R.string.transition_finished)) }
|
bold { append(context.getString(R.string.transition_finished)) }
|
||||||
|
Loading…
Reference in New Issue
Block a user