Feature/Improve chapter download with valid existing download handling (#1553)

* Fix early exit on download for existing download for FolderProvider

The current check only worked for the "ArchiveProvider". The "FolderProvider" never moved the existing download to the cache folder.

In case the existing download is considered to be reusable, there is no need to proceed with the download logic.

* Fix "ArchiveProvider#extractExistingDownload"

The "ChaptersFilesProvider#extractExistingDownload" expects the download to be extracted into the final download folder.
However, the "ArchiveProvider" extracted the download into the chapter download cache folder.

* Add chapter download function call requirements
This commit is contained in:
schroda
2025-08-01 01:55:09 +02:00
committed by GitHub
parent 87aae46a1f
commit 1d9991e562
4 changed files with 69 additions and 58 deletions
@@ -2,6 +2,7 @@ package suwayomi.tachidesk.manga.impl
import kotlinx.coroutines.CoroutineScope
import org.jetbrains.exposed.sql.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
@@ -32,6 +33,9 @@ object ChapterDownloadHelper {
chapterId: Int,
): Boolean = provider(mangaId, chapterId).delete()
/**
* This function should never be called without calling [getChapterDownloadReady] beforehand.
*/
suspend fun download(
mangaId: Int,
chapterId: Int,
@@ -99,7 +99,7 @@ private class ChapterForDownload(
if (!doPageCountsMatch) {
log.debug { "use page count of downloaded chapter" }
updatePageCount(ChapterDownloadHelper.getImageCount(mangaId, chapterId))
updatePageCount(downloadPageCount)
}
return asDataClass()
@@ -13,8 +13,8 @@ import libcore.net.MimeUtils
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Page
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
import suwayomi.tachidesk.manga.impl.util.createComicInfoFile
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
@@ -105,6 +105,27 @@ abstract class ChaptersFilesProvider<Type : FileType>(
scope: CoroutineScope,
step: suspend (DownloadChapter?, Boolean) -> Unit,
): Boolean {
val existingDownloadPageCount =
try {
getImageCount()
} catch (_: Exception) {
0
}
val pageCount = download.chapter.pageCount
check(pageCount > 0) { "pageCount must be greater than 0 - ChapterForDownload#getChapterDownloadReady not called" }
check(existingDownloadPageCount == 0 || existingDownloadPageCount == pageCount) {
"existingDownloadPageCount must be 0 or equal to pageCount - ChapterForDownload#getChapterDownloadReady not called"
}
val doesUnrecognizedDownloadExist = existingDownloadPageCount == pageCount
if (doesUnrecognizedDownloadExist) {
download.progress = 1f
step(download, false)
return true
}
extractExistingDownload()
val finalDownloadFolder = getChapterDownloadPath(mangaId, chapterId)
@@ -113,57 +134,45 @@ abstract class ChaptersFilesProvider<Type : FileType>(
val downloadCacheFolder = File(cacheChapterDir)
downloadCacheFolder.mkdirs()
val pageCount = download.chapter.pageCount
if (
downloadCacheFolder
.listFiles()
.orEmpty()
.filter { it.name != COMIC_INFO_FILE }
.size >= pageCount
) {
download.progress = 1f
step(download, false)
} else {
for (pageNum in 0 until pageCount) {
var pageProgressJob: Job? = null
val fileName = Page.getPageName(pageNum, pageCount) // might have to change this to index stored in database
for (pageNum in 0 until pageCount) {
var pageProgressJob: Job? = null
val fileName = Page.getPageName(pageNum, pageCount) // might have to change this to index stored in database
val pageExistsInFinalDownloadFolder = ImageResponse.findFileNameStartingWith(finalDownloadFolder, fileName) != null
val pageExistsInCacheDownloadFolder = ImageResponse.findFileNameStartingWith(cacheChapterDir, fileName) != null
val pageExistsInFinalDownloadFolder = ImageResponse.findFileNameStartingWith(finalDownloadFolder, fileName) != null
val pageExistsInCacheDownloadFolder = ImageResponse.findFileNameStartingWith(cacheChapterDir, fileName) != null
val doesPageAlreadyExist = pageExistsInFinalDownloadFolder || pageExistsInCacheDownloadFolder
if (doesPageAlreadyExist) {
continue
}
try {
Page
.getPageImage(
mangaId = download.mangaId,
chapterIndex = download.chapterIndex,
index = pageNum,
) { flow ->
pageProgressJob =
flow
.sample(100)
.distinctUntilChanged()
.onEach {
download.progress = (pageNum.toFloat() + (it.toFloat() * 0.01f)) / pageCount
step(
null,
false,
) // don't throw on canceled download here since we can't do anything
}.launchIn(scope)
}.first
.close()
} finally {
// always cancel the page progress job even if it throws an exception to avoid memory leaks
pageProgressJob?.cancel()
}
// TODO: retry on error with 2,4,8 seconds of wait
download.progress = ((pageNum + 1).toFloat()) / pageCount
step(download, false)
val doesPageAlreadyExist = pageExistsInFinalDownloadFolder || pageExistsInCacheDownloadFolder
if (doesPageAlreadyExist) {
continue
}
try {
Page
.getPageImage(
mangaId = download.mangaId,
chapterIndex = download.chapterIndex,
index = pageNum,
) { flow ->
pageProgressJob =
flow
.sample(100)
.distinctUntilChanged()
.onEach {
download.progress = (pageNum.toFloat() + (it.toFloat() * 0.01f)) / pageCount
step(
null,
false,
) // don't throw on canceled download here since we can't do anything
}.launchIn(scope)
}.first
.close()
} finally {
// always cancel the page progress job even if it throws an exception to avoid memory leaks
pageProgressJob?.cancel()
}
// TODO: retry on error with 2,4,8 seconds of wait
download.progress = ((pageNum + 1).toFloat()) / pageCount
step(download, false)
}
createComicInfoFile(
@@ -180,17 +189,14 @@ abstract class ChaptersFilesProvider<Type : FileType>(
handleSuccessfulDownload()
transaction {
ChapterTable.update({ ChapterTable.id eq chapterId }) {
it[ChapterTable.pageCount] = getImageCount()
}
}
File(cacheChapterDir).deleteRecursively()
return true
}
/**
* This function should never be called without calling [getChapterDownloadReady] beforehand.
*/
override fun download(): FileDownload3Args<DownloadChapter, CoroutineScope, suspend (DownloadChapter?, Boolean) -> Unit> =
FileDownload3Args(::downloadImpl)
@@ -10,6 +10,7 @@ import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider
import suwayomi.tachidesk.manga.impl.download.fileProvider.FileType
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
import suwayomi.tachidesk.manga.impl.util.getMangaDownloadDir
import suwayomi.tachidesk.manga.impl.util.storage.FileDeletionHelper
import suwayomi.tachidesk.server.ApplicationDirs
@@ -37,13 +38,13 @@ class ArchiveProvider(
override fun extractExistingDownload() {
val outputFile = File(getChapterCbzPath(mangaId, chapterId))
val chapterCacheFolder = File(getChapterCachePath(mangaId, chapterId))
val chapterDownloadFolder = File(getChapterDownloadPath(mangaId, chapterId))
if (!outputFile.exists()) {
return
}
extractCbzFile(outputFile, chapterCacheFolder)
extractCbzFile(outputFile, chapterDownloadFolder)
}
override suspend fun handleSuccessfulDownload() {