mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2026-01-21 03:02:47 +01:00
@@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// }
|
||||
//}
|
||||
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<String>, val missingTrackers: List<String>)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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<String>, val missingTrackers: List<String>)
|
||||
object LegacyBackupValidator: AbstractBackupValidator() {
|
||||
|
||||
/**
|
||||
* Checks for critical backup file data.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String>? {
|
||||
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
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Long, String> = emptyMap()
|
||||
|
||||
val parser = ProtoBuf
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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<BackupCategory>) { // TODO
|
||||
// db.inTransaction {
|
||||
// backupManager.restoreCategories(backupCategories)
|
||||
// }
|
||||
//
|
||||
// restoreProgress += 1
|
||||
// showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||
}
|
||||
|
||||
|
||||
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) { // 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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<BackupManga>,
|
||||
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
|
||||
// Bump by 100 to specify this is a 0.x value
|
||||
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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<String> = 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<BackupChapter> = emptyList(),
|
||||
@ProtoNumber(17) var categories: List<Int> = emptyList(),
|
||||
@ProtoNumber(18) var tracking: List<BackupTracking> = 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<BackupHistory> = 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<ChapterImpl> {
|
||||
return chapters.map {
|
||||
it.toChapterImpl()
|
||||
}
|
||||
}
|
||||
|
||||
fun getTrackingImpl(): List<TrackImpl> {
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||
|
||||
import kotlinx.serialization.Serializer
|
||||
|
||||
@Serializer(forClass = Backup::class)
|
||||
object BackupSerializer
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user