diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index a0dbd6f19..11f2aebbd 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/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.data.cache.PagePreviewCache import eu.kanade.tachiyomi.util.storage.DiskUtil diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt index 926c4f604..9be9bb334 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/CreateBackupScreen.kt +++ b/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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 3ad372b01..a84971044 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateFlags.kt similarity index 93% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateFlags.kt index e91719a93..07e5158be 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateFlags.kt +++ b/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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt similarity index 96% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreateJob.kt index f314d9c01..e4fcf6cd0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreateJob.kt +++ b/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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt similarity index 94% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt index 1ffb4f3ce..14a87e584 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupCreator.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/create/BackupCreator.kt @@ -1,16 +1,17 @@ -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_CUSTOM_INFO -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_HISTORY -import eu.kanade.tachiyomi.data.backup.BackupCreateFlags.BACKUP_READ_MANGA -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_CUSTOM_INFO +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_HISTORY +import eu.kanade.tachiyomi.data.backup.create.BackupCreateFlags.BACKUP_READ_MANGA +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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt index 7bf2d0bc3..34e4cac31 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupSource.kt +++ b/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( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt similarity index 92% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestoreJob.kt index e4595a4b5..507993d9c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestoreJob.kt +++ b/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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt new file mode 100644 index 000000000..053ecbe27 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/BackupRestorer.kt @@ -0,0 +1,181 @@ +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.BackupSavedSearch +import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences +import eu.kanade.tachiyomi.util.BackupUtil +import eu.kanade.tachiyomi.util.system.createFileInCacheDir +import exh.source.MERGED_SOURCE_ID +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 tachiyomi.i18n.sy.SYMR +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(), + // SY --> + private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer() + // SY <-- +) { + + private var restoreAmount = 0 + private var restoreProgress = 0 + private val errors = mutableListOf>() + + /** + * Mapping of source ID to source name from backup data + */ + private var sourceMapping: Map = 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 + 4 // +4 for categories, app prefs, source prefs, saved searches + + // 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) + // SY --> + restoreSavedSearches(backup.backupSavedSearches) + // SY <-- + restoreAppPreferences(backup.backupPreferences) + restoreSourcePreferences(backup.backupSourcePreferences) + restoreManga(backup.backupManga, backup.backupCategories) + + // TODO: optionally trigger online library + tracker update + } + } + + private fun CoroutineScope.restoreCategories(backupCategories: List) = launch { + ensureActive() + categoriesRestorer.restoreCategories(backupCategories) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.categories), + restoreProgress, + restoreAmount, + isSync, + ) + } + + // SY --> + private fun CoroutineScope.restoreSavedSearches(backupSavedSearches: List) = launch { + ensureActive() + savedSearchRestorer.restoreSavedSearches(backupSavedSearches) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(SYMR.strings.saved_searches), + restoreProgress, + restoreAmount, + isSync, + ) + } + // SY <-- + + private fun CoroutineScope.restoreManga( + backupMangas: List, + backupCategories: List, + ) = launch { + mangaRestorer.sortByNew(backupMangas) + /* SY --> */.sortedBy { it.source == MERGED_SOURCE_ID } /* SY <-- */ + .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) = launch { + ensureActive() + preferenceRestorer.restoreAppPreferences(preferences) + + restoreProgress += 1 + notifier.showRestoreProgress( + context.stringResource(MR.strings.app_settings), + restoreProgress, + restoreAmount, + isSync, + ) + } + + private fun CoroutineScope.restoreSourcePreferences(preferences: List) = 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("") + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/CategoriesRestorer.kt new file mode 100644 index 000000000..5557bb59f --- /dev/null +++ b/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) { + 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, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt similarity index 62% rename from app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt index bc55434dc..bc684d2ff 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/MangaRestorer.kt @@ -1,7 +1,5 @@ -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 @@ -9,29 +7,8 @@ import eu.kanade.tachiyomi.data.backup.models.BackupFlatMetadata import eu.kanade.tachiyomi.data.backup.models.BackupHistory import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupMergedMangaReference -import eu.kanade.tachiyomi.data.backup.models.BackupPreference -import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch -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 exh.EXHMigrations -import exh.source.MERGED_SOURCE_ID -import exh.util.nullIfBlank -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.data.manga.MangaMapper @@ -39,7 +16,6 @@ import tachiyomi.data.manga.MergedMangaMapper 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.GetFlatMetadataById import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId @@ -50,21 +26,13 @@ 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 tachiyomi.i18n.sy.SYMR 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(), @@ -72,11 +40,7 @@ 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(), // SY --> private val setCustomMangaInfo: SetCustomMangaInfo = Injekt.get(), private val insertFlatMetadata: InsertFlatMetadata = Injekt.get(), @@ -84,208 +48,53 @@ class BackupRestorer( // SY <-- ) { - 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 = emptyMap() - - private val errors = mutableListOf>() - - 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 + 4 // +4 for categories, app prefs, source prefs, saved searches - - // 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) - - // SY --> - ensureActive() - restoreSavedSearches(backup.backupSavedSearches) - // SY <-- - - ensureActive() - restoreAppPreferences(backup.backupPreferences) - - ensureActive() - restoreSourcePreferences(backup.backupSourcePreferences) - - // Restore individual manga - backup.backupManga.sortByNew() - /* SY --> */.sortedBy { it.source == MERGED_SOURCE_ID } /* SY <-- */ - .forEach { - ensureActive() - restoreManga(it, backup.backupCategories, sync) - } - - // TODO: optionally trigger online library + tracker update - } - } - - private suspend fun List.sortByNew(): List { + suspend fun sortByNew(backupMangas: List): List { val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() } .groupBy({ it.source }, { it.url }) - return this + return backupMangas .sortedWith( compareBy { it.url in urlsBySource[it.source].orEmpty() } .then(compareByDescending { it.lastModifiedAt }), ) } - private suspend fun restoreCategories(backupCategories: List) { - 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, - ) - } - - // SY --> - private suspend fun restoreSavedSearches(backupSavedSearches: List) { - if (backupSavedSearches.isEmpty()) return - - val currentSavedSearches = handler.awaitList { - saved_searchQueries.selectNamesAndSources() - } - - handler.await { - backupSavedSearches.filter { backupSavedSearch -> - currentSavedSearches.none { it.source == backupSavedSearch.source && it.name == backupSavedSearch.name } - }.forEach { - saved_searchQueries.insert( - source = it.source, - name = it.name, - query = it.query.nullIfBlank(), - filtersJson = it.filterList.nullIfBlank() - ?.takeUnless { it == "[]" }, - ) - } - } - - restoreProgress += 1 - notifier.showRestoreProgress( - context.stringResource(SYMR.strings.saved_searches), - restoreProgress, - restoreAmount, - false, - ) - } - // SY <-- - - private suspend fun restoreManga( + suspend fun restoreManga( backupManga: BackupManga, backupCategories: List, - sync: Boolean, ) { - try { - val dbManga = findExistingManga(backupManga) - var manga = backupManga.getMangaImpl() - // SY --> - manga = EXHMigrations.migrateBackupEntry(manga) - // SY <-- - 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, - // SY --> - mergedMangaReferences = backupManga.mergedMangaReferences, - flatMetadata = backupManga.flatMetadata, - customManga = backupManga.getCustomMangaInfo() - // SY <-- - ) - } catch (e: Exception) { - val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString() - errors.add(Date() to "${backupManga.title} [$sourceName]: ${e.message}") + val dbManga = findExistingManga(backupManga) + var manga = backupManga.getMangaImpl() + // SY --> + manga = EXHMigrations.migrateBackupEntry(manga) + // SY <-- + 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, + // SY --> + mergedMangaReferences = backupManga.mergedMangaReferences, + flatMetadata = backupManga.flatMetadata, + customManga = backupManga.getCustomMangaInfo() + // SY <-- + ) } private suspend fun findExistingManga(backupManga: BackupManga): Manga? { @@ -631,7 +440,7 @@ class BackupRestorer( * @param manga the merge manga for the references * @param backupMergedMangaReferences the list of backup manga references for the merged manga */ - internal suspend fun restoreMergedMangaReferencesForManga( + private suspend fun restoreMergedMangaReferencesForManga( mergeMangaId: Long, backupMergedMangaReferences: List, ) { @@ -678,88 +487,17 @@ class BackupRestorer( } } - internal suspend fun restoreFlatMetadata(mangaId: Long, backupFlatMetadata: BackupFlatMetadata) { + private suspend fun restoreFlatMetadata(mangaId: Long, backupFlatMetadata: BackupFlatMetadata) { if (getFlatMetadataById.await(mangaId) == null) { insertFlatMetadata.await(backupFlatMetadata.getFlatMetadata(mangaId)) } } - internal fun restoreEditedInfo(mangaJson: CustomMangaInfo?) { + private fun restoreEditedInfo(mangaJson: CustomMangaInfo?) { mangaJson ?: return setCustomMangaInfo.set(mangaJson) } // SY <-- private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L) - - private fun restoreAppPreferences(preferences: List) { - 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) { - 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, - 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) - } - } - } - } - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/PreferenceRestorer.kt new file mode 100644 index 000000000..69622d60b --- /dev/null +++ b/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) { + restorePreferences(preferences, preferenceStore) + + LibraryUpdateJob.setupTask(context) + BackupCreateJob.setupTask(context) + } + + fun restoreSourcePreferences(preferences: List) { + preferences.forEach { + val sourcePrefs = AndroidPreferenceStore(context, sourcePreferences(it.sourceKey)) + restorePreferences(it.prefs, sourcePrefs) + } + } + + private fun restorePreferences( + toRestore: List, + 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) + } + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/SavedSearchRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/SavedSearchRestorer.kt new file mode 100644 index 000000000..4a4456244 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/restore/SavedSearchRestorer.kt @@ -0,0 +1,33 @@ +package eu.kanade.tachiyomi.data.backup.restore + +import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch +import exh.util.nullIfBlank +import tachiyomi.data.DatabaseHandler +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class SavedSearchRestorer( + private val handler: DatabaseHandler = Injekt.get(), +) { + suspend fun restoreSavedSearches(backupSavedSearches: List) { + if (backupSavedSearches.isEmpty()) return + + val currentSavedSearches = handler.awaitList { + saved_searchQueries.selectNamesAndSources() + } + + handler.await { + backupSavedSearches.filter { backupSavedSearch -> + currentSavedSearches.none { it.source == backupSavedSearch.source && it.name == backupSavedSearch.name } + }.forEach { + saved_searchQueries.insert( + source = it.source, + name = it.name, + query = it.query.nullIfBlank(), + filtersJson = it.filterList.nullIfBlank() + ?.takeUnless { it == "[]" }, + ) + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index f4262a1ea..92d5311d5 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt index e67d7cd2f..05401603f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/BackupUtil.kt +++ b/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 diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index ccda925b2..c3e580e7e 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -9,7 +9,7 @@ import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.ui.UiPreferences import eu.kanade.tachiyomi.BuildConfig 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.cache.PagePreviewCache import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.track.TrackerManager