mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 03:14:40 -05:00
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:
@@ -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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
Reference in New Issue
Block a user