|
@@ -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(),
|
|
|
)
|