mirror of
https://github.com/mihonapp/mihon.git
synced 2024-11-15 15:02:49 +01:00
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:
parent
86a21e2506
commit
35ce19d8f3
@ -7,6 +7,8 @@ import eu.kanade.tachiyomi.data.backup.models.BackupChapter
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import logcat.LogPriority
|
||||
import logcat.logcat
|
||||
import tachiyomi.domain.sync.SyncPreferences
|
||||
import java.time.Instant
|
||||
|
||||
@ -92,39 +94,86 @@ abstract class SyncService(
|
||||
localMangaList: List<BackupManga>?,
|
||||
remoteMangaList: List<BackupManga>?,
|
||||
): List<BackupManga> {
|
||||
val logTag = "MergeMangaLists"
|
||||
|
||||
// Convert null lists to empty to simplify logic
|
||||
val localMangaListSafe = localMangaList.orEmpty()
|
||||
val remoteMangaListSafe = remoteMangaList.orEmpty()
|
||||
|
||||
// Associate both local and remote manga by their unique keys (source and url)
|
||||
val localMangaMap = localMangaListSafe.associateBy { Pair(it.source, it.url) }
|
||||
val remoteMangaMap = remoteMangaListSafe.associateBy { Pair(it.source, it.url) }
|
||||
logcat(logTag, LogPriority.DEBUG) { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
|
||||
|
||||
// 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
|
||||
return (localMangaMap.keys + remoteMangaMap.keys).mapNotNull { key ->
|
||||
val local = localMangaMap[key]
|
||||
val remote = remoteMangaMap[key]
|
||||
val mergedList = (localMangaMap.keys + remoteMangaMap.keys).distinct().mapNotNull { compositeKey ->
|
||||
val local = localMangaMap[compositeKey]
|
||||
val remote = remoteMangaMap[compositeKey]
|
||||
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Processing key: $compositeKey. Local favorite: ${local?.favorite}, Remote favorite: ${remote?.favorite}"
|
||||
}
|
||||
|
||||
when {
|
||||
local != null && remote == null -> local
|
||||
local == null && remote != null -> remote
|
||||
local != null && remote == null -> {
|
||||
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 -> {
|
||||
// Compare last modified times and merge chapters
|
||||
val localTime = Instant.ofEpochMilli(local.lastModifiedAt)
|
||||
val remoteTime = Instant.ofEpochMilli(remote.lastModifiedAt)
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Inspecting timestamps for ${local.title}. Local lastModifiedAt: ${local.lastModifiedAt}, Remote lastModifiedAt: ${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)
|
||||
|
||||
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) {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Keeping local version of ${local.title} with merged chapters." }
|
||||
local.copy(chapters = mergedChapters)
|
||||
} else {
|
||||
logcat(LogPriority.DEBUG, logTag) { "Keeping remote version of ${remote.title} with merged chapters." }
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two lists of BackupChapter objects, selecting the most recent chapter based on the lastModifiedAt value.
|
||||
* If lastModifiedAt is null for a chapter, it treats that chapter as the oldest possible for comparison purposes.
|
||||
@ -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.
|
||||
* - 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>,
|
||||
remoteChapters: List<BackupChapter>,
|
||||
): List<BackupChapter> {
|
||||
// Associate chapters by URL for both local and remote
|
||||
val localChapterMap = localChapters.associateBy { it.url }
|
||||
val remoteChapterMap = remoteChapters.associateBy { it.url }
|
||||
): List<BackupChapter> {
|
||||
val logTag = "MergeChapters"
|
||||
|
||||
// Define a function to create a composite key from a chapter
|
||||
fun chapterCompositeKey(chapter: BackupChapter): String {
|
||||
return "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
|
||||
}
|
||||
|
||||
// Create maps using composite keys
|
||||
val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) }
|
||||
val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) }
|
||||
|
||||
logcat(LogPriority.DEBUG, logTag) { "Starting chapter merge. Local chapters: ${localChapters.size}, Remote chapters: ${remoteChapters.size}" }
|
||||
|
||||
// Merge both chapter maps
|
||||
return (localChapterMap.keys + remoteChapterMap.keys).mapNotNull { url ->
|
||||
// Determine the most recent chapter by comparing lastModifiedAt, considering null as Instant.MIN
|
||||
val localChapter = localChapterMap[url]
|
||||
val remoteChapter = remoteChapterMap[url]
|
||||
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 -> localChapter
|
||||
localChapter == null && remoteChapter != null -> remoteChapter
|
||||
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 = localChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) } ?: Instant.MIN
|
||||
val remoteInstant = remoteChapter.lastModifiedAt.let { Instant.ofEpochMilli(it) } ?: Instant.MIN
|
||||
if (localInstant >= remoteInstant) localChapter else remoteChapter
|
||||
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.
|
||||
*
|
||||
|
Loading…
Reference in New Issue
Block a user