This commit is contained in:
Bartu Özen
2025-12-20 20:09:48 +03:00
parent 17af9b6419
commit a4bfecc09c
7 changed files with 348 additions and 283 deletions

View File

@@ -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 {
}
}
}

View File

@@ -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}" }

View File

@@ -1,3 +1,5 @@
@file:Suppress("ktlint:standard:filename")
package suwayomi.tachidesk.graphql.types
enum class StartSyncResult {

View File

@@ -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
}
}

View File

@@ -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()

View File

@@ -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()
}

View File

@@ -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()
}
}
}