Move backup restoring functions from BackupManager to BackupRestorer
Cette révision appartient à :
Parent
0f42b9f154
révision
6dab94a937
5 fichiers modifiés avec 662 ajouts et 662 suppressions
|
@ -49,7 +49,7 @@ class BackupCreateJob(private val context: Context, workerParams: WorkerParamete
|
|||
}
|
||||
|
||||
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()))
|
||||
Result.success()
|
||||
} catch (e: Exception) {
|
||||
|
|
268
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt
Fichier normal
268
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt
Fichier normal
|
@ -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.net.Uri
|
||||
import eu.kanade.domain.chapter.model.copyFrom
|
||||
import eu.kanade.domain.manga.interactor.UpdateManga
|
||||
import eu.kanade.tachiyomi.R
|
||||
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.StringPreferenceValue
|
||||
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.util.BackupUtil
|
||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
|
@ -23,7 +25,14 @@ import kotlinx.coroutines.coroutineScope
|
|||
import kotlinx.coroutines.isActive
|
||||
import tachiyomi.core.preference.AndroidPreferenceStore
|
||||
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.history.model.HistoryUpdate
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import tachiyomi.domain.track.model.Track
|
||||
|
@ -34,20 +43,24 @@ import java.text.SimpleDateFormat
|
|||
import java.time.ZonedDateTime
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.math.max
|
||||
|
||||
class BackupRestorer(
|
||||
private val context: Context,
|
||||
private val notifier: BackupNotifier,
|
||||
) {
|
||||
|
||||
private val handler: DatabaseHandler = Injekt.get()
|
||||
private val updateManga: UpdateManga = Injekt.get()
|
||||
private val getCategories: GetCategories = Injekt.get()
|
||||
private val fetchInterval: FetchInterval = Injekt.get()
|
||||
|
||||
private val preferenceStore: PreferenceStore = Injekt.get()
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get()
|
||||
|
||||
private var now = ZonedDateTime.now()
|
||||
private var currentFetchWindow = fetchInterval.getWindow(now)
|
||||
|
||||
private var backupManager = BackupManager(context)
|
||||
|
||||
private var restoreAmount = 0
|
||||
private var restoreProgress = 0
|
||||
|
||||
|
@ -102,7 +115,7 @@ class BackupRestorer(
|
|||
private suspend fun performRestore(uri: Uri, sync: Boolean): Boolean {
|
||||
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
|
||||
if (backup.backupCategories.isNotEmpty()) {
|
||||
|
@ -134,7 +147,38 @@ class BackupRestorer(
|
|||
}
|
||||
|
||||
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
|
||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories), context.getString(R.string.restoring_backup))
|
||||
|
@ -149,14 +193,14 @@ class BackupRestorer(
|
|||
val tracks = backupManga.getTrackingImpl()
|
||||
|
||||
try {
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga.url, manga.source)
|
||||
val dbManga = getMangaFromDatabase(manga.url, manga.source)
|
||||
val restoredManga = if (dbManga == null) {
|
||||
// Manga not in database
|
||||
restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories)
|
||||
} else {
|
||||
// Manga 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
|
||||
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
|
||||
*
|
||||
|
@ -189,12 +277,131 @@ class BackupRestorer(
|
|||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
): Manga {
|
||||
val fetchedManga = backupManager.restoreNewManga(manga)
|
||||
backupManager.restoreChapters(fetchedManga, chapters)
|
||||
val fetchedManga = restoreNewManga(manga)
|
||||
restoreChapters(fetchedManga, chapters)
|
||||
restoreExtras(fetchedManga, categories, history, tracks, backupCategories)
|
||||
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(
|
||||
backupManga: Manga,
|
||||
chapters: List<Chapter>,
|
||||
|
@ -203,19 +410,187 @@ class BackupRestorer(
|
|||
tracks: List<Track>,
|
||||
backupCategories: List<BackupCategory>,
|
||||
): Manga {
|
||||
backupManager.restoreChapters(backupManga, chapters)
|
||||
restoreChapters(backupManga, chapters)
|
||||
restoreExtras(backupManga, categories, history, tracks, backupCategories)
|
||||
return backupManga
|
||||
}
|
||||
|
||||
private suspend fun restoreExtras(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
|
||||
backupManager.restoreCategories(manga, categories, backupCategories)
|
||||
backupManager.restoreHistory(history)
|
||||
backupManager.restoreTracking(manga, tracks)
|
||||
restoreCategories(manga, categories, backupCategories)
|
||||
restoreHistory(history)
|
||||
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>) {
|
||||
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>) {
|
||||
|
@ -223,6 +598,9 @@ class BackupRestorer(
|
|||
val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
|
||||
restorePreferences(it.prefs, sourcePrefs)
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.source_settings), context.getString(R.string.restoring_backup))
|
||||
}
|
||||
|
||||
private fun restorePreferences(
|
||||
|
|
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util
|
|||
|
||||
import android.content.Context
|
||||
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.BackupSerializer
|
||||
import okio.buffer
|
||||
|
@ -14,7 +14,7 @@ object BackupUtil {
|
|||
* Decode a potentially-gzipped backup.
|
||||
*/
|
||||
fun decodeBackup(context: Context, uri: Uri): Backup {
|
||||
val backupManager = BackupManager(context)
|
||||
val backupCreator = BackupCreator(context)
|
||||
|
||||
val backupStringSource = context.contentResolver.openInputStream(uri)!!.source().buffer()
|
||||
|
||||
|
@ -27,6 +27,6 @@ object BackupUtil {
|
|||
backupStringSource
|
||||
}.use { it.readByteArray() }
|
||||
|
||||
return backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||
return backupCreator.parser.decodeFromByteArray(BackupSerializer, backupString)
|
||||
}
|
||||
}
|
||||
|
|
Référencer dans un nouveau ticket