From 59d2151c920863175383c9f576fa4a126f5b09ad Mon Sep 17 00:00:00 2001 From: schroda <50052685+schroda@users.noreply.github.com> Date: Mon, 28 Apr 2025 00:54:54 +0200 Subject: [PATCH] Prevent duplicated chapter pages (#1353) In case "ChapterForDownload#asDownloadReady" was called in quick succession, the page list got inserted twice. This caused problems with getting the images from the rest endpoint, because they are selected by sorting them by asc index and selecting the page by using the provided index as an offset. This, however, only works as long as there are no duplicates, otherwise, page indexes 1, 2; 3, 4; 5, 6; ... will just return the same page. --- .../manga/impl/chapter/ChapterForDownload.kt | 22 +++++++++++++--- .../M0045_PreventDuplicatedChapterPages.kt | 26 +++++++++++++++++++ 2 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0045_PreventDuplicatedChapterPages.kt diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt index 6b5fe8f0..5222b1e0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/chapter/ChapterForDownload.kt @@ -11,6 +11,9 @@ import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.SChapter import io.github.oshai.kotlinlogging.KLogger import io.github.oshai.kotlinlogging.KotlinLogging +import io.github.reactivecircus.cache4k.Cache +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.and @@ -26,6 +29,7 @@ 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 kotlin.time.Duration.Companion.minutes suspend fun getChapterDownloadReady( chapterId: Int? = null, @@ -43,6 +47,12 @@ suspend fun getChapterDownloadReadyByIndex( mangaId: Int, ): ChapterDataClass = getChapterDownloadReady(chapterIndex = chapterIndex, mangaId = mangaId) +private val mutexByChapterId: Cache = + Cache + .Builder() + .expireAfterAccess(10.minutes) + .build() + private class ChapterForDownload( optChapterId: Int? = null, optChapterIndex: Int? = null, @@ -69,9 +79,7 @@ private class ChapterForDownload( markAsNotDownloaded() - val pageList = fetchPageList() - - updateDatabasePages(pageList) + updatePageList() } return asDataClass() @@ -109,6 +117,14 @@ private class ChapterForDownload( }.first() } + private suspend fun updatePageList() { + val mutex = mutexByChapterId.get(chapterId) { Mutex() } + mutex.withLock { + val pageList = fetchPageList() + updateDatabasePages(pageList) + } + } + private suspend fun fetchPageList(): List { val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() } val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference]) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0045_PreventDuplicatedChapterPages.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0045_PreventDuplicatedChapterPages.kt new file mode 100644 index 00000000..e3985bc3 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0045_PreventDuplicatedChapterPages.kt @@ -0,0 +1,26 @@ +package suwayomi.tachidesk.server.database.migration + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import de.neonew.exposed.migrations.helpers.SQLMigration + +@Suppress("ClassName", "unused") +class M0045_PreventDuplicatedChapterPages : SQLMigration() { + override val sql: String = + """ + DELETE FROM PAGE + WHERE ID NOT IN ( + SELECT MIN(ID) + FROM PAGE + GROUP BY INDEX, "imageUrl", CHAPTER + ); + + ALTER TABLE PAGE + ADD CONSTRAINT UC_PAGE UNIQUE (INDEX, imageUrl, CHAPTER) + """.trimIndent() +}