refactor SyncYomiService

This commit is contained in:
Aria Moradi 2023-07-07 20:12:47 +03:30
parent 878f7097d1
commit a2a1201dec
2 changed files with 214 additions and 206 deletions

View File

@ -2,16 +2,36 @@ package eu.kanade.tachiyomi.data.sync.service
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.data.backup.models.Backup import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.sync.models.SyncData import eu.kanade.tachiyomi.data.sync.models.SyncData
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.domain.sync.SyncPreferences import tachiyomi.domain.sync.SyncPreferences
import java.time.Instant
abstract class SyncService( abstract class SyncService(
val context: Context, val context: Context,
val json: Json, val json: Json,
val syncPreferences: SyncPreferences, val syncPreferences: SyncPreferences,
) { ) {
abstract suspend fun doSync(syncData: SyncData): Backup? open suspend fun doSync(syncData: SyncData): Backup? {
beforeSync()
val remoteSData = pushSyncData()
val finalSyncData =
if (remoteSData == null) {
pullSyncData(syncData)
syncData
} else {
val mergedSyncData = mergeSyncData(syncData, remoteSData)
pullSyncData(mergedSyncData)
mergedSyncData
}
return finalSyncData.backup
}
/** /**
* Decodes the given sync data string into a Backup object. * Decodes the given sync data string into a Backup object.
@ -23,4 +43,189 @@ abstract class SyncService(
val syncData = json.decodeFromString(SyncData.serializer(), data) val syncData = json.decodeFromString(SyncData.serializer(), data)
return syncData.backup!! return syncData.backup!!
} }
/**
* For refreshing tokens and other possible operations before connecting to the remote storage
*/
open suspend fun beforeSync() {}
/**
* Download sync data from the remote storage
*/
abstract suspend fun pushSyncData(): SyncData?
/**
* Upload sync data to the remote storage
*/
abstract suspend fun pullSyncData(syncData: SyncData)
/**
* Merges the local and remote sync data into a single JSON string.
*
* @param localSyncData The SData containing the local sync data.
* @param remoteSyncData The SData containing the remote sync data.
* @return The JSON string containing the merged sync data.
*/
fun mergeSyncData(localSyncData: SyncData, remoteSyncData: SyncData): SyncData {
val mergedMangaList = mergeMangaLists(localSyncData.backup?.backupManga, remoteSyncData.backup?.backupManga)
val mergedCategoriesList = mergeCategoriesLists(localSyncData.backup?.backupCategories, remoteSyncData.backup?.backupCategories)
// Create the merged Backup object
val mergedBackup = Backup(
backupManga = mergedMangaList,
backupCategories = mergedCategoriesList,
backupBrokenSources = localSyncData.backup?.backupBrokenSources ?: emptyList(),
backupSources = localSyncData.backup?.backupSources ?: emptyList(),
)
// Create the merged SData object
return SyncData(
sync = localSyncData.sync, // always use the local sync info
backup = mergedBackup,
device = localSyncData.device, // always use the local device info
)
}
/**
* Merges two lists of SyncManga objects, prioritizing the manga with the most recent lastModifiedAt value.
* If lastModifiedAt is null, the function defaults to Instant.MIN for comparison purposes.
*
* @param localMangaList The list of local SyncManga objects.
* @param remoteMangaList The list of remote SyncManga objects.
* @return The merged list of SyncManga objects.
*/
private fun mergeMangaLists(localMangaList: List<BackupManga>?, remoteMangaList: List<BackupManga>?): List<BackupManga> {
if (localMangaList == null) return remoteMangaList ?: emptyList()
if (remoteMangaList == null) return localMangaList
val localMangaMap = localMangaList.associateBy { Pair(it.source, it.url) }
val remoteMangaMap = remoteMangaList.associateBy { Pair(it.source, it.url) }
val mergedMangaMap = mutableMapOf<Pair<Long, String>, BackupManga>()
localMangaMap.forEach { (key, localManga) ->
val remoteManga = remoteMangaMap[key]
if (remoteManga != null) {
val localInstant = localManga.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
val remoteInstant = remoteManga.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
val mergedManga = if ((localInstant ?: Instant.MIN) >= (
remoteInstant
?: Instant.MIN
)
) {
localManga
} else {
remoteManga
}
val localChapters = localManga.chapters
val remoteChapters = remoteManga.chapters
val mergedChapters = mergeChapters(localChapters, remoteChapters)
val isFavorite = if ((localInstant ?: Instant.MIN) >= (
remoteInstant
?: Instant.MIN
)
) {
localManga.favorite
} else {
remoteManga.favorite
}
mergedMangaMap[key] = mergedManga.copy(chapters = mergedChapters, favorite = isFavorite)
} else {
mergedMangaMap[key] = localManga
}
}
remoteMangaMap.forEach { (key, remoteManga) ->
if (!mergedMangaMap.containsKey(key)) {
mergedMangaMap[key] = remoteManga
}
}
return mergedMangaMap.values.toList()
}
/**
* Merges two lists of SyncChapter objects, prioritizing the chapter with the most recent lastModifiedAt value.
* If lastModifiedAt is null, the function defaults to Instant.MIN for comparison purposes.
*
* @param localChapters The list of local SyncChapter objects.
* @param remoteChapters The list of remote SyncChapter objects.
* @return The merged list of SyncChapter objects.
*/
private fun mergeChapters(localChapters: List<BackupChapter>, remoteChapters: List<BackupChapter>): List<BackupChapter> {
val localChapterMap = localChapters.associateBy { it.url }
val remoteChapterMap = remoteChapters.associateBy { it.url }
val mergedChapterMap = mutableMapOf<String, BackupChapter>()
localChapterMap.forEach { (url, localChapter) ->
val remoteChapter = remoteChapterMap[url]
if (remoteChapter != null) {
val localInstant = localChapter.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
val remoteInstant = remoteChapter.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
val mergedChapter =
if ((localInstant ?: Instant.MIN) >= (remoteInstant ?: Instant.MIN)) {
localChapter
} else {
remoteChapter
}
mergedChapterMap[url] = mergedChapter
} else {
mergedChapterMap[url] = localChapter
}
}
remoteChapterMap.forEach { (url, remoteChapter) ->
if (!mergedChapterMap.containsKey(url)) {
mergedChapterMap[url] = remoteChapter
}
}
return mergedChapterMap.values.toList()
}
/**
* Merges two lists of SyncCategory objects, prioritizing the category with the most recent order value.
*
* @param localCategoriesList The list of local SyncCategory objects.
* @param remoteCategoriesList The list of remote SyncCategory objects.
* @return The merged list of SyncCategory objects.
*/
private fun mergeCategoriesLists(localCategoriesList: List<BackupCategory>?, remoteCategoriesList: List<BackupCategory>?): List<BackupCategory> {
if (localCategoriesList == null) return remoteCategoriesList ?: emptyList()
if (remoteCategoriesList == null) return localCategoriesList
val localCategoriesMap = localCategoriesList.associateBy { it.name }
val remoteCategoriesMap = remoteCategoriesList.associateBy { it.name }
val mergedCategoriesMap = mutableMapOf<String, BackupCategory>()
localCategoriesMap.forEach { (name, localCategory) ->
val remoteCategory = remoteCategoriesMap[name]
if (remoteCategory != null) {
// Compare and merge local and remote categories
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
mergedCategoriesMap[name] = localCategory
}
}
// Add any categories from the remote list that are not in the local list
remoteCategoriesMap.forEach { (name, remoteCategory) ->
if (!mergedCategoriesMap.containsKey(name)) {
mergedCategoriesMap[name] = remoteCategory
}
}
return mergedCategoriesMap.values.toList()
}
} }

View File

@ -1,10 +1,6 @@
package eu.kanade.tachiyomi.data.sync.service package eu.kanade.tachiyomi.data.sync.service
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.models.BackupManga
import eu.kanade.tachiyomi.data.sync.SyncNotifier import eu.kanade.tachiyomi.data.sync.SyncNotifier
import eu.kanade.tachiyomi.data.sync.models.SyncData import eu.kanade.tachiyomi.data.sync.models.SyncData
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
@ -15,12 +11,10 @@ import logcat.LogPriority
import okhttp3.Headers import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.gzip import okhttp3.RequestBody.Companion.gzip
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.system.logcat import tachiyomi.core.util.system.logcat
import tachiyomi.domain.sync.SyncPreferences import tachiyomi.domain.sync.SyncPreferences
import java.time.Instant
class SyncYomiSyncService( class SyncYomiSyncService(
context: Context, context: Context,
@ -28,33 +22,7 @@ class SyncYomiSyncService(
syncPreferences: SyncPreferences, syncPreferences: SyncPreferences,
private val notifier: SyncNotifier, private val notifier: SyncNotifier,
) : SyncService(context, json, syncPreferences) { ) : SyncService(context, json, syncPreferences) {
override suspend fun doSync(syncData: SyncData): Backup? { override suspend fun pushSyncData(): SyncData? {
logcat(
LogPriority.DEBUG,
) { "SyncYomi sync started!" }
val jsonData = json.encodeToString(syncData)
val mediaType = "application/gzip".toMediaTypeOrNull()
val body = jsonData.toRequestBody(mediaType).gzip()
val remoteSyncData = downloadSyncData()
val finalSyncData =
if (remoteSyncData?.backup == null) {
uploadSyncData(body)
syncData
} else {
val mergedSyncData = mergeSyncData(syncData, remoteSyncData)
val encodeMergedData = json.encodeToString(mergedSyncData)
uploadSyncData(encodeMergedData.toRequestBody(mediaType).gzip())
mergedSyncData
}
return finalSyncData.backup
}
suspend fun downloadSyncData(): SyncData? {
val host = syncPreferences.syncHost().get() val host = syncPreferences.syncHost().get()
val apiKey = syncPreferences.syncAPIKey().get() val apiKey = syncPreferences.syncAPIKey().get()
val deviceId = syncPreferences.deviceID().get() val deviceId = syncPreferences.deviceID().get()
@ -81,7 +49,7 @@ class SyncYomiSyncService(
} }
} }
suspend fun uploadSyncData(body: RequestBody) { override suspend fun pullSyncData(syncData: SyncData) {
val host = syncPreferences.syncHost().get() val host = syncPreferences.syncHost().get()
val apiKey = syncPreferences.syncAPIKey().get() val apiKey = syncPreferences.syncAPIKey().get()
val uploadUrl = "$host/api/sync/upload" val uploadUrl = "$host/api/sync/upload"
@ -90,6 +58,11 @@ class SyncYomiSyncService(
val headers = Headers.Builder().add("Content-Type", "application/gzip").add("Content-Encoding", "gzip").add("X-API-Token", apiKey).build() val headers = Headers.Builder().add("Content-Type", "application/gzip").add("Content-Encoding", "gzip").add("X-API-Token", apiKey).build()
val mediaType = "application/gzip".toMediaTypeOrNull()
val jsonData = json.encodeToString(syncData)
val body = jsonData.toRequestBody(mediaType).gzip()
val uploadRequest = POST( val uploadRequest = POST(
url = uploadUrl, url = uploadUrl,
headers = headers, headers = headers,
@ -108,174 +81,4 @@ class SyncYomiSyncService(
} }
} }
} }
}
/**
* Merges the local and remote sync data into a single JSON string.
*
* @param localSyncData The SyncData containing the local sync data.
* @param remoteSyncData The SyncData containing the remote sync data.
* @return The JSON string containing the merged sync data.
*/
fun mergeSyncData(localSyncData: SyncData, remoteSyncData: SyncData): SyncData {
val mergedMangaList = mergeMangaLists(localSyncData.backup?.backupManga, remoteSyncData.backup?.backupManga)
val mergedCategoriesList = mergeCategoriesLists(localSyncData.backup?.backupCategories, remoteSyncData.backup?.backupCategories)
// Create the merged Backup object
val mergedBackup = Backup(
backupManga = mergedMangaList,
backupCategories = mergedCategoriesList,
backupBrokenSources = localSyncData.backup?.backupBrokenSources ?: emptyList(),
backupSources = localSyncData.backup?.backupSources ?: emptyList(),
)
// Create the merged SyncData object
return SyncData(
sync = localSyncData.sync, // always use the local sync info
backup = mergedBackup,
device = localSyncData.device, // always use the local device info
)
}
/**
* Merges two lists of SyncManga objects, prioritizing the manga with the most recent lastModifiedAt value.
* If lastModifiedAt is null, the function defaults to Instant.MIN for comparison purposes.
*
* @param localMangaList The list of local SyncManga objects.
* @param remoteMangaList The list of remote SyncManga objects.
* @return The merged list of SyncManga objects.
*/
private fun mergeMangaLists(localMangaList: List<BackupManga>?, remoteMangaList: List<BackupManga>?): List<BackupManga> {
if (localMangaList == null) return remoteMangaList ?: emptyList()
if (remoteMangaList == null) return localMangaList
val localMangaMap = localMangaList.associateBy { Pair(it.source, it.url) }
val remoteMangaMap = remoteMangaList.associateBy { Pair(it.source, it.url) }
val mergedMangaMap = mutableMapOf<Pair<Long, String>, BackupManga>()
localMangaMap.forEach { (key, localManga) ->
val remoteManga = remoteMangaMap[key]
if (remoteManga != null) {
val localInstant = localManga.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
val remoteInstant = remoteManga.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
val mergedManga = if ((localInstant ?: Instant.MIN) >= (
remoteInstant
?: Instant.MIN
)
) {
localManga
} else {
remoteManga
}
val localChapters = localManga.chapters
val remoteChapters = remoteManga.chapters
val mergedChapters = mergeChapters(localChapters, remoteChapters)
val isFavorite = if ((localInstant ?: Instant.MIN) >= (
remoteInstant
?: Instant.MIN
)
) {
localManga.favorite
} else {
remoteManga.favorite
}
mergedMangaMap[key] = mergedManga.copy(chapters = mergedChapters, favorite = isFavorite)
} else {
mergedMangaMap[key] = localManga
}
}
remoteMangaMap.forEach { (key, remoteManga) ->
if (!mergedMangaMap.containsKey(key)) {
mergedMangaMap[key] = remoteManga
}
}
return mergedMangaMap.values.toList()
}
/**
* Merges two lists of SyncChapter objects, prioritizing the chapter with the most recent lastModifiedAt value.
* If lastModifiedAt is null, the function defaults to Instant.MIN for comparison purposes.
*
* @param localChapters The list of local SyncChapter objects.
* @param remoteChapters The list of remote SyncChapter objects.
* @return The merged list of SyncChapter objects.
*/
private fun mergeChapters(localChapters: List<BackupChapter>, remoteChapters: List<BackupChapter>): List<BackupChapter> {
val localChapterMap = localChapters.associateBy { it.url }
val remoteChapterMap = remoteChapters.associateBy { it.url }
val mergedChapterMap = mutableMapOf<String, BackupChapter>()
localChapterMap.forEach { (url, localChapter) ->
val remoteChapter = remoteChapterMap[url]
if (remoteChapter != null) {
val localInstant = localChapter.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
val remoteInstant = remoteChapter.lastModifiedAt?.let { Instant.ofEpochMilli(it) }
val mergedChapter =
if ((localInstant ?: Instant.MIN) >= (remoteInstant ?: Instant.MIN)) {
localChapter
} else {
remoteChapter
}
mergedChapterMap[url] = mergedChapter
} else {
mergedChapterMap[url] = localChapter
}
}
remoteChapterMap.forEach { (url, remoteChapter) ->
if (!mergedChapterMap.containsKey(url)) {
mergedChapterMap[url] = remoteChapter
}
}
return mergedChapterMap.values.toList()
}
/**
* Merges two lists of SyncCategory objects, prioritizing the category with the most recent order value.
*
* @param localCategoriesList The list of local SyncCategory objects.
* @param remoteCategoriesList The list of remote SyncCategory objects.
* @return The merged list of SyncCategory objects.
*/
private fun mergeCategoriesLists(localCategoriesList: List<BackupCategory>?, remoteCategoriesList: List<BackupCategory>?): List<BackupCategory> {
if (localCategoriesList == null) return remoteCategoriesList ?: emptyList()
if (remoteCategoriesList == null) return localCategoriesList
val localCategoriesMap = localCategoriesList.associateBy { it.name }
val remoteCategoriesMap = remoteCategoriesList.associateBy { it.name }
val mergedCategoriesMap = mutableMapOf<String, BackupCategory>()
localCategoriesMap.forEach { (name, localCategory) ->
val remoteCategory = remoteCategoriesMap[name]
if (remoteCategory != null) {
// Compare and merge local and remote categories
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
mergedCategoriesMap[name] = localCategory
}
}
// Add any categories from the remote list that are not in the local list
remoteCategoriesMap.forEach { (name, remoteCategory) ->
if (!mergedCategoriesMap.containsKey(name)) {
mergedCategoriesMap[name] = remoteCategory
}
}
return mergedCategoriesMap.values.toList()
}
}