瀏覽代碼

Split restoring logic into smaller classes

arkon 1 年之前
父節點
當前提交
cd16522805

+ 2 - 2
app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt

@@ -37,9 +37,9 @@ import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
 import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget
 import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
 import eu.kanade.presentation.util.relativeTimeSpanString
-import eu.kanade.tachiyomi.data.backup.BackupCreateJob
 import eu.kanade.tachiyomi.data.backup.BackupFileValidator
-import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
+import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
 import eu.kanade.tachiyomi.data.cache.ChapterCache
 import eu.kanade.tachiyomi.util.storage.DiskUtil
 import eu.kanade.tachiyomi.util.system.DeviceUtil

+ 2 - 2
app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt

@@ -29,8 +29,8 @@ import cafe.adriel.voyager.navigator.LocalNavigator
 import cafe.adriel.voyager.navigator.currentOrThrow
 import eu.kanade.presentation.components.AppBar
 import eu.kanade.presentation.util.Screen
-import eu.kanade.tachiyomi.data.backup.BackupCreateFlags
-import eu.kanade.tachiyomi.data.backup.BackupCreateJob
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
 import eu.kanade.tachiyomi.data.backup.models.Backup
 import eu.kanade.tachiyomi.util.system.DeviceUtil
 import eu.kanade.tachiyomi.util.system.toast

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/Migrations.kt

@@ -7,7 +7,7 @@ import eu.kanade.domain.base.BasePreferences
 import eu.kanade.domain.source.service.SourcePreferences
 import eu.kanade.domain.ui.UiPreferences
 import eu.kanade.tachiyomi.core.security.SecurityPreferences
-import eu.kanade.tachiyomi.data.backup.BackupCreateJob
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
 import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
 import eu.kanade.tachiyomi.data.track.TrackerManager
 import eu.kanade.tachiyomi.network.NetworkPreferences

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt → app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateFlags.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.backup
+package eu.kanade.tachiyomi.data.backup.create
 
 internal object BackupCreateFlags {
     const val BACKUP_CATEGORY = 0x1

+ 3 - 1
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt → app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.backup
+package eu.kanade.tachiyomi.data.backup.create
 
 import android.content.Context
 import android.net.Uri
@@ -14,6 +14,8 @@ import androidx.work.PeriodicWorkRequestBuilder
 import androidx.work.WorkerParameters
 import androidx.work.workDataOf
 import com.hippo.unifile.UniFile
+import eu.kanade.tachiyomi.data.backup.BackupNotifier
+import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
 import eu.kanade.tachiyomi.data.notification.Notifications
 import eu.kanade.tachiyomi.util.system.cancelNotification
 import eu.kanade.tachiyomi.util.system.isRunning

+ 8 - 7
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt → app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt

@@ -1,14 +1,15 @@
-package eu.kanade.tachiyomi.data.backup
+package eu.kanade.tachiyomi.data.backup.create
 
 import android.content.Context
 import android.net.Uri
 import com.hippo.unifile.UniFile
-import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_APP_PREFS
-import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CATEGORY
-import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CHAPTER
-import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_HISTORY
-import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_SOURCE_PREFS
-import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_TRACK
+import eu.kanade.tachiyomi.data.backup.BackupFileValidator
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CHAPTER
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_HISTORY
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_SOURCE_PREFS
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_TRACK
 import eu.kanade.tachiyomi.data.backup.models.Backup
 import eu.kanade.tachiyomi.data.backup.models.BackupCategory
 import eu.kanade.tachiyomi.data.backup.models.BackupChapter

+ 3 - 1
app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt

@@ -8,7 +8,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
 data class BrokenBackupSource(
     @ProtoNumber(0) var name: String = "",
     @ProtoNumber(1) var sourceId: Long,
-)
+) {
+    fun toBackupSource() = BackupSource(name, sourceId)
+}
 
 @Serializable
 data class BackupSource(

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt → app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.data.backup
+package eu.kanade.tachiyomi.data.backup.restore
 
 import android.content.Context
 import android.net.Uri
@@ -9,6 +9,7 @@ import androidx.work.ForegroundInfo
 import androidx.work.OneTimeWorkRequestBuilder
 import androidx.work.WorkerParameters
 import androidx.work.workDataOf
+import eu.kanade.tachiyomi.data.backup.BackupNotifier
 import eu.kanade.tachiyomi.data.notification.Notifications
 import eu.kanade.tachiyomi.util.system.cancelNotification
 import eu.kanade.tachiyomi.util.system.isRunning
@@ -28,13 +29,12 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
     override suspend fun doWork(): Result {
         val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
             ?: return Result.failure()
-        val sync = inputData.getBoolean(SYNC_KEY, false)
+        val isSync = inputData.getBoolean(SYNC_KEY, false)
 
         setForegroundSafely()
 
         return try {
-            val restorer = BackupRestorer(context, notifier)
-            restorer.syncFromBackup(uri, sync)
+            BackupRestorer(context, notifier, isSync).restore(uri)
             Result.success()
         } catch (e: Exception) {
             if (e is CancellationException) {

+ 156 - 0
app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt

@@ -0,0 +1,156 @@
+package eu.kanade.tachiyomi.data.backup.restore
+
+import android.content.Context
+import android.net.Uri
+import eu.kanade.tachiyomi.data.backup.BackupNotifier
+import eu.kanade.tachiyomi.data.backup.models.BackupCategory
+import eu.kanade.tachiyomi.data.backup.models.BackupManga
+import eu.kanade.tachiyomi.data.backup.models.BackupPreference
+import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
+import eu.kanade.tachiyomi.util.BackupUtil
+import eu.kanade.tachiyomi.util.system.createFileInCacheDir
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.launch
+import tachiyomi.core.i18n.stringResource
+import tachiyomi.i18n.MR
+import java.io.File
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+class BackupRestorer(
+    private val context: Context,
+    private val notifier: BackupNotifier,
+    private val isSync: Boolean,
+
+    private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
+    private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
+    private val mangaRestorer: MangaRestorer = MangaRestorer(),
+) {
+
+    private var restoreAmount = 0
+    private var restoreProgress = 0
+    private val errors = mutableListOf<Pair<Date, String>>()
+
+    /**
+     * Mapping of source ID to source name from backup data
+     */
+    private var sourceMapping: Map<Long, String> = emptyMap()
+
+    suspend fun restore(uri: Uri) {
+        val startTime = System.currentTimeMillis()
+
+        restoreFromFile(uri)
+
+        val time = System.currentTimeMillis() - startTime
+
+        val logFile = writeErrorLog()
+
+        notifier.showRestoreComplete(
+            time,
+            errors.size,
+            logFile.parent,
+            logFile.name,
+            isSync,
+        )
+    }
+
+    private suspend fun restoreFromFile(uri: Uri) {
+        val backup = BackupUtil.decodeBackup(context, uri)
+
+        restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
+
+        // Store source mapping for error messages
+        val backupMaps = backup.backupSources + backup.backupBrokenSources.map { it.toBackupSource() }
+        sourceMapping = backupMaps.associate { it.sourceId to it.name }
+
+        coroutineScope {
+            restoreCategories(backup.backupCategories)
+            restoreAppPreferences(backup.backupPreferences)
+            restoreSourcePreferences(backup.backupSourcePreferences)
+            restoreManga(backup.backupManga, backup.backupCategories)
+
+            // TODO: optionally trigger online library + tracker update
+        }
+    }
+
+    private fun CoroutineScope.restoreCategories(backupCategories: List<BackupCategory>) = launch {
+        ensureActive()
+        categoriesRestorer.restoreCategories(backupCategories)
+
+        restoreProgress += 1
+        notifier.showRestoreProgress(
+            context.stringResource(MR.strings.categories),
+            restoreProgress,
+            restoreAmount,
+            isSync,
+        )
+    }
+
+    private fun CoroutineScope.restoreManga(
+        backupMangas: List<BackupManga>,
+        backupCategories: List<BackupCategory>,
+    ) = launch {
+        mangaRestorer.sortByNew(backupMangas)
+            .forEach {
+                ensureActive()
+
+                try {
+                    mangaRestorer.restoreManga(it, backupCategories)
+                } catch (e: Exception) {
+                    val sourceName = sourceMapping[it.source] ?: it.source.toString()
+                    errors.add(Date() to "${it.title} [$sourceName]: ${e.message}")
+                }
+
+                restoreProgress += 1
+                notifier.showRestoreProgress(it.title, restoreProgress, restoreAmount, isSync)
+            }
+    }
+
+    private fun CoroutineScope.restoreAppPreferences(preferences: List<BackupPreference>) = launch {
+        ensureActive()
+        preferenceRestorer.restoreAppPreferences(preferences)
+
+        restoreProgress += 1
+        notifier.showRestoreProgress(
+            context.stringResource(MR.strings.app_settings),
+            restoreProgress,
+            restoreAmount,
+            isSync,
+        )
+    }
+
+    private fun CoroutineScope.restoreSourcePreferences(preferences: List<BackupSourcePreferences>) = launch {
+        ensureActive()
+        preferenceRestorer.restoreSourcePreferences(preferences)
+
+        restoreProgress += 1
+        notifier.showRestoreProgress(
+            context.stringResource(MR.strings.source_settings),
+            restoreProgress,
+            restoreAmount,
+            isSync,
+        )
+    }
+
+    private fun writeErrorLog(): File {
+        try {
+            if (errors.isNotEmpty()) {
+                val file = context.createFileInCacheDir("tachiyomi_restore.txt")
+                val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
+
+                file.bufferedWriter().use { out ->
+                    errors.forEach { (date, message) ->
+                        out.write("[${sdf.format(date)}] $message\n")
+                    }
+                }
+                return file
+            }
+        } catch (e: Exception) {
+            // Empty
+        }
+        return File("")
+    }
+}

+ 36 - 0
app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt

@@ -0,0 +1,36 @@
+package eu.kanade.tachiyomi.data.backup.restore
+
+import eu.kanade.tachiyomi.data.backup.models.BackupCategory
+import tachiyomi.data.DatabaseHandler
+import tachiyomi.domain.category.interactor.GetCategories
+import tachiyomi.domain.library.service.LibraryPreferences
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class CategoriesRestorer(
+    private val handler: DatabaseHandler = Injekt.get(),
+    private val getCategories: GetCategories = Injekt.get(),
+    private val libraryPreferences: LibraryPreferences = Injekt.get(),
+) {
+
+    suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
+        if (backupCategories.isNotEmpty()) {
+            val dbCategories = getCategories.await()
+            val dbCategoriesByName = dbCategories.associateBy { it.name }
+
+            val categories = backupCategories.map {
+                dbCategoriesByName[it.name]
+                    ?: handler.awaitOneExecutable {
+                        categoriesQueries.insert(it.name, it.order, it.flags)
+                        categoriesQueries.selectLastInsertedRowId()
+                    }.let { id -> it.toCategory(id) }
+            }
+
+            libraryPreferences.categorizedDisplaySettings().set(
+                (dbCategories + categories)
+                    .distinctBy { it.flags }
+                    .size > 1,
+            )
+        }
+    }
+}

+ 21 - 239
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt → app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt

@@ -1,57 +1,29 @@
-package eu.kanade.tachiyomi.data.backup
+package eu.kanade.tachiyomi.data.backup.restore
 
-import android.content.Context
-import android.net.Uri
 import eu.kanade.domain.manga.interactor.UpdateManga
 import eu.kanade.tachiyomi.data.backup.models.BackupCategory
 import eu.kanade.tachiyomi.data.backup.models.BackupChapter
 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.BackupSource
-import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
 import eu.kanade.tachiyomi.data.backup.models.BackupTracking
-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.library.LibraryUpdateJob
-import eu.kanade.tachiyomi.source.sourcePreferences
-import eu.kanade.tachiyomi.util.BackupUtil
-import eu.kanade.tachiyomi.util.system.createFileInCacheDir
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.ensureActive
-import tachiyomi.core.i18n.stringResource
-import tachiyomi.core.preference.AndroidPreferenceStore
-import tachiyomi.core.preference.PreferenceStore
 import tachiyomi.data.DatabaseHandler
 import tachiyomi.data.UpdateStrategyColumnAdapter
 import tachiyomi.domain.category.interactor.GetCategories
 import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
 import tachiyomi.domain.chapter.model.Chapter
-import tachiyomi.domain.library.service.LibraryPreferences
 import tachiyomi.domain.manga.interactor.FetchInterval
 import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
 import tachiyomi.domain.manga.model.Manga
 import tachiyomi.domain.track.interactor.GetTracks
 import tachiyomi.domain.track.interactor.InsertTrack
 import tachiyomi.domain.track.model.Track
-import tachiyomi.i18n.MR
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
-import java.io.File
-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,
-
+class MangaRestorer(
     private val handler: DatabaseHandler = Injekt.get(),
     private val getCategories: GetCategories = Injekt.get(),
     private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
@@ -59,167 +31,48 @@ class BackupRestorer(
     private val updateManga: UpdateManga = Injekt.get(),
     private val getTracks: GetTracks = Injekt.get(),
     private val insertTrack: InsertTrack = Injekt.get(),
-    private val fetchInterval: FetchInterval = Injekt.get(),
-
-    private val preferenceStore: PreferenceStore = Injekt.get(),
-    private val libraryPreferences: LibraryPreferences = Injekt.get(),
+    fetchInterval: FetchInterval = Injekt.get(),
 ) {
 
-    private var restoreAmount = 0
-    private var restoreProgress = 0
-
     private var now = ZonedDateTime.now()
     private var currentFetchWindow = fetchInterval.getWindow(now)
 
-    /**
-     * Mapping of source ID to source name from backup data
-     */
-    private var sourceMapping: Map<Long, String> = emptyMap()
-
-    private val errors = mutableListOf<Pair<Date, String>>()
-
-    suspend fun syncFromBackup(uri: Uri, sync: Boolean) {
-        val startTime = System.currentTimeMillis()
-
-        prepareState()
-        restoreFromFile(uri, sync)
-
-        val endTime = System.currentTimeMillis()
-        val time = endTime - startTime
-
-        val logFile = writeErrorLog()
-
-        notifier.showRestoreComplete(
-            time,
-            errors.size,
-            logFile.parent,
-            logFile.name,
-            sync,
-        )
-    }
-
-    private fun writeErrorLog(): File {
-        try {
-            if (errors.isNotEmpty()) {
-                val file = context.createFileInCacheDir("tachiyomi_restore.txt")
-                val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
-
-                file.bufferedWriter().use { out ->
-                    errors.forEach { (date, message) ->
-                        out.write("[${sdf.format(date)}] $message\n")
-                    }
-                }
-                return file
-            }
-        } catch (e: Exception) {
-            // Empty
-        }
-        return File("")
-    }
-
-    private fun prepareState() {
+    init {
         now = ZonedDateTime.now()
         currentFetchWindow = fetchInterval.getWindow(now)
     }
 
-    private suspend fun restoreFromFile(uri: Uri, sync: Boolean) {
-        val backup = BackupUtil.decodeBackup(context, uri)
-
-        restoreAmount = backup.backupManga.size + 3 // +3 for categories, app prefs, source prefs
-
-        // Store source mapping for error messages
-        val backupMaps = backup.backupBrokenSources.map { BackupSource(it.name, it.sourceId) } + backup.backupSources
-        sourceMapping = backupMaps.associate { it.sourceId to it.name }
-
-        coroutineScope {
-            ensureActive()
-            restoreCategories(backup.backupCategories)
-
-            ensureActive()
-            restoreAppPreferences(backup.backupPreferences)
-
-            ensureActive()
-            restoreSourcePreferences(backup.backupSourcePreferences)
-
-            backup.backupManga.sortByNew()
-                .forEach {
-                    ensureActive()
-                    restoreManga(it, backup.backupCategories, sync)
-                }
-
-            // TODO: optionally trigger online library + tracker update
-        }
-    }
-
-    private suspend fun List<BackupManga>.sortByNew(): List<BackupManga> {
+    suspend fun sortByNew(backupMangas: List<BackupManga>): List<BackupManga> {
         val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() }
             .groupBy({ it.source }, { it.url })
 
-        return this
+        return backupMangas
             .sortedWith(
                 compareBy<BackupManga> { it.url in urlsBySource[it.source].orEmpty() }
                     .then(compareByDescending { it.lastModifiedAt }),
             )
     }
 
-    private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
-        if (backupCategories.isNotEmpty()) {
-            val dbCategories = getCategories.await()
-            val dbCategoriesByName = dbCategories.associateBy { it.name }
-
-            val categories = backupCategories.map {
-                dbCategoriesByName[it.name]
-                    ?: handler.awaitOneExecutable {
-                        categoriesQueries.insert(it.name, it.order, it.flags)
-                        categoriesQueries.selectLastInsertedRowId()
-                    }.let { id -> it.toCategory(id) }
-            }
-
-            libraryPreferences.categorizedDisplaySettings().set(
-                (dbCategories + categories)
-                    .distinctBy { it.flags }
-                    .size > 1,
-            )
-        }
-
-        restoreProgress += 1
-        notifier.showRestoreProgress(
-            context.stringResource(MR.strings.categories),
-            restoreProgress,
-            restoreAmount,
-            false,
-        )
-    }
-
-    private suspend fun restoreManga(
+    suspend fun restoreManga(
         backupManga: BackupManga,
         backupCategories: List<BackupCategory>,
-        sync: Boolean,
     ) {
-        try {
-            val dbManga = findExistingManga(backupManga)
-            val manga = backupManga.getMangaImpl()
-            val restoredManga = if (dbManga == null) {
-                restoreNewManga(manga)
-            } else {
-                restoreExistingManga(manga, dbManga)
-            }
-
-            restoreMangaDetails(
-                manga = restoredManga,
-                chapters = backupManga.chapters,
-                categories = backupManga.categories,
-                backupCategories = backupCategories,
-                history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
-                tracks = backupManga.tracking,
-            )
-        } catch (e: Exception) {
-            val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString()
-            errors.add(Date() to "${backupManga.title} [$sourceName]: ${e.message}")
+        val dbManga = findExistingManga(backupManga)
+        val manga = backupManga.getMangaImpl()
+        val restoredManga = if (dbManga == null) {
+            restoreNewManga(manga)
+        } else {
+            restoreExistingManga(manga, dbManga)
         }
 
-        restoreProgress += 1
-        notifier.showRestoreProgress(backupManga.title, restoreProgress, restoreAmount, sync)
+        restoreMangaDetails(
+            manga = restoredManga,
+            chapters = backupManga.chapters,
+            categories = backupManga.categories,
+            backupCategories = backupCategories,
+            history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
+            tracks = backupManga.tracking,
+        )
     }
 
     private suspend fun findExistingManga(backupManga: BackupManga): Manga? {
@@ -546,75 +399,4 @@ class BackupRestorer(
     }
 
     private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L)
-
-    private fun restoreAppPreferences(preferences: List<BackupPreference>) {
-        restorePreferences(preferences, preferenceStore)
-
-        LibraryUpdateJob.setupTask(context)
-        BackupCreateJob.setupTask(context)
-
-        restoreProgress += 1
-        notifier.showRestoreProgress(
-            context.stringResource(MR.strings.app_settings),
-            restoreProgress,
-            restoreAmount,
-            false,
-        )
-    }
-
-    private fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
-        preferences.forEach {
-            val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
-            restorePreferences(it.prefs, sourcePrefs)
-        }
-
-        restoreProgress += 1
-        notifier.showRestoreProgress(
-            context.stringResource(MR.strings.source_settings),
-            restoreProgress,
-            restoreAmount,
-            false,
-        )
-    }
-
-    private fun restorePreferences(
-        toRestore: List<BackupPreference>,
-        preferenceStore: PreferenceStore,
-    ) {
-        val prefs = preferenceStore.getAll()
-        toRestore.forEach { (key, value) ->
-            when (value) {
-                is IntPreferenceValue -> {
-                    if (prefs[key] is Int?) {
-                        preferenceStore.getInt(key).set(value.value)
-                    }
-                }
-                is LongPreferenceValue -> {
-                    if (prefs[key] is Long?) {
-                        preferenceStore.getLong(key).set(value.value)
-                    }
-                }
-                is FloatPreferenceValue -> {
-                    if (prefs[key] is Float?) {
-                        preferenceStore.getFloat(key).set(value.value)
-                    }
-                }
-                is StringPreferenceValue -> {
-                    if (prefs[key] is String?) {
-                        preferenceStore.getString(key).set(value.value)
-                    }
-                }
-                is BooleanPreferenceValue -> {
-                    if (prefs[key] is Boolean?) {
-                        preferenceStore.getBoolean(key).set(value.value)
-                    }
-                }
-                is StringSetPreferenceValue -> {
-                    if (prefs[key] is Set<*>?) {
-                        preferenceStore.getStringSet(key).set(value.value)
-                    }
-                }
-            }
-        }
-    }
 }

+ 79 - 0
app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt

@@ -0,0 +1,79 @@
+package eu.kanade.tachiyomi.data.backup.restore
+
+import android.content.Context
+import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
+import eu.kanade.tachiyomi.data.backup.models.BackupPreference
+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.library.LibraryUpdateJob
+import eu.kanade.tachiyomi.source.sourcePreferences
+import tachiyomi.core.preference.AndroidPreferenceStore
+import tachiyomi.core.preference.PreferenceStore
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+
+class PreferenceRestorer(
+    private val context: Context,
+    private val preferenceStore: PreferenceStore = Injekt.get(),
+) {
+
+    fun restoreAppPreferences(preferences: List<BackupPreference>) {
+        restorePreferences(preferences, preferenceStore)
+
+        LibraryUpdateJob.setupTask(context)
+        BackupCreateJob.setupTask(context)
+    }
+
+    fun restoreSourcePreferences(preferences: List<BackupSourcePreferences>) {
+        preferences.forEach {
+            val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey))
+            restorePreferences(it.prefs, sourcePrefs)
+        }
+    }
+
+    private fun restorePreferences(
+        toRestore: List<BackupPreference>,
+        preferenceStore: PreferenceStore,
+    ) {
+        val prefs = preferenceStore.getAll()
+        toRestore.forEach { (key, value) ->
+            when (value) {
+                is IntPreferenceValue -> {
+                    if (prefs[key] is Int?) {
+                        preferenceStore.getInt(key).set(value.value)
+                    }
+                }
+                is LongPreferenceValue -> {
+                    if (prefs[key] is Long?) {
+                        preferenceStore.getLong(key).set(value.value)
+                    }
+                }
+                is FloatPreferenceValue -> {
+                    if (prefs[key] is Float?) {
+                        preferenceStore.getFloat(key).set(value.value)
+                    }
+                }
+                is StringPreferenceValue -> {
+                    if (prefs[key] is String?) {
+                        preferenceStore.getString(key).set(value.value)
+                    }
+                }
+                is BooleanPreferenceValue -> {
+                    if (prefs[key] is Boolean?) {
+                        preferenceStore.getBoolean(key).set(value.value)
+                    }
+                }
+                is StringSetPreferenceValue -> {
+                    if (prefs[key] is Set<*>?) {
+                        preferenceStore.getStringSet(key).set(value.value)
+                    }
+                }
+            }
+        }
+    }
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt

@@ -7,7 +7,7 @@ import android.content.Intent
 import android.net.Uri
 import android.os.Build
 import androidx.core.net.toUri
-import eu.kanade.tachiyomi.data.backup.BackupRestoreJob
+import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
 import eu.kanade.tachiyomi.data.download.DownloadManager
 import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
 import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt

@@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util
 
 import android.content.Context
 import android.net.Uri
-import eu.kanade.tachiyomi.data.backup.BackupCreator
+import eu.kanade.tachiyomi.data.backup.create.BackupCreator
 import eu.kanade.tachiyomi.data.backup.models.Backup
 import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
 import okio.buffer