mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01: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:
@@ -8,6 +8,7 @@ import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
||||
import suwayomi.tachidesk.graphql.types.MangaType
|
||||
import suwayomi.tachidesk.manga.impl.Library
|
||||
import suwayomi.tachidesk.manga.impl.Manga
|
||||
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
@@ -44,7 +45,7 @@ class MangaMutation {
|
||||
val patch: UpdateMangaPatch
|
||||
)
|
||||
|
||||
private fun updateMangas(ids: List<Int>, patch: UpdateMangaPatch) {
|
||||
private suspend fun updateMangas(ids: List<Int>, patch: UpdateMangaPatch) {
|
||||
transaction {
|
||||
if (patch.inLibrary != null) {
|
||||
MangaTable.update({ MangaTable.id inList ids }) { update ->
|
||||
@@ -53,38 +54,48 @@ class MangaMutation {
|
||||
}
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
if (patch.inLibrary != null) {
|
||||
ids.forEach {
|
||||
Library.handleMangaThumbnail(it, patch.inLibrary)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun updateManga(input: UpdateMangaInput): UpdateMangaPayload {
|
||||
fun updateManga(input: UpdateMangaInput): CompletableFuture<UpdateMangaPayload> {
|
||||
val (clientMutationId, id, patch) = input
|
||||
|
||||
return future {
|
||||
updateMangas(listOf(id), patch)
|
||||
|
||||
}.thenApply {
|
||||
val manga = transaction {
|
||||
MangaType(MangaTable.select { MangaTable.id eq id }.first())
|
||||
}
|
||||
|
||||
return UpdateMangaPayload(
|
||||
UpdateMangaPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
manga = manga
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMangas(input: UpdateMangasInput): UpdateMangasPayload {
|
||||
fun updateMangas(input: UpdateMangasInput): CompletableFuture<UpdateMangasPayload> {
|
||||
val (clientMutationId, ids, patch) = input
|
||||
|
||||
return future {
|
||||
updateMangas(ids, patch)
|
||||
|
||||
}.thenApply {
|
||||
val mangas = transaction {
|
||||
MangaTable.select { MangaTable.id inList ids }.map { MangaType(it) }
|
||||
}
|
||||
|
||||
return UpdateMangasPayload(
|
||||
UpdateMangasPayload(
|
||||
clientMutationId = clientMutationId,
|
||||
mangas = mangas
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class FetchMangaInput(
|
||||
val clientMutationId: String? = null,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package suwayomi.tachidesk.manga.impl
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import suwayomi.tachidesk.manga.impl.download.ArchiveProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.DownloadedFilesProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.FolderProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.ArchiveProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.FolderProvider
|
||||
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
|
||||
@@ -13,7 +13,7 @@ import java.io.InputStream
|
||||
|
||||
object ChapterDownloadHelper {
|
||||
fun getImage(mangaId: Int, chapterId: Int, index: Int): Pair<InputStream, String> {
|
||||
return provider(mangaId, chapterId).getImage(index)
|
||||
return provider(mangaId, chapterId).getImage().execute(index)
|
||||
}
|
||||
|
||||
fun delete(mangaId: Int, chapterId: Int): Boolean {
|
||||
@@ -27,11 +27,11 @@ object ChapterDownloadHelper {
|
||||
scope: CoroutineScope,
|
||||
step: suspend (DownloadChapter?, Boolean) -> Unit
|
||||
): Boolean {
|
||||
return provider(mangaId, chapterId).download(download, scope, step)
|
||||
return provider(mangaId, chapterId).download().execute(download, scope, step)
|
||||
}
|
||||
|
||||
// return the appropriate provider based on how the download was saved. For the logic is simple but will evolve when new types of downloads are available
|
||||
private fun provider(mangaId: Int, chapterId: Int): DownloadedFilesProvider {
|
||||
private fun provider(mangaId: Int, chapterId: Int): ChaptersFilesProvider {
|
||||
val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
|
||||
val cbzFile = File(getChapterCbzPath(mangaId, chapterId))
|
||||
if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId)
|
||||
|
||||
@@ -7,6 +7,10 @@ package suwayomi.tachidesk.manga.impl
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import org.jetbrains.exposed.sql.and
|
||||
import org.jetbrains.exposed.sql.insert
|
||||
import org.jetbrains.exposed.sql.select
|
||||
@@ -19,6 +23,8 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import java.time.Instant
|
||||
|
||||
object Library {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
|
||||
suspend fun addMangaToLibrary(mangaId: Int) {
|
||||
val manga = getManga(mangaId)
|
||||
if (!manga.inLibrary) {
|
||||
@@ -41,6 +47,8 @@ object Library {
|
||||
}
|
||||
}
|
||||
}
|
||||
}.apply {
|
||||
handleMangaThumbnail(mangaId, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,6 +60,22 @@ object Library {
|
||||
MangaTable.update({ MangaTable.id eq manga.id }) {
|
||||
it[inLibrary] = false
|
||||
}
|
||||
}.apply {
|
||||
handleMangaThumbnail(mangaId, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun handleMangaThumbnail(mangaId: Int, inLibrary: Boolean) {
|
||||
scope.launch {
|
||||
try {
|
||||
if (inLibrary) {
|
||||
ThumbnailDownloadHelper.download(mangaId)
|
||||
} else {
|
||||
ThumbnailDownloadHelper.delete(mangaId)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,12 +25,13 @@ import org.kodein.di.conf.global
|
||||
import org.kodein.di.instance
|
||||
import suwayomi.tachidesk.manga.impl.MangaList.proxyThumbnailUrl
|
||||
import suwayomi.tachidesk.manga.impl.Source.getSource
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.MissingThumbnailException
|
||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrNull
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.source.StubSource
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.clearCachedImage
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getCachedImageResponse
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import suwayomi.tachidesk.manga.impl.util.updateMangaDownloadDir
|
||||
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||
@@ -126,7 +127,7 @@ object Manga {
|
||||
if (!sManga.thumbnail_url.isNullOrEmpty() && sManga.thumbnail_url != mangaEntry[MangaTable.thumbnail_url]) {
|
||||
it[MangaTable.thumbnail_url] = sManga.thumbnail_url
|
||||
it[MangaTable.thumbnailUrlLastFetched] = Instant.now().epochSecond
|
||||
clearMangaThumbnailCache(mangaId)
|
||||
clearThumbnail(mangaId)
|
||||
}
|
||||
|
||||
it[MangaTable.realUrl] = runCatching {
|
||||
@@ -232,15 +233,16 @@ object Manga {
|
||||
|
||||
private val applicationDirs by DI.global.instance<ApplicationDirs>()
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||
val cacheSaveDir = applicationDirs.thumbnailsRoot
|
||||
|
||||
suspend fun fetchMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||
val cacheSaveDir = applicationDirs.tempThumbnailCacheRoot
|
||||
val fileName = mangaId.toString()
|
||||
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
val sourceId = mangaEntry[MangaTable.sourceReference]
|
||||
|
||||
return when (val source = getCatalogueSourceOrStub(sourceId)) {
|
||||
is HttpSource -> getCachedImageResponse(cacheSaveDir, fileName) {
|
||||
is HttpSource -> getImageResponse(cacheSaveDir, fileName) {
|
||||
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||
?: if (!mangaEntry[MangaTable.initialized]) {
|
||||
// initialize then try again
|
||||
@@ -272,7 +274,7 @@ object Manga {
|
||||
imageFile.inputStream() to contentType
|
||||
}
|
||||
|
||||
is StubSource -> getCachedImageResponse(cacheSaveDir, fileName) {
|
||||
is StubSource -> getImageResponse(cacheSaveDir, fileName) {
|
||||
val thumbnailUrl = mangaEntry[MangaTable.thumbnail_url]
|
||||
?: throw NullPointerException("No thumbnail found")
|
||||
network.client.newCall(
|
||||
@@ -284,10 +286,25 @@ object Manga {
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearMangaThumbnailCache(mangaId: Int) {
|
||||
val saveDir = applicationDirs.thumbnailsRoot
|
||||
suspend fun getMangaThumbnail(mangaId: Int): Pair<InputStream, String> {
|
||||
val mangaEntry = transaction { MangaTable.select { MangaTable.id eq mangaId }.first() }
|
||||
|
||||
if (mangaEntry[MangaTable.inLibrary]) {
|
||||
return try {
|
||||
ThumbnailDownloadHelper.getImage(mangaId)
|
||||
} catch (_: MissingThumbnailException) {
|
||||
ThumbnailDownloadHelper.download(mangaId)
|
||||
ThumbnailDownloadHelper.getImage(mangaId)
|
||||
}
|
||||
}
|
||||
|
||||
return fetchMangaThumbnail(mangaId)
|
||||
}
|
||||
|
||||
private fun clearThumbnail(mangaId: Int) {
|
||||
val fileName = mangaId.toString()
|
||||
|
||||
clearCachedImage(saveDir, fileName)
|
||||
clearCachedImage(applicationDirs.tempThumbnailCacheRoot, fileName)
|
||||
clearCachedImage(applicationDirs.thumbnailDownloadsRoot, fileName)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import org.jetbrains.exposed.sql.update
|
||||
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getCachedImageResponse
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
@@ -99,7 +99,7 @@ object Page {
|
||||
val cacheSaveDir = getChapterCachePath(mangaId, chapterId)
|
||||
|
||||
// Note: don't care about invalidating cache because OS cache is not permanent
|
||||
return getCachedImageResponse(cacheSaveDir, fileName) {
|
||||
return getImageResponse(cacheSaveDir, fileName) {
|
||||
source.fetchImage(tachiyomiPage).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package suwayomi.tachidesk.manga.impl
|
||||
|
||||
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.ThumbnailFileProvider
|
||||
import java.io.InputStream
|
||||
|
||||
object ThumbnailDownloadHelper {
|
||||
fun getImage(mangaId: Int): Pair<InputStream, String> {
|
||||
return provider(mangaId).getImage().execute()
|
||||
}
|
||||
|
||||
fun delete(mangaId: Int): Boolean {
|
||||
return provider(mangaId).delete()
|
||||
}
|
||||
|
||||
suspend fun download(mangaId: Int): Boolean {
|
||||
return provider(mangaId).download().execute()
|
||||
}
|
||||
|
||||
// return the appropriate provider based on how the download was saved. For the logic is simple but will evolve when new types of downloads are available
|
||||
private fun provider(mangaId: Int): ThumbnailFileProvider {
|
||||
return ThumbnailFileProvider(mangaId)
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -40,7 +40,7 @@ import suwayomi.tachidesk.manga.impl.util.PackageTools.getPackageInfo
|
||||
import suwayomi.tachidesk.manga.impl.util.PackageTools.loadExtensionSources
|
||||
import suwayomi.tachidesk.manga.impl.util.network.await
|
||||
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getCachedImageResponse
|
||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse.getImageResponse
|
||||
import suwayomi.tachidesk.manga.model.table.ExtensionTable
|
||||
import suwayomi.tachidesk.manga.model.table.SourceTable
|
||||
import suwayomi.tachidesk.server.ApplicationDirs
|
||||
@@ -329,7 +329,7 @@ object Extension {
|
||||
|
||||
val cacheSaveDir = "${applicationDirs.extensionsRoot}/icon"
|
||||
|
||||
return getCachedImageResponse(cacheSaveDir, apkName) {
|
||||
return getImageResponse(cacheSaveDir, apkName) {
|
||||
network.client.newCall(
|
||||
GET(iconUrl)
|
||||
).await()
|
||||
|
||||
@@ -44,6 +44,10 @@ private fun getChapterDir(mangaId: Int, chapterId: Int): String {
|
||||
return getMangaDir(mangaId) + "/$chapterDir"
|
||||
}
|
||||
|
||||
fun getThumbnailDownloadPath(mangaId: Int): String {
|
||||
return applicationDirs.thumbnailDownloadsRoot + "/$mangaId"
|
||||
}
|
||||
|
||||
fun getChapterDownloadPath(mangaId: Int, chapterId: Int): String {
|
||||
return applicationDirs.mangaDownloadsRoot + "/" + getChapterDir(mangaId, chapterId)
|
||||
}
|
||||
@@ -66,8 +70,8 @@ fun updateMangaDownloadDir(mangaId: Int, newTitle: String): Boolean {
|
||||
|
||||
val newMangaDir = SafePath.buildValidFilename(newTitle)
|
||||
|
||||
val oldDir = "${applicationDirs.mangaDownloadsRoot}/$sourceDir/$mangaDir"
|
||||
val newDir = "${applicationDirs.mangaDownloadsRoot}/$sourceDir/$newMangaDir"
|
||||
val oldDir = "${applicationDirs.downloadsRoot}/$sourceDir/$mangaDir"
|
||||
val newDir = "${applicationDirs.downloadsRoot}/$sourceDir/$newMangaDir"
|
||||
|
||||
val oldDirFile = File(oldDir)
|
||||
val newDirFile = File(newDir)
|
||||
|
||||
@@ -30,6 +30,14 @@ object ImageResponse {
|
||||
return null
|
||||
}
|
||||
|
||||
fun getCachedImageResponse(cachedFile: String, filePath: String): Pair<InputStream, String> {
|
||||
val fileType = cachedFile.substringAfter("$filePath.")
|
||||
return Pair(
|
||||
pathToInputStream(cachedFile),
|
||||
"image/$fileType"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a cached image response
|
||||
*
|
||||
@@ -38,7 +46,7 @@ object ImageResponse {
|
||||
* @param cacheSavePath where to save the cached image. Caller should decide to use perma cache or temp cache (OS temp dir)
|
||||
* @param fileName what the saved cache file should be named
|
||||
*/
|
||||
suspend fun getCachedImageResponse(
|
||||
suspend fun getImageResponse(
|
||||
saveDir: String,
|
||||
fileName: String,
|
||||
fetcher: suspend () -> Response
|
||||
@@ -50,11 +58,7 @@ object ImageResponse {
|
||||
|
||||
// in case the cached file is a ".tmp" file something went wrong with the previous download, and it has to be downloaded again
|
||||
if (cachedFile != null && !cachedFile.endsWith(".tmp")) {
|
||||
val fileType = cachedFile.substringAfter("$filePath.")
|
||||
return Pair(
|
||||
pathToInputStream(cachedFile),
|
||||
"image/$fileType"
|
||||
)
|
||||
return getCachedImageResponse(cachedFile, filePath)
|
||||
}
|
||||
|
||||
val response = fetcher()
|
||||
|
||||
@@ -45,13 +45,16 @@ class ApplicationDirs(
|
||||
) {
|
||||
val cacheRoot = System.getProperty("java.io.tmpdir") + "/tachidesk"
|
||||
val extensionsRoot = "$dataRoot/extensions"
|
||||
val thumbnailsRoot = "$dataRoot/thumbnails"
|
||||
val mangaDownloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" }
|
||||
val downloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" }
|
||||
val localMangaRoot = serverConfig.localSourcePath.ifBlank { "$dataRoot/local" }
|
||||
val webUIRoot = "$dataRoot/webUI"
|
||||
val automatedBackupRoot = serverConfig.backupPath.ifBlank { "$dataRoot/backups" }
|
||||
|
||||
val tempThumbnailCacheRoot = "$tempRoot/thumbnails"
|
||||
val tempMangaCacheRoot = "$tempRoot/manga-cache"
|
||||
|
||||
val thumbnailDownloadsRoot = "$downloadsRoot/thumbnails"
|
||||
val mangaDownloadsRoot = "$downloadsRoot/mangas"
|
||||
}
|
||||
|
||||
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
|
||||
@@ -96,7 +99,7 @@ fun applicationSetup() {
|
||||
logger.debug("Data Root directory is set to: ${applicationDirs.dataRoot}")
|
||||
|
||||
// Migrate Directories from old versions
|
||||
File("$ApplicationRootDir/manga-thumbnails").renameTo(applicationDirs.thumbnailsRoot)
|
||||
File("$ApplicationRootDir/manga-thumbnails").renameTo(applicationDirs.tempThumbnailCacheRoot)
|
||||
File("$ApplicationRootDir/manga-local").renameTo(applicationDirs.localMangaRoot)
|
||||
File("$ApplicationRootDir/anime-thumbnails").delete()
|
||||
|
||||
@@ -105,8 +108,8 @@ fun applicationSetup() {
|
||||
applicationDirs.dataRoot,
|
||||
applicationDirs.extensionsRoot,
|
||||
applicationDirs.extensionsRoot + "/icon",
|
||||
applicationDirs.thumbnailsRoot,
|
||||
applicationDirs.mangaDownloadsRoot,
|
||||
applicationDirs.tempThumbnailCacheRoot,
|
||||
applicationDirs.downloadsRoot,
|
||||
applicationDirs.localMangaRoot
|
||||
).forEach {
|
||||
File(it).mkdirs()
|
||||
|
||||
@@ -74,8 +74,8 @@ open class ApplicationTest {
|
||||
applicationDirs.dataRoot,
|
||||
applicationDirs.extensionsRoot,
|
||||
applicationDirs.extensionsRoot + "/icon",
|
||||
applicationDirs.thumbnailsRoot,
|
||||
applicationDirs.mangaDownloadsRoot,
|
||||
applicationDirs.tempThumbnailCacheRoot,
|
||||
applicationDirs.downloadsRoot,
|
||||
applicationDirs.localMangaRoot
|
||||
).forEach {
|
||||
File(it).mkdirs()
|
||||
|
||||
Reference in New Issue
Block a user