Fix/chapter downloaded check (#1012)

* Properly check for first page in cbz files

The download check for cbz files only checked if the archive existed but didn't check for the first page

* Streamline getImageImpl of ChapterDownloadProviders

* Exclude comic info file from page list

In case the download folder did not contain any page files, only the comic info file existed, which caused the download check to incorrectly detect the first page

* Add logging to ChapterForDownload#asDownloadReady
This commit is contained in:
schroda
2024-09-01 00:54:06 +02:00
committed by GitHub
parent 9f49587245
commit ef6be74ec2
5 changed files with 103 additions and 49 deletions
@@ -41,7 +41,7 @@ object ChapterDownloadHelper {
private fun provider(
mangaId: Int,
chapterId: Int,
): ChaptersFilesProvider {
): ChaptersFilesProvider<*> {
val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
val cbzFile = File(getChapterCbzPath(mangaId, chapterId))
if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId)
@@ -9,6 +9,8 @@ package suwayomi.tachidesk.manga.impl.chapter
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import mu.KLogger
import mu.KotlinLogging
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and
@@ -17,17 +19,13 @@ import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Page.getPageName
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
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.PageTable
import suwayomi.tachidesk.manga.model.table.toDataClass
import java.io.File
suspend fun getChapterDownloadReady(
chapterId: Int? = null,
@@ -55,8 +53,25 @@ private class ChapterForDownload(
optChapterIndex: Int? = null,
optMangaId: Int? = null,
) {
var chapterEntry: ResultRow
val chapterId: Int
val chapterIndex: Int
val mangaId: Int
val logger: KLogger
suspend fun asDownloadReady(): ChapterDataClass {
if (isNotCompletelyDownloaded()) {
val log = KotlinLogging.logger("${logger.name}::asDownloadReady")
val isMarkedAsDownloaded = chapterEntry[ChapterTable.isDownloaded]
val doesFirstPageExist = firstPageExists()
val isDownloaded = isMarkedAsDownloaded && doesFirstPageExist
log.debug { "isDownloaded= $isDownloaded (isMarkedAsDownloaded= $isMarkedAsDownloaded, doesFirstPageExist= $doesFirstPageExist)" }
if (!isDownloaded) {
log.debug { "reset download status and fetch page list" }
markAsNotDownloaded()
val pageList = fetchPageList()
@@ -69,16 +84,16 @@ private class ChapterForDownload(
private fun asDataClass() = ChapterTable.toDataClass(chapterEntry)
var chapterEntry: ResultRow
val chapterId: Int
val chapterIndex: Int
val mangaId: Int
init {
chapterEntry = freshChapterEntry(optChapterId, optChapterIndex, optMangaId)
chapterId = chapterEntry[ChapterTable.id].value
chapterIndex = chapterEntry[ChapterTable.sourceOrder]
mangaId = chapterEntry[ChapterTable.manga].value
logger =
KotlinLogging.logger(
"${ChapterForDownload::class.java.name}(mangaId= $mangaId, chapterId= $chapterId, chapterIndex= $chapterIndex)",
)
}
private fun freshChapterEntry(
@@ -151,24 +166,12 @@ private class ChapterForDownload(
}
}
private fun isNotCompletelyDownloaded(): Boolean {
return !(
chapterEntry[ChapterTable.isDownloaded] &&
(firstPageExists() || File(getChapterCbzPath(mangaId, chapterEntry[ChapterTable.id].value)).exists())
)
}
private fun firstPageExists(): Boolean {
val chapterId = chapterEntry[ChapterTable.id].value
val chapterDir = getChapterDownloadPath(mangaId, chapterId)
println(chapterDir)
println(getPageName(0))
return ImageResponse.findFileNameStartingWith(
chapterDir,
getPageName(0),
) != null
return try {
ChapterDownloadHelper.getImage(mangaId, chapterId, 0).first.close()
true
} catch (e: Exception) {
false
}
}
}
@@ -1,5 +1,6 @@
package suwayomi.tachidesk.manga.impl.download.fileProvider
import eu.kanade.tachiyomi.source.local.metadata.COMIC_INFO_FILE
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
@@ -7,6 +8,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.manga.impl.Page
@@ -20,11 +22,54 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
import java.io.File
import java.io.InputStream
sealed class FileType {
data class RegularFile(val file: File) : FileType()
data class ZipFile(val entry: ZipArchiveEntry) : FileType()
fun getName(): String {
return when (this) {
is FileType.RegularFile -> {
this.file.name
}
is FileType.ZipFile -> {
this.entry.name
}
}
}
fun getExtension(): String {
return when (this) {
is FileType.RegularFile -> {
this.file.extension
}
is FileType.ZipFile -> {
this.entry.name.substringAfterLast(".")
}
}
}
}
/*
* Base class for downloaded chapter files provider, example: Folder, Archive
*/
abstract class ChaptersFilesProvider(val mangaId: Int, val chapterId: Int) : DownloadedFilesProvider {
abstract fun getImageImpl(index: Int): Pair<InputStream, String>
abstract class ChaptersFilesProvider<Type : FileType>(val mangaId: Int, val chapterId: Int) : DownloadedFilesProvider {
protected abstract fun getImageFiles(): List<Type>
protected abstract fun getImageInputStream(image: Type): InputStream
fun getImageImpl(index: Int): Pair<InputStream, String> {
val images = getImageFiles().filter { it.getName() != COMIC_INFO_FILE }.sortedBy { it.getName() }
if (images.isEmpty()) {
throw Exception("no downloaded images found")
}
val image = images[index]
val imageFileType = image.getExtension()
return Pair(getImageInputStream(image).buffered(), "image/$imageFileType")
}
override fun getImage(): RetrieveFile1Args<Int> {
return RetrieveFile1Args(::getImageImpl)
@@ -10,6 +10,7 @@ import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
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.getMangaDownloadDir
@@ -20,14 +21,14 @@ import java.io.InputStream
private val applicationDirs by DI.global.instance<ApplicationDirs>()
class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mangaId, chapterId) {
override fun getImageImpl(index: Int): Pair<InputStream, String> {
val cbzPath = getChapterCbzPath(mangaId, chapterId)
val zipFile = ZipFile(cbzPath)
val zipEntry = zipFile.entries.toList().sortedWith(compareBy({ it.name }, { it.name }))[index]
val inputStream = zipFile.getInputStream(zipEntry)
val fileType = zipEntry.name.substringAfterLast(".")
return Pair(inputStream.buffered(), "image/$fileType")
class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider<FileType.ZipFile>(mangaId, chapterId) {
override fun getImageFiles(): List<FileType.ZipFile> {
val zipFile = ZipFile(getChapterCbzPath(mangaId, chapterId))
return zipFile.entries.toList().map { FileType.ZipFile(it) }
}
override fun getImageInputStream(image: FileType.ZipFile): InputStream {
return ZipFile(getChapterCbzPath(mangaId, chapterId)).getInputStream(image.entry)
}
override fun extractExistingDownload() {
@@ -4,27 +4,32 @@ import org.kodein.di.DI
import org.kodein.di.conf.global
import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider
import suwayomi.tachidesk.manga.impl.download.fileProvider.FileType.RegularFile
import suwayomi.tachidesk.manga.impl.util.getChapterCachePath
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
import suwayomi.tachidesk.manga.impl.util.storage.FileDeletionHelper
import suwayomi.tachidesk.server.ApplicationDirs
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
private val applicationDirs by DI.global.instance<ApplicationDirs>()
/*
* Provides downloaded files when pages were downloaded into folders
* */
class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mangaId, chapterId) {
override fun getImageImpl(index: Int): Pair<InputStream, String> {
val chapterDir = getChapterDownloadPath(mangaId, chapterId)
val folder = File(chapterDir)
folder.mkdirs()
val file = folder.listFiles()?.sortedBy { it.name }?.get(index)
val fileType = file!!.name.substringAfterLast(".")
return Pair(FileInputStream(file).buffered(), "image/$fileType")
class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider<RegularFile>(mangaId, chapterId) {
override fun getImageFiles(): List<RegularFile> {
val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
if (!chapterFolder.exists()) {
throw Exception("download folder does not exist")
}
return chapterFolder.listFiles().orEmpty().toList().map(::RegularFile)
}
override fun getImageInputStream(image: RegularFile): FileInputStream {
return FileInputStream(image.file)
}
override fun extractExistingDownload() {