From a4bfecc09c1b270deabc8c89b6967bc12359160a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartu=20=C3=96zen?= Date: Sat, 20 Dec 2025 20:09:48 +0300 Subject: [PATCH] Lint --- .../tachidesk/global/impl/sync/SyncManager.kt | 212 ++++++------ .../global/impl/sync/SyncYomiSyncService.kt | 318 ++++++++++-------- .../tachidesk/graphql/types/SyncType.kt | 2 + .../impl/backup/proto/ProtoBackupImport.kt | 7 +- .../proto/handlers/BackupMangaHandler.kt | 2 +- .../database/migration/M0053_SyncYomi.kt | 19 +- .../database/trigger/SyncYomiTriggers.kt | 71 ++-- 7 files changed, 348 insertions(+), 283 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncManager.kt index 55a66548d..e1beeb274 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncManager.kt @@ -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 { - return transaction { MangaTable.selectAll().map { MangaTable.toDataClass(it) } } - } + private fun getAllMangaFromDB(): List = transaction { MangaTable.selectAll().map { MangaTable.toDataClass(it) } } - private fun getAllMangaThatNeedsSync(): List { - return transaction { + private fun getAllMangaThatNeedsSync(): List = + 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() val nonFavorites = mutableListOf() - 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 { } } } - diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncYomiSyncService.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncYomiSyncService.kt index 71d507f6c..37911a17b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncYomiSyncService.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/impl/sync/SyncYomiSyncService.kt @@ -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): BackupManga { - return theManga.copy( - categories = theManga.categories.mapNotNull { - theMap[it]?.let { category -> - mergedCategoriesMapByName[category.name]?.order - } - }, + fun updateCategories( + theManga: BackupManga, + theMap: Map, + ): 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, remoteChapters: List, ): List { - 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}" } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SyncType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SyncType.kt index fff42f9d2..fb4a270e7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SyncType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SyncType.kt @@ -1,3 +1,5 @@ +@file:Suppress("ktlint:standard:filename") + package suwayomi.tachidesk.graphql.types enum class StartSyncResult { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt index 93591bfde..1be15fc77 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupImport.kt @@ -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 } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/handlers/BackupMangaHandler.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/handlers/BackupMangaHandler.kt index c5477891c..bbf33c191 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/handlers/BackupMangaHandler.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/handlers/BackupMangaHandler.kt @@ -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() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0053_SyncYomi.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0053_SyncYomi.kt index fd86bbf80..ed2410d4b 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0053_SyncYomi.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0053_SyncYomi.kt @@ -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() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/database/trigger/SyncYomiTriggers.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/database/trigger/SyncYomiTriggers.kt index 58cfa50e0..62709bbf3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/database/trigger/SyncYomiTriggers.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/database/trigger/SyncYomiTriggers.kt @@ -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() + } } } -