瀏覽代碼

Skip updating unchanged chapters and tracks when restoring backup

arkon 1 年之前
父節點
當前提交
ad3d915fc5

+ 82 - 90
app/src/main/java/eu/kanade/tachiyomi/data/backup/BackupRestorer.kt

@@ -4,11 +4,13 @@ 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
@@ -25,7 +27,6 @@ import tachiyomi.core.i18n.stringResource
 import tachiyomi.core.preference.AndroidPreferenceStore
 import tachiyomi.core.preference.PreferenceStore
 import tachiyomi.data.DatabaseHandler
-import tachiyomi.data.Manga_sync
 import tachiyomi.data.UpdateStrategyColumnAdapter
 import tachiyomi.domain.category.interactor.GetCategories
 import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
@@ -33,9 +34,10 @@ 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.interactor.GetTracks
+import tachiyomi.domain.track.interactor.InsertTrack
 import tachiyomi.domain.track.model.Track
 import tachiyomi.i18n.MR
 import uy.kohesive.injekt.Injekt
@@ -53,10 +55,11 @@ class BackupRestorer(
 
     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 getTracks: GetTracks = Injekt.get(),
+    private val insertTrack: InsertTrack = Injekt.get(),
     private val fetchInterval: FetchInterval = Injekt.get(),
 
     private val preferenceStore: PreferenceStore = Injekt.get(),
@@ -205,12 +208,11 @@ class BackupRestorer(
 
             restoreMangaDetails(
                 manga = restoredManga,
-                chapters = backupManga.getChaptersImpl(),
+                chapters = backupManga.chapters,
                 categories = backupManga.categories,
                 backupCategories = backupCategories,
-                history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead, it.readDuration) } +
-                    backupManga.history,
-                tracks = backupManga.getTrackingImpl(),
+                history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
+                tracks = backupManga.tracking,
             )
         } catch (e: Exception) {
             val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString()
@@ -283,20 +285,30 @@ class BackupRestorer(
         )
     }
 
-    private suspend fun restoreChapters(manga: Manga, chapters: List<Chapter>) {
+    private suspend fun restoreChapters(manga: Manga, backupChapters: List<BackupChapter>) {
         val dbChaptersByUrl = getChaptersByMangaId.await(manga.id)
             .associateBy { it.url }
 
-        val processed = chapters.map { chapter ->
-            var updatedChapter = chapter
+        val (existingChapters, newChapters) = backupChapters
+            .mapNotNull {
+                val chapter = it.toChapterImpl()
 
-            val dbChapter = dbChaptersByUrl[updatedChapter.url]
-            if (dbChapter != null) {
-                updatedChapter = updatedChapter
+                val dbChapter = dbChaptersByUrl[chapter.url]
+                    ?: // New chapter
+                    return@mapNotNull chapter
+
+                if (chapter.forComparison() == dbChapter.forComparison()) {
+                    // Same state; skip
+                    return@mapNotNull null
+                }
+
+                // Update to an existing chapter
+                var updatedChapter = chapter
                     .copyFrom(dbChapter)
                     .copy(
                         id = dbChapter.id,
-                        bookmark = updatedChapter.bookmark || dbChapter.bookmark,
+                        mangaId = manga.id,
+                        bookmark = chapter.bookmark || dbChapter.bookmark,
                     )
                 if (dbChapter.read && !updatedChapter.read) {
                     updatedChapter = updatedChapter.copy(
@@ -308,17 +320,18 @@ class BackupRestorer(
                         lastPageRead = dbChapter.lastPageRead,
                     )
                 }
+                updatedChapter
             }
+            .partition { it.id > 0 }
 
-            updatedChapter.copy(mangaId = manga.id)
-        }
-
-        val (existingChapters, newChapters) = processed.partition { it.id > 0 }
-        insertChapters(newChapters)
-        updateKnownChapters(existingChapters)
+        insertNewChapters(newChapters)
+        updateExistingChapters(existingChapters)
     }
 
-    private suspend fun insertChapters(chapters: List<Chapter>) {
+    private fun Chapter.forComparison() =
+        this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L)
+
+    private suspend fun insertNewChapters(chapters: List<Chapter>) {
         handler.await(true) {
             chapters.forEach { chapter ->
                 chaptersQueries.insert(
@@ -338,7 +351,7 @@ class BackupRestorer(
         }
     }
 
-    private suspend fun updateKnownChapters(chapters: List<Chapter>) {
+    private suspend fun updateExistingChapters(chapters: List<Chapter>) {
         handler.await(true) {
             chapters.forEach { chapter ->
                 chaptersQueries.update(
@@ -393,16 +406,16 @@ class BackupRestorer(
 
     private suspend fun restoreMangaDetails(
         manga: Manga,
-        chapters: List<Chapter>,
+        chapters: List<BackupChapter>,
         categories: List<Long>,
         backupCategories: List<BackupCategory>,
         history: List<BackupHistory>,
-        tracks: List<Track>,
+        tracks: List<BackupTracking>,
     ): Manga {
-        restoreChapters(manga, chapters)
         restoreCategories(manga, categories, backupCategories)
-        restoreHistory(history)
+        restoreChapters(manga, chapters)
         restoreTracking(manga, tracks)
+        restoreHistory(history)
         updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
         return manga
     }
@@ -441,10 +454,9 @@ class BackupRestorer(
         }
     }
 
-    private suspend fun restoreHistory(history: List<BackupHistory>) {
-        // List containing history to be updated
+    private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
         val toUpdate = mutableListOf<HistoryUpdate>()
-        for ((url, lastRead, readDuration) in history) {
+        for ((url, lastRead, readDuration) in backupHistory) {
             var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
             // Check if history already in database and update
             if (dbHistory != null) {
@@ -474,76 +486,53 @@ class BackupRestorer(
                     }
             }
         }
-        handler.await(true) {
-            toUpdate.forEach { payload ->
-                historyQueries.upsert(
-                    payload.chapterId,
-                    payload.readAt,
-                    payload.sessionReadDuration,
-                )
+        if (toUpdate.isNotEmpty()) {
+            handler.await(true) {
+                toUpdate.forEach { payload ->
+                    historyQueries.upsert(
+                        payload.chapterId,
+                        payload.readAt,
+                        payload.sessionReadDuration,
+                    )
+                }
             }
         }
     }
 
-    private suspend fun restoreTracking(manga: Manga, tracks: List<Track>) {
-        // Get tracks from database
-        val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) }
-        val toUpdate = mutableListOf<Manga_sync>()
-        val toInsert = mutableListOf<Track>()
-
-        tracks
-            // Fix foreign keys with the current manga id
-            .map { it.copy(mangaId = manga.id) }
-            .forEach { track ->
-                var isInDatabase = false
-                for (dbTrack in dbTracks) {
-                    if (track.syncId == dbTrack.sync_id) {
-                        // The sync is already in the db, only update its fields
-                        var temp = dbTrack
-                        if (track.remoteId != dbTrack.remote_id) {
-                            temp = temp.copy(remote_id = track.remoteId)
-                        }
-                        if (track.libraryId != dbTrack.library_id) {
-                            temp = temp.copy(library_id = track.libraryId)
-                        }
-                        temp = temp.copy(last_chapter_read = max(dbTrack.last_chapter_read, track.lastChapterRead))
-                        isInDatabase = true
-                        toUpdate.add(temp)
-                        break
-                    }
-                }
-                if (!isInDatabase) {
-                    // Insert new sync. Let the db assign the id
-                    toInsert.add(track.copy(id = 0))
-                }
-            }
-
-        // Update database
-        if (toUpdate.isNotEmpty()) {
-            handler.await(true) {
-                toUpdate.forEach { track ->
-                    manga_syncQueries.update(
-                        track.manga_id,
-                        track.sync_id,
-                        track.remote_id,
-                        track.library_id,
-                        track.title,
-                        track.last_chapter_read,
-                        track.total_chapters,
-                        track.status,
-                        track.score,
-                        track.remote_url,
-                        track.start_date,
-                        track.finish_date,
-                        track._id,
+    private suspend fun restoreTracking(manga: Manga, backupTracks: List<BackupTracking>) {
+        val dbTrackBySyncId = getTracks.await(manga.id).associateBy { it.syncId }
+
+        val (existingTracks, newTracks) = backupTracks
+            .mapNotNull {
+                val track = it.getTrackImpl()
+                val dbTrack = dbTrackBySyncId[track.syncId]
+                    ?: // New track
+                    return@mapNotNull track.copy(
+                        id = 0, // Let DB assign new ID
+                        mangaId = manga.id,
                     )
+
+                if (track.forComparison() == dbTrack.forComparison()) {
+                    // Same state; skip
+                    return@mapNotNull null
                 }
+
+                // Update to an existing track
+                dbTrack.copy(
+                    remoteId = track.remoteId,
+                    libraryId = track.libraryId,
+                    lastChapterRead = max(dbTrack.lastChapterRead, track.lastChapterRead),
+                )
             }
+            .partition { it.id > 0 }
+
+        if (newTracks.isNotEmpty()) {
+            insertTrack.awaitAll(newTracks)
         }
-        if (toInsert.isNotEmpty()) {
+        if (existingTracks.isNotEmpty()) {
             handler.await(true) {
-                toInsert.forEach { track ->
-                    manga_syncQueries.insert(
+                existingTracks.forEach { track ->
+                    manga_syncQueries.update(
                         track.mangaId,
                         track.syncId,
                         track.remoteId,
@@ -556,12 +545,15 @@ class BackupRestorer(
                         track.remoteUrl,
                         track.startDate,
                         track.finishDate,
+                        track.id,
                     )
                 }
             }
         }
     }
 
+    private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L)
+
     private fun restoreAppPreferences(preferences: List<BackupPreference>) {
         restorePreferences(preferences, preferenceStore)
 

+ 5 - 1
app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt

@@ -16,4 +16,8 @@ data class BrokenBackupHistory(
     @ProtoNumber(0) var url: String,
     @ProtoNumber(1) var lastRead: Long,
     @ProtoNumber(2) var readDuration: Long = 0,
-)
+) {
+    fun toBackupHistory(): BackupHistory {
+        return BackupHistory(url, lastRead, readDuration)
+    }
+}

+ 0 - 14
app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt

@@ -4,9 +4,7 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy
 import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
 import kotlinx.serialization.Serializable
 import kotlinx.serialization.protobuf.ProtoNumber
-import tachiyomi.domain.chapter.model.Chapter
 import tachiyomi.domain.manga.model.Manga
-import tachiyomi.domain.track.model.Track
 
 @Suppress("DEPRECATION")
 @Serializable
@@ -63,18 +61,6 @@ data class BackupManga(
         )
     }
 
-    fun getChaptersImpl(): List<Chapter> {
-        return chapters.map {
-            it.toChapterImpl()
-        }
-    }
-
-    fun getTrackingImpl(): List<Track> {
-        return tracking.map {
-            it.getTrackingImpl()
-        }
-    }
-
     companion object {
         fun copyFrom(manga: Manga): BackupManga {
             return BackupManga(

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt

@@ -30,7 +30,7 @@ data class BackupTracking(
 ) {
 
     @Suppress("DEPRECATION")
-    fun getTrackingImpl(): Track {
+    fun getTrackImpl(): Track {
         return Track(
             id = -1,
             mangaId = -1,

+ 35 - 0
data/src/main/java/tachiyomi/data/track/TrackMapper.kt

@@ -0,0 +1,35 @@
+package tachiyomi.data.track
+
+import tachiyomi.domain.track.model.Track
+
+object TrackMapper {
+    fun mapTrack(
+        id: Long,
+        mangaId: Long,
+        syncId: Long,
+        remoteId: Long,
+        libraryId: Long?,
+        title: String,
+        lastChapterRead: Double,
+        totalChapters: Long,
+        status: Long,
+        score: Double,
+        remoteUrl: String,
+        startDate: Long,
+        finishDate: Long,
+    ): Track = Track(
+        id = id,
+        mangaId = mangaId,
+        syncId = syncId,
+        remoteId = remoteId,
+        libraryId = libraryId,
+        title = title,
+        lastChapterRead = lastChapterRead,
+        totalChapters = totalChapters,
+        status = status,
+        score = score,
+        remoteUrl = remoteUrl,
+        startDate = startDate,
+        finishDate = finishDate,
+    )
+}

+ 4 - 34
data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt

@@ -10,24 +10,24 @@ class TrackRepositoryImpl(
 ) : TrackRepository {
 
     override suspend fun getTrackById(id: Long): Track? {
-        return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, ::mapTrack) }
+        return handler.awaitOneOrNull { manga_syncQueries.getTrackById(id, TrackMapper::mapTrack) }
     }
 
     override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
         return handler.awaitList {
-            manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack)
+            manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
         }
     }
 
     override fun getTracksAsFlow(): Flow<List<Track>> {
         return handler.subscribeToList {
-            manga_syncQueries.getTracks(::mapTrack)
+            manga_syncQueries.getTracks(TrackMapper::mapTrack)
         }
     }
 
     override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
         return handler.subscribeToList {
-            manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack)
+            manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
         }
     }
 
@@ -68,34 +68,4 @@ class TrackRepositoryImpl(
             }
         }
     }
-
-    private fun mapTrack(
-        id: Long,
-        mangaId: Long,
-        syncId: Long,
-        remoteId: Long,
-        libraryId: Long?,
-        title: String,
-        lastChapterRead: Double,
-        totalChapters: Long,
-        status: Long,
-        score: Double,
-        remoteUrl: String,
-        startDate: Long,
-        finishDate: Long,
-    ): Track = Track(
-        id = id,
-        mangaId = mangaId,
-        syncId = syncId,
-        remoteId = remoteId,
-        libraryId = libraryId,
-        title = title,
-        lastChapterRead = lastChapterRead,
-        totalChapters = totalChapters,
-        status = status,
-        score = score,
-        remoteUrl = remoteUrl,
-        startDate = startDate,
-        finishDate = finishDate,
-    )
 }