Split restoring logic into smaller classes
This commit is contained in:
parent
5fec881387
commit
cd16522805
14 changed files with 318 additions and 260 deletions
|
@ -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.BasePreferenceWidget
|
||||||
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding
|
||||||
import eu.kanade.presentation.util.relativeTimeSpanString
|
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.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.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
|
|
|
@ -29,8 +29,8 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.util.Screen
|
import eu.kanade.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateJob
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
|
|
@ -7,7 +7,7 @@ import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.tachiyomi.core.security.SecurityPreferences
|
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.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.track.TrackerManager
|
import eu.kanade.tachiyomi.data.track.TrackerManager
|
||||||
import eu.kanade.tachiyomi.network.NetworkPreferences
|
import eu.kanade.tachiyomi.network.NetworkPreferences
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup.create
|
||||||
|
|
||||||
internal object BackupCreateFlags {
|
internal object BackupCreateFlags {
|
||||||
const val BACKUP_CATEGORY = 0x1
|
const val BACKUP_CATEGORY = 0x1
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup.create
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -14,6 +14,8 @@ import androidx.work.PeriodicWorkRequestBuilder
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
import com.hippo.unifile.UniFile
|
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.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
|
@ -1,14 +1,15 @@
|
||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup.create
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_APP_PREFS
|
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_APP_PREFS
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_CHAPTER
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_HISTORY
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_CHAPTER
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_SOURCE_PREFS
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_TRACK
|
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.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
|
@ -8,7 +8,9 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
data class BrokenBackupSource(
|
data class BrokenBackupSource(
|
||||||
@ProtoNumber(0) var name: String = "",
|
@ProtoNumber(0) var name: String = "",
|
||||||
@ProtoNumber(1) var sourceId: Long,
|
@ProtoNumber(1) var sourceId: Long,
|
||||||
)
|
) {
|
||||||
|
fun toBackupSource() = BackupSource(name, sourceId)
|
||||||
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class BackupSource(
|
data class BackupSource(
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup.restore
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
@ -9,6 +9,7 @@ import androidx.work.ForegroundInfo
|
||||||
import androidx.work.OneTimeWorkRequestBuilder
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import androidx.work.workDataOf
|
import androidx.work.workDataOf
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.util.system.cancelNotification
|
import eu.kanade.tachiyomi.util.system.cancelNotification
|
||||||
import eu.kanade.tachiyomi.util.system.isRunning
|
import eu.kanade.tachiyomi.util.system.isRunning
|
||||||
|
@ -28,13 +29,12 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
|
||||||
override suspend fun doWork(): Result {
|
override suspend fun doWork(): Result {
|
||||||
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
|
||||||
?: return Result.failure()
|
?: return Result.failure()
|
||||||
val sync = inputData.getBoolean(SYNC_KEY, false)
|
val isSync = inputData.getBoolean(SYNC_KEY, false)
|
||||||
|
|
||||||
setForegroundSafely()
|
setForegroundSafely()
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val restorer = BackupRestorer(context, notifier)
|
BackupRestorer(context, notifier, isSync).restore(uri)
|
||||||
restorer.syncFromBackup(uri, sync)
|
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) {
|
if (e is CancellationException) {
|
|
@ -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("")
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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.domain.manga.interactor.UpdateManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
import eu.kanade.tachiyomi.data.backup.models.BackupHistory
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
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.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.DatabaseHandler
|
||||||
import tachiyomi.data.UpdateStrategyColumnAdapter
|
import tachiyomi.data.UpdateStrategyColumnAdapter
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
|
||||||
import tachiyomi.domain.manga.interactor.FetchInterval
|
import tachiyomi.domain.manga.interactor.FetchInterval
|
||||||
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
|
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
|
||||||
import tachiyomi.domain.manga.model.Manga
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.track.interactor.GetTracks
|
import tachiyomi.domain.track.interactor.GetTracks
|
||||||
import tachiyomi.domain.track.interactor.InsertTrack
|
import tachiyomi.domain.track.interactor.InsertTrack
|
||||||
import tachiyomi.domain.track.model.Track
|
import tachiyomi.domain.track.model.Track
|
||||||
import tachiyomi.i18n.MR
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class BackupRestorer(
|
class MangaRestorer(
|
||||||
private val context: Context,
|
|
||||||
private val notifier: BackupNotifier,
|
|
||||||
|
|
||||||
private val handler: DatabaseHandler = Injekt.get(),
|
private val handler: DatabaseHandler = Injekt.get(),
|
||||||
private val getCategories: GetCategories = Injekt.get(),
|
private val getCategories: GetCategories = Injekt.get(),
|
||||||
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
|
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
|
||||||
|
@ -59,167 +31,48 @@ class BackupRestorer(
|
||||||
private val updateManga: UpdateManga = Injekt.get(),
|
private val updateManga: UpdateManga = Injekt.get(),
|
||||||
private val getTracks: GetTracks = Injekt.get(),
|
private val getTracks: GetTracks = Injekt.get(),
|
||||||
private val insertTrack: InsertTrack = Injekt.get(),
|
private val insertTrack: InsertTrack = Injekt.get(),
|
||||||
private val fetchInterval: FetchInterval = Injekt.get(),
|
fetchInterval: FetchInterval = Injekt.get(),
|
||||||
|
|
||||||
private val preferenceStore: PreferenceStore = Injekt.get(),
|
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var restoreAmount = 0
|
|
||||||
private var restoreProgress = 0
|
|
||||||
|
|
||||||
private var now = ZonedDateTime.now()
|
private var now = ZonedDateTime.now()
|
||||||
private var currentFetchWindow = fetchInterval.getWindow(now)
|
private var currentFetchWindow = fetchInterval.getWindow(now)
|
||||||
|
|
||||||
/**
|
init {
|
||||||
* 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() {
|
|
||||||
now = ZonedDateTime.now()
|
now = ZonedDateTime.now()
|
||||||
currentFetchWindow = fetchInterval.getWindow(now)
|
currentFetchWindow = fetchInterval.getWindow(now)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreFromFile(uri: Uri, sync: Boolean) {
|
suspend fun sortByNew(backupMangas: List<BackupManga>): List<BackupManga> {
|
||||||
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> {
|
|
||||||
val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() }
|
val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() }
|
||||||
.groupBy({ it.source }, { it.url })
|
.groupBy({ it.source }, { it.url })
|
||||||
|
|
||||||
return this
|
return backupMangas
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
compareBy<BackupManga> { it.url in urlsBySource[it.source].orEmpty() }
|
compareBy<BackupManga> { it.url in urlsBySource[it.source].orEmpty() }
|
||||||
.then(compareByDescending { it.lastModifiedAt }),
|
.then(compareByDescending { it.lastModifiedAt }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun restoreCategories(backupCategories: List<BackupCategory>) {
|
suspend fun restoreManga(
|
||||||
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(
|
|
||||||
backupManga: BackupManga,
|
backupManga: BackupManga,
|
||||||
backupCategories: List<BackupCategory>,
|
backupCategories: List<BackupCategory>,
|
||||||
sync: Boolean,
|
|
||||||
) {
|
) {
|
||||||
try {
|
val dbManga = findExistingManga(backupManga)
|
||||||
val dbManga = findExistingManga(backupManga)
|
val manga = backupManga.getMangaImpl()
|
||||||
val manga = backupManga.getMangaImpl()
|
val restoredManga = if (dbManga == null) {
|
||||||
val restoredManga = if (dbManga == null) {
|
restoreNewManga(manga)
|
||||||
restoreNewManga(manga)
|
} else {
|
||||||
} else {
|
restoreExistingManga(manga, dbManga)
|
||||||
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}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreProgress += 1
|
restoreMangaDetails(
|
||||||
notifier.showRestoreProgress(backupManga.title, restoreProgress, restoreAmount, sync)
|
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? {
|
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 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.net.toUri
|
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.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||||
import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
|
import eu.kanade.tachiyomi.data.updater.AppUpdateDownloadJob
|
||||||
|
|
|
@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.util
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
import eu.kanade.tachiyomi.data.backup.models.BackupSerializer
|
||||||
import okio.buffer
|
import okio.buffer
|
||||||
|
|
Reference in a new issue