Feature/decouple thumbnail downloads and cache (#581)

* Rename "DownloadedFilesProvider" to "ChaptersFilesProvider"

* Move files into sub packages

* Further abstract "DownloadedFilesProvider"

* Rename "getCachedImageResponse" to "getImageResponse"

* Extract getting cached image response into new function

* Decouple thumbnail cache and download

* Download and delete permanent thumbnails

When adding/removing manga from/to the library make sure the permanent thumbnail files will get handled properly

* Move thumbnail cache to actual temp folder

* Rename "mangaDownloadsRoot" to "downloadRoot"

* Move manga downloads into "mangas" subfolder

* Clear downloaded thumbnail
This commit is contained in:
schroda
2023-08-12 17:14:43 +02:00
committed by GitHub
parent b8b92c8d69
commit f2dd67d87f
19 changed files with 342 additions and 82 deletions

View File

@@ -1,20 +0,0 @@
package suwayomi.tachidesk.manga.impl.download
import kotlinx.coroutines.CoroutineScope
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
import java.io.InputStream
/*
* Base class for downloaded chapter files provider, example: Folder, Archive
* */
abstract class DownloadedFilesProvider(val mangaId: Int, val chapterId: Int) {
abstract fun getImage(index: Int): Pair<InputStream, String>
abstract suspend fun download(
download: DownloadChapter,
scope: CoroutineScope,
step: suspend (DownloadChapter?, Boolean) -> Unit
): Boolean
abstract fun delete(): Boolean
}

View File

@@ -0,0 +1,40 @@
package suwayomi.tachidesk.manga.impl.download.fileProvider
import kotlinx.coroutines.CoroutineScope
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
import java.io.InputStream
/*
* Base class for downloaded chapter files provider, example: Folder, Archive
* */
abstract class ChaptersFilesProvider(val mangaId: Int, val chapterId: Int) : DownloadedFilesProvider {
abstract fun getImageImpl(index: Int): Pair<InputStream, String>
override fun getImage(): RetrieveFile1Args<Int> {
return object : RetrieveFile1Args<Int> {
override fun execute(a: Int): Pair<InputStream, String> {
return getImageImpl(a)
}
}
}
abstract suspend fun downloadImpl(
download: DownloadChapter,
scope: CoroutineScope,
step: suspend (DownloadChapter?, Boolean) -> Unit
): Boolean
override fun download(): FileDownload3Args<DownloadChapter, CoroutineScope, suspend (DownloadChapter?, Boolean) -> Unit> {
return object : FileDownload3Args<DownloadChapter, CoroutineScope, suspend (DownloadChapter?, Boolean) -> Unit> {
override suspend fun execute(
a: DownloadChapter,
b: CoroutineScope,
c: suspend (DownloadChapter?, Boolean) -> Unit
): Boolean {
return downloadImpl(a, b, c)
}
}
}
abstract override fun delete(): Boolean
}

View File

@@ -0,0 +1,5 @@
package suwayomi.tachidesk.manga.impl.download.fileProvider
interface DownloadedFilesProvider : FileDownloader, FileRetriever {
fun delete(): Boolean
}

View File

@@ -0,0 +1,29 @@
package suwayomi.tachidesk.manga.impl.download.fileProvider
@FunctionalInterface
interface FileDownload {
suspend fun executeDownload(vararg args: Any): Boolean
}
@FunctionalInterface
interface FileDownload0Args : FileDownload {
suspend fun execute(): Boolean
override suspend fun executeDownload(vararg args: Any): Boolean {
return execute()
}
}
@FunctionalInterface
interface FileDownload3Args<A, B, C> : FileDownload {
suspend fun execute(a: A, b: B, c: C): Boolean
override suspend fun executeDownload(vararg args: Any): Boolean {
return execute(args[0] as A, args[1] as B, args[2] as C)
}
}
@FunctionalInterface
interface FileDownloader {
fun download(): FileDownload
}

View File

@@ -0,0 +1,31 @@
package suwayomi.tachidesk.manga.impl.download.fileProvider
import java.io.InputStream
@FunctionalInterface
interface RetrieveFile {
fun executeGetImage(vararg args: Any): Pair<InputStream, String>
}
@FunctionalInterface
interface RetrieveFile0Args : RetrieveFile {
fun execute(): Pair<InputStream, String>
override fun executeGetImage(vararg args: Any): Pair<InputStream, String> {
return execute()
}
}
@FunctionalInterface
interface RetrieveFile1Args<A> : RetrieveFile {
fun execute(a: A): Pair<InputStream, String>
override fun executeGetImage(vararg args: Any): Pair<InputStream, String> {
return execute(args[0] as A)
}
}
@FunctionalInterface
interface FileRetriever {
fun getImage(): RetrieveFile
}

View File

@@ -1,4 +1,4 @@
package suwayomi.tachidesk.manga.impl.download
package suwayomi.tachidesk.manga.impl.download.fileProvider.impl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@@ -7,14 +7,15 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
import org.apache.commons.compress.archivers.zip.ZipFile
import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
import java.io.File
import java.io.InputStream
class ArchiveProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(mangaId, chapterId) {
override fun getImage(index: Int): Pair<InputStream, String> {
class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mangaId, chapterId) {
override fun getImageImpl(index: Int): Pair<InputStream, String> {
val cbzPath = getChapterCbzPath(mangaId, chapterId)
val zipFile = ZipFile(cbzPath)
val zipEntry = zipFile.entries.toList().sortedWith(compareBy({ it.name }, { it.name }))[index]
@@ -23,7 +24,7 @@ class ArchiveProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(ma
return Pair(inputStream.buffered(), "image/$fileType")
}
override suspend fun download(
override suspend fun downloadImpl(
download: DownloadChapter,
scope: CoroutineScope,
step: suspend (DownloadChapter?, Boolean) -> Unit
@@ -33,7 +34,7 @@ class ArchiveProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(ma
val chapterFolder = File(chapterDir)
if (outputFile.exists()) handleExistingCbzFile(outputFile, chapterFolder)
FolderProvider(mangaId, chapterId).download(download, scope, step)
FolderProvider(mangaId, chapterId).download().execute(download, scope, step)
withContext(Dispatchers.IO) {
outputFile.createNewFile()

View File

@@ -1,4 +1,4 @@
package suwayomi.tachidesk.manga.impl.download
package suwayomi.tachidesk.manga.impl.download.fileProvider.impl
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import suwayomi.tachidesk.manga.impl.Page
import suwayomi.tachidesk.manga.impl.Page.getPageName
import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
@@ -19,8 +20,8 @@ import java.io.InputStream
/*
* Provides downloaded files when pages were downloaded into folders
* */
class FolderProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(mangaId, chapterId) {
override fun getImage(index: Int): Pair<InputStream, String> {
class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mangaId, chapterId) {
override fun getImageImpl(index: Int): Pair<InputStream, String> {
val chapterDir = getChapterDownloadPath(mangaId, chapterId)
val folder = File(chapterDir)
folder.mkdirs()
@@ -30,7 +31,7 @@ class FolderProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(man
}
@OptIn(FlowPreview::class)
override suspend fun download(
override suspend fun downloadImpl(
download: DownloadChapter,
scope: CoroutineScope,
step: suspend (DownloadChapter?, Boolean) -> Unit

View File

@@ -0,0 +1,87 @@
package suwayomi.tachidesk.manga.impl.download.fileProvider.impl
import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.Manga
import suwayomi.tachidesk.manga.impl.download.fileProvider.DownloadedFilesProvider
import suwayomi.tachidesk.manga.impl.download.fileProvider.FileDownload0Args
import suwayomi.tachidesk.manga.impl.download.fileProvider.RetrieveFile0Args
import suwayomi.tachidesk.manga.impl.util.getThumbnailDownloadPath
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getCachedImageResponse
import suwayomi.tachidesk.server.ApplicationDirs
import java.io.File
import java.io.InputStream
class MissingThumbnailException : Exception("No thumbnail found")
private val applicationDirs by DI.global.instance<ApplicationDirs>()
class ThumbnailFileProvider(val mangaId: Int) : DownloadedFilesProvider {
private fun getFilePath(): String? {
val thumbnailDir = applicationDirs.thumbnailDownloadsRoot
val fileName = mangaId.toString()
return ImageResponse.findFileNameStartingWith(thumbnailDir, fileName)
}
fun getImageImpl(): Pair<InputStream, String> {
val filePathWithoutExt = getThumbnailDownloadPath(mangaId)
val filePath = getFilePath()
if (filePath.isNullOrEmpty()) {
throw MissingThumbnailException()
}
return getCachedImageResponse(filePath, filePathWithoutExt)
}
override fun getImage(): RetrieveFile0Args {
return object : RetrieveFile0Args {
override fun execute(): Pair<InputStream, String> {
return getImageImpl()
}
}
}
suspend fun downloadImpl(): Boolean {
val isExistingFile = getFilePath() != null
if (isExistingFile) {
return true
}
Manga.fetchMangaThumbnail(mangaId).first.use { image ->
makeSureDownloadDirExists()
val filePath = getThumbnailDownloadPath(mangaId)
ImageResponse.saveImage(filePath, image)
}
return true
}
override fun download(): FileDownload0Args {
return object : FileDownload0Args {
override suspend fun execute(): Boolean {
return downloadImpl()
}
}
}
override fun delete(): Boolean {
val filePath = getFilePath()
if (filePath.isNullOrEmpty()) {
return true
}
return File(filePath).delete()
}
private fun makeSureDownloadDirExists() {
val downloadDirPath = applicationDirs.thumbnailDownloadsRoot
val downloadDir = File(downloadDirPath)
if (!downloadDir.exists()) {
downloadDir.mkdir()
}
}
}