diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadCache.kt index 94016c1208..a22286a9cb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadCache.kt @@ -1,6 +1,8 @@ package eu.kanade.tachiyomi.data.download.anime +import android.app.Application import android.content.Context +import android.net.Uri import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.animesource.AnimeSource import eu.kanade.tachiyomi.extension.anime.AnimeExtensionManager @@ -12,6 +14,8 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.debounce @@ -26,7 +30,15 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeoutOrNull +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromByteArray +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.protobuf.ProtoBuf import logcat.LogPriority import tachiyomi.core.common.storage.extension @@ -212,27 +224,28 @@ class AnimeDownloadCache( * @param animeUniFile the directory of the anime. * @param anime the anime of the episode. */ - @Synchronized - fun addEpisode(episodeDirName: String, animeUniFile: UniFile, anime: Anime) { - // Retrieve the cached source directory or cache a new one - var sourceDir = rootDownloadsDir.sourceDirs[anime.source] - if (sourceDir == null) { - val source = sourceManager.get(anime.source) ?: return - val sourceUniFile = provider.findSourceDir(source) ?: return - sourceDir = SourceDirectory(sourceUniFile) - rootDownloadsDir.sourceDirs += anime.source to sourceDir - } + suspend fun addEpisode(episodeDirName: String, animeUniFile: UniFile, anime: Anime) { + rootDownloadsDirLock.withLock { + // Retrieve the cached source directory or cache a new one + var sourceDir = rootDownloadsDir.sourceDirs[anime.source] + if (sourceDir == null) { + val source = sourceManager.get(anime.source) ?: return + val sourceUniFile = provider.findSourceDir(source) ?: return + sourceDir = SourceDirectory(sourceUniFile) + rootDownloadsDir.sourceDirs += anime.source to sourceDir + } - // Retrieve the cached anime directory or cache a new one - val animeDirName = provider.getAnimeDirName(anime.title) - var animeDir = sourceDir.animeDirs[animeDirName] - if (animeDir == null) { - animeDir = AnimeDirectory(animeUniFile) - sourceDir.animeDirs += animeDirName to animeDir - } + // Retrieve the cached anime directory or cache a new one + val animeDirName = provider.getAnimeDirName(anime.title) + var animeDir = sourceDir.animeDirs[animeDirName] + if (animeDir == null) { + animeDir = AnimeDirectory(animeUniFile) + sourceDir.animeDirs += animeDirName to animeDir + } - // Save the episode directory - animeDir.episodeDirs += episodeDirName + // Save the chapter directory + animeDir.episodeDirs += episodeDirName + } notifyChanges() } @@ -243,15 +256,18 @@ class AnimeDownloadCache( * @param episode the episode to remove. * @param anime the anime of the episode. */ - @Synchronized - fun removeEpisode(episode: Episode, anime: Anime) { - val sourceDir = rootDownloadsDir.sourceDirs[anime.source] ?: return - val animeDir = sourceDir.animeDirs[provider.getAnimeDirName(anime.title)] ?: return - provider.getValidEpisodeDirNames(episode.name, episode.scanlator).forEach { - if (it in animeDir.episodeDirs) { - animeDir.episodeDirs -= it + suspend fun removeEpisode(episode: Episode, anime: Anime) { + rootDownloadsDirLock.withLock { + val sourceDir = rootDownloadsDir.sourceDirs[anime.source] ?: return + val animeDir = sourceDir.animeDirs[provider.getAnimeDirName(anime.title)] ?: return + provider.getValidEpisodeDirNames(episode.name, episode.scanlator).forEach { + if (it in animeDir.episodeDirs) { + animeDir.episodeDirs -= it + } } } + + notifyChanges() } /** @@ -260,17 +276,19 @@ class AnimeDownloadCache( * @param episodes the list of episode to remove. * @param anime the anime of the episode. */ - @Synchronized - fun removeEpisodes(episodes: List, anime: Anime) { - val sourceDir = rootDownloadsDir.sourceDirs[anime.source] ?: return - val animeDir = sourceDir.animeDirs[provider.getAnimeDirName(anime.title)] ?: return - episodes.forEach { episode -> - provider.getValidEpisodeDirNames(episode.name, episode.scanlator).forEach { - if (it in animeDir.episodeDirs) { - animeDir.episodeDirs -= it + suspend fun removeEpisodes(episodes: List, anime: Anime) { + rootDownloadsDirLock.withLock { + val sourceDir = rootDownloadsDir.sourceDirs[anime.source] ?: return + val animeDir = sourceDir.animeDirs[provider.getAnimeDirName(anime.title)] ?: return + episodes.forEach { episode -> + provider.getValidEpisodeDirNames(episode.name, episode.scanlator).forEach { + if (it in animeDir.episodeDirs) { + animeDir.episodeDirs -= it + } } } } + notifyChanges() } @@ -279,19 +297,22 @@ class AnimeDownloadCache( * * @param anime the anime to remove. */ - @Synchronized - fun removeAnime(anime: Anime) { - val sourceDir = rootDownloadsDir.sourceDirs[anime.source] ?: return - val animeDirName = provider.getAnimeDirName(anime.title) - if (sourceDir.animeDirs.containsKey(animeDirName)) { - sourceDir.animeDirs -= animeDirName + suspend fun removeAnime(anime: Anime) { + rootDownloadsDirLock.withLock { + val sourceDir = rootDownloadsDir.sourceDirs[anime.source] ?: return + val animeDirName = provider.getAnimeDirName(anime.title) + if (sourceDir.animeDirs.containsKey(animeDirName)) { + sourceDir.animeDirs -= animeDirName + } } notifyChanges() } - fun removeSource(source: AnimeSource) { - rootDownloadsDir.sourceDirs -= source.id + suspend fun removeSource(source: AnimeSource) { + rootDownloadsDirLock.withLock { + rootDownloadsDir.sourceDirs -= source.id + } notifyChanges() } @@ -340,7 +361,6 @@ class AnimeDownloadCache( sourceId?.let { it to SourceDirectory(dir) } } .toMap() - .let { ConcurrentHashMap(it) } rootDownloadsDir.sourceDirs = sourceDirs @@ -353,7 +373,7 @@ class AnimeDownloadCache( sourceDir.animeDirs = ConcurrentHashMap(animeDirs) - animeDirs.values.forEach { animeDir -> + sourceDir.animeDirs.values.forEach { animeDir -> val episodeDirs = animeDir.dir?.listFiles().orEmpty() .mapNotNull { when { @@ -384,7 +404,6 @@ class AnimeDownloadCache( if (exception != null && exception !is CancellationException) { logcat(LogPriority.ERROR, exception) { "Failed to create download cache" } } - lastRenew = System.currentTimeMillis() notifyChanges() } @@ -402,29 +421,78 @@ class AnimeDownloadCache( scope.launchNonCancellable { _changes.send(Unit) } + updateDiskCache() + } + + private var updateDiskCacheJob: Job? = null + + @Suppress("MagicNumber") + private fun updateDiskCache() { + updateDiskCacheJob?.cancel() + updateDiskCacheJob = scope.launchIO { + delay(1000) + ensureActive() + val bytes = ProtoBuf.encodeToByteArray(rootDownloadsDir) + ensureActive() + try { + diskCacheFile.writeBytes(bytes) + } catch (e: Throwable) { + logcat( + priority = LogPriority.ERROR, + throwable = e, + message = { "Failed to write disk cache file" }, + ) + } + } } } /** * Class to store the files under the root downloads directory. */ +@Serializable private class RootDirectory( + @Serializable(with = UniFileAsStringSerializer::class) val dir: UniFile?, - var sourceDirs: ConcurrentHashMap = ConcurrentHashMap(), + var sourceDirs: Map = mapOf(), ) /** * Class to store the files under a source directory. */ +@Serializable private class SourceDirectory( + @Serializable(with = UniFileAsStringSerializer::class) val dir: UniFile?, - var animeDirs: ConcurrentHashMap = ConcurrentHashMap(), + var animeDirs: Map = mapOf(), ) /** * Class to store the files under a manga directory. */ +@Serializable private class AnimeDirectory( + @Serializable(with = UniFileAsStringSerializer::class) val dir: UniFile?, var episodeDirs: MutableSet = mutableSetOf(), ) + +private object UniFileAsStringSerializer : KSerializer { + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("UniFile", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: UniFile?) { + return if (value == null) { + encoder.encodeNull() + } else { + encoder.encodeString(value.uri.toString()) + } + } + + override fun deserialize(decoder: Decoder): UniFile? { + return if (decoder.decodeNotNullMark()) { + UniFile.fromUri(Injekt.get(), Uri.parse(decoder.decodeString())) + } else { + decoder.decodeNull() + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt index 4e01afb9ac..572824ac00 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloadManager.kt @@ -384,7 +384,7 @@ class AnimeDownloadManager( * @param oldEpisode the existing episode with the old name. * @param newEpisode the target episode with the new name. */ - fun renameEpisode(source: AnimeSource, anime: Anime, oldEpisode: Episode, newEpisode: Episode) { + suspend fun renameEpisode(source: AnimeSource, anime: Anime, oldEpisode: Episode, newEpisode: Episode) { val oldNames = provider.getValidEpisodeDirNames(oldEpisode.name, oldEpisode.scanlator) val animeDir = provider.getAnimeDir(anime.title, source) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt index ecd27627db..c8a2640b16 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/anime/AnimeDownloader.kt @@ -1244,7 +1244,7 @@ class AnimeDownloader( * @param tmpDir the directory where the download is currently stored. * @param dirname the real (non temporary) directory name of the download. */ - private fun ensureSuccessfulAnimeDownload( + private suspend fun ensureSuccessfulAnimeDownload( download: AnimeDownload, animeDir: UniFile, tmpDir: UniFile, diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadCache.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadCache.kt index e017eda732..f477810cb6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadCache.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/manga/MangaDownloadCache.kt @@ -250,6 +250,7 @@ class MangaDownloadCache( // Save the chapter directory mangaDir.chapterDirs += chapterDirName } + notifyChanges() } @@ -279,7 +280,6 @@ class MangaDownloadCache( * @param chapters the list of chapter to remove. * @param manga the manga of the chapter. */ - suspend fun removeChapters(chapters: List, manga: Manga) { rootDownloadsDirLock.withLock { val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return @@ -375,7 +375,7 @@ class MangaDownloadCache( sourceDir.mangaDirs = ConcurrentHashMap(mangaDirs) - mangaDirs.values.forEach { mangaDir -> + sourceDir.mangaDirs.values.forEach { mangaDir -> val chapterDirs = mangaDir.dir?.listFiles().orEmpty() .mapNotNull { when {