Remote Image Processing (#1684)

* Update ServerConfig.kt

* Update ConversionUtil.kt

* Update Page.kt

* Update ServerConfig.kt

fixed deletions caused by ide

* Update ServerConfig.kt

* Update ServerConfig.kt

* Cleanup

* Post-processing terminology

* More comments

* Lint

* Add known image mimes

* Fix weird mime set/get

* Implement different downloadConversions and serveConversions

* Lint

* Improve Post-Processing massivly

* Fix thumbnail build

* Use Array for headers

* Actually fix headers

* Actually fix headers 2

* Manually parse DownloadConversion

* Cleanup parse

* Fix write

* Update TypeName

* Optimize imports

* Remove header type

* Fix build

---------

Co-authored-by: Syer10 <syer10@users.noreply.github.com>
This commit is contained in:
FadedSociety
2025-11-12 14:23:34 -07:00
committed by GitHub
parent 3e47859d88
commit 0a7e6cce87
16 changed files with 527 additions and 188 deletions

View File

@@ -1,21 +1,42 @@
package suwayomi.tachidesk.graphql.types
import kotlin.time.Duration
// These types belong to SettingsType.kt. However, since that file is auto-generated, these types need to be placed in
// a "static" file.
data class DownloadConversion(
class DownloadConversion(
val target: String,
val compressionLevel: Double? = null,
val callTimeout: Duration? = null,
val connectTimeout: Duration? = null,
val headers: Map<String, String>? = null,
)
interface SettingsDownloadConversion {
val mimeType: String
val target: String
val compressionLevel: Double?
val callTimeout: Duration?
val connectTimeout: Duration?
val headers: List<SettingsDownloadConversionHeader>?
}
class SettingsDownloadConversionType(
override val mimeType: String,
override val target: String,
override val compressionLevel: Double?,
override val callTimeout: Duration?,
override val connectTimeout: Duration?,
override val headers: List<SettingsDownloadConversionHeaderType>?
) : SettingsDownloadConversion
interface SettingsDownloadConversionHeader {
val name: String
val value: String
}
class SettingsDownloadConversionHeaderType(
override val name: String,
override val value: String,
) : SettingsDownloadConversionHeader

View File

@@ -4,6 +4,8 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import suwayomi.tachidesk.graphql.types.SettingsDownloadConversion
import suwayomi.tachidesk.graphql.types.SettingsDownloadConversionHeader
import kotlin.time.Duration
@OptIn(ExperimentalSerializationApi::class)
@Serializable
@@ -11,4 +13,15 @@ class BackupSettingsDownloadConversionType(
@ProtoNumber(1) override val mimeType: String,
@ProtoNumber(2) override val target: String,
@ProtoNumber(3) override val compressionLevel: Double?,
) : SettingsDownloadConversion
@ProtoNumber(4) override val callTimeout: Duration?,
@ProtoNumber(5) override val connectTimeout: Duration?,
@ProtoNumber(6) override val headers: List<BackupSettingsDownloadConversionHeaderType>?
) : SettingsDownloadConversion
@OptIn(ExperimentalSerializationApi::class)
@Serializable
class BackupSettingsDownloadConversionHeaderType(
@ProtoNumber(1) override val name: String,
@ProtoNumber(2) override val value: String
): SettingsDownloadConversionHeader

View File

@@ -33,11 +33,13 @@ import suwayomi.tachidesk.graphql.types.DownloadConversion
import suwayomi.tachidesk.graphql.types.KoreaderSyncChecksumMethod
import suwayomi.tachidesk.graphql.types.KoreaderSyncConflictStrategy
import suwayomi.tachidesk.graphql.types.KoreaderSyncLegacyStrategy
import suwayomi.tachidesk.graphql.types.SettingsDownloadConversionHeaderType
import suwayomi.tachidesk.graphql.types.SettingsDownloadConversionType
import suwayomi.tachidesk.graphql.types.WebUIChannel
import suwayomi.tachidesk.graphql.types.WebUIFlavor
import suwayomi.tachidesk.graphql.types.WebUIInterface
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSettingsDownloadConversionHeaderType
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSettingsDownloadConversionType
import suwayomi.tachidesk.manga.impl.extension.repoMatchRegex
import suwayomi.tachidesk.server.settings.BooleanSetting
@@ -537,8 +539,8 @@ class ServerConfig(
excludeFromBackup = true,
)
val downloadConversions: MutableStateFlow<Map<String, DownloadConversion>> by MapSetting<String, DownloadConversion>(
protoNumber = 57,
fun createDownloadConversionsMap(protoNumber: Int, key: String) = MapSetting<String, DownloadConversion>(
protoNumber = protoNumber,
defaultValue = emptyMap(),
group = SettingGroup.DOWNLOADER,
typeInfo =
@@ -559,6 +561,14 @@ class ServerConfig(
it.key,
it.value.target,
it.value.compressionLevel,
it.value.callTimeout,
it.value.connectTimeout,
it.value.headers?.map { header ->
SettingsDownloadConversionHeaderType(
header.key,
header.value,
)
},
)
}
},
@@ -571,6 +581,11 @@ class ServerConfig(
DownloadConversion(
target = it.target,
compressionLevel = it.compressionLevel,
callTimeout = it.callTimeout,
connectTimeout = it.connectTimeout,
headers = it.headers?.associate { header ->
header.name to header.value
},
)
}
},
@@ -583,6 +598,14 @@ class ServerConfig(
it.key,
it.value.target,
it.value.compressionLevel,
it.value.callTimeout,
it.value.connectTimeout,
it.value.headers?.map { header ->
BackupSettingsDownloadConversionHeaderType(
header.key,
header.value,
)
},
)
}
},
@@ -590,12 +613,16 @@ class ServerConfig(
description =
"""
map input mime type to conversion information, or "default" for others
server.downloadConversions."image/webp" = {
target = "image/jpeg" # image type to convert to
server.$key."image/webp" = {
target = "image/jpeg" # image type to convert to, can also be a url to an external server
compressionLevel = 0.8 # quality in range [0,1], leave away to use default compression
}
""".trimIndent(),
)
val downloadConversions: MutableStateFlow<Map<String, DownloadConversion>> by createDownloadConversionsMap(
protoNumber = 57,
key = "downloadConversions"
)
val jwtAudience: MutableStateFlow<String> by StringSetting(
protoNumber = 58,
@@ -669,6 +696,7 @@ class ServerConfig(
typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.KoreaderSyncChecksumMethod")),
)
@Suppress("DEPRECATION")
@Deprecated("Use koreaderSyncStrategyForward and koreaderSyncStrategyBackward instead")
val koreaderSyncStrategy: MutableStateFlow<KoreaderSyncLegacyStrategy> by MigratedConfigValue(
protoNumber = 64,
@@ -707,15 +735,11 @@ class ServerConfig(
),
readMigrated = {
// This is a best-effort reverse mapping. It's not perfect but covers common cases.
when {
koreaderSyncStrategyForward.value == KoreaderSyncConflictStrategy.PROMPT &&
koreaderSyncStrategyBackward.value == KoreaderSyncConflictStrategy.PROMPT -> KoreaderSyncLegacyStrategy.PROMPT
koreaderSyncStrategyForward.value == KoreaderSyncConflictStrategy.KEEP_REMOTE &&
koreaderSyncStrategyBackward.value == KoreaderSyncConflictStrategy.KEEP_LOCAL -> KoreaderSyncLegacyStrategy.SILENT
koreaderSyncStrategyForward.value == KoreaderSyncConflictStrategy.KEEP_LOCAL &&
koreaderSyncStrategyBackward.value == KoreaderSyncConflictStrategy.KEEP_LOCAL -> KoreaderSyncLegacyStrategy.SEND
koreaderSyncStrategyForward.value == KoreaderSyncConflictStrategy.KEEP_REMOTE &&
koreaderSyncStrategyBackward.value == KoreaderSyncConflictStrategy.KEEP_REMOTE -> KoreaderSyncLegacyStrategy.RECEIVE
when (koreaderSyncStrategyForward.value) {
KoreaderSyncConflictStrategy.PROMPT if koreaderSyncStrategyBackward.value == KoreaderSyncConflictStrategy.PROMPT -> KoreaderSyncLegacyStrategy.PROMPT
KoreaderSyncConflictStrategy.KEEP_REMOTE if koreaderSyncStrategyBackward.value == KoreaderSyncConflictStrategy.KEEP_LOCAL -> KoreaderSyncLegacyStrategy.SILENT
KoreaderSyncConflictStrategy.KEEP_LOCAL if koreaderSyncStrategyBackward.value == KoreaderSyncConflictStrategy.KEEP_LOCAL -> KoreaderSyncLegacyStrategy.SEND
KoreaderSyncConflictStrategy.KEEP_REMOTE if koreaderSyncStrategyBackward.value == KoreaderSyncConflictStrategy.KEEP_REMOTE -> KoreaderSyncLegacyStrategy.RECEIVE
else -> KoreaderSyncLegacyStrategy.DISABLED
}
},
@@ -885,6 +909,11 @@ class ServerConfig(
description = "Controls the MimeType that Suwayomi sends in OPDS entries for CBZ archives. Also affects global CBZ download. Modern follows recent IANA standard (2017), while LEGACY (deprecated mimetype for .cbz) and COMPATIBLE (deprecated mimetype for all comic archives) might be more compatible with older clients.",
)
val serveConversions: MutableStateFlow<Map<String, DownloadConversion>> by createDownloadConversionsMap(
protoNumber = 84,
key = "serveConversions"
)
/** ****************************************************************** **/

View File

@@ -14,6 +14,7 @@ object ConfigTypeRegistration {
registerCustomType(MutableStateFlowType())
registerCustomType(DurationType())
registerCustomType(DownloadConversionType())
registered = true
}

View File

@@ -0,0 +1,73 @@
package suwayomi.tachidesk.server.util
import com.typesafe.config.Config
import com.typesafe.config.ConfigFactory
import com.typesafe.config.ConfigValue
import io.github.config4k.ClassContainer
import io.github.config4k.CustomType
import io.github.config4k.extract
import io.github.config4k.toConfig
import suwayomi.tachidesk.graphql.types.DownloadConversion
import kotlin.time.Duration
class DownloadConversionType : CustomType {
override fun parse(
clazz: ClassContainer,
config: Config,
name: String,
): Any? {
val target = config.extract<String>("$name.target")
val compressionLevel = config.extract<Double?>("$name.compressionLevel")
val callTimeout = config.extract<Duration?>("$name.callTimeout")
val connectTimeout = config.extract<Duration?>("$name.connectTimeout")
val headers = config.extract<Map<String, String>?>("$name.headers")
return DownloadConversion(
target = target,
compressionLevel = compressionLevel,
callTimeout = callTimeout,
connectTimeout = connectTimeout,
headers = headers,
)
}
override fun testParse(clazz: ClassContainer): Boolean =
clazz.mapperClass.qualifiedName == "suwayomi.tachidesk.graphql.types.DownloadConversion"
override fun testToConfig(obj: Any): Boolean = obj is DownloadConversion
override fun toConfig(
obj: Any,
name: String,
): Config {
val conversion = obj as DownloadConversion
val builder = ConfigFactory.empty()
var config =
builder
.withValue("$name.target", conversion.target.asConfigValue())
.withValueIfPresent("$name.compressionLevel", conversion.compressionLevel)
.withValueIfPresent("$name.callTimeout", conversion.callTimeout?.toString())
.withValueIfPresent("$name.connectTimeout", conversion.connectTimeout?.toString())
if (conversion.headers != null) {
config =
config
.withValue("$name.headers", conversion.headers.asConfigValue())
}
return config
}
private fun Config.withValueIfPresent(
key: String,
value: Any?,
): Config =
if (value != null) {
withValue(key, value.asConfigValue())
} else {
this
}
private fun Any.asConfigValue(): ConfigValue = toConfig("internal").getValue("internal")
}