From c9bb8d4e7e9038d63a2efe864548f42cd17ffed5 Mon Sep 17 00:00:00 2001 From: akabhirav Date: Sun, 12 Feb 2023 21:19:26 +0530 Subject: [PATCH 1/2] Download as CBZ --- .../manga/impl/ChapterDownloadHelper.kt | 9 ++ .../manga/impl/chapter/ChapterForDownload.kt | 7 +- .../manga/impl/download/ArchiveProvider.kt | 84 +++++++++++++++++++ .../tachidesk/manga/impl/util/DirName.kt | 5 ++ .../suwayomi/tachidesk/server/ServerConfig.kt | 3 + .../src/main/resources/server-reference.conf | 3 + .../src/test/resources/server-reference.conf | 3 + 7 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/ArchiveProvider.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt index 768e80e01..b423c89f2 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt @@ -1,9 +1,14 @@ 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.model.DownloadChapter +import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath +import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath +import suwayomi.tachidesk.server.serverConfig +import java.io.File import java.io.InputStream object ChapterDownloadHelper { @@ -27,6 +32,10 @@ object ChapterDownloadHelper { // 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 { + val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId)) + val cbzFile = File(getChapterCbzPath(mangaId, chapterId)) + if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId) + if (!chapterFolder.exists() && serverConfig.downloadAsCbz) return ArchiveProvider(mangaId, chapterId) return FolderProvider(mangaId, chapterId) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt index 324f8f492..9e0cc7f8c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt @@ -16,6 +16,7 @@ import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.manga.impl.Page.getPageName +import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub @@ -25,6 +26,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.PageTable import suwayomi.tachidesk.manga.model.table.toDataClass +import java.io.File suspend fun getChapterDownloadReady(chapterIndex: Int, mangaId: Int): ChapterDataClass { val chapter = ChapterForDownload(chapterIndex, mangaId) @@ -127,7 +129,10 @@ private class ChapterForDownload( } private fun isNotCompletelyDownloaded(): Boolean { - return !(chapterEntry[ChapterTable.isDownloaded] && firstPageExists()) + return !( + chapterEntry[ChapterTable.isDownloaded] && + (firstPageExists() || File(getChapterCbzPath(mangaId, chapterEntry[ChapterTable.id].value)).exists()) + ) } private fun firstPageExists(): Boolean { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/ArchiveProvider.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/ArchiveProvider.kt new file mode 100644 index 000000000..665d9f715 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/ArchiveProvider.kt @@ -0,0 +1,84 @@ +package suwayomi.tachidesk.manga.impl.download + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +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 +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipInputStream +import java.util.zip.ZipOutputStream + +class ArchiveProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(mangaId, chapterId) { + override fun getImage(index: Int): Pair { + val cbzPath = getChapterCbzPath(mangaId, chapterId) + val zipFile = ZipFile(cbzPath) + val zipEntry = zipFile.entries().toList().sortedWith(compareBy({ it.name }, { it.name }))[index] + val inputStream = zipFile.getInputStream(zipEntry) + val fileType = zipEntry.name.substringAfterLast(".") + return Pair(inputStream.buffered(), "image/$fileType") + } + + override suspend fun download( + download: DownloadChapter, + 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) + + withContext(Dispatchers.IO) { + outputFile.createNewFile() + } + + FolderProvider(mangaId, chapterId).download(download, scope, step) + + ZipOutputStream(outputFile.outputStream()).use { zipOut -> + if (chapterFolder.isDirectory) { + chapterFolder.listFiles()?.sortedBy { it.name }?.forEach { + val entry = ZipEntry(it.name) + zipOut.putNextEntry(entry) + it.inputStream().copyTo(zipOut) + zipOut.closeEntry() + } + } + } + + if (chapterFolder.exists() && chapterFolder.isDirectory) { + chapterFolder.deleteRecursively() + } + + return true + } + + override fun delete(): Boolean { + val cbzFile = File(getChapterCbzPath(mangaId, chapterId)) + if (cbzFile.exists()) return cbzFile.delete() + return false + } + + private fun handleExistingCbzFile(cbzFile: File, chapterFolder: File) { + if (!chapterFolder.exists()) chapterFolder.mkdirs() + ZipInputStream(cbzFile.inputStream()).use { zipInputStream -> + var zipEntry = zipInputStream.nextEntry + while (zipEntry != null) { + val file = File(chapterFolder, zipEntry.name) + if (!file.exists()) { + file.parentFile.mkdirs() + file.createNewFile() + } + file.outputStream().use { outputStream -> + zipInputStream.copyTo(outputStream) + } + zipEntry = zipInputStream.nextEntry + } + } + cbzFile.delete() + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/DirName.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/DirName.kt index d31648a50..5b13fc5f9 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/DirName.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/DirName.kt @@ -47,6 +47,11 @@ private fun getChapterDir(mangaId: Int, chapterId: Int): String { fun getChapterDownloadPath(mangaId: Int, chapterId: Int): String { return applicationDirs.mangaDownloadsRoot + "/" + getChapterDir(mangaId, chapterId) } + +fun getChapterCbzPath(mangaId: Int, chapterId: Int): String { + return getChapterDownloadPath(mangaId, chapterId) + ".cbz" +} + fun getChapterCachePath(mangaId: Int, chapterId: Int): String { return applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt index 51e083fa1..a6b06ce66 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt @@ -23,6 +23,9 @@ class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPro val socksProxyHost: String by overridableConfig val socksProxyPort: String by overridableConfig + // downloader + val downloadAsCbz: Boolean by overridableConfig + // misc val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config) val systemTrayEnabled: Boolean by overridableConfig diff --git a/server/src/main/resources/server-reference.conf b/server/src/main/resources/server-reference.conf index 1a625ff97..d69eeeb44 100644 --- a/server/src/main/resources/server-reference.conf +++ b/server/src/main/resources/server-reference.conf @@ -19,6 +19,9 @@ server.basicAuthEnabled = false server.basicAuthUsername = "" server.basicAuthPassword = "" +# downloader +server.downloadAsCbz = false + # misc server.debugLogsEnabled = false server.systemTrayEnabled = true diff --git a/server/src/test/resources/server-reference.conf b/server/src/test/resources/server-reference.conf index bcf6390fb..3073f8592 100644 --- a/server/src/test/resources/server-reference.conf +++ b/server/src/test/resources/server-reference.conf @@ -7,6 +7,9 @@ server.socksProxyEnabled = false server.socksProxyHost = "" server.socksProxyPort = "" +# downloader +server.downloadAsCbz = false + # misc server.debugLogsEnabled = true server.systemTrayEnabled = false From 6c6e26f6c45212ae6f94ddb29b9e850f10b7d32b Mon Sep 17 00:00:00 2001 From: akabhirav Date: Sun, 12 Feb 2023 23:11:10 +0530 Subject: [PATCH 2/2] Better error handling for zips (code review changes) --- .../tachidesk/manga/impl/download/ArchiveProvider.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/ArchiveProvider.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/ArchiveProvider.kt index 665d9f715..57fc96e39 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/ArchiveProvider.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/ArchiveProvider.kt @@ -43,9 +43,14 @@ class ArchiveProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(ma if (chapterFolder.isDirectory) { chapterFolder.listFiles()?.sortedBy { it.name }?.forEach { val entry = ZipEntry(it.name) - zipOut.putNextEntry(entry) - it.inputStream().copyTo(zipOut) - zipOut.closeEntry() + try { + zipOut.putNextEntry(entry) + it.inputStream().use { inputStream -> + inputStream.copyTo(zipOut) + } + } finally { + zipOut.closeEntry() + } } } }