mirror of
https://github.com/mihonapp/mihon.git
synced 2025-08-19 21:11:31 +02:00
Compare commits
3 Commits
57e6bf1d13
...
ed2b7df11d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed2b7df11d | ||
|
|
7c85f50705 | ||
|
|
157a996f40 |
@@ -22,7 +22,7 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "app.mihon"
|
||||
|
||||
versionCode = 4
|
||||
versionCode = 5
|
||||
versionName = "0.16.3"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
|
||||
@@ -98,4 +98,6 @@ private fun Manga.toBackupManga() =
|
||||
updateStrategy = this.updateStrategy,
|
||||
lastModifiedAt = this.lastModifiedAt,
|
||||
favoriteModifiedAt = this.favoriteModifiedAt,
|
||||
version = this.version,
|
||||
isSyncing = this.isSyncing,
|
||||
)
|
||||
|
||||
@@ -21,6 +21,8 @@ data class BackupChapter(
|
||||
@ProtoNumber(9) var chapterNumber: Float = 0F,
|
||||
@ProtoNumber(10) var sourceOrder: Long = 0,
|
||||
@ProtoNumber(11) var lastModifiedAt: Long = 0,
|
||||
@ProtoNumber(12) var version: Long = 0,
|
||||
@ProtoNumber(13) var isSyncing: Long = 0,
|
||||
) {
|
||||
fun toChapterImpl(): Chapter {
|
||||
return Chapter.create().copy(
|
||||
@@ -35,6 +37,8 @@ data class BackupChapter(
|
||||
dateUpload = this@BackupChapter.dateUpload,
|
||||
sourceOrder = this@BackupChapter.sourceOrder,
|
||||
lastModifiedAt = this@BackupChapter.lastModifiedAt,
|
||||
version = this@BackupChapter.version,
|
||||
isSyncing = this@BackupChapter.isSyncing
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -53,6 +57,8 @@ val backupChapterMapper = {
|
||||
dateFetch: Long,
|
||||
dateUpload: Long,
|
||||
lastModifiedAt: Long,
|
||||
version: Long,
|
||||
isSyncing: Long,
|
||||
->
|
||||
BackupChapter(
|
||||
url = url,
|
||||
@@ -66,5 +72,7 @@ val backupChapterMapper = {
|
||||
dateUpload = dateUpload,
|
||||
sourceOrder = sourceOrder,
|
||||
lastModifiedAt = lastModifiedAt,
|
||||
version = version,
|
||||
isSyncing = isSyncing
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,8 @@ data class BackupManga(
|
||||
@ProtoNumber(106) var lastModifiedAt: Long = 0,
|
||||
@ProtoNumber(107) var favoriteModifiedAt: Long? = null,
|
||||
@ProtoNumber(108) var excludedScanlators: List<String> = emptyList(),
|
||||
@ProtoNumber(109) var version: Long = 0,
|
||||
@ProtoNumber(110) var isSyncing: Long = 0,
|
||||
) {
|
||||
fun getMangaImpl(): Manga {
|
||||
return Manga.create().copy(
|
||||
@@ -58,6 +60,8 @@ data class BackupManga(
|
||||
updateStrategy = this@BackupManga.updateStrategy,
|
||||
lastModifiedAt = this@BackupManga.lastModifiedAt,
|
||||
favoriteModifiedAt = this@BackupManga.favoriteModifiedAt,
|
||||
version = this@BackupManga.version,
|
||||
isSyncing = this@BackupManga.isSyncing
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,11 @@ class MangaRestorer(
|
||||
tracks = backupManga.tracking,
|
||||
excludedScanlators = backupManga.excludedScanlators,
|
||||
)
|
||||
|
||||
if (isSync) {
|
||||
mangasQueries.resetIsSyncing()
|
||||
chaptersQueries.resetIsSyncing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +89,7 @@ class MangaRestorer(
|
||||
}
|
||||
|
||||
private suspend fun restoreExistingManga(manga: Manga, dbManga: Manga): Manga {
|
||||
return if (manga.lastModifiedAt > dbManga.lastModifiedAt) {
|
||||
return if (manga.version > dbManga.version) {
|
||||
updateManga(dbManga.copyFrom(manga).copy(id = dbManga.id))
|
||||
} else {
|
||||
updateManga(manga.copyFrom(dbManga).copy(id = dbManga.id))
|
||||
@@ -92,6 +97,21 @@ class MangaRestorer(
|
||||
}
|
||||
|
||||
private fun Manga.copyFrom(newer: Manga): Manga {
|
||||
if (isSync) {
|
||||
return this.copy(
|
||||
favorite = this.favorite || newer.favorite,
|
||||
author = newer.author,
|
||||
artist = newer.artist,
|
||||
description = newer.description,
|
||||
genre = newer.genre,
|
||||
thumbnailUrl = newer.thumbnailUrl,
|
||||
status = newer.status,
|
||||
initialized = this.initialized || newer.initialized,
|
||||
version = newer.version,
|
||||
isSyncing = 1,
|
||||
)
|
||||
}
|
||||
|
||||
return this.copy(
|
||||
favorite = this.favorite || newer.favorite,
|
||||
author = newer.author,
|
||||
@@ -101,6 +121,7 @@ class MangaRestorer(
|
||||
thumbnailUrl = newer.thumbnailUrl,
|
||||
status = newer.status,
|
||||
initialized = this.initialized || newer.initialized,
|
||||
version = newer.version,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -127,6 +148,8 @@ class MangaRestorer(
|
||||
dateAdded = manga.dateAdded,
|
||||
mangaId = manga.id,
|
||||
updateStrategy = manga.updateStrategy.let(UpdateStrategyColumnAdapter::encode),
|
||||
version = manga.version,
|
||||
isSyncing = manga.isSyncing
|
||||
)
|
||||
}
|
||||
return manga
|
||||
@@ -138,6 +161,7 @@ class MangaRestorer(
|
||||
return manga.copy(
|
||||
initialized = manga.description != null,
|
||||
id = insertManga(manga),
|
||||
version = manga.version
|
||||
)
|
||||
}
|
||||
|
||||
@@ -169,6 +193,7 @@ class MangaRestorer(
|
||||
bookmark = chapter.bookmark || dbChapter.bookmark,
|
||||
read = chapter.read,
|
||||
lastPageRead = chapter.lastPageRead,
|
||||
isSyncing = 1,
|
||||
)
|
||||
} else {
|
||||
chapter.copyFrom(dbChapter).let {
|
||||
@@ -184,7 +209,7 @@ class MangaRestorer(
|
||||
}
|
||||
|
||||
private fun Chapter.forComparison() =
|
||||
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L)
|
||||
this.copy(id = 0L, mangaId = 0L, dateFetch = 0L, dateUpload = 0L, lastModifiedAt = 0L, version = 0L)
|
||||
|
||||
private suspend fun insertNewChapters(chapters: List<Chapter>) {
|
||||
handler.await(true) {
|
||||
@@ -201,6 +226,7 @@ class MangaRestorer(
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
chapter.version,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -222,6 +248,8 @@ class MangaRestorer(
|
||||
dateFetch = null,
|
||||
dateUpload = null,
|
||||
chapterId = chapter.id,
|
||||
version = chapter.version,
|
||||
isSyncing = chapter.isSyncing,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -254,6 +282,7 @@ class MangaRestorer(
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
updateStrategy = manga.updateStrategy,
|
||||
version = manga.version,
|
||||
)
|
||||
mangasQueries.selectLastInsertedRowId()
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ interface Chapter : SChapter, Serializable {
|
||||
var source_order: Int
|
||||
|
||||
var last_modified: Long
|
||||
|
||||
var version: Long
|
||||
|
||||
var isSyncing: Long
|
||||
}
|
||||
|
||||
fun Chapter.toDomainChapter(): DomainChapter? {
|
||||
@@ -39,5 +43,7 @@ fun Chapter.toDomainChapter(): DomainChapter? {
|
||||
chapterNumber = chapter_number.toDouble(),
|
||||
scanlator = scanlator,
|
||||
lastModifiedAt = last_modified,
|
||||
version = version,
|
||||
isSyncing = isSyncing,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,10 @@ class ChapterImpl : Chapter {
|
||||
|
||||
override var last_modified: Long = 0
|
||||
|
||||
override var version: Long = 0
|
||||
|
||||
override var isSyncing: Long = 0
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other == null || javaClass != other.javaClass) return false
|
||||
|
||||
@@ -23,7 +23,6 @@ import tachiyomi.data.Chapters
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
import tachiyomi.data.manga.MangaMapper.mapManga
|
||||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||
import tachiyomi.domain.manga.model.Manga
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -47,7 +46,6 @@ class SyncManager(
|
||||
encodeDefaults = true
|
||||
ignoreUnknownKeys = true
|
||||
},
|
||||
private val getFavorites: GetFavorites = Injekt.get(),
|
||||
private val getCategories: GetCategories = Injekt.get(),
|
||||
) {
|
||||
private val backupCreator: BackupCreator = BackupCreator(context, false)
|
||||
@@ -72,8 +70,15 @@ class SyncManager(
|
||||
* from the database using the BackupManager, then synchronizes the data with a sync service.
|
||||
*/
|
||||
suspend fun syncData() {
|
||||
// Reset isSyncing in case it was left over or failed syncing during restore.
|
||||
handler.await(inTransaction = true) {
|
||||
mangasQueries.resetIsSyncing()
|
||||
chaptersQueries.resetIsSyncing()
|
||||
}
|
||||
|
||||
val syncOptions = syncPreferences.getSyncSettings()
|
||||
val databaseManga = getAllMangaFromDB()
|
||||
val databaseManga = getAllMangaThatNeedsSync()
|
||||
|
||||
val backupOptions = BackupOptions(
|
||||
libraryEntries = syncOptions.libraryEntries,
|
||||
categories = syncOptions.categories,
|
||||
@@ -198,132 +203,47 @@ class SyncManager(
|
||||
return handler.awaitList { mangasQueries.getAllManga(::mapManga) }
|
||||
}
|
||||
|
||||
private suspend fun getAllMangaThatNeedsSync(): List<Manga> {
|
||||
return handler.awaitList { mangasQueries.getMangasWithFavoriteTimestamp(::mapManga) }
|
||||
}
|
||||
|
||||
private suspend fun isMangaDifferent(localManga: Manga, remoteManga: BackupManga): Boolean {
|
||||
val localChapters = handler.await { chaptersQueries.getChaptersByMangaId(localManga.id, 0).executeAsList() }
|
||||
val localCategories = getCategories.await(localManga.id).map { it.order }
|
||||
val logTag = "isMangaDifferent"
|
||||
|
||||
// Logging the start of comparison
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Comparing local manga (Title: ${localManga.title}) with remote manga (Title: ${remoteManga.title})"
|
||||
}
|
||||
|
||||
var isDifferent = false
|
||||
|
||||
// Compare each field and log if they are different
|
||||
if (localManga.source != remoteManga.source) {
|
||||
logDifference(logTag, "Source", localManga.source.toString(), remoteManga.source.toString())
|
||||
isDifferent = true
|
||||
}
|
||||
if (localManga.url != remoteManga.url) {
|
||||
logDifference(logTag, "URL", localManga.url, remoteManga.url)
|
||||
isDifferent = true
|
||||
}
|
||||
if (localManga.favorite != remoteManga.favorite) {
|
||||
logDifference(logTag, "Favorite", localManga.favorite.toString(), remoteManga.favorite.toString())
|
||||
isDifferent = true
|
||||
}
|
||||
if (areChaptersDifferent(localChapters, remoteManga.chapters)) {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Chapters are different for manga: ${localManga.title}"
|
||||
}
|
||||
isDifferent = true
|
||||
}
|
||||
|
||||
if (localCategories.toSet() != remoteManga.categories.toSet()) {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Categories differ for manga: ${localManga.title}. " +
|
||||
"Local categories: ${localCategories.joinToString()}, " +
|
||||
"Remote categories: ${remoteManga.categories.joinToString()}"
|
||||
}
|
||||
isDifferent = true
|
||||
}
|
||||
|
||||
// Log final result
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Manga difference check result for local manga (Title: ${localManga.title}): $isDifferent"
|
||||
}
|
||||
|
||||
return isDifferent
|
||||
}
|
||||
|
||||
private fun logDifference(tag: String, field: String, localValue: String, remoteValue: String) {
|
||||
logcat(LogPriority.DEBUG, tag) {
|
||||
"Difference found in $field. Local: $localValue, Remote: $remoteValue"
|
||||
}
|
||||
}
|
||||
|
||||
private fun areChaptersDifferent(localChapters: List<Chapters>, remoteChapters: List<BackupChapter>): Boolean {
|
||||
val logTag = "areChaptersDifferent"
|
||||
|
||||
// Early return if the sizes are different
|
||||
if (localChapters.size != remoteChapters.size) {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Chapter lists differ in size. Local: ${localChapters.size}, Remote: ${remoteChapters.size}"
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Convert all remote chapters to Chapter instances
|
||||
val convertedRemoteChapters = remoteChapters.map { it.toChapterImpl() }
|
||||
if (localManga.version != remoteManga.version) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Create a map for the local chapters for efficient comparison
|
||||
if (localCategories.toSet() != remoteManga.categories.toSet()) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
private fun areChaptersDifferent(localChapters: List<Chapters>, remoteChapters: List<BackupChapter>): Boolean {
|
||||
val localChapterMap = localChapters.associateBy { it.url }
|
||||
val remoteChapterMap = remoteChapters.associateBy { it.url }
|
||||
|
||||
var isDifferent = false
|
||||
|
||||
// Check for any differences
|
||||
for (remoteChapter in convertedRemoteChapters) {
|
||||
val localChapter = localChapterMap[remoteChapter.url]
|
||||
|
||||
if (localChapter == null) {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Local chapter not found for URL: ${remoteChapter.url}"
|
||||
}
|
||||
isDifferent = true
|
||||
continue
|
||||
}
|
||||
|
||||
if (localChapter.url != remoteChapter.url) {
|
||||
logDifference(logTag, "URL", localChapter.url, remoteChapter.url)
|
||||
isDifferent = true
|
||||
}
|
||||
if (localChapter.read != remoteChapter.read) {
|
||||
logDifference(logTag, "Read Status", localChapter.read.toString(), remoteChapter.read.toString())
|
||||
isDifferent = true
|
||||
}
|
||||
|
||||
if (localChapter.bookmark != remoteChapter.bookmark) {
|
||||
logDifference(
|
||||
logTag,
|
||||
"Bookmark Status",
|
||||
localChapter.bookmark.toString(),
|
||||
remoteChapter.bookmark.toString(),
|
||||
)
|
||||
isDifferent = true
|
||||
}
|
||||
|
||||
if (localChapter.last_page_read != remoteChapter.lastPageRead) {
|
||||
logDifference(
|
||||
logTag,
|
||||
"Last Page Read",
|
||||
localChapter.last_page_read.toString(),
|
||||
remoteChapter.lastPageRead.toString(),
|
||||
)
|
||||
isDifferent = true
|
||||
}
|
||||
|
||||
// Break the loop if a difference is found
|
||||
if (isDifferent) break
|
||||
if (localChapterMap.size != remoteChapterMap.size) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (!isDifferent) {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"No differences found in chapters."
|
||||
for ((url, localChapter) in localChapterMap) {
|
||||
val remoteChapter = remoteChapterMap[url]
|
||||
|
||||
// If a matching remote chapter doesn't exist, or the version numbers are different, consider them different
|
||||
if (remoteChapter == null || localChapter.version != remoteChapter.version) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return isDifferent
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -339,8 +259,8 @@ class SyncManager(
|
||||
val logTag = "filterFavoritesAndNonFavorites"
|
||||
|
||||
val elapsedTimeMillis = measureTimeMillis {
|
||||
val databaseMangaFavorites = getFavorites.await()
|
||||
val localMangaMap = databaseMangaFavorites.associateBy {
|
||||
val databaseManga = getAllMangaFromDB()
|
||||
val localMangaMap = databaseManga.associateBy {
|
||||
Triple(it.source, it.url, it.title)
|
||||
}
|
||||
|
||||
@@ -384,10 +304,12 @@ class SyncManager(
|
||||
*/
|
||||
private suspend fun updateNonFavorites(nonFavorites: List<BackupManga>) {
|
||||
val localMangaList = getAllMangaFromDB()
|
||||
val localMangaMap = localMangaList.associateBy { it.url }
|
||||
|
||||
val localMangaMap = localMangaList.associateBy { Triple(it.source, it.url, it.title) }
|
||||
|
||||
nonFavorites.forEach { nonFavorite ->
|
||||
localMangaMap[nonFavorite.url]?.let { localManga ->
|
||||
val key = Triple(nonFavorite.source, nonFavorite.url, nonFavorite.title)
|
||||
localMangaMap[key]?.let { localManga ->
|
||||
if (localManga.favorite != nonFavorite.favorite) {
|
||||
val updatedManga = localManga.copy(favorite = nonFavorite.favorite)
|
||||
mangaRestorer.updateManga(updatedManga)
|
||||
|
||||
@@ -13,7 +13,6 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import logcat.LogPriority
|
||||
import logcat.logcat
|
||||
import java.time.Instant
|
||||
|
||||
@Serializable
|
||||
data class SyncData(
|
||||
@@ -100,7 +99,6 @@ abstract class SyncService(
|
||||
): List<BackupManga> {
|
||||
val logTag = "MergeMangaLists"
|
||||
|
||||
// Convert null lists to empty to simplify logic
|
||||
val localMangaListSafe = localMangaList.orEmpty()
|
||||
val remoteMangaListSafe = remoteMangaList.orEmpty()
|
||||
|
||||
@@ -108,7 +106,6 @@ abstract class SyncService(
|
||||
"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()}"
|
||||
}
|
||||
@@ -121,65 +118,31 @@ abstract class SyncService(
|
||||
"Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}"
|
||||
}
|
||||
|
||||
// Prepare to merge both sets of manga
|
||||
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}"
|
||||
}
|
||||
|
||||
// New version comparison logic
|
||||
when {
|
||||
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 -> local
|
||||
local == null && remote != null -> remote
|
||||
local != null && remote != null -> {
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Inspecting timestamps for ${local.title}. " +
|
||||
"Local lastModifiedAt: ${local.lastModifiedAt * 1000L}, " +
|
||||
"Remote lastModifiedAt: ${remote.lastModifiedAt * 1000L}"
|
||||
}
|
||||
// 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) {
|
||||
// Compare versions to decide which manga to keep
|
||||
if (local.version >= remote.version) {
|
||||
logcat(
|
||||
LogPriority.DEBUG,
|
||||
logTag,
|
||||
logTag
|
||||
) { "Keeping local version of ${local.title} with merged chapters." }
|
||||
local.copy(chapters = mergedChapters)
|
||||
local.copy(chapters = mergeChapters(local.chapters, remote.chapters))
|
||||
} else {
|
||||
logcat(
|
||||
LogPriority.DEBUG,
|
||||
logTag,
|
||||
logTag
|
||||
) { "Keeping remote version of ${remote.title} with merged chapters." }
|
||||
remote.copy(chapters = mergedChapters)
|
||||
remote.copy(chapters = mergeChapters(local.chapters, remote.chapters))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
logcat(LogPriority.DEBUG, logTag) { "No manga found for key: $compositeKey. Skipping." }
|
||||
null
|
||||
}
|
||||
else -> null // No manga found for key
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,12 +163,10 @@ abstract class SyncService(
|
||||
): 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) }
|
||||
|
||||
@@ -213,7 +174,7 @@ abstract class SyncService(
|
||||
"Starting chapter merge. Local chapters: ${localChapters.size}, Remote chapters: ${remoteChapters.size}"
|
||||
}
|
||||
|
||||
// Merge both chapter maps
|
||||
// Merge both chapter maps based on version numbers
|
||||
val mergedChapters = (localChapterMap.keys + remoteChapterMap.keys).distinct().mapNotNull { compositeKey ->
|
||||
val localChapter = localChapterMap[compositeKey]
|
||||
val remoteChapter = remoteChapterMap[compositeKey]
|
||||
@@ -233,16 +194,12 @@ abstract class SyncService(
|
||||
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
|
||||
// Use version number to decide which chapter to keep
|
||||
val chosenChapter = if (localChapter.version >= remoteChapter.version) localChapter else remoteChapter
|
||||
logcat(LogPriority.DEBUG, logTag) {
|
||||
"Merging chapter: ${chosenChapter.name}. Chosen from: ${if (localInstant >= remoteInstant) {
|
||||
"Local"
|
||||
} else {
|
||||
"Remote"
|
||||
}}."
|
||||
"Merging chapter: ${chosenChapter.name}. Chosen version from: ${
|
||||
if (localChapter.version >= remoteChapter.version) "Local" else "Remote"
|
||||
}, Local version: ${localChapter.version}, Remote version: ${remoteChapter.version}."
|
||||
}
|
||||
chosenChapter
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class ChapterRepositoryImpl(
|
||||
chapter.sourceOrder,
|
||||
chapter.dateFetch,
|
||||
chapter.dateUpload,
|
||||
chapter.version
|
||||
)
|
||||
val lastInsertId = chaptersQueries.selectLastInsertedRowId().executeAsOne()
|
||||
chapter.copy(id = lastInsertId)
|
||||
@@ -64,6 +65,8 @@ class ChapterRepositoryImpl(
|
||||
dateFetch = chapterUpdate.dateFetch,
|
||||
dateUpload = chapterUpdate.dateUpload,
|
||||
chapterId = chapterUpdate.id,
|
||||
version = chapterUpdate.version,
|
||||
isSyncing = chapterUpdate.isSyncing,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -138,6 +141,8 @@ class ChapterRepositoryImpl(
|
||||
dateFetch: Long,
|
||||
dateUpload: Long,
|
||||
lastModifiedAt: Long,
|
||||
version: Long,
|
||||
isSyncing: Long,
|
||||
): Chapter = Chapter(
|
||||
id = id,
|
||||
mangaId = mangaId,
|
||||
@@ -152,5 +157,7 @@ class ChapterRepositoryImpl(
|
||||
chapterNumber = chapterNumber,
|
||||
scanlator = scanlator,
|
||||
lastModifiedAt = lastModifiedAt,
|
||||
version = version,
|
||||
isSyncing = isSyncing,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ object MangaMapper {
|
||||
calculateInterval: Long,
|
||||
lastModifiedAt: Long,
|
||||
favoriteModifiedAt: Long?,
|
||||
version: Long,
|
||||
isSyncing: Long,
|
||||
): Manga = Manga(
|
||||
id = id,
|
||||
source = source,
|
||||
@@ -51,6 +53,8 @@ object MangaMapper {
|
||||
initialized = initialized,
|
||||
lastModifiedAt = lastModifiedAt,
|
||||
favoriteModifiedAt = favoriteModifiedAt,
|
||||
version = version,
|
||||
isSyncing = isSyncing,
|
||||
)
|
||||
|
||||
fun mapLibraryManga(
|
||||
@@ -76,6 +80,8 @@ object MangaMapper {
|
||||
calculateInterval: Long,
|
||||
lastModifiedAt: Long,
|
||||
favoriteModifiedAt: Long?,
|
||||
version: Long,
|
||||
isSyncing: Long,
|
||||
totalCount: Long,
|
||||
readCount: Double,
|
||||
latestUpload: Long,
|
||||
@@ -107,6 +113,8 @@ object MangaMapper {
|
||||
calculateInterval,
|
||||
lastModifiedAt,
|
||||
favoriteModifiedAt,
|
||||
version,
|
||||
isSyncing,
|
||||
),
|
||||
category = category,
|
||||
totalChapters = totalCount,
|
||||
|
||||
@@ -106,6 +106,7 @@ class MangaRepositoryImpl(
|
||||
coverLastModified = manga.coverLastModified,
|
||||
dateAdded = manga.dateAdded,
|
||||
updateStrategy = manga.updateStrategy,
|
||||
version = manga.version,
|
||||
)
|
||||
mangasQueries.selectLastInsertedRowId()
|
||||
}
|
||||
@@ -155,6 +156,8 @@ class MangaRepositoryImpl(
|
||||
dateAdded = value.dateAdded,
|
||||
mangaId = value.id,
|
||||
updateStrategy = value.updateStrategy?.let(UpdateStrategyColumnAdapter::encode),
|
||||
version = value.version,
|
||||
isSyncing = value.isSyncing,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ CREATE TABLE chapters(
|
||||
date_fetch INTEGER NOT NULL,
|
||||
date_upload INTEGER NOT NULL,
|
||||
last_modified_at INTEGER NOT NULL DEFAULT 0,
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
is_syncing INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
@@ -30,6 +32,16 @@ BEGIN
|
||||
WHERE _id = new._id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER update_chapter_version AFTER UPDATE ON chapters
|
||||
BEGIN
|
||||
UPDATE chapters SET version = version + 1
|
||||
WHERE _id = new._id AND new.is_syncing = 0 AND (
|
||||
new.read != old.read OR
|
||||
new.bookmark != old.bookmark OR
|
||||
new.last_page_read != old.last_page_read
|
||||
);
|
||||
END;
|
||||
|
||||
getChapterById:
|
||||
SELECT *
|
||||
FROM chapters
|
||||
@@ -73,9 +85,14 @@ removeChaptersWithIds:
|
||||
DELETE FROM chapters
|
||||
WHERE _id IN :chapterIds;
|
||||
|
||||
resetIsSyncing:
|
||||
UPDATE chapters
|
||||
SET is_syncing = 0
|
||||
WHERE is_syncing = 1;
|
||||
|
||||
insert:
|
||||
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at)
|
||||
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0);
|
||||
INSERT INTO chapters(manga_id, url, name, scanlator, read, bookmark, last_page_read, chapter_number, source_order, date_fetch, date_upload, last_modified_at, version, is_syncing)
|
||||
VALUES (:mangaId, :url, :name, :scanlator, :read, :bookmark, :lastPageRead, :chapterNumber, :sourceOrder, :dateFetch, :dateUpload, 0, :version, 0);
|
||||
|
||||
update:
|
||||
UPDATE chapters
|
||||
@@ -89,7 +106,9 @@ SET manga_id = coalesce(:mangaId, manga_id),
|
||||
chapter_number = coalesce(:chapterNumber, chapter_number),
|
||||
source_order = coalesce(:sourceOrder, source_order),
|
||||
date_fetch = coalesce(:dateFetch, date_fetch),
|
||||
date_upload = coalesce(:dateUpload, date_upload)
|
||||
date_upload = coalesce(:dateUpload, date_upload),
|
||||
version = coalesce(:version, version),
|
||||
is_syncing = coalesce(:isSyncing, is_syncing)
|
||||
WHERE _id = :chapterId;
|
||||
|
||||
selectLastInsertedRowId:
|
||||
|
||||
@@ -25,7 +25,9 @@ CREATE TABLE mangas(
|
||||
update_strategy INTEGER AS UpdateStrategy NOT NULL DEFAULT 0,
|
||||
calculate_interval INTEGER DEFAULT 0 NOT NULL,
|
||||
last_modified_at INTEGER NOT NULL DEFAULT 0,
|
||||
favorite_modified_at INTEGER
|
||||
favorite_modified_at INTEGER,
|
||||
version INTEGER NOT NULL DEFAULT 0,
|
||||
is_syncing INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
|
||||
@@ -48,6 +50,16 @@ BEGIN
|
||||
WHERE _id = new._id;
|
||||
END;
|
||||
|
||||
CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas
|
||||
BEGIN
|
||||
UPDATE mangas SET version = version + 1
|
||||
WHERE _id = new._id AND new.is_syncing = 0 AND (
|
||||
new.url != old.url OR
|
||||
new.description != old.description OR
|
||||
new.favorite != old.favorite
|
||||
);
|
||||
END;
|
||||
|
||||
getMangaById:
|
||||
SELECT *
|
||||
FROM mangas
|
||||
@@ -104,6 +116,11 @@ resetViewerFlags:
|
||||
UPDATE mangas
|
||||
SET viewer = 0;
|
||||
|
||||
resetIsSyncing:
|
||||
UPDATE mangas
|
||||
SET is_syncing = 0
|
||||
WHERE is_syncing = 1;
|
||||
|
||||
getSourceIdsWithNonLibraryManga:
|
||||
SELECT source, COUNT(*) AS manga_count
|
||||
FROM mangas
|
||||
@@ -116,8 +133,8 @@ WHERE favorite = 0
|
||||
AND source IN :sourceIds;
|
||||
|
||||
insert:
|
||||
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at)
|
||||
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0);
|
||||
INSERT INTO mangas(source, url, artist, author, description, genre, title, status, thumbnail_url, favorite, last_update, next_update, initialized, viewer, chapter_flags, cover_last_modified, date_added, update_strategy, calculate_interval, last_modified_at, version)
|
||||
VALUES (:source, :url, :artist, :author, :description, :genre, :title, :status, :thumbnailUrl, :favorite, :lastUpdate, :nextUpdate, :initialized, :viewerFlags, :chapterFlags, :coverLastModified, :dateAdded, :updateStrategy, :calculateInterval, 0, :version);
|
||||
|
||||
update:
|
||||
UPDATE mangas SET
|
||||
@@ -139,7 +156,9 @@ UPDATE mangas SET
|
||||
cover_last_modified = coalesce(:coverLastModified, cover_last_modified),
|
||||
date_added = coalesce(:dateAdded, date_added),
|
||||
update_strategy = coalesce(:updateStrategy, update_strategy),
|
||||
calculate_interval = coalesce(:calculateInterval, calculate_interval)
|
||||
calculate_interval = coalesce(:calculateInterval, calculate_interval),
|
||||
version = coalesce(:version, version),
|
||||
is_syncing = coalesce(:isSyncing, is_syncing)
|
||||
WHERE _id = :mangaId;
|
||||
|
||||
selectLastInsertedRowId:
|
||||
|
||||
@@ -2,25 +2,22 @@ CREATE TABLE mangas_categories(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
manga_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
last_modified_at INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY(category_id) REFERENCES categories (_id)
|
||||
ON DELETE CASCADE,
|
||||
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TRIGGER update_last_modified_at_mangas_categories
|
||||
AFTER UPDATE ON mangas_categories
|
||||
FOR EACH ROW
|
||||
CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories
|
||||
BEGIN
|
||||
UPDATE mangas_categories
|
||||
SET last_modified_at = strftime('%s', 'now')
|
||||
WHERE _id = new._id;
|
||||
UPDATE mangas
|
||||
SET version = version + 1
|
||||
WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0;
|
||||
END;
|
||||
|
||||
insert:
|
||||
INSERT INTO mangas_categories(manga_id, category_id, last_modified_at)
|
||||
VALUES (:mangaId, :categoryId, 0);
|
||||
INSERT INTO mangas_categories(manga_id, category_id)
|
||||
VALUES (:mangaId, :categoryId);
|
||||
|
||||
deleteMangaCategoryByMangaId:
|
||||
DELETE FROM mangas_categories
|
||||
|
||||
40
data/src/main/sqldelight/tachiyomi/migrations/2.sqm
Normal file
40
data/src/main/sqldelight/tachiyomi/migrations/2.sqm
Normal file
@@ -0,0 +1,40 @@
|
||||
-- Mangas table
|
||||
ALTER TABLE mangas ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE mangas ADD COLUMN is_syncing INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Chapters table
|
||||
ALTER TABLE chapters ADD COLUMN version INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE chapters ADD COLUMN is_syncing INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- Mangas triggers
|
||||
DROP TRIGGER IF EXISTS update_manga_version;
|
||||
CREATE TRIGGER update_manga_version AFTER UPDATE ON mangas
|
||||
BEGIN
|
||||
UPDATE mangas SET version = version + 1
|
||||
WHERE _id = new._id AND new.is_syncing = 0 AND (
|
||||
new.url != old.url OR
|
||||
new.description != old.description OR
|
||||
new.favorite != old.favorite
|
||||
);
|
||||
END;
|
||||
|
||||
-- Chapters triggers
|
||||
DROP TRIGGER IF EXISTS update_chapter_version;
|
||||
CREATE TRIGGER update_chapter_version AFTER UPDATE ON chapters
|
||||
BEGIN
|
||||
UPDATE chapters SET version = version + 1
|
||||
WHERE _id = new._id AND new.is_syncing = 0 AND (
|
||||
new.read != old.read OR
|
||||
new.bookmark != old.bookmark OR
|
||||
new.last_page_read != old.last_page_read
|
||||
);
|
||||
END;
|
||||
|
||||
-- manga_categories table
|
||||
DROP TRIGGER IF EXISTS insert_manga_category_update_version;
|
||||
CREATE TRIGGER insert_manga_category_update_version AFTER INSERT ON mangas_categories
|
||||
BEGIN
|
||||
UPDATE mangas
|
||||
SET version = version + 1
|
||||
WHERE _id = new.manga_id AND (SELECT is_syncing FROM mangas WHERE _id = new.manga_id) = 0;
|
||||
END;
|
||||
@@ -14,6 +14,8 @@ data class Chapter(
|
||||
val chapterNumber: Double,
|
||||
val scanlator: String?,
|
||||
val lastModifiedAt: Long,
|
||||
val version: Long,
|
||||
val isSyncing: Long,
|
||||
) {
|
||||
val isRecognizedNumber: Boolean
|
||||
get() = chapterNumber >= 0f
|
||||
@@ -43,6 +45,8 @@ data class Chapter(
|
||||
chapterNumber = -1.0,
|
||||
scanlator = null,
|
||||
lastModifiedAt = 0,
|
||||
version = 1,
|
||||
isSyncing = 0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ data class ChapterUpdate(
|
||||
val dateUpload: Long? = null,
|
||||
val chapterNumber: Double? = null,
|
||||
val scanlator: String? = null,
|
||||
val version: Long? = null,
|
||||
val isSyncing: Long? = null,
|
||||
)
|
||||
|
||||
fun Chapter.toChapterUpdate(): ChapterUpdate {
|
||||
@@ -29,5 +31,7 @@ fun Chapter.toChapterUpdate(): ChapterUpdate {
|
||||
dateUpload,
|
||||
chapterNumber,
|
||||
scanlator,
|
||||
version,
|
||||
isSyncing
|
||||
)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ data class Manga(
|
||||
val initialized: Boolean,
|
||||
val lastModifiedAt: Long,
|
||||
val favoriteModifiedAt: Long?,
|
||||
val version: Long,
|
||||
val isSyncing: Long,
|
||||
|
||||
) : Serializable {
|
||||
|
||||
val expectedNextUpdate: Instant?
|
||||
@@ -122,6 +125,8 @@ data class Manga(
|
||||
initialized = false,
|
||||
lastModifiedAt = 0L,
|
||||
favoriteModifiedAt = null,
|
||||
version = 0L,
|
||||
isSyncing = 0L,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ data class MangaUpdate(
|
||||
val thumbnailUrl: String? = null,
|
||||
val updateStrategy: UpdateStrategy? = null,
|
||||
val initialized: Boolean? = null,
|
||||
val version: Long? = null,
|
||||
val isSyncing: Long? = null,
|
||||
)
|
||||
|
||||
fun Manga.toMangaUpdate(): MangaUpdate {
|
||||
@@ -47,5 +49,7 @@ fun Manga.toMangaUpdate(): MangaUpdate {
|
||||
thumbnailUrl = thumbnailUrl,
|
||||
updateStrategy = updateStrategy,
|
||||
initialized = initialized,
|
||||
version = version,
|
||||
isSyncing = isSyncing
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user