mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 03:14:40 -05:00
Compare commits
3 Commits
8e63dfffab
...
renovate/g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a65c7d31b0 | ||
|
|
c8f5d83e9c | ||
|
|
be5e3f022e |
@@ -7,7 +7,7 @@ okhttp = "5.4.0" # Major version is locked by Tachiyomi extensions
|
||||
javalin = "7.2.2"
|
||||
jte = "3.2.4"
|
||||
jackson = "3.2.0" # jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
|
||||
exposed = "1.3.0"
|
||||
exposed = "1.2.0"
|
||||
dex2jar = "2.4.37"
|
||||
polyglot = "25.0.3"
|
||||
settings = "1.3.0"
|
||||
|
||||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
|
||||
4
gradlew
vendored
4
gradlew
vendored
@@ -20,7 +20,7 @@
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# Gradle start up script for POSIX generated by Gradle.
|
||||
# gradlew start up script for POSIX generated by Gradle.
|
||||
#
|
||||
# Important for running:
|
||||
#
|
||||
@@ -29,7 +29,7 @@
|
||||
# bash, then to run this script, type that shell name before the whole
|
||||
# command line, like:
|
||||
#
|
||||
# ksh Gradle
|
||||
# ksh gradlew
|
||||
#
|
||||
# Busybox and similar reduced shells will NOT work, because this script
|
||||
# requires all of these POSIX shell features:
|
||||
|
||||
4
gradlew.bat
vendored
4
gradlew.bat
vendored
@@ -19,7 +19,7 @@
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem gradlew startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@@ -72,7 +72,7 @@ echo location of your Java installation. 1>&2
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
@rem Execute gradlew
|
||||
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
|
||||
@rem which allows us to clear the local environment before executing the java command
|
||||
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
|
||||
|
||||
@@ -4,11 +4,15 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.PUT
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.javalin.http.HttpStatus
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
@@ -36,31 +40,66 @@ object SyncYomiSyncService {
|
||||
message: String?,
|
||||
) : 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(
|
||||
syncData: SyncData,
|
||||
startDate: Instant,
|
||||
setSyncState: (SyncManager.SyncState) -> Unit,
|
||||
): Backup? {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
||||
setSyncState(SyncManager.SyncState.Downloading(startDate))
|
||||
val (remoteData, etag) = pullSyncData()
|
||||
|
||||
val finalSyncData =
|
||||
if (remoteData != null) {
|
||||
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||
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
|
||||
return try {
|
||||
val (remoteData, etag) = pullSyncData()
|
||||
|
||||
val finalSyncData =
|
||||
if (remoteData != null) {
|
||||
require(etag.isNotEmpty()) { "ETag should never be empty if remote data is not null" }
|
||||
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))
|
||||
}
|
||||
|
||||
if (finalSyncData.backup != null) {
|
||||
setSyncState(SyncManager.SyncState.Uploading(startDate))
|
||||
val success = pushSyncData(finalSyncData, etag)
|
||||
if (success) {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
||||
} else {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
|
||||
}
|
||||
|
||||
finalSyncData.backup
|
||||
} catch (e: Exception) {
|
||||
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> {
|
||||
@@ -122,8 +161,8 @@ object SyncYomiSyncService {
|
||||
private suspend fun pushSyncData(
|
||||
syncData: SyncData,
|
||||
eTag: String,
|
||||
) {
|
||||
val backup = syncData.backup ?: return
|
||||
): Boolean {
|
||||
val backup = syncData.backup ?: return true
|
||||
|
||||
val host = serverConfig.syncYomiHost.value
|
||||
val apiKey = serverConfig.syncYomiApiKey.value
|
||||
@@ -160,7 +199,7 @@ object SyncYomiSyncService {
|
||||
|
||||
val response = client.newCall(uploadRequest).await()
|
||||
|
||||
if (response.isSuccessful) {
|
||||
return if (response.isSuccessful) {
|
||||
val newETag =
|
||||
response.headers["ETag"]
|
||||
?.takeIf { it.isNotEmpty() } ?: throw SyncYomiException("Missing ETag")
|
||||
@@ -169,12 +208,53 @@ object SyncYomiSyncService {
|
||||
.putString("last_sync_etag", newETag)
|
||||
.apply()
|
||||
logger.debug { "SyncYomi sync completed" }
|
||||
true
|
||||
} else if (response.code == HttpStatus.PRECONDITION_FAILED.code) {
|
||||
// other clients updated remote data, will try next time
|
||||
logger.debug { "SyncYomi sync failed with 412" }
|
||||
false
|
||||
} else {
|
||||
val responseBody = response.body.string()
|
||||
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,6 +16,7 @@ import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import xyz.nulldev.androidcompat.util.SafePath
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
|
||||
@@ -76,13 +77,46 @@ object ChapterDownloadHelper {
|
||||
.select(ChapterTable.columns + MangaTable.columns)
|
||||
.where { ChapterTable.id eq chapterId }
|
||||
.firstOrNull() ?: throw IllegalArgumentException("ChapterId $chapterId not found")
|
||||
|
||||
val chapter = ChapterTable.toDataClass(row)
|
||||
val mangaTitle = row[MangaTable.title]
|
||||
val mangaTitle = row[MangaTable.title].trim()
|
||||
|
||||
val scanlatorPart = chapter.scanlator?.let { "[$it] " } ?: ""
|
||||
val fileName = "$mangaTitle - $scanlatorPart${chapter.name}.cbz"
|
||||
val scanlatorName = chapter.scanlator?.trim()?.takeIf { it.isNotEmpty() }
|
||||
val chapterName = chapter.name.trim().takeIf { it.isNotEmpty() }
|
||||
|
||||
Pair(chapter, fileName)
|
||||
val 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(
|
||||
|
||||
Reference in New Issue
Block a user