Selaa lähdekoodia

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.)
arkon 1 vuosi sitten
vanhempi
commit
58daedc89e

+ 16 - 9
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(

+ 127 - 173
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 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 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 var now = ZonedDateTime.now()
-    private var currentFetchWindow = fetchInterval.getWindow(now)
+    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 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<BackupManga>.sortByNew(): List<BackupManga> {
+        val urlsBySource = handler.awaitList { mangasQueries.getAllMangaSourceAndUrl() }
+            .groupBy({ it.source }, { it.url })
+
+        return this
+            .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()
@@ -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<BackupCategory>, 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<BackupCategory>,
+        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),
-            )
-        } else {
-            showRestoreProgress(
-                restoreProgress,
-                restoreAmount,
-                manga.title,
-                context.stringResource(MR.strings.restoring_backup),
-            )
-        }
+        notifier.showRestoreProgress(backupManga.title, restoreProgress, restoreAmount, sync)
     }
 
-    /**
-     * 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 suspend fun findExistingManga(backupManga: BackupManga): Manga? {
+        return getMangaByUrlAndSourceId.await(backupManga.url, backupManga.source)
     }
 
-    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 restoreExistingManga(manga: Manga, dbManga: Manga): Manga {
+        return if (manga.lastModifiedAt > dbManga.lastModifiedAt) {
+            updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id))
+        } else {
+            updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id))
+        }
+    }
+
+    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 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<Chapter>,
-        categories: List<Int>,
-        history: List<BackupHistory>,
-        tracks: List<Track>,
-        backupCategories: List<BackupCategory>,
     ): 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<Chapter>) {
@@ -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<Chapter>) {
         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<Chapter>) {
         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,
+    private suspend fun restoreMangaDetails(
+        manga: Manga,
         chapters: List<Chapter>,
-        categories: List<Int>,
-        history: List<BackupHistory>,
-        tracks: List<Track>,
+        categories: List<Long>,
         backupCategories: List<BackupCategory>,
-    ): Manga {
-        restoreChapters(backupManga, chapters)
-        restoreExtras(backupManga, categories, history, tracks, backupCategories)
-        return backupManga
-    }
-
-    private suspend fun restoreExtras(
-        manga: Manga,
-        categories: List<Int>,
         history: List<BackupHistory>,
         tracks: List<Track>,
-        backupCategories: List<BackupCategory>,
-    ) {
+    ): 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<Int>, backupCategories: List<BackupCategory>) {
+    private suspend fun restoreCategories(
+        manga: Manga,
+        categories: List<Long>,
+        backupCategories: List<BackupCategory>,
+    ) {
         val dbCategories = getCategories.await()
-        val mangaCategoriesToUpdate = mutableListOf<Pair<Long, Long>>()
-
-        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 dbCategoriesByName = dbCategories.associateBy { it.name }
+
+        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<BackupHistory>) {
         // List containing history to be updated
         val toUpdate = mutableListOf<HistoryUpdate>()
@@ -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<Track>) {
         // 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)
-    }
 }

+ 0 - 18
app/src/main/java/eu/kanade/tachiyomi/source/model/SMangaExtensions.kt

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

+ 1 - 1
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))
     }
 

+ 4 - 0
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

+ 1 - 1
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)
     }
 }