diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt index c5653818..4df003ea 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt @@ -46,12 +46,12 @@ object Chapter { } private suspend fun getSourceChapters(mangaId: Int): List { - val mangaDetails = getManga(mangaId) - val source = getHttpSource(mangaDetails.sourceId.toLong()) + val manga = getManga(mangaId) + val source = getHttpSource(manga.sourceId.toLong()) val chapterList = source.fetchChapterList( SManga.create().apply { - title = mangaDetails.title - url = mangaDetails.url + title = manga.title + url = manga.url } ).awaitSingle() @@ -69,7 +69,7 @@ object Chapter { it[scanlator] = fetchedChapter.scanlator it[chapterIndex] = index + 1 - it[manga] = mangaId + it[ChapterTable.manga] = mangaId } } else { ChapterTable.update({ ChapterTable.url eq fetchedChapter.url }) { @@ -79,7 +79,7 @@ object Chapter { it[scanlator] = fetchedChapter.scanlator it[chapterIndex] = index + 1 - it[manga] = mangaId + it[ChapterTable.manga] = mangaId } } } 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 8f901da1..19843b2f 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 @@ -11,20 +11,36 @@ import mu.KotlinLogging import okio.buffer import okio.gzip import okio.source +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.insertAndGetId +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.manga.impl.Category +import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator.ValidationResult +import suwayomi.tachidesk.manga.impl.backup.models.Chapter +import suwayomi.tachidesk.manga.impl.backup.models.Manga +import suwayomi.tachidesk.manga.impl.backup.models.Track import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.validate import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory +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.BackupSerializer +import suwayomi.tachidesk.manga.model.table.CategoryTable +import suwayomi.tachidesk.manga.model.table.ChapterTable +import suwayomi.tachidesk.manga.model.table.MangaTable import java.io.InputStream +import java.util.Date private val logger = KotlinLogging.logger {} object ProtoBackupImport : ProtoBackupBase() { - var restoreAmount = 0 + private var restoreAmount = 0 + + private val errors = mutableListOf>() suspend fun performRestore(sourceStream: InputStream): ValidationResult { - val backupString = sourceStream.source().gzip().buffer().use { it.readByteArray() } val backup = parser.decodeFromByteArray(BackupSerializer, backupString) @@ -37,43 +53,124 @@ object ProtoBackupImport : ProtoBackupBase() { restoreCategories(backup.backupCategories) } + val categoryMapping = transaction { + backup.backupCategories.associate { + it.order to CategoryTable.select { CategoryTable.name eq it.name }.first()[CategoryTable.id].value + } + } + // Store source mapping for error messages sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap() // Restore individual manga backup.backupManga.forEach { - restoreManga(it, backup.backupCategories) + restoreManga(it, backup.backupCategories, categoryMapping) } - // TODO: optionally trigger online library + tracker update + logger.info { + """ + Restore Errors: + ${ errors.joinToString("\n") { "${it.first} - ${it.second}" } } + Restore Summary: + - Missing Sources: + ${validationResult.missingSources.joinToString("\n ")} + - Missing Trackers: +${validationResult.missingTrackers.joinToString("\n")} + """.trimIndent() + } return validationResult } - private fun restoreCategories(backupCategories: List) { // TODO -// db.inTransaction { -// backupManager.restoreCategories(backupCategories) -// } -// -// restoreProgress += 1 -// showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories)) + private fun restoreCategories(backupCategories: List) { + val dbCategories = Category.getCategoryList() + + // Iterate over them and create missing categories + backupCategories.forEach { category -> + if (dbCategories.none { it.name == category.name }) { + Category.createCategory(category.name) + } + } } - private fun restoreManga(backupManga: BackupManga, backupCategories: List) { // TODO -// val manga = backupManga.getMangaImpl() -// val chapters = backupManga.getChaptersImpl() -// val categories = backupManga.categories -// val history = backupManga.history -// val tracks = backupManga.getTrackingImpl() -// -// try { -// restoreMangaData(manga, chapters, categories, history, tracks, backupCategories) -// } catch (e: Exception) { -// val sourceName = sourceMapping[manga.source] ?: manga.source.toString() -// errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") -// } -// -// restoreProgress += 1 -// showRestoreProgress(restoreProgress, restoreAmount, manga.title) + private fun restoreManga( + backupManga: BackupManga, + backupCategories: List, + categoryMapping: Map + ) { // TODO + val manga = backupManga.getMangaImpl() + val chapters = backupManga.getChaptersImpl() + val categories = backupManga.categories + val history = backupManga.history + val tracks = backupManga.getTrackingImpl() + + try { + restoreMangaData(manga, chapters, categories, history, tracks, backupCategories, categoryMapping) + } catch (e: Exception) { + val sourceName = sourceMapping[manga.source] ?: manga.source.toString() + errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") + } + } + + private fun restoreMangaData( + manga: Manga, + chapters: List, + categories: List, + history: List, + tracks: List, + backupCategories: List, + categoryMapping: Map + ) { + val dbManga = transaction { + MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) } + .firstOrNull() + } + if (dbManga == null) { // Manga not in database + transaction { + // insert manga to database + val mangaId = MangaTable.insertAndGetId { + it[url] = manga.url + it[title] = manga.title + + it[artist] = manga.artist + it[author] = manga.author + it[description] = manga.description + it[genre] = manga.genre + it[status] = manga.status + it[thumbnail_url] = manga.thumbnail_url + + it[sourceReference] = manga.source + + it[initialized] = manga.description != null + }.value + + // insert chapter data + chapters.forEach { chapter -> + ChapterTable.insert { + it[url] = chapter.url + it[name] = chapter.name + it[date_upload] = chapter.date_upload + it[chapter_number] = chapter.chapter_number + it[scanlator] = chapter.scanlator + + it[chapterIndex] = chapter.source_order + it[ChapterTable.manga] = mangaId + } + } + + // insert categories + categories.forEach { backupCategoryOrder -> + CategoryManga.addMangaToCategory(mangaId, categoryMapping[backupCategoryOrder]!!) + } + } + } else { // Manga in database + // merge chapter data + + // merge categories + } + + // TODO: insert/merge history + + // TODO: insert/merge tracking } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/GetHttpSource.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/GetHttpSource.kt index e2a98d9c..d3fe4e27 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/GetHttpSource.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/util/GetHttpSource.kt @@ -32,7 +32,7 @@ object GetHttpSource { } val sourceRecord = transaction { - SourceTable.select { SourceTable.id eq sourceId }.first() + SourceTable.select { SourceTable.id eq sourceId }.firstOrNull()!! } val extensionId = sourceRecord[SourceTable.extension]