mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-06-30 17:34:39 -05:00
Lint
This commit is contained in:
@@ -60,22 +60,23 @@ object SyncManager {
|
||||
serverConfig.syncInterval,
|
||||
) { enabled, interval -> Pair(enabled, interval) },
|
||||
{ (enabled, interval) ->
|
||||
currentTaskId = if (enabled && interval > 0) {
|
||||
val intervalMs = interval.minutes.inWholeMilliseconds
|
||||
currentTaskId =
|
||||
if (enabled && interval > 0) {
|
||||
val intervalMs = interval.minutes.inWholeMilliseconds
|
||||
|
||||
currentTaskId?.let { HAScheduler.deschedule(it) }
|
||||
currentTaskId?.let { HAScheduler.deschedule(it) }
|
||||
|
||||
HAScheduler.schedule(
|
||||
{
|
||||
startSync()
|
||||
},
|
||||
interval = intervalMs,
|
||||
delay = intervalMs,
|
||||
name = "sync",
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
HAScheduler.schedule(
|
||||
{
|
||||
startSync()
|
||||
},
|
||||
interval = intervalMs,
|
||||
delay = intervalMs,
|
||||
name = "sync",
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
},
|
||||
ignoreInitialValue = false,
|
||||
)
|
||||
@@ -132,29 +133,32 @@ object SyncManager {
|
||||
|
||||
val databaseManga = getAllMangaThatNeedsSync()
|
||||
|
||||
val backupFlags = BackupFlags(
|
||||
includeManga = serverConfig.syncDataManga.value,
|
||||
includeCategories = serverConfig.syncDataCategories.value,
|
||||
includeChapters = serverConfig.syncDataChapters.value,
|
||||
includeTracking = serverConfig.syncDataTracking.value,
|
||||
includeHistory = serverConfig.syncDataHistory.value,
|
||||
includeClientData = false,
|
||||
includeServerSettings = false,
|
||||
)
|
||||
val backupFlags =
|
||||
BackupFlags(
|
||||
includeManga = serverConfig.syncDataManga.value,
|
||||
includeCategories = serverConfig.syncDataCategories.value,
|
||||
includeChapters = serverConfig.syncDataChapters.value,
|
||||
includeTracking = serverConfig.syncDataTracking.value,
|
||||
includeHistory = serverConfig.syncDataHistory.value,
|
||||
includeClientData = false,
|
||||
includeServerSettings = false,
|
||||
)
|
||||
|
||||
val backupMangas = BackupMangaHandler.backup(backupFlags)
|
||||
|
||||
val backup = Backup(
|
||||
BackupMangaHandler.backup(backupFlags),
|
||||
BackupCategoryHandler.backup(backupFlags).filter { it.name != Category.DEFAULT_CATEGORY_NAME },
|
||||
BackupSourceHandler.backup(backupMangas, backupFlags),
|
||||
emptyMap(),
|
||||
null,
|
||||
)
|
||||
val backup =
|
||||
Backup(
|
||||
BackupMangaHandler.backup(backupFlags),
|
||||
BackupCategoryHandler.backup(backupFlags).filter { it.name != Category.DEFAULT_CATEGORY_NAME },
|
||||
BackupSourceHandler.backup(backupMangas, backupFlags),
|
||||
emptyMap(),
|
||||
null,
|
||||
)
|
||||
|
||||
val syncData = SyncData(
|
||||
backup = backup,
|
||||
)
|
||||
val syncData =
|
||||
SyncData(
|
||||
backup = backup,
|
||||
)
|
||||
|
||||
val remoteBackup = SyncYomiSyncService.doSync(syncData)
|
||||
|
||||
@@ -167,7 +171,8 @@ object SyncManager {
|
||||
if (remoteBackup === syncData.backup) {
|
||||
// nothing changed
|
||||
logger.debug { "Skip restore due to remote was overwrite from local" }
|
||||
syncPreferences.edit()
|
||||
syncPreferences
|
||||
.edit()
|
||||
.putLong("last_sync_timestamp", Clock.System.now().toEpochMilliseconds())
|
||||
.apply()
|
||||
return
|
||||
@@ -181,7 +186,8 @@ object SyncManager {
|
||||
// Check if it's first sync based on lastSyncTimestamp
|
||||
if (syncPreferences.getLong("last_sync_timestamp", 0) == 0L && databaseManga.isNotEmpty()) {
|
||||
// It's first sync no need to restore data. (just update remote data)
|
||||
syncPreferences.edit()
|
||||
syncPreferences
|
||||
.edit()
|
||||
.putLong("last_sync_timestamp", Clock.System.now().toEpochMilliseconds())
|
||||
.apply()
|
||||
return
|
||||
@@ -190,16 +196,18 @@ object SyncManager {
|
||||
val (filteredFavorites, nonFavorites) = filterFavoritesAndNonFavorites(remoteBackup)
|
||||
updateNonFavorites(nonFavorites)
|
||||
|
||||
val newSyncData = backup.copy(
|
||||
backupManga = filteredFavorites,
|
||||
backupCategories = remoteBackup.backupCategories,
|
||||
backupSources = remoteBackup.backupSources,
|
||||
)
|
||||
val newSyncData =
|
||||
backup.copy(
|
||||
backupManga = filteredFavorites,
|
||||
backupCategories = remoteBackup.backupCategories,
|
||||
backupSources = remoteBackup.backupSources,
|
||||
)
|
||||
|
||||
// It's local sync no need to restore data. (just update remote data)
|
||||
if (filteredFavorites.isEmpty()) {
|
||||
// update the sync timestamp
|
||||
syncPreferences.edit()
|
||||
syncPreferences
|
||||
.edit()
|
||||
.putLong("last_sync_timestamp", Clock.System.now().toEpochMilliseconds())
|
||||
.apply()
|
||||
return
|
||||
@@ -208,48 +216,52 @@ object SyncManager {
|
||||
val backupStream = ProtoBuf.encodeToByteArray(Backup.serializer(), newSyncData).inputStream()
|
||||
ProtoBackupImport.restore(
|
||||
sourceStream = backupStream,
|
||||
flags = BackupFlags(
|
||||
includeManga = true,
|
||||
includeCategories = true,
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
includeClientData = false,
|
||||
includeServerSettings = false,
|
||||
),
|
||||
flags =
|
||||
BackupFlags(
|
||||
includeManga = true,
|
||||
includeCategories = true,
|
||||
includeChapters = true,
|
||||
includeTracking = true,
|
||||
includeHistory = true,
|
||||
includeClientData = false,
|
||||
includeServerSettings = false,
|
||||
),
|
||||
isSync = true,
|
||||
)
|
||||
|
||||
// update the sync timestamp
|
||||
syncPreferences.edit()
|
||||
syncPreferences
|
||||
.edit()
|
||||
.putLong("last_sync_timestamp", Clock.System.now().toEpochMilliseconds())
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun getAllMangaFromDB(): List<MangaDataClass> {
|
||||
return transaction { MangaTable.selectAll().map { MangaTable.toDataClass(it) } }
|
||||
}
|
||||
private fun getAllMangaFromDB(): List<MangaDataClass> = transaction { MangaTable.selectAll().map { MangaTable.toDataClass(it) } }
|
||||
|
||||
private fun getAllMangaThatNeedsSync(): List<MangaDataClass> {
|
||||
return transaction {
|
||||
private fun getAllMangaThatNeedsSync(): List<MangaDataClass> =
|
||||
transaction {
|
||||
MangaTable.selectAll().where { MangaTable.inLibrary eq true }.map { MangaTable.toDataClass(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun isMangaDifferent(localManga: MangaDataClass, remoteManga: BackupManga): Boolean {
|
||||
val localChapters = transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.manga eq localManga.id }
|
||||
.map { ChapterTable.toDataClass(it) }
|
||||
}
|
||||
val localCategories = transaction {
|
||||
CategoryMangaTable
|
||||
.innerJoin(CategoryTable)
|
||||
.selectAll()
|
||||
.where { CategoryMangaTable.manga eq localManga.id }
|
||||
.map { it[CategoryTable.order] }
|
||||
}
|
||||
private fun isMangaDifferent(
|
||||
localManga: MangaDataClass,
|
||||
remoteManga: BackupManga,
|
||||
): Boolean {
|
||||
val localChapters =
|
||||
transaction {
|
||||
ChapterTable
|
||||
.selectAll()
|
||||
.where { ChapterTable.manga eq localManga.id }
|
||||
.map { ChapterTable.toDataClass(it) }
|
||||
}
|
||||
val localCategories =
|
||||
transaction {
|
||||
CategoryMangaTable
|
||||
.innerJoin(CategoryTable)
|
||||
.selectAll()
|
||||
.where { CategoryMangaTable.manga eq localManga.id }
|
||||
.map { it[CategoryTable.order] }
|
||||
}
|
||||
|
||||
if (areChaptersDifferent(localChapters, remoteManga.chapters)) {
|
||||
return true
|
||||
@@ -293,39 +305,44 @@ object SyncManager {
|
||||
val favorites = mutableListOf<BackupManga>()
|
||||
val nonFavorites = mutableListOf<BackupManga>()
|
||||
|
||||
val elapsedTimeMillis = measureTimeMillis {
|
||||
val databaseManga = getAllMangaFromDB()
|
||||
val localMangaMap = databaseManga.associateBy {
|
||||
Triple(it.sourceId.toLong(), it.url, it.title)
|
||||
}
|
||||
|
||||
logger.debug { "Starting to filter favorites and non-favorites from backup data." }
|
||||
|
||||
backup.backupManga.forEach { remoteManga ->
|
||||
val compositeKey = Triple(remoteManga.source, remoteManga.url, remoteManga.title)
|
||||
val localManga = localMangaMap[compositeKey]
|
||||
when {
|
||||
// Checks if the manga is in favorites and needs updating or adding
|
||||
remoteManga.favorite -> {
|
||||
if (localManga == null || isMangaDifferent(localManga, remoteManga)) {
|
||||
logger.debug { "Adding to favorites: ${remoteManga.title}" }
|
||||
favorites.add(remoteManga)
|
||||
} else {
|
||||
logger.debug { "Already up-to-date favorite: ${remoteManga.title}" }
|
||||
}
|
||||
val elapsedTimeMillis =
|
||||
measureTimeMillis {
|
||||
val databaseManga = getAllMangaFromDB()
|
||||
val localMangaMap =
|
||||
databaseManga.associateBy {
|
||||
Triple(it.sourceId.toLong(), it.url, it.title)
|
||||
}
|
||||
// Handle non-favorites
|
||||
!remoteManga.favorite -> {
|
||||
logger.debug { "Adding to non-favorites: ${remoteManga.title}" }
|
||||
nonFavorites.add(remoteManga)
|
||||
|
||||
logger.debug { "Starting to filter favorites and non-favorites from backup data." }
|
||||
|
||||
backup.backupManga.forEach { remoteManga ->
|
||||
val compositeKey = Triple(remoteManga.source, remoteManga.url, remoteManga.title)
|
||||
val localManga = localMangaMap[compositeKey]
|
||||
when {
|
||||
// Checks if the manga is in favorites and needs updating or adding
|
||||
remoteManga.favorite -> {
|
||||
if (localManga == null || isMangaDifferent(localManga, remoteManga)) {
|
||||
logger.debug { "Adding to favorites: ${remoteManga.title}" }
|
||||
favorites.add(remoteManga)
|
||||
} else {
|
||||
logger.debug { "Already up-to-date favorite: ${remoteManga.title}" }
|
||||
}
|
||||
}
|
||||
|
||||
// Handle non-favorites
|
||||
!remoteManga.favorite -> {
|
||||
logger.debug { "Adding to non-favorites: ${remoteManga.title}" }
|
||||
nonFavorites.add(remoteManga)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val minutes = elapsedTimeMillis / 60000
|
||||
val seconds = (elapsedTimeMillis % 60000) / 1000
|
||||
logger.debug { "Filtering completed in ${minutes}m ${seconds}s. Favorites found: ${favorites.size}, Non-favorites found: ${nonFavorites.size}" }
|
||||
logger.debug {
|
||||
"Filtering completed in ${minutes}m ${seconds}s. Favorites found: ${favorites.size}, Non-favorites found: ${nonFavorites.size}"
|
||||
}
|
||||
|
||||
return Pair(favorites, nonFavorites)
|
||||
}
|
||||
@@ -378,4 +395,3 @@ object SyncManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,21 +31,24 @@ object SyncYomiSyncService {
|
||||
private val network: NetworkHelper by injectLazy()
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private class SyncYomiException(message: String?) : Exception(message)
|
||||
private class SyncYomiException(
|
||||
message: String?,
|
||||
) : Exception(message)
|
||||
|
||||
suspend fun doSync(syncData: SyncData): Backup? {
|
||||
try {
|
||||
val (remoteData, etag) = pullSyncData()
|
||||
|
||||
val finalSyncData = if (remoteData != null) {
|
||||
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||
logger.debug { "Try update remote data with ETag($etag)" }
|
||||
mergeSyncData(syncData, remoteData)
|
||||
} else {
|
||||
// init or overwrite remote data
|
||||
logger.debug { "Try overwrite remote data with ETag($etag)" }
|
||||
syncData
|
||||
}
|
||||
val finalSyncData =
|
||||
if (remoteData != null) {
|
||||
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||
logger.debug { "Try update remote data with ETag($etag)" }
|
||||
mergeSyncData(syncData, remoteData)
|
||||
} else {
|
||||
// init or overwrite remote data
|
||||
logger.debug { "Try overwrite remote data with ETag($etag)" }
|
||||
syncData
|
||||
}
|
||||
|
||||
pushSyncData(finalSyncData, etag)
|
||||
return finalSyncData.backup
|
||||
@@ -67,10 +70,11 @@ object SyncYomiSyncService {
|
||||
}
|
||||
val headers = headersBuilder.build()
|
||||
|
||||
val downloadRequest = GET(
|
||||
url = downloadUrl,
|
||||
headers = headers,
|
||||
)
|
||||
val downloadRequest =
|
||||
GET(
|
||||
url = downloadUrl,
|
||||
headers = headers,
|
||||
)
|
||||
|
||||
val response = network.client.newCall(downloadRequest).await()
|
||||
|
||||
@@ -85,12 +89,14 @@ object SyncYomiSyncService {
|
||||
}
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val newETag = response.headers["ETag"]
|
||||
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
||||
val newETag =
|
||||
response.headers["ETag"]
|
||||
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
||||
|
||||
val byteArray = response.body.byteStream().use {
|
||||
return@use it.readBytes()
|
||||
}
|
||||
val byteArray =
|
||||
response.body.byteStream().use {
|
||||
return@use it.readBytes()
|
||||
}
|
||||
|
||||
return try {
|
||||
val backup = ProtoBuf.decodeFromByteArray(Backup.serializer(), byteArray)
|
||||
@@ -108,7 +114,10 @@ object SyncYomiSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun pushSyncData(syncData: SyncData, eTag: String) {
|
||||
private suspend fun pushSyncData(
|
||||
syncData: SyncData,
|
||||
eTag: String,
|
||||
) {
|
||||
val backup = syncData.backup ?: return
|
||||
|
||||
val host = serverConfig.syncYomiHost.value
|
||||
@@ -123,11 +132,13 @@ object SyncYomiSyncService {
|
||||
val headers = headersBuilder.build()
|
||||
|
||||
// Set timeout to 30 seconds
|
||||
val client = network.client.newBuilder()
|
||||
.connectTimeout(timeout, TimeUnit.SECONDS)
|
||||
.readTimeout(timeout, TimeUnit.SECONDS)
|
||||
.writeTimeout(timeout, TimeUnit.SECONDS)
|
||||
.build()
|
||||
val client =
|
||||
network.client
|
||||
.newBuilder()
|
||||
.connectTimeout(timeout, TimeUnit.SECONDS)
|
||||
.readTimeout(timeout, TimeUnit.SECONDS)
|
||||
.writeTimeout(timeout, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
val byteArray = ProtoBuf.encodeToByteArray(Backup.serializer(), backup)
|
||||
if (byteArray.isEmpty()) {
|
||||
@@ -135,18 +146,21 @@ object SyncYomiSyncService {
|
||||
}
|
||||
val body = byteArray.toRequestBody("application/octet-stream".toMediaType())
|
||||
|
||||
val uploadRequest = PUT(
|
||||
url = uploadUrl,
|
||||
headers = headers,
|
||||
body = body,
|
||||
)
|
||||
val uploadRequest =
|
||||
PUT(
|
||||
url = uploadUrl,
|
||||
headers = headers,
|
||||
body = body,
|
||||
)
|
||||
|
||||
val response = client.newCall(uploadRequest).await()
|
||||
|
||||
if (response.isSuccessful) {
|
||||
val newETag = response.headers["ETag"]
|
||||
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
||||
syncPreferences.edit()
|
||||
val newETag =
|
||||
response.headers["ETag"]
|
||||
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
||||
syncPreferences
|
||||
.edit()
|
||||
.putString("last_sync_etag", newETag)
|
||||
.apply()
|
||||
logger.debug { "SyncYomi sync completed" }
|
||||
@@ -159,29 +173,34 @@ object SyncYomiSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
fun mergeSyncData(localSyncData: SyncData, remoteSyncData: SyncData): SyncData {
|
||||
fun mergeSyncData(
|
||||
localSyncData: SyncData,
|
||||
remoteSyncData: SyncData,
|
||||
): SyncData {
|
||||
val mergedCategoriesList =
|
||||
mergeCategoriesLists(localSyncData.backup?.backupCategories, remoteSyncData.backup?.backupCategories)
|
||||
|
||||
val mergedMangaList = mergeMangaLists(
|
||||
localSyncData.backup?.backupManga,
|
||||
remoteSyncData.backup?.backupManga,
|
||||
localSyncData.backup?.backupCategories ?: emptyList(),
|
||||
remoteSyncData.backup?.backupCategories ?: emptyList(),
|
||||
mergedCategoriesList,
|
||||
)
|
||||
val mergedMangaList =
|
||||
mergeMangaLists(
|
||||
localSyncData.backup?.backupManga,
|
||||
remoteSyncData.backup?.backupManga,
|
||||
localSyncData.backup?.backupCategories ?: emptyList(),
|
||||
remoteSyncData.backup?.backupCategories ?: emptyList(),
|
||||
mergedCategoriesList,
|
||||
)
|
||||
|
||||
val mergedSourcesList =
|
||||
mergeSourcesLists(localSyncData.backup?.backupSources, remoteSyncData.backup?.backupSources)
|
||||
|
||||
// Create the merged Backup object
|
||||
val mergedBackup = Backup(
|
||||
backupManga = mergedMangaList,
|
||||
backupCategories = mergedCategoriesList,
|
||||
backupSources = mergedSourcesList,
|
||||
meta = emptyMap(),
|
||||
serverSettings = null,
|
||||
)
|
||||
val mergedBackup =
|
||||
Backup(
|
||||
backupManga = mergedMangaList,
|
||||
backupCategories = mergedCategoriesList,
|
||||
backupSources = mergedSourcesList,
|
||||
meta = emptyMap(),
|
||||
serverSettings = null,
|
||||
)
|
||||
|
||||
// Create the merged SData object
|
||||
return SyncData(
|
||||
@@ -201,9 +220,8 @@ object SyncYomiSyncService {
|
||||
|
||||
logger.debug { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
|
||||
|
||||
fun mangaCompositeKey(manga: BackupManga): String {
|
||||
return "${manga.source}|${manga.url}|${manga.title.lowercase().trim()}|${manga.author?.lowercase()?.trim()}"
|
||||
}
|
||||
fun mangaCompositeKey(manga: BackupManga): String =
|
||||
"${manga.source}|${manga.url}|${manga.title.lowercase().trim()}|${manga.author?.lowercase()?.trim()}"
|
||||
|
||||
// Create maps using composite keys
|
||||
val localMangaMap = localMangaListSafe.associateBy { mangaCompositeKey(it) }
|
||||
@@ -213,51 +231,65 @@ object SyncYomiSyncService {
|
||||
val remoteCategoriesMapByOrder = remoteCategories.associateBy { it.order }
|
||||
val mergedCategoriesMapByName = mergedCategories.associateBy { it.name }
|
||||
|
||||
fun updateCategories(theManga: BackupManga, theMap: Map<Int, BackupCategory>): BackupManga {
|
||||
return theManga.copy(
|
||||
categories = theManga.categories.mapNotNull {
|
||||
theMap[it]?.let { category ->
|
||||
mergedCategoriesMapByName[category.name]?.order
|
||||
}
|
||||
},
|
||||
fun updateCategories(
|
||||
theManga: BackupManga,
|
||||
theMap: Map<Int, BackupCategory>,
|
||||
): BackupManga =
|
||||
theManga.copy(
|
||||
categories =
|
||||
theManga.categories.mapNotNull {
|
||||
theMap[it]?.let { category ->
|
||||
mergedCategoriesMapByName[category.name]?.order
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
logger.debug { "Starting merge. Local list size: ${localMangaListSafe.size}, Remote list size: ${remoteMangaListSafe.size}" }
|
||||
|
||||
val mergedList = (localMangaMap.keys + remoteMangaMap.keys).distinct().mapNotNull { compositeKey ->
|
||||
val local = localMangaMap[compositeKey]
|
||||
val remote = remoteMangaMap[compositeKey]
|
||||
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 -> {
|
||||
// Compare versions to decide which manga to keep
|
||||
if (local.version >= remote.version) {
|
||||
logger.debug { "Keeping local version of ${local.title} with merged chapters." }
|
||||
updateCategories(
|
||||
local.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
|
||||
localCategoriesMapByOrder,
|
||||
)
|
||||
} else {
|
||||
logger.debug { "Keeping remote version of ${remote.title} with merged chapters." }
|
||||
updateCategories(
|
||||
remote.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
|
||||
remoteCategoriesMapByOrder,
|
||||
)
|
||||
// New version comparison logic
|
||||
when {
|
||||
local != null && remote == null -> {
|
||||
updateCategories(local, localCategoriesMapByOrder)
|
||||
}
|
||||
}
|
||||
|
||||
else -> null // No manga found for key
|
||||
local == null && remote != null -> {
|
||||
updateCategories(remote, remoteCategoriesMapByOrder)
|
||||
}
|
||||
|
||||
local != null && remote != null -> {
|
||||
// Compare versions to decide which manga to keep
|
||||
if (local.version >= remote.version) {
|
||||
logger.debug { "Keeping local version of ${local.title} with merged chapters." }
|
||||
updateCategories(
|
||||
local.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
|
||||
localCategoriesMapByOrder,
|
||||
)
|
||||
} else {
|
||||
logger.debug { "Keeping remote version of ${remote.title} with merged chapters." }
|
||||
updateCategories(
|
||||
remote.copy(chapters = mergeChapters(local.chapters, remote.chapters)),
|
||||
remoteCategoriesMapByOrder,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
null
|
||||
} // No manga found for key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Counting favorites and non-favorites
|
||||
val (favorites, nonFavorites) = mergedList.partition { it.favorite }
|
||||
|
||||
logger.debug { "Merge completed. Total merged manga: ${mergedList.size}, Favorites: ${favorites.size}, Non-Favorites: ${nonFavorites.size}" }
|
||||
logger.debug {
|
||||
"Merge completed. Total merged manga: ${mergedList.size}, Favorites: ${favorites.size}, Non-Favorites: ${nonFavorites.size}"
|
||||
}
|
||||
|
||||
return mergedList
|
||||
}
|
||||
@@ -266,9 +298,7 @@ object SyncYomiSyncService {
|
||||
localChapters: List<BackupChapter>,
|
||||
remoteChapters: List<BackupChapter>,
|
||||
): List<BackupChapter> {
|
||||
fun chapterCompositeKey(chapter: BackupChapter): String {
|
||||
return "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
|
||||
}
|
||||
fun chapterCompositeKey(chapter: BackupChapter): String = "${chapter.url}|${chapter.name}|${chapter.chapterNumber}"
|
||||
|
||||
val localChapterMap = localChapters.associateBy { chapterCompositeKey(it) }
|
||||
val remoteChapterMap = remoteChapters.associateBy { chapterCompositeKey(it) }
|
||||
@@ -276,45 +306,51 @@ object SyncYomiSyncService {
|
||||
logger.debug { "Starting chapter merge. Local chapters: ${localChapters.size}, Remote chapters: ${remoteChapters.size}" }
|
||||
|
||||
// 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]
|
||||
val mergedChapters =
|
||||
(localChapterMap.keys + remoteChapterMap.keys).distinct().mapNotNull { compositeKey ->
|
||||
val localChapter = localChapterMap[compositeKey]
|
||||
val remoteChapter = remoteChapterMap[compositeKey]
|
||||
|
||||
logger.debug { "Processing chapter key: $compositeKey. Local chapter: ${localChapter != null}, Remote chapter: ${remoteChapter != null}" }
|
||||
|
||||
when {
|
||||
localChapter != null && remoteChapter == null -> {
|
||||
logger.debug { "Keeping local chapter: ${localChapter.name}." }
|
||||
localChapter
|
||||
logger.debug {
|
||||
"Processing chapter key: $compositeKey. Local chapter: ${localChapter != null}, Remote chapter: ${remoteChapter != null}"
|
||||
}
|
||||
|
||||
localChapter == null && remoteChapter != null -> {
|
||||
logger.debug { "Taking remote chapter: ${remoteChapter.name}." }
|
||||
remoteChapter
|
||||
}
|
||||
when {
|
||||
localChapter != null && remoteChapter == null -> {
|
||||
logger.debug { "Keeping local chapter: ${localChapter.name}." }
|
||||
localChapter
|
||||
}
|
||||
|
||||
localChapter != null && remoteChapter != null -> {
|
||||
// Use version number to decide which chapter to keep
|
||||
val chosenChapter = if (localChapter.version >= remoteChapter.version) {
|
||||
// If there mare more chapter on remote, local sourceOrder will need to be updated to maintain correct source order.
|
||||
if (localChapters.size < remoteChapters.size) {
|
||||
localChapter.copy(sourceOrder = remoteChapter.sourceOrder)
|
||||
} else {
|
||||
localChapter
|
||||
}
|
||||
} else {
|
||||
localChapter == null && remoteChapter != null -> {
|
||||
logger.debug { "Taking remote chapter: ${remoteChapter.name}." }
|
||||
remoteChapter
|
||||
}
|
||||
logger.debug { "Merging chapter: ${chosenChapter.name}. Chosen version from: ${if (localChapter.version >= remoteChapter.version) "Local" else "Remote"}, Local version: ${localChapter.version}, Remote version: ${remoteChapter.version}." }
|
||||
chosenChapter
|
||||
}
|
||||
|
||||
else -> {
|
||||
logger.debug { "No chapter found for composite key: $compositeKey. Skipping." }
|
||||
null
|
||||
localChapter != null && remoteChapter != null -> {
|
||||
// Use version number to decide which chapter to keep
|
||||
val chosenChapter =
|
||||
if (localChapter.version >= remoteChapter.version) {
|
||||
// If there mare more chapter on remote, local sourceOrder will need to be updated to maintain correct source order.
|
||||
if (localChapters.size < remoteChapters.size) {
|
||||
localChapter.copy(sourceOrder = remoteChapter.sourceOrder)
|
||||
} else {
|
||||
localChapter
|
||||
}
|
||||
} else {
|
||||
remoteChapter
|
||||
}
|
||||
logger.debug {
|
||||
"Merging chapter: ${chosenChapter.name}. Chosen version from: ${if (localChapter.version >= remoteChapter.version) "Local" else "Remote"}, Local version: ${localChapter.version}, Remote version: ${remoteChapter.version}."
|
||||
}
|
||||
chosenChapter
|
||||
}
|
||||
|
||||
else -> {
|
||||
logger.debug { "No chapter found for composite key: $compositeKey. Skipping." }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug { "Chapter merge completed. Total merged chapters: ${mergedChapters.size}" }
|
||||
|
||||
@@ -336,11 +372,12 @@ object SyncYomiSyncService {
|
||||
val remoteCategory = remoteCategoriesMap[name]
|
||||
if (remoteCategory != null) {
|
||||
// Compare and merge local and remote categories
|
||||
val mergedCategory = if (localCategory.order > remoteCategory.order) {
|
||||
localCategory
|
||||
} else {
|
||||
remoteCategory
|
||||
}
|
||||
val mergedCategory =
|
||||
if (localCategory.order > remoteCategory.order) {
|
||||
localCategory
|
||||
} else {
|
||||
remoteCategory
|
||||
}
|
||||
mergedCategoriesMap[name] = mergedCategory
|
||||
} else {
|
||||
// If the category is only in the local list, add it to the merged list
|
||||
@@ -369,29 +406,32 @@ object SyncYomiSyncService {
|
||||
logger.debug { "Starting source merge. Local sources: ${localSources?.size}, Remote sources: ${remoteSources?.size}" }
|
||||
|
||||
// Merge both source maps
|
||||
val mergedSources = (localSourceMap.keys + remoteSourceMap.keys).distinct().mapNotNull { sourceId ->
|
||||
val localSource = localSourceMap[sourceId]
|
||||
val remoteSource = remoteSourceMap[sourceId]
|
||||
val mergedSources =
|
||||
(localSourceMap.keys + remoteSourceMap.keys).distinct().mapNotNull { sourceId ->
|
||||
val localSource = localSourceMap[sourceId]
|
||||
val remoteSource = remoteSourceMap[sourceId]
|
||||
|
||||
logger.debug { "Processing source ID: $sourceId. Local source: ${localSource != null}, Remote source: ${remoteSource != null}" }
|
||||
|
||||
when {
|
||||
localSource != null && remoteSource == null -> {
|
||||
logger.debug { "Using local source: ${localSource.name}." }
|
||||
localSource
|
||||
logger.debug {
|
||||
"Processing source ID: $sourceId. Local source: ${localSource != null}, Remote source: ${remoteSource != null}"
|
||||
}
|
||||
|
||||
remoteSource != null && localSource == null -> {
|
||||
logger.debug { "Using remote source: ${remoteSource.name}." }
|
||||
remoteSource
|
||||
}
|
||||
when {
|
||||
localSource != null && remoteSource == null -> {
|
||||
logger.debug { "Using local source: ${localSource.name}." }
|
||||
localSource
|
||||
}
|
||||
|
||||
else -> {
|
||||
logger.debug { "Remote and local is not empty: $sourceId. Skipping." }
|
||||
null
|
||||
remoteSource != null && localSource == null -> {
|
||||
logger.debug { "Using remote source: ${remoteSource.name}." }
|
||||
remoteSource
|
||||
}
|
||||
|
||||
else -> {
|
||||
logger.debug { "Remote and local is not empty: $sourceId. Skipping." }
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug { "Source merge completed. Total merged sources: ${mergedSources.size}" }
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
@file:Suppress("ktlint:standard:filename")
|
||||
|
||||
package suwayomi.tachidesk.graphql.types
|
||||
|
||||
enum class StartSyncResult {
|
||||
|
||||
@@ -165,8 +165,7 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
.source()
|
||||
.run {
|
||||
if (!isSync) gzip() else this
|
||||
}
|
||||
.buffer()
|
||||
}.buffer()
|
||||
.use { it.readByteArray() }
|
||||
val backup = parser.decodeFromByteArray(Backup.serializer(), backupString)
|
||||
|
||||
@@ -246,10 +245,10 @@ object ProtoBackupImport : ProtoBackupBase() {
|
||||
|
||||
if (isSync) {
|
||||
transaction {
|
||||
MangaTable.update({ MangaTable.isSyncing eq true}) {
|
||||
MangaTable.update({ MangaTable.isSyncing eq true }) {
|
||||
it[isSyncing] = false
|
||||
}
|
||||
ChapterTable.update({ ChapterTable.isSyncing eq true}) {
|
||||
ChapterTable.update({ ChapterTable.isSyncing eq true }) {
|
||||
it[isSyncing] = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ object BackupMangaHandler {
|
||||
it.uploadDate,
|
||||
it.chapterNumber,
|
||||
chapters.size - it.index,
|
||||
it.version
|
||||
it.version,
|
||||
).apply {
|
||||
if (flags.includeClientData) {
|
||||
this.meta = chapterToMeta[it.id] ?: emptyMap()
|
||||
|
||||
@@ -6,12 +6,14 @@ import suwayomi.tachidesk.server.serverConfig
|
||||
|
||||
@Suppress("ClassName", "unused")
|
||||
class M0053_SyncYomi : SQLMigration() {
|
||||
override val sql = when (serverConfig.databaseType.value) {
|
||||
DatabaseType.POSTGRESQL -> postgresQuery()
|
||||
DatabaseType.H2 -> h2Query()
|
||||
}
|
||||
override val sql =
|
||||
when (serverConfig.databaseType.value) {
|
||||
DatabaseType.POSTGRESQL -> postgresQuery()
|
||||
DatabaseType.H2 -> h2Query()
|
||||
}
|
||||
|
||||
fun postgresQuery(): String = """
|
||||
fun postgresQuery(): String =
|
||||
"""
|
||||
ALTER TABLE manga ADD COLUMN version BIGINT DEFAULT 0;
|
||||
ALTER TABLE manga ADD COLUMN is_syncing BOOLEAN DEFAULT FALSE;
|
||||
|
||||
@@ -75,9 +77,10 @@ class M0053_SyncYomi : SQLMigration() {
|
||||
AFTER INSERT ON categorymanga
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION insert_manga_category_update_version();
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
|
||||
fun h2Query() = """
|
||||
fun h2Query() =
|
||||
"""
|
||||
ALTER TABLE manga ADD COLUMN version BIGINT DEFAULT 0;
|
||||
ALTER TABLE manga ADD COLUMN is_syncing BOOLEAN DEFAULT FALSE;
|
||||
|
||||
@@ -98,5 +101,5 @@ class M0053_SyncYomi : SQLMigration() {
|
||||
AFTER INSERT ON categorymanga
|
||||
FOR EACH ROW
|
||||
CALL "suwayomi.tachidesk.server.database.trigger.InsertMangaCategoryUpdateVersionTrigger";
|
||||
""".trimIndent()
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
@@ -12,19 +12,21 @@ class UpdateMangaVersionTrigger : TriggerAdapter() {
|
||||
newRow: ResultSet,
|
||||
) {
|
||||
val isSyncing = newRow.getBoolean("is_syncing")
|
||||
val hasChanged = oldRow.getString("url") != newRow.getString("url") ||
|
||||
oldRow.getString("description") != newRow.getString("description") ||
|
||||
oldRow.getBoolean("in_library") != newRow.getBoolean("in_library")
|
||||
val hasChanged =
|
||||
oldRow.getString("url") != newRow.getString("url") ||
|
||||
oldRow.getString("description") != newRow.getString("description") ||
|
||||
oldRow.getBoolean("in_library") != newRow.getBoolean("in_library")
|
||||
|
||||
if (!isSyncing && hasChanged) {
|
||||
val id = newRow.getInt("id")
|
||||
|
||||
conn.prepareStatement(
|
||||
"UPDATE MANGA SET version = version + 1 WHERE id = ?",
|
||||
).use {
|
||||
it.setInt(1, id)
|
||||
it.executeUpdate()
|
||||
}
|
||||
conn
|
||||
.prepareStatement(
|
||||
"UPDATE MANGA SET version = version + 1 WHERE id = ?",
|
||||
).use {
|
||||
it.setInt(1, id)
|
||||
it.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,28 +39,31 @@ class UpdateChapterAndMangaVersionTrigger : TriggerAdapter() {
|
||||
newRow: ResultSet,
|
||||
) {
|
||||
val isSyncing = newRow.getBoolean("is_syncing")
|
||||
val hasChanged = oldRow.getBoolean("read") != newRow.getBoolean("read") ||
|
||||
oldRow.getBoolean("bookmark") != newRow.getBoolean("bookmark") ||
|
||||
oldRow.getInt("last_page_read") != newRow.getInt("last_page_read")
|
||||
val hasChanged =
|
||||
oldRow.getBoolean("read") != newRow.getBoolean("read") ||
|
||||
oldRow.getBoolean("bookmark") != newRow.getBoolean("bookmark") ||
|
||||
oldRow.getInt("last_page_read") != newRow.getInt("last_page_read")
|
||||
|
||||
if (!isSyncing && hasChanged) {
|
||||
val chapterId = newRow.getInt("id")
|
||||
val mangaId = newRow.getInt("manga")
|
||||
|
||||
conn.prepareStatement(
|
||||
"UPDATE CHAPTER SET version = version + 1 WHERE id = ?",
|
||||
).use {
|
||||
it.setInt(1, chapterId)
|
||||
it.executeUpdate()
|
||||
}
|
||||
conn
|
||||
.prepareStatement(
|
||||
"UPDATE CHAPTER SET version = version + 1 WHERE id = ?",
|
||||
).use {
|
||||
it.setInt(1, chapterId)
|
||||
it.executeUpdate()
|
||||
}
|
||||
|
||||
conn.prepareStatement(
|
||||
"UPDATE MANGA SET version = version + 1 WHERE id = ? AND (SELECT is_syncing FROM MANGA WHERE id = ?) = FALSE",
|
||||
).use {
|
||||
it.setInt(1, mangaId)
|
||||
it.setInt(2, mangaId)
|
||||
it.executeUpdate()
|
||||
}
|
||||
conn
|
||||
.prepareStatement(
|
||||
"UPDATE MANGA SET version = version + 1 WHERE id = ? AND (SELECT is_syncing FROM MANGA WHERE id = ?) = FALSE",
|
||||
).use {
|
||||
it.setInt(1, mangaId)
|
||||
it.setInt(2, mangaId)
|
||||
it.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -72,13 +77,13 @@ class InsertMangaCategoryUpdateVersionTrigger : TriggerAdapter() {
|
||||
) {
|
||||
val mangaId = newRow.getInt("manga")
|
||||
|
||||
conn.prepareStatement(
|
||||
"UPDATE MANGA SET version = version + 1 WHERE id = ? AND (SELECT is_syncing FROM MANGA WHERE id = ?) = FALSE",
|
||||
).use {
|
||||
it.setInt(1, mangaId)
|
||||
it.setInt(2, mangaId)
|
||||
it.executeUpdate()
|
||||
}
|
||||
conn
|
||||
.prepareStatement(
|
||||
"UPDATE MANGA SET version = version + 1 WHERE id = ? AND (SELECT is_syncing FROM MANGA WHERE id = ?) = FALSE",
|
||||
).use {
|
||||
it.setInt(1, mangaId)
|
||||
it.setInt(2, mangaId)
|
||||
it.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user