mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 22:37:56 +01:00 
			
		
		
		
	Remove logic for restoring legacy JSON backups
- Protobuf backups have been around for 1.5 years now - The ability to restore online-dependant data from JSON backups gets harder as time goes on and sources drift - If users really need a way to restore them, they can use an older version of the app, or a separate tool for translating between the formats could be created
This commit is contained in:
		| @@ -12,14 +12,15 @@ import eu.kanade.tachiyomi.source.Source | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import eu.kanade.tachiyomi.source.model.toSChapter | ||||
| import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource | ||||
| import uy.kohesive.injekt.injectLazy | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| abstract class AbstractBackupManager(protected val context: Context) { | ||||
|  | ||||
|     internal val databaseHelper: DatabaseHelper by injectLazy() | ||||
|     internal val sourceManager: SourceManager by injectLazy() | ||||
|     internal val trackManager: TrackManager by injectLazy() | ||||
|     protected val preferences: PreferencesHelper by injectLazy() | ||||
|     internal val db: DatabaseHelper = Injekt.get() | ||||
|     internal val sourceManager: SourceManager = Injekt.get() | ||||
|     internal val trackManager: TrackManager = Injekt.get() | ||||
|     protected val preferences: PreferencesHelper = Injekt.get() | ||||
|  | ||||
|     abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String | ||||
|  | ||||
| @@ -29,7 +30,7 @@ abstract class AbstractBackupManager(protected val context: Context) { | ||||
|      * @return [Manga], null if not found | ||||
|      */ | ||||
|     internal fun getMangaFromDatabase(manga: Manga): Manga? = | ||||
|         databaseHelper.getManga(manga.url, manga.source).executeAsBlocking() | ||||
|         db.getManga(manga.url, manga.source).executeAsBlocking() | ||||
|  | ||||
|     /** | ||||
|      * Fetches chapter information. | ||||
| @@ -42,7 +43,7 @@ abstract class AbstractBackupManager(protected val context: Context) { | ||||
|     internal suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>): Pair<List<Chapter>, List<Chapter>> { | ||||
|         val fetchedChapters = source.getChapterList(manga.toMangaInfo()) | ||||
|             .map { it.toSChapter() } | ||||
|         val syncedChapters = syncChaptersWithSource(databaseHelper, fetchedChapters, manga, source) | ||||
|         val syncedChapters = syncChaptersWithSource(db, fetchedChapters, manga, source) | ||||
|         if (syncedChapters.first.isNotEmpty()) { | ||||
|             chapters.forEach { it.manga_id = manga.id } | ||||
|             updateChapters(chapters) | ||||
| @@ -56,7 +57,7 @@ abstract class AbstractBackupManager(protected val context: Context) { | ||||
|      * @return [Manga] from library | ||||
|      */ | ||||
|     protected fun getFavoriteManga(): List<Manga> = | ||||
|         databaseHelper.getFavoriteMangas().executeAsBlocking() | ||||
|         db.getFavoriteMangas().executeAsBlocking() | ||||
|  | ||||
|     /** | ||||
|      * Inserts manga and returns id | ||||
| @@ -64,27 +65,27 @@ abstract class AbstractBackupManager(protected val context: Context) { | ||||
|      * @return id of [Manga], null if not found | ||||
|      */ | ||||
|     internal fun insertManga(manga: Manga): Long? = | ||||
|         databaseHelper.insertManga(manga).executeAsBlocking().insertedId() | ||||
|         db.insertManga(manga).executeAsBlocking().insertedId() | ||||
|  | ||||
|     /** | ||||
|      * Inserts list of chapters | ||||
|      */ | ||||
|     protected fun insertChapters(chapters: List<Chapter>) { | ||||
|         databaseHelper.insertChapters(chapters).executeAsBlocking() | ||||
|         db.insertChapters(chapters).executeAsBlocking() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Updates a list of chapters | ||||
|      */ | ||||
|     protected fun updateChapters(chapters: List<Chapter>) { | ||||
|         databaseHelper.updateChaptersBackup(chapters).executeAsBlocking() | ||||
|         db.updateChaptersBackup(chapters).executeAsBlocking() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Updates a list of chapters with known database ids | ||||
|      */ | ||||
|     protected fun updateKnownChapters(chapters: List<Chapter>) { | ||||
|         databaseHelper.updateKnownChaptersBackup(chapters).executeAsBlocking() | ||||
|         db.updateKnownChaptersBackup(chapters).executeAsBlocking() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|   | ||||
| @@ -2,17 +2,9 @@ package eu.kanade.tachiyomi.data.backup | ||||
|  | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import uy.kohesive.injekt.injectLazy | ||||
|  | ||||
| abstract class AbstractBackupRestoreValidator { | ||||
|     protected val sourceManager: SourceManager by injectLazy() | ||||
|     protected val trackManager: TrackManager by injectLazy() | ||||
|  | ||||
|     abstract fun validate(context: Context, uri: Uri): Results | ||||
|  | ||||
|     data class Results(val missingSources: List<String>, val missingTrackers: List<String>) | ||||
| } | ||||
|  | ||||
| class ValidatorParseException(e: Exception) : RuntimeException(e) | ||||
|   | ||||
| @@ -6,11 +6,6 @@ object BackupConst { | ||||
|  | ||||
|     private const val NAME = "BackupRestoreServices" | ||||
|     const val EXTRA_URI = "$ID.$NAME.EXTRA_URI" | ||||
|     const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS" | ||||
|     const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE" | ||||
|  | ||||
|     const val BACKUP_TYPE_LEGACY = 0 | ||||
|     const val BACKUP_TYPE_FULL = 1 | ||||
|  | ||||
|     // Filter options | ||||
|     internal const val BACKUP_CATEGORY = 0x1 | ||||
|   | ||||
| @@ -9,7 +9,6 @@ import android.os.PowerManager | ||||
| import androidx.core.content.ContextCompat | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.backup.full.FullBackupRestore | ||||
| import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore | ||||
| import eu.kanade.tachiyomi.data.notification.Notifications | ||||
| import eu.kanade.tachiyomi.util.system.acquireWakeLock | ||||
| import eu.kanade.tachiyomi.util.system.isServiceRunning | ||||
| @@ -44,11 +43,10 @@ class BackupRestoreService : Service() { | ||||
|          * @param context context of application | ||||
|          * @param uri path of Uri | ||||
|          */ | ||||
|         fun start(context: Context, uri: Uri, mode: Int) { | ||||
|         fun start(context: Context, uri: Uri) { | ||||
|             if (!isRunning(context)) { | ||||
|                 val intent = Intent(context, BackupRestoreService::class.java).apply { | ||||
|                     putExtra(BackupConst.EXTRA_URI, uri) | ||||
|                     putExtra(BackupConst.EXTRA_MODE, mode) | ||||
|                 } | ||||
|                 ContextCompat.startForegroundService(context, intent) | ||||
|             } | ||||
| @@ -118,15 +116,11 @@ class BackupRestoreService : Service() { | ||||
|      */ | ||||
|     override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { | ||||
|         val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY | ||||
|         val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL) | ||||
|  | ||||
|         // Cancel any previous job if needed. | ||||
|         backupRestore?.job?.cancel() | ||||
|  | ||||
|         backupRestore = when (mode) { | ||||
|             BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier) | ||||
|             else -> LegacyBackupRestore(this, notifier) | ||||
|         } | ||||
|         backupRestore = FullBackupRestore(this, notifier) | ||||
|  | ||||
|         val handler = CoroutineExceptionHandler { _, exception -> | ||||
|             logcat(LogPriority.ERROR, exception) | ||||
|   | ||||
| @@ -50,7 +50,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|         // Create root object | ||||
|         var backup: Backup? = null | ||||
|  | ||||
|         databaseHelper.inTransaction { | ||||
|         db.inTransaction { | ||||
|             val databaseManga = getFavoriteManga() | ||||
|  | ||||
|             backup = Backup( | ||||
| @@ -136,7 +136,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|     private fun backupCategories(options: Int): List<BackupCategory> { | ||||
|         // Check if user wants category information in backup | ||||
|         return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { | ||||
|             databaseHelper.getCategories() | ||||
|             db.getCategories() | ||||
|                 .executeAsBlocking() | ||||
|                 .map { BackupCategory.copyFrom(it) } | ||||
|         } else { | ||||
| @@ -158,7 +158,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|         // Check if user wants chapter information in backup | ||||
|         if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { | ||||
|             // Backup all the chapters | ||||
|             val chapters = databaseHelper.getChapters(manga).executeAsBlocking() | ||||
|             val chapters = db.getChapters(manga).executeAsBlocking() | ||||
|             if (chapters.isNotEmpty()) { | ||||
|                 mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) } | ||||
|             } | ||||
| @@ -167,7 +167,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|         // Check if user wants category information in backup | ||||
|         if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { | ||||
|             // Backup categories for this manga | ||||
|             val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking() | ||||
|             val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking() | ||||
|             if (categoriesForManga.isNotEmpty()) { | ||||
|                 mangaObject.categories = categoriesForManga.mapNotNull { it.order } | ||||
|             } | ||||
| @@ -175,7 +175,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|  | ||||
|         // Check if user wants track information in backup | ||||
|         if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { | ||||
|             val tracks = databaseHelper.getTracks(manga).executeAsBlocking() | ||||
|             val tracks = db.getTracks(manga).executeAsBlocking() | ||||
|             if (tracks.isNotEmpty()) { | ||||
|                 mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) } | ||||
|             } | ||||
| @@ -183,10 +183,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|  | ||||
|         // Check if user wants history information in backup | ||||
|         if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { | ||||
|             val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking() | ||||
|             val historyForManga = db.getHistoryByMangaId(manga.id!!).executeAsBlocking() | ||||
|             if (historyForManga.isNotEmpty()) { | ||||
|                 val history = historyForManga.mapNotNull { history -> | ||||
|                     val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url | ||||
|                     val url = db.getChapter(history.chapter_id).executeAsBlocking()?.url | ||||
|                     url?.let { BackupHistory(url, history.last_read) } | ||||
|                 } | ||||
|                 if (history.isNotEmpty()) { | ||||
| @@ -224,7 +224,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|      */ | ||||
|     internal fun restoreCategories(backupCategories: List<BackupCategory>) { | ||||
|         // Get categories from file and from db | ||||
|         val dbCategories = databaseHelper.getCategories().executeAsBlocking() | ||||
|         val dbCategories = db.getCategories().executeAsBlocking() | ||||
|  | ||||
|         // Iterate over them | ||||
|         backupCategories.map { it.getCategoryImpl() }.forEach { category -> | ||||
| @@ -244,7 +244,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|             if (!found) { | ||||
|                 // Let the db assign the id | ||||
|                 category.id = null | ||||
|                 val result = databaseHelper.insertCategory(category).executeAsBlocking() | ||||
|                 val result = db.insertCategory(category).executeAsBlocking() | ||||
|                 category.id = result.insertedId()?.toInt() | ||||
|             } | ||||
|         } | ||||
| @@ -257,7 +257,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|      * @param categories the categories to restore. | ||||
|      */ | ||||
|     internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) { | ||||
|         val dbCategories = databaseHelper.getCategories().executeAsBlocking() | ||||
|         val dbCategories = db.getCategories().executeAsBlocking() | ||||
|         val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size) | ||||
|         categories.forEach { backupCategoryOrder -> | ||||
|             backupCategories.firstOrNull { | ||||
| @@ -273,8 +273,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|  | ||||
|         // Update database | ||||
|         if (mangaCategoriesToUpdate.isNotEmpty()) { | ||||
|             databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking() | ||||
|             databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking() | ||||
|             db.deleteOldMangasCategories(listOf(manga)).executeAsBlocking() | ||||
|             db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -287,7 +287,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|         // List containing history to be updated | ||||
|         val historyToBeUpdated = ArrayList<History>(history.size) | ||||
|         for ((url, lastRead) in history) { | ||||
|             val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking() | ||||
|             val dbHistory = db.getHistoryByChapterUrl(url).executeAsBlocking() | ||||
|             // Check if history already in database and update | ||||
|             if (dbHistory != null) { | ||||
|                 dbHistory.apply { | ||||
| @@ -296,7 +296,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|                 historyToBeUpdated.add(dbHistory) | ||||
|             } else { | ||||
|                 // If not in database create | ||||
|                 databaseHelper.getChapter(url).executeAsBlocking()?.let { | ||||
|                 db.getChapter(url).executeAsBlocking()?.let { | ||||
|                     val historyToAdd = History.create(it).apply { | ||||
|                         last_read = lastRead | ||||
|                     } | ||||
| @@ -304,7 +304,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() | ||||
|         db.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
| @@ -318,7 +318,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|         tracks.map { it.manga_id = manga.id!! } | ||||
|  | ||||
|         // Get tracks from database | ||||
|         val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking() | ||||
|         val dbTracks = db.getTracks(manga).executeAsBlocking() | ||||
|         val trackToUpdate = mutableListOf<Track>() | ||||
|  | ||||
|         tracks.forEach { track -> | ||||
| @@ -346,12 +346,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { | ||||
|         } | ||||
|         // Update database | ||||
|         if (trackToUpdate.isNotEmpty()) { | ||||
|             databaseHelper.insertTracks(trackToUpdate).executeAsBlocking() | ||||
|             db.insertTracks(trackToUpdate).executeAsBlocking() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) { | ||||
|         val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() | ||||
|         val dbChapters = db.getChapters(manga).executeAsBlocking() | ||||
|  | ||||
|         chapters.forEach { chapter -> | ||||
|             val dbChapter = dbChapters.find { it.url == chapter.url } | ||||
|   | ||||
| @@ -34,8 +34,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa | ||||
|         } | ||||
|  | ||||
|         // Store source mapping for error messages | ||||
|         var backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources | ||||
|         sourceMapping = backupMaps.map { it.sourceId to it.name }.toMap() | ||||
|         val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources | ||||
|         sourceMapping = backupMaps.associate { it.sourceId to it.name } | ||||
|  | ||||
|         // Restore individual manga | ||||
|         backup.backupManga.forEach { | ||||
|   | ||||
| @@ -4,14 +4,20 @@ import android.content.Context | ||||
| import android.net.Uri | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator | ||||
| import eu.kanade.tachiyomi.data.backup.ValidatorParseException | ||||
| import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer | ||||
| import eu.kanade.tachiyomi.data.track.TrackManager | ||||
| import eu.kanade.tachiyomi.source.SourceManager | ||||
| import okio.buffer | ||||
| import okio.gzip | ||||
| import okio.source | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class FullBackupRestoreValidator : AbstractBackupRestoreValidator() { | ||||
|  | ||||
|     private val sourceManager: SourceManager = Injekt.get() | ||||
|     private val trackManager: TrackManager = Injekt.get() | ||||
|  | ||||
|     /** | ||||
|      * Checks for critical backup file data. | ||||
|      * | ||||
| @@ -27,11 +33,11 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() { | ||||
|                     .use { it.readByteArray() } | ||||
|             backupManager.parser.decodeFromByteArray(BackupSerializer, backupString) | ||||
|         } catch (e: Exception) { | ||||
|             throw ValidatorParseException(e) | ||||
|             throw IllegalStateException(e) | ||||
|         } | ||||
|  | ||||
|         if (backup.backupManga.isEmpty()) { | ||||
|             throw Exception(context.getString(R.string.invalid_backup_file_missing_manga)) | ||||
|             throw IllegalStateException(context.getString(R.string.invalid_backup_file_missing_manga)) | ||||
|         } | ||||
|  | ||||
|         val sources = backup.backupSources.associate { it.sourceId to it.name } | ||||
|   | ||||
| @@ -1,252 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.backup.legacy | ||||
|  | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import eu.kanade.tachiyomi.data.backup.AbstractBackupManager | ||||
| 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.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.History | ||||
| import eu.kanade.tachiyomi.data.database.models.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.MangaCategory | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| 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: 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") | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Create backup Json file from database | ||||
|      * | ||||
|      * @param uri path of Uri | ||||
|      * @param isAutoBackup backup called from scheduled backup job | ||||
|      */ | ||||
|     override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean) = | ||||
|         throw IllegalStateException("Legacy backup creation is not supported") | ||||
|  | ||||
|     fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) { | ||||
|         manga.id = dbManga.id | ||||
|         manga.copyFrom(dbManga) | ||||
|         manga.favorite = true | ||||
|         insertManga(manga) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fetches manga information | ||||
|      * | ||||
|      * @param source source of manga | ||||
|      * @param manga manga that needs updating | ||||
|      * @return Updated manga. | ||||
|      */ | ||||
|     suspend fun fetchManga(source: Source, manga: Manga): Manga { | ||||
|         val networkManga = source.getMangaDetails(manga.toMangaInfo()) | ||||
|         return manga.also { | ||||
|             it.copyFrom(networkManga.toSManga()) | ||||
|             it.favorite = true | ||||
|             it.initialized = true | ||||
|             it.id = insertManga(manga) | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Restore the categories from Json | ||||
|      * | ||||
|      * @param backupCategories array containing categories | ||||
|      */ | ||||
|     internal fun restoreCategories(backupCategories: List<Category>) { | ||||
|         // Get categories from file and from db | ||||
|         val dbCategories = databaseHelper.getCategories().executeAsBlocking() | ||||
|  | ||||
|         // Iterate over them | ||||
|         backupCategories.forEach { category -> | ||||
|             // Used to know if the category is already in the db | ||||
|             var found = false | ||||
|             for (dbCategory in dbCategories) { | ||||
|                 // If the category is already in the db, assign the id to the file's category | ||||
|                 // and do nothing | ||||
|                 if (category.name == dbCategory.name) { | ||||
|                     category.id = dbCategory.id | ||||
|                     found = true | ||||
|                     break | ||||
|                 } | ||||
|             } | ||||
|             // If the category isn't in the db, remove the id and insert a new category | ||||
|             // Store the inserted id in the category | ||||
|             if (!found) { | ||||
|                 // Let the db assign the id | ||||
|                 category.id = null | ||||
|                 val result = databaseHelper.insertCategory(category).executeAsBlocking() | ||||
|                 category.id = result.insertedId()?.toInt() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Restores the categories a manga is in. | ||||
|      * | ||||
|      * @param manga the manga whose categories have to be restored. | ||||
|      * @param categories the categories to restore. | ||||
|      */ | ||||
|     internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) { | ||||
|         val dbCategories = databaseHelper.getCategories().executeAsBlocking() | ||||
|         val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size) | ||||
|         for (backupCategoryStr in categories) { | ||||
|             for (dbCategory in dbCategories) { | ||||
|                 if (backupCategoryStr == dbCategory.name) { | ||||
|                     mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory)) | ||||
|                     break | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         // Update database | ||||
|         if (mangaCategoriesToUpdate.isNotEmpty()) { | ||||
|             databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking() | ||||
|             databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Restore history from Json | ||||
|      * | ||||
|      * @param history list containing history to be restored | ||||
|      */ | ||||
|     internal fun restoreHistoryForManga(history: List<DHistory>) { | ||||
|         // List containing history to be updated | ||||
|         val historyToBeUpdated = ArrayList<History>(history.size) | ||||
|         for ((url, lastRead) in history) { | ||||
|             val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking() | ||||
|             // Check if history already in database and update | ||||
|             if (dbHistory != null) { | ||||
|                 dbHistory.apply { | ||||
|                     last_read = max(lastRead, dbHistory.last_read) | ||||
|                 } | ||||
|                 historyToBeUpdated.add(dbHistory) | ||||
|             } else { | ||||
|                 // If not in database create | ||||
|                 databaseHelper.getChapter(url).executeAsBlocking()?.let { | ||||
|                     val historyToAdd = History.create(it).apply { | ||||
|                         last_read = lastRead | ||||
|                     } | ||||
|                     historyToBeUpdated.add(historyToAdd) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Restores the sync of a manga. | ||||
|      * | ||||
|      * @param manga the manga whose sync have to be restored. | ||||
|      * @param tracks the track list to restore. | ||||
|      */ | ||||
|     internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) { | ||||
|         // Get tracks from database | ||||
|         val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking() | ||||
|         val trackToUpdate = ArrayList<Track>(tracks.size) | ||||
|  | ||||
|         tracks.forEach { track -> | ||||
|             // Fix foreign keys with the current manga id | ||||
|             track.manga_id = manga.id!! | ||||
|  | ||||
|             val service = trackManager.getService(track.sync_id) | ||||
|             if (service != null && service.isLogged) { | ||||
|                 var isInDatabase = false | ||||
|                 for (dbTrack in dbTracks) { | ||||
|                     if (track.sync_id == dbTrack.sync_id) { | ||||
|                         // The sync is already in the db, only update its fields | ||||
|                         if (track.media_id != dbTrack.media_id) { | ||||
|                             dbTrack.media_id = track.media_id | ||||
|                         } | ||||
|                         if (track.library_id != dbTrack.library_id) { | ||||
|                             dbTrack.library_id = track.library_id | ||||
|                         } | ||||
|                         dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read) | ||||
|                         isInDatabase = true | ||||
|                         trackToUpdate.add(dbTrack) | ||||
|                         break | ||||
|                     } | ||||
|                 } | ||||
|                 if (!isInDatabase) { | ||||
|                     // Insert new sync. Let the db assign the id | ||||
|                     track.id = null | ||||
|                     trackToUpdate.add(track) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         // Update database | ||||
|         if (trackToUpdate.isNotEmpty()) { | ||||
|             databaseHelper.insertTracks(trackToUpdate).executeAsBlocking() | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Restore the chapters for manga if chapters already in database | ||||
|      * | ||||
|      * @param manga manga of chapters | ||||
|      * @param chapters list containing chapters that get restored | ||||
|      * @return boolean answering if chapter fetch is not needed | ||||
|      */ | ||||
|     internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean { | ||||
|         val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() | ||||
|  | ||||
|         // Return if fetch is needed | ||||
|         if (dbChapters.isEmpty() || dbChapters.size < chapters.size) { | ||||
|             return false | ||||
|         } | ||||
|  | ||||
|         for (chapter in chapters) { | ||||
|             val pos = dbChapters.indexOf(chapter) | ||||
|             if (pos != -1) { | ||||
|                 val dbChapter = dbChapters[pos] | ||||
|                 chapter.id = dbChapter.id | ||||
|                 chapter.copyFrom(dbChapter) | ||||
|                 break | ||||
|             } | ||||
|  | ||||
|             chapter.manga_id = manga.id | ||||
|         } | ||||
|  | ||||
|         // Filter the chapters that couldn't be found. | ||||
|         updateChapters(chapters.filter { it.id != null }) | ||||
|  | ||||
|         return true | ||||
|     } | ||||
| } | ||||
| @@ -1,184 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.backup.legacy | ||||
|  | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| 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.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.Manga | ||||
| import eu.kanade.tachiyomi.data.database.models.Track | ||||
| import eu.kanade.tachiyomi.source.Source | ||||
| import kotlinx.serialization.json.Json | ||||
| import kotlinx.serialization.json.JsonObject | ||||
| import kotlinx.serialization.json.decodeFromJsonElement | ||||
| import kotlinx.serialization.json.decodeFromStream | ||||
| import kotlinx.serialization.json.intOrNull | ||||
| import kotlinx.serialization.json.jsonPrimitive | ||||
| import java.util.Date | ||||
|  | ||||
| class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) { | ||||
|  | ||||
|     override suspend fun performRestore(uri: Uri): Boolean { | ||||
|         // Read the json and create a Json Object, | ||||
|         // cannot use the backupManager json deserializer one because its not initialized yet | ||||
|         val backupObject = Json.decodeFromStream<JsonObject>( | ||||
|             context.contentResolver.openInputStream(uri)!!, | ||||
|         ) | ||||
|  | ||||
|         // Get parser version | ||||
|         val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1 | ||||
|  | ||||
|         // Initialize manager | ||||
|         backupManager = LegacyBackupManager(context, version) | ||||
|  | ||||
|         // 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 | ||||
|         backup.categories?.let { restoreCategories(it) } | ||||
|  | ||||
|         // Store source mapping for error messages | ||||
|         sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList()) | ||||
|  | ||||
|         // Restore individual manga | ||||
|         backup.mangas.forEach { | ||||
|             if (job?.isActive != true) { | ||||
|                 return false | ||||
|             } | ||||
|  | ||||
|             restoreManga(it) | ||||
|         } | ||||
|  | ||||
|         return true | ||||
|     } | ||||
|  | ||||
|     private fun restoreCategories(categoriesJson: List<Category>) { | ||||
|         db.inTransaction { | ||||
|             backupManager.restoreCategories(categoriesJson) | ||||
|         } | ||||
|  | ||||
|         restoreProgress += 1 | ||||
|         showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories)) | ||||
|     } | ||||
|  | ||||
|     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() | ||||
|  | ||||
|         try { | ||||
|             if (source != null) { | ||||
|                 restoreMangaData(manga, source, chapters, categories, history, tracks) | ||||
|             } else { | ||||
|                 errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}") | ||||
|             } | ||||
|         } catch (e: Exception) { | ||||
|             errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") | ||||
|         } | ||||
|  | ||||
|         restoreProgress += 1 | ||||
|         showRestoreProgress(restoreProgress, restoreAmount, manga.title) | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Returns a manga restore observable | ||||
|      * | ||||
|      * @param manga manga data from json | ||||
|      * @param source source to get manga data from | ||||
|      * @param chapters chapters data from json | ||||
|      * @param categories categories data from json | ||||
|      * @param history history data from json | ||||
|      * @param tracks tracking data from json | ||||
|      */ | ||||
|     private suspend fun restoreMangaData( | ||||
|         manga: Manga, | ||||
|         source: Source, | ||||
|         chapters: List<Chapter>, | ||||
|         categories: List<String>, | ||||
|         history: List<DHistory>, | ||||
|         tracks: List<Track>, | ||||
|     ) { | ||||
|         val dbManga = backupManager.getMangaFromDatabase(manga) | ||||
|  | ||||
|         db.inTransaction { | ||||
|             if (dbManga == null) { | ||||
|                 // Manga not in database | ||||
|                 restoreMangaFetch(source, manga, chapters, categories, history, tracks) | ||||
|             } else { // Manga in database | ||||
|                 // Copy information from manga already in database | ||||
|                 backupManager.restoreMangaNoFetch(manga, dbManga) | ||||
|                 // Fetch rest of manga information | ||||
|                 restoreMangaNoFetch(source, manga, chapters, categories, history, tracks) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * Fetches manga information. | ||||
|      * | ||||
|      * @param manga manga that needs updating | ||||
|      * @param chapters chapters of manga that needs updating | ||||
|      * @param categories categories that need updating | ||||
|      */ | ||||
|     private suspend fun restoreMangaFetch( | ||||
|         source: Source, | ||||
|         manga: Manga, | ||||
|         chapters: List<Chapter>, | ||||
|         categories: List<String>, | ||||
|         history: List<DHistory>, | ||||
|         tracks: List<Track>, | ||||
|     ) { | ||||
|         try { | ||||
|             val fetchedManga = backupManager.fetchManga(source, manga) | ||||
|             fetchedManga.id ?: return | ||||
|  | ||||
|             updateChapters(source, fetchedManga, chapters) | ||||
|  | ||||
|             restoreExtraForManga(fetchedManga, categories, history, tracks) | ||||
|  | ||||
|             updateTracking(fetchedManga, tracks) | ||||
|         } catch (e: Exception) { | ||||
|             errors.add(Date() to "${manga.title} - ${e.message}") | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private suspend fun restoreMangaNoFetch( | ||||
|         source: Source, | ||||
|         backupManga: Manga, | ||||
|         chapters: List<Chapter>, | ||||
|         categories: List<String>, | ||||
|         history: List<DHistory>, | ||||
|         tracks: List<Track>, | ||||
|     ) { | ||||
|         if (!backupManager.restoreChaptersForManga(backupManga, chapters)) { | ||||
|             updateChapters(source, backupManga, chapters) | ||||
|         } | ||||
|  | ||||
|         restoreExtraForManga(backupManga, categories, history, tracks) | ||||
|  | ||||
|         updateTracking(backupManga, tracks) | ||||
|     } | ||||
|  | ||||
|     private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) { | ||||
|         // Restore categories | ||||
|         backupManager.restoreCategoriesForManga(manga, categories) | ||||
|  | ||||
|         // Restore history | ||||
|         backupManager.restoreHistoryForManga(history) | ||||
|  | ||||
|         // Restore tracking | ||||
|         backupManager.restoreTrackForManga(manga, tracks) | ||||
|     } | ||||
| } | ||||
| @@ -1,66 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.backup.legacy | ||||
|  | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator | ||||
| import eu.kanade.tachiyomi.data.backup.ValidatorParseException | ||||
| import eu.kanade.tachiyomi.data.backup.legacy.models.Backup | ||||
| import kotlinx.serialization.json.decodeFromStream | ||||
|  | ||||
| class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() { | ||||
|  | ||||
|     /** | ||||
|      * Checks for critical backup file data. | ||||
|      * | ||||
|      * @throws Exception if version or manga cannot be found. | ||||
|      * @return List of missing sources or missing trackers. | ||||
|      */ | ||||
|     override fun validate(context: Context, uri: Uri): Results { | ||||
|         val backupManager = LegacyBackupManager(context) | ||||
|  | ||||
|         val backup = try { | ||||
|             backupManager.parser.decodeFromStream<Backup>( | ||||
|                 context.contentResolver.openInputStream(uri)!!, | ||||
|             ) | ||||
|         } catch (e: Exception) { | ||||
|             throw ValidatorParseException(e) | ||||
|         } | ||||
|  | ||||
|         if (backup.version == null) { | ||||
|             throw Exception(context.getString(R.string.invalid_backup_file_missing_data)) | ||||
|         } | ||||
|  | ||||
|         if (backup.mangas.isEmpty()) { | ||||
|             throw Exception(context.getString(R.string.invalid_backup_file_missing_manga)) | ||||
|         } | ||||
|  | ||||
|         val sources = getSourceMapping(backup.extensions ?: emptyList()) | ||||
|         val missingSources = sources | ||||
|             .filter { sourceManager.get(it.key) == null } | ||||
|             .values | ||||
|             .sorted() | ||||
|  | ||||
|         val trackers = backup.mangas | ||||
|             .filterNot { it.track.isNullOrEmpty() } | ||||
|             .flatMap { it.track ?: emptyList() } | ||||
|             .map { it.sync_id } | ||||
|             .distinct() | ||||
|         val missingTrackers = trackers | ||||
|             .mapNotNull { trackManager.getService(it) } | ||||
|             .filter { !it.isLogged } | ||||
|             .map { context.getString(it.nameRes()) } | ||||
|             .sorted() | ||||
|  | ||||
|         return Results(missingSources, missingTrackers) | ||||
|     } | ||||
|  | ||||
|     companion object { | ||||
|         fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> { | ||||
|             return extensionsMapping.associate { | ||||
|                 val items = it.split(":") | ||||
|                 items[0].toLong() to items[1] | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,37 +0,0 @@ | ||||
| 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 | ||||
|  | ||||
| @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 | ||||
|  | ||||
|         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,3 +0,0 @@ | ||||
| package eu.kanade.tachiyomi.data.backup.legacy.models | ||||
|  | ||||
| data class DHistory(val url: String, val lastRead: Long) | ||||
| @@ -1,49 +0,0 @@ | ||||
| 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,66 +0,0 @@ | ||||
| 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,41 +0,0 @@ | ||||
| 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,56 +0,0 @@ | ||||
| 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,68 +0,0 @@ | ||||
| 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.float | ||||
| 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.long | ||||
|             library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long | ||||
|             last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.float | ||||
|             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>() | ||||
| @@ -29,12 +29,4 @@ object TrackTable { | ||||
|     const val COL_START_DATE = "start_date" | ||||
|  | ||||
|     const val COL_FINISH_DATE = "finish_date" | ||||
|  | ||||
|     val insertFromTempTable: String | ||||
|         get() = | ||||
|             """ | ||||
|             |INSERT INTO $TABLE($COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE) | ||||
|             |SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE | ||||
|             |FROM ${TABLE}_tmp | ||||
|             """.trimMargin() | ||||
| } | ||||
|   | ||||
| @@ -22,10 +22,8 @@ import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.data.backup.BackupConst | ||||
| import eu.kanade.tachiyomi.data.backup.BackupCreatorJob | ||||
| import eu.kanade.tachiyomi.data.backup.BackupRestoreService | ||||
| import eu.kanade.tachiyomi.data.backup.ValidatorParseException | ||||
| import eu.kanade.tachiyomi.data.backup.full.FullBackupRestoreValidator | ||||
| import eu.kanade.tachiyomi.data.backup.full.models.BackupFull | ||||
| import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestoreValidator | ||||
| import eu.kanade.tachiyomi.ui.base.controller.DialogController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe | ||||
| import eu.kanade.tachiyomi.util.preference.bindTo | ||||
| @@ -272,19 +270,9 @@ class SettingsBackupController : SettingsController() { | ||||
|             val uri: Uri = args.getParcelable(KEY_URI)!! | ||||
|  | ||||
|             return try { | ||||
|                 var type = BackupConst.BACKUP_TYPE_FULL | ||||
|                 val results = try { | ||||
|                     FullBackupRestoreValidator().validate(activity, uri) | ||||
|                 } catch (_: ValidatorParseException) { | ||||
|                     type = BackupConst.BACKUP_TYPE_LEGACY | ||||
|                     LegacyBackupRestoreValidator().validate(activity, uri) | ||||
|                 } | ||||
|                 val results = FullBackupRestoreValidator().validate(activity, uri) | ||||
|  | ||||
|                 var message = if (type == BackupConst.BACKUP_TYPE_FULL) { | ||||
|                     activity.getString(R.string.backup_restore_content_full) | ||||
|                 } else { | ||||
|                     activity.getString(R.string.backup_restore_content) | ||||
|                 } | ||||
|                 var message = activity.getString(R.string.backup_restore_content_full) | ||||
|                 if (results.missingSources.isNotEmpty()) { | ||||
|                     message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}" | ||||
|                 } | ||||
| @@ -296,7 +284,7 @@ class SettingsBackupController : SettingsController() { | ||||
|                     .setTitle(R.string.pref_restore_backup) | ||||
|                     .setMessage(message) | ||||
|                     .setPositiveButton(R.string.action_restore) { _, _ -> | ||||
|                         BackupRestoreService.start(activity, uri, type) | ||||
|                         BackupRestoreService.start(activity, uri) | ||||
|                     } | ||||
|                     .create() | ||||
|             } catch (e: Exception) { | ||||
|   | ||||
| @@ -436,16 +436,13 @@ | ||||
|     <string name="pref_backup_service_category">Automatic backups</string> | ||||
|     <string name="pref_backup_interval">Backup frequency</string> | ||||
|     <string name="pref_backup_slots">Maximum backups</string> | ||||
|     <string name="source_not_found_name">Source not found: %1$s</string> | ||||
|     <string name="tracker_not_logged_in">Not logged in: %1$s</string> | ||||
|     <string name="backup_restore_invalid_uri">Error: empty URI</string> | ||||
|     <string name="backup_created">Backup created</string> | ||||
|     <string name="invalid_backup_file">Invalid backup file</string> | ||||
|     <string name="invalid_backup_file_missing_data">File is missing data.</string> | ||||
|     <string name="invalid_backup_file_missing_manga">Backup does not contain any manga.</string> | ||||
|     <string name="backup_restore_missing_sources">Missing sources:</string> | ||||
|     <string name="backup_restore_missing_trackers">Trackers not logged into:</string> | ||||
|     <string name="backup_restore_content">Restore uses sources to fetch data, carrier costs may apply.\n\nMake sure you have installed all necessary extensions and are logged in to sources and tracking services before restoring.</string> | ||||
|     <string name="backup_restore_content_full">Data from the backup file will be restored.\n\nYou will need to install any missing extensions and log in to tracking services afterwards to use them.</string> | ||||
|     <string name="restore_completed">Restore completed</string> | ||||
|     <string name="restore_duration">%02d min, %02d sec</string> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user