diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupBase.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupBase.kt index 02fcbe4..205ff63 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupBase.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupBase.kt @@ -21,9 +21,10 @@ import ir.armor.tachidesk.impl.backup.models.CategoryImpl import ir.armor.tachidesk.impl.backup.models.ChapterImpl import ir.armor.tachidesk.impl.backup.models.MangaImpl import ir.armor.tachidesk.impl.backup.models.TrackImpl +import java.util.Date open class LegacyBackupBase { - internal val parser: Gson = when (version) { + protected val parser: Gson = when (version) { 2 -> GsonBuilder() .registerTypeAdapter(MangaTypeAdapter.build()) .registerTypeHierarchyAdapter(ChapterTypeAdapter.build()) @@ -34,6 +35,10 @@ open class LegacyBackupBase { else -> throw Exception("Unknown backup version") } + protected var sourceMapping: Map = emptyMap() + + protected val errors = mutableListOf>() + companion object { internal const val version = 2 } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt index ecdb2ec..05e1fed 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupImport.kt @@ -1,8 +1,33 @@ package ir.armor.tachidesk.impl.backup.legacy +import com.github.salomonbrys.kotson.fromJson +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject import com.google.gson.JsonParser +import eu.kanade.tachiyomi.source.Source +import eu.kanade.tachiyomi.source.model.SManga +import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupRestoreValidator.ValidationResult +import ir.armor.tachidesk.impl.backup.legacy.LegacyBackupRestoreValidator.validate +import ir.armor.tachidesk.impl.backup.legacy.models.Backup +import ir.armor.tachidesk.impl.backup.legacy.models.DHistory +import ir.armor.tachidesk.impl.backup.models.Chapter +import ir.armor.tachidesk.impl.backup.models.ChapterImpl +import ir.armor.tachidesk.impl.backup.models.Manga +import ir.armor.tachidesk.impl.backup.models.MangaImpl +import ir.armor.tachidesk.impl.backup.models.Track +import ir.armor.tachidesk.impl.backup.models.TrackImpl +import ir.armor.tachidesk.impl.util.GetHttpSource.getHttpSource +import ir.armor.tachidesk.impl.util.awaitSingle +import ir.armor.tachidesk.model.database.MangaTable import mu.KotlinLogging +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update import java.io.InputStream +import java.util.Date /* * Copyright (C) Contributors to the Suwayomi project @@ -14,10 +39,163 @@ import java.io.InputStream private val logger = KotlinLogging.logger {} object LegacyBackupImport : LegacyBackupBase() { - fun restoreLegacyBackup(sourceStream: InputStream) { + suspend fun restoreLegacyBackup(sourceStream: InputStream): ValidationResult { val reader = sourceStream.bufferedReader() val json = JsonParser.parseReader(reader).asJsonObject - logger.info("$json") + val validationResult = validate(json) + + val mangasJson = json.get(Backup.MANGAS).asJsonArray + + // Restore categories + json.get(Backup.CATEGORIES)?.let { restoreCategories(it) } + + // Store source mapping for error messages + sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json) + + // Restore individual manga + mangasJson.forEach { + restoreManga(it.asJsonObject) + } + + logger.info { + """ + Restore Errors: + ${ + errors.map { + "${it.first} - ${it.second}" + }.joinToString("\n") + } + Restore Summary: + - Missing Sources: + ${validationResult.missingSources.joinToString("\n")} + - Missing Trackers: + ${validationResult.missingTrackers.joinToString("\n")} + """.trimIndent() + } + + return validationResult + } + + private fun restoreCategories(categoriesJson: JsonElement) { // TODO +// db.inTransaction { +// backupManager.restoreCategories(categoriesJson.asJsonArray) +// } +// +// restoreProgress += 1 +// showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories)) + } + + private suspend fun restoreManga(mangaJson: JsonObject) { + val manga = parser.fromJson( + mangaJson.get( + Backup.MANGA + ) + ) + val chapters = parser.fromJson>( + mangaJson.get(Backup.CHAPTERS) + ?: JsonArray() + ) + val categories = parser.fromJson>( + mangaJson.get(Backup.CATEGORIES) + ?: JsonArray() + ) + val history = parser.fromJson>( + mangaJson.get(Backup.HISTORY) + ?: JsonArray() + ) + val tracks = parser.fromJson>( + mangaJson.get(Backup.TRACK) + ?: JsonArray() + ) + + val source = try { + getHttpSource(manga.source) + } catch (e: NullPointerException) { + null + } + val sourceName = sourceMapping[manga.source] ?: manga.source.toString() + + logger.debug("Restoring Manga: ${manga.title} from $sourceName") + + try { + if (source != null) { + restoreMangaData(manga, source, chapters, categories, history, tracks) + } else { + errors.add(Date() to "${manga.title} [$sourceName]: Source not found: $sourceName (${manga.source})") + } + } catch (e: Exception) { + errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") + } + } + + /** + * @param manga manga data from json + * @param source source to get manga data from + * @param chapters chapters data from json + * @param categories categories data from json + * @param history history data from json + * @param tracks tracking data from json + */ + private suspend fun restoreMangaData( + manga: Manga, + source: Source, + chapters: List, + categories: List, + history: List, + tracks: List + ) { + fetchManga(source, manga) + +// updateChapters(source, fetchedManga, chapters) + +// backupManager.restoreCategoriesForManga(manga, categories) + +// backupManager.restoreHistoryForManga(history) + +// backupManager.restoreTrackForManga(manga, tracks) + +// updateTracking(fetchedManga, tracks) + } + + /** + * Fetches manga information + * + * @param source source of manga + * @param manga manga that needs updating + * @return Updated manga. + */ + private suspend fun fetchManga(source: Source, manga: Manga): SManga { + transaction { + if (MangaTable.select { (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }.firstOrNull() == null) { + MangaTable.insert { + it[url] = manga.url + it[title] = manga.title + + it[sourceReference] = manga.source + } + } + MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) { + it[MangaTable.inLibrary] = true + } + } + + val fetchedManga = source.fetchMangaDetails(manga).awaitSingle() + + transaction { + MangaTable.update({ (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq manga.source) }) { + + it[artist] = fetchedManga.artist + it[author] = fetchedManga.author + it[description] = fetchedManga.description + it[genre] = fetchedManga.genre + it[status] = fetchedManga.status + if (fetchedManga.thumbnail_url != null && fetchedManga.thumbnail_url!!.isNotEmpty()) + it[MangaTable.thumbnail_url] = fetchedManga.thumbnail_url + + } + } + + return fetchedManga } } diff --git a/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupValidator.kt b/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupValidator.kt new file mode 100644 index 0000000..e6c61ea --- /dev/null +++ b/server/src/main/kotlin/ir/armor/tachidesk/impl/backup/legacy/LegacyBackupValidator.kt @@ -0,0 +1,71 @@ +package ir.armor.tachidesk.impl.backup.legacy + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import com.google.gson.JsonObject +import ir.armor.tachidesk.impl.backup.legacy.models.Backup +import ir.armor.tachidesk.model.database.SourceTable +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction + +object LegacyBackupRestoreValidator { + data class ValidationResult(val missingSources: List, val missingTrackers: List) + + /** + * Checks for critical backup file data. + * + * @throws Exception if version or manga cannot be found. + * @return List of missing sources or missing trackers. + */ + fun validate(json: JsonObject): ValidationResult { + val version = json.get(Backup.VERSION) + val mangasJson = json.get(Backup.MANGAS) + if (version == null || mangasJson == null) { + throw Exception("File is missing data.") + } + + val mangas = mangasJson.asJsonArray + if (mangas.size() == 0) { + throw Exception("Backup does not contain any manga.") + } + + val sources = getSourceMapping(json) + val missingSources = transaction { + sources + .filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null } + .map { "${it.value} (${it.key})" } + .sorted() + } + + val trackers = mangas + .filter { it.asJsonObject.has("track") } + .flatMap { it.asJsonObject["track"].asJsonArray } + .map { it.asJsonObject["s"].asInt } + .distinct() + + val missingTrackers = listOf("") +// val missingTrackers = trackers +// .mapNotNull { trackManager.getService(it) } +// .filter { !it.isLogged } +// .map { context.getString(it.nameRes()) } +// .sorted() + + return ValidationResult(missingSources, missingTrackers) + } + + fun getSourceMapping(json: JsonObject): Map { + val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap() + + return extensionsMapping.asJsonArray + .map { + val items = it.asString.split(":") + items[0].toLong() to items[1] + } + .toMap() + } +} diff --git a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt index 25cf25b..7f034de 100644 --- a/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/ir/armor/tachidesk/server/JavalinSetup.kt @@ -327,8 +327,12 @@ object JavalinSetup { } // expects a Tachiyomi legacy backup json to be uploaded - app.get("/api/v1/backup/legacy/import") { ctx -> - restoreLegacyBackup(ctx.bodyAsInputStream()) + app.post("/api/v1/backup/legacy/import") { ctx -> + ctx.result( + future { + restoreLegacyBackup(ctx.bodyAsInputStream()) + } + ) } // returns a Tachiyomi legacy backup json created from the current database