Feature/optimize backup import (#1270)

* Optimize restoring manga chapters

* Streamline restoring manga data

* Optimize restoring manga trackers

* Simplify passing manga category restore data

* Properly prevent mangas from getting added to default category

76595233fc never actually worked...

* Extract logic to add manga to categories from gql mutation

* Optimize restoring manga categories

* Optimize restoring categories
This commit is contained in:
schroda
2025-02-16 19:00:26 +01:00
committed by GitHub
parent 36cb899b91
commit 633ea97848
5 changed files with 198 additions and 219 deletions

View File

@@ -6,7 +6,6 @@ import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus import org.jetbrains.exposed.sql.SqlExpressionBuilder.minus
import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus import org.jetbrains.exposed.sql.SqlExpressionBuilder.plus
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
@@ -17,9 +16,8 @@ import suwayomi.tachidesk.graphql.types.CategoryMetaType
import suwayomi.tachidesk.graphql.types.CategoryType import suwayomi.tachidesk.graphql.types.CategoryType
import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.MangaType
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.Category.DEFAULT_CATEGORY_ID import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.util.lang.isEmpty import suwayomi.tachidesk.manga.impl.util.lang.isEmpty
import suwayomi.tachidesk.manga.impl.util.lang.isNotEmpty
import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude import suwayomi.tachidesk.manga.model.dataclass.IncludeOrExclude
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.CategoryMetaTable
@@ -398,28 +396,7 @@ class CategoryMutation {
} }
} }
if (!patch.addToCategories.isNullOrEmpty()) { if (!patch.addToCategories.isNullOrEmpty()) {
val newCategories = CategoryManga.addMangasToCategories(ids, patch.addToCategories)
buildList {
ids.filter { it != DEFAULT_CATEGORY_ID }.forEach { mangaId ->
patch.addToCategories.forEach { categoryId ->
val existingMapping =
CategoryMangaTable
.selectAll()
.where {
(CategoryMangaTable.manga eq mangaId) and (CategoryMangaTable.category eq categoryId)
}.isNotEmpty()
if (!existingMapping) {
add(mangaId to categoryId)
}
}
}
}
CategoryMangaTable.batchInsert(newCategories) { (manga, category) ->
this[CategoryMangaTable.manga] = manga
this[CategoryMangaTable.category] = category
}
} }
} }
} }

View File

@@ -11,9 +11,9 @@ import org.jetbrains.exposed.sql.SortOrder
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
import org.jetbrains.exposed.sql.andWhere import org.jetbrains.exposed.sql.andWhere
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
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
@@ -28,27 +28,36 @@ object Category {
/** /**
* The new category will be placed at the end of the list * The new category will be placed at the end of the list
*/ */
fun createCategory(name: String): Int { fun createCategory(name: String): Int = createCategories(listOf(name)).first()
// creating a category named Default is illegal
if (name.equals(DEFAULT_CATEGORY_NAME, ignoreCase = true)) return -1
return transaction { fun createCategories(names: List<String>): List<Int> =
if (CategoryTable.selectAll().where { CategoryTable.name eq name }.firstOrNull() == null) { transaction {
val newCategoryId = val categoryIdToName = getCategoryList().associate { it.id to it.name.lowercase() }
CategoryTable
.insertAndGetId {
it[CategoryTable.name] = name
it[CategoryTable.order] = Int.MAX_VALUE
}.value
normalizeCategories() val categoriesToCreate =
names
.filter {
!it.equals(DEFAULT_CATEGORY_NAME, true)
}.filter { !categoryIdToName.values.contains(it.lowercase()) }
newCategoryId val newCategoryIdsByName =
} else { CategoryTable
-1 .batchInsert(categoriesToCreate) {
this[CategoryTable.name] = it
this[CategoryTable.order] = Int.MAX_VALUE
}.associate { it[CategoryTable.name] to it[CategoryTable.id].value }
normalizeCategories()
names.map {
// creating a category named Default is illegal
if (it.equals(DEFAULT_CATEGORY_NAME, true)) {
DEFAULT_CATEGORY_ID
} else {
newCategoryIdsByName[it] ?: categoryIdToName.entries.find { (_, name) -> name.equals(it, true) }!!.key
}
} }
} }
}
fun updateCategory( fun updateCategory(
categoryId: Int, categoryId: Int,

View File

@@ -12,16 +12,15 @@ import org.jetbrains.exposed.sql.SortOrder
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.alias import org.jetbrains.exposed.sql.alias
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.count import org.jetbrains.exposed.sql.count
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.leftJoin import org.jetbrains.exposed.sql.leftJoin
import org.jetbrains.exposed.sql.max import org.jetbrains.exposed.sql.max
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.wrapAsExpression import org.jetbrains.exposed.sql.wrapAsExpression
import suwayomi.tachidesk.manga.impl.Category.DEFAULT_CATEGORY_ID import suwayomi.tachidesk.manga.impl.Category.DEFAULT_CATEGORY_ID
import suwayomi.tachidesk.manga.impl.util.lang.isEmpty
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.table.CategoryMangaTable import suwayomi.tachidesk.manga.model.table.CategoryMangaTable
@@ -35,22 +34,38 @@ object CategoryManga {
mangaId: Int, mangaId: Int,
categoryId: Int, categoryId: Int,
) { ) {
if (categoryId == DEFAULT_CATEGORY_ID) return addMangaToCategories(mangaId, listOf(categoryId))
}
fun notAlreadyInCategory() = fun addMangaToCategories(
CategoryMangaTable mangaId: Int,
.selectAll() categoryIds: List<Int>,
.where { ) {
(CategoryMangaTable.category eq categoryId) and (CategoryMangaTable.manga eq mangaId) addMangasToCategories(listOf(mangaId), categoryIds)
}.isEmpty() }
transaction { fun addMangasToCategories(
if (notAlreadyInCategory()) { mangaIds: List<Int>,
CategoryMangaTable.insert { categoryIds: List<Int>,
it[CategoryMangaTable.category] = categoryId ) {
it[CategoryMangaTable.manga] = mangaId val filteredCategoryIds = categoryIds.filter { it != DEFAULT_CATEGORY_ID }
val mangaIdsToCategoryIds = getMangasCategories(mangaIds).mapValues { it.value.map { category -> category.id } }
val mangaIdsToNewCategoryIds =
mangaIds.associateWith { mangaId ->
filteredCategoryIds.filter { categoryId ->
!(mangaIdsToCategoryIds[mangaId]?.contains(categoryId) ?: false)
} }
} }
val newMangaCategoryMappings =
mangaIdsToNewCategoryIds.flatMap { (mangaId, newCategoryIds) ->
newCategoryIds.map { mangaId to it }
}
CategoryMangaTable.batchInsert(newMangaCategoryMappings) { (mangaId, categoryId) ->
this[CategoryMangaTable.manga] = mangaId
this[CategoryMangaTable.category] = categoryId
} }
} }

View File

@@ -21,17 +21,18 @@ import kotlinx.coroutines.sync.withLock
import okio.buffer import okio.buffer
import okio.gzip import okio.gzip
import okio.source import okio.source
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.and import org.jetbrains.exposed.sql.and
import org.jetbrains.exposed.sql.batchInsert import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.insertAndGetId import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
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.graphql.types.toStatus import suwayomi.tachidesk.graphql.types.toStatus
import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.Category
import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.CategoryManga
import suwayomi.tachidesk.manga.impl.Manga.clearThumbnail
import suwayomi.tachidesk.manga.impl.backup.models.Chapter import suwayomi.tachidesk.manga.impl.backup.models.Chapter
import suwayomi.tachidesk.manga.impl.backup.models.Manga import suwayomi.tachidesk.manga.impl.backup.models.Manga
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult
@@ -45,7 +46,6 @@ import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrack
import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrackRecordDataClass import suwayomi.tachidesk.manga.impl.track.tracker.model.toTrackRecordDataClass
import suwayomi.tachidesk.manga.model.dataclass.TrackRecordDataClass import suwayomi.tachidesk.manga.model.dataclass.TrackRecordDataClass
import suwayomi.tachidesk.manga.model.table.CategoryTable
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.server.database.dbTransaction import suwayomi.tachidesk.server.database.dbTransaction
@@ -179,26 +179,8 @@ object ProtoBackupImport : ProtoBackupBase() {
restoreAmount = backup.backupManga.size + 1 // +1 for categories restoreAmount = backup.backupManga.size + 1 // +1 for categories
updateRestoreState(id, BackupRestoreState.RestoringCategories(backup.backupManga.size)) updateRestoreState(id, BackupRestoreState.RestoringCategories(backup.backupManga.size))
// Restore categories
if (backup.backupCategories.isNotEmpty()) {
restoreCategories(backup.backupCategories)
}
val categoryMapping = val categoryMapping = restoreCategories(backup.backupCategories)
transaction {
backup.backupCategories.associate {
val dbCategory =
CategoryTable
.selectAll()
.where { CategoryTable.name eq it.name }
.firstOrNull()
val categoryId =
dbCategory?.let { categoryResultRow ->
categoryResultRow[CategoryTable.id].value
} ?: Category.DEFAULT_CATEGORY_ID
it.order to categoryId
}
}
// Store source mapping for error messages // Store source mapping for error messages
sourceMapping = backup.getSourceMap() sourceMapping = backup.getSourceMap()
@@ -216,7 +198,6 @@ object ProtoBackupImport : ProtoBackupBase() {
restoreManga( restoreManga(
backupManga = manga, backupManga = manga,
backupCategories = backup.backupCategories,
categoryMapping = categoryMapping, categoryMapping = categoryMapping,
) )
} }
@@ -240,20 +221,16 @@ object ProtoBackupImport : ProtoBackupBase() {
return validationResult return validationResult
} }
private fun restoreCategories(backupCategories: List<BackupCategory>) { private fun restoreCategories(backupCategories: List<BackupCategory>): Map<Int, Int> {
val dbCategories = Category.getCategoryList() val categoryIds = Category.createCategories(backupCategories.map { it.name })
// Iterate over them and create missing categories return backupCategories.withIndex().associate { (index, backupCategory) ->
backupCategories.forEach { category -> backupCategory.order to categoryIds[index]
if (dbCategories.none { it.name == category.name }) {
Category.createCategory(category.name)
}
} }
} }
private fun restoreManga( private fun restoreManga(
backupManga: BackupManga, backupManga: BackupManga,
backupCategories: List<BackupCategory>,
categoryMapping: Map<Int, Int>, categoryMapping: Map<Int, Int>,
) { ) {
val manga = backupManga.getMangaImpl() val manga = backupManga.getMangaImpl()
@@ -261,8 +238,10 @@ object ProtoBackupImport : ProtoBackupBase() {
val categories = backupManga.categories val categories = backupManga.categories
val history = backupManga.history val history = backupManga.history
val dbCategoryIds = categories.map { categoryMapping[it]!! }
try { try {
restoreMangaData(manga, chapters, categories, history, backupManga.tracking, backupCategories, categoryMapping) restoreMangaData(manga, chapters, dbCategoryIds, history, backupManga.tracking)
} catch (e: Exception) { } catch (e: Exception) {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString() val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
@@ -273,11 +252,9 @@ object ProtoBackupImport : ProtoBackupBase() {
private fun restoreMangaData( private fun restoreMangaData(
manga: Manga, manga: Manga,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<Int>, categoryIds: List<Int>,
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<BackupTracking>, tracks: List<BackupTracking>,
backupCategories: List<BackupCategory>,
categoryMapping: Map<Int, Int>,
) { ) {
val dbManga = val dbManga =
transaction { transaction {
@@ -286,12 +263,13 @@ object ProtoBackupImport : ProtoBackupBase() {
.where { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) } .where { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }
.firstOrNull() .firstOrNull()
} }
val restoreMode = if (dbManga != null) RestoreMode.EXISTING else RestoreMode.NEW
val mangaId = val mangaId =
if (dbManga == null) { // Manga not in database transaction {
transaction { val mangaId =
// insert manga to database if (dbManga == null) {
val mangaId = // insert manga to database
MangaTable MangaTable
.insertAndGetId { .insertAndGetId {
it[url] = manga.url it[url] = manga.url
@@ -313,47 +291,36 @@ object ProtoBackupImport : ProtoBackupBase() {
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added) it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
}.value }.value
} else {
val dbMangaId = dbManga[MangaTable.id].value
// delete thumbnail in case cached data still exists // Merge manga data
clearThumbnail(mangaId) MangaTable.update({ MangaTable.id eq dbMangaId }) {
it[artist] = manga.artist ?: dbManga[artist]
it[author] = manga.author ?: dbManga[author]
it[description] = manga.description ?: dbManga[description]
it[genre] = manga.genre ?: dbManga[genre]
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url ?: dbManga[thumbnail_url]
it[updateStrategy] = manga.update_strategy.name
// insert chapter data it[initialized] = dbManga[initialized] || manga.description != null
restoreMangaChapterData(mangaId, RestoreMode.NEW, chapters)
// insert categories it[inLibrary] = manga.favorite || dbManga[inLibrary]
restoreMangaCategoryData(mangaId, categories, categoryMapping)
mangaId it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
} }
} else { // Manga in database
transaction {
val mangaId = dbManga[MangaTable.id].value
// Merge manga data dbMangaId
MangaTable.update({ MangaTable.id eq mangaId }) {
it[artist] = manga.artist ?: dbManga[artist]
it[author] = manga.author ?: dbManga[author]
it[description] = manga.description ?: dbManga[description]
it[genre] = manga.genre ?: dbManga[genre]
it[status] = manga.status
it[thumbnail_url] = manga.thumbnail_url ?: dbManga[thumbnail_url]
it[updateStrategy] = manga.update_strategy.name
it[initialized] = dbManga[initialized] || manga.description != null
it[inLibrary] = manga.favorite || dbManga[inLibrary]
it[inLibraryAt] = TimeUnit.MILLISECONDS.toSeconds(manga.date_added)
} }
// merge chapter data // merge chapter data
restoreMangaChapterData(mangaId, RestoreMode.EXISTING, chapters) restoreMangaChapterData(mangaId, restoreMode, chapters)
// merge categories // merge categories
restoreMangaCategoryData(mangaId, categories, categoryMapping) restoreMangaCategoryData(mangaId, categoryIds)
mangaId mangaId
}
} }
restoreMangaTrackerData(mangaId, tracks) restoreMangaTrackerData(mangaId, tracks)
@@ -361,80 +328,72 @@ object ProtoBackupImport : ProtoBackupBase() {
// TODO: insert/merge history // TODO: insert/merge history
} }
private fun getMangaChapterToRestoreInfo(
mangaId: Int,
restoreMode: RestoreMode,
chapters: List<Chapter>,
): Pair<List<Chapter>, List<Pair<Chapter, ResultRow>>> {
val uniqueChapters = chapters.distinctBy { it.url }
if (restoreMode == RestoreMode.NEW) {
return Pair(uniqueChapters, emptyList())
}
val dbChaptersByUrl = ChapterTable.selectAll().where { ChapterTable.manga eq mangaId }.associateBy { it[ChapterTable.url] }
val (chaptersToUpdate, chaptersToInsert) = uniqueChapters.partition { dbChaptersByUrl.contains(it.url) }
val chaptersToUpdateToDbChapter = chaptersToUpdate.map { it to dbChaptersByUrl[it.url]!! }
return chaptersToInsert to chaptersToUpdateToDbChapter
}
private fun restoreMangaChapterData( private fun restoreMangaChapterData(
mangaId: Int, mangaId: Int,
restoreMode: RestoreMode, restoreMode: RestoreMode,
chapters: List<Chapter>, chapters: List<Chapter>,
) = dbTransaction { ) = dbTransaction {
val uniqueChapters = chapters.distinctBy { it.url } val (chaptersToInsert, chaptersToUpdateToDbChapter) = getMangaChapterToRestoreInfo(mangaId, restoreMode, chapters)
val chaptersLength = uniqueChapters.size
if (restoreMode == RestoreMode.NEW) { ChapterTable.batchInsert(chaptersToInsert) { chapter ->
ChapterTable.batchInsert(uniqueChapters) { chapter -> this[ChapterTable.url] = chapter.url
this[ChapterTable.url] = chapter.url this[ChapterTable.name] = chapter.name
this[ChapterTable.name] = chapter.name if (chapter.date_upload == 0L) {
if (chapter.date_upload == 0L) { this[ChapterTable.date_upload] = chapter.date_fetch
this[ChapterTable.date_upload] = chapter.date_fetch } else {
} else { this[ChapterTable.date_upload] = chapter.date_upload
this[ChapterTable.date_upload] = chapter.date_upload
}
this[ChapterTable.chapter_number] = chapter.chapter_number
this[ChapterTable.scanlator] = chapter.scanlator
this[ChapterTable.sourceOrder] = chaptersLength - chapter.source_order
this[ChapterTable.manga] = mangaId
this[ChapterTable.isRead] = chapter.read
this[ChapterTable.lastPageRead] = chapter.last_page_read.coerceAtLeast(0)
this[ChapterTable.isBookmarked] = chapter.bookmark
this[ChapterTable.fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch)
} }
this[ChapterTable.chapter_number] = chapter.chapter_number
this[ChapterTable.scanlator] = chapter.scanlator
this[ChapterTable.sourceOrder] = chaptersToInsert.size - chapter.source_order
this[ChapterTable.manga] = mangaId
this[ChapterTable.isRead] = chapter.read
this[ChapterTable.lastPageRead] = chapter.last_page_read.coerceAtLeast(0)
this[ChapterTable.isBookmarked] = chapter.bookmark
this[ChapterTable.fetchedAt] = TimeUnit.MILLISECONDS.toSeconds(chapter.date_fetch)
} }
// merge chapter data if (chaptersToUpdateToDbChapter.isNotEmpty()) {
val dbChapters = ChapterTable.selectAll().where { ChapterTable.manga eq mangaId } BatchUpdateStatement(ChapterTable).apply {
chaptersToUpdateToDbChapter.forEach { (backupChapter, dbChapter) ->
uniqueChapters.forEach { chapter -> addBatch(EntityID(dbChapter[ChapterTable.id].value, ChapterTable))
val dbChapter = dbChapters.find { it[ChapterTable.url] == chapter.url } this[ChapterTable.isRead] = backupChapter.read || dbChapter[ChapterTable.isRead]
this[ChapterTable.lastPageRead] =
if (dbChapter == null) { max(backupChapter.last_page_read, dbChapter[ChapterTable.lastPageRead]).coerceAtLeast(0)
ChapterTable.insert { this[ChapterTable.isBookmarked] = backupChapter.bookmark || dbChapter[ChapterTable.isBookmarked]
it[url] = chapter.url
it[name] = chapter.name
if (chapter.date_upload == 0L) {
it[date_upload] = chapter.date_fetch
} else {
it[date_upload] = chapter.date_upload
}
it[chapter_number] = chapter.chapter_number
it[scanlator] = chapter.scanlator
it[sourceOrder] = chaptersLength - chapter.source_order
it[ChapterTable.manga] = mangaId
it[isRead] = chapter.read
it[lastPageRead] = chapter.last_page_read.coerceAtLeast(0)
it[isBookmarked] = chapter.bookmark
}
} else {
ChapterTable.update({ (ChapterTable.url eq dbChapter[ChapterTable.url]) and (ChapterTable.manga eq mangaId) }) {
it[isRead] = chapter.read || dbChapter[isRead]
it[lastPageRead] = max(chapter.last_page_read, dbChapter[lastPageRead]).coerceAtLeast(0)
it[isBookmarked] = chapter.bookmark || dbChapter[isBookmarked]
} }
execute(this@dbTransaction)
} }
} }
} }
private fun restoreMangaCategoryData( private fun restoreMangaCategoryData(
mangaId: Int, mangaId: Int,
categories: List<Int>, categoryIds: List<Int>,
categoryMapping: Map<Int, Int>,
) { ) {
categories.forEach { backupCategoryOrder -> CategoryManga.addMangaToCategories(mangaId, categoryIds)
CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!)
}
} }
private fun restoreMangaTrackerData( private fun restoreMangaTrackerData(
@@ -473,8 +432,8 @@ object ProtoBackupImport : ProtoBackupBase() {
} }
}.partition { (it.id ?: -1) > 0 } }.partition { (it.id ?: -1) > 0 }
existingTracks.forEach(Tracker::updateTrackRecord) Tracker.updateTrackRecords(existingTracks)
newTracks.forEach(Tracker::insertTrackRecord) Tracker.insertTrackRecords(newTracks)
} }
private fun TrackRecordDataClass.forComparison() = this.copy(id = 0, mangaId = 0) private fun TrackRecordDataClass.forComparison() = this.copy(id = 0, mangaId = 0)

View File

@@ -6,13 +6,15 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.sql.ResultRow import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SortOrder import org.jetbrains.exposed.sql.SortOrder
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
import org.jetbrains.exposed.sql.batchInsert
import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.deleteWhere
import org.jetbrains.exposed.sql.insertAndGetId
import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
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.track.tracker.DeletableTrackService import suwayomi.tachidesk.manga.impl.track.tracker.DeletableTrackService
@@ -25,6 +27,18 @@ import suwayomi.tachidesk.manga.model.dataclass.TrackSearchDataClass
import suwayomi.tachidesk.manga.model.dataclass.TrackerDataClass import suwayomi.tachidesk.manga.model.dataclass.TrackerDataClass
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.TrackRecordTable import suwayomi.tachidesk.manga.model.table.TrackRecordTable
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.finishDate
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.lastChapterRead
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.libraryId
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.mangaId
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.remoteId
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.remoteUrl
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.score
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.startDate
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.status
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.title
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.totalChapters
import suwayomi.tachidesk.manga.model.table.TrackRecordTable.trackerId
import suwayomi.tachidesk.manga.model.table.TrackSearchTable import suwayomi.tachidesk.manga.model.table.TrackSearchTable
import suwayomi.tachidesk.manga.model.table.insertAll import suwayomi.tachidesk.manga.model.table.insertAll
import suwayomi.tachidesk.server.generated.BuildConfig import suwayomi.tachidesk.server.generated.BuildConfig
@@ -394,44 +408,49 @@ object Track {
} }
} }
fun updateTrackRecord(track: Track): Int = fun updateTrackRecord(track: Track) = updateTrackRecords(listOf(track))
fun updateTrackRecords(tracks: List<Track>) =
transaction { transaction {
TrackRecordTable.update( if (tracks.isNotEmpty()) {
{ BatchUpdateStatement(TrackRecordTable).apply {
(TrackRecordTable.mangaId eq track.manga_id) and tracks.forEach {
(TrackRecordTable.trackerId eq track.sync_id) addBatch(EntityID(it.id!!, TrackRecordTable))
}, this[remoteId] = it.media_id
) { this[libraryId] = it.library_id
it[remoteId] = track.media_id this[title] = it.title
it[libraryId] = track.library_id this[lastChapterRead] = it.last_chapter_read.toDouble()
it[title] = track.title this[totalChapters] = it.total_chapters
it[lastChapterRead] = track.last_chapter_read.toDouble() this[status] = it.status
it[totalChapters] = track.total_chapters this[score] = it.score.toDouble()
it[status] = track.status this[remoteUrl] = it.tracking_url
it[score] = track.score.toDouble() this[startDate] = it.started_reading_date
it[remoteUrl] = track.tracking_url this[finishDate] = it.finished_reading_date
it[startDate] = track.started_reading_date }
it[finishDate] = track.finished_reading_date execute(this@transaction)
}
} }
} }
fun insertTrackRecord(track: Track): Int = fun insertTrackRecord(track: Track): Int = insertTrackRecords(listOf(track)).first()
fun insertTrackRecords(tracks: List<Track>): List<Int> =
transaction { transaction {
TrackRecordTable TrackRecordTable
.insertAndGetId { .batchInsert(tracks) {
it[mangaId] = track.manga_id this[mangaId] = it.manga_id
it[trackerId] = track.sync_id this[trackerId] = it.sync_id
it[remoteId] = track.media_id this[remoteId] = it.media_id
it[libraryId] = track.library_id this[libraryId] = it.library_id
it[title] = track.title this[title] = it.title
it[lastChapterRead] = track.last_chapter_read.toDouble() this[lastChapterRead] = it.last_chapter_read.toDouble()
it[totalChapters] = track.total_chapters this[totalChapters] = it.total_chapters
it[status] = track.status this[status] = it.status
it[score] = track.score.toDouble() this[score] = it.score.toDouble()
it[remoteUrl] = track.tracking_url this[remoteUrl] = it.tracking_url
it[startDate] = track.started_reading_date this[startDate] = it.started_reading_date
it[finishDate] = track.finished_reading_date this[finishDate] = it.finished_reading_date
}.value }.map { it[TrackRecordTable.id].value }
} }
@Serializable @Serializable