Always renew download cache if no sources have been loaded yet

Fixes #7705. Somewhat janky solution to handle when loading the sources on app start is slower than
the initial download cache priming.
This commit is contained in:
arkon 2022-10-16 16:30:51 -04:00
parent d6cbff2837
commit 558aad1a71

View file

@ -14,7 +14,7 @@ import java.util.concurrent.TimeUnit
/** /**
* Cache where we dump the downloads directory from the filesystem. This class is needed because * Cache where we dump the downloads directory from the filesystem. This class is needed because
* directory checking is expensive and it slowdowns the app. The cache is invalidated by the time * directory checking is expensive and it slows down the app. The cache is invalidated by the time
* defined in [renewInterval] as we don't have any control over the filesystem and the user can * defined in [renewInterval] as we don't have any control over the filesystem and the user can
* delete the folders at any time without the app noticing. * delete the folders at any time without the app noticing.
* *
@ -41,27 +41,16 @@ class DownloadCache(
*/ */
private var lastRenew = 0L private var lastRenew = 0L
/** private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
* The root directory for downloads.
*/
private var rootDir = RootDirectory(getDirectoryFromPreference())
init { init {
downloadPreferences.downloadsDirectory().changes() downloadPreferences.downloadsDirectory().changes()
.onEach { .onEach {
lastRenew = 0L // invalidate cache lastRenew = 0L // invalidate cache
rootDir = RootDirectory(getDirectoryFromPreference()) rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
} }
} }
/**
* Returns the downloads directory from the user's preferences.
*/
private fun getDirectoryFromPreference(): UniFile {
val dir = downloadPreferences.downloadsDirectory().get()
return UniFile.fromUri(context, dir.toUri())
}
/** /**
* Returns true if the chapter is downloaded. * Returns true if the chapter is downloaded.
* *
@ -83,9 +72,9 @@ class DownloadCache(
return provider.findChapterDir(chapterName, chapterScanlator, mangaTitle, source) != null return provider.findChapterDir(chapterName, chapterScanlator, mangaTitle, source) != null
} }
checkRenew() renewCache()
val sourceDir = rootDir.files[sourceId] val sourceDir = rootDownloadsDir.files[sourceId]
if (sourceDir != null) { if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(mangaTitle)] val mangaDir = sourceDir.files[provider.getMangaDirName(mangaTitle)]
if (mangaDir != null) { if (mangaDir != null) {
@ -101,9 +90,9 @@ class DownloadCache(
* @param manga the manga to check. * @param manga the manga to check.
*/ */
fun getDownloadCount(manga: Manga): Int { fun getDownloadCount(manga: Manga): Int {
checkRenew() renewCache()
val sourceDir = rootDir.files[manga.source] val sourceDir = rootDownloadsDir.files[manga.source]
if (sourceDir != null) { if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(manga.title)] val mangaDir = sourceDir.files[provider.getMangaDirName(manga.title)]
if (mangaDir != null) { if (mangaDir != null) {
@ -113,54 +102,6 @@ class DownloadCache(
return 0 return 0
} }
/**
* Checks if the cache needs a renewal and performs it if needed.
*/
@Synchronized
private fun checkRenew() {
if (lastRenew + renewInterval < System.currentTimeMillis()) {
renew()
lastRenew = System.currentTimeMillis()
}
}
/**
* Renews the downloads cache.
*/
private fun renew() {
val sources = sourceManager.getOnlineSources() + sourceManager.getStubSources()
val sourceDirs = rootDir.dir.listFiles()
.orEmpty()
.associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry ->
sources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id
}
rootDir.files = sourceDirs
sourceDirs.values.forEach { sourceDir ->
val mangaDirs = sourceDir.dir.listFiles()
.orEmpty()
.associateNotNullKeys { it.name to MangaDirectory(it) }
sourceDir.files = mangaDirs
mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles()
.orEmpty()
.mapNotNull { chapterDir ->
chapterDir.name
?.replace(".cbz", "")
?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
}
.toHashSet()
mangaDir.files = chapterDirs
}
}
}
/** /**
* Adds a chapter that has just been download to this cache. * Adds a chapter that has just been download to this cache.
* *
@ -171,12 +112,12 @@ class DownloadCache(
@Synchronized @Synchronized
fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) { fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
// Retrieve the cached source directory or cache a new one // Retrieve the cached source directory or cache a new one
var sourceDir = rootDir.files[manga.source] var sourceDir = rootDownloadsDir.files[manga.source]
if (sourceDir == null) { if (sourceDir == null) {
val source = sourceManager.get(manga.source) ?: return val source = sourceManager.get(manga.source) ?: return
val sourceUniFile = provider.findSourceDir(source) ?: return val sourceUniFile = provider.findSourceDir(source) ?: return
sourceDir = SourceDirectory(sourceUniFile) sourceDir = SourceDirectory(sourceUniFile)
rootDir.files += manga.source to sourceDir rootDownloadsDir.files += manga.source to sourceDir
} }
// Retrieve the cached manga directory or cache a new one // Retrieve the cached manga directory or cache a new one
@ -199,7 +140,7 @@ class DownloadCache(
*/ */
@Synchronized @Synchronized
fun removeChapter(chapter: Chapter, manga: Manga) { fun removeChapter(chapter: Chapter, manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return val sourceDir = rootDownloadsDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga.title)] ?: return val mangaDir = sourceDir.files[provider.getMangaDirName(manga.title)] ?: return
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach { provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
if (it in mangaDir.files) { if (it in mangaDir.files) {
@ -216,7 +157,7 @@ class DownloadCache(
*/ */
@Synchronized @Synchronized
fun removeChapters(chapters: List<Chapter>, manga: Manga) { fun removeChapters(chapters: List<Chapter>, manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return val sourceDir = rootDownloadsDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga.title)] ?: return val mangaDir = sourceDir.files[provider.getMangaDirName(manga.title)] ?: return
chapters.forEach { chapter -> chapters.forEach { chapter ->
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach { provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
@ -234,13 +175,94 @@ class DownloadCache(
*/ */
@Synchronized @Synchronized
fun removeManga(manga: Manga) { fun removeManga(manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return val sourceDir = rootDownloadsDir.files[manga.source] ?: return
val mangaDirName = provider.getMangaDirName(manga.title) val mangaDirName = provider.getMangaDirName(manga.title)
if (mangaDirName in sourceDir.files) { if (mangaDirName in sourceDir.files) {
sourceDir.files -= mangaDirName sourceDir.files -= mangaDirName
} }
} }
/**
* Returns the downloads directory from the user's preferences.
*/
private fun getDirectoryFromPreference(): UniFile {
val dir = downloadPreferences.downloadsDirectory().get()
return UniFile.fromUri(context, dir.toUri())
}
/**
* Renews the downloads cache.
*/
@Synchronized
private fun renewCache() {
if (lastRenew + renewInterval >= System.currentTimeMillis()) {
return
}
val sources = sourceManager.getOnlineSources() + sourceManager.getStubSources()
// Ensure we try again later if no sources have been loaded
if (sources.isEmpty()) {
return
}
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
}
rootDownloadsDir.files = sourceDirs
sourceDirs.values.forEach { sourceDir ->
val mangaDirs = sourceDir.dir.listFiles()
.orEmpty()
.associateNotNullKeys { it.name to MangaDirectory(it) }
sourceDir.files = mangaDirs
mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles()
.orEmpty()
.mapNotNull { chapterDir ->
chapterDir.name
?.replace(".cbz", "")
?.takeUnless { it.endsWith(Downloader.TMP_DIR_SUFFIX) }
}
.toHashSet()
mangaDir.files = chapterDirs
}
}
lastRenew = System.currentTimeMillis()
}
/**
* 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> {
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
}
}
/** /**
* Class to store the files under the root downloads directory. * Class to store the files under the root downloads directory.
*/ */
@ -264,27 +286,3 @@ class DownloadCache(
val dir: UniFile, val dir: UniFile,
var files: Set<String> = hashSetOf(), var files: Set<String> = hashSetOf(),
) )
/**
* 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> {
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
}
}