diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt index af4a5c50..67e445f1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt @@ -476,24 +476,31 @@ object MangaController { documentWith = { withOperation { summary("Download chapter as CBZ") - description("Get the CBZ file of the specified chapter") + description("Get the CBZ file of the specified chapter, or its metadata via a HEAD request.") } }, behaviorOf = { ctx, chapterId, markAsRead -> - val shouldMarkAsRead = if (ctx.method() == HandlerType.HEAD) false else markAsRead - ctx.future { - future { ChapterDownloadHelper.getCbzForDownload(chapterId, shouldMarkAsRead) } - .thenApply { (inputStream, fileName, fileSize) -> - ctx.header("Content-Type", "application/vnd.comicbook+zip") - ctx.header("Content-Disposition", "attachment; filename=\"$fileName\"") - ctx.header("Content-Length", fileSize.toString()) - if (ctx.method() == HandlerType.HEAD) { - inputStream.close() - ctx.status(200) - } else { + if (ctx.method() == HandlerType.HEAD) { + ctx.future { + future { ChapterDownloadHelper.getCbzMetadataForDownload(chapterId) } + .thenApply { (fileName, fileSize, contentType) -> + ctx.header("Content-Type", contentType) + ctx.header("Content-Disposition", "attachment; filename=\"$fileName\"") + ctx.header("Content-Length", fileSize.toString()) + ctx.status(HttpStatus.OK) + } + } + } else { + val shouldMarkAsRead = markAsRead ?: false + ctx.future { + future { ChapterDownloadHelper.getCbzForDownload(chapterId, shouldMarkAsRead) } + .thenApply { (inputStream, fileName, fileSize) -> + ctx.header("Content-Type", "application/vnd.comicbook+zip") + ctx.header("Content-Disposition", "attachment; filename=\"$fileName\"") + ctx.header("Content-Length", fileSize.toString()) ctx.result(inputStream) } - } + } } }, withResults = { 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 05b37e93..1804bb3f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt @@ -94,4 +94,26 @@ object ChapterDownloadHelper { return Triple(cbzFile.first, fileName, cbzFile.second) } + + fun getCbzMetadataForDownload(chapterId: Int): Triple { // fileName, fileSize, contentType + val (chapterData, mangaTitle) = + transaction { + val row = + (ChapterTable innerJoin MangaTable) + .select(ChapterTable.columns + MangaTable.columns) + .where { ChapterTable.id eq chapterId } + .firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found") + val chapter = ChapterTable.toDataClass(row) + val title = row[MangaTable.title] + Pair(chapter, title) + } + + val scanlatorPart = chapterData.scanlator?.let { "[$it] " } ?: "" + val fileName = "$mangaTitle - $scanlatorPart${chapterData.name}.cbz" + + val fileSize = provider(chapterData.mangaId, chapterData.id).getArchiveSize() + val contentType = "application/vnd.comicbook+zip" + + return Triple(fileName, fileSize, contentType) + } } 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 fa48312d..13b1d283 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 @@ -220,6 +220,8 @@ abstract class ChaptersFilesProvider( abstract fun getAsArchiveStream(): Pair + abstract fun getArchiveSize(): Long + private fun maybeConvertPages(chapterCacheFolder: File) { val conversions = serverConfig.downloadConversions.value 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 f3ca3549..9ea6e8b2 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 @@ -108,6 +108,11 @@ class ArchiveProvider( return cbzFile.inputStream() to cbzFile.length() } + override fun getArchiveSize(): Long { + val cbzFile = File(getChapterCbzPath(mangaId, chapterId)) + return if (cbzFile.exists()) cbzFile.length() else 0L + } + private fun extractCbzFile( cbzFile: File, chapterFolder: File, 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 c11296fd..f07f52f9 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 @@ -101,4 +101,11 @@ class FolderProvider( val zipData = byteArrayOutputStream.toByteArray() return ByteArrayInputStream(zipData) to zipData.size.toLong() } + + override fun getArchiveSize(): Long { + val chapterDir = File(getChapterDownloadPath(mangaId, chapterId)) + if (!chapterDir.exists() || !chapterDir.isDirectory) return 0L + // Approximation: actual CBZ size is slightly larger due to ZIP metadata, but sufficient for Content-Length header. + return chapterDir.listFiles()?.filter { it.isFile }?.sumOf { it.length() } ?: 0L + } }