Move backup restoring functions from BackupManager to BackupRestorer
This commit is contained in:
parent
0f42b9f154
commit
6dab94a937
5 changed files with 662 additions and 662 deletions
|
@ -49,7 +49,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val location = BackupManager(context).createBackup(uri, flags, isAutoBackup)
|
val location = BackupCreator(context).createBackup(uri, flags, isAutoBackup)
|
||||||
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
|
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|
|
@ -0,0 +1,268 @@
|
||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.preferenceKey
|
||||||
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
|
import eu.kanade.tachiyomi.util.system.hasPermission
|
||||||
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
import logcat.LogPriority
|
||||||
|
import okio.buffer
|
||||||
|
import okio.gzip
|
||||||
|
import okio.sink
|
||||||
|
import tachiyomi.core.preference.Preference
|
||||||
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
import tachiyomi.core.util.system.logcat
|
||||||
|
import tachiyomi.data.DatabaseHandler
|
||||||
|
import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
|
import tachiyomi.domain.category.model.Category
|
||||||
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
|
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
import tachiyomi.domain.source.service.SourceManager
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
class BackupCreator(
|
||||||
|
private val context: Context,
|
||||||
|
) {
|
||||||
|
|
||||||
|
private val handler: DatabaseHandler = Injekt.get()
|
||||||
|
private val sourceManager: SourceManager = Injekt.get()
|
||||||
|
private val backupPreferences: BackupPreferences = Injekt.get()
|
||||||
|
private val getCategories: GetCategories = Injekt.get()
|
||||||
|
private val getFavorites: GetFavorites = Injekt.get()
|
||||||
|
private val getHistory: GetHistory = Injekt.get()
|
||||||
|
private val preferenceStore: PreferenceStore = Injekt.get()
|
||||||
|
|
||||||
|
internal val parser = ProtoBuf
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup file.
|
||||||
|
*
|
||||||
|
* @param uri path of Uri
|
||||||
|
* @param isAutoBackup backup called from scheduled backup job
|
||||||
|
*/
|
||||||
|
suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
||||||
|
if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||||
|
throw IllegalStateException(context.getString(R.string.missing_storage_permission))
|
||||||
|
}
|
||||||
|
|
||||||
|
val databaseManga = getFavorites.await()
|
||||||
|
val backup = Backup(
|
||||||
|
backupMangas(databaseManga, flags),
|
||||||
|
backupCategories(flags),
|
||||||
|
emptyList(),
|
||||||
|
prepExtensionInfoForSync(databaseManga),
|
||||||
|
backupAppPreferences(flags),
|
||||||
|
backupSourcePreferences(flags),
|
||||||
|
)
|
||||||
|
|
||||||
|
var file: UniFile? = null
|
||||||
|
try {
|
||||||
|
file = (
|
||||||
|
if (isAutoBackup) {
|
||||||
|
// Get dir of file and create
|
||||||
|
var dir = UniFile.fromUri(context, uri)
|
||||||
|
dir = dir.createDirectory("automatic")
|
||||||
|
|
||||||
|
// Delete older backups
|
||||||
|
val numberOfBackups = backupPreferences.numberOfBackups().get()
|
||||||
|
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
|
||||||
|
.orEmpty()
|
||||||
|
.sortedByDescending { it.name }
|
||||||
|
.drop(numberOfBackups - 1)
|
||||||
|
.forEach { it.delete() }
|
||||||
|
|
||||||
|
// Create new file to place backup
|
||||||
|
dir.createFile(Backup.getFilename())
|
||||||
|
} else {
|
||||||
|
UniFile.fromUri(context, uri)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
?: throw Exception(context.getString(R.string.create_backup_file_error))
|
||||||
|
|
||||||
|
if (!file.isFile) {
|
||||||
|
throw IllegalStateException("Failed to get handle on a backup file")
|
||||||
|
}
|
||||||
|
|
||||||
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
||||||
|
if (byteArray.isEmpty()) {
|
||||||
|
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
||||||
|
}
|
||||||
|
|
||||||
|
file.openOutputStream().also {
|
||||||
|
// Force overwrite old file
|
||||||
|
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||||
|
}.sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
|
val fileUri = file.uri
|
||||||
|
|
||||||
|
// Make sure it's a valid backup file
|
||||||
|
BackupFileValidator().validate(context, fileUri)
|
||||||
|
|
||||||
|
return fileUri.toString()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR, e)
|
||||||
|
file?.delete()
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
|
||||||
|
return mangas
|
||||||
|
.asSequence()
|
||||||
|
.map(Manga::source)
|
||||||
|
.distinct()
|
||||||
|
.map(sourceManager::getOrStub)
|
||||||
|
.map(BackupSource::copyFrom)
|
||||||
|
.toList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup the categories of library
|
||||||
|
*
|
||||||
|
* @return list of [BackupCategory] to be backed up
|
||||||
|
*/
|
||||||
|
private suspend fun backupCategories(options: Int): List<BackupCategory> {
|
||||||
|
// Check if user wants category information in backup
|
||||||
|
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
|
getCategories.await()
|
||||||
|
.filterNot(Category::isSystemCategory)
|
||||||
|
.map(backupCategoryMapper)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
||||||
|
return mangas.map {
|
||||||
|
backupManga(it, flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a manga to Json
|
||||||
|
*
|
||||||
|
* @param manga manga that gets converted
|
||||||
|
* @param options options for the backup
|
||||||
|
* @return [BackupManga] containing manga in a serializable form
|
||||||
|
*/
|
||||||
|
private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
|
||||||
|
// Entry for this manga
|
||||||
|
val mangaObject = BackupManga.copyFrom(manga)
|
||||||
|
|
||||||
|
// Check if user wants chapter information in backup
|
||||||
|
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
||||||
|
// Backup all the chapters
|
||||||
|
val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
|
||||||
|
if (chapters.isNotEmpty()) {
|
||||||
|
mangaObject.chapters = chapters
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants category information in backup
|
||||||
|
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
||||||
|
// Backup categories for this manga
|
||||||
|
val categoriesForManga = getCategories.await(manga.id)
|
||||||
|
if (categoriesForManga.isNotEmpty()) {
|
||||||
|
mangaObject.categories = categoriesForManga.map { it.order }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants track information in backup
|
||||||
|
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
||||||
|
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
||||||
|
if (tracks.isNotEmpty()) {
|
||||||
|
mangaObject.tracking = tracks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user wants history information in backup
|
||||||
|
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
||||||
|
val historyByMangaId = getHistory.await(manga.id)
|
||||||
|
if (historyByMangaId.isNotEmpty()) {
|
||||||
|
val history = historyByMangaId.map { history ->
|
||||||
|
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
||||||
|
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
||||||
|
}
|
||||||
|
if (history.isNotEmpty()) {
|
||||||
|
mangaObject.history = history
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mangaObject
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupAppPreferences(flags: Int): List<BackupPreference> {
|
||||||
|
if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
|
||||||
|
|
||||||
|
return preferenceStore.getAll().toBackupPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
|
||||||
|
if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
|
||||||
|
|
||||||
|
return sourceManager.getOnlineSources()
|
||||||
|
.filterIsInstance<ConfigurableSource>()
|
||||||
|
.map {
|
||||||
|
BackupSourcePreferences(
|
||||||
|
it.preferenceKey(),
|
||||||
|
it.sourcePreferences().all.toBackupPreferences(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
|
||||||
|
return this.filterKeys { !Preference.isPrivate(it) }
|
||||||
|
.mapNotNull { (key, value) ->
|
||||||
|
when (value) {
|
||||||
|
is Int -> BackupPreference(key, IntPreferenceValue(value))
|
||||||
|
is Long -> BackupPreference(key, LongPreferenceValue(value))
|
||||||
|
is Float -> BackupPreference(key, FloatPreferenceValue(value))
|
||||||
|
is String -> BackupPreference(key, StringPreferenceValue(value))
|
||||||
|
is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
|
||||||
|
is Set<*> -> (value as? Set<String>)?.let {
|
||||||
|
BackupPreference(key, StringSetPreferenceValue(it))
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,646 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.data.backup
|
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.domain.chapter.model.copyFrom
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_APP_PREFS_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_SOURCE_PREFS_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSource
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BooleanPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupCategoryMapper
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupChapterMapper
|
|
||||||
import eu.kanade.tachiyomi.data.backup.models.backupTrackMapper
|
|
||||||
import eu.kanade.tachiyomi.source.ConfigurableSource
|
|
||||||
import eu.kanade.tachiyomi.source.model.copyFrom
|
|
||||||
import eu.kanade.tachiyomi.source.preferenceKey
|
|
||||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
|
||||||
import eu.kanade.tachiyomi.util.system.hasPermission
|
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
|
||||||
import logcat.LogPriority
|
|
||||||
import okio.buffer
|
|
||||||
import okio.gzip
|
|
||||||
import okio.sink
|
|
||||||
import tachiyomi.core.preference.Preference
|
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
|
||||||
import tachiyomi.core.util.system.logcat
|
|
||||||
import tachiyomi.data.DatabaseHandler
|
|
||||||
import tachiyomi.data.Manga_sync
|
|
||||||
import tachiyomi.data.Mangas
|
|
||||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
|
||||||
import tachiyomi.domain.backup.service.BackupPreferences
|
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
|
||||||
import tachiyomi.domain.category.model.Category
|
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
|
||||||
import tachiyomi.domain.history.model.HistoryUpdate
|
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
|
||||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
|
||||||
import tachiyomi.domain.manga.model.Manga
|
|
||||||
import tachiyomi.domain.source.service.SourceManager
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.Date
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
class BackupManager(
|
|
||||||
private val context: Context,
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val handler: DatabaseHandler = Injekt.get()
|
|
||||||
private val sourceManager: SourceManager = Injekt.get()
|
|
||||||
private val backupPreferences: BackupPreferences = Injekt.get()
|
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get()
|
|
||||||
private val getCategories: GetCategories = Injekt.get()
|
|
||||||
private val getFavorites: GetFavorites = Injekt.get()
|
|
||||||
private val getHistory: GetHistory = Injekt.get()
|
|
||||||
private val preferenceStore: PreferenceStore = Injekt.get()
|
|
||||||
|
|
||||||
internal val parser = ProtoBuf
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create backup file from database
|
|
||||||
*
|
|
||||||
* @param uri path of Uri
|
|
||||||
* @param isAutoBackup backup called from scheduled backup job
|
|
||||||
*/
|
|
||||||
suspend fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
|
||||||
if (!context.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
|
||||||
throw IllegalStateException(context.getString(R.string.missing_storage_permission))
|
|
||||||
}
|
|
||||||
|
|
||||||
val databaseManga = getFavorites.await()
|
|
||||||
val backup = Backup(
|
|
||||||
backupMangas(databaseManga, flags),
|
|
||||||
backupCategories(flags),
|
|
||||||
emptyList(),
|
|
||||||
prepExtensionInfoForSync(databaseManga),
|
|
||||||
backupAppPreferences(flags),
|
|
||||||
backupSourcePreferences(flags),
|
|
||||||
)
|
|
||||||
|
|
||||||
var file: UniFile? = null
|
|
||||||
try {
|
|
||||||
file = (
|
|
||||||
if (isAutoBackup) {
|
|
||||||
// Get dir of file and create
|
|
||||||
var dir = UniFile.fromUri(context, uri)
|
|
||||||
dir = dir.createDirectory("automatic")
|
|
||||||
|
|
||||||
// Delete older backups
|
|
||||||
val numberOfBackups = backupPreferences.numberOfBackups().get()
|
|
||||||
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
|
|
||||||
.orEmpty()
|
|
||||||
.sortedByDescending { it.name }
|
|
||||||
.drop(numberOfBackups - 1)
|
|
||||||
.forEach { it.delete() }
|
|
||||||
|
|
||||||
// Create new file to place backup
|
|
||||||
dir.createFile(Backup.getFilename())
|
|
||||||
} else {
|
|
||||||
UniFile.fromUri(context, uri)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
?: throw Exception(context.getString(R.string.create_backup_file_error))
|
|
||||||
|
|
||||||
if (!file.isFile) {
|
|
||||||
throw IllegalStateException("Failed to get handle on a backup file")
|
|
||||||
}
|
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup)
|
|
||||||
if (byteArray.isEmpty()) {
|
|
||||||
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
|
||||||
}
|
|
||||||
|
|
||||||
file.openOutputStream().also {
|
|
||||||
// Force overwrite old file
|
|
||||||
(it as? FileOutputStream)?.channel?.truncate(0)
|
|
||||||
}.sink().gzip().buffer().use { it.write(byteArray) }
|
|
||||||
val fileUri = file.uri
|
|
||||||
|
|
||||||
// Make sure it's a valid backup file
|
|
||||||
BackupFileValidator().validate(context, fileUri)
|
|
||||||
|
|
||||||
return fileUri.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logcat(LogPriority.ERROR, e)
|
|
||||||
file?.delete()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun prepExtensionInfoForSync(mangas: List<Manga>): List<BackupSource> {
|
|
||||||
return mangas
|
|
||||||
.asSequence()
|
|
||||||
.map(Manga::source)
|
|
||||||
.distinct()
|
|
||||||
.map(sourceManager::getOrStub)
|
|
||||||
.map(BackupSource::copyFrom)
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup the categories of library
|
|
||||||
*
|
|
||||||
* @return list of [BackupCategory] to be backed up
|
|
||||||
*/
|
|
||||||
private suspend fun backupCategories(options: Int): List<BackupCategory> {
|
|
||||||
// Check if user wants category information in backup
|
|
||||||
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
|
||||||
getCategories.await()
|
|
||||||
.filterNot(Category::isSystemCategory)
|
|
||||||
.map(backupCategoryMapper)
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
|
|
||||||
return mangas.map {
|
|
||||||
backupManga(it, flags)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a manga to Json
|
|
||||||
*
|
|
||||||
* @param manga manga that gets converted
|
|
||||||
* @param options options for the backup
|
|
||||||
* @return [BackupManga] containing manga in a serializable form
|
|
||||||
*/
|
|
||||||
private suspend fun backupManga(manga: Manga, options: Int): BackupManga {
|
|
||||||
// Entry for this manga
|
|
||||||
val mangaObject = BackupManga.copyFrom(manga)
|
|
||||||
|
|
||||||
// Check if user wants chapter information in backup
|
|
||||||
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
|
|
||||||
// Backup all the chapters
|
|
||||||
val chapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id, backupChapterMapper) }
|
|
||||||
if (chapters.isNotEmpty()) {
|
|
||||||
mangaObject.chapters = chapters
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants category information in backup
|
|
||||||
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
|
|
||||||
// Backup categories for this manga
|
|
||||||
val categoriesForManga = getCategories.await(manga.id)
|
|
||||||
if (categoriesForManga.isNotEmpty()) {
|
|
||||||
mangaObject.categories = categoriesForManga.map { it.order }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants track information in backup
|
|
||||||
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
|
|
||||||
val tracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id, backupTrackMapper) }
|
|
||||||
if (tracks.isNotEmpty()) {
|
|
||||||
mangaObject.tracking = tracks
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if user wants history information in backup
|
|
||||||
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
|
|
||||||
val historyByMangaId = getHistory.await(manga.id)
|
|
||||||
if (historyByMangaId.isNotEmpty()) {
|
|
||||||
val history = historyByMangaId.map { history ->
|
|
||||||
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
|
||||||
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
|
||||||
}
|
|
||||||
if (history.isNotEmpty()) {
|
|
||||||
mangaObject.history = history
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return mangaObject
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupAppPreferences(flags: Int): List<BackupPreference> {
|
|
||||||
if (flags and BACKUP_APP_PREFS_MASK != BACKUP_APP_PREFS) return emptyList()
|
|
||||||
|
|
||||||
return preferenceStore.getAll().toBackupPreferences()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
|
|
||||||
if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
|
|
||||||
|
|
||||||
return sourceManager.getOnlineSources()
|
|
||||||
.filterIsInstance<ConfigurableSource>()
|
|
||||||
.map {
|
|
||||||
BackupSourcePreferences(
|
|
||||||
it.preferenceKey(),
|
|
||||||
it.sourcePreferences().all.toBackupPreferences()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
|
|
||||||
return this.filterKeys { !Preference.isPrivate(it) }
|
|
||||||
.mapNotNull { (key, value) ->
|
|
||||||
when (value) {
|
|
||||||
is Int -> BackupPreference(key, IntPreferenceValue(value))
|
|
||||||
is Long -> BackupPreference(key, LongPreferenceValue(value))
|
|
||||||
is Float -> BackupPreference(key, FloatPreferenceValue(value))
|
|
||||||
is String -> BackupPreference(key, StringPreferenceValue(value))
|
|
||||||
is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
|
|
||||||
is Set<*> -> (value as? Set<String>)?.let {
|
|
||||||
BackupPreference(key, StringSetPreferenceValue(it))
|
|
||||||
}
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
|
||||||
var updatedManga = manga.copy(id = dbManga._id)
|
|
||||||
updatedManga = updatedManga.copyFrom(dbManga)
|
|
||||||
updateManga(updatedManga)
|
|
||||||
return updatedManga
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches manga information
|
|
||||||
*
|
|
||||||
* @param manga manga that needs updating
|
|
||||||
* @return Updated manga info.
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreNewManga(manga: Manga): Manga {
|
|
||||||
return manga.copy(
|
|
||||||
initialized = manga.description != null,
|
|
||||||
id = insertManga(manga),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore the categories from Json
|
|
||||||
*
|
|
||||||
* @param backupCategories list containing categories
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
|
||||||
// Get categories from file and from db
|
|
||||||
val dbCategories = getCategories.await()
|
|
||||||
|
|
||||||
val categories = backupCategories.map {
|
|
||||||
var category = it.getCategory()
|
|
||||||
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 = category.copy(id = dbCategory.id)
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
// Let the db assign the id
|
|
||||||
val id = handler.awaitOneExecutable {
|
|
||||||
categoriesQueries.insert(category.name, category.order, category.flags)
|
|
||||||
categoriesQueries.selectLastInsertedRowId()
|
|
||||||
}
|
|
||||||
category = category.copy(id = id)
|
|
||||||
}
|
|
||||||
|
|
||||||
category
|
|
||||||
}
|
|
||||||
|
|
||||||
libraryPreferences.categorizedDisplaySettings().set(
|
|
||||||
(dbCategories + categories)
|
|
||||||
.distinctBy { it.flags }
|
|
||||||
.size > 1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the categories a manga is in.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose categories have to be restored.
|
|
||||||
* @param categories the categories to restore.
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
|
||||||
val dbCategories = getCategories.await()
|
|
||||||
val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
|
|
||||||
|
|
||||||
categories.forEach { backupCategoryOrder ->
|
|
||||||
backupCategories.firstOrNull {
|
|
||||||
it.order == backupCategoryOrder.toLong()
|
|
||||||
}?.let { backupCategory ->
|
|
||||||
dbCategories.firstOrNull { dbCategory ->
|
|
||||||
dbCategory.name == backupCategory.name
|
|
||||||
}?.let { dbCategory ->
|
|
||||||
mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update database
|
|
||||||
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
|
||||||
handler.await(true) {
|
|
||||||
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
|
||||||
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
|
||||||
mangas_categoriesQueries.insert(mangaId, categoryId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore history from Json
|
|
||||||
*
|
|
||||||
* @param history list containing history to be restored
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreHistory(history: List<BackupHistory>) {
|
|
||||||
// List containing history to be updated
|
|
||||||
val toUpdate = mutableListOf<HistoryUpdate>()
|
|
||||||
for ((url, lastRead, readDuration) in history) {
|
|
||||||
var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
|
|
||||||
// Check if history already in database and update
|
|
||||||
if (dbHistory != null) {
|
|
||||||
dbHistory = dbHistory.copy(
|
|
||||||
last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)),
|
|
||||||
time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read,
|
|
||||||
)
|
|
||||||
toUpdate.add(
|
|
||||||
HistoryUpdate(
|
|
||||||
chapterId = dbHistory.chapter_id,
|
|
||||||
readAt = dbHistory.last_read!!,
|
|
||||||
sessionReadDuration = dbHistory.time_read,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// If not in database create
|
|
||||||
handler
|
|
||||||
.awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
|
|
||||||
?.let {
|
|
||||||
toUpdate.add(
|
|
||||||
HistoryUpdate(
|
|
||||||
chapterId = it._id,
|
|
||||||
readAt = Date(lastRead),
|
|
||||||
sessionReadDuration = readDuration,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
handler.await(true) {
|
|
||||||
toUpdate.forEach { payload ->
|
|
||||||
historyQueries.upsert(
|
|
||||||
payload.chapterId,
|
|
||||||
payload.readAt,
|
|
||||||
payload.sessionReadDuration,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores the sync of a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga whose sync have to be restored.
|
|
||||||
* @param tracks the track list to restore.
|
|
||||||
*/
|
|
||||||
internal suspend fun restoreTracking(manga: Manga, tracks: List<tachiyomi.domain.track.model.Track>) {
|
|
||||||
// Get tracks from database
|
|
||||||
val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
|
|
||||||
val toUpdate = mutableListOf<Manga_sync>()
|
|
||||||
val toInsert = mutableListOf<tachiyomi.domain.track.model.Track>()
|
|
||||||
|
|
||||||
tracks
|
|
||||||
// Fix foreign keys with the current manga id
|
|
||||||
.map { it.copy(mangaId = manga.id) }
|
|
||||||
.forEach { track ->
|
|
||||||
var isInDatabase = false
|
|
||||||
for (dbTrack in dbTracks) {
|
|
||||||
if (track.syncId == dbTrack.sync_id) {
|
|
||||||
// The sync is already in the db, only update its fields
|
|
||||||
var temp = dbTrack
|
|
||||||
if (track.remoteId != dbTrack.remote_id) {
|
|
||||||
temp = temp.copy(remote_id = track.remoteId)
|
|
||||||
}
|
|
||||||
if (track.libraryId != dbTrack.library_id) {
|
|
||||||
temp = temp.copy(library_id = track.libraryId)
|
|
||||||
}
|
|
||||||
temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
|
|
||||||
isInDatabase = true
|
|
||||||
toUpdate.add(temp)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isInDatabase) {
|
|
||||||
// Insert new sync. Let the db assign the id
|
|
||||||
toInsert.add(track.copy(id = 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update database
|
|
||||||
if (toUpdate.isNotEmpty()) {
|
|
||||||
handler.await(true) {
|
|
||||||
toUpdate.forEach { track ->
|
|
||||||
manga_syncQueries.update(
|
|
||||||
track.manga_id,
|
|
||||||
track.sync_id,
|
|
||||||
track.remote_id,
|
|
||||||
track.library_id,
|
|
||||||
track.title,
|
|
||||||
track.last_chapter_read,
|
|
||||||
track.total_chapters,
|
|
||||||
track.status,
|
|
||||||
track.score,
|
|
||||||
track.remote_url,
|
|
||||||
track.start_date,
|
|
||||||
track.finish_date,
|
|
||||||
track._id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (toInsert.isNotEmpty()) {
|
|
||||||
handler.await(true) {
|
|
||||||
toInsert.forEach { track ->
|
|
||||||
manga_syncQueries.insert(
|
|
||||||
track.mangaId,
|
|
||||||
track.syncId,
|
|
||||||
track.remoteId,
|
|
||||||
track.libraryId,
|
|
||||||
track.title,
|
|
||||||
track.lastChapterRead,
|
|
||||||
track.totalChapters,
|
|
||||||
track.status,
|
|
||||||
track.score,
|
|
||||||
track.remoteUrl,
|
|
||||||
track.startDate,
|
|
||||||
track.finishDate,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
internal suspend fun restoreChapters(manga: Manga, chapters: List<tachiyomi.domain.chapter.model.Chapter>) {
|
|
||||||
val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id) }
|
|
||||||
|
|
||||||
val processed = chapters.map { chapter ->
|
|
||||||
var updatedChapter = chapter
|
|
||||||
val dbChapter = dbChapters.find { it.url == updatedChapter.url }
|
|
||||||
if (dbChapter != null) {
|
|
||||||
updatedChapter = updatedChapter.copy(id = dbChapter._id)
|
|
||||||
updatedChapter = updatedChapter.copyFrom(dbChapter)
|
|
||||||
if (dbChapter.read && !updatedChapter.read) {
|
|
||||||
updatedChapter = updatedChapter.copy(read = true, lastPageRead = dbChapter.last_page_read)
|
|
||||||
} else if (updatedChapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) {
|
|
||||||
updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read)
|
|
||||||
}
|
|
||||||
if (!updatedChapter.bookmark && dbChapter.bookmark) {
|
|
||||||
updatedChapter = updatedChapter.copy(bookmark = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updatedChapter.copy(mangaId = manga.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newChapters = processed.groupBy { it.id > 0 }
|
|
||||||
newChapters[true]?.let { updateKnownChapters(it) }
|
|
||||||
newChapters[false]?.let { insertChapters(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns manga
|
|
||||||
*
|
|
||||||
* @return [Manga], null if not found
|
|
||||||
*/
|
|
||||||
internal suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
|
|
||||||
return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts manga and returns id
|
|
||||||
*
|
|
||||||
* @return id of [Manga], null if not found
|
|
||||||
*/
|
|
||||||
private suspend fun insertManga(manga: Manga): Long {
|
|
||||||
return handler.awaitOneExecutable(true) {
|
|
||||||
mangasQueries.insert(
|
|
||||||
source = manga.source,
|
|
||||||
url = manga.url,
|
|
||||||
artist = manga.artist,
|
|
||||||
author = manga.author,
|
|
||||||
description = manga.description,
|
|
||||||
genre = manga.genre,
|
|
||||||
title = manga.title,
|
|
||||||
status = manga.status,
|
|
||||||
thumbnailUrl = manga.thumbnailUrl,
|
|
||||||
favorite = manga.favorite,
|
|
||||||
lastUpdate = manga.lastUpdate,
|
|
||||||
nextUpdate = 0L,
|
|
||||||
calculateInterval = 0L,
|
|
||||||
initialized = manga.initialized,
|
|
||||||
viewerFlags = manga.viewerFlags,
|
|
||||||
chapterFlags = manga.chapterFlags,
|
|
||||||
coverLastModified = manga.coverLastModified,
|
|
||||||
dateAdded = manga.dateAdded,
|
|
||||||
updateStrategy = manga.updateStrategy,
|
|
||||||
)
|
|
||||||
mangasQueries.selectLastInsertedRowId()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun updateManga(manga: Manga): Long {
|
|
||||||
handler.await(true) {
|
|
||||||
mangasQueries.update(
|
|
||||||
source = manga.source,
|
|
||||||
url = manga.url,
|
|
||||||
artist = manga.artist,
|
|
||||||
author = manga.author,
|
|
||||||
description = manga.description,
|
|
||||||
genre = manga.genre?.joinToString(separator = ", "),
|
|
||||||
title = manga.title,
|
|
||||||
status = manga.status,
|
|
||||||
thumbnailUrl = manga.thumbnailUrl,
|
|
||||||
favorite = manga.favorite,
|
|
||||||
lastUpdate = manga.lastUpdate,
|
|
||||||
nextUpdate = null,
|
|
||||||
calculateInterval = null,
|
|
||||||
initialized = manga.initialized,
|
|
||||||
viewer = manga.viewerFlags,
|
|
||||||
chapterFlags = manga.chapterFlags,
|
|
||||||
coverLastModified = manga.coverLastModified,
|
|
||||||
dateAdded = manga.dateAdded,
|
|
||||||
mangaId = manga.id,
|
|
||||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return manga.id
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inserts list of chapters
|
|
||||||
*/
|
|
||||||
private suspend fun insertChapters(chapters: List<tachiyomi.domain.chapter.model.Chapter>) {
|
|
||||||
handler.await(true) {
|
|
||||||
chapters.forEach { chapter ->
|
|
||||||
chaptersQueries.insert(
|
|
||||||
chapter.mangaId,
|
|
||||||
chapter.url,
|
|
||||||
chapter.name,
|
|
||||||
chapter.scanlator,
|
|
||||||
chapter.read,
|
|
||||||
chapter.bookmark,
|
|
||||||
chapter.lastPageRead,
|
|
||||||
chapter.chapterNumber,
|
|
||||||
chapter.sourceOrder,
|
|
||||||
chapter.dateFetch,
|
|
||||||
chapter.dateUpload,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates a list of chapters with known database ids
|
|
||||||
*/
|
|
||||||
private suspend fun updateKnownChapters(chapters: List<tachiyomi.domain.chapter.model.Chapter>) {
|
|
||||||
handler.await(true) {
|
|
||||||
chapters.forEach { chapter ->
|
|
||||||
chaptersQueries.update(
|
|
||||||
mangaId = null,
|
|
||||||
url = null,
|
|
||||||
name = null,
|
|
||||||
scanlator = null,
|
|
||||||
read = chapter.read,
|
|
||||||
bookmark = chapter.bookmark,
|
|
||||||
lastPageRead = chapter.lastPageRead,
|
|
||||||
chapterNumber = null,
|
|
||||||
sourceOrder = null,
|
|
||||||
dateFetch = null,
|
|
||||||
dateUpload = null,
|
|
||||||
chapterId = chapter.id,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,6 +2,7 @@ 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.domain.chapter.model.copyFrom
|
||||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
|
@ -16,6 +17,7 @@ import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue
|
||||||
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
import eu.kanade.tachiyomi.data.backup.models.LongPreferenceValue
|
||||||
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
import eu.kanade.tachiyomi.data.backup.models.StringPreferenceValue
|
||||||
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
import eu.kanade.tachiyomi.data.backup.models.StringSetPreferenceValue
|
||||||
|
import eu.kanade.tachiyomi.source.model.copyFrom
|
||||||
import eu.kanade.tachiyomi.source.sourcePreferences
|
import eu.kanade.tachiyomi.source.sourcePreferences
|
||||||
import eu.kanade.tachiyomi.util.BackupUtil
|
import eu.kanade.tachiyomi.util.BackupUtil
|
||||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
|
@ -23,7 +25,14 @@ import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import tachiyomi.core.preference.AndroidPreferenceStore
|
import tachiyomi.core.preference.AndroidPreferenceStore
|
||||||
import tachiyomi.core.preference.PreferenceStore
|
import tachiyomi.core.preference.PreferenceStore
|
||||||
|
import tachiyomi.data.DatabaseHandler
|
||||||
|
import tachiyomi.data.Manga_sync
|
||||||
|
import tachiyomi.data.Mangas
|
||||||
|
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||||
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
|
import tachiyomi.domain.history.model.HistoryUpdate
|
||||||
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
|
@ -34,20 +43,24 @@ import java.text.SimpleDateFormat
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class BackupRestorer(
|
class BackupRestorer(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
private val notifier: BackupNotifier,
|
private val notifier: BackupNotifier,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val handler: DatabaseHandler = Injekt.get()
|
||||||
private val updateManga: UpdateManga = Injekt.get()
|
private val updateManga: UpdateManga = Injekt.get()
|
||||||
|
private val getCategories: GetCategories = Injekt.get()
|
||||||
private val fetchInterval: FetchInterval = Injekt.get()
|
private val fetchInterval: FetchInterval = Injekt.get()
|
||||||
|
|
||||||
private val preferenceStore: PreferenceStore = Injekt.get()
|
private val preferenceStore: PreferenceStore = Injekt.get()
|
||||||
|
private val libraryPreferences: LibraryPreferences = Injekt.get()
|
||||||
|
|
||||||
private var now = ZonedDateTime.now()
|
private var now = ZonedDateTime.now()
|
||||||
private var currentFetchWindow = fetchInterval.getWindow(now)
|
private var currentFetchWindow = fetchInterval.getWindow(now)
|
||||||
|
|
||||||
private var backupManager = BackupManager(context)
|
|
||||||
|
|
||||||
private var restoreAmount = 0
|
private var restoreAmount = 0
|
||||||
private var restoreProgress = 0
|
private var restoreProgress = 0
|
||||||
|
|
||||||
|
@ -102,7 +115,7 @@ class BackupRestorer(
|
||||||
private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean {
|
private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean {
|
||||||
val backup = BackupUtil.decodeBackup(context, uri)
|
val backup = BackupUtil.decodeBackup(context, uri)
|
||||||
|
|
||||||
restoreAmount = backup.backupManga.size + 1 // +1 for categories
|
restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
|
||||||
|
|
||||||
// Restore categories
|
// Restore categories
|
||||||
if (backup.backupCategories.isNotEmpty()) {
|
if (backup.backupCategories.isNotEmpty()) {
|
||||||
|
@ -134,7 +147,38 @@ class BackupRestorer(
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
||||||
backupManager.restoreCategories(backupCategories)
|
// Get categories from file and from db
|
||||||
|
val dbCategories = getCategories.await()
|
||||||
|
|
||||||
|
val categories = backupCategories.map {
|
||||||
|
var category = it.getCategory()
|
||||||
|
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 = category.copy(id = dbCategory.id)
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
// Let the db assign the id
|
||||||
|
val id = handler.awaitOneExecutable {
|
||||||
|
categoriesQueries.insert(category.name, category.order, category.flags)
|
||||||
|
categoriesQueries.selectLastInsertedRowId()
|
||||||
|
}
|
||||||
|
category = category.copy(id = id)
|
||||||
|
}
|
||||||
|
|
||||||
|
category
|
||||||
|
}
|
||||||
|
|
||||||
|
libraryPreferences.categorizedDisplaySettings().set(
|
||||||
|
(dbCategories + categories)
|
||||||
|
.distinctBy { it.flags }
|
||||||
|
.size > 1,
|
||||||
|
)
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreProgress += 1
|
||||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories), context.getString(R.string.restoring_backup))
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories), context.getString(R.string.restoring_backup))
|
||||||
|
@ -149,14 +193,14 @@ class BackupRestorer(
|
||||||
val tracks = backupManga.getTrackingImpl()
|
val tracks = backupManga.getTrackingImpl()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
|
val dbManga = getMangaFromDatabase(manga.url, manga.source)
|
||||||
val restoredManga = if (dbManga == null) {
|
val restoredManga = if (dbManga == null) {
|
||||||
// Manga not in database
|
// Manga not in database
|
||||||
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
||||||
} else {
|
} else {
|
||||||
// Manga in database
|
// Manga in database
|
||||||
// Copy information from manga already in database
|
// Copy information from manga already in database
|
||||||
val updatedManga = backupManager.restoreExistingManga(manga, dbManga)
|
val updatedManga = restoreExistingManga(manga, dbManga)
|
||||||
// Fetch rest of manga information
|
// Fetch rest of manga information
|
||||||
restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
|
restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories)
|
||||||
}
|
}
|
||||||
|
@ -174,6 +218,50 @@ class BackupRestorer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns manga
|
||||||
|
*
|
||||||
|
* @return [Manga], null if not found
|
||||||
|
*/
|
||||||
|
private suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? {
|
||||||
|
return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga {
|
||||||
|
var updatedManga = manga.copy(id = dbManga._id)
|
||||||
|
updatedManga = updatedManga.copyFrom(dbManga)
|
||||||
|
updateManga(updatedManga)
|
||||||
|
return updatedManga
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateManga(manga: Manga): Long {
|
||||||
|
handler.await(true) {
|
||||||
|
mangasQueries.update(
|
||||||
|
source = manga.source,
|
||||||
|
url = manga.url,
|
||||||
|
artist = manga.artist,
|
||||||
|
author = manga.author,
|
||||||
|
description = manga.description,
|
||||||
|
genre = manga.genre?.joinToString(separator = ", "),
|
||||||
|
title = manga.title,
|
||||||
|
status = manga.status,
|
||||||
|
thumbnailUrl = manga.thumbnailUrl,
|
||||||
|
favorite = manga.favorite,
|
||||||
|
lastUpdate = manga.lastUpdate,
|
||||||
|
nextUpdate = null,
|
||||||
|
calculateInterval = null,
|
||||||
|
initialized = manga.initialized,
|
||||||
|
viewer = manga.viewerFlags,
|
||||||
|
chapterFlags = manga.chapterFlags,
|
||||||
|
coverLastModified = manga.coverLastModified,
|
||||||
|
dateAdded = manga.dateAdded,
|
||||||
|
mangaId = manga.id,
|
||||||
|
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return manga.id
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetches manga information
|
* Fetches manga information
|
||||||
*
|
*
|
||||||
|
@ -189,12 +277,131 @@ class BackupRestorer(
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
): Manga {
|
): Manga {
|
||||||
val fetchedManga = backupManager.restoreNewManga(manga)
|
val fetchedManga = restoreNewManga(manga)
|
||||||
backupManager.restoreChapters(fetchedManga, chapters)
|
restoreChapters(fetchedManga, chapters)
|
||||||
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
||||||
return fetchedManga
|
return fetchedManga
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun restoreChapters(manga: Manga, chapters: List<Chapter>) {
|
||||||
|
val dbChapters = handler.awaitList { chaptersQueries.getChaptersByMangaId(manga.id) }
|
||||||
|
|
||||||
|
val processed = chapters.map { chapter ->
|
||||||
|
var updatedChapter = chapter
|
||||||
|
val dbChapter = dbChapters.find { it.url == updatedChapter.url }
|
||||||
|
if (dbChapter != null) {
|
||||||
|
updatedChapter = updatedChapter.copy(id = dbChapter._id)
|
||||||
|
updatedChapter = updatedChapter.copyFrom(dbChapter)
|
||||||
|
if (dbChapter.read && !updatedChapter.read) {
|
||||||
|
updatedChapter = updatedChapter.copy(read = true, lastPageRead = dbChapter.last_page_read)
|
||||||
|
} else if (updatedChapter.lastPageRead == 0L && dbChapter.last_page_read != 0L) {
|
||||||
|
updatedChapter = updatedChapter.copy(lastPageRead = dbChapter.last_page_read)
|
||||||
|
}
|
||||||
|
if (!updatedChapter.bookmark && dbChapter.bookmark) {
|
||||||
|
updatedChapter = updatedChapter.copy(bookmark = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatedChapter.copy(mangaId = manga.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
val newChapters = processed.groupBy { it.id > 0 }
|
||||||
|
newChapters[true]?.let { updateKnownChapters(it) }
|
||||||
|
newChapters[false]?.let { insertChapters(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts list of chapters
|
||||||
|
*/
|
||||||
|
private suspend fun insertChapters(chapters: List<Chapter>) {
|
||||||
|
handler.await(true) {
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
chaptersQueries.insert(
|
||||||
|
chapter.mangaId,
|
||||||
|
chapter.url,
|
||||||
|
chapter.name,
|
||||||
|
chapter.scanlator,
|
||||||
|
chapter.read,
|
||||||
|
chapter.bookmark,
|
||||||
|
chapter.lastPageRead,
|
||||||
|
chapter.chapterNumber,
|
||||||
|
chapter.sourceOrder,
|
||||||
|
chapter.dateFetch,
|
||||||
|
chapter.dateUpload,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates a list of chapters with known database ids
|
||||||
|
*/
|
||||||
|
private suspend fun updateKnownChapters(chapters: List<Chapter>) {
|
||||||
|
handler.await(true) {
|
||||||
|
chapters.forEach { chapter ->
|
||||||
|
chaptersQueries.update(
|
||||||
|
mangaId = null,
|
||||||
|
url = null,
|
||||||
|
name = null,
|
||||||
|
scanlator = null,
|
||||||
|
read = chapter.read,
|
||||||
|
bookmark = chapter.bookmark,
|
||||||
|
lastPageRead = chapter.lastPageRead,
|
||||||
|
chapterNumber = null,
|
||||||
|
sourceOrder = null,
|
||||||
|
dateFetch = null,
|
||||||
|
dateUpload = null,
|
||||||
|
chapterId = chapter.id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches manga information
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return Updated manga info.
|
||||||
|
*/
|
||||||
|
private suspend fun restoreNewManga(manga: Manga): Manga {
|
||||||
|
return manga.copy(
|
||||||
|
initialized = manga.description != null,
|
||||||
|
id = insertManga(manga),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts manga and returns id
|
||||||
|
*
|
||||||
|
* @return id of [Manga], null if not found
|
||||||
|
*/
|
||||||
|
private suspend fun insertManga(manga: Manga): Long {
|
||||||
|
return handler.awaitOneExecutable(true) {
|
||||||
|
mangasQueries.insert(
|
||||||
|
source = manga.source,
|
||||||
|
url = manga.url,
|
||||||
|
artist = manga.artist,
|
||||||
|
author = manga.author,
|
||||||
|
description = manga.description,
|
||||||
|
genre = manga.genre,
|
||||||
|
title = manga.title,
|
||||||
|
status = manga.status,
|
||||||
|
thumbnailUrl = manga.thumbnailUrl,
|
||||||
|
favorite = manga.favorite,
|
||||||
|
lastUpdate = manga.lastUpdate,
|
||||||
|
nextUpdate = 0L,
|
||||||
|
calculateInterval = 0L,
|
||||||
|
initialized = manga.initialized,
|
||||||
|
viewerFlags = manga.viewerFlags,
|
||||||
|
chapterFlags = manga.chapterFlags,
|
||||||
|
coverLastModified = manga.coverLastModified,
|
||||||
|
dateAdded = manga.dateAdded,
|
||||||
|
updateStrategy = manga.updateStrategy,
|
||||||
|
)
|
||||||
|
mangasQueries.selectLastInsertedRowId()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun restoreNewManga(
|
private suspend fun restoreNewManga(
|
||||||
backupManga: Manga,
|
backupManga: Manga,
|
||||||
chapters: List<Chapter>,
|
chapters: List<Chapter>,
|
||||||
|
@ -203,19 +410,187 @@ class BackupRestorer(
|
||||||
tracks: List<Track>,
|
tracks: List<Track>,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
): Manga {
|
): Manga {
|
||||||
backupManager.restoreChapters(backupManga, chapters)
|
restoreChapters(backupManga, chapters)
|
||||||
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
||||||
return backupManga
|
return backupManga
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
||||||
backupManager.restoreCategories(manga, categories, backupCategories)
|
restoreCategories(manga, categories, backupCategories)
|
||||||
backupManager.restoreHistory(history)
|
restoreHistory(history)
|
||||||
backupManager.restoreTracking(manga, tracks)
|
restoreTracking(manga, tracks)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the categories a manga is in.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose categories have to be restored.
|
||||||
|
* @param categories the categories to restore.
|
||||||
|
*/
|
||||||
|
private suspend fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
|
||||||
|
val dbCategories = getCategories.await()
|
||||||
|
val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
|
||||||
|
|
||||||
|
categories.forEach { backupCategoryOrder ->
|
||||||
|
backupCategories.firstOrNull {
|
||||||
|
it.order == backupCategoryOrder.toLong()
|
||||||
|
}?.let { backupCategory ->
|
||||||
|
dbCategories.firstOrNull { dbCategory ->
|
||||||
|
dbCategory.name == backupCategory.name
|
||||||
|
}?.let { dbCategory ->
|
||||||
|
mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
if (mangaCategoriesToUpdate.isNotEmpty()) {
|
||||||
|
handler.await(true) {
|
||||||
|
mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id)
|
||||||
|
mangaCategoriesToUpdate.forEach { (mangaId, categoryId) ->
|
||||||
|
mangas_categoriesQueries.insert(mangaId, categoryId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore history from Json
|
||||||
|
*
|
||||||
|
* @param history list containing history to be restored
|
||||||
|
*/
|
||||||
|
private suspend fun restoreHistory(history: List<BackupHistory>) {
|
||||||
|
// List containing history to be updated
|
||||||
|
val toUpdate = mutableListOf<HistoryUpdate>()
|
||||||
|
for ((url, lastRead, readDuration) in history) {
|
||||||
|
var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
|
||||||
|
// Check if history already in database and update
|
||||||
|
if (dbHistory != null) {
|
||||||
|
dbHistory = dbHistory.copy(
|
||||||
|
last_read = Date(max(lastRead, dbHistory.last_read?.time ?: 0L)),
|
||||||
|
time_read = max(readDuration, dbHistory.time_read) - dbHistory.time_read,
|
||||||
|
)
|
||||||
|
toUpdate.add(
|
||||||
|
HistoryUpdate(
|
||||||
|
chapterId = dbHistory.chapter_id,
|
||||||
|
readAt = dbHistory.last_read!!,
|
||||||
|
sessionReadDuration = dbHistory.time_read,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// If not in database create
|
||||||
|
handler
|
||||||
|
.awaitOneOrNull { chaptersQueries.getChapterByUrl(url) }
|
||||||
|
?.let {
|
||||||
|
toUpdate.add(
|
||||||
|
HistoryUpdate(
|
||||||
|
chapterId = it._id,
|
||||||
|
readAt = Date(lastRead),
|
||||||
|
sessionReadDuration = readDuration,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
handler.await(true) {
|
||||||
|
toUpdate.forEach { payload ->
|
||||||
|
historyQueries.upsert(
|
||||||
|
payload.chapterId,
|
||||||
|
payload.readAt,
|
||||||
|
payload.sessionReadDuration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores the sync of a manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga whose sync have to be restored.
|
||||||
|
* @param tracks the track list to restore.
|
||||||
|
*/
|
||||||
|
private suspend fun restoreTracking(manga: Manga, tracks: List<Track>) {
|
||||||
|
// Get tracks from database
|
||||||
|
val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
|
||||||
|
val toUpdate = mutableListOf<Manga_sync>()
|
||||||
|
val toInsert = mutableListOf<Track>()
|
||||||
|
|
||||||
|
tracks
|
||||||
|
// Fix foreign keys with the current manga id
|
||||||
|
.map { it.copy(mangaId = manga.id) }
|
||||||
|
.forEach { track ->
|
||||||
|
var isInDatabase = false
|
||||||
|
for (dbTrack in dbTracks) {
|
||||||
|
if (track.syncId == dbTrack.sync_id) {
|
||||||
|
// The sync is already in the db, only update its fields
|
||||||
|
var temp = dbTrack
|
||||||
|
if (track.remoteId != dbTrack.remote_id) {
|
||||||
|
temp = temp.copy(remote_id = track.remoteId)
|
||||||
|
}
|
||||||
|
if (track.libraryId != dbTrack.library_id) {
|
||||||
|
temp = temp.copy(library_id = track.libraryId)
|
||||||
|
}
|
||||||
|
temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
|
||||||
|
isInDatabase = true
|
||||||
|
toUpdate.add(temp)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isInDatabase) {
|
||||||
|
// Insert new sync. Let the db assign the id
|
||||||
|
toInsert.add(track.copy(id = 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update database
|
||||||
|
if (toUpdate.isNotEmpty()) {
|
||||||
|
handler.await(true) {
|
||||||
|
toUpdate.forEach { track ->
|
||||||
|
manga_syncQueries.update(
|
||||||
|
track.manga_id,
|
||||||
|
track.sync_id,
|
||||||
|
track.remote_id,
|
||||||
|
track.library_id,
|
||||||
|
track.title,
|
||||||
|
track.last_chapter_read,
|
||||||
|
track.total_chapters,
|
||||||
|
track.status,
|
||||||
|
track.score,
|
||||||
|
track.remote_url,
|
||||||
|
track.start_date,
|
||||||
|
track.finish_date,
|
||||||
|
track._id,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toInsert.isNotEmpty()) {
|
||||||
|
handler.await(true) {
|
||||||
|
toInsert.forEach { track ->
|
||||||
|
manga_syncQueries.insert(
|
||||||
|
track.mangaId,
|
||||||
|
track.syncId,
|
||||||
|
track.remoteId,
|
||||||
|
track.libraryId,
|
||||||
|
track.title,
|
||||||
|
track.lastChapterRead,
|
||||||
|
track.totalChapters,
|
||||||
|
track.status,
|
||||||
|
track.score,
|
||||||
|
track.remoteUrl,
|
||||||
|
track.startDate,
|
||||||
|
track.finishDate,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreAppPreferences(preferences: List<BackupPreference>) {
|
private fun restoreAppPreferences(preferences: List<BackupPreference>) {
|
||||||
restorePreferences(preferences, preferenceStore)
|
restorePreferences(preferences, preferenceStore)
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.app_settings), context.getString(R.string.restoring_backup))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
|
private fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
|
||||||
|
@ -223,6 +598,9 @@ class BackupRestorer(
|
||||||
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
|
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
|
||||||
restorePreferences(it.prefs, sourcePrefs)
|
restorePreferences(it.prefs, sourcePrefs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.source_settings), context.getString(R.string.restoring_backup))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restorePreferences(
|
private fun restorePreferences(
|
||||||
|
|
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupManager
|
import eu.kanade.tachiyomi.data.backup.BackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
@ -14,7 +14,7 @@ object BackupUtil {
|
||||||
* Decode a potentially-gzipped backup.
|
* Decode a potentially-gzipped backup.
|
||||||
*/
|
*/
|
||||||
fun decodeBackup(context: Context, uri: Uri): Backup {
|
fun decodeBackup(context: Context, uri: Uri): Backup {
|
||||||
val backupManager = BackupManager(context)
|
val backupCreator = BackupCreator(context)
|
||||||
|
|
||||||
val backupStringSource = context.contentResolver.openInputStream(uri)!!.source().buffer()
|
val backupStringSource = context.contentResolver.openInputStream(uri)!!.source().buffer()
|
||||||
|
|
||||||
|
@ -27,6 +27,6 @@ object BackupUtil {
|
||||||
backupStringSource
|
backupStringSource
|
||||||
}.use { it.readByteArray() }
|
}.use { it.readByteArray() }
|
||||||
|
|
||||||
return backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
return backupCreator.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Reference in a new issue