mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-11 15:22:05 +01:00
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:
@@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user