From 8c4a2cb529640a5e6880bbf6ef4c57a8ec44a6a8 Mon Sep 17 00:00:00 2001 From: Mitchell Syer Date: Sat, 28 Jun 2025 17:04:04 -0400 Subject: [PATCH] Add chapter lastReadAt to backups as BackupHistory (#1477) * Add chapter lastReadAt to backups as BackupHistory * MaxOrNull --- .../impl/backup/proto/ProtoBackupExport.kt | 58 ++++++++++++------- .../impl/backup/proto/ProtoBackupImport.kt | 13 ++++- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt index 51007e49..557d117b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt @@ -34,6 +34,7 @@ import suwayomi.tachidesk.manga.impl.backup.BackupFlags import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupChapter +import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource @@ -53,8 +54,8 @@ import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import java.io.InputStream -import java.util.concurrent.TimeUnit import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.seconds object ProtoBackupExport : ProtoBackupBase() { private val logger = KotlinLogging.logger { } @@ -222,7 +223,7 @@ object ProtoBackupExport : ProtoBackupBase() { genre = mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(), status = MangaStatus.valueOf(mangaRow[MangaTable.status]).value, thumbnailUrl = mangaRow[MangaTable.thumbnail_url], - dateAdded = TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]), + dateAdded = mangaRow[MangaTable.inLibraryAt].seconds.inWholeMilliseconds, viewer = 0, // not supported in Tachidesk updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy]), ) @@ -233,7 +234,7 @@ object ProtoBackupExport : ProtoBackupBase() { backupManga.meta = Manga.getMangaMetaMap(mangaId) } - if (flags.includeChapters) { + if (flags.includeChapters || flags.includeHistory) { val chapters = transaction { ChapterTable @@ -244,27 +245,42 @@ object ProtoBackupExport : ProtoBackupBase() { ChapterTable.toDataClass(it) } } - val chapterToMeta = Chapter.getChaptersMetaMaps(chapters.map { it.id }) + if (flags.includeChapters) { + val chapterToMeta = Chapter.getChaptersMetaMaps(chapters.map { it.id }) - backupManga.chapters = - chapters.map { - BackupChapter( - it.url, - it.name, - it.scanlator, - it.read, - it.bookmarked, - it.lastPageRead, - TimeUnit.SECONDS.toMillis(it.fetchedAt), - it.uploadDate, - it.chapterNumber, - chapters.size - it.index, - ).apply { - if (flags.includeClientData) { - this.meta = chapterToMeta[it.id] ?: emptyMap() + backupManga.chapters = + chapters.map { + BackupChapter( + it.url, + it.name, + it.scanlator, + it.read, + it.bookmarked, + it.lastPageRead, + it.fetchedAt.seconds.inWholeMilliseconds, + it.uploadDate, + it.chapterNumber, + chapters.size - it.index, + ).apply { + if (flags.includeClientData) { + this.meta = chapterToMeta[it.id] ?: emptyMap() + } } } - } + } + if (flags.includeHistory) { + backupManga.history = + chapters.mapNotNull { + if (it.lastReadAt > 0) { + BackupHistory( + url = it.url, + lastRead = it.lastReadAt.seconds.inWholeMilliseconds, + ) + } else { + null + } + } + } } if (flags.includeCategories) { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt index df04865f..d2daa4c4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt @@ -61,6 +61,7 @@ import java.io.InputStream import java.util.Date import java.util.Timer import java.util.TimerTask +import java.util.concurrent.ConcurrentHashMap import kotlin.math.max import kotlin.time.Duration.Companion.milliseconds import suwayomi.tachidesk.manga.impl.track.Track as Tracker @@ -106,7 +107,7 @@ object ProtoBackupImport : ProtoBackupBase() { ) : BackupRestoreState() } - private val backupRestoreIdToState = mutableMapOf() + private val backupRestoreIdToState = ConcurrentHashMap() val notifyFlow = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = DROP_OLDEST) @@ -294,7 +295,6 @@ object ProtoBackupImport : ProtoBackupBase() { } } - @Suppress("UNUSED_PARAMETER") // TODO: remove private fun restoreMangaData( manga: BackupManga, chapters: List, @@ -368,7 +368,7 @@ object ProtoBackupImport : ProtoBackupBase() { } // merge chapter data - restoreMangaChapterData(mangaId, restoreMode, chapters) + restoreMangaChapterData(mangaId, restoreMode, chapters, history) // merge categories restoreMangaCategoryData(mangaId, categoryIds) @@ -404,8 +404,10 @@ object ProtoBackupImport : ProtoBackupBase() { mangaId: Int, restoreMode: RestoreMode, chapters: List, + history: List, ) = dbTransaction { val (chaptersToInsert, chaptersToUpdateToDbChapter) = getMangaChapterToRestoreInfo(mangaId, restoreMode, chapters) + val historyByChapter = history.groupBy({ it.url }, { it.lastRead }) val insertedChapterIds = ChapterTable @@ -428,6 +430,8 @@ object ProtoBackupImport : ProtoBackupBase() { this[ChapterTable.isBookmarked] = chapter.bookmark this[ChapterTable.fetchedAt] = chapter.dateFetch.milliseconds.inWholeSeconds + + this[ChapterTable.lastReadAt] = historyByChapter[chapter.url]?.maxOrNull()?.milliseconds?.inWholeSeconds ?: 0 }.map { it[ChapterTable.id].value } if (chaptersToUpdateToDbChapter.isNotEmpty()) { @@ -438,6 +442,9 @@ object ProtoBackupImport : ProtoBackupBase() { this[ChapterTable.lastPageRead] = max(backupChapter.lastPageRead, dbChapter[ChapterTable.lastPageRead]).coerceAtLeast(0) this[ChapterTable.isBookmarked] = backupChapter.bookmark || dbChapter[ChapterTable.isBookmarked] + this[ChapterTable.lastReadAt] = + (historyByChapter[backupChapter.url]?.maxOrNull()?.milliseconds?.inWholeSeconds ?: 0) + .coerceAtLeast(dbChapter[ChapterTable.lastReadAt]) } execute(this@dbTransaction) }