feat(sync): prevent deleted "ghost chapters" from reappearing during sync. (#1575)
* feat(sync): prevent deleted "ghost chapters" from reappearing during sync. - Pass lastSyncTime down to mergeChapters in SyncService.kt. - Apply timestamp-based tombstoning logic to chapter merging. When a chapter is missing from either the local or remote backup, its `lastModifiedAt` timestamp is checked against the device's last sync time. - Ensure that chapters deleted on one device (or removed by a source) are recognized as deletions and dropped from the merged backup, rather than being erroneously restored as "new" chapters on subsequent syncs. * chore: change timestamp to use duration-based calculations * chore: spotless
This commit is contained in:
@@ -14,6 +14,7 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import logcat.LogPriority
|
||||
import logcat.logcat
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Serializable
|
||||
@@ -135,14 +136,31 @@ abstract class SyncService(
|
||||
"Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}"
|
||||
}
|
||||
|
||||
val lastSyncTime = syncPreferences.lastSyncTimestamp().get().milliseconds.inWholeSeconds
|
||||
val syncOptions = syncPreferences.getSyncSettings()
|
||||
|
||||
val mergedList = (localMangaMap.keys + remoteMangaMap.keys).distinct().mapNotNull { compositeKey ->
|
||||
val local = localMangaMap[compositeKey]
|
||||
val remote = remoteMangaMap[compositeKey]
|
||||
|
||||
// New version comparison logic
|
||||
when {
|
||||
local != null && remote == null -> updateCategories(local, localCategoriesMapByOrder)
|
||||
local == null && remote != null -> updateCategories(remote, remoteCategoriesMapByOrder)
|
||||
local != null && remote == null -> {
|
||||
if (lastSyncTime == 0L || local.lastModifiedAt > lastSyncTime) {
|
||||
updateCategories(local, localCategoriesMapByOrder)
|
||||
} else {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Dropping local manga deleted on remote: ${local.title}." }
|
||||
null
|
||||
}
|
||||
}
|
||||
local == null && remote != null -> {
|
||||
if (lastSyncTime == 0L || remote.lastModifiedAt > lastSyncTime) {
|
||||
updateCategories(remote, remoteCategoriesMapByOrder)
|
||||
} else {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Dropping deleted remote manga: ${remote.title}." }
|
||||
null
|
||||
}
|
||||
}
|
||||
local != null && remote != null -> {
|
||||
// Compare versions to decide which manga to keep
|
||||
if (local.version >= remote.version) {
|
||||
@@ -150,7 +168,7 @@ abstract class SyncService(
|
||||
"Keeping local version of ${local.title} with merged chapters."
|
||||
}
|
||||
updateCategories(
|
||||
local.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
|
||||
local.copy(chapters = mergeChapters(local.chapters, remote.chapters, lastSyncTime, syncOptions.chapters)),
|
||||
localCategoriesMapByOrder,
|
||||
)
|
||||
} else {
|
||||
@@ -158,7 +176,7 @@ abstract class SyncService(
|
||||
"Keeping remote version of ${remote.title} with merged chapters."
|
||||
}
|
||||
updateCategories(
|
||||
remote.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
|
||||
remote.copy(chapters = mergeChapters(local.chapters, remote.chapters, lastSyncTime, syncOptions.chapters)),
|
||||
remoteCategoriesMapByOrder,
|
||||
)
|
||||
}
|
||||
@@ -198,9 +216,15 @@ abstract class SyncService(
|
||||
private fun mergeChapters(
|
||||
localChapters: List<BackupChapter>,
|
||||
remoteChapters: List<BackupChapter>,
|
||||
lastSyncTime: Long,
|
||||
syncingChapters: Boolean,
|
||||
): List<BackupChapter> {
|
||||
val logTag = "MergeChapters"
|
||||
|
||||
if (!syncingChapters) {
|
||||
return remoteChapters // If not syncing chapters, keep remote untouched
|
||||
}
|
||||
|
||||
fun chapterCompositeKey(chapter: BackupChapter): String {
|
||||
return "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
|
||||
}
|
||||
@@ -224,12 +248,22 @@ abstract class SyncService(
|
||||
|
||||
when {
|
||||
localChapter != null && remoteChapter == null -> {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Keeping local chapter: ${localChapter.name}." }
|
||||
localChapter
|
||||
if (lastSyncTime == 0L || localChapter.lastModifiedAt > lastSyncTime) {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Keeping local chapter: ${localChapter.name}." }
|
||||
localChapter
|
||||
} else {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Dropping local chapter deleted on remote: ${localChapter.name}." }
|
||||
null
|
||||
}
|
||||
}
|
||||
localChapter == null && remoteChapter != null -> {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Taking remote chapter: ${remoteChapter.name}." }
|
||||
remoteChapter
|
||||
if (lastSyncTime == 0L || remoteChapter.lastModifiedAt > lastSyncTime) {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Taking remote chapter: ${remoteChapter.name}." }
|
||||
remoteChapter
|
||||
} else {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Dropping deleted remote chapter: ${remoteChapter.name}." }
|
||||
null
|
||||
}
|
||||
}
|
||||
localChapter != null && remoteChapter != null -> {
|
||||
// Use version number to decide which chapter to keep
|
||||
|
||||
Reference in New Issue
Block a user