mirror of
https://github.com/tachiyomiorg/tachiyomi-extensions-inspector.git
synced 2025-03-01 06:45:32 +01:00
fist version of a working backup system
This commit is contained in:
parent
cd16d32a35
commit
0c79f207c3
@ -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.ChapterImpl
|
||||||
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
import ir.armor.tachidesk.impl.backup.models.MangaImpl
|
||||||
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
import ir.armor.tachidesk.impl.backup.models.TrackImpl
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
open class LegacyBackupBase {
|
open class LegacyBackupBase {
|
||||||
internal val parser: Gson = when (version) {
|
protected val parser: Gson = when (version) {
|
||||||
2 -> GsonBuilder()
|
2 -> GsonBuilder()
|
||||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||||
@ -34,6 +35,10 @@ open class LegacyBackupBase {
|
|||||||
else -> throw Exception("Unknown backup version")
|
else -> throw Exception("Unknown backup version")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected var sourceMapping: Map<Long, String> = emptyMap()
|
||||||
|
|
||||||
|
protected val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
internal const val version = 2
|
internal const val version = 2
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,33 @@
|
|||||||
package ir.armor.tachidesk.impl.backup.legacy
|
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 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 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.io.InputStream
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
@ -14,10 +39,163 @@ import java.io.InputStream
|
|||||||
private val logger = KotlinLogging.logger {}
|
private val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
object LegacyBackupImport : LegacyBackupBase() {
|
object LegacyBackupImport : LegacyBackupBase() {
|
||||||
fun restoreLegacyBackup(sourceStream: InputStream) {
|
suspend fun restoreLegacyBackup(sourceStream: InputStream): ValidationResult {
|
||||||
val reader = sourceStream.bufferedReader()
|
val reader = sourceStream.bufferedReader()
|
||||||
val json = JsonParser.parseReader(reader).asJsonObject
|
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<MangaImpl>(
|
||||||
|
mangaJson.get(
|
||||||
|
Backup.MANGA
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val chapters = parser.fromJson<List<ChapterImpl>>(
|
||||||
|
mangaJson.get(Backup.CHAPTERS)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val categories = parser.fromJson<List<String>>(
|
||||||
|
mangaJson.get(Backup.CATEGORIES)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val history = parser.fromJson<List<DHistory>>(
|
||||||
|
mangaJson.get(Backup.HISTORY)
|
||||||
|
?: JsonArray()
|
||||||
|
)
|
||||||
|
val tracks = parser.fromJson<List<TrackImpl>>(
|
||||||
|
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<Chapter>,
|
||||||
|
categories: List<String>,
|
||||||
|
history: List<DHistory>,
|
||||||
|
tracks: List<Track>
|
||||||
|
) {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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<String>, val missingTrackers: List<String>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<Long, String> {
|
||||||
|
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||||
|
|
||||||
|
return extensionsMapping.asJsonArray
|
||||||
|
.map {
|
||||||
|
val items = it.asString.split(":")
|
||||||
|
items[0].toLong() to items[1]
|
||||||
|
}
|
||||||
|
.toMap()
|
||||||
|
}
|
||||||
|
}
|
@ -327,8 +327,12 @@ object JavalinSetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// expects a Tachiyomi legacy backup json to be uploaded
|
// expects a Tachiyomi legacy backup json to be uploaded
|
||||||
app.get("/api/v1/backup/legacy/import") { ctx ->
|
app.post("/api/v1/backup/legacy/import") { ctx ->
|
||||||
restoreLegacyBackup(ctx.bodyAsInputStream())
|
ctx.result(
|
||||||
|
future {
|
||||||
|
restoreLegacyBackup(ctx.bodyAsInputStream())
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// returns a Tachiyomi legacy backup json created from the current database
|
// returns a Tachiyomi legacy backup json created from the current database
|
||||||
|
Loading…
x
Reference in New Issue
Block a user