mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 06:17:57 +01:00 
			
		
		
		
	Move backup restoring functions from BackupManager to BackupRestorer
This commit is contained in:
		| @@ -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) { | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user