From 58daedc89ee18d04e7af5bab12629680dba4096c Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 14 Dec 2023 23:26:02 -0500 Subject: [PATCH] Clean up manga restoring logic Some behavior changes: - It prioritizes new entries, then anything more recently updated - It copies the more recently updated entry's metadata (description, thumbnail, etc.) --- .../tachiyomi/data/backup/BackupNotifier.kt | 25 +- .../tachiyomi/data/backup/BackupRestorer.kt | 302 ++++++++---------- .../source/model/SMangaExtensions.kt | 18 -- .../ui/deeplink/DeepLinkScreenModel.kt | 2 +- .../main/sqldelight/tachiyomi/data/mangas.sq | 4 + .../interactor/GetMangaByUrlAndSourceId.kt | 2 +- 6 files changed, 150 insertions(+), 203 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaExtensions.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt index 71bdbcc08..be914cf5b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupNotifier.kt @@ -68,18 +68,18 @@ class BackupNotifier(private val context: Context) { } } - fun showBackupComplete(unifile: UniFile) { + fun showBackupComplete(file: UniFile) { context.cancelNotification(Notifications.ID_BACKUP_PROGRESS) with(completeNotificationBuilder) { setContentTitle(context.stringResource(MR.strings.backup_created)) - setContentText(unifile.filePath ?: unifile.name) + setContentText(file.filePath ?: file.name) clearActions() addAction( R.drawable.ic_share_24dp, context.stringResource(MR.strings.action_share), - NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri), + NotificationReceiver.shareBackupPendingBroadcast(context, file.uri), ) show(Notifications.ID_BACKUP_COMPLETE) @@ -88,13 +88,16 @@ class BackupNotifier(private val context: Context) { fun showRestoreProgress( content: String = "", - contentTitle: String = context.stringResource( - MR.strings.restoring_backup, - ), progress: Int = 0, maxAmount: Int = 100, + sync: Boolean = false, ): NotificationCompat.Builder { val builder = with(progressNotificationBuilder) { + val contentTitle = if (sync) { + context.stringResource(MR.strings.syncing_library) + } else { + context.stringResource(MR.strings.restoring_backup) + } setContentTitle(contentTitle) if (!preferences.hideNotificationContent().get()) { @@ -133,10 +136,14 @@ class BackupNotifier(private val context: Context) { errorCount: Int, path: String?, file: String?, - contentTitle: String = context.stringResource( - MR.strings.restore_completed, - ), + sync: Boolean, ) { + val contentTitle = if (sync) { + context.stringResource(MR.strings.library_sync_complete) + } else { + context.stringResource(MR.strings.restore_completed) + } + context.cancelNotification(Notifications.ID_RESTORE_PROGRESS) val timeString = context.stringResource( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt index 32829963e..c980875f0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt @@ -16,7 +16,6 @@ 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.model.copyFrom import eu.kanade.tachiyomi.source.sourcePreferences import eu.kanade.tachiyomi.util.BackupUtil import eu.kanade.tachiyomi.util.system.createFileInCacheDir @@ -27,7 +26,6 @@ import tachiyomi.core.preference.AndroidPreferenceStore import tachiyomi.core.preference.PreferenceStore import tachiyomi.data.DatabaseHandler import tachiyomi.data.Manga_sync -import tachiyomi.data.Mangas import tachiyomi.data.UpdateStrategyColumnAdapter import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId @@ -35,6 +33,8 @@ import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.history.model.HistoryUpdate import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.manga.interactor.FetchInterval +import tachiyomi.domain.manga.interactor.GetManga +import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.track.model.Track import tachiyomi.i18n.MR @@ -50,23 +50,25 @@ import kotlin.math.max class BackupRestorer( private val context: Context, private val notifier: BackupNotifier, + + private val handler: DatabaseHandler = Injekt.get(), + private val getCategories: GetCategories = Injekt.get(), + private val getManga: GetManga = Injekt.get(), + private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(), + private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), + private val updateManga: UpdateManga = Injekt.get(), + private val fetchInterval: FetchInterval = Injekt.get(), + + private val preferenceStore: PreferenceStore = Injekt.get(), + private val libraryPreferences: LibraryPreferences = Injekt.get(), ) { - private val handler: DatabaseHandler = Injekt.get() - private val updateManga: UpdateManga = Injekt.get() - private val getCategories: GetCategories = Injekt.get() - private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get() - private val fetchInterval: FetchInterval = Injekt.get() - - private val preferenceStore: PreferenceStore = Injekt.get() - private val libraryPreferences: LibraryPreferences = Injekt.get() - - private var now = ZonedDateTime.now() - private var currentFetchWindow = fetchInterval.getWindow(now) - 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 */ @@ -76,27 +78,22 @@ class BackupRestorer( suspend fun syncFromBackup(uri: Uri, sync: Boolean) { val startTime = System.currentTimeMillis() - restoreProgress = 0 - errors.clear() - performRestore(uri, sync) + prepareState() + restoreFromFile(uri, sync) val endTime = System.currentTimeMillis() val time = endTime - startTime val logFile = writeErrorLog() - if (sync) { - notifier.showRestoreComplete( - time, - errors.size, - logFile.parent, - logFile.name, - contentTitle = context.stringResource(MR.strings.library_sync_complete), - ) - } else { - notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name) - } + notifier.showRestoreComplete( + time, + errors.size, + logFile.parent, + logFile.name, + sync, + ) } private fun writeErrorLog(): File { @@ -118,7 +115,12 @@ class BackupRestorer( return File("") } - private suspend fun performRestore(uri: Uri, sync: Boolean) { + private fun prepareState() { + 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 @@ -126,8 +128,6 @@ class BackupRestorer( // 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 } - now = ZonedDateTime.now() - currentFetchWindow = fetchInterval.getWindow(now) coroutineScope { ensureActive() @@ -139,16 +139,27 @@ class BackupRestorer( ensureActive() restoreSourcePreferences(backup.backupSourcePreferences) - // Restore individual manga - backup.backupManga.forEach { - ensureActive() - restoreManga(it, backup.backupCategories, sync) - } + backup.backupManga.sortByNew() + .forEach { + ensureActive() + restoreManga(it, backup.backupCategories, sync) + } // TODO: optionally trigger online library + tracker update } } + private suspend fun List.sortByNew(): List { + val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() } + .groupBy({ it.source }, { it.url }) + + return this + .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() @@ -170,75 +181,72 @@ class BackupRestorer( } restoreProgress += 1 - showRestoreProgress( + notifier.showRestoreProgress( + context.stringResource(MR.strings.categories), restoreProgress, restoreAmount, - context.stringResource(MR.strings.categories), - context.stringResource(MR.strings.restoring_backup), + false, ) } - private suspend fun restoreManga(backupManga: BackupManga, backupCategories: List, sync: Boolean) { - val manga = backupManga.getMangaImpl() - val chapters = backupManga.getChaptersImpl() - val categories = backupManga.categories.map { it.toInt() } - val history = - backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead, it.readDuration) } + backupManga.history - val tracks = backupManga.getTrackingImpl() - + private suspend fun restoreManga( + backupManga: BackupManga, + backupCategories: List, + sync: Boolean, + ) { try { - val dbManga = getMangaFromDatabase(manga.url, manga.source) + val dbManga = findExistingManga(backupManga) + val manga = backupManga.getMangaImpl() val restoredManga = if (dbManga == null) { - // Manga not in database - restoreExistingManga(manga, chapters, categories, history, tracks, backupCategories) + restoreNewManga(manga) } else { - // Manga in database - // Copy information from manga already in database - val updatedManga = restoreExistingManga(manga, dbManga) - // Fetch rest of manga information - restoreNewManga(updatedManga, chapters, categories, history, tracks, backupCategories) + restoreExistingManga(manga, dbManga) } - updateManga.awaitUpdateFetchInterval(restoredManga, now, currentFetchWindow) + + restoreMangaDetails( + manga = restoredManga, + chapters = backupManga.getChaptersImpl(), + categories = backupManga.categories, + backupCategories = backupCategories, + history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead, it.readDuration) } + + backupManga.history, + tracks = backupManga.getTrackingImpl(), + ) } catch (e: Exception) { - val sourceName = sourceMapping[manga.source] ?: manga.source.toString() - errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}") + val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString() + errors.add(Date() to "${backupManga.title} [$sourceName]: ${e.message}") } restoreProgress += 1 - if (sync) { - showRestoreProgress( - restoreProgress, - restoreAmount, - manga.title, - context.stringResource(MR.strings.syncing_library), - ) + notifier.showRestoreProgress(backupManga.title, restoreProgress, restoreAmount, sync) + } + + private suspend fun findExistingManga(backupManga: BackupManga): Manga? { + return getMangaByUrlAndSourceId.await(backupManga.url, backupManga.source) + } + + private suspend fun restoreExistingManga(manga: Manga, dbManga: Manga): Manga { + return if (manga.lastModifiedAt > dbManga.lastModifiedAt) { + updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id)) } else { - showRestoreProgress( - restoreProgress, - restoreAmount, - manga.title, - context.stringResource(MR.strings.restoring_backup), - ) + updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id)) } } - /** - * Returns manga - * - * @return [Manga], null if not found - */ - private suspend fun getMangaFromDatabase(url: String, source: Long): Mangas? { - return handler.awaitOneOrNull { mangasQueries.getMangaByUrlAndSource(url, source) } + private fun Manga.copyFrom(newer: Manga): Manga { + return this.copy( + favorite = this.favorite || newer.favorite, + author = newer.author, + artist = newer.artist, + description = newer.description, + genre = newer.genre, + thumbnailUrl = newer.thumbnailUrl, + status = newer.status, + initialized = this.initialized || newer.initialized, + ) } - private suspend fun restoreExistingManga(manga: Manga, dbManga: Mangas): Manga { - var updatedManga = manga.copy(id = dbManga._id) - updatedManga = updatedManga.copyFrom(dbManga) - updateManga(updatedManga) - return updatedManga - } - - private suspend fun updateManga(manga: Manga): Long { + private suspend fun updateManga(manga: Manga): Manga { handler.await(true) { mangasQueries.update( source = manga.source, @@ -263,28 +271,16 @@ class BackupRestorer( updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode), ) } - return manga.id + return manga } - /** - * Fetches manga information - * - * @param manga manga that needs updating - * @param chapters chapters of manga that needs updating - * @param categories categories that need updating - */ - private suspend fun restoreExistingManga( + private suspend fun restoreNewManga( manga: Manga, - chapters: List, - categories: List, - history: List, - tracks: List, - backupCategories: List, ): Manga { - val fetchedManga = restoreNewManga(manga) - restoreChapters(fetchedManga, chapters) - restoreExtras(fetchedManga, categories, history, tracks, backupCategories) - return fetchedManga + return manga.copy( + initialized = manga.description != null, + id = insertManga(manga), + ) } private suspend fun restoreChapters(manga: Manga, chapters: List) { @@ -318,13 +314,10 @@ class BackupRestorer( } val (existingChapters, newChapters) = processed.partition { it.id > 0 } - updateKnownChapters(existingChapters) insertChapters(newChapters) + updateKnownChapters(existingChapters) } - /** - * Inserts list of chapters - */ private suspend fun insertChapters(chapters: List) { handler.await(true) { chapters.forEach { chapter -> @@ -345,9 +338,6 @@ class BackupRestorer( } } - /** - * Updates a list of chapters with known database ids - */ private suspend fun updateKnownChapters(chapters: List) { handler.await(true) { chapters.forEach { chapter -> @@ -369,19 +359,6 @@ class BackupRestorer( } } - /** - * Fetches manga information - * - * @param manga manga that needs updating - * @return Updated manga info. - */ - private suspend fun restoreNewManga(manga: Manga): Manga { - return manga.copy( - initialized = manga.description != null, - id = insertManga(manga), - ) - } - /** * Inserts manga and returns id * @@ -414,29 +391,20 @@ class BackupRestorer( } } - private suspend fun restoreNewManga( - backupManga: Manga, - chapters: List, - categories: List, - history: List, - tracks: List, - backupCategories: List, - ): Manga { - restoreChapters(backupManga, chapters) - restoreExtras(backupManga, categories, history, tracks, backupCategories) - return backupManga - } - - private suspend fun restoreExtras( + private suspend fun restoreMangaDetails( manga: Manga, - categories: List, + chapters: List, + categories: List, + backupCategories: List, history: List, tracks: List, - backupCategories: List, - ) { + ): Manga { + restoreChapters(manga, chapters) restoreCategories(manga, categories, backupCategories) restoreHistory(history) restoreTracking(manga, tracks) + updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow) + return manga } /** @@ -445,23 +413,24 @@ class BackupRestorer( * @param manga the manga whose categories have to be restored. * @param categories the categories to restore. */ - private suspend fun restoreCategories(manga: Manga, categories: List, backupCategories: List) { + private suspend fun restoreCategories( + manga: Manga, + categories: List, + backupCategories: List, + ) { val dbCategories = getCategories.await() - val mangaCategoriesToUpdate = mutableListOf>() + val dbCategoriesByName = dbCategories.associateBy { it.name } - categories.forEach { backupCategoryOrder -> - backupCategories.firstOrNull { - it.order == backupCategoryOrder.toLong() - }?.let { backupCategory -> - dbCategories.firstOrNull { dbCategory -> - dbCategory.name == backupCategory.name - }?.let { dbCategory -> - mangaCategoriesToUpdate.add(Pair(manga.id, dbCategory.id)) + val backupCategoriesByOrder = backupCategories.associateBy { it.order } + + val mangaCategoriesToUpdate = categories.mapNotNull { backupCategoryOrder -> + backupCategoriesByOrder[backupCategoryOrder]?.let { backupCategory -> + dbCategoriesByName[backupCategory.name]?.let { dbCategory -> + Pair(manga.id, dbCategory.id) } } } - // Update database if (mangaCategoriesToUpdate.isNotEmpty()) { handler.await(true) { mangas_categoriesQueries.deleteMangaCategoryByMangaId(manga.id) @@ -472,11 +441,6 @@ class BackupRestorer( } } - /** - * Restore history from Json - * - * @param history list containing history to be restored - */ private suspend fun restoreHistory(history: List) { // List containing history to be updated val toUpdate = mutableListOf() @@ -496,7 +460,7 @@ class BackupRestorer( ), ) } else { - // If not in database create + // If not in database, create handler .awaitOneOrNull { chaptersQueries.getChapterByUrl(url) } ?.let { @@ -521,12 +485,6 @@ class BackupRestorer( } } - /** - * Restores the sync of a manga. - * - * @param manga the manga whose sync have to be restored. - * @param tracks the track list to restore. - */ private suspend fun restoreTracking(manga: Manga, tracks: List) { // Get tracks from database val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) } @@ -611,11 +569,11 @@ class BackupRestorer( BackupCreateJob.setupTask(context) restoreProgress += 1 - showRestoreProgress( + notifier.showRestoreProgress( + context.stringResource(MR.strings.app_settings), restoreProgress, restoreAmount, - context.stringResource(MR.strings.app_settings), - context.stringResource(MR.strings.restoring_backup), + false, ) } @@ -626,11 +584,11 @@ class BackupRestorer( } restoreProgress += 1 - showRestoreProgress( + notifier.showRestoreProgress( + context.stringResource(MR.strings.source_settings), restoreProgress, restoreAmount, - context.stringResource(MR.strings.source_settings), - context.stringResource(MR.strings.restoring_backup), + false, ) } @@ -674,8 +632,4 @@ class BackupRestorer( } } } - - private fun showRestoreProgress(progress: Int, amount: Int, title: String, contentTitle: String) { - notifier.showRestoreProgress(title, contentTitle, progress, amount) - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaExtensions.kt deleted file mode 100644 index 812c6110f..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaExtensions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package eu.kanade.tachiyomi.source.model - -import tachiyomi.data.Mangas -import tachiyomi.domain.manga.model.Manga - -fun Manga.copyFrom(other: Mangas): Manga { - var manga = this - other.author?.let { manga = manga.copy(author = it) } - other.artist?.let { manga = manga.copy(artist = it) } - other.description?.let { manga = manga.copy(description = it) } - other.genre?.let { manga = manga.copy(genre = it) } - other.thumbnail_url?.let { manga = manga.copy(thumbnailUrl = it) } - manga = manga.copy(status = other.status) - if (!initialized) { - manga = manga.copy(initialized = other.initialized) - } - return manga -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt index 51a39ea0a..e21821430 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/DeepLinkScreenModel.kt @@ -74,7 +74,7 @@ class DeepLinkScreenModel( } private suspend fun getMangaFromSManga(sManga: SManga, sourceId: Long): Manga { - return getMangaByUrlAndSourceId.awaitManga(sManga.url, sourceId) + return getMangaByUrlAndSourceId.await(sManga.url, sourceId) ?: networkToLocalManga.await(sManga.toDomainManga(sourceId)) } diff --git a/data/src/main/sqldelight/tachiyomi/data/mangas.sq b/data/src/main/sqldelight/tachiyomi/data/mangas.sq index 220b908ad..d944ab0b1 100644 --- a/data/src/main/sqldelight/tachiyomi/data/mangas.sq +++ b/data/src/main/sqldelight/tachiyomi/data/mangas.sq @@ -70,6 +70,10 @@ getAllManga: SELECT * FROM mangas; +getAllMangaSourceAndUrl: +SELECT source, url +FROM mangas; + getMangasWithFavoriteTimestamp: SELECT * FROM mangas diff --git a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt index 507000d82..c245a7da0 100644 --- a/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt +++ b/domain/src/main/java/tachiyomi/domain/manga/interactor/GetMangaByUrlAndSourceId.kt @@ -6,7 +6,7 @@ import tachiyomi.domain.manga.repository.MangaRepository class GetMangaByUrlAndSourceId( private val mangaRepository: MangaRepository, ) { - suspend fun awaitManga(url: String, sourceId: Long): Manga? { + suspend fun await(url: String, sourceId: Long): Manga? { return mangaRepository.getMangaByUrlAndSourceId(url, sourceId) } }