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

View File

@@ -2,6 +2,7 @@ package suwayomi.tachidesk.manga.impl
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.jetbrains.exposed.sql.transactions.transaction 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.ChaptersFilesProvider
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.ArchiveProvider import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.ArchiveProvider
import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.FolderProvider import suwayomi.tachidesk.manga.impl.download.fileProvider.impl.FolderProvider
@@ -32,6 +33,9 @@ object ChapterDownloadHelper {
chapterId: Int, chapterId: Int,
): Boolean = provider(mangaId, chapterId).delete() ): Boolean = provider(mangaId, chapterId).delete()
/**
* This function should never be called without calling [getChapterDownloadReady] beforehand.
*/
suspend fun download( suspend fun download(
mangaId: Int, mangaId: Int,
chapterId: Int, chapterId: Int,

View File

@@ -99,7 +99,7 @@ private class ChapterForDownload(
if (!doPageCountsMatch) { if (!doPageCountsMatch) {
log.debug { "use page count of downloaded chapter" } log.debug { "use page count of downloaded chapter" }
updatePageCount(ChapterDownloadHelper.getImageCount(mangaId, chapterId)) updatePageCount(downloadPageCount)
} }
return asDataClass() return asDataClass()

View File

@@ -13,8 +13,8 @@ import libcore.net.MimeUtils
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Page 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.download.model.DownloadChapter
import suwayomi.tachidesk.manga.impl.util.createComicInfoFile import suwayomi.tachidesk.manga.impl.util.createComicInfoFile
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
@@ -105,6 +105,27 @@ abstract class ChaptersFilesProvider<Type : FileType>(
scope: CoroutineScope, scope: CoroutineScope,
step: suspend (DownloadChapter?, Boolean) -> Unit, step: suspend (DownloadChapter?, Boolean) -> Unit,
): Boolean { ): 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() extractExistingDownload()
val finalDownloadFolder = getChapterDownloadPath(mangaId, chapterId) val finalDownloadFolder = getChapterDownloadPath(mangaId, chapterId)
@@ -113,57 +134,45 @@ abstract class ChaptersFilesProvider<Type : FileType>(
val downloadCacheFolder = File(cacheChapterDir) val downloadCacheFolder = File(cacheChapterDir)
downloadCacheFolder.mkdirs() downloadCacheFolder.mkdirs()
val pageCount = download.chapter.pageCount for (pageNum in 0 until pageCount) {
if ( var pageProgressJob: Job? = null
downloadCacheFolder val fileName = Page.getPageName(pageNum, pageCount) // might have to change this to index stored in database
.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
val pageExistsInFinalDownloadFolder = ImageResponse.findFileNameStartingWith(finalDownloadFolder, fileName) != null val pageExistsInFinalDownloadFolder = ImageResponse.findFileNameStartingWith(finalDownloadFolder, fileName) != null
val pageExistsInCacheDownloadFolder = ImageResponse.findFileNameStartingWith(cacheChapterDir, fileName) != null val pageExistsInCacheDownloadFolder = ImageResponse.findFileNameStartingWith(cacheChapterDir, fileName) != null
val doesPageAlreadyExist = pageExistsInFinalDownloadFolder || pageExistsInCacheDownloadFolder val doesPageAlreadyExist = pageExistsInFinalDownloadFolder || pageExistsInCacheDownloadFolder
if (doesPageAlreadyExist) { if (doesPageAlreadyExist) {
continue 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)
} }
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( createComicInfoFile(
@@ -180,17 +189,14 @@ abstract class ChaptersFilesProvider<Type : FileType>(
handleSuccessfulDownload() handleSuccessfulDownload()
transaction {
ChapterTable.update({ ChapterTable.id eq chapterId }) {
it[ChapterTable.pageCount] = getImageCount()
}
}
File(cacheChapterDir).deleteRecursively() File(cacheChapterDir).deleteRecursively()
return true return true
} }
/**
* This function should never be called without calling [getChapterDownloadReady] beforehand.
*/
override fun download(): FileDownload3Args<DownloadChapter, CoroutineScope, suspend (DownloadChapter?, Boolean) -> Unit> = override fun download(): FileDownload3Args<DownloadChapter, CoroutineScope, suspend (DownloadChapter?, Boolean) -> Unit> =
FileDownload3Args(::downloadImpl) FileDownload3Args(::downloadImpl)

View File

@@ -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.download.fileProvider.FileType
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath 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.getMangaDownloadDir
import suwayomi.tachidesk.manga.impl.util.storage.FileDeletionHelper import suwayomi.tachidesk.manga.impl.util.storage.FileDeletionHelper
import suwayomi.tachidesk.server.ApplicationDirs import suwayomi.tachidesk.server.ApplicationDirs
@@ -37,13 +38,13 @@ class ArchiveProvider(
override fun extractExistingDownload() { override fun extractExistingDownload() {
val outputFile = File(getChapterCbzPath(mangaId, chapterId)) val outputFile = File(getChapterCbzPath(mangaId, chapterId))
val chapterCacheFolder = File(getChapterCachePath(mangaId, chapterId)) val chapterDownloadFolder = File(getChapterDownloadPath(mangaId, chapterId))
if (!outputFile.exists()) { if (!outputFile.exists()) {
return return
} }
extractCbzFile(outputFile, chapterCacheFolder) extractCbzFile(outputFile, chapterDownloadFolder)
} }
override suspend fun handleSuccessfulDownload() { override suspend fun handleSuccessfulDownload() {