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

View File

@@ -41,7 +41,7 @@ object ChapterDownloadHelper {
private fun provider( private fun provider(
mangaId: Int, mangaId: Int,
chapterId: Int, chapterId: Int,
): ChaptersFilesProvider { ): ChaptersFilesProvider<*> {
val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId)) val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
val cbzFile = File(getChapterCbzPath(mangaId, chapterId)) val cbzFile = File(getChapterCbzPath(mangaId, chapterId))
if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId) if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId)

View File

@@ -9,6 +9,8 @@ package suwayomi.tachidesk.manga.impl.chapter
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import mu.KLogger
import mu.KotlinLogging
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.and 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.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.update import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.manga.impl.Page.getPageName import suwayomi.tachidesk.manga.impl.ChapterDownloadHelper
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogueSourceOrStub 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.dataclass.ChapterDataClass
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.PageTable import suwayomi.tachidesk.manga.model.table.PageTable
import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.manga.model.table.toDataClass
import java.io.File
suspend fun getChapterDownloadReady( suspend fun getChapterDownloadReady(
chapterId: Int? = null, chapterId: Int? = null,
@@ -55,8 +53,25 @@ private class ChapterForDownload(
optChapterIndex: Int? = null, optChapterIndex: Int? = null,
optMangaId: Int? = null, optMangaId: Int? = null,
) { ) {
var chapterEntry: ResultRow
val chapterId: Int
val chapterIndex: Int
val mangaId: Int
val logger: KLogger
suspend fun asDownloadReady(): ChapterDataClass { 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() markAsNotDownloaded()
val pageList = fetchPageList() val pageList = fetchPageList()
@@ -69,16 +84,16 @@ private class ChapterForDownload(
private fun asDataClass() = ChapterTable.toDataClass(chapterEntry) private fun asDataClass() = ChapterTable.toDataClass(chapterEntry)
var chapterEntry: ResultRow
val chapterId: Int
val chapterIndex: Int
val mangaId: Int
init { init {
chapterEntry = freshChapterEntry(optChapterId, optChapterIndex, optMangaId) chapterEntry = freshChapterEntry(optChapterId, optChapterIndex, optMangaId)
chapterId = chapterEntry[ChapterTable.id].value chapterId = chapterEntry[ChapterTable.id].value
chapterIndex = chapterEntry[ChapterTable.sourceOrder] chapterIndex = chapterEntry[ChapterTable.sourceOrder]
mangaId = chapterEntry[ChapterTable.manga].value mangaId = chapterEntry[ChapterTable.manga].value
logger =
KotlinLogging.logger(
"${ChapterForDownload::class.java.name}(mangaId= $mangaId, chapterId= $chapterId, chapterIndex= $chapterIndex)",
)
} }
private fun freshChapterEntry( 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 { private fun firstPageExists(): Boolean {
val chapterId = chapterEntry[ChapterTable.id].value return try {
ChapterDownloadHelper.getImage(mangaId, chapterId, 0).first.close()
val chapterDir = getChapterDownloadPath(mangaId, chapterId) true
} catch (e: Exception) {
println(chapterDir) false
println(getPageName(0)) }
return ImageResponse.findFileNameStartingWith(
chapterDir,
getPageName(0),
) != null
} }
} }

View File

@@ -1,5 +1,6 @@
package suwayomi.tachidesk.manga.impl.download.fileProvider package suwayomi.tachidesk.manga.impl.download.fileProvider
import eu.kanade.tachiyomi.source.local.metadata.COMIC_INFO_FILE
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@@ -7,6 +8,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.select
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import suwayomi.tachidesk.manga.impl.Page import suwayomi.tachidesk.manga.impl.Page
@@ -20,11 +22,54 @@ import suwayomi.tachidesk.manga.model.table.MangaTable
import java.io.File import java.io.File
import java.io.InputStream 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 * Base class for downloaded chapter files provider, example: Folder, Archive
*/ */
abstract class ChaptersFilesProvider(val mangaId: Int, val chapterId: Int) : DownloadedFilesProvider { abstract class ChaptersFilesProvider<Type : FileType>(val mangaId: Int, val chapterId: Int) : DownloadedFilesProvider {
abstract fun getImageImpl(index: Int): Pair<InputStream, String> 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> { override fun getImage(): RetrieveFile1Args<Int> {
return RetrieveFile1Args(::getImageImpl) return RetrieveFile1Args(::getImageImpl)

View File

@@ -10,6 +10,7 @@ import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider 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.getChapterCachePath
import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath import suwayomi.tachidesk.manga.impl.util.getChapterCbzPath
import suwayomi.tachidesk.manga.impl.util.getMangaDownloadDir import suwayomi.tachidesk.manga.impl.util.getMangaDownloadDir
@@ -20,14 +21,14 @@ import java.io.InputStream
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mangaId, chapterId) { class ArchiveProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider<FileType.ZipFile>(mangaId, chapterId) {
override fun getImageImpl(index: Int): Pair<InputStream, String> { override fun getImageFiles(): List<FileType.ZipFile> {
val cbzPath = getChapterCbzPath(mangaId, chapterId) val zipFile = ZipFile(getChapterCbzPath(mangaId, chapterId))
val zipFile = ZipFile(cbzPath) return zipFile.entries.toList().map { FileType.ZipFile(it) }
val zipEntry = zipFile.entries.toList().sortedWith(compareBy({ it.name }, { it.name }))[index] }
val inputStream = zipFile.getInputStream(zipEntry)
val fileType = zipEntry.name.substringAfterLast(".") override fun getImageInputStream(image: FileType.ZipFile): InputStream {
return Pair(inputStream.buffered(), "image/$fileType") return ZipFile(getChapterCbzPath(mangaId, chapterId)).getInputStream(image.entry)
} }
override fun extractExistingDownload() { override fun extractExistingDownload() {

View File

@@ -4,27 +4,32 @@ import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
import suwayomi.tachidesk.manga.impl.download.fileProvider.ChaptersFilesProvider 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.getChapterCachePath
import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
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
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream
private val applicationDirs by DI.global.instance<ApplicationDirs>() private val applicationDirs by DI.global.instance<ApplicationDirs>()
/* /*
* Provides downloaded files when pages were downloaded into folders * Provides downloaded files when pages were downloaded into folders
* */ * */
class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider(mangaId, chapterId) { class FolderProvider(mangaId: Int, chapterId: Int) : ChaptersFilesProvider<RegularFile>(mangaId, chapterId) {
override fun getImageImpl(index: Int): Pair<InputStream, String> { override fun getImageFiles(): List<RegularFile> {
val chapterDir = getChapterDownloadPath(mangaId, chapterId) val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
val folder = File(chapterDir)
folder.mkdirs() if (!chapterFolder.exists()) {
val file = folder.listFiles()?.sortedBy { it.name }?.get(index) throw Exception("download folder does not exist")
val fileType = file!!.name.substringAfterLast(".") }
return Pair(FileInputStream(file).buffered(), "image/$fileType")
return chapterFolder.listFiles().orEmpty().toList().map(::RegularFile)
}
override fun getImageInputStream(image: RegularFile): FileInputStream {
return FileInputStream(image.file)
} }
override fun extractExistingDownload() { override fun extractExistingDownload() {