|
@@ -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)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
}
|