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 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 kotlinx.serialization.json.Json
import tachiyomi.domain.sync.SyncPreferences
import java.time.Instant
abstract class SyncService(
val context: Context,
val json: Json,
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.
@ -23,4 +43,189 @@ abstract class SyncService(
val syncData = json.decodeFromString(SyncData.serializer(), data)
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
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.models.SyncData
import eu.kanade.tachiyomi.network.GET
@ -15,12 +11,10 @@ import logcat.LogPriority
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.gzip
import okhttp3.RequestBody.Companion.toRequestBody
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.sync.SyncPreferences
import java.time.Instant
class SyncYomiSyncService(
context: Context,
@ -28,33 +22,7 @@ class SyncYomiSyncService(
syncPreferences: SyncPreferences,
private val notifier: SyncNotifier,
) : SyncService(context, json, syncPreferences) {
override suspend fun doSync(syncData: SyncData): Backup? {
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? {
override suspend fun pushSyncData(): SyncData? {
val host = syncPreferences.syncHost().get()
val apiKey = syncPreferences.syncAPIKey().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 apiKey = syncPreferences.syncAPIKey().get()
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 mediaType = "application/gzip".toMediaTypeOrNull()
val jsonData = json.encodeToString(syncData)
val body = jsonData.toRequestBody(mediaType).gzip()
val uploadRequest = POST(
url = uploadUrl,
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()
}
}
}