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