Legacy backup conversion to Kotlin Serialization (#5282)

* Legacy backup conversion to Kotlin Serialization

* Fix BackupTest compiling
This commit is contained in:
jobobby04 2021-06-04 18:50:22 -04:00 committed by GitHub
parent b03ebc1fa4
commit 597cec3064
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 416 additions and 365 deletions

View File

@ -2,44 +2,52 @@ package eu.kanade.tachiyomi.data.backup.legacy
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.registerTypeAdapter
import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.JsonArray
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer
import eu.kanade.tachiyomi.data.database.models.CategoryImpl import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaImplTypeSerializer
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeSerializer
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackImplTypeSerializer
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeSerializer
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.contextual
import kotlin.math.max import kotlin.math.max
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) { class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
val parser: Gson = when (version) { val parser: Json = when (version) {
2 -> GsonBuilder() 2 -> Json {
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build()) // Forks may have added items to backup
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build()) ignoreUnknownKeys = true
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build()) // Register custom serializers
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build()) serializersModule = SerializersModule {
.create() contextual(MangaTypeSerializer)
contextual(MangaImplTypeSerializer)
contextual(ChapterTypeSerializer)
contextual(ChapterImplTypeSerializer)
contextual(CategoryTypeSerializer)
contextual(CategoryImplTypeSerializer)
contextual(TrackTypeSerializer)
contextual(TrackImplTypeSerializer)
contextual(HistoryTypeSerializer)
}
}
else -> throw Exception("Unknown backup version") else -> throw Exception("Unknown backup version")
} }
@ -79,12 +87,11 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
/** /**
* Restore the categories from Json * Restore the categories from Json
* *
* @param jsonCategories array containing categories * @param backupCategories array containing categories
*/ */
internal fun restoreCategories(jsonCategories: JsonArray) { internal fun restoreCategories(backupCategories: List<Category>) {
// Get categories from file and from db // Get categories from file and from db
val dbCategories = databaseHelper.getCategories().executeAsBlocking() val dbCategories = databaseHelper.getCategories().executeAsBlocking()
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
// Iterate over them // Iterate over them
backupCategories.forEach { category -> backupCategories.forEach { category ->

View File

@ -2,88 +2,80 @@ package eu.kanade.tachiyomi.data.backup.legacy
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
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.stream.JsonReader
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
import eu.kanade.tachiyomi.data.backup.BackupNotifier import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
import eu.kanade.tachiyomi.data.backup.legacy.models.MangaObject
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonPrimitive
import okio.buffer
import okio.source
import java.util.Date import java.util.Date
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) { class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
override suspend fun performRestore(uri: Uri): Boolean { override suspend fun performRestore(uri: Uri): Boolean {
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader()) // Read the json and create a Json Object,
val json = JsonParser.parseReader(reader).asJsonObject // cannot use the backupManager json deserializer one because its not initialized yet
val backupObject = Json.decodeFromString<JsonObject>(
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
)
val version = json.get(Backup.VERSION)?.asInt ?: 1 // Get parser version
val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1
// Initialize manager
backupManager = LegacyBackupManager(context, version) backupManager = LegacyBackupManager(context, version)
val mangasJson = json.get(MANGAS).asJsonArray // Decode the json object to a Backup object
restoreAmount = mangasJson.size() + 1 // +1 for categories val backup = backupManager.parser.decodeFromJsonElement<Backup>(backupObject)
restoreAmount = backup.mangas.size + 1 // +1 for categories
// Restore categories // Restore categories
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) } backup.categories?.let { restoreCategories(it) }
// Store source mapping for error messages // Store source mapping for error messages
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json) sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList())
// Restore individual manga // Restore individual manga
mangasJson.forEach { backup.mangas.forEach {
if (job?.isActive != true) { if (job?.isActive != true) {
return false return false
} }
restoreManga(it.asJsonObject) restoreManga(it)
} }
return true return true
} }
private fun restoreCategories(categoriesJson: JsonElement) { private fun restoreCategories(categoriesJson: List<Category>) {
db.inTransaction { db.inTransaction {
backupManager.restoreCategories(categoriesJson.asJsonArray) backupManager.restoreCategories(categoriesJson)
} }
restoreProgress += 1 restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories)) showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
} }
private suspend fun restoreManga(mangaJson: JsonObject) { private suspend fun restoreManga(mangaJson: MangaObject) {
val manga = backupManager.parser.fromJson<MangaImpl>( val manga = mangaJson.manga
mangaJson.get( val chapters = mangaJson.chapters ?: emptyList()
Backup.MANGA val categories = mangaJson.categories ?: emptyList()
) val history = mangaJson.history ?: emptyList()
) val tracks = mangaJson.track ?: emptyList()
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
mangaJson.get(Backup.CHAPTERS)
?: JsonArray()
)
val categories = backupManager.parser.fromJson<List<String>>(
mangaJson.get(Backup.CATEGORIES)
?: JsonArray()
)
val history = backupManager.parser.fromJson<List<DHistory>>(
mangaJson.get(Backup.HISTORY)
?: JsonArray()
)
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
mangaJson.get(Backup.TRACK)
?: JsonArray()
)
val source = backupManager.sourceManager.get(manga.source) val source = backupManager.sourceManager.get(manga.source)
val sourceName = sourceMapping[manga.source] ?: manga.source.toString() val sourceName = sourceMapping[manga.source] ?: manga.source.toString()

View File

@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.backup.legacy
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
import kotlinx.serialization.decodeFromString
import okio.buffer
import okio.source
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() { class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
/** /**
@ -17,30 +17,30 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
* @return List of missing sources or missing trackers. * @return List of missing sources or missing trackers.
*/ */
override fun validate(context: Context, uri: Uri): Results { override fun validate(context: Context, uri: Uri): Results {
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader()) val backupManager = LegacyBackupManager(context)
val json = JsonParser.parseReader(reader).asJsonObject
val version = json.get(Backup.VERSION) val backup = backupManager.parser.decodeFromString<Backup>(
val mangasJson = json.get(Backup.MANGAS) context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
if (version == null || mangasJson == null) { )
if (backup.version == null) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_data)) throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
} }
val mangas = mangasJson.asJsonArray if (backup.mangas.isEmpty()) {
if (mangas.size() == 0) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga)) throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
} }
val sources = getSourceMapping(json) val sources = getSourceMapping(backup.extensions ?: emptyList())
val missingSources = sources val missingSources = sources
.filter { sourceManager.get(it.key) == null } .filter { sourceManager.get(it.key) == null }
.values .values
.sorted() .sorted()
val trackers = mangas val trackers = backup.mangas
.filter { it.asJsonObject.has("track") } .filterNot { it.track.isNullOrEmpty() }
.flatMap { it.asJsonObject["track"].asJsonArray } .flatMap { it.track ?: emptyList() }
.map { it.asJsonObject["s"].asInt } .map { it.sync_id }
.distinct() .distinct()
val missingTrackers = trackers val missingTrackers = trackers
.mapNotNull { trackManager.getService(it) } .mapNotNull { trackManager.getService(it) }
@ -52,12 +52,10 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
} }
companion object { companion object {
fun getSourceMapping(json: JsonObject): Map<Long, String> { fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap() return extensionsMapping
return extensionsMapping.asJsonArray
.map { .map {
val items = it.asString.split(":") val items = it.split(":")
items[0].toLong() to items[1] items[0].toLong() to items[1]
} }
.toMap() .toMap()

View File

@ -1,25 +1,37 @@
package eu.kanade.tachiyomi.data.backup.legacy.models package eu.kanade.tachiyomi.data.backup.legacy.models
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
/** @Serializable
* Json values data class Backup(
*/ val version: Int? = null,
object Backup { var mangas: MutableList<MangaObject> = mutableListOf(),
const val CURRENT_VERSION = 2 var categories: List<@Contextual Category>? = null,
const val MANGA = "manga" var extensions: List<String>? = null
const val MANGAS = "mangas" ) {
const val TRACK = "track" companion object {
const val CHAPTERS = "chapters" const val CURRENT_VERSION = 2
const val CATEGORIES = "categories"
const val EXTENSIONS = "extensions"
const val HISTORY = "history"
const val VERSION = "version"
fun getDefaultFilename(): String { fun getDefaultFilename(): String {
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date()) val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
return "tachiyomi_$date.json" return "tachiyomi_$date.json"
}
} }
} }
@Serializable
data class MangaObject(
var manga: @Contextual Manga,
var chapters: List<@Contextual Chapter>? = null,
var categories: List<String>? = null,
var track: List<@Contextual Track>? = null,
var history: List<@Contextual DHistory>? = null
)

View File

@ -1,31 +0,0 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
/**
* JSON Serializer used to write / read [CategoryImpl] to / from json
*/
object CategoryTypeAdapter {
fun build(): TypeAdapter<CategoryImpl> {
return typeAdapter {
write {
beginArray()
value(it.name)
value(it.order)
endArray()
}
read {
beginArray()
val category = CategoryImpl()
category.name = nextString()
category.order = nextInt()
endArray()
category
}
}
}
}

View File

@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
/**
* JSON Serializer used to write / read [CategoryImpl] to / from json
*/
open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Category")
override fun serialize(encoder: Encoder, value: T) {
encoder as JsonEncoder
encoder.encodeJsonElement(
buildJsonArray {
add(value.name)
add(value.order)
}
)
}
@Suppress("UNCHECKED_CAST")
override fun deserialize(decoder: Decoder): T {
// make a category impl and cast as T so that the serializer accepts it
return CategoryImpl().apply {
decoder as JsonDecoder
val array = decoder.decodeJsonElement().jsonArray
name = array[0].jsonPrimitive.content
order = array[1].jsonPrimitive.int
} as T
}
}
// Allow for serialization of a category and category impl
object CategoryTypeSerializer : CategoryBaseSerializer<Category>()
object CategoryImplTypeSerializer : CategoryBaseSerializer<CategoryImpl>()

View File

@ -1,59 +0,0 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonToken
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
/**
* JSON Serializer used to write / read [ChapterImpl] to / from json
*/
object ChapterTypeAdapter {
private const val URL = "u"
private const val READ = "r"
private const val BOOKMARK = "b"
private const val LAST_READ = "l"
fun build(): TypeAdapter<ChapterImpl> {
return typeAdapter {
write {
if (it.read || it.bookmark || it.last_page_read != 0) {
beginObject()
name(URL)
value(it.url)
if (it.read) {
name(READ)
value(1)
}
if (it.bookmark) {
name(BOOKMARK)
value(1)
}
if (it.last_page_read != 0) {
name(LAST_READ)
value(it.last_page_read)
}
endObject()
}
}
read {
val chapter = ChapterImpl()
beginObject()
while (hasNext()) {
if (peek() == JsonToken.NAME) {
when (nextName()) {
URL -> chapter.url = nextString()
READ -> chapter.read = nextInt() == 1
BOOKMARK -> chapter.bookmark = nextInt() == 1
LAST_READ -> chapter.last_page_read = nextInt()
}
}
}
endObject()
chapter
}
}
}
}

View File

@ -0,0 +1,66 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.put
/**
* JSON Serializer used to write / read [ChapterImpl] to / from json
*/
open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
override val descriptor = buildClassSerialDescriptor("Chapter")
override fun serialize(encoder: Encoder, value: T) {
encoder as JsonEncoder
encoder.encodeJsonElement(
buildJsonObject {
put(URL, value.url)
if (value.read) {
put(READ, 1)
}
if (value.bookmark) {
put(BOOKMARK, 1)
}
if (value.last_page_read != 0) {
put(LAST_READ, value.last_page_read)
}
}
)
}
@Suppress("UNCHECKED_CAST")
override fun deserialize(decoder: Decoder): T {
// make a chapter impl and cast as T so that the serializer accepts it
return ChapterImpl().apply {
decoder as JsonDecoder
val jsonObject = decoder.decodeJsonElement().jsonObject
url = jsonObject[URL]!!.jsonPrimitive.content
read = jsonObject[READ]?.jsonPrimitive?.intOrNull == 1
bookmark = jsonObject[BOOKMARK]?.jsonPrimitive?.intOrNull == 1
last_page_read = jsonObject[LAST_READ]?.jsonPrimitive?.intOrNull ?: last_page_read
} as T
}
companion object {
private const val URL = "u"
private const val READ = "r"
private const val BOOKMARK = "b"
private const val LAST_READ = "l"
}
}
// Allow for serialization of a chapter and chapter impl
object ChapterTypeSerializer : ChapterBaseSerializer<Chapter>()
object ChapterImplTypeSerializer : ChapterBaseSerializer<ChapterImpl>()

View File

@ -1,32 +0,0 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
/**
* JSON Serializer used to write / read [DHistory] to / from json
*/
object HistoryTypeAdapter {
fun build(): TypeAdapter<DHistory> {
return typeAdapter {
write {
if (it.lastRead != 0L) {
beginArray()
value(it.url)
value(it.lastRead)
endArray()
}
}
read {
beginArray()
val url = nextString()
val lastRead = nextLong()
endArray()
DHistory(url, lastRead)
}
}
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
/**
* JSON Serializer used to write / read [DHistory] to / from json
*/
object HistoryTypeSerializer : KSerializer<DHistory> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("History")
override fun serialize(encoder: Encoder, value: DHistory) {
encoder as JsonEncoder
encoder.encodeJsonElement(
buildJsonArray {
add(value.url)
add(value.lastRead)
}
)
}
override fun deserialize(decoder: Decoder): DHistory {
decoder as JsonDecoder
val array = decoder.decodeJsonElement().jsonArray
return DHistory(
url = array[0].jsonPrimitive.content,
lastRead = array[1].jsonPrimitive.long
)
}
}

View File

@ -1,37 +0,0 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import eu.kanade.tachiyomi.data.database.models.MangaImpl
/**
* JSON Serializer used to write / read [MangaImpl] to / from json
*/
object MangaTypeAdapter {
fun build(): TypeAdapter<MangaImpl> {
return typeAdapter {
write {
beginArray()
value(it.url)
value(it.title)
value(it.source)
value(it.viewer_flags)
value(it.chapter_flags)
endArray()
}
read {
beginArray()
val manga = MangaImpl()
manga.url = nextString()
manga.title = nextString()
manga.source = nextLong()
manga.viewer_flags = nextInt()
manga.chapter_flags = nextInt()
endArray()
manga
}
}
}
}

View File

@ -0,0 +1,56 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.add
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
/**
* JSON Serializer used to write / read [MangaImpl] to / from json
*/
open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
override fun serialize(encoder: Encoder, value: T) {
encoder as JsonEncoder
encoder.encodeJsonElement(
buildJsonArray {
add(value.url)
add(value.title)
add(value.source)
add(value.viewer_flags)
add(value.chapter_flags)
}
)
}
@Suppress("UNCHECKED_CAST")
override fun deserialize(decoder: Decoder): T {
// make a manga impl and cast as T so that the serializer accepts it
return MangaImpl().apply {
decoder as JsonDecoder
val array = decoder.decodeJsonElement().jsonArray
url = array[0].jsonPrimitive.content
title = array[1].jsonPrimitive.content
source = array[2].jsonPrimitive.long
viewer_flags = array[3].jsonPrimitive.int
chapter_flags = array[4].jsonPrimitive.int
} as T
}
}
// Allow for serialization of a manga and manga impl
object MangaTypeSerializer : MangaBaseSerializer<Manga>()
object MangaImplTypeSerializer : MangaBaseSerializer<MangaImpl>()

View File

@ -1,59 +0,0 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonToken
import eu.kanade.tachiyomi.data.database.models.TrackImpl
/**
* JSON Serializer used to write / read [TrackImpl] to / from json
*/
object TrackTypeAdapter {
private const val SYNC = "s"
private const val MEDIA = "r"
private const val LIBRARY = "ml"
private const val TITLE = "t"
private const val LAST_READ = "l"
private const val TRACKING_URL = "u"
fun build(): TypeAdapter<TrackImpl> {
return typeAdapter {
write {
beginObject()
name(TITLE)
value(it.title)
name(SYNC)
value(it.sync_id)
name(MEDIA)
value(it.media_id)
name(LIBRARY)
value(it.library_id)
name(LAST_READ)
value(it.last_chapter_read)
name(TRACKING_URL)
value(it.tracking_url)
endObject()
}
read {
val track = TrackImpl()
beginObject()
while (hasNext()) {
if (peek() == JsonToken.NAME) {
when (nextName()) {
TITLE -> track.title = nextString()
SYNC -> track.sync_id = nextInt()
MEDIA -> track.media_id = nextInt()
LIBRARY -> track.library_id = nextLong()
LAST_READ -> track.last_chapter_read = nextInt()
TRACKING_URL -> track.tracking_url = nextString()
}
}
}
endObject()
track
}
}
}
}

View File

@ -0,0 +1,67 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.JsonDecoder
import kotlinx.serialization.json.JsonEncoder
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.put
/**
* JSON Serializer used to write / read [TrackImpl] to / from json
*/
open class TrackBaseSerializer<T : Track> : KSerializer<T> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Track")
override fun serialize(encoder: Encoder, value: T) {
encoder as JsonEncoder
encoder.encodeJsonElement(
buildJsonObject {
put(TITLE, value.title)
put(SYNC, value.sync_id)
put(MEDIA, value.media_id)
put(LIBRARY, value.library_id)
put(LAST_READ, value.last_chapter_read)
put(TRACKING_URL, value.tracking_url)
}
)
}
@Suppress("UNCHECKED_CAST")
override fun deserialize(decoder: Decoder): T {
// make a track impl and cast as T so that the serializer accepts it
return TrackImpl().apply {
decoder as JsonDecoder
val jsonObject = decoder.decodeJsonElement().jsonObject
title = jsonObject[TITLE]!!.jsonPrimitive.content
sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.int
tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
} as T
}
companion object {
private const val SYNC = "s"
private const val MEDIA = "r"
private const val LIBRARY = "ml"
private const val TITLE = "t"
private const val LAST_READ = "l"
private const val TRACKING_URL = "u"
}
}
// Allow for serialization of a track and track impl
object TrackTypeSerializer : TrackBaseSerializer<Track>()
object TrackImplTypeSerializer : TrackBaseSerializer<TrackImpl>()

View File

@ -3,9 +3,6 @@ package eu.kanade.tachiyomi.data.backup
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
@ -17,12 +14,16 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.buildJsonObject
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.Before import org.junit.Before
import org.junit.Test import org.junit.Test
@ -47,16 +48,10 @@ import uy.kohesive.injekt.api.addSingleton
@RunWith(CustomRobolectricGradleTestRunner::class) @RunWith(CustomRobolectricGradleTestRunner::class)
class BackupTest { class BackupTest {
// Create root object // Create root object
var root = JsonObject() var root = Backup()
// Create information object // Create information object
var information = JsonObject() var information = buildJsonObject {}
// Create manga array
var mangaEntries = JsonArray()
// Create category array
var categoryEntries = JsonArray()
lateinit var app: Application lateinit var app: Application
lateinit var context: Context lateinit var context: Context
@ -83,11 +78,6 @@ class BackupTest {
source = mock(HttpSource::class.java) source = mock(HttpSource::class.java)
`when`(legacyBackupManager.sourceManager.get(anyLong())).thenReturn(source) `when`(legacyBackupManager.sourceManager.get(anyLong())).thenReturn(source)
root.add(Backup.MANGAS, mangaEntries)
root.add(Backup.CATEGORIES, categoryEntries)
clearJson()
} }
/** /**
@ -95,11 +85,8 @@ class BackupTest {
*/ */
@Test @Test
fun testRestoreEmptyCategory() { fun testRestoreEmptyCategory() {
// Create backup of empty database
legacyBackupManager.backupCategories(categoryEntries)
// Restore Json // Restore Json
legacyBackupManager.restoreCategories(categoryEntries) legacyBackupManager.restoreCategories(root.categories ?: emptyList())
// Check if empty // Check if empty
val dbCats = db.getCategories().executeAsBlocking() val dbCats = db.getCategories().executeAsBlocking()
@ -115,7 +102,7 @@ class BackupTest {
val category = addSingleCategory("category") val category = addSingleCategory("category")
// Restore Json // Restore Json
legacyBackupManager.restoreCategories(categoryEntries) legacyBackupManager.restoreCategories(root.categories ?: emptyList())
// Check if successful // Check if successful
val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking() val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
@ -139,7 +126,7 @@ class BackupTest {
db.insertCategory(category).executeAsBlocking() db.insertCategory(category).executeAsBlocking()
// Restore Json // Restore Json
legacyBackupManager.restoreCategories(categoryEntries) legacyBackupManager.restoreCategories(root.categories ?: emptyList())
// Check if successful // Check if successful
val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking() val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
@ -167,9 +154,6 @@ class BackupTest {
assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue) assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue)
assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue) assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue)
// Update json with all options enabled
mangaEntries.add(legacyBackupManager.backupMangaObject(manga, 1))
// Change manga in database to default values // Change manga in database to default values
val dbManga = getSingleManga("One Piece") val dbManga = getSingleManga("One Piece")
dbManga.id = manga.id dbManga.id = manga.id
@ -198,9 +182,9 @@ class BackupTest {
// Restore Json // Restore Json
// Create JSON from manga to test parser // Create JSON from manga to test parser
val json = legacyBackupManager.parser.toJsonTree(manga) val json = legacyBackupManager.parser.encodeToString(manga)
// Restore JSON from manga to test parser // Restore JSON from manga to test parser
val jsonManga = legacyBackupManager.parser.fromJson<MangaImpl>(json) val jsonManga = legacyBackupManager.parser.decodeFromString<Manga>(json)
// Restore manga with fetch observable // Restore manga with fetch observable
val networkManga = getSingleManga("One Piece") val networkManga = getSingleManga("One Piece")
@ -237,8 +221,8 @@ class BackupTest {
} }
// Check parser // Check parser
val chaptersJson = legacyBackupManager.parser.toJsonTree(chapters) val chaptersJson = legacyBackupManager.parser.encodeToString(chapters)
val restoredChapters = legacyBackupManager.parser.fromJson<List<ChapterImpl>>(chaptersJson) val restoredChapters = legacyBackupManager.parser.decodeFromString<List<Chapter>>(chaptersJson)
// Fetch chapters from upstream // Fetch chapters from upstream
// Create list // Create list
@ -275,8 +259,8 @@ class BackupTest {
historyList.add(historyJson) historyList.add(historyJson)
// Check parser // Check parser
val historyListJson = legacyBackupManager.parser.toJsonTree(historyList) val historyListJson = legacyBackupManager.parser.encodeToString(historyList)
val history = legacyBackupManager.parser.fromJson<List<DHistory>>(historyListJson) val history = legacyBackupManager.parser.decodeFromString<List<DHistory>>(historyListJson)
// Restore categories // Restore categories
legacyBackupManager.restoreHistoryForManga(history) legacyBackupManager.restoreHistoryForManga(history)
@ -314,8 +298,8 @@ class BackupTest {
// Check parser and restore already in database // Check parser and restore already in database
var trackList = listOf(track) var trackList = listOf(track)
// Check parser // Check parser
var trackListJson = legacyBackupManager.parser.toJsonTree(trackList) var trackListJson = legacyBackupManager.parser.encodeToString(trackList)
var trackListRestore = legacyBackupManager.parser.fromJson<List<TrackImpl>>(trackListJson) var trackListRestore = legacyBackupManager.parser.decodeFromString<List<Track>>(trackListJson)
legacyBackupManager.restoreTrackForManga(manga, trackListRestore) legacyBackupManager.restoreTrackForManga(manga, trackListRestore)
// Assert if restore works. // Assert if restore works.
@ -337,8 +321,8 @@ class BackupTest {
trackList = listOf(track2) trackList = listOf(track2)
// Check parser // Check parser
trackListJson = legacyBackupManager.parser.toJsonTree(trackList) trackListJson = legacyBackupManager.parser.encodeToString(trackList)
trackListRestore = legacyBackupManager.parser.fromJson<List<TrackImpl>>(trackListJson) trackListRestore = legacyBackupManager.parser.decodeFromString<List<Track>>(trackListJson)
legacyBackupManager.restoreTrackForManga(manga2, trackListRestore) legacyBackupManager.restoreTrackForManga(manga2, trackListRestore)
// Assert if restore works. // Assert if restore works.
@ -348,16 +332,13 @@ class BackupTest {
} }
private fun clearJson() { private fun clearJson() {
root = JsonObject() root = Backup()
information = JsonObject() information = buildJsonObject {}
mangaEntries = JsonArray()
categoryEntries = JsonArray()
} }
private fun addSingleCategory(name: String): Category { private fun addSingleCategory(name: String): Category {
val category = Category.create(name) val category = Category.create(name)
val catJson = legacyBackupManager.parser.toJsonTree(category) root.categories = listOf(category)
categoryEntries.add(catJson)
return category return category
} }