Skip updating unchanged chapters and tracks when restoring backup

This commit is contained in:
arkon 2023-12-15 22:42:24 -05:00
parent 36f400d542
commit ad3d915fc5
6 changed files with 129 additions and 142 deletions

View file

@ -4,11 +4,13 @@ import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.domain.manga.interactor.UpdateManga import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.tachiyomi.data.backup.models.BackupCategory 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.BackupHistory
import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.backup.models.BackupPreference import eu.kanade.tachiyomi.data.backup.models.BackupPreference
import eu.kanade.tachiyomi.data.backup.models.BackupSource import eu.kanade.tachiyomi.data.backup.models.BackupSource
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences 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.BooleanPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue import eu.kanade.tachiyomi.data.backup.models.FloatPreferenceValue
import eu.kanade.tachiyomi.data.backup.models.IntPreferenceValue 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.AndroidPreferenceStore
import tachiyomi.core.preference.PreferenceStore import tachiyomi.core.preference.PreferenceStore
import tachiyomi.data.DatabaseHandler import tachiyomi.data.DatabaseHandler
import tachiyomi.data.Manga_sync
import tachiyomi.data.UpdateStrategyColumnAdapter import tachiyomi.data.UpdateStrategyColumnAdapter
import tachiyomi.domain.category.interactor.GetCategories import tachiyomi.domain.category.interactor.GetCategories
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId 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.history.model.HistoryUpdate
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.FetchInterval import tachiyomi.domain.manga.interactor.FetchInterval
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId import tachiyomi.domain.manga.interactor.GetMangaByUrlAndSourceId
import tachiyomi.domain.manga.model.Manga 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.domain.track.model.Track
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -53,10 +55,11 @@ class BackupRestorer(
private val handler: DatabaseHandler = Injekt.get(), private val handler: DatabaseHandler = Injekt.get(),
private val getCategories: GetCategories = Injekt.get(), private val getCategories: GetCategories = Injekt.get(),
private val getManga: GetManga = Injekt.get(),
private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(), private val getMangaByUrlAndSourceId: GetMangaByUrlAndSourceId = Injekt.get(),
private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(), private val getChaptersByMangaId: GetChaptersByMangaId = Injekt.get(),
private val updateManga: UpdateManga = 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 fetchInterval: FetchInterval = Injekt.get(),
private val preferenceStore: PreferenceStore = Injekt.get(), private val preferenceStore: PreferenceStore = Injekt.get(),
@ -205,12 +208,11 @@ class BackupRestorer(
restoreMangaDetails( restoreMangaDetails(
manga = restoredManga, manga = restoredManga,
chapters = backupManga.getChaptersImpl(), chapters = backupManga.chapters,
categories = backupManga.categories, categories = backupManga.categories,
backupCategories = backupCategories, backupCategories = backupCategories,
history = backupManga.brokenHistory.map { BackupHistory(it.url, it.lastRead, it.readDuration) } + history = backupManga.history + backupManga.brokenHistory.map { it.toBackupHistory() },
backupManga.history, tracks = backupManga.tracking,
tracks = backupManga.getTrackingImpl(),
) )
} catch (e: Exception) { } catch (e: Exception) {
val sourceName = sourceMapping[backupManga.source] ?: backupManga.source.toString() 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) val dbChaptersByUrl = getChaptersByMangaId.await(manga.id)
.associateBy { it.url } .associateBy { it.url }
val processed = chapters.map { chapter -> val (existingChapters, newChapters) = backupChapters
var updatedChapter = chapter .mapNotNull {
val chapter = it.toChapterImpl()
val dbChapter = dbChaptersByUrl[updatedChapter.url] val dbChapter = dbChaptersByUrl[chapter.url]
if (dbChapter != null) { ?: // New chapter
updatedChapter = updatedChapter return@mapNotNull chapter
if (chapter.forComparison() == dbChapter.forComparison()) {
// Same state; skip
return@mapNotNull null
}
// Update to an existing chapter
var updatedChapter = chapter
.copyFrom(dbChapter) .copyFrom(dbChapter)
.copy( .copy(
id = dbChapter.id, id = dbChapter.id,
bookmark = updatedChapter.bookmark || dbChapter.bookmark, mangaId = manga.id,
bookmark = chapter.bookmark || dbChapter.bookmark,
) )
if (dbChapter.read && !updatedChapter.read) { if (dbChapter.read && !updatedChapter.read) {
updatedChapter = updatedChapter.copy( updatedChapter = updatedChapter.copy(
@ -308,17 +320,18 @@ class BackupRestorer(
lastPageRead = dbChapter.lastPageRead, lastPageRead = dbChapter.lastPageRead,
) )
} }
updatedChapter
} }
.partition { it.id > 0 }
updatedChapter.copy(mangaId = manga.id) insertNewChapters(newChapters)
} updateExistingChapters(existingChapters)
val (existingChapters, newChapters) = processed.partition { it.id > 0 }
insertChapters(newChapters)
updateKnownChapters(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) { handler.await(true) {
chapters.forEach { chapter -> chapters.forEach { chapter ->
chaptersQueries.insert( 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) { handler.await(true) {
chapters.forEach { chapter -> chapters.forEach { chapter ->
chaptersQueries.update( chaptersQueries.update(
@ -393,16 +406,16 @@ class BackupRestorer(
private suspend fun restoreMangaDetails( private suspend fun restoreMangaDetails(
manga: Manga, manga: Manga,
chapters: List<Chapter>, chapters: List<BackupChapter>,
categories: List<Long>, categories: List<Long>,
backupCategories: List<BackupCategory>, backupCategories: List<BackupCategory>,
history: List<BackupHistory>, history: List<BackupHistory>,
tracks: List<Track>, tracks: List<BackupTracking>,
): Manga { ): Manga {
restoreChapters(manga, chapters)
restoreCategories(manga, categories, backupCategories) restoreCategories(manga, categories, backupCategories)
restoreHistory(history) restoreChapters(manga, chapters)
restoreTracking(manga, tracks) restoreTracking(manga, tracks)
restoreHistory(history)
updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow) updateManga.awaitUpdateFetchInterval(manga, now, currentFetchWindow)
return manga return manga
} }
@ -441,10 +454,9 @@ class BackupRestorer(
} }
} }
private suspend fun restoreHistory(history: List<BackupHistory>) { private suspend fun restoreHistory(backupHistory: List<BackupHistory>) {
// List containing history to be updated
val toUpdate = mutableListOf<HistoryUpdate>() val toUpdate = mutableListOf<HistoryUpdate>()
for ((url, lastRead, readDuration) in history) { for ((url, lastRead, readDuration) in backupHistory) {
var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) } var dbHistory = handler.awaitOneOrNull { historyQueries.getHistoryByChapterUrl(url) }
// Check if history already in database and update // Check if history already in database and update
if (dbHistory != null) { 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<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()) { if (toUpdate.isNotEmpty()) {
handler.await(true) { handler.await(true) {
toUpdate.forEach { track -> toUpdate.forEach { payload ->
manga_syncQueries.update( historyQueries.upsert(
track.manga_id, payload.chapterId,
track.sync_id, payload.readAt,
track.remote_id, payload.sessionReadDuration,
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,
) )
} }
} }
} }
if (toInsert.isNotEmpty()) { }
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 (existingTracks.isNotEmpty()) {
handler.await(true) { handler.await(true) {
toInsert.forEach { track -> existingTracks.forEach { track ->
manga_syncQueries.insert( manga_syncQueries.update(
track.mangaId, track.mangaId,
track.syncId, track.syncId,
track.remoteId, track.remoteId,
@ -556,12 +545,15 @@ class BackupRestorer(
track.remoteUrl, track.remoteUrl,
track.startDate, track.startDate,
track.finishDate, track.finishDate,
track.id,
) )
} }
} }
} }
} }
private fun Track.forComparison() = this.copy(id = 0L, mangaId = 0L)
private fun restoreAppPreferences(preferences: List<BackupPreference>) { private fun restoreAppPreferences(preferences: List<BackupPreference>) {
restorePreferences(preferences, preferenceStore) restorePreferences(preferences, preferenceStore)

View file

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

View file

@ -4,9 +4,7 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.track.model.Track
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
@Serializable @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 { companion object {
fun copyFrom(manga: Manga): BackupManga { fun copyFrom(manga: Manga): BackupManga {
return BackupManga( return BackupManga(

View file

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

View file

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

View file

@ -10,24 +10,24 @@ class TrackRepositoryImpl(
) : TrackRepository { ) : TrackRepository {
override suspend fun getTrackById(id: Long): Track? { 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> { override suspend fun getTracksByMangaId(mangaId: Long): List<Track> {
return handler.awaitList { return handler.awaitList {
manga_syncQueries.getTracksByMangaId(mangaId, ::mapTrack) manga_syncQueries.getTracksByMangaId(mangaId, TrackMapper::mapTrack)
} }
} }
override fun getTracksAsFlow(): Flow<List<Track>> { override fun getTracksAsFlow(): Flow<List<Track>> {
return handler.subscribeToList { return handler.subscribeToList {
manga_syncQueries.getTracks(::mapTrack) manga_syncQueries.getTracks(TrackMapper::mapTrack)
} }
} }
override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> { override fun getTracksByMangaIdAsFlow(mangaId: Long): Flow<List<Track>> {
return handler.subscribeToList { 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,
)
} }