feat: Refactor merge logic with composite keys and debugging logs

Refactored the mergeMangaLists and mergeChapters functions to use composite keys for enhanced manga and chapter identification. Implemented composite keys incorporating multiple fields (source, url, title, and author for manga; url, name, and chapterNumber for chapters) to ensure a unique and robust matching process. Added detailed debugging logs at each step of the merge to provide insights into the matching process, making it easier to trace and debug issues related to manga and chapter mismatches. These improvements ensure greater accuracy and reliability in identifying and merging manga and chapters across local and remote lists.

Signed-off-by: KaiserBh <kaiserbh@proton.me>
This commit is contained in:
KaiserBh 2024-01-08 06:52:23 +11:00
parent 86a21e2506
commit 35ce19d8f3
No known key found for this signature in database
GPG Key ID: 14D73B142042BBA9

View File

@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupManga import eu.kanade.tachiyomi.data.backup.models.BackupManga
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import logcat.LogPriority
import logcat.logcat
import tachiyomi.domain.sync.SyncPreferences import tachiyomi.domain.sync.SyncPreferences
import java.time.Instant import java.time.Instant
@ -92,37 +94,84 @@ abstract class SyncService(
localMangaList: List<BackupManga>?, localMangaList: List<BackupManga>?,
remoteMangaList: List<BackupManga>?, remoteMangaList: List<BackupManga>?,
): List<BackupManga> { ): List<BackupManga> {
val logTag = "MergeMangaLists"
// Convert null lists to empty to simplify logic // Convert null lists to empty to simplify logic
val localMangaListSafe = localMangaList.orEmpty() val localMangaListSafe = localMangaList.orEmpty()
val remoteMangaListSafe = remoteMangaList.orEmpty() val remoteMangaListSafe = remoteMangaList.orEmpty()
// Associate both local and remote manga by their unique keys (source and url) logcat(logTag, LogPriority.DEBUG) { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
val localMangaMap = localMangaListSafe.associateBy { Pair(it.source, it.url) }
val remoteMangaMap = remoteMangaListSafe.associateBy { Pair(it.source, it.url) } // Define a function to create a composite key from manga
fun mangaCompositeKey(manga: BackupManga): String {
return "${manga.source}|${manga.url}|${manga.title.lowercase().trim()}|${manga.author?.lowercase()?.trim()}"
}
// Create maps using composite keys
val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) }
val remoteMangaMap = remoteMangaListSafe.associateBy { mangaCompositeKey(it) }
logcat(LogPriority.DEBUG, logTag) { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
// Prepare to merge both sets of manga // Prepare to merge both sets of manga
return (localMangaMap.keys + remoteMangaMap.keys).mapNotNull { key -> val mergedList = (localMangaMap.keys + remoteMangaMap.keys).distinct().mapNotNull { compositeKey ->
val local = localMangaMap[key] val local = localMangaMap[compositeKey]
val remote = remoteMangaMap[key] val remote = remoteMangaMap[compositeKey]
logcat(LogPriority.DEBUG, logTag) {
"Processing key: $compositeKey. Local favorite: ${local?.favorite}, Remote favorite: ${remote?.favorite}"
}
when { when {
local != null && remote == null -> local local != null && remote == null -> {
local == null && remote != null -> remote logcat(LogPriority.DEBUG, logTag) {
"Taking local manga: ${local.title} as it is not present remotely. Favorite status: ${local.favorite}"
}
local
}
local == null && remote != null -> {
logcat(LogPriority.DEBUG, logTag) {
"Taking remote manga: ${remote.title} as it is not present locally. Favorite status: ${remote.favorite}"
}
remote
}
local != null && remote != null -> { local != null && remote != null -> {
// Compare last modified times and merge chapters logcat(LogPriority.DEBUG, logTag) {
val localTime = Instant.ofEpochMilli(local.lastModifiedAt) "Inspecting timestamps for ${local.title}. Local lastModifiedAt: ${local.lastModifiedAt}, Remote lastModifiedAt: ${remote.lastModifiedAt}"
val remoteTime = Instant.ofEpochMilli(remote.lastModifiedAt) }
// Convert seconds to milliseconds for accurate time comparison
val localTime = Instant.ofEpochMilli(local.lastModifiedAt * 1000L)
val remoteTime = Instant.ofEpochMilli(remote.lastModifiedAt * 1000L)
val mergedChapters = mergeChapters(local.chapters, remote.chapters) val mergedChapters = mergeChapters(local.chapters, remote.chapters)
logcat(LogPriority.DEBUG, logTag) {
"Merging manga: ${local.title}. Local time: $localTime, Remote time: $remoteTime, Local favorite: ${local.favorite}, Remote favorite: ${remote.favorite}"
}
if (localTime >= remoteTime) { if (localTime >= remoteTime) {
logcat(LogPriority.DEBUG, logTag) { "Keeping local version of ${local.title} with merged chapters." }
local.copy(chapters = mergedChapters) local.copy(chapters = mergedChapters)
} else { } else {
logcat(LogPriority.DEBUG, logTag) { "Keeping remote version of ${remote.title} with merged chapters." }
remote.copy(chapters = mergedChapters) remote.copy(chapters = mergedChapters)
} }
} }
else -> null else -> {
logcat(LogPriority.DEBUG, logTag) { "No manga found for key: $compositeKey. Skipping." }
null
}
} }
} }
// Counting favorites and non-favorites
val (favorites, nonFavorites) = mergedList.partition { it.favorite }
logcat(LogPriority.DEBUG, logTag) {
"Merge completed. Total merged manga: ${mergedList.size}, Favorites: ${favorites.size}, Non-Favorites: ${nonFavorites.size}"
}
return mergedList
} }
/** /**
@ -142,33 +191,63 @@ abstract class SyncService(
* - If lastModifiedAt is null or missing, the chapter is considered the oldest for safety, ensuring that any chapter with a valid timestamp is preferred. * - If lastModifiedAt is null or missing, the chapter is considered the oldest for safety, ensuring that any chapter with a valid timestamp is preferred.
* - The resulting list contains the most recent chapters from the combined set of local and remote chapters. * - The resulting list contains the most recent chapters from the combined set of local and remote chapters.
*/ */
private fun mergeChapters( private fun mergeChapters(
localChapters: List<BackupChapter>, localChapters: List<BackupChapter>,
remoteChapters: List<BackupChapter>, remoteChapters: List<BackupChapter>,
): List<BackupChapter> { ): List<BackupChapter> {
// Associate chapters by URL for both local and remote val logTag = "MergeChapters"
val localChapterMap = localChapters.associateBy { it.url }
val remoteChapterMap = remoteChapters.associateBy { it.url }
// Merge both chapter maps // Define a function to create a composite key from a chapter
return (localChapterMap.keys + remoteChapterMap.keys).mapNotNull { url -> fun chapterCompositeKey(chapter: BackupChapter): String {
// Determine the most recent chapter by comparing lastModifiedAt, considering null as Instant.MIN return "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
val localChapter = localChapterMap[url] }
val remoteChapter = remoteChapterMap[url]
when { // Create maps using composite keys
localChapter != null && remoteChapter == null -> localChapter val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) }
localChapter == null && remoteChapter != null -> remoteChapter val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) }
localChapter != null && remoteChapter != null -> {
val localInstant = localChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) } ?: Instant.MIN logcat(LogPriority.DEBUG, logTag) { "Starting chapter merge. Local chapters: ${localChapters.size}, Remote chapters: ${remoteChapters.size}" }
val remoteInstant = remoteChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) } ?: Instant.MIN
if (localInstant >= remoteInstant) localChapter else remoteChapter // Merge both chapter maps
val mergedChapters = (localChapterMap.keys + remoteChapterMap.keys).distinct().mapNotNull { compositeKey ->
val localChapter = localChapterMap[compositeKey]
val remoteChapter = remoteChapterMap[compositeKey]
logcat(LogPriority.DEBUG, logTag) {
"Processing chapter key: $compositeKey. Local chapter: ${localChapter != null}, Remote chapter: ${remoteChapter != null}"
}
when {
localChapter != null && remoteChapter == null -> {
logcat(LogPriority.DEBUG, logTag) { "Keeping local chapter: ${localChapter.name}." }
localChapter
}
localChapter == null && remoteChapter != null -> {
logcat(LogPriority.DEBUG, logTag) { "Taking remote chapter: ${remoteChapter.name}." }
remoteChapter
}
localChapter != null && remoteChapter != null -> {
val localInstant = Instant.ofEpochMilli(localChapter.lastModifiedAt * 1000L)
val remoteInstant = Instant.ofEpochMilli(remoteChapter.lastModifiedAt * 1000L)
val chosenChapter = if (localInstant >= remoteInstant) localChapter else remoteChapter
logcat(LogPriority.DEBUG, logTag) {
"Merging chapter: ${chosenChapter.name}. Chosen from: ${if (localInstant >= remoteInstant) "Local" else "Remote"}."
} }
else -> null chosenChapter
}
else -> {
logcat(LogPriority.DEBUG, logTag) { "No chapter found for composite key: $compositeKey. Skipping." }
null
} }
} }
} }
logcat(LogPriority.DEBUG, logTag) { "Chapter merge completed. Total merged chapters: ${mergedChapters.size}" }
return mergedChapters
}
/** /**
* Merges two lists of SyncCategory objects, prioritizing the category with the most recent order value. * Merges two lists of SyncCategory objects, prioritizing the category with the most recent order value.
* *