Add chapter lastReadAt to backups as BackupHistory (#1477)

* Add chapter lastReadAt to backups as BackupHistory

* MaxOrNull
This commit is contained in:
Mitchell Syer
2025-06-28 17:04:04 -04:00
committed by GitHub
parent 16d4893480
commit 8c4a2cb529
2 changed files with 47 additions and 24 deletions
@@ -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.Backup
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory 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.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.BackupManga
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource 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 uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
import kotlin.time.Duration.Companion.seconds
object ProtoBackupExport : ProtoBackupBase() { object ProtoBackupExport : ProtoBackupBase() {
private val logger = KotlinLogging.logger { } private val logger = KotlinLogging.logger { }
@@ -222,7 +223,7 @@ object ProtoBackupExport : ProtoBackupBase() {
genre = mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(), genre = mangaRow[MangaTable.genre]?.split(", ") ?: emptyList(),
status = MangaStatus.valueOf(mangaRow[MangaTable.status]).value, status = MangaStatus.valueOf(mangaRow[MangaTable.status]).value,
thumbnailUrl = mangaRow[MangaTable.thumbnail_url], thumbnailUrl = mangaRow[MangaTable.thumbnail_url],
dateAdded = TimeUnit.SECONDS.toMillis(mangaRow[MangaTable.inLibraryAt]), dateAdded = mangaRow[MangaTable.inLibraryAt].seconds.inWholeMilliseconds,
viewer = 0, // not supported in Tachidesk viewer = 0, // not supported in Tachidesk
updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy]), updateStrategy = UpdateStrategy.valueOf(mangaRow[MangaTable.updateStrategy]),
) )
@@ -233,7 +234,7 @@ object ProtoBackupExport : ProtoBackupBase() {
backupManga.meta = Manga.getMangaMetaMap(mangaId) backupManga.meta = Manga.getMangaMetaMap(mangaId)
} }
if (flags.includeChapters) { if (flags.includeChapters || flags.includeHistory) {
val chapters = val chapters =
transaction { transaction {
ChapterTable ChapterTable
@@ -244,27 +245,42 @@ object ProtoBackupExport : ProtoBackupBase() {
ChapterTable.toDataClass(it) ChapterTable.toDataClass(it)
} }
} }
val chapterToMeta = Chapter.getChaptersMetaMaps(chapters.map { it.id }) if (flags.includeChapters) {
val chapterToMeta = Chapter.getChaptersMetaMaps(chapters.map { it.id })
backupManga.chapters = backupManga.chapters =
chapters.map { chapters.map {
BackupChapter( BackupChapter(
it.url, it.url,
it.name, it.name,
it.scanlator, it.scanlator,
it.read, it.read,
it.bookmarked, it.bookmarked,
it.lastPageRead, it.lastPageRead,
TimeUnit.SECONDS.toMillis(it.fetchedAt), it.fetchedAt.seconds.inWholeMilliseconds,
it.uploadDate, it.uploadDate,
it.chapterNumber, it.chapterNumber,
chapters.size - it.index, chapters.size - it.index,
).apply { ).apply {
if (flags.includeClientData) { if (flags.includeClientData) {
this.meta = chapterToMeta[it.id] ?: emptyMap() 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) { if (flags.includeCategories) {
@@ -61,6 +61,7 @@ import java.io.InputStream
import java.util.Date import java.util.Date
import java.util.Timer import java.util.Timer
import java.util.TimerTask import java.util.TimerTask
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max import kotlin.math.max
import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.milliseconds
import suwayomi.tachidesk.manga.impl.track.Track as Tracker import suwayomi.tachidesk.manga.impl.track.Track as Tracker
@@ -106,7 +107,7 @@ object ProtoBackupImport : ProtoBackupBase() {
) : BackupRestoreState() ) : BackupRestoreState()
} }
private val backupRestoreIdToState = mutableMapOf<String, BackupRestoreState>() private val backupRestoreIdToState = ConcurrentHashMap<String, BackupRestoreState>()
val notifyFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = DROP_OLDEST) val notifyFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = DROP_OLDEST)
@@ -294,7 +295,6 @@ object ProtoBackupImport : ProtoBackupBase() {
} }
} }
@Suppress("UNUSED_PARAMETER") // TODO: remove
private fun restoreMangaData( private fun restoreMangaData(
manga: BackupManga, manga: BackupManga,
chapters: List<BackupChapter>, chapters: List<BackupChapter>,
@@ -368,7 +368,7 @@ object ProtoBackupImport : ProtoBackupBase() {
} }
// merge chapter data // merge chapter data
restoreMangaChapterData(mangaId, restoreMode, chapters) restoreMangaChapterData(mangaId, restoreMode, chapters, history)
// merge categories // merge categories
restoreMangaCategoryData(mangaId, categoryIds) restoreMangaCategoryData(mangaId, categoryIds)
@@ -404,8 +404,10 @@ object ProtoBackupImport : ProtoBackupBase() {
mangaId: Int, mangaId: Int,
restoreMode: RestoreMode, restoreMode: RestoreMode,
chapters: List<BackupChapter>, chapters: List<BackupChapter>,
history: List<BackupHistory>,
) = dbTransaction { ) = dbTransaction {
val (chaptersToInsert, chaptersToUpdateToDbChapter) = getMangaChapterToRestoreInfo(mangaId, restoreMode, chapters) val (chaptersToInsert, chaptersToUpdateToDbChapter) = getMangaChapterToRestoreInfo(mangaId, restoreMode, chapters)
val historyByChapter = history.groupBy({ it.url }, { it.lastRead })
val insertedChapterIds = val insertedChapterIds =
ChapterTable ChapterTable
@@ -428,6 +430,8 @@ object ProtoBackupImport : ProtoBackupBase() {
this[ChapterTable.isBookmarked] = chapter.bookmark this[ChapterTable.isBookmarked] = chapter.bookmark
this[ChapterTable.fetchedAt] = chapter.dateFetch.milliseconds.inWholeSeconds this[ChapterTable.fetchedAt] = chapter.dateFetch.milliseconds.inWholeSeconds
this[ChapterTable.lastReadAt] = historyByChapter[chapter.url]?.maxOrNull()?.milliseconds?.inWholeSeconds ?: 0
}.map { it[ChapterTable.id].value } }.map { it[ChapterTable.id].value }
if (chaptersToUpdateToDbChapter.isNotEmpty()) { if (chaptersToUpdateToDbChapter.isNotEmpty()) {
@@ -438,6 +442,9 @@ object ProtoBackupImport : ProtoBackupBase() {
this[ChapterTable.lastPageRead] = this[ChapterTable.lastPageRead] =
max(backupChapter.lastPageRead, dbChapter[ChapterTable.lastPageRead]).coerceAtLeast(0) max(backupChapter.lastPageRead, dbChapter[ChapterTable.lastPageRead]).coerceAtLeast(0)
this[ChapterTable.isBookmarked] = backupChapter.bookmark || dbChapter[ChapterTable.isBookmarked] 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) execute(this@dbTransaction)
} }