From 1c9a139006f7a9e399c964b2a88650fb757d8369 Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Tue, 29 Aug 2023 01:25:50 +0200 Subject: [PATCH] Always return "ArchiveProvider" in case "downloadAsCbz" is enabled (#671) * Move chapter download logic to base class * Do not reuse "FolderProvider" in "ArchiveProviders" download function Due to reusing the "FolderProvider" to download a chapter as a cbz file, a normal chapter download folder was created. In case the download was aborted before the cbz file got created and the folder deleted, the next time the chapter got downloaded, the wrong "FileProvider" was selected, causing the chapter not to get downloaded as a cbz file. --- .../fileProvider/ChaptersFilesProvider.kt | 48 +++++++++++++++- .../fileProvider/impl/ArchiveProvider.kt | 17 +++--- .../fileProvider/impl/FolderProvider.kt | 55 +++++-------------- 3 files changed, 68 insertions(+), 52 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/ChaptersFilesProvider.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/ChaptersFilesProvider.kt index f7646767e..01092db57 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/ChaptersFilesProvider.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/ChaptersFilesProvider.kt @@ -1,7 +1,16 @@ package suwayomi.tachidesk.manga.impl.download.fileProvider import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.sample +import suwayomi.tachidesk.manga.impl.Page import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter +import suwayomi.tachidesk.manga.impl.util.getChapterCachePath +import java.io.File import java.io.InputStream /* @@ -14,11 +23,46 @@ abstract class ChaptersFilesProvider(val mangaId: Int, val chapterId: Int) : Dow return RetrieveFile1Args(::getImageImpl) } - abstract suspend fun downloadImpl( + @OptIn(FlowPreview::class) + open suspend fun downloadImpl( download: DownloadChapter, scope: CoroutineScope, step: suspend (DownloadChapter?, Boolean) -> Unit - ): Boolean + ): Boolean { + val pageCount = download.chapter.pageCount + val chapterDir = getChapterCachePath(mangaId, chapterId) + val folder = File(chapterDir) + folder.mkdirs() + + for (pageNum in 0 until pageCount) { + var pageProgressJob: Job? = null + val fileName = Page.getPageName(pageNum) // might have to change this to index stored in database + if (File(folder, fileName).exists()) continue + try { + Page.getPageImage( + mangaId = download.mangaId, + chapterIndex = download.chapterIndex, + index = pageNum + ) { flow -> + pageProgressJob = flow + .sample(100) + .distinctUntilChanged() + .onEach { + download.progress = (pageNum.toFloat() + (it.toFloat() * 0.01f)) / pageCount + step(null, false) // don't throw on canceled download here since we can't do anything + } + .launchIn(scope) + } + } finally { + // always cancel the page progress job even if it throws an exception to avoid memory leaks + pageProgressJob?.cancel() + } + // TODO: retry on error with 2,4,8 seconds of wait + download.progress = ((pageNum + 1).toFloat()) / pageCount + step(download, false) + } + return true + } override fun download(): FileDownload3Args Unit> { return FileDownload3Args(::downloadImpl) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/impl/ArchiveProvider.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/impl/ArchiveProvider.kt index fd3c60e68..5b021dc7d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/impl/ArchiveProvider.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/impl/ArchiveProvider.kt @@ -9,8 +9,8 @@ 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.getChapterCachePath import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath -import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath import java.io.File import java.io.InputStream @@ -29,20 +29,19 @@ class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mang scope: CoroutineScope, step: suspend (DownloadChapter?, Boolean) -> Unit ): Boolean { - val chapterDir = getChapterDownloadPath(mangaId, chapterId) val outputFile = File(getChapterCbzPath(mangaId, chapterId)) - val chapterFolder = File(chapterDir) - if (outputFile.exists()) handleExistingCbzFile(outputFile, chapterFolder) + val chapterCacheFolder = File(getChapterCachePath(mangaId, chapterId)) + if (outputFile.exists()) handleExistingCbzFile(outputFile, chapterCacheFolder) - FolderProvider(mangaId, chapterId).download().execute(download, scope, step) + super.downloadImpl(download, scope, step) withContext(Dispatchers.IO) { outputFile.createNewFile() } ZipArchiveOutputStream(outputFile.outputStream()).use { zipOut -> - if (chapterFolder.isDirectory) { - chapterFolder.listFiles()?.sortedBy { it.name }?.forEach { + if (chapterCacheFolder.isDirectory) { + chapterCacheFolder.listFiles()?.sortedBy { it.name }?.forEach { val entry = ZipArchiveEntry(it.name) try { zipOut.putArchiveEntry(entry) @@ -56,8 +55,8 @@ class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mang } } - if (chapterFolder.exists() && chapterFolder.isDirectory) { - chapterFolder.deleteRecursively() + if (chapterCacheFolder.exists() && chapterCacheFolder.isDirectory) { + chapterCacheFolder.deleteRecursively() } return true diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/impl/FolderProvider.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/impl/FolderProvider.kt index 7ae570232..8f217fa22 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/impl/FolderProvider.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/fileProvider/impl/FolderProvider.kt @@ -1,18 +1,10 @@ package suwayomi.tachidesk.manga.impl.download.fileProvider.impl import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.launchIn -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.getChapterCachePath import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath -import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse import java.io.File import java.io.FileInputStream import java.io.InputStream @@ -30,47 +22,28 @@ class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(manga return Pair(FileInputStream(file).buffered(), "image/$fileType") } - @OptIn(FlowPreview::class) override suspend fun downloadImpl( download: DownloadChapter, scope: CoroutineScope, step: suspend (DownloadChapter?, Boolean) -> Unit ): Boolean { - val pageCount = download.chapter.pageCount val chapterDir = getChapterDownloadPath(mangaId, chapterId) val folder = File(chapterDir) - folder.mkdirs() - for (pageNum in 0 until pageCount) { - var pageProgressJob: Job? = null - val fileName = getPageName(pageNum) // might have to change this to index stored in database - if (isExistingFile(folder, fileName)) continue - try { - Page.getPageImage( - mangaId = download.mangaId, - chapterIndex = download.chapterIndex, - index = pageNum - ) { flow -> - pageProgressJob = flow - .sample(100) - .distinctUntilChanged() - .onEach { - download.progress = (pageNum.toFloat() + (it.toFloat() * 0.01f)) / pageCount - step(null, false) // don't throw on canceled download here since we can't do anything - } - .launchIn(scope) - }.first.use { image -> - val filePath = "$chapterDir/$fileName" - ImageResponse.saveImage(filePath, image) - } - } finally { - // always cancel the page progress job even if it throws an exception to avoid memory leaks - pageProgressJob?.cancel() - } - // TODO: retry on error with 2,4,8 seconds of wait - download.progress = ((pageNum + 1).toFloat()) / pageCount - step(download, false) + val alreadyDownloaded = folder.exists() + if (alreadyDownloaded) { + return true + } + + val downloadSucceeded = super.downloadImpl(download, scope, step) + if (!downloadSucceeded) { + return false } + + folder.mkdirs() + val cacheChapterDir = getChapterCachePath(mangaId, chapterId) + File(cacheChapterDir).renameTo(folder) + return true }