mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 19:34:35 -05:00
Compare commits
1 Commits
renovate/j
...
8e63dfffab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e63dfffab |
@@ -14,7 +14,7 @@ val getTachideskVersion = { "v2.2.${getCommitCount()}" }
|
|||||||
|
|
||||||
val webUIRevisionTag = "r3136"
|
val webUIRevisionTag = "r3136"
|
||||||
|
|
||||||
val webviewJbrRelease = "jbr-release-25.0.3b508.16"
|
val webviewJbrRelease = "jbr-release-25.0.3b508.4"
|
||||||
|
|
||||||
private val getCommitCount = {
|
private val getCommitCount = {
|
||||||
runCatching {
|
runCatching {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ okhttp = "5.4.0" # Major version is locked by Tachiyomi extensions
|
|||||||
javalin = "7.2.2"
|
javalin = "7.2.2"
|
||||||
jte = "3.2.4"
|
jte = "3.2.4"
|
||||||
jackson = "3.2.0" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
jackson = "3.2.0" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||||
exposed = "1.2.0"
|
exposed = "1.3.0"
|
||||||
dex2jar = "2.4.37"
|
dex2jar = "2.4.37"
|
||||||
polyglot = "25.0.3"
|
polyglot = "25.0.3"
|
||||||
settings = "1.3.0"
|
settings = "1.3.0"
|
||||||
|
|||||||
@@ -4,15 +4,11 @@ import android.app.Application
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
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 io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.javalin.http.HttpStatus
|
import io.javalin.http.HttpStatus
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.SerializationException
|
import kotlinx.serialization.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
@@ -40,66 +36,31 @@ object SyncYomiSyncService {
|
|||||||
message: String?,
|
message: String?,
|
||||||
) : Exception(message)
|
) : Exception(message)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class SyncEvent(
|
|
||||||
val event: SyncEventStatus,
|
|
||||||
val device_Name: String? = null,
|
|
||||||
val message: String? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private enum class SyncEventStatus {
|
|
||||||
SYNC_STARTED,
|
|
||||||
SYNC_SUCCESS,
|
|
||||||
SYNC_FAILED,
|
|
||||||
SYNC_ERROR,
|
|
||||||
SYNC_CANCELLED,
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun doSync(
|
suspend fun doSync(
|
||||||
syncData: SyncData,
|
syncData: SyncData,
|
||||||
startDate: Instant,
|
startDate: Instant,
|
||||||
setSyncState: (SyncManager.SyncState) -> Unit,
|
setSyncState: (SyncManager.SyncState) -> Unit,
|
||||||
): Backup? {
|
): Backup? {
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
|
||||||
setSyncState(SyncManager.SyncState.Downloading(startDate))
|
setSyncState(SyncManager.SyncState.Downloading(startDate))
|
||||||
|
val (remoteData, etag) = pullSyncData()
|
||||||
|
|
||||||
return try {
|
val finalSyncData =
|
||||||
val (remoteData, etag) = pullSyncData()
|
if (remoteData != null) {
|
||||||
|
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||||
val finalSyncData =
|
logger.debug { "Try update remote data with ETag($etag)" }
|
||||||
if (remoteData != null) {
|
setSyncState(SyncManager.SyncState.Merging(startDate))
|
||||||
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
mergeSyncData(syncData, remoteData)
|
||||||
logger.debug { "Try update remote data with ETag($etag)" }
|
|
||||||
setSyncState(SyncManager.SyncState.Merging(startDate))
|
|
||||||
mergeSyncData(syncData, remoteData)
|
|
||||||
} else {
|
|
||||||
// init or overwrite remote data
|
|
||||||
logger.debug { "Try overwrite remote data with ETag($etag)" }
|
|
||||||
syncData
|
|
||||||
}
|
|
||||||
|
|
||||||
if (finalSyncData.backup != null) {
|
|
||||||
setSyncState(SyncManager.SyncState.Uploading(startDate))
|
|
||||||
}
|
|
||||||
|
|
||||||
val success = pushSyncData(finalSyncData, etag)
|
|
||||||
if (success) {
|
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
|
||||||
} else {
|
} else {
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
|
// init or overwrite remote data
|
||||||
|
logger.debug { "Try overwrite remote data with ETag($etag)" }
|
||||||
|
syncData
|
||||||
}
|
}
|
||||||
|
|
||||||
finalSyncData.backup
|
if (finalSyncData.backup != null) {
|
||||||
} catch (e: Exception) {
|
setSyncState(SyncManager.SyncState.Uploading(startDate))
|
||||||
if (e is CancellationException) {
|
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
logger.error { "Error syncing: ${e.message}" }
|
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
|
|
||||||
throw e
|
|
||||||
}
|
}
|
||||||
|
pushSyncData(finalSyncData, etag)
|
||||||
|
return finalSyncData.backup
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun pullSyncData(): Pair<SyncData?, String> {
|
private suspend fun pullSyncData(): Pair<SyncData?, String> {
|
||||||
@@ -161,8 +122,8 @@ object SyncYomiSyncService {
|
|||||||
private suspend fun pushSyncData(
|
private suspend fun pushSyncData(
|
||||||
syncData: SyncData,
|
syncData: SyncData,
|
||||||
eTag: String,
|
eTag: String,
|
||||||
): Boolean {
|
) {
|
||||||
val backup = syncData.backup ?: return true
|
val backup = syncData.backup ?: return
|
||||||
|
|
||||||
val host = serverConfig.syncYomiHost.value
|
val host = serverConfig.syncYomiHost.value
|
||||||
val apiKey = serverConfig.syncYomiApiKey.value
|
val apiKey = serverConfig.syncYomiApiKey.value
|
||||||
@@ -199,7 +160,7 @@ object SyncYomiSyncService {
|
|||||||
|
|
||||||
val response = client.newCall(uploadRequest).await()
|
val response = client.newCall(uploadRequest).await()
|
||||||
|
|
||||||
return if (response.isSuccessful) {
|
if (response.isSuccessful) {
|
||||||
val newETag =
|
val newETag =
|
||||||
response.headers["ETag"]
|
response.headers["ETag"]
|
||||||
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
||||||
@@ -208,53 +169,12 @@ object SyncYomiSyncService {
|
|||||||
.putString("last_sync_etag", newETag)
|
.putString("last_sync_etag", newETag)
|
||||||
.apply()
|
.apply()
|
||||||
logger.debug { "SyncYomi sync completed" }
|
logger.debug { "SyncYomi sync completed" }
|
||||||
true
|
|
||||||
} else if (response.code == HttpStatus.PRECONDITION_FAILED.code) {
|
} else if (response.code == HttpStatus.PRECONDITION_FAILED.code) {
|
||||||
// other clients updated remote data, will try next time
|
// other clients updated remote data, will try next time
|
||||||
logger.debug { "SyncYomi sync failed with 412" }
|
logger.debug { "SyncYomi sync failed with 412" }
|
||||||
false
|
|
||||||
} else {
|
} else {
|
||||||
val responseBody = response.body.string()
|
val responseBody = response.body.string()
|
||||||
logger.error { "SyncError: $responseBody" }
|
logger.error { "SyncError: $responseBody" }
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun reportSyncEvent(
|
|
||||||
event: SyncEventStatus,
|
|
||||||
message: String? = null,
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
val host = serverConfig.syncYomiHost.value
|
|
||||||
val apiKey = serverConfig.syncYomiApiKey.value
|
|
||||||
val url = "$host/api/sync/event"
|
|
||||||
|
|
||||||
val headers = Headers.Builder().add("X-API-Token", apiKey).build()
|
|
||||||
|
|
||||||
// Use a fixed server name.
|
|
||||||
val bodyObj =
|
|
||||||
SyncEvent(
|
|
||||||
event = event,
|
|
||||||
device_Name = "Suwayomi Server",
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
network.client
|
|
||||||
.newCall(request)
|
|
||||||
.await()
|
|
||||||
.close()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
logger.error { "Failed to report sync event: ${e.message}" }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable
|
|||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
import xyz.nulldev.androidcompat.util.SafePath
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
@@ -77,46 +76,13 @@ object ChapterDownloadHelper {
|
|||||||
.select(ChapterTable.columns + MangaTable.columns)
|
.select(ChapterTable.columns + MangaTable.columns)
|
||||||
.where { ChapterTable.id eq chapterId }
|
.where { ChapterTable.id eq chapterId }
|
||||||
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
||||||
|
|
||||||
val chapter = ChapterTable.toDataClass(row)
|
val chapter = ChapterTable.toDataClass(row)
|
||||||
val mangaTitle = row[MangaTable.title].trim()
|
val mangaTitle = row[MangaTable.title]
|
||||||
|
|
||||||
val scanlatorName = chapter.scanlator?.trim()?.takeIf { it.isNotEmpty() }
|
val scanlatorPart = chapter.scanlator?.let { "[$it] " } ?: ""
|
||||||
val chapterName = chapter.name.trim().takeIf { it.isNotEmpty() }
|
val fileName = "$mangaTitle - $scanlatorPart${chapter.name}.cbz"
|
||||||
|
|
||||||
val fileName =
|
Pair(chapter, fileName)
|
||||||
buildString {
|
|
||||||
append(mangaTitle)
|
|
||||||
append(" - ")
|
|
||||||
|
|
||||||
if (chapterName != null) {
|
|
||||||
append(chapterName)
|
|
||||||
} else if (chapter.chapterNumber >= 0f) {
|
|
||||||
// chapterNumber is stored as Float, drop .0 for whole numbers.
|
|
||||||
val formatNumber =
|
|
||||||
if (chapter.chapterNumber % 1 == 0f) {
|
|
||||||
chapter.chapterNumber.toInt().toString()
|
|
||||||
} else {
|
|
||||||
chapter.chapterNumber.toString()
|
|
||||||
}
|
|
||||||
append("#$formatNumber")
|
|
||||||
} else {
|
|
||||||
// Fallback when neither name nor valid chapter number exists
|
|
||||||
append("#${chapter.index}")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scanlatorName != null) {
|
|
||||||
append(" [")
|
|
||||||
append(scanlatorName)
|
|
||||||
append("]")
|
|
||||||
}
|
|
||||||
append(".cbz")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sanitize filename for OS compatibility
|
|
||||||
val safeFileName = SafePath.buildValidFilename(fileName)
|
|
||||||
|
|
||||||
Pair(chapter, safeFileName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCbzForDownload(
|
fun getCbzForDownload(
|
||||||
|
|||||||
Reference in New Issue
Block a user