mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-08 21:34:34 -05:00
* feat(download): improve chapter download filenames * refactor(download): use SafePath helper for filename sanitization
152 lines
5.8 KiB
Kotlin
152 lines
5.8 KiB
Kotlin
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<InputStream, String> = 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<InputStream, Long> = provider(mangaId, chapterId).getAsArchiveStream()
|
|
|
|
fun getChapterArchiveSize(
|
|
mangaId: Int,
|
|
chapterId: Int,
|
|
): Long = provider(mangaId, chapterId).getArchiveSize()
|
|
|
|
private fun getChapterWithCbzFileName(chapterId: Int): Pair<ChapterDataClass, String> =
|
|
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<InputStream, String, Long> {
|
|
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<String, Long> { // fileName, fileSize
|
|
val (chapterData, fileName) = getChapterWithCbzFileName(chapterId)
|
|
|
|
val fileSize = provider(chapterData.mangaId, chapterData.id).getArchiveSize()
|
|
|
|
return Pair(fileName, fileSize)
|
|
}
|
|
}
|