mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	refactor SyncYomiService
This commit is contained in:
		| @@ -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() | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
|  } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user