diff --git a/server/build.gradle.kts b/server/build.gradle.kts index 15bad474..6a53416a 100644 --- a/server/build.gradle.kts +++ b/server/build.gradle.kts @@ -7,6 +7,7 @@ import java.time.Instant plugins { application + kotlin("plugin.serialization") id("com.github.johnrengelman.shadow") version "7.0.0" id("org.jmailen.kotlinter") version "3.4.3" id("com.github.gmazzo.buildconfig") version "3.0.2" @@ -143,7 +144,8 @@ tasks { freeCompilerArgs = listOf( "-Xopt-in=kotlin.RequiresOptIn", "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", - "-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi" + "-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi", + "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi", ) } } diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt index 63911e10..4c0f1ce4 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/source/model/SManga.kt @@ -61,3 +61,31 @@ interface SManga : Serializable { } } } + +//fun SManga.toMangaInfo(): MangaInfo { +// return MangaInfo( +// key = this.url, +// title = this.title, +// artist = this.artist ?: "", +// author = this.author ?: "", +// description = this.description ?: "", +// genres = this.genre?.split(", ") ?: emptyList(), +// status = this.status, +// cover = this.thumbnail_url ?: "" +// ) +//} +// +//fun MangaInfo.toSManga(): SManga { +// val mangaInfo = this +// return SManga.create().apply { +// url = mangaInfo.key +// title = mangaInfo.title +// artist = mangaInfo.artist +// author = mangaInfo.author +// description = mangaInfo.description +// genre = mangaInfo.genres.joinToString(", ") +// status = mangaInfo.status +// thumbnail_url = mangaInfo.cover +// } +//} + diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt index 6c836749..1ef6e2a4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt @@ -86,11 +86,19 @@ object MangaAPI { } path("backup") { - post("legacy/import", BackupController::legacyImport) - post("legacy/import/file", BackupController::legacyImportFile) + path("legacy") { // legacy json + post("import", BackupController::legacyImport) + post("import/file", BackupController::legacyImportFile) - get("legacy/export", BackupController::legacyExport) - get("legacy/export/file", BackupController::legacyExportFile) + get("export", BackupController::legacyExport) + get("export/file", BackupController::legacyExportFile) + } + + post("import", BackupController::protobufImport) + post("import/file", BackupController::protobufImportFile) + + get("export", BackupController::protobufExport) + get("export/file", BackupController::protobufExportFile) } path("downloads") { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt index 84d1dde8..003c568b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt @@ -4,6 +4,8 @@ import io.javalin.http.Context import suwayomi.tachidesk.manga.impl.backup.BackupFlags import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupExport import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupImport +import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport +import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.server.JavalinSetup import java.text.SimpleDateFormat import java.util.Date @@ -20,7 +22,7 @@ object BackupController { fun legacyImport(ctx: Context) { ctx.result( JavalinSetup.future { - LegacyBackupImport.restoreLegacyBackup(ctx.bodyAsInputStream()) + LegacyBackupImport.performRestore(ctx.bodyAsInputStream()) } ) } @@ -29,7 +31,7 @@ object BackupController { fun legacyImportFile(ctx: Context) { ctx.result( JavalinSetup.future { - LegacyBackupImport.restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content) + LegacyBackupImport.performRestore(ctx.uploadedFile("backup.json")!!.content) } ) } @@ -39,7 +41,7 @@ object BackupController { ctx.contentType("application/json") ctx.result( JavalinSetup.future { - LegacyBackupExport.createLegacyBackup( + LegacyBackupExport.createBackup( BackupFlags( includeManga = true, includeCategories = true, @@ -55,13 +57,69 @@ object BackupController { /** returns a Tachiyomi legacy backup json created from the current database as a file */ fun legacyExportFile(ctx: Context) { ctx.contentType("application/json") - val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm") - val currentDate = sdf.format(Date()) + val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date()) - ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"") + ctx.header("Content-Disposition", """attachment; filename="tachidesk_$currentDate.json"""") ctx.result( JavalinSetup.future { - LegacyBackupExport.createLegacyBackup( + LegacyBackupExport.createBackup( + BackupFlags( + includeManga = true, + includeCategories = true, + includeChapters = true, + includeTracking = true, + includeHistory = true, + ) + ) + } + ) + } + + /** expects a Tachiyomi protobuf backup in the body */ + fun protobufImport(ctx: Context) { + ctx.result( + JavalinSetup.future { + ProtoBackupImport.performRestore(ctx.bodyAsInputStream()) + } + ) + } + + /** expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */ + fun protobufImportFile(ctx: Context) { + ctx.result( + JavalinSetup.future { + ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content) + } + ) + } + + /** returns a Tachiyomi protobuf backup created from the current database as a body */ + fun protobufExport(ctx: Context) { // TODO + ctx.contentType("application/octet-stream") + ctx.result( + JavalinSetup.future { + ProtoBackupExport.createBackup( + BackupFlags( + includeManga = true, + includeCategories = true, + includeChapters = true, + includeTracking = true, + includeHistory = true, + ) + ) + } + ) + } + + /** returns a Tachiyomi protobuf backup created from the current database as a file */ + fun protobufExportFile(ctx: Context) { + ctx.contentType("application/octet-stream") + val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date()) + + ctx.header("Content-Disposition", """attachment; filename="tachidesk_$currentDate.proto.gz"""") + ctx.result( + JavalinSetup.future { + ProtoBackupExport.createBackup( BackupFlags( includeManga = true, includeCategories = true, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/AbstractBackupValidator.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/AbstractBackupValidator.kt new file mode 100644 index 00000000..a207de49 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/AbstractBackupValidator.kt @@ -0,0 +1,12 @@ +package suwayomi.tachidesk.manga.impl.backup + +/* + * 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/. */ + +abstract class AbstractBackupValidator { + data class ValidationResult(val missingSources: List, val missingTrackers: List) +} \ No newline at end of file diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupExport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupExport.kt index 454a40ab..cbcbd323 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupExport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupExport.kt @@ -29,7 +29,7 @@ import suwayomi.tachidesk.manga.model.table.MangaTable object LegacyBackupExport : LegacyBackupBase() { - suspend fun createLegacyBackup(flags: BackupFlags): String? { + suspend fun createBackup(flags: BackupFlags): ByteArray { // Create root object val root = JsonObject() @@ -77,7 +77,7 @@ object LegacyBackupExport : LegacyBackupBase() { backupExtensionInfo(extensionEntries, extensions) } - return parser.toJson(root) + return parser.toJson(root).encodeToByteArray() } private fun backupMangaObject(manga: Manga, options: BackupFlags): JsonElement { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupImport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupImport.kt index aaf90173..7f5d01c5 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupImport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupImport.kt @@ -1,5 +1,12 @@ package suwayomi.tachidesk.manga.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.github.salomonbrys.kotson.fromJson import com.google.gson.JsonArray import com.google.gson.JsonElement @@ -15,7 +22,7 @@ import org.jetbrains.exposed.sql.transactions.transaction import org.jetbrains.exposed.sql.update import suwayomi.tachidesk.manga.impl.Category.createCategory import suwayomi.tachidesk.manga.impl.Category.getCategoryList -import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupValidator.ValidationResult +import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator.ValidationResult import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupValidator.validate import suwayomi.tachidesk.manga.impl.backup.legacy.models.Backup import suwayomi.tachidesk.manga.impl.backup.legacy.models.DHistory @@ -32,17 +39,10 @@ import suwayomi.tachidesk.manga.model.table.MangaTable import java.io.InputStream import java.util.Date -/* - * 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/. */ - private val logger = KotlinLogging.logger {} object LegacyBackupImport : LegacyBackupBase() { - suspend fun restoreLegacyBackup(sourceStream: InputStream): ValidationResult { + suspend fun performRestore(sourceStream: InputStream): ValidationResult { val reader = sourceStream.bufferedReader() val json = JsonParser.parseReader(reader).asJsonObject diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupValidator.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupValidator.kt index 889c90fb..f57ba9ff 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupValidator.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/LegacyBackupValidator.kt @@ -10,11 +10,11 @@ package suwayomi.tachidesk.manga.impl.backup.legacy import com.google.gson.JsonObject import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator import suwayomi.tachidesk.manga.impl.backup.legacy.models.Backup import suwayomi.tachidesk.manga.model.table.SourceTable -object LegacyBackupValidator { - data class ValidationResult(val missingSources: List, val missingTrackers: List) +object LegacyBackupValidator: AbstractBackupValidator() { /** * Checks for critical backup file data. diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/serializer/MangaTypeAdapter.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/serializer/MangaTypeAdapter.kt index dee97338..c54b7621 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/serializer/MangaTypeAdapter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/legacy/serializer/MangaTypeAdapter.kt @@ -16,7 +16,7 @@ object MangaTypeAdapter { value(it.url) value(it.title) value(it.source) - value(it.viewer) + value(it.viewer_flags) value(it.chapter_flags) endArray() } @@ -27,7 +27,7 @@ object MangaTypeAdapter { manga.url = nextString() manga.title = nextString() manga.source = nextLong() - manga.viewer = nextInt() + manga.viewer_flags = nextInt() manga.chapter_flags = nextInt() endArray() manga diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/Manga.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/Manga.kt index 4d035018..d4109284 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/Manga.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/Manga.kt @@ -1,8 +1,20 @@ package suwayomi.tachidesk.manga.impl.backup.models import eu.kanade.tachiyomi.source.model.SManga +//import eu.kanade.tachiyomi.ui.reader.setting.OrientationType +//import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType + + +// substitute for eu.kanade.tachiyomi.ui.reader.setting.OrientationType +object OrientationType { + const val MASK = 0x00000038 +} + +// substitute for eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType +object ReadingModeType { + const val MASK = 0x00000007 +} -// import tachiyomi.source.model.MangaInfo interface Manga : SManga { @@ -10,85 +22,100 @@ interface Manga : SManga { var source: Long - /** is in library */ var favorite: Boolean + // last time the chapter list changed in any way var last_update: Long + // predicted next update time based on latest (by date) 4 chapters' deltas + var next_update: Long + var date_added: Long - var viewer: Int + var viewer_flags: Int var chapter_flags: Int var cover_last_modified: Long fun setChapterOrder(order: Int) { - setFlags(order, SORT_MASK) + setChapterFlags(order, CHAPTER_SORT_MASK) } fun sortDescending(): Boolean { - return chapter_flags and SORT_MASK == SORT_DESC + return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC } fun getGenres(): List? { return genre?.split(", ")?.map { it.trim() } } - private fun setFlags(flag: Int, mask: Int) { + private fun setChapterFlags(flag: Int, mask: Int) { chapter_flags = chapter_flags and mask.inv() or (flag and mask) } + private fun setViewerFlags(flag: Int, mask: Int) { + viewer_flags = viewer_flags and mask.inv() or (flag and mask) + } + // Used to display the chapter's title one way or another var displayMode: Int - get() = chapter_flags and DISPLAY_MASK - set(mode) = setFlags(mode, DISPLAY_MASK) + get() = chapter_flags and CHAPTER_DISPLAY_MASK + set(mode) = setChapterFlags(mode, CHAPTER_DISPLAY_MASK) var readFilter: Int - get() = chapter_flags and READ_MASK - set(filter) = setFlags(filter, READ_MASK) + get() = chapter_flags and CHAPTER_READ_MASK + set(filter) = setChapterFlags(filter, CHAPTER_READ_MASK) var downloadedFilter: Int - get() = chapter_flags and DOWNLOADED_MASK - set(filter) = setFlags(filter, DOWNLOADED_MASK) + get() = chapter_flags and CHAPTER_DOWNLOADED_MASK + set(filter) = setChapterFlags(filter, CHAPTER_DOWNLOADED_MASK) var bookmarkedFilter: Int - get() = chapter_flags and BOOKMARKED_MASK - set(filter) = setFlags(filter, BOOKMARKED_MASK) + get() = chapter_flags and CHAPTER_BOOKMARKED_MASK + set(filter) = setChapterFlags(filter, CHAPTER_BOOKMARKED_MASK) var sorting: Int - get() = chapter_flags and SORTING_MASK - set(sort) = setFlags(sort, SORTING_MASK) + get() = chapter_flags and CHAPTER_SORTING_MASK + set(sort) = setChapterFlags(sort, CHAPTER_SORTING_MASK) + + var readingModeType: Int + get() = viewer_flags and ReadingModeType.MASK + set(readingMode) = setViewerFlags(readingMode, ReadingModeType.MASK) + + var orientationType: Int + get() = viewer_flags and OrientationType.MASK + set(rotationType) = setViewerFlags(rotationType, OrientationType.MASK) companion object { - const val SORT_DESC = 0x00000000 - const val SORT_ASC = 0x00000001 - const val SORT_MASK = 0x00000001 - // Generic filter that does not filter anything const val SHOW_ALL = 0x00000000 - const val SHOW_UNREAD = 0x00000002 - const val SHOW_READ = 0x00000004 - const val READ_MASK = 0x00000006 + const val CHAPTER_SORT_DESC = 0x00000000 + const val CHAPTER_SORT_ASC = 0x00000001 + const val CHAPTER_SORT_MASK = 0x00000001 - const val SHOW_DOWNLOADED = 0x00000008 - const val SHOW_NOT_DOWNLOADED = 0x00000010 - const val DOWNLOADED_MASK = 0x00000018 + const val CHAPTER_SHOW_UNREAD = 0x00000002 + const val CHAPTER_SHOW_READ = 0x00000004 + const val CHAPTER_READ_MASK = 0x00000006 - const val SHOW_BOOKMARKED = 0x00000020 - const val SHOW_NOT_BOOKMARKED = 0x00000040 - const val BOOKMARKED_MASK = 0x00000060 + const val CHAPTER_SHOW_DOWNLOADED = 0x00000008 + const val CHAPTER_SHOW_NOT_DOWNLOADED = 0x00000010 + const val CHAPTER_DOWNLOADED_MASK = 0x00000018 - const val SORTING_SOURCE = 0x00000000 - const val SORTING_NUMBER = 0x00000100 - const val SORTING_UPLOAD_DATE = 0x00000200 - const val SORTING_MASK = 0x00000300 + const val CHAPTER_SHOW_BOOKMARKED = 0x00000020 + const val CHAPTER_SHOW_NOT_BOOKMARKED = 0x00000040 + const val CHAPTER_BOOKMARKED_MASK = 0x00000060 - const val DISPLAY_NAME = 0x00000000 - const val DISPLAY_NUMBER = 0x00100000 - const val DISPLAY_MASK = 0x00100000 + const val CHAPTER_SORTING_SOURCE = 0x00000000 + const val CHAPTER_SORTING_NUMBER = 0x00000100 + const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200 + const val CHAPTER_SORTING_MASK = 0x00000300 + + const val CHAPTER_DISPLAY_NAME = 0x00000000 + const val CHAPTER_DISPLAY_NUMBER = 0x00100000 + const val CHAPTER_DISPLAY_MASK = 0x00100000 fun create(source: Long): Manga = MangaImpl().apply { this.source = source @@ -102,7 +129,7 @@ interface Manga : SManga { } } -// fun Manga.toMangaInfo(): MangaInfo { +//fun Manga.toMangaInfo(): MangaInfo { // return MangaInfo( // artist = this.artist ?: "", // author = this.author ?: "", @@ -113,4 +140,4 @@ interface Manga : SManga { // status = this.status, // title = this.title // ) -// } +//} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/MangaImpl.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/MangaImpl.kt index 8124cdda..a0f6f8fe 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/MangaImpl.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/models/MangaImpl.kt @@ -5,7 +5,7 @@ import suwayomi.tachidesk.manga.model.table.MangaTable open class MangaImpl : Manga { - override var id: Long? = 0 + override var id: Long? = null override var source: Long = -1 @@ -29,6 +29,8 @@ open class MangaImpl : Manga { override var last_update: Long = 0 + override var next_update: Long = 0 + override var date_added: Long = 0 override var initialized: Boolean = false @@ -42,7 +44,7 @@ open class MangaImpl : Manga { * 4 -> Webtoon * 5 -> Continues Vertical */ - override var viewer: Int = 0 + override var viewer_flags: Int = 0 /** Contains some useful info about */ @@ -70,7 +72,7 @@ open class MangaImpl : Manga { url = mangaRecord[MangaTable.url] title = mangaRecord[MangaTable.title] source = mangaRecord[MangaTable.sourceReference] - viewer = 0 // TODO: implement + viewer_flags = 0 // TODO: implement chapter_flags = 0 // TODO: implement } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupBase.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupBase.kt new file mode 100644 index 00000000..66c15327 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupBase.kt @@ -0,0 +1,16 @@ +package suwayomi.tachidesk.manga.impl.backup.proto + +import kotlinx.serialization.protobuf.ProtoBuf + +/* + * 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/. */ + +open class ProtoBackupBase { + var sourceMapping: Map = emptyMap() + + val parser = ProtoBuf +} \ No newline at end of file diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt new file mode 100644 index 00000000..84d574d1 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt @@ -0,0 +1,41 @@ +package suwayomi.tachidesk.manga.impl.backup.proto + +/* + * 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 okio.buffer +import okio.gzip +import okio.sink +import suwayomi.tachidesk.manga.impl.backup.BackupFlags +import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup +import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer +import java.io.ByteArrayOutputStream + +object ProtoBackupExport: ProtoBackupBase() { + suspend fun createBackup(flags: BackupFlags): ByteArray { + // Create root object + var backup: Backup? = null + +// databaseHelper.inTransaction { +// val databaseManga = getFavoriteManga() +// +// backup = Backup( +// backupManga(databaseManga, flags), +// backupCategories(), +// backupExtensionInfo(databaseManga) +// ) +// } + + val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!) + byteArray.inputStream() + + val byteStream = ByteArrayOutputStream() + byteStream.sink().gzip().buffer().use { it.write(byteArray) } + + return byteStream.toByteArray() + } +} \ No newline at end of file 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 new file mode 100644 index 00000000..1c88e0ea --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt @@ -0,0 +1,80 @@ +package suwayomi.tachidesk.manga.impl.backup.proto + +/* + * 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 mu.KotlinLogging +import okio.buffer +import okio.gzip +import okio.source +import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator.ValidationResult +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.BackupManga +import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSerializer +import java.io.InputStream + +private val logger = KotlinLogging.logger {} + +object ProtoBackupImport: ProtoBackupBase() { + var restoreAmount = 0 + + suspend fun performRestore(sourceStream: InputStream): ValidationResult { + + val backupString = sourceStream.source().gzip().buffer().use { it.readByteArray() } + val backup = parser.decodeFromByteArray(BackupSerializer, backupString) + + val validationResult = validate(backup) + + restoreAmount = backup.backupManga.size + 1 // +1 for categories + + // Restore categories + if (backup.backupCategories.isNotEmpty()) { + restoreCategories(backup.backupCategories) + } + + // 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) + } + + // TODO: optionally trigger online library + tracker update + + 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 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) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupValidator.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupValidator.kt new file mode 100644 index 00000000..2f4deae2 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupValidator.kt @@ -0,0 +1,45 @@ +package suwayomi.tachidesk.manga.impl.backup.proto + +/* + * 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 org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator +import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup +import suwayomi.tachidesk.manga.model.table.SourceTable + +object ProtoBackupValidator: AbstractBackupValidator() { + fun validate(backup: Backup): ValidationResult { + if (backup.backupManga.isEmpty()) { + throw Exception("Backup does not contain any manga.") + } + + val sources = backup.backupSources.map { it.sourceId to it.name }.toMap() + + val missingSources = transaction { + sources + .filter { SourceTable.select { SourceTable.id eq it.key }.firstOrNull() == null } + .map { "${it.value} (${it.key})" } + .sorted() + } + +// val trackers = backup.backupManga +// .flatMap { it.tracking } +// .map { it.syncId } +// .distinct() + + val missingTrackers = listOf("") +// val missingTrackers = trackers +// .mapNotNull { trackManager.getService(it) } +// .filter { !it.isLogged } +// .map { context.getString(it.nameRes()) } +// .sorted() + + return ValidationResult(missingSources, missingTrackers) + } +} \ No newline at end of file diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/Backup.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/Backup.kt new file mode 100644 index 00000000..f764af7b --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/Backup.kt @@ -0,0 +1,12 @@ +package suwayomi.tachidesk.manga.impl.backup.proto.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class Backup( + @ProtoNumber(1) val backupManga: List, + @ProtoNumber(2) var backupCategories: List = emptyList(), + // Bump by 100 to specify this is a 0.x value + @ProtoNumber(100) var backupSources: List = emptyList(), +) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupCategory.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupCategory.kt new file mode 100644 index 00000000..33479aaa --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupCategory.kt @@ -0,0 +1,33 @@ +package suwayomi.tachidesk.manga.impl.backup.proto.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import suwayomi.tachidesk.manga.impl.backup.models.Category +import suwayomi.tachidesk.manga.impl.backup.models.CategoryImpl + +@Serializable +class BackupCategory( + @ProtoNumber(1) var name: String, + @ProtoNumber(2) var order: Int = 0, + // @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x + // Bump by 100 to specify this is a 0.x value + @ProtoNumber(100) var flags: Int = 0, +) { + fun getCategoryImpl(): CategoryImpl { + return CategoryImpl().apply { + name = this@BackupCategory.name + flags = this@BackupCategory.flags + order = this@BackupCategory.order + } + } + + companion object { + fun copyFrom(category: Category): BackupCategory { + return BackupCategory( + name = category.name, + order = category.order, + flags = category.flags + ) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupChapter.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupChapter.kt new file mode 100644 index 00000000..38691c0e --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupChapter.kt @@ -0,0 +1,56 @@ +package suwayomi.tachidesk.manga.impl.backup.proto.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import suwayomi.tachidesk.manga.impl.backup.models.Chapter +import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl + +@Serializable +data class BackupChapter( + // in 1.x some of these values have different names + // url is called key in 1.x + @ProtoNumber(1) var url: String, + @ProtoNumber(2) var name: String, + @ProtoNumber(3) var scanlator: String? = null, + @ProtoNumber(4) var read: Boolean = false, + @ProtoNumber(5) var bookmark: Boolean = false, + // lastPageRead is called progress in 1.x + @ProtoNumber(6) var lastPageRead: Int = 0, + @ProtoNumber(7) var dateFetch: Long = 0, + @ProtoNumber(8) var dateUpload: Long = 0, + // chapterNumber is called number is 1.x + @ProtoNumber(9) var chapterNumber: Float = 0F, + @ProtoNumber(10) var sourceOrder: Int = 0, +) { + fun toChapterImpl(): ChapterImpl { + return ChapterImpl().apply { + url = this@BackupChapter.url + name = this@BackupChapter.name + chapter_number = this@BackupChapter.chapterNumber + scanlator = this@BackupChapter.scanlator + read = this@BackupChapter.read + bookmark = this@BackupChapter.bookmark + last_page_read = this@BackupChapter.lastPageRead + date_fetch = this@BackupChapter.dateFetch + date_upload = this@BackupChapter.dateUpload + source_order = this@BackupChapter.sourceOrder + } + } + + companion object { + fun copyFrom(chapter: Chapter): BackupChapter { + return BackupChapter( + url = chapter.url, + name = chapter.name, + chapterNumber = chapter.chapter_number, + scanlator = chapter.scanlator, + read = chapter.read, + bookmark = chapter.bookmark, + lastPageRead = chapter.last_page_read, + dateFetch = chapter.date_fetch, + dateUpload = chapter.date_upload, + sourceOrder = chapter.source_order + ) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupFull.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupFull.kt new file mode 100644 index 00000000..c83402f0 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupFull.kt @@ -0,0 +1,12 @@ +package suwayomi.tachidesk.manga.impl.backup.proto.models + +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale + +object BackupFull { + fun getDefaultFilename(): String { + val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) + return "tachiyomi_$date.proto.gz" + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupHistory.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupHistory.kt new file mode 100644 index 00000000..9ad1c80e --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupHistory.kt @@ -0,0 +1,10 @@ +package suwayomi.tachidesk.manga.impl.backup.proto.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class BackupHistory( + @ProtoNumber(0) var url: String, + @ProtoNumber(1) var lastRead: Long +) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupManga.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupManga.kt new file mode 100644 index 00000000..b3fb583c --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupManga.kt @@ -0,0 +1,89 @@ +package suwayomi.tachidesk.manga.impl.backup.proto.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import suwayomi.tachidesk.manga.impl.backup.models.ChapterImpl +import suwayomi.tachidesk.manga.impl.backup.models.Manga +import suwayomi.tachidesk.manga.impl.backup.models.MangaImpl +import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl + +@Serializable +data class BackupManga( + // in 1.x some of these values have different names + @ProtoNumber(1) var source: Long, + // url is called key in 1.x + @ProtoNumber(2) var url: String, + @ProtoNumber(3) var title: String = "", + @ProtoNumber(4) var artist: String? = null, + @ProtoNumber(5) var author: String? = null, + @ProtoNumber(6) var description: String? = null, + @ProtoNumber(7) var genre: List = emptyList(), + @ProtoNumber(8) var status: Int = 0, + // thumbnailUrl is called cover in 1.x + @ProtoNumber(9) var thumbnailUrl: String? = null, + // @ProtoNumber(10) val customCover: String = "", 1.x value, not used in 0.x + // @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x + // @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x + @ProtoNumber(13) var dateAdded: Long = 0, + @ProtoNumber(14) var viewer: Int = 0, // Replaced by viewer_flags + // @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x + @ProtoNumber(16) var chapters: List = emptyList(), + @ProtoNumber(17) var categories: List = emptyList(), + @ProtoNumber(18) var tracking: List = emptyList(), + // Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x + @ProtoNumber(100) var favorite: Boolean = true, + @ProtoNumber(101) var chapterFlags: Int = 0, + @ProtoNumber(102) var history: List = emptyList(), + @ProtoNumber(103) var viewer_flags: Int? = null +) { + fun getMangaImpl(): MangaImpl { + return MangaImpl().apply { + url = this@BackupManga.url + title = this@BackupManga.title + artist = this@BackupManga.artist + author = this@BackupManga.author + description = this@BackupManga.description + genre = this@BackupManga.genre.joinToString() + status = this@BackupManga.status + thumbnail_url = this@BackupManga.thumbnailUrl + favorite = this@BackupManga.favorite + source = this@BackupManga.source + date_added = this@BackupManga.dateAdded + viewer_flags = this@BackupManga.viewer_flags ?: this@BackupManga.viewer + chapter_flags = this@BackupManga.chapterFlags + } + } + + fun getChaptersImpl(): List { + return chapters.map { + it.toChapterImpl() + } + } + + fun getTrackingImpl(): List { + return tracking.map { + it.getTrackingImpl() + } + } + + companion object { + fun copyFrom(manga: Manga): BackupManga { + return BackupManga( + url = manga.url, + title = manga.title, + artist = manga.artist, + author = manga.author, + description = manga.description, + genre = manga.getGenres() ?: emptyList(), + status = manga.status, + thumbnailUrl = manga.thumbnail_url, + favorite = manga.favorite, + source = manga.source, + dateAdded = manga.date_added, + viewer = manga.readingModeType, + viewer_flags = manga.viewer_flags, + chapterFlags = manga.chapter_flags + ) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupSerializer.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupSerializer.kt new file mode 100644 index 00000000..9c4a6734 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupSerializer.kt @@ -0,0 +1,6 @@ +package suwayomi.tachidesk.manga.impl.backup.proto.models + +import kotlinx.serialization.Serializer + +@Serializer(forClass = Backup::class) +object BackupSerializer diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupSource.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupSource.kt new file mode 100644 index 00000000..7f7ddbe1 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupSource.kt @@ -0,0 +1,20 @@ +package suwayomi.tachidesk.manga.impl.backup.proto.models + +import eu.kanade.tachiyomi.source.Source +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber + +@Serializable +data class BackupSource( + @ProtoNumber(0) var name: String = "", + @ProtoNumber(1) var sourceId: Long +) { + companion object { + fun copyFrom(source: Source): BackupSource { + return BackupSource( + name = source.name, + sourceId = source.id + ) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupTracking.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupTracking.kt new file mode 100644 index 00000000..662c70ff --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupTracking.kt @@ -0,0 +1,65 @@ +package suwayomi.tachidesk.manga.impl.backup.proto.models + +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoNumber +import suwayomi.tachidesk.manga.impl.backup.models.Track +import suwayomi.tachidesk.manga.impl.backup.models.TrackImpl + +@Serializable +data class BackupTracking( + // in 1.x some of these values have different types or names + // syncId is called siteId in 1,x + @ProtoNumber(1) var syncId: Int, + // LibraryId is not null in 1.x + @ProtoNumber(2) var libraryId: Long, + @ProtoNumber(3) var mediaId: Int = 0, + // trackingUrl is called mediaUrl in 1.x + @ProtoNumber(4) var trackingUrl: String = "", + @ProtoNumber(5) var title: String = "", + // lastChapterRead is called last read, and it has been changed to a float in 1.x + @ProtoNumber(6) var lastChapterRead: Float = 0F, + @ProtoNumber(7) var totalChapters: Int = 0, + @ProtoNumber(8) var score: Float = 0F, + @ProtoNumber(9) var status: Int = 0, + // startedReadingDate is called startReadTime in 1.x + @ProtoNumber(10) var startedReadingDate: Long = 0, + // finishedReadingDate is called endReadTime in 1.x + @ProtoNumber(11) var finishedReadingDate: Long = 0, +) { + fun getTrackingImpl(): TrackImpl { + return TrackImpl().apply { + sync_id = this@BackupTracking.syncId + media_id = this@BackupTracking.mediaId + library_id = this@BackupTracking.libraryId + title = this@BackupTracking.title + // convert from float to int because of 1.x types + last_chapter_read = this@BackupTracking.lastChapterRead.toInt() + total_chapters = this@BackupTracking.totalChapters + score = this@BackupTracking.score + status = this@BackupTracking.status + started_reading_date = this@BackupTracking.startedReadingDate + finished_reading_date = this@BackupTracking.finishedReadingDate + tracking_url = this@BackupTracking.trackingUrl + } + } + + companion object { + fun copyFrom(track: Track): BackupTracking { + return BackupTracking( + syncId = track.sync_id, + mediaId = track.media_id, + // forced not null so its compatible with 1.x backup system + libraryId = track.library_id!!, + title = track.title, + // convert to float for 1.x + lastChapterRead = track.last_chapter_read.toFloat(), + totalChapters = track.total_chapters, + score = track.score, + status = track.status, + startedReadingDate = track.started_reading_date, + finishedReadingDate = track.finished_reading_date, + trackingUrl = track.tracking_url + ) + } + } +}