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

(cherry picked from commit d1be221d7aaa811e50417235021c7e038704d276)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/models/Backup.kt
This commit is contained in:
arkon 2022-05-29 12:24:39 -04:00 committed by Jobobby04
parent 2ae1b6ac3e
commit 6e85e69268
21 changed files with 60 additions and 1140 deletions

View File

@ -16,18 +16,19 @@ import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.online.all.EHentai import eu.kanade.tachiyomi.source.online.all.EHentai
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import exh.eh.EHentaiThrottleManager import exh.eh.EHentaiThrottleManager
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
abstract class AbstractBackupManager(protected val context: Context) { abstract class AbstractBackupManager(protected val context: Context) {
internal val databaseHelper: DatabaseHelper by injectLazy() internal val db: DatabaseHelper = Injekt.get()
internal val databaseHandler: DatabaseHandler by injectLazy() internal val database: DatabaseHandler = Injekt.get()
internal val sourceManager: SourceManager by injectLazy() internal val sourceManager: SourceManager = Injekt.get()
internal val trackManager: TrackManager by injectLazy() internal val trackManager: TrackManager = Injekt.get()
protected val preferences: PreferencesHelper by injectLazy() protected val preferences: PreferencesHelper = Injekt.get()
// SY --> // SY -->
protected val customMangaManager: CustomMangaManager by injectLazy() protected val customMangaManager: CustomMangaManager = Injekt.get()
// SY <-- // SY <--
abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String
@ -38,7 +39,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
* @return [Manga], null if not found * @return [Manga], null if not found
*/ */
internal fun getMangaFromDatabase(manga: Manga): Manga? = internal fun getMangaFromDatabase(manga: Manga): Manga? =
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking() db.getManga(manga.url, manga.source).executeAsBlocking()
/** /**
* Fetches chapter information. * Fetches chapter information.
@ -58,7 +59,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
.map { it.toSChapter() } .map { it.toSChapter() }
} }
// SY <-- // SY <--
val syncedChapters = syncChaptersWithSource(databaseHelper, fetchedChapters, manga, source) val syncedChapters = syncChaptersWithSource(db, fetchedChapters, manga, source)
if (syncedChapters.first.isNotEmpty()) { if (syncedChapters.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id } chapters.forEach { it.manga_id = manga.id }
updateChapters(chapters) updateChapters(chapters)
@ -72,11 +73,11 @@ abstract class AbstractBackupManager(protected val context: Context) {
* @return [Manga] from library * @return [Manga] from library
*/ */
protected fun getFavoriteManga(): List<Manga> = protected fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking() db.getFavoriteMangas().executeAsBlocking()
// SY --> // SY -->
protected fun getReadManga(): List<Manga> = protected fun getReadManga(): List<Manga> =
databaseHelper.getReadNotInLibraryMangas().executeAsBlocking() db.getReadNotInLibraryMangas().executeAsBlocking()
/** /**
* Returns list containing merged manga that are possibly not in the library * Returns list containing merged manga that are possibly not in the library
@ -84,7 +85,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
* @return merged [Manga] that are possibly not in the library * @return merged [Manga] that are possibly not in the library
*/ */
protected fun getMergedManga(): List<Manga> = protected fun getMergedManga(): List<Manga> =
databaseHelper.getMergedMangas().executeAsBlocking() db.getMergedMangas().executeAsBlocking()
// SY <-- // SY <--
/** /**
@ -93,27 +94,27 @@ abstract class AbstractBackupManager(protected val context: Context) {
* @return id of [Manga], null if not found * @return id of [Manga], null if not found
*/ */
internal fun insertManga(manga: Manga): Long? = internal fun insertManga(manga: Manga): Long? =
databaseHelper.insertManga(manga).executeAsBlocking().insertedId() db.insertManga(manga).executeAsBlocking().insertedId()
/** /**
* Inserts list of chapters * Inserts list of chapters
*/ */
protected fun insertChapters(chapters: List<Chapter>) { protected fun insertChapters(chapters: List<Chapter>) {
databaseHelper.insertChapters(chapters).executeAsBlocking() db.insertChapters(chapters).executeAsBlocking()
} }
/** /**
* Updates a list of chapters * Updates a list of chapters
*/ */
protected fun updateChapters(chapters: List<Chapter>) { protected fun updateChapters(chapters: List<Chapter>) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking() db.updateChaptersBackup(chapters).executeAsBlocking()
} }
/** /**
* Updates a list of chapters with known database ids * Updates a list of chapters with known database ids
*/ */
protected fun updateKnownChapters(chapters: List<Chapter>) { protected fun updateKnownChapters(chapters: List<Chapter>) {
databaseHelper.updateKnownChaptersBackup(chapters).executeAsBlocking() db.updateKnownChaptersBackup(chapters).executeAsBlocking()
} }
/** /**

View File

@ -2,17 +2,9 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.net.Uri 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 { abstract class AbstractBackupRestoreValidator {
protected val sourceManager: SourceManager by injectLazy()
protected val trackManager: TrackManager by injectLazy()
abstract fun validate(context: Context, uri: Uri): Results abstract fun validate(context: Context, uri: Uri): Results
data class Results(val missingSources: List<String>, val missingTrackers: List<String>) data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
} }
class ValidatorParseException(e: Exception) : RuntimeException(e)

View File

@ -6,11 +6,6 @@ object BackupConst {
private const val NAME = "BackupRestoreServices" private const val NAME = "BackupRestoreServices"
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI" 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 // Filter options
internal const val BACKUP_CATEGORY = 0x1 internal const val BACKUP_CATEGORY = 0x1

View File

@ -9,7 +9,6 @@ import android.os.PowerManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.full.FullBackupRestore 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.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
@ -44,11 +43,10 @@ class BackupRestoreService : Service() {
* @param context context of application * @param context context of application
* @param uri path of Uri * @param uri path of Uri
*/ */
fun start(context: Context, uri: Uri, mode: Int) { fun start(context: Context, uri: Uri) {
if (!isRunning(context)) { if (!isRunning(context)) {
val intent = Intent(context, BackupRestoreService::class.java).apply { val intent = Intent(context, BackupRestoreService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri) putExtra(BackupConst.EXTRA_URI, uri)
putExtra(BackupConst.EXTRA_MODE, mode)
} }
ContextCompat.startForegroundService(context, intent) ContextCompat.startForegroundService(context, intent)
} }
@ -118,15 +116,11 @@ class BackupRestoreService : Service() {
*/ */
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY 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. // Cancel any previous job if needed.
backupRestore?.job?.cancel() backupRestore?.job?.cancel()
backupRestore = when (mode) { backupRestore = FullBackupRestore(this, notifier)
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier)
else -> LegacyBackupRestore(this, notifier)
}
val handler = CoroutineExceptionHandler { _, exception -> val handler = CoroutineExceptionHandler { _, exception ->
logcat(LogPriority.ERROR, exception) logcat(LogPriority.ERROR, exception)

View File

@ -69,7 +69,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// Create root object // Create root object
var backup: Backup? = null var backup: Backup? = null
databaseHelper.inTransaction { db.inTransaction {
val databaseManga = getFavoriteManga() /* SY --> */ + if (flags and BACKUP_READ_MANGA_MASK == BACKUP_READ_MANGA) { val databaseManga = getFavoriteManga() /* SY --> */ + if (flags and BACKUP_READ_MANGA_MASK == BACKUP_READ_MANGA) {
getReadManga() getReadManga()
} else { } else {
@ -160,7 +160,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
private fun backupCategories(options: Int): List<BackupCategory> { private fun backupCategories(options: Int): List<BackupCategory> {
// Check if user wants category information in backup // Check if user wants category information in backup
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
databaseHelper.getCategories() db.getCategories()
.executeAsBlocking() .executeAsBlocking()
.map { BackupCategory.copyFrom(it) } .map { BackupCategory.copyFrom(it) }
} else { } else {
@ -201,7 +201,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// SY --> // SY -->
if (manga.source == MERGED_SOURCE_ID) { if (manga.source == MERGED_SOURCE_ID) {
manga.id?.let { mangaId -> manga.id?.let { mangaId ->
mangaObject.mergedMangaReferences = databaseHelper.getMergedMangaReferences(mangaId) mangaObject.mergedMangaReferences = db.getMergedMangaReferences(mangaId)
.executeAsBlocking() .executeAsBlocking()
.map { BackupMergedMangaReference.copyFrom(it) } .map { BackupMergedMangaReference.copyFrom(it) }
} }
@ -210,7 +210,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
val source = sourceManager.get(manga.source)?.getMainSource<MetadataSource<*, *>>() val source = sourceManager.get(manga.source)?.getMainSource<MetadataSource<*, *>>()
if (source != null) { if (source != null) {
manga.id?.let { mangaId -> manga.id?.let { mangaId ->
databaseHelper.getFlatMetadataForManga(mangaId).executeAsBlocking()?.let { flatMetadata -> db.getFlatMetadataForManga(mangaId).executeAsBlocking()?.let { flatMetadata ->
mangaObject.flatMetadata = BackupFlatMetadata.copyFrom(flatMetadata) mangaObject.flatMetadata = BackupFlatMetadata.copyFrom(flatMetadata)
} }
} }
@ -220,7 +220,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// Check if user wants chapter information in backup // Check if user wants chapter information in backup
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) { if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
// Backup all the chapters // Backup all the chapters
val chapters = databaseHelper.getChapters(manga).executeAsBlocking() val chapters = db.getChapters(manga).executeAsBlocking()
if (chapters.isNotEmpty()) { if (chapters.isNotEmpty()) {
mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) } mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) }
} }
@ -229,7 +229,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// Check if user wants category information in backup // Check if user wants category information in backup
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) { if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
// Backup categories for this manga // Backup categories for this manga
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking() val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
if (categoriesForManga.isNotEmpty()) { if (categoriesForManga.isNotEmpty()) {
mangaObject.categories = categoriesForManga.mapNotNull { it.order } mangaObject.categories = categoriesForManga.mapNotNull { it.order }
} }
@ -237,7 +237,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// Check if user wants track information in backup // Check if user wants track information in backup
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) { if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
val tracks = databaseHelper.getTracks(manga).executeAsBlocking() val tracks = db.getTracks(manga).executeAsBlocking()
if (tracks.isNotEmpty()) { if (tracks.isNotEmpty()) {
mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) } mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) }
} }
@ -245,10 +245,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// Check if user wants history information in backup // Check if user wants history information in backup
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) { 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()) { if (historyForManga.isNotEmpty()) {
val history = historyForManga.mapNotNull { history -> 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) } url?.let { BackupHistory(url, history.last_read) }
} }
if (history.isNotEmpty()) { if (history.isNotEmpty()) {
@ -286,7 +286,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*/ */
internal fun restoreCategories(backupCategories: List<BackupCategory>) { internal fun restoreCategories(backupCategories: List<BackupCategory>) {
// Get categories from file and from db // Get categories from file and from db
val dbCategories = databaseHelper.getCategories().executeAsBlocking() val dbCategories = db.getCategories().executeAsBlocking()
// Iterate over them // Iterate over them
backupCategories.map { it.getCategoryImpl() }.forEach { category -> backupCategories.map { it.getCategoryImpl() }.forEach { category ->
@ -306,7 +306,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
if (!found) { if (!found) {
// Let the db assign the id // Let the db assign the id
category.id = null category.id = null
val result = databaseHelper.insertCategory(category).executeAsBlocking() val result = db.insertCategory(category).executeAsBlocking()
category.id = result.insertedId()?.toInt() category.id = result.insertedId()?.toInt()
} }
} }
@ -319,7 +319,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @param categories the categories to restore. * @param categories the categories to restore.
*/ */
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) { 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) val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
categories.forEach { backupCategoryOrder -> categories.forEach { backupCategoryOrder ->
backupCategories.firstOrNull { backupCategories.firstOrNull {
@ -335,8 +335,8 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// Update database // Update database
if (mangaCategoriesToUpdate.isNotEmpty()) { if (mangaCategoriesToUpdate.isNotEmpty()) {
databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking() db.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking() db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
} }
} }
@ -349,7 +349,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// List containing history to be updated // List containing history to be updated
val historyToBeUpdated = ArrayList<History>(history.size) val historyToBeUpdated = ArrayList<History>(history.size)
for ((url, lastRead) in history) { 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 // Check if history already in database and update
if (dbHistory != null) { if (dbHistory != null) {
dbHistory.apply { dbHistory.apply {
@ -358,7 +358,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
historyToBeUpdated.add(dbHistory) historyToBeUpdated.add(dbHistory)
} else { } else {
// If not in database create // If not in database create
databaseHelper.getChapter(url).executeAsBlocking()?.let { db.getChapter(url).executeAsBlocking()?.let {
val historyToAdd = History.create(it).apply { val historyToAdd = History.create(it).apply {
last_read = lastRead last_read = lastRead
} }
@ -366,7 +366,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
} }
} }
} }
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() db.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
} }
/** /**
@ -380,7 +380,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
tracks.map { it.manga_id = manga.id!! } tracks.map { it.manga_id = manga.id!! }
// Get tracks from database // Get tracks from database
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking() val dbTracks = db.getTracks(manga).executeAsBlocking()
val trackToUpdate = mutableListOf<Track>() val trackToUpdate = mutableListOf<Track>()
tracks.forEach { track -> tracks.forEach { track ->
@ -408,12 +408,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
} }
// Update database // Update database
if (trackToUpdate.isNotEmpty()) { if (trackToUpdate.isNotEmpty()) {
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking() db.insertTracks(trackToUpdate).executeAsBlocking()
} }
} }
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) { internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking() val dbChapters = db.getChapters(manga).executeAsBlocking()
chapters.forEach { chapter -> chapters.forEach { chapter ->
val dbChapter = dbChapters.find { it.url == chapter.url } val dbChapter = dbChapters.find { it.url == chapter.url }
@ -441,11 +441,11 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// SY --> // SY -->
internal suspend fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) { internal suspend fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
val currentSavedSearches = databaseHandler.awaitList { val currentSavedSearches = database.awaitList {
saved_searchQueries.selectAll(savedSearchMapper) saved_searchQueries.selectAll(savedSearchMapper)
} }
databaseHandler.await(true) { database.await(true) {
backupSavedSearches.filter { backupSavedSearch -> backupSavedSearches.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.forEach { }.forEach {
@ -469,7 +469,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*/ */
internal fun restoreMergedMangaReferencesForManga(manga: Manga, backupMergedMangaReferences: List<BackupMergedMangaReference>) { internal fun restoreMergedMangaReferencesForManga(manga: Manga, backupMergedMangaReferences: List<BackupMergedMangaReference>) {
// Get merged manga references from file and from db // Get merged manga references from file and from db
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking() val dbMergedMangaReferences = db.getMergedMangaReferences().executeAsBlocking()
// Iterate over them // Iterate over them
backupMergedMangaReferences.forEach { backupMergedMangaReference -> backupMergedMangaReferences.forEach { backupMergedMangaReference ->
@ -487,11 +487,11 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
// Store the inserted id in the backupMergedMangaReference // Store the inserted id in the backupMergedMangaReference
if (!found) { if (!found) {
// Let the db assign the id // Let the db assign the id
val mergedManga = databaseHelper.getManga(backupMergedMangaReference.mangaUrl, backupMergedMangaReference.mangaSourceId).executeAsBlocking() ?: return@forEach val mergedManga = db.getManga(backupMergedMangaReference.mangaUrl, backupMergedMangaReference.mangaSourceId).executeAsBlocking() ?: return@forEach
val mergedMangaReference = backupMergedMangaReference.getMergedMangaReference() val mergedMangaReference = backupMergedMangaReference.getMergedMangaReference()
mergedMangaReference.mergeId = manga.id mergedMangaReference.mergeId = manga.id
mergedMangaReference.mangaId = mergedManga.id mergedMangaReference.mangaId = mergedManga.id
databaseHelper.insertMergedManga(mergedMangaReference).executeAsBlocking() db.insertMergedManga(mergedMangaReference).executeAsBlocking()
} }
} }
} }
@ -499,10 +499,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
internal fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) { internal fun restoreFlatMetadata(manga: Manga, backupFlatMetadata: BackupFlatMetadata) {
val mangaId = manga.id ?: return val mangaId = manga.id ?: return
launchIO { launchIO {
databaseHelper.getFlatMetadataForManga(mangaId).executeOnIO().let { db.getFlatMetadataForManga(mangaId).executeOnIO().let {
if (it == null) { if (it == null) {
val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId) val flatMetadata = backupFlatMetadata.getFlatMetadata(mangaId)
databaseHelper.insertFlatMetadataAsync(flatMetadata).await() db.insertFlatMetadataAsync(flatMetadata).await()
} }
} }
} }

View File

@ -49,8 +49,8 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
// SY <-- // SY <--
// Store source mapping for error messages // Store source mapping for error messages
var backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
sourceMapping = backupMaps.map { it.sourceId to it.name }.toMap() sourceMapping = backupMaps.associate { it.sourceId to it.name }
// Restore individual manga, sort by merged source so that merged source manga go last and merged references get the proper ids // Restore individual manga, sort by merged source so that merged source manga go last and merged references get the proper ids
backup.backupManga /* SY --> */.sortedBy { it.source == MERGED_SOURCE_ID } /* SY <-- */.forEach { backup.backupManga /* SY --> */.sortedBy { it.source == MERGED_SOURCE_ID } /* SY <-- */.forEach {

View File

@ -4,14 +4,20 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
import eu.kanade.tachiyomi.data.backup.ValidatorParseException
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer 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.buffer
import okio.gzip import okio.gzip
import okio.source import okio.source
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() { class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
private val sourceManager: SourceManager = Injekt.get()
private val trackManager: TrackManager = Injekt.get()
/** /**
* Checks for critical backup file data. * Checks for critical backup file data.
* *
@ -27,11 +33,11 @@ class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
.use { it.readByteArray() } .use { it.readByteArray() }
backupManager.parser.decodeFromByteArray(BackupSerializer, backupString) backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
} catch (e: Exception) { } catch (e: Exception) {
throw ValidatorParseException(e) throw IllegalStateException(e)
} }
if (backup.backupManga.isEmpty()) { 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 } val sources = backup.backupSources.associate { it.sourceId to it.name }

View File

@ -1,377 +0,0 @@
package eu.kanade.tachiyomi.data.backup.legacy
import android.content.Context
import android.net.Uri
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.tachiyomi.R
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.MergedMangaTypeSerializer
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 eu.kanade.tachiyomi.source.online.all.MergedSource
import exh.eh.EHentaiThrottleManager
import exh.merged.sql.models.MergedMangaReference
import exh.savedsearches.models.SavedSearch
import exh.source.MERGED_SOURCE_ID
import exh.util.nullIfBlank
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
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)
// SY -->
contextual(MergedMangaTypeSerializer)
// SY <--
}
}
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)
}
}
/**
* [Observable] that fetches chapter information
*
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
*/
override suspend fun restoreChapters(source: Source, manga: Manga, chapters: List<Chapter>, throttleManager: EHentaiThrottleManager): Pair<List<Chapter>, List<Chapter>> {
// SY -->
return if (source is MergedSource) {
val syncedChapters = source.fetchChaptersAndSync(manga, false)
syncedChapters.first.onEach {
it.manga_id = manga.id
}
updateChapters(syncedChapters.first)
syncedChapters
} else {
super.restoreChapters(source, manga, chapters, throttleManager)
}
}
/**
* 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
}
// SY -->
internal suspend fun restoreSavedSearches(jsonSavedSearches: String) {
val backupSavedSearches = jsonSavedSearches.split("***").toSet()
val currentSavedSearches = databaseHandler.awaitList {
saved_searchQueries.selectAll(savedSearchMapper)
}
databaseHandler.await(true) {
backupSavedSearches.mapNotNull {
runCatching {
val content = parser.decodeFromString<JsonObject>(it.substringAfter(':'))
SavedSearch(
id = null,
source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null,
content["name"]!!.jsonPrimitive.content,
content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
Json.encodeToString(content["filters"]!!.jsonArray),
)
}.getOrNull()
}.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.forEach {
saved_searchQueries.insertSavedSearch(
_id = null,
source = it.source,
name = it.name,
query = it.query.nullIfBlank(),
filters_json = it.filtersJson.nullIfBlank()
?.takeUnless { it == "[]" },
)
}
}
}
/**
* Restore the categories from Json
*
* @param backupMergedMangaReferences array containing md manga references
*/
internal fun restoreMergedMangaReferences(backupMergedMangaReferences: List<MergedMangaReference>) {
// Get merged manga references from file and from db
val dbMergedMangaReferences = databaseHelper.getMergedMangaReferences().executeAsBlocking()
var lastMergeManga: Manga? = null
// Iterate over them
backupMergedMangaReferences.forEach { mergedMangaReference ->
// Used to know if the merged manga reference is already in the db
var found = false
for (dbMergedMangaReference in dbMergedMangaReferences) {
// If the mergedMangaReference is already in the db, assign the id to the file's mergedMangaReference
// and do nothing
if (mergedMangaReference.mergeUrl == dbMergedMangaReference.mergeUrl && mergedMangaReference.mangaUrl == dbMergedMangaReference.mangaUrl) {
mergedMangaReference.id = dbMergedMangaReference.id
mergedMangaReference.mergeId = dbMergedMangaReference.mergeId
mergedMangaReference.mangaId = dbMergedMangaReference.mangaId
found = true
break
}
}
// If the mergedMangaReference isn't in the db, remove the id and insert a new mergedMangaReference
// Store the inserted id in the mergedMangaReference
if (!found) {
// Let the db assign the id
var mergedManga = if (mergedMangaReference.mergeUrl != lastMergeManga?.url) databaseHelper.getManga(mergedMangaReference.mergeUrl, MERGED_SOURCE_ID).executeAsBlocking() else lastMergeManga
if (mergedManga == null) {
mergedManga = Manga.create(MERGED_SOURCE_ID).apply {
url = mergedMangaReference.mergeUrl
title = context.getString(R.string.refresh_merge)
}
mergedManga.id = databaseHelper.insertManga(mergedManga).executeAsBlocking().insertedId()
}
val manga = databaseHelper.getManga(mergedMangaReference.mangaUrl, mergedMangaReference.mangaSourceId).executeAsBlocking() ?: return@forEach
lastMergeManga = mergedManga
mergedMangaReference.mergeId = mergedManga.id
mergedMangaReference.mangaId = manga.id
mergedMangaReference.id = null
val result = databaseHelper.insertMergedManga(mergedMangaReference).executeAsBlocking()
mergedMangaReference.id = result.insertedId()
}
}
}
// SY <--
}

View File

@ -1,218 +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 exh.EXHMigrations
import exh.merged.sql.models.MergedMangaReference
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 {
// SY -->
throttleManager.resetThrottle()
// SY <--
// 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 + 3 // +1 for categories, +1 for saved searches, +1 for merged manga references
// SY -->
backup.savedSearches?.let { restoreSavedSearches(it) }
backup.mergedMangaReferences?.let { restoreMergedMangaReferences(it) }
// SY <--
// 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
}
// SY -->
private suspend fun restoreSavedSearches(savedSearches: String) {
backupManager.restoreSavedSearches(savedSearches)
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.saved_searches))
}
private fun restoreMergedMangaReferences(mergedMangaReferences: List<MergedMangaReference>) {
db.inTransaction {
backupManager.restoreMergedMangaReferences(mergedMangaReferences)
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.merged_references))
}
// SY <--
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()
// EXH -->
EXHMigrations.migrateBackupEntry(manga)
// <-- EXH
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)
}
}

View File

@ -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]
}
}
}
}

View File

@ -1,43 +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 exh.merged.sql.models.MergedMangaReference
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
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,
// SY Specific values
@SerialName("mergedmangareferences")
var mergedMangaReferences: List<@Contextual MergedMangaReference>? = null,
var savedSearches: 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,
)

View File

@ -1,3 +0,0 @@
package eu.kanade.tachiyomi.data.backup.legacy.models
data class DHistory(val url: String, val lastRead: Long)

View File

@ -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>()

View File

@ -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>()

View File

@ -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,
)
}
}

View File

@ -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>()

View File

@ -1,58 +0,0 @@
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import exh.merged.sql.models.MergedMangaReference
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.boolean
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 [MergedMangaReference] to / from json
*/
object MergedMangaTypeSerializer : KSerializer<MergedMangaReference> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
override fun serialize(encoder: Encoder, value: MergedMangaReference) {
encoder as JsonEncoder
encoder.encodeJsonElement(
buildJsonArray {
add(value.mangaUrl)
add(value.mergeUrl)
add(value.mangaSourceId)
add(value.chapterSortMode)
add(value.chapterPriority)
add(value.getChapterUpdates)
add(value.isInfoManga)
add(value.downloadChapters)
},
)
}
override fun deserialize(decoder: Decoder): MergedMangaReference {
decoder as JsonDecoder
val array = decoder.decodeJsonElement().jsonArray
return MergedMangaReference(
id = null,
mangaUrl = array[0].jsonPrimitive.content,
mergeUrl = array[1].jsonPrimitive.content,
mangaSourceId = array[2].jsonPrimitive.long,
chapterSortMode = array[3].jsonPrimitive.int,
chapterPriority = array[4].jsonPrimitive.int,
getChapterUpdates = array[5].jsonPrimitive.boolean,
isInfoManga = array[6].jsonPrimitive.boolean,
downloadChapters = array[7].jsonPrimitive.boolean,
mangaId = null,
mergeId = null,
)
}
}

View File

@ -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>()

View File

@ -29,12 +29,4 @@ object TrackTable {
const val COL_START_DATE = "start_date" const val COL_START_DATE = "start_date"
const val COL_FINISH_DATE = "finish_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()
} }

View File

@ -22,10 +22,8 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupConst import eu.kanade.tachiyomi.data.backup.BackupConst
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.backup.BackupRestoreService 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.FullBackupRestoreValidator
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull 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.DialogController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.util.preference.bindTo import eu.kanade.tachiyomi.util.preference.bindTo
@ -280,19 +278,9 @@ class SettingsBackupController : SettingsController() {
val uri: Uri = args.getParcelable(KEY_URI)!! val uri: Uri = args.getParcelable(KEY_URI)!!
return try { return try {
var type = BackupConst.BACKUP_TYPE_FULL val results = FullBackupRestoreValidator().validate(activity, uri)
val results = try {
FullBackupRestoreValidator().validate(activity, uri)
} catch (_: ValidatorParseException) {
type = BackupConst.BACKUP_TYPE_LEGACY
LegacyBackupRestoreValidator().validate(activity, uri)
}
var message = if (type == BackupConst.BACKUP_TYPE_FULL) { var message = activity.getString(R.string.backup_restore_content_full)
activity.getString(R.string.backup_restore_content_full)
} else {
activity.getString(R.string.backup_restore_content)
}
if (results.missingSources.isNotEmpty()) { if (results.missingSources.isNotEmpty()) {
message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}" message += "\n\n${activity.getString(R.string.backup_restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}"
} }
@ -304,7 +292,7 @@ class SettingsBackupController : SettingsController() {
.setTitle(R.string.pref_restore_backup) .setTitle(R.string.pref_restore_backup)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.action_restore) { _, _ -> .setPositiveButton(R.string.action_restore) { _, _ ->
BackupRestoreService.start(activity, uri, type) BackupRestoreService.start(activity, uri)
} }
.create() .create()
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -436,16 +436,13 @@
<string name="pref_backup_service_category">Automatic backups</string> <string name="pref_backup_service_category">Automatic backups</string>
<string name="pref_backup_interval">Backup frequency</string> <string name="pref_backup_interval">Backup frequency</string>
<string name="pref_backup_slots">Maximum backups</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="tracker_not_logged_in">Not logged in: %1$s</string>
<string name="backup_restore_invalid_uri">Error: empty URI</string> <string name="backup_restore_invalid_uri">Error: empty URI</string>
<string name="backup_created">Backup created</string> <string name="backup_created">Backup created</string>
<string name="invalid_backup_file">Invalid backup file</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="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_sources">Missing sources:</string>
<string name="backup_restore_missing_trackers">Trackers not logged into:</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="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_completed">Restore completed</string>
<string name="restore_duration">%02d min, %02d sec</string> <string name="restore_duration">%02d min, %02d sec</string>