mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-16 17:24:07 -05:00
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:
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package suwayomi.tachidesk.manga.impl.download.fileProvider
|
||||
|
||||
interface DownloadedFilesProvider : FileDownloader, FileRetriever {
|
||||
fun delete(): Boolean
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user