Skip to content

Commit 555f73b

Browse files
authored
Download as CBZ (#490)
* Download as CBZ * Better error handling for zips (code review changes)
1 parent 544bf2e commit 555f73b

File tree

7 files changed

+118
-1
lines changed

7 files changed

+118
-1
lines changed

server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt

+9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package suwayomi.tachidesk.manga.impl
22

33
import kotlinx.coroutines.CoroutineScope
4+
import suwayomi.tachidesk.manga.impl.download.ArchiveProvider
45
import suwayomi.tachidesk.manga.impl.download.DownloadedFilesProvider
56
import suwayomi.tachidesk.manga.impl.download.FolderProvider
67
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
8+
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
9+
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
10+
import suwayomi.tachidesk.server.serverConfig
11+
import java.io.File
712
import java.io.InputStream
813

914
object ChapterDownloadHelper {
@@ -27,6 +32,10 @@ object ChapterDownloadHelper {
2732

2833
// 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
2934
private fun provider(mangaId: Int, chapterId: Int): DownloadedFilesProvider {
35+
val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
36+
val cbzFile = File(getChapterCbzPath(mangaId, chapterId))
37+
if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId)
38+
if (!chapterFolder.exists() && serverConfig.downloadAsCbz) return ArchiveProvider(mangaId, chapterId)
3039
return FolderProvider(mangaId, chapterId)
3140
}
3241
}

server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt

+6-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import org.jetbrains.exposed.sql.select
1616
import org.jetbrains.exposed.sql.transactions.transaction
1717
import org.jetbrains.exposed.sql.update
1818
import suwayomi.tachidesk.manga.impl.Page.getPageName
19+
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
1920
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
2021
import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle
2122
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
@@ -25,6 +26,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable
2526
import suwayomi.tachidesk.manga.model.table.MangaTable
2627
import suwayomi.tachidesk.manga.model.table.PageTable
2728
import suwayomi.tachidesk.manga.model.table.toDataClass
29+
import java.io.File
2830

2931
suspend fun getChapterDownloadReady(chapterIndex: Int, mangaId: Int): ChapterDataClass {
3032
val chapter = ChapterForDownload(chapterIndex, mangaId)
@@ -127,7 +129,10 @@ private class ChapterForDownload(
127129
}
128130

129131
private fun isNotCompletelyDownloaded(): Boolean {
130-
return !(chapterEntry[ChapterTable.isDownloaded] && firstPageExists())
132+
return !(
133+
chapterEntry[ChapterTable.isDownloaded] &&
134+
(firstPageExists() || File(getChapterCbzPath(mangaId, chapterEntry[ChapterTable.id].value)).exists())
135+
)
131136
}
132137

133138
private fun firstPageExists(): Boolean {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package suwayomi.tachidesk.manga.impl.download
2+
3+
import kotlinx.coroutines.CoroutineScope
4+
import kotlinx.coroutines.Dispatchers
5+
import kotlinx.coroutines.withContext
6+
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
7+
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
8+
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
9+
import java.io.File
10+
import java.io.InputStream
11+
import java.util.zip.ZipEntry
12+
import java.util.zip.ZipFile
13+
import java.util.zip.ZipInputStream
14+
import java.util.zip.ZipOutputStream
15+
16+
class ArchiveProvider(mangaId: Int, chapterId: Int) : DownloadedFilesProvider(mangaId, chapterId) {
17+
override fun getImage(index: Int): Pair<InputStream, String> {
18+
val cbzPath = getChapterCbzPath(mangaId, chapterId)
19+
val zipFile = ZipFile(cbzPath)
20+
val zipEntry = zipFile.entries().toList().sortedWith(compareBy({ it.name }, { it.name }))[index]
21+
val inputStream = zipFile.getInputStream(zipEntry)
22+
val fileType = zipEntry.name.substringAfterLast(".")
23+
return Pair(inputStream.buffered(), "image/$fileType")
24+
}
25+
26+
override suspend fun download(
27+
download: DownloadChapter,
28+
scope: CoroutineScope,
29+
step: suspend (DownloadChapter?, Boolean) -> Unit
30+
): Boolean {
31+
val chapterDir = getChapterDownloadPath(mangaId, chapterId)
32+
val outputFile = File(getChapterCbzPath(mangaId, chapterId))
33+
val chapterFolder = File(chapterDir)
34+
if (outputFile.exists()) handleExistingCbzFile(outputFile, chapterFolder)
35+
36+
withContext(Dispatchers.IO) {
37+
outputFile.createNewFile()
38+
}
39+
40+
FolderProvider(mangaId, chapterId).download(download, scope, step)
41+
42+
ZipOutputStream(outputFile.outputStream()).use { zipOut ->
43+
if (chapterFolder.isDirectory) {
44+
chapterFolder.listFiles()?.sortedBy { it.name }?.forEach {
45+
val entry = ZipEntry(it.name)
46+
try {
47+
zipOut.putNextEntry(entry)
48+
it.inputStream().use { inputStream ->
49+
inputStream.copyTo(zipOut)
50+
}
51+
} finally {
52+
zipOut.closeEntry()
53+
}
54+
}
55+
}
56+
}
57+
58+
if (chapterFolder.exists() && chapterFolder.isDirectory) {
59+
chapterFolder.deleteRecursively()
60+
}
61+
62+
return true
63+
}
64+
65+
override fun delete(): Boolean {
66+
val cbzFile = File(getChapterCbzPath(mangaId, chapterId))
67+
if (cbzFile.exists()) return cbzFile.delete()
68+
return false
69+
}
70+
71+
private fun handleExistingCbzFile(cbzFile: File, chapterFolder: File) {
72+
if (!chapterFolder.exists()) chapterFolder.mkdirs()
73+
ZipInputStream(cbzFile.inputStream()).use { zipInputStream ->
74+
var zipEntry = zipInputStream.nextEntry
75+
while (zipEntry != null) {
76+
val file = File(chapterFolder, zipEntry.name)
77+
if (!file.exists()) {
78+
file.parentFile.mkdirs()
79+
file.createNewFile()
80+
}
81+
file.outputStream().use { outputStream ->
82+
zipInputStream.copyTo(outputStream)
83+
}
84+
zipEntry = zipInputStream.nextEntry
85+
}
86+
}
87+
cbzFile.delete()
88+
}
89+
}

server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/DirName.kt

+5
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ private fun getChapterDir(mangaId: Int, chapterId: Int): String {
4747
fun getChapterDownloadPath(mangaId: Int, chapterId: Int): String {
4848
return applicationDirs.mangaDownloadsRoot + "/" + getChapterDir(mangaId, chapterId)
4949
}
50+
51+
fun getChapterCbzPath(mangaId: Int, chapterId: Int): String {
52+
return getChapterDownloadPath(mangaId, chapterId) + ".cbz"
53+
}
54+
5055
fun getChapterCachePath(mangaId: Int, chapterId: Int): String {
5156
return applicationDirs.tempMangaCacheRoot + "/" + getChapterDir(mangaId, chapterId)
5257
}

server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ class ServerConfig(config: Config, moduleName: String = MODULE_NAME) : SystemPro
2323
val socksProxyHost: String by overridableConfig
2424
val socksProxyPort: String by overridableConfig
2525

26+
// downloader
27+
val downloadAsCbz: Boolean by overridableConfig
28+
2629
// misc
2730
val debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
2831
val systemTrayEnabled: Boolean by overridableConfig

server/src/main/resources/server-reference.conf

+3
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ server.basicAuthEnabled = false
1919
server.basicAuthUsername = ""
2020
server.basicAuthPassword = ""
2121

22+
# downloader
23+
server.downloadAsCbz = false
24+
2225
# misc
2326
server.debugLogsEnabled = false
2427
server.systemTrayEnabled = true

server/src/test/resources/server-reference.conf

+3
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ server.socksProxyEnabled = false
77
server.socksProxyHost = ""
88
server.socksProxyPort = ""
99

10+
# downloader
11+
server.downloadAsCbz = false
12+
1013
# misc
1114
server.debugLogsEnabled = true
1215
server.systemTrayEnabled = false

0 commit comments

Comments
 (0)