package suwayomi.tachidesk.manga.impl import kotlinx.coroutines.CoroutineScope import org.jetbrains.exposed.v1.core.eq import org.jetbrains.exposed.v1.jdbc.select import org.jetbrains.exposed.v1.jdbc.transactions.transaction import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady 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.DownloadQueueItem import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath import suwayomi.tachidesk.manga.model.dataclass.ChapterDataClass import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.server.serverConfig import xyz.nulldev.androidcompat.util.SafePath import java.io.File import java.io.InputStream object ChapterDownloadHelper { fun getImage( mangaId: Int, chapterId: Int, index: Int, ): Pair = provider(mangaId, chapterId).getImage().execute(index) fun getImageCount( mangaId: Int, chapterId: Int, ): Int = provider(mangaId, chapterId).getImageCount() fun delete( mangaId: Int, chapterId: Int, ): Boolean = provider(mangaId, chapterId).delete() /** * This function should never be called without calling [getChapterDownloadReady] beforehand. */ suspend fun download( mangaId: Int, chapterId: Int, download: DownloadQueueItem, scope: CoroutineScope, step: suspend (DownloadQueueItem?, Boolean) -> Unit, ): Boolean = 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, ): ChaptersFilesProvider<*> { val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId)) val cbzFile = File(getChapterCbzPath(mangaId, chapterId)) if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId) if (!chapterFolder.exists() && serverConfig.downloadAsCbz.value) return ArchiveProvider(mangaId, chapterId) return FolderProvider(mangaId, chapterId) } fun getArchiveStreamWithSize( mangaId: Int, chapterId: Int, ): Pair = provider(mangaId, chapterId).getAsArchiveStream() fun getChapterArchiveSize( mangaId: Int, chapterId: Int, ): Long = provider(mangaId, chapterId).getArchiveSize() private fun getChapterWithCbzFileName(chapterId: Int): Pair = 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 mangaTitle = row[MangaTable.title].trim() val scanlatorName = chapter.scanlator?.trim()?.takeIf { it.isNotEmpty() } val chapterName = chapter.name.trim().takeIf { it.isNotEmpty() } val fileName = buildString { append(mangaTitle) append(" - ") if (chapterName != null) { append(chapterName) } else if (chapter.chapterNumber >= 0f) { // chapterNumber is stored as Float, drop .0 for whole numbers. val formatNumber = if (chapter.chapterNumber % 1 == 0f) { chapter.chapterNumber.toInt().toString() } else { chapter.chapterNumber.toString() } append("#$formatNumber") } else { // Fallback when neither name nor valid chapter number exists append("#${chapter.index}") } if (scanlatorName != null) { append(" [") append(scanlatorName) append("]") } append(".cbz") } // Sanitize filename for OS compatibility val safeFileName = SafePath.buildValidFilename(fileName) Pair(chapter, safeFileName) } fun getCbzForDownload( chapterId: Int, markAsRead: Boolean?, ): Triple { val (chapterData, fileName) = getChapterWithCbzFileName(chapterId) val cbzFile = provider(chapterData.mangaId, chapterData.id).getAsArchiveStream() if (markAsRead == true) { Chapter.modifyChapter( chapterData.mangaId, chapterData.index, isRead = true, isBookmarked = null, markPrevRead = null, lastPageRead = null, ) } return Triple(cbzFile.first, fileName, cbzFile.second) } fun getCbzMetadataForDownload(chapterId: Int): Pair { // fileName, fileSize val (chapterData, fileName) = getChapterWithCbzFileName(chapterId) val fileSize = provider(chapterData.mangaId, chapterData.id).getArchiveSize() return Pair(fileName, fileSize) } }