mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +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