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 c980875f0..3ec3208d0 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 @@ -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) { + private suspend fun restoreChapters(manga: Manga, backupChapters: List) { 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) { + private fun Chapter.forComparison() = + this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L) + + private suspend fun insertNewChapters(chapters: List) { handler.await(true) { chapters.forEach { chapter -> chaptersQueries.insert( @@ -338,7 +351,7 @@ class BackupRestorer( } } - private suspend fun updateKnownChapters(chapters: List) { + private suspend fun updateExistingChapters(chapters: List) { handler.await(true) { chapters.forEach { chapter -> chaptersQueries.update( @@ -393,16 +406,16 @@ class BackupRestorer( private suspend fun restoreMangaDetails( manga: Manga, - chapters: List, + chapters: List, categories: List, backupCategories: List, history: List, - tracks: List, + tracks: List, ): 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) { - // List containing history to be updated + private suspend fun restoreHistory(backupHistory: List) { val toUpdate = mutableListOf() - 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, - ) - } - } - } - - private suspend fun restoreTracking(manga: Manga, tracks: List) { - // Get tracks from database - val dbTracks = handler.awaitList { manga_syncQueries.getTracksByMangaId(manga.id) } - val toUpdate = mutableListOf() - val toInsert = mutableListOf() - - 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, + toUpdate.forEach { payload -> + historyQueries.upsert( + payload.chapterId, + payload.readAt, + payload.sessionReadDuration, ) } } } - if (toInsert.isNotEmpty()) { + } + + private suspend fun restoreTracking(manga: Manga, backupTracks: List) { + 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 (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) { restorePreferences(preferences, preferenceStore) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt index 8b47f0d8d..fe693f4d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupHistory.kt +++ b/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) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt index 8dd429c15..003b1ae19 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupManga.kt +++ b/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 { - return chapters.map { - it.toChapterImpl() - } - } - - fun getTrackingImpl(): List { - return tracking.map { - it.getTrackingImpl() - } - } - companion object { fun copyFrom(manga: Manga): BackupManga { return BackupManga( diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt index b45b30ca2..35d486492 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/models/BackupTracking.kt +++ b/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, diff --git a/data/src/main/java/tachiyomi/data/track/TrackMapper.kt b/data/src/main/java/tachiyomi/data/track/TrackMapper.kt new file mode 100644 index 000000000..8ac852731 --- /dev/null +++ b/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, + ) +} diff --git a/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt b/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt index 1966a013b..19a3daa02 100644 --- a/data/src/main/java/tachiyomi/data/track/TrackRepositoryImpl.kt +++ b/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 { return handler.awaitList { - manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack) + manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack) } } override fun getTracksAsFlow(): Flow> { return handler.subscribeToList { - manga_syncQueries.getTracks(::mapTrack) + manga_syncQueries.getTracks(TrackMapper::mapTrack) } } override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow> { 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, - ) }