feat: Add sync events to SyncYomi (jobobby04/TachiyomiSY#1558)
* feat: Add sync events to SyncYomi Now it will send the events back to `SyncYomi` server and then forward those to the notifications services that are enabled, such as discord, telegram, and etc. * chore: fix build error. (cherry picked from commit 2776e411272499eba2f91acfea3cb40dc0d4c622) * Improve client management and response handling --------- Co-authored-by: Cuong-Tran <16017808+cuong-tran@users.noreply.github.com>
This commit is contained in:
parent
bad1dd264d
commit
38acd6f110
1 changed files with 100 additions and 26 deletions
|
|
@ -5,8 +5,14 @@ import eu.kanade.domain.sync.SyncPreferences
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
import eu.kanade.tachiyomi.network.PUT
|
||||||
import eu.kanade.tachiyomi.network.await
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.NonCancellable
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
|
@ -21,7 +27,6 @@ import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
|
|
||||||
class SyncYomiSyncService(
|
class SyncYomiSyncService(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
@ -32,9 +37,34 @@ class SyncYomiSyncService(
|
||||||
private val protoBuf: ProtoBuf = Injekt.get(),
|
private val protoBuf: ProtoBuf = Injekt.get(),
|
||||||
) : SyncService(context, json, syncPreferences) {
|
) : SyncService(context, json, syncPreferences) {
|
||||||
|
|
||||||
|
// KMK -->
|
||||||
|
companion object {
|
||||||
|
private val client by lazy { OkHttpClient() }
|
||||||
|
}
|
||||||
|
// KMK <--
|
||||||
|
|
||||||
private class SyncYomiException(message: String?) : Exception(message)
|
private class SyncYomiException(message: String?) : Exception(message)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data class SyncEvent(
|
||||||
|
val event: SyncEventStatus,
|
||||||
|
@SerialName("device_name")
|
||||||
|
val deviceName: String? = null,
|
||||||
|
val message: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private enum class SyncEventStatus {
|
||||||
|
SYNC_STARTED,
|
||||||
|
SYNC_SUCCESS,
|
||||||
|
SYNC_FAILED,
|
||||||
|
SYNC_ERROR,
|
||||||
|
SYNC_CANCELLED,
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun doSync(syncData: SyncData): Backup? {
|
override suspend fun doSync(syncData: SyncData): Backup? {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val (remoteData, etag) = pullSyncData()
|
val (remoteData, etag) = pullSyncData()
|
||||||
|
|
||||||
|
|
@ -52,11 +82,26 @@ class SyncYomiSyncService(
|
||||||
syncData
|
syncData
|
||||||
}
|
}
|
||||||
|
|
||||||
pushSyncData(finalSyncData, etag)
|
val success = pushSyncData(finalSyncData, etag)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
||||||
|
} else {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
|
||||||
|
// KMK -->
|
||||||
|
return null
|
||||||
|
// KMK <--
|
||||||
|
}
|
||||||
|
|
||||||
return finalSyncData.backup
|
return finalSyncData.backup
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
if (e is CancellationException) {
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
||||||
notifier.showSyncError(e.message)
|
notifier.showSyncError(e.message)
|
||||||
|
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +123,6 @@ class SyncYomiSyncService(
|
||||||
headers = headers,
|
headers = headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
val client = OkHttpClient()
|
|
||||||
val response = client.newCall(downloadRequest).await()
|
val response = client.newCall(downloadRequest).await()
|
||||||
|
|
||||||
if (response.code == HttpStatus.SC_NOT_MODIFIED) {
|
if (response.code == HttpStatus.SC_NOT_MODIFIED) {
|
||||||
|
|
@ -123,13 +167,12 @@ class SyncYomiSyncService(
|
||||||
/**
|
/**
|
||||||
* Return true if update success
|
* Return true if update success
|
||||||
*/
|
*/
|
||||||
private suspend fun pushSyncData(syncData: SyncData, eTag: String) {
|
private suspend fun pushSyncData(syncData: SyncData, eTag: String): Boolean {
|
||||||
val backup = syncData.backup ?: return
|
val backup = syncData.backup ?: return true
|
||||||
|
|
||||||
val host = syncPreferences.clientHost().get()
|
val host = syncPreferences.clientHost().get()
|
||||||
val apiKey = syncPreferences.clientAPIKey().get()
|
val apiKey = syncPreferences.clientAPIKey().get()
|
||||||
val uploadUrl = "$host/api/sync/content"
|
val uploadUrl = "$host/api/sync/content"
|
||||||
val timeout = 30L
|
|
||||||
|
|
||||||
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
||||||
if (eTag.isNotEmpty()) {
|
if (eTag.isNotEmpty()) {
|
||||||
|
|
@ -137,13 +180,6 @@ class SyncYomiSyncService(
|
||||||
}
|
}
|
||||||
val headers = headersBuilder.build()
|
val headers = headersBuilder.build()
|
||||||
|
|
||||||
// Set timeout to 30 seconds
|
|
||||||
val client = OkHttpClient.Builder()
|
|
||||||
.connectTimeout(timeout, TimeUnit.SECONDS)
|
|
||||||
.readTimeout(timeout, TimeUnit.SECONDS)
|
|
||||||
.writeTimeout(timeout, TimeUnit.SECONDS)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val byteArray = protoBuf.encodeToByteArray(Backup.serializer(), backup)
|
val byteArray = protoBuf.encodeToByteArray(Backup.serializer(), backup)
|
||||||
if (byteArray.isEmpty()) {
|
if (byteArray.isEmpty()) {
|
||||||
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
||||||
|
|
@ -156,20 +192,58 @@ class SyncYomiSyncService(
|
||||||
body = body,
|
body = body,
|
||||||
)
|
)
|
||||||
|
|
||||||
val response = client.newCall(uploadRequest).await()
|
client.newCall(uploadRequest).await()
|
||||||
|
// KMK -->
|
||||||
|
.use { response ->
|
||||||
|
// KMK <--
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
val newETag = response.headers["ETag"]
|
||||||
|
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
||||||
|
syncPreferences.lastSyncEtag().set(newETag)
|
||||||
|
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
||||||
|
return true
|
||||||
|
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
||||||
|
// other clients updated remote data, will try next time
|
||||||
|
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
val responseBody = response.body.string()
|
||||||
|
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
||||||
|
logcat(LogPriority.ERROR) { "SyncError: $responseBody" }
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (response.isSuccessful) {
|
private suspend fun reportSyncEvent(event: SyncEventStatus, message: String? = null) {
|
||||||
val newETag = response.headers["ETag"]
|
withContext(NonCancellable) {
|
||||||
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
try {
|
||||||
syncPreferences.lastSyncEtag().set(newETag)
|
val host = syncPreferences.clientHost().get()
|
||||||
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
val apiKey = syncPreferences.clientAPIKey().get()
|
||||||
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
val url = "$host/api/sync/event"
|
||||||
// other clients updated remote data, will try next time
|
|
||||||
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
||||||
} else {
|
val headers = headersBuilder.build()
|
||||||
val responseBody = response.body.string()
|
|
||||||
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
val bodyObj = SyncEvent(
|
||||||
logcat(LogPriority.ERROR) { "SyncError: $responseBody" }
|
event = event,
|
||||||
|
deviceName = android.os.Build.MODEL,
|
||||||
|
message = message,
|
||||||
|
)
|
||||||
|
|
||||||
|
val jsonBody = json.encodeToString(SyncEvent.serializer(), bodyObj)
|
||||||
|
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||||
|
|
||||||
|
val request = POST(
|
||||||
|
url = url,
|
||||||
|
headers = headers,
|
||||||
|
body = requestBody,
|
||||||
|
)
|
||||||
|
|
||||||
|
client.newCall(request).await().close()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(LogPriority.ERROR) { "Failed to report sync event: ${e.message}" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue