mirror of
https://git.mihon.tech/mihonapp/mihon
synced 2024-11-24 22:25:53 +03:00
Legacy backup conversion to Kotlin Serialization (#5282)
* Legacy backup conversion to Kotlin Serialization * Fix BackupTest compiling
This commit is contained in:
parent
b03ebc1fa4
commit
597cec3064
15 changed files with 416 additions and 365 deletions
|
@ -2,44 +2,52 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||
|
||||
import android.content.Context
|
||||
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.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.serializer.CategoryTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
|
||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer
|
||||
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.ChapterImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
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.TrackImpl
|
||||
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
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
|
||||
|
||||
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
|
||||
|
||||
val parser: Gson = when (version) {
|
||||
2 -> GsonBuilder()
|
||||
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
|
||||
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
|
||||
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
|
||||
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
|
||||
.create()
|
||||
val parser: Json = when (version) {
|
||||
2 -> Json {
|
||||
// Forks may have added items to backup
|
||||
ignoreUnknownKeys = true
|
||||
|
||||
// Register custom serializers
|
||||
serializersModule = SerializersModule {
|
||||
contextual(MangaTypeSerializer)
|
||||
contextual(MangaImplTypeSerializer)
|
||||
contextual(ChapterTypeSerializer)
|
||||
contextual(ChapterImplTypeSerializer)
|
||||
contextual(CategoryTypeSerializer)
|
||||
contextual(CategoryImplTypeSerializer)
|
||||
contextual(TrackTypeSerializer)
|
||||
contextual(TrackImplTypeSerializer)
|
||||
contextual(HistoryTypeSerializer)
|
||||
}
|
||||
}
|
||||
else -> throw Exception("Unknown backup version")
|
||||
}
|
||||
|
||||
|
@ -79,12 +87,11 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
|||
/**
|
||||
* 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
|
||||
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
|
||||
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
|
||||
|
||||
// Iterate over them
|
||||
backupCategories.forEach { category ->
|
||||
|
|
|
@ -2,88 +2,80 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||
|
||||
import android.content.Context
|
||||
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.data.backup.AbstractBackupRestore
|
||||
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.MANGAS
|
||||
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.ChapterImpl
|
||||
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.TrackImpl
|
||||
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
|
||||
|
||||
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
|
||||
|
||||
override suspend fun performRestore(uri: Uri): Boolean {
|
||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||
val json = JsonParser.parseReader(reader).asJsonObject
|
||||
// Read the json and create a Json Object,
|
||||
// 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)
|
||||
|
||||
val mangasJson = json.get(MANGAS).asJsonArray
|
||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||
// Decode the json object to a Backup object
|
||||
val backup = backupManager.parser.decodeFromJsonElement<Backup>(backupObject)
|
||||
|
||||
restoreAmount = backup.mangas.size + 1 // +1 for categories
|
||||
|
||||
// Restore categories
|
||||
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
|
||||
backup.categories?.let { restoreCategories(it) }
|
||||
|
||||
// Store source mapping for error messages
|
||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
|
||||
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList())
|
||||
|
||||
// Restore individual manga
|
||||
mangasJson.forEach {
|
||||
backup.mangas.forEach {
|
||||
if (job?.isActive != true) {
|
||||
return false
|
||||
}
|
||||
|
||||
restoreManga(it.asJsonObject)
|
||||
restoreManga(it)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun restoreCategories(categoriesJson: JsonElement) {
|
||||
private fun restoreCategories(categoriesJson: List<Category>) {
|
||||
db.inTransaction {
|
||||
backupManager.restoreCategories(categoriesJson.asJsonArray)
|
||||
backupManager.restoreCategories(categoriesJson)
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
|
||||
}
|
||||
|
||||
private suspend fun restoreManga(mangaJson: JsonObject) {
|
||||
val manga = backupManager.parser.fromJson<MangaImpl>(
|
||||
mangaJson.get(
|
||||
Backup.MANGA
|
||||
)
|
||||
)
|
||||
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()
|
||||
)
|
||||
private suspend fun restoreManga(mangaJson: MangaObject) {
|
||||
val manga = mangaJson.manga
|
||||
val chapters = mangaJson.chapters ?: emptyList()
|
||||
val categories = mangaJson.categories ?: emptyList()
|
||||
val history = mangaJson.history ?: emptyList()
|
||||
val tracks = mangaJson.track ?: emptyList()
|
||||
|
||||
val source = backupManager.sourceManager.get(manga.source)
|
||||
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
|
||||
|
|
|
@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.backup.legacy
|
|||
|
||||
import android.content.Context
|
||||
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.data.backup.AbstractBackupRestoreValidator
|
||||
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import okio.buffer
|
||||
import okio.source
|
||||
|
||||
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
||||
/**
|
||||
|
@ -17,30 +17,30 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||
* @return List of missing sources or missing trackers.
|
||||
*/
|
||||
override fun validate(context: Context, uri: Uri): Results {
|
||||
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||
val json = JsonParser.parseReader(reader).asJsonObject
|
||||
val backupManager = LegacyBackupManager(context)
|
||||
|
||||
val version = json.get(Backup.VERSION)
|
||||
val mangasJson = json.get(Backup.MANGAS)
|
||||
if (version == null || mangasJson == null) {
|
||||
val backup = backupManager.parser.decodeFromString<Backup>(
|
||||
context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
|
||||
)
|
||||
|
||||
if (backup.version == null) {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
|
||||
}
|
||||
|
||||
val mangas = mangasJson.asJsonArray
|
||||
if (mangas.size() == 0) {
|
||||
if (backup.mangas.isEmpty()) {
|
||||
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
|
||||
}
|
||||
|
||||
val sources = getSourceMapping(json)
|
||||
val sources = getSourceMapping(backup.extensions ?: emptyList())
|
||||
val missingSources = sources
|
||||
.filter { sourceManager.get(it.key) == null }
|
||||
.values
|
||||
.sorted()
|
||||
|
||||
val trackers = mangas
|
||||
.filter { it.asJsonObject.has("track") }
|
||||
.flatMap { it.asJsonObject["track"].asJsonArray }
|
||||
.map { it.asJsonObject["s"].asInt }
|
||||
val trackers = backup.mangas
|
||||
.filterNot { it.track.isNullOrEmpty() }
|
||||
.flatMap { it.track ?: emptyList() }
|
||||
.map { it.sync_id }
|
||||
.distinct()
|
||||
val missingTrackers = trackers
|
||||
.mapNotNull { trackManager.getService(it) }
|
||||
|
@ -52,12 +52,10 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
fun getSourceMapping(json: JsonObject): Map<Long, String> {
|
||||
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
|
||||
|
||||
return extensionsMapping.asJsonArray
|
||||
fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
|
||||
return extensionsMapping
|
||||
.map {
|
||||
val items = it.asString.split(":")
|
||||
val items = it.split(":")
|
||||
items[0].toLong() to items[1]
|
||||
}
|
||||
.toMap()
|
||||
|
|
|
@ -1,25 +1,37 @@
|
|||
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.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Json values
|
||||
*/
|
||||
object Backup {
|
||||
@Serializable
|
||||
data class Backup(
|
||||
val version: Int? = null,
|
||||
var mangas: MutableList<MangaObject> = mutableListOf(),
|
||||
var categories: List<@Contextual Category>? = null,
|
||||
var extensions: List<String>? = null
|
||||
) {
|
||||
companion object {
|
||||
const val CURRENT_VERSION = 2
|
||||
const val MANGA = "manga"
|
||||
const val MANGAS = "mangas"
|
||||
const val TRACK = "track"
|
||||
const val CHAPTERS = "chapters"
|
||||
const val CATEGORIES = "categories"
|
||||
const val EXTENSIONS = "extensions"
|
||||
const val HISTORY = "history"
|
||||
const val VERSION = "version"
|
||||
|
||||
fun getDefaultFilename(): String {
|
||||
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||
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
|
||||
)
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>()
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>()
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>()
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>()
|
|
@ -3,9 +3,6 @@ package eu.kanade.tachiyomi.data.backup
|
|||
import android.app.Application
|
||||
import android.content.Context
|
||||
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.CustomRobolectricGradleTestRunner
|
||||
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.Manga
|
||||
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.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
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.junit.Before
|
||||
import org.junit.Test
|
||||
|
@ -47,16 +48,10 @@ import uy.kohesive.injekt.api.addSingleton
|
|||
@RunWith(CustomRobolectricGradleTestRunner::class)
|
||||
class BackupTest {
|
||||
// Create root object
|
||||
var root = JsonObject()
|
||||
var root = Backup()
|
||||
|
||||
// Create information object
|
||||
var information = JsonObject()
|
||||
|
||||
// Create manga array
|
||||
var mangaEntries = JsonArray()
|
||||
|
||||
// Create category array
|
||||
var categoryEntries = JsonArray()
|
||||
var information = buildJsonObject {}
|
||||
|
||||
lateinit var app: Application
|
||||
lateinit var context: Context
|
||||
|
@ -83,11 +78,6 @@ class BackupTest {
|
|||
|
||||
source = mock(HttpSource::class.java)
|
||||
`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
|
||||
fun testRestoreEmptyCategory() {
|
||||
// Create backup of empty database
|
||||
legacyBackupManager.backupCategories(categoryEntries)
|
||||
|
||||
// Restore Json
|
||||
legacyBackupManager.restoreCategories(categoryEntries)
|
||||
legacyBackupManager.restoreCategories(root.categories ?: emptyList())
|
||||
|
||||
// Check if empty
|
||||
val dbCats = db.getCategories().executeAsBlocking()
|
||||
|
@ -115,7 +102,7 @@ class BackupTest {
|
|||
val category = addSingleCategory("category")
|
||||
|
||||
// Restore Json
|
||||
legacyBackupManager.restoreCategories(categoryEntries)
|
||||
legacyBackupManager.restoreCategories(root.categories ?: emptyList())
|
||||
|
||||
// Check if successful
|
||||
val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
|
||||
|
@ -139,7 +126,7 @@ class BackupTest {
|
|||
db.insertCategory(category).executeAsBlocking()
|
||||
|
||||
// Restore Json
|
||||
legacyBackupManager.restoreCategories(categoryEntries)
|
||||
legacyBackupManager.restoreCategories(root.categories ?: emptyList())
|
||||
|
||||
// Check if successful
|
||||
val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
|
||||
|
@ -167,9 +154,6 @@ class BackupTest {
|
|||
assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.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
|
||||
val dbManga = getSingleManga("One Piece")
|
||||
dbManga.id = manga.id
|
||||
|
@ -198,9 +182,9 @@ class BackupTest {
|
|||
|
||||
// Restore Json
|
||||
// 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
|
||||
val jsonManga = legacyBackupManager.parser.fromJson<MangaImpl>(json)
|
||||
val jsonManga = legacyBackupManager.parser.decodeFromString<Manga>(json)
|
||||
|
||||
// Restore manga with fetch observable
|
||||
val networkManga = getSingleManga("One Piece")
|
||||
|
@ -237,8 +221,8 @@ class BackupTest {
|
|||
}
|
||||
|
||||
// Check parser
|
||||
val chaptersJson = legacyBackupManager.parser.toJsonTree(chapters)
|
||||
val restoredChapters = legacyBackupManager.parser.fromJson<List<ChapterImpl>>(chaptersJson)
|
||||
val chaptersJson = legacyBackupManager.parser.encodeToString(chapters)
|
||||
val restoredChapters = legacyBackupManager.parser.decodeFromString<List<Chapter>>(chaptersJson)
|
||||
|
||||
// Fetch chapters from upstream
|
||||
// Create list
|
||||
|
@ -275,8 +259,8 @@ class BackupTest {
|
|||
historyList.add(historyJson)
|
||||
|
||||
// Check parser
|
||||
val historyListJson = legacyBackupManager.parser.toJsonTree(historyList)
|
||||
val history = legacyBackupManager.parser.fromJson<List<DHistory>>(historyListJson)
|
||||
val historyListJson = legacyBackupManager.parser.encodeToString(historyList)
|
||||
val history = legacyBackupManager.parser.decodeFromString<List<DHistory>>(historyListJson)
|
||||
|
||||
// Restore categories
|
||||
legacyBackupManager.restoreHistoryForManga(history)
|
||||
|
@ -314,8 +298,8 @@ class BackupTest {
|
|||
// Check parser and restore already in database
|
||||
var trackList = listOf(track)
|
||||
// Check parser
|
||||
var trackListJson = legacyBackupManager.parser.toJsonTree(trackList)
|
||||
var trackListRestore = legacyBackupManager.parser.fromJson<List<TrackImpl>>(trackListJson)
|
||||
var trackListJson = legacyBackupManager.parser.encodeToString(trackList)
|
||||
var trackListRestore = legacyBackupManager.parser.decodeFromString<List<Track>>(trackListJson)
|
||||
legacyBackupManager.restoreTrackForManga(manga, trackListRestore)
|
||||
|
||||
// Assert if restore works.
|
||||
|
@ -337,8 +321,8 @@ class BackupTest {
|
|||
trackList = listOf(track2)
|
||||
|
||||
// Check parser
|
||||
trackListJson = legacyBackupManager.parser.toJsonTree(trackList)
|
||||
trackListRestore = legacyBackupManager.parser.fromJson<List<TrackImpl>>(trackListJson)
|
||||
trackListJson = legacyBackupManager.parser.encodeToString(trackList)
|
||||
trackListRestore = legacyBackupManager.parser.decodeFromString<List<Track>>(trackListJson)
|
||||
legacyBackupManager.restoreTrackForManga(manga2, trackListRestore)
|
||||
|
||||
// Assert if restore works.
|
||||
|
@ -348,16 +332,13 @@ class BackupTest {
|
|||
}
|
||||
|
||||
private fun clearJson() {
|
||||
root = JsonObject()
|
||||
information = JsonObject()
|
||||
mangaEntries = JsonArray()
|
||||
categoryEntries = JsonArray()
|
||||
root = Backup()
|
||||
information = buildJsonObject {}
|
||||
}
|
||||
|
||||
private fun addSingleCategory(name: String): Category {
|
||||
val category = Category.create(name)
|
||||
val catJson = legacyBackupManager.parser.toJsonTree(category)
|
||||
categoryEntries.add(catJson)
|
||||
root.categories = listOf(category)
|
||||
return category
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue