mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01:00
Feature/streamline settings (#1614)
* Cleanup graphql setting mutation
* Validate values read from config
* Generate server-reference.conf files from ServerConfig
* Remove unnecessary enum value handling in config value update
Commit df0078b725 introduced the usage of config4k, which handles enums automatically. Thus, this handling is outdated and not needed anymore
* Generate gql SettingsType from ServerConfig
* Extract settings backup logic
* Generate settings backup files
* Move "group" arg to second position
To make it easier to detect and have it at the same position consistently for all settings.
* Remove setting generation from compilation
* Extract setting generation code into new module
* Extract pure setting generation code into new module
* Remove generated settings files from src tree
* Force each setting to set a default value
This commit is contained in:
@@ -9,3 +9,6 @@ ij_kotlin_name_count_to_use_star_import_for_members=2147483647
|
|||||||
ktlint_standard_discouraged-comment-location=disabled
|
ktlint_standard_discouraged-comment-location=disabled
|
||||||
ktlint_standard_if-else-wrapping=disabled
|
ktlint_standard_if-else-wrapping=disabled
|
||||||
ktlint_standard_no-consecutive-comments=disabled
|
ktlint_standard_no-consecutive-comments=disabled
|
||||||
|
|
||||||
|
[**/generated/**]
|
||||||
|
ktlint=disabled
|
||||||
@@ -113,8 +113,7 @@ open class ConfigManager {
|
|||||||
value: Any,
|
value: Any,
|
||||||
) {
|
) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val actualValue = if (value is Enum<*>) value.name else value
|
val configValue = value.toConfig("internal").getValue("internal")
|
||||||
val configValue = actualValue.toConfig("internal").getValue("internal")
|
|
||||||
|
|
||||||
updateUserConfigFile(path, configValue)
|
updateUserConfigFile(path, configValue)
|
||||||
internalConfig = internalConfig.withValue(path, configValue)
|
internalConfig = internalConfig.withValue(path, configValue)
|
||||||
|
|||||||
@@ -92,6 +92,9 @@ dependencies {
|
|||||||
// i18n
|
// i18n
|
||||||
implementation(projects.server.i18n)
|
implementation(projects.server.i18n)
|
||||||
|
|
||||||
|
// Settings module
|
||||||
|
implementation(projects.server.serverConfig)
|
||||||
|
|
||||||
// uncomment to test extensions directly
|
// uncomment to test extensions directly
|
||||||
// implementation(fileTree("lib/"))
|
// implementation(fileTree("lib/"))
|
||||||
implementation(kotlin("script-runtime"))
|
implementation(kotlin("script-runtime"))
|
||||||
@@ -123,6 +126,15 @@ sourceSets {
|
|||||||
main {
|
main {
|
||||||
resources {
|
resources {
|
||||||
srcDir("src/main/resources")
|
srcDir("src/main/resources")
|
||||||
|
srcDir("build/generated/src/main/resources")
|
||||||
|
}
|
||||||
|
kotlin {
|
||||||
|
srcDir("build/generated/src/main/kotlin")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
test {
|
||||||
|
resources {
|
||||||
|
srcDir("build/generated/src/test/resources")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,4 +241,12 @@ tasks {
|
|||||||
runKtlintCheckOverMainSourceSet {
|
runKtlintCheckOverMainSourceSet {
|
||||||
mustRunAfter(generateJte)
|
mustRunAfter(generateJte)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
compileKotlin {
|
||||||
|
dependsOn(":server:server-config-generate:generateSettings")
|
||||||
|
}
|
||||||
|
|
||||||
|
processResources {
|
||||||
|
dependsOn(":server:server-config-generate:generateSettings")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
62
server/server-config-generate/build.gradle.kts
Normal file
62
server/server-config-generate/build.gradle.kts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
plugins {
|
||||||
|
id(
|
||||||
|
libs.plugins.kotlin.jvm
|
||||||
|
.get()
|
||||||
|
.pluginId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Core Kotlin
|
||||||
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
|
implementation(kotlin("reflect"))
|
||||||
|
|
||||||
|
// Config handling
|
||||||
|
implementation(libs.config)
|
||||||
|
implementation(libs.config4k)
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation(libs.slf4japi)
|
||||||
|
implementation(libs.kotlinlogging)
|
||||||
|
|
||||||
|
// Serialization
|
||||||
|
implementation(libs.serialization.json)
|
||||||
|
implementation(libs.serialization.protobuf)
|
||||||
|
|
||||||
|
// Depend on server-config module for access to ServerConfig and SettingsRegistry
|
||||||
|
implementation(projects.server.serverConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks {
|
||||||
|
register<JavaExec>("generateSettings") {
|
||||||
|
group = "build setup"
|
||||||
|
description = "Generates settings from ServerConfig"
|
||||||
|
|
||||||
|
dependsOn(compileKotlin)
|
||||||
|
|
||||||
|
// Use this module's classpath which includes server-config as dependency
|
||||||
|
classpath = sourceSets.main.get().runtimeClasspath
|
||||||
|
|
||||||
|
mainClass.set("suwayomi.tachidesk.server.settings.generation.SettingsGeneratorKt")
|
||||||
|
|
||||||
|
// Get reference to server project for file paths
|
||||||
|
val serverProject = project(":server")
|
||||||
|
|
||||||
|
// Set working directory to the server module directory
|
||||||
|
workingDir = serverProject.projectDir
|
||||||
|
|
||||||
|
inputs.files(
|
||||||
|
serverProject.sourceSets.main.get().allSource.filter {
|
||||||
|
it.name.contains("ServerConfig") || it.name.contains("Settings")
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
outputs.files(
|
||||||
|
serverProject.file("build/generated/src/main/resources/server-reference.conf"),
|
||||||
|
serverProject.file("build/generated/src/test/resources/server-reference.conf"),
|
||||||
|
serverProject.file("build/generated/src/main/kotlin/suwayomi/tachidesk/graphql/types/SettingsType.kt"),
|
||||||
|
serverProject.file("build/generated/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models/BackupServerSettings.kt"),
|
||||||
|
serverProject.file("build/generated/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/handlers/BackupSettingsHandler.kt"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
@file:JvmName("SettingsGeneratorKt")
|
||||||
|
|
||||||
|
package suwayomi.tachidesk.server.settings.generation
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main function to generate settings files from ServerConfig
|
||||||
|
* This is called by the generateSettingsFiles Gradle task
|
||||||
|
*/
|
||||||
|
fun main() {
|
||||||
|
println("Generating settings files from ServerConfig registry...")
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Set output directories relative to the current working directory (server module)
|
||||||
|
val outputDir = File("build/generated/src/main/resources")
|
||||||
|
val testOutputDir = File("build/generated/src/test/resources")
|
||||||
|
val graphqlOutputDir = File("build/generated/src/main/kotlin/suwayomi/tachidesk/graphql/types")
|
||||||
|
val backupSettingsOutputDir = File("build/generated/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/models")
|
||||||
|
val backupSettingsHandlerOutputDir = File("build/generated/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/handlers")
|
||||||
|
|
||||||
|
SettingsGenerator.generate(
|
||||||
|
outputDir = outputDir,
|
||||||
|
testOutputDir = testOutputDir,
|
||||||
|
graphqlOutputDir = graphqlOutputDir,
|
||||||
|
backupSettingsOutputDir = backupSettingsOutputDir,
|
||||||
|
backupSettingsHandlerOutputDir = backupSettingsHandlerOutputDir,
|
||||||
|
)
|
||||||
|
|
||||||
|
println("✅ Settings files generation completed successfully!")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
println("❌ Error generating settings files: ${e.message}")
|
||||||
|
e.printStackTrace()
|
||||||
|
System.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings.generation
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
|
|
||||||
|
internal fun String.addIndentation(times: Int): String = this.prependIndent(" ".repeat(times))
|
||||||
|
|
||||||
|
object KotlinFileGeneratorHelper {
|
||||||
|
fun createFileHeader(packageName: String): String =
|
||||||
|
buildString {
|
||||||
|
appendLine("@file:Suppress(\"ktlint\")")
|
||||||
|
appendLine()
|
||||||
|
appendLine("/*")
|
||||||
|
appendLine(" * Copyright (C) Contributors to the Suwayomi project")
|
||||||
|
appendLine(" *")
|
||||||
|
appendLine(" * This Source Code Form is subject to the terms of the Mozilla Public")
|
||||||
|
appendLine(" * License, v. 2.0. If a copy of the MPL was not distributed with this")
|
||||||
|
appendLine(" * file, You can obtain one at https://mozilla.org/MPL/2.0/. */")
|
||||||
|
appendLine()
|
||||||
|
appendLine("package $packageName")
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createImports(
|
||||||
|
staticImports: List<String>,
|
||||||
|
settings: List<SettingsRegistry.SettingMetadata>,
|
||||||
|
): String =
|
||||||
|
buildString {
|
||||||
|
staticImports.forEach { appendLine("import $it") }
|
||||||
|
settings
|
||||||
|
.mapNotNull { it.typeInfo.imports }
|
||||||
|
.flatten()
|
||||||
|
.distinct()
|
||||||
|
.forEach { appendLine("import $it") }
|
||||||
|
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings.generation
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.text.appendLine
|
||||||
|
|
||||||
|
object SettingsBackupServerSettingsGenerator {
|
||||||
|
fun generate(
|
||||||
|
settings: Map<String, SettingsRegistry.SettingMetadata>,
|
||||||
|
outputFile: File,
|
||||||
|
) {
|
||||||
|
outputFile.parentFile.mkdirs()
|
||||||
|
|
||||||
|
val settingsToInclude = settings.values
|
||||||
|
|
||||||
|
if (settingsToInclude.isEmpty()) {
|
||||||
|
println("Warning: No settings found to create BackupServerSettings from.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortedSettings = settingsToInclude.sortedBy { it.protoNumber }
|
||||||
|
|
||||||
|
outputFile.writeText(
|
||||||
|
buildString {
|
||||||
|
appendLine(KotlinFileGeneratorHelper.createFileHeader("suwayomi.tachidesk.manga.impl.backup.proto.models"))
|
||||||
|
writeImports(sortedSettings)
|
||||||
|
writeClass(sortedSettings)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
println("BackupServerSettingsGenerator generated successfully! Total settings: ${settingsToInclude.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeImports(settings: List<SettingsRegistry.SettingMetadata>) {
|
||||||
|
appendLine(
|
||||||
|
KotlinFileGeneratorHelper.createImports(
|
||||||
|
listOf(
|
||||||
|
"kotlinx.serialization.Serializable",
|
||||||
|
"kotlinx.serialization.protobuf.ProtoNumber",
|
||||||
|
"suwayomi.tachidesk.graphql.types.Settings",
|
||||||
|
),
|
||||||
|
settings,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeClass(sortedSettings: List<SettingsRegistry.SettingMetadata>) {
|
||||||
|
appendLine("@Serializable")
|
||||||
|
appendLine("data class BackupServerSettings(")
|
||||||
|
|
||||||
|
writeSettings(sortedSettings, indentation = 4)
|
||||||
|
|
||||||
|
appendLine(") : Settings")
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeSettings(
|
||||||
|
sortedSettings: List<SettingsRegistry.SettingMetadata>,
|
||||||
|
indentation: Int,
|
||||||
|
) {
|
||||||
|
sortedSettings.forEach { setting ->
|
||||||
|
val deprecated = setting.deprecated
|
||||||
|
if (deprecated != null) {
|
||||||
|
val replaceWithSuffix = deprecated.replaceWith?.let { ", ReplaceWith(\"$it\")" } ?: ""
|
||||||
|
appendLine(
|
||||||
|
"@Deprecated(\"${deprecated.message}\"$replaceWithSuffix)".addIndentation(
|
||||||
|
indentation,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLine(
|
||||||
|
"@ProtoNumber(${setting.protoNumber}) override var ${setting.name}: ${getSettingType(setting)},"
|
||||||
|
.addIndentation(indentation),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSettingType(setting: SettingsRegistry.SettingMetadata): String =
|
||||||
|
setting.typeInfo.backupType
|
||||||
|
?: setting.typeInfo.specificType
|
||||||
|
?: setting.typeInfo.type.simpleName
|
||||||
|
?: throw RuntimeException("Unknown setting type: ${setting.typeInfo}")
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings.generation
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.text.appendLine
|
||||||
|
|
||||||
|
object SettingsBackupSettingsHandlerGenerator {
|
||||||
|
fun generate(
|
||||||
|
settings: Map<String, SettingsRegistry.SettingMetadata>,
|
||||||
|
outputFile: File,
|
||||||
|
) {
|
||||||
|
outputFile.parentFile.mkdirs()
|
||||||
|
|
||||||
|
val settingsToInclude = settings.values
|
||||||
|
|
||||||
|
if (settingsToInclude.isEmpty()) {
|
||||||
|
println("Warning: No settings found to create BackupServerSettings from.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupedSettings = settingsToInclude.groupBy { it.group }
|
||||||
|
|
||||||
|
outputFile.writeText(
|
||||||
|
buildString {
|
||||||
|
appendLine(KotlinFileGeneratorHelper.createFileHeader("suwayomi.tachidesk.manga.impl.backup.proto.handlers"))
|
||||||
|
writeImports(groupedSettings.values.flatten())
|
||||||
|
writeHandler(groupedSettings)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
println("BackupServerSettings generated successfully! Total settings: ${settingsToInclude.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeImports(settings: List<SettingsRegistry.SettingMetadata>) {
|
||||||
|
appendLine(
|
||||||
|
KotlinFileGeneratorHelper.createImports(
|
||||||
|
listOf(
|
||||||
|
"suwayomi.tachidesk.graphql.mutations.SettingsMutation",
|
||||||
|
"suwayomi.tachidesk.manga.impl.backup.BackupFlags",
|
||||||
|
"suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings",
|
||||||
|
"suwayomi.tachidesk.server.serverConfig",
|
||||||
|
"suwayomi.tachidesk.server.settings.SettingsRegistry",
|
||||||
|
),
|
||||||
|
settings,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeHandler(groupedSettings: Map<String, List<SettingsRegistry.SettingMetadata>>) {
|
||||||
|
appendLine("object BackupSettingsHandler {")
|
||||||
|
|
||||||
|
writeBackupFunction(groupedSettings)
|
||||||
|
appendLine()
|
||||||
|
writeRestoreFunction(groupedSettings.values.flatten())
|
||||||
|
|
||||||
|
appendLine("}")
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeBackupFunction(groupedSettings: Map<String, List<SettingsRegistry.SettingMetadata>>) {
|
||||||
|
val indentation = 4
|
||||||
|
val contentIndentation = indentation * 2
|
||||||
|
|
||||||
|
appendLine("fun backup(flags: BackupFlags): BackupServerSettings? {".addIndentation(indentation))
|
||||||
|
appendLine("if (!flags.includeServerSettings) { return null }".addIndentation(contentIndentation))
|
||||||
|
appendLine()
|
||||||
|
appendLine("return BackupServerSettings(".addIndentation(contentIndentation))
|
||||||
|
writeSettings(groupedSettings, indentation * 3)
|
||||||
|
appendLine(")".addIndentation(contentIndentation))
|
||||||
|
appendLine("}".addIndentation(indentation))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeRestoreFunction(settings: List<SettingsRegistry.SettingMetadata>) {
|
||||||
|
val indentation = 4
|
||||||
|
val contentIndentation = indentation * 2
|
||||||
|
|
||||||
|
appendLine("fun restore(backupServerSettings: BackupServerSettings?) {".addIndentation(indentation))
|
||||||
|
appendLine("if (backupServerSettings == null) { return }".addIndentation(contentIndentation))
|
||||||
|
appendLine()
|
||||||
|
appendLine("SettingsMutation().updateSettings(".addIndentation(contentIndentation))
|
||||||
|
appendLine("backupServerSettings.copy(".addIndentation(indentation * 3))
|
||||||
|
|
||||||
|
val deprecatedSettings = settings.filter { it.typeInfo.restoreLegacy != null }
|
||||||
|
deprecatedSettings.forEach { setting ->
|
||||||
|
appendLine(
|
||||||
|
"${setting.name} = SettingsRegistry.get(\"${setting.name}\")!!.typeInfo.restoreLegacy!!(".addIndentation(indentation * 4) +
|
||||||
|
"backupServerSettings.${setting.name}" +
|
||||||
|
") as ${getSettingType(setting, false)},",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
appendLine("),".addIndentation(indentation * 3))
|
||||||
|
appendLine(")".addIndentation(contentIndentation))
|
||||||
|
appendLine("}".addIndentation(indentation))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeSettings(
|
||||||
|
groupedSettings: Map<String, List<SettingsRegistry.SettingMetadata>>,
|
||||||
|
indentation: Int,
|
||||||
|
) {
|
||||||
|
groupedSettings.forEach { (group, settings) ->
|
||||||
|
appendLine("// $group".addIndentation(indentation))
|
||||||
|
settings.forEach { setting -> writeSetting(setting, indentation) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeSetting(
|
||||||
|
setting: SettingsRegistry.SettingMetadata,
|
||||||
|
indentation: Int,
|
||||||
|
) {
|
||||||
|
appendLine("${setting.name} = ${getConfigAccess(setting)},".addIndentation(indentation))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSettingType(
|
||||||
|
setting: SettingsRegistry.SettingMetadata,
|
||||||
|
asBackup: Boolean,
|
||||||
|
): String {
|
||||||
|
val possibleType = setting.typeInfo.specificType ?: setting.typeInfo.type.simpleName
|
||||||
|
|
||||||
|
val exception = RuntimeException("Unknown setting type: ${setting.typeInfo}")
|
||||||
|
|
||||||
|
if (asBackup) {
|
||||||
|
return setting.typeInfo.backupType ?: possibleType ?: throw exception
|
||||||
|
}
|
||||||
|
|
||||||
|
return possibleType ?: throw exception
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfigAccess(setting: SettingsRegistry.SettingMetadata): String {
|
||||||
|
if (setting.typeInfo.convertToBackupType != null) {
|
||||||
|
return "SettingsRegistry.get(\"${setting.name}\")!!.typeInfo.convertToBackupType!!(" +
|
||||||
|
"serverConfig.${setting.name}.value" +
|
||||||
|
") as ${getSettingType(setting, true)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "serverConfig.${setting.name}.value"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings.generation
|
||||||
|
|
||||||
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
|
import io.github.config4k.toConfig
|
||||||
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object SettingsConfigFileGenerator {
|
||||||
|
private const val SERVER_PREFIX = "server."
|
||||||
|
|
||||||
|
fun generate(
|
||||||
|
outputDir: File,
|
||||||
|
testOutputDir: File,
|
||||||
|
settings: Map<String, SettingsRegistry.SettingMetadata>,
|
||||||
|
) {
|
||||||
|
// Config files only include up-to-date settings.
|
||||||
|
val settingsToInclude = settings.filterValues { it.deprecated == null }
|
||||||
|
|
||||||
|
if (settingsToInclude.isEmpty()) {
|
||||||
|
println("Warning: No settings found to write to config files.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
generateServerReferenceConf(settingsToInclude, outputDir)
|
||||||
|
generateServerReferenceConf(settingsToInclude, testOutputDir)
|
||||||
|
|
||||||
|
println("Settings config file generated successfully! Total settings: ${settingsToInclude.size}")
|
||||||
|
println("- Main config: ${outputDir.resolve("server-reference.conf").absolutePath}")
|
||||||
|
println("- Test config: ${testOutputDir.resolve("server-reference.conf").absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateServerReferenceConf(
|
||||||
|
settings: Map<String, SettingsRegistry.SettingMetadata>,
|
||||||
|
outputDir: File,
|
||||||
|
) {
|
||||||
|
outputDir.mkdirs()
|
||||||
|
val outputFile = outputDir.resolve("server-reference.conf")
|
||||||
|
|
||||||
|
val groupedSettings = settings.values.groupBy { it.group }
|
||||||
|
|
||||||
|
// Write the config with comments
|
||||||
|
outputFile.writeText(
|
||||||
|
buildString {
|
||||||
|
writeSettings(groupedSettings)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeSettings(groupedSettings: Map<String, List<SettingsRegistry.SettingMetadata>>) {
|
||||||
|
val renderOptions =
|
||||||
|
ConfigRenderOptions
|
||||||
|
.defaults()
|
||||||
|
.setOriginComments(false)
|
||||||
|
.setComments(false)
|
||||||
|
.setFormatted(true)
|
||||||
|
.setJson(false)
|
||||||
|
|
||||||
|
var isFirstGroup = true
|
||||||
|
groupedSettings.forEach { (groupName, groupSettings) ->
|
||||||
|
// Prevent empty line at start of the file
|
||||||
|
if (!isFirstGroup) {
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
isFirstGroup = false
|
||||||
|
|
||||||
|
appendLine("# $groupName")
|
||||||
|
|
||||||
|
groupSettings.forEach { setting ->
|
||||||
|
writeSetting(setting, renderOptions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeSetting(
|
||||||
|
setting: SettingsRegistry.SettingMetadata,
|
||||||
|
renderOptions: ConfigRenderOptions,
|
||||||
|
) {
|
||||||
|
val key = "$SERVER_PREFIX${setting.name}"
|
||||||
|
|
||||||
|
val configValue = setting.defaultValue.toConfig("internal").getValue("internal")
|
||||||
|
var renderedValue = configValue.render(renderOptions)
|
||||||
|
|
||||||
|
// Force quotes on all string values for consistency
|
||||||
|
// Check if it's a string value that's not already quoted
|
||||||
|
if (setting.defaultValue is String && !renderedValue.startsWith("\"")) {
|
||||||
|
renderedValue = "\"$renderedValue\""
|
||||||
|
}
|
||||||
|
|
||||||
|
val settingString = "$key = $renderedValue"
|
||||||
|
|
||||||
|
val description = setting.description
|
||||||
|
if (description != null) {
|
||||||
|
val descriptionLines = description.split("\n")
|
||||||
|
|
||||||
|
if (descriptionLines.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLine("$settingString # ${descriptionLines[0]}")
|
||||||
|
descriptionLines.drop(1).forEach { line ->
|
||||||
|
appendLine("# $line")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
appendLine(settingString)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings.generation
|
||||||
|
|
||||||
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import suwayomi.tachidesk.server.ServerConfig
|
||||||
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
|
import suwayomi.tachidesk.server.util.ConfigTypeRegistration
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility to generate settings files from ServerConfig and SettingsRegistry
|
||||||
|
* This can be run as a standalone main function to generate all required files
|
||||||
|
*/
|
||||||
|
object SettingsGenerator {
|
||||||
|
init {
|
||||||
|
// Register custom types for config serialization
|
||||||
|
ConfigTypeRegistration.registerCustomTypes()
|
||||||
|
triggerSettingRegistration()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force registration of all settings without full ServerConfig instantiation
|
||||||
|
*/
|
||||||
|
private fun triggerSettingRegistration() {
|
||||||
|
// This creates a minimal instance just to trigger delegate registration
|
||||||
|
try {
|
||||||
|
val mockConfig =
|
||||||
|
ConfigFactory.parseString(
|
||||||
|
"""
|
||||||
|
server {
|
||||||
|
ip = "0.0.0.0"
|
||||||
|
port = 4567
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val tempConfig = ServerConfig { mockConfig.getConfig("server") }
|
||||||
|
// Access all properties to trigger delegate registrations
|
||||||
|
tempConfig::class.memberProperties.forEach { prop ->
|
||||||
|
try {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(prop as KProperty1<ServerConfig, Any?>).get(tempConfig)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Ignore errors during registration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Registration failed, but we tried
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generate(
|
||||||
|
outputDir: File,
|
||||||
|
testOutputDir: File,
|
||||||
|
graphqlOutputDir: File,
|
||||||
|
backupSettingsOutputDir: File,
|
||||||
|
backupSettingsHandlerOutputDir: File,
|
||||||
|
) {
|
||||||
|
val settings = SettingsRegistry.getAll()
|
||||||
|
|
||||||
|
if (settings.isEmpty()) {
|
||||||
|
println("Warning: No settings found in registry. Settings might not be initialized.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
println(" - Total: ${settings.size}")
|
||||||
|
println(" - Deprecated: ${settings.values.count { it.deprecated != null }}")
|
||||||
|
println(" - Require restart: ${settings.values.count { it.requiresRestart }}")
|
||||||
|
|
||||||
|
SettingsConfigFileGenerator.generate(outputDir, testOutputDir, settings)
|
||||||
|
|
||||||
|
val settingsTypeFile = graphqlOutputDir.resolve("SettingsType.kt")
|
||||||
|
SettingsGraphqlTypeGenerator.generate(settings, settingsTypeFile)
|
||||||
|
|
||||||
|
val backupServerSettingsFile = backupSettingsOutputDir.resolve("BackupServerSettings.kt")
|
||||||
|
SettingsBackupServerSettingsGenerator.generate(settings, backupServerSettingsFile)
|
||||||
|
|
||||||
|
val backupSettingsHandlerFile = backupSettingsHandlerOutputDir.resolve("BackupSettingsHandler.kt")
|
||||||
|
SettingsBackupSettingsHandlerGenerator.generate(settings, backupSettingsHandlerFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings.generation
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.text.appendLine
|
||||||
|
|
||||||
|
object SettingsGraphqlTypeGenerator {
|
||||||
|
fun generate(
|
||||||
|
settings: Map<String, SettingsRegistry.SettingMetadata>,
|
||||||
|
outputFile: File,
|
||||||
|
) {
|
||||||
|
outputFile.parentFile.mkdirs()
|
||||||
|
|
||||||
|
val settingsToInclude = settings.values
|
||||||
|
|
||||||
|
if (settingsToInclude.isEmpty()) {
|
||||||
|
println("Warning: No settings found to create graphql type from.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val groupedSettings = settingsToInclude.groupBy { it.group }
|
||||||
|
|
||||||
|
outputFile.writeText(
|
||||||
|
buildString {
|
||||||
|
appendLine(KotlinFileGeneratorHelper.createFileHeader("suwayomi.tachidesk.graphql.types"))
|
||||||
|
writeImports(groupedSettings.values.flatten())
|
||||||
|
writeSettingsInterface(groupedSettings)
|
||||||
|
writePartialSettingsType(groupedSettings)
|
||||||
|
writeSettingsType(groupedSettings)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
println("Graphql type generated successfully! Total settings: ${settingsToInclude.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeImports(settings: List<SettingsRegistry.SettingMetadata>) {
|
||||||
|
appendLine(
|
||||||
|
KotlinFileGeneratorHelper.createImports(
|
||||||
|
listOf(
|
||||||
|
"com.expediagroup.graphql.generator.annotations.GraphQLDeprecated",
|
||||||
|
"com.expediagroup.graphql.generator.annotations.GraphQLIgnore",
|
||||||
|
"suwayomi.tachidesk.graphql.server.primitives.Node",
|
||||||
|
"suwayomi.tachidesk.server.ServerConfig",
|
||||||
|
"suwayomi.tachidesk.server.serverConfig",
|
||||||
|
"suwayomi.tachidesk.server.settings.SettingsRegistry",
|
||||||
|
),
|
||||||
|
settings,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeSettingsInterface(groupedSettings: Map<String, List<SettingsRegistry.SettingMetadata>>) {
|
||||||
|
appendLine("interface Settings : Node {")
|
||||||
|
|
||||||
|
writeSettings(groupedSettings, indentation = 4, asType = true, isOverride = false, isNullable = true, isInterface = true)
|
||||||
|
|
||||||
|
appendLine("}")
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writePartialSettingsType(groupedSettings: Map<String, List<SettingsRegistry.SettingMetadata>>) {
|
||||||
|
appendLine("data class PartialSettingsType(")
|
||||||
|
|
||||||
|
writeSettings(groupedSettings, indentation = 4, asType = true, isOverride = true, isNullable = true, isInterface = false)
|
||||||
|
|
||||||
|
appendLine(") : Settings")
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeSettingsType(groupedSettings: Map<String, List<SettingsRegistry.SettingMetadata>>) {
|
||||||
|
appendLine("class SettingsType(")
|
||||||
|
|
||||||
|
writeSettings(groupedSettings, indentation = 4, asType = true, isOverride = true, isNullable = false, isInterface = false)
|
||||||
|
|
||||||
|
appendLine(") : Settings {")
|
||||||
|
|
||||||
|
// Write secondary constructor
|
||||||
|
val indentation = 4
|
||||||
|
appendLine("@Suppress(\"UNCHECKED_CAST\")".addIndentation(indentation))
|
||||||
|
appendLine("constructor(config: ServerConfig = serverConfig) : this(".addIndentation(indentation))
|
||||||
|
|
||||||
|
writeSettings(
|
||||||
|
groupedSettings,
|
||||||
|
indentation = indentation * 2,
|
||||||
|
asType = false,
|
||||||
|
isOverride = false,
|
||||||
|
isNullable = false,
|
||||||
|
isInterface = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
appendLine(")".addIndentation(indentation))
|
||||||
|
|
||||||
|
appendLine("}")
|
||||||
|
appendLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeSettings(
|
||||||
|
groupedSettings: Map<String, List<SettingsRegistry.SettingMetadata>>,
|
||||||
|
indentation: Int,
|
||||||
|
asType: Boolean,
|
||||||
|
isOverride: Boolean,
|
||||||
|
isNullable: Boolean,
|
||||||
|
isInterface: Boolean,
|
||||||
|
) {
|
||||||
|
groupedSettings.forEach { (group, settings) ->
|
||||||
|
appendLine("// $group".addIndentation(indentation))
|
||||||
|
settings.forEach { setting -> writeSetting(setting, indentation, asType, isOverride, isNullable, isInterface) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun StringBuilder.writeSetting(
|
||||||
|
setting: SettingsRegistry.SettingMetadata,
|
||||||
|
indentation: Int,
|
||||||
|
asType: Boolean,
|
||||||
|
isOverride: Boolean,
|
||||||
|
isNullable: Boolean,
|
||||||
|
isInterface: Boolean,
|
||||||
|
) {
|
||||||
|
if (!asType) {
|
||||||
|
appendLine("${getConfigAccess(setting)},".addIndentation(indentation))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setting.requiresRestart) {
|
||||||
|
appendLine("@GraphQLIgnore".addIndentation(indentation))
|
||||||
|
}
|
||||||
|
|
||||||
|
val deprecated = setting.deprecated
|
||||||
|
if (deprecated != null) {
|
||||||
|
val replaceWithSuffix = deprecated.replaceWith?.let { ", ReplaceWith(\"$it\")" } ?: ""
|
||||||
|
appendLine(
|
||||||
|
"@GraphQLDeprecated(\"${deprecated.message}\"$replaceWithSuffix)".addIndentation(
|
||||||
|
indentation,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val overridePrefix = if (isOverride) "override " else ""
|
||||||
|
val nullableSuffix = if (isNullable) "?" else ""
|
||||||
|
val commaSuffix = if (isOverride) "," else ""
|
||||||
|
appendLine(
|
||||||
|
"${overridePrefix}val ${setting.name}: ${getGraphQLType(
|
||||||
|
setting,
|
||||||
|
isInterface,
|
||||||
|
)}$nullableSuffix$commaSuffix".addIndentation(indentation),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getGraphQLType(
|
||||||
|
setting: SettingsRegistry.SettingMetadata,
|
||||||
|
isInterface: Boolean,
|
||||||
|
): String {
|
||||||
|
val possibleType = setting.typeInfo.specificType ?: setting.typeInfo.type.simpleName
|
||||||
|
|
||||||
|
val exception = RuntimeException("Unknown setting type: ${setting.typeInfo}")
|
||||||
|
|
||||||
|
if (isInterface) {
|
||||||
|
return setting.typeInfo.interfaceType ?: possibleType ?: throw exception
|
||||||
|
}
|
||||||
|
|
||||||
|
return possibleType ?: throw exception
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfigAccess(setting: SettingsRegistry.SettingMetadata): String {
|
||||||
|
if (setting.typeInfo.convertToGqlType != null) {
|
||||||
|
return "SettingsRegistry.get(\"${setting.name}\")!!.typeInfo.convertToGqlType!!(" +
|
||||||
|
"config.${setting.name}.value" +
|
||||||
|
") as ${getGraphQLType(setting, false)}"
|
||||||
|
}
|
||||||
|
|
||||||
|
return "config.${setting.name}.value"
|
||||||
|
}
|
||||||
|
}
|
||||||
39
server/server-config/build.gradle.kts
Normal file
39
server/server-config/build.gradle.kts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
plugins {
|
||||||
|
id(
|
||||||
|
libs.plugins.kotlin.jvm
|
||||||
|
.get()
|
||||||
|
.pluginId,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Core Kotlin
|
||||||
|
implementation(kotlin("stdlib-jdk8"))
|
||||||
|
implementation(kotlin("reflect"))
|
||||||
|
|
||||||
|
// Coroutines for MutableStateFlow
|
||||||
|
implementation(libs.coroutines.core)
|
||||||
|
implementation(libs.coroutines.jdk8)
|
||||||
|
|
||||||
|
// Config handling
|
||||||
|
implementation(libs.config)
|
||||||
|
implementation(libs.config4k)
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation(libs.slf4japi)
|
||||||
|
implementation(libs.kotlinlogging)
|
||||||
|
|
||||||
|
// Database (for SortOrder enum used in ServerConfig)
|
||||||
|
implementation(libs.exposed.core)
|
||||||
|
|
||||||
|
// GraphQL types used in ServerConfig
|
||||||
|
implementation(libs.graphql.kotlin.scheme)
|
||||||
|
|
||||||
|
// AndroidCompat for SystemPropertyOverridableConfigModule
|
||||||
|
implementation(projects.androidCompat.config)
|
||||||
|
|
||||||
|
// Serialization
|
||||||
|
implementation(libs.serialization.json)
|
||||||
|
implementation(libs.serialization.protobuf)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package suwayomi.tachidesk.graphql.types
|
||||||
|
|
||||||
|
enum class KoreaderSyncChecksumMethod {
|
||||||
|
BINARY,
|
||||||
|
FILENAME,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class KoreaderSyncStrategy {
|
||||||
|
PROMPT, // Ask on conflict
|
||||||
|
SILENT, // Always use latest
|
||||||
|
SEND, // Send changes only
|
||||||
|
RECEIVE, // Receive changes only
|
||||||
|
DISABLED,
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package suwayomi.tachidesk.graphql.types
|
||||||
|
|
||||||
|
// 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(
|
||||||
|
val target: String,
|
||||||
|
val compressionLevel: Double? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface SettingsDownloadConversion {
|
||||||
|
val mimeType: String
|
||||||
|
val target: String
|
||||||
|
val compressionLevel: Double?
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsDownloadConversionType(
|
||||||
|
override val mimeType: String,
|
||||||
|
override val target: String,
|
||||||
|
override val compressionLevel: Double?,
|
||||||
|
) : SettingsDownloadConversion
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package suwayomi.tachidesk.graphql.types
|
||||||
|
|
||||||
|
enum class WebUIInterface {
|
||||||
|
BROWSER,
|
||||||
|
ELECTRON,
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WebUIChannel {
|
||||||
|
BUNDLED, // the default webUI version bundled with the server release
|
||||||
|
STABLE,
|
||||||
|
PREVIEW,
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(channel: String): WebUIChannel = entries.find { it.name.lowercase() == channel.lowercase() } ?: STABLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class WebUIFlavor(
|
||||||
|
val uiName: String,
|
||||||
|
val repoUrl: String,
|
||||||
|
val versionMappingUrl: String,
|
||||||
|
val latestReleaseInfoUrl: String,
|
||||||
|
val baseFileName: String,
|
||||||
|
) {
|
||||||
|
WEBUI(
|
||||||
|
"WebUI",
|
||||||
|
"https://github.com/Suwayomi/Suwayomi-WebUI-preview",
|
||||||
|
"https://raw.githubusercontent.com/Suwayomi/Suwayomi-WebUI/master/versionToServerVersionMapping.json",
|
||||||
|
"https://api.github.com/repos/Suwayomi/Suwayomi-WebUI-preview/releases/latest",
|
||||||
|
"Suwayomi-WebUI",
|
||||||
|
),
|
||||||
|
VUI(
|
||||||
|
"VUI",
|
||||||
|
"https://github.com/Suwayomi/Suwayomi-VUI",
|
||||||
|
"https://raw.githubusercontent.com/Suwayomi/Suwayomi-VUI/master/versionToServerVersionMapping.json",
|
||||||
|
"https://api.github.com/repos/Suwayomi/Suwayomi-VUI/releases/latest",
|
||||||
|
"Suwayomi-VUI",
|
||||||
|
),
|
||||||
|
CUSTOM("Custom", "", "", "", ""),
|
||||||
|
;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun from(flavor: String): WebUIFlavor = entries.find { it.name.lowercase() == flavor.lowercase() } ?: WEBUI
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
import suwayomi.tachidesk.graphql.types.SettingsDownloadConversion
|
||||||
|
|
||||||
|
@OptIn(ExperimentalSerializationApi::class)
|
||||||
|
@Serializable
|
||||||
|
class BackupSettingsDownloadConversionType(
|
||||||
|
@ProtoNumber(1) override val mimeType: String,
|
||||||
|
@ProtoNumber(2) override val target: String,
|
||||||
|
@ProtoNumber(3) override val compressionLevel: Double?,
|
||||||
|
) : SettingsDownloadConversion
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.extension
|
||||||
|
|
||||||
|
object ExtensionsList {
|
||||||
|
val repoMatchRegex =
|
||||||
|
(
|
||||||
|
"https:\\/\\/(?>www\\.|raw\\.)?(github|githubusercontent)\\.com" +
|
||||||
|
"\\/([^\\/]+)\\/([^\\/]+)(?>(?>\\/tree|\\/blob)?\\/([^\\/\\n]*))?(?>\\/([^\\/\\n]*\\.json)?)?"
|
||||||
|
).toRegex()
|
||||||
|
}
|
||||||
@@ -0,0 +1,721 @@
|
|||||||
|
package suwayomi.tachidesk.server
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Contributors to the Suwayomi project
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.SupervisorJob
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
|
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||||
|
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
||||||
|
import suwayomi.tachidesk.graphql.types.KoreaderSyncChecksumMethod
|
||||||
|
import suwayomi.tachidesk.graphql.types.KoreaderSyncStrategy
|
||||||
|
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.proto.models.BackupSettingsDownloadConversionType
|
||||||
|
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.repoMatchRegex
|
||||||
|
import suwayomi.tachidesk.server.settings.BooleanSetting
|
||||||
|
import suwayomi.tachidesk.server.settings.DisableableDoubleSetting
|
||||||
|
import suwayomi.tachidesk.server.settings.DisableableIntSetting
|
||||||
|
import suwayomi.tachidesk.server.settings.DoubleSetting
|
||||||
|
import suwayomi.tachidesk.server.settings.DurationSetting
|
||||||
|
import suwayomi.tachidesk.server.settings.EnumSetting
|
||||||
|
import suwayomi.tachidesk.server.settings.IntSetting
|
||||||
|
import suwayomi.tachidesk.server.settings.ListSetting
|
||||||
|
import suwayomi.tachidesk.server.settings.MapSetting
|
||||||
|
import suwayomi.tachidesk.server.settings.MigratedConfigValue
|
||||||
|
import suwayomi.tachidesk.server.settings.PathSetting
|
||||||
|
import suwayomi.tachidesk.server.settings.SettingGroup
|
||||||
|
import suwayomi.tachidesk.server.settings.SettingsRegistry
|
||||||
|
import suwayomi.tachidesk.server.settings.StringSetting
|
||||||
|
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule
|
||||||
|
import kotlin.collections.associate
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||||
|
|
||||||
|
const val SERVER_CONFIG_MODULE_NAME = "server"
|
||||||
|
|
||||||
|
// Settings are ordered by "protoNumber".
|
||||||
|
class ServerConfig(
|
||||||
|
getConfig: () -> Config,
|
||||||
|
) : SystemPropertyOverridableConfigModule(
|
||||||
|
getConfig,
|
||||||
|
SERVER_CONFIG_MODULE_NAME,
|
||||||
|
) {
|
||||||
|
val ip: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 1,
|
||||||
|
group = SettingGroup.NETWORK,
|
||||||
|
defaultValue = "0.0.0.0",
|
||||||
|
pattern = "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$".toRegex(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val port: MutableStateFlow<Int> by IntSetting(
|
||||||
|
protoNumber = 2,
|
||||||
|
group = SettingGroup.NETWORK,
|
||||||
|
defaultValue = 4567,
|
||||||
|
min = 1,
|
||||||
|
max = 65535,
|
||||||
|
)
|
||||||
|
|
||||||
|
val socksProxyEnabled: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 3,
|
||||||
|
group = SettingGroup.PROXY,
|
||||||
|
defaultValue = false,
|
||||||
|
)
|
||||||
|
|
||||||
|
val socksProxyVersion: MutableStateFlow<Int> by IntSetting(
|
||||||
|
protoNumber = 4,
|
||||||
|
group = SettingGroup.PROXY,
|
||||||
|
defaultValue = 5,
|
||||||
|
min = 4,
|
||||||
|
max = 5,
|
||||||
|
)
|
||||||
|
|
||||||
|
val socksProxyHost: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 5,
|
||||||
|
group = SettingGroup.PROXY,
|
||||||
|
defaultValue = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val socksProxyPort: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 6,
|
||||||
|
group = SettingGroup.PROXY,
|
||||||
|
defaultValue = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val socksProxyUsername: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 7,
|
||||||
|
group = SettingGroup.PROXY,
|
||||||
|
defaultValue = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val socksProxyPassword: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 8,
|
||||||
|
group = SettingGroup.PROXY,
|
||||||
|
defaultValue = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val webUIFlavor: MutableStateFlow<WebUIFlavor> by EnumSetting(
|
||||||
|
protoNumber = 9,
|
||||||
|
group = SettingGroup.WEB_UI,
|
||||||
|
defaultValue = WebUIFlavor.WEBUI,
|
||||||
|
enumClass = WebUIFlavor::class,
|
||||||
|
typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.WebUIFlavor")),
|
||||||
|
)
|
||||||
|
|
||||||
|
val initialOpenInBrowserEnabled: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 10,
|
||||||
|
group = SettingGroup.WEB_UI,
|
||||||
|
defaultValue = true,
|
||||||
|
description = "Open client on startup",
|
||||||
|
)
|
||||||
|
|
||||||
|
val webUIInterface: MutableStateFlow<WebUIInterface> by EnumSetting(
|
||||||
|
protoNumber = 11,
|
||||||
|
group = SettingGroup.WEB_UI,
|
||||||
|
defaultValue = WebUIInterface.BROWSER,
|
||||||
|
enumClass = WebUIInterface::class,
|
||||||
|
typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.WebUIInterface")),
|
||||||
|
)
|
||||||
|
|
||||||
|
val electronPath: MutableStateFlow<String> by PathSetting(
|
||||||
|
protoNumber = 12,
|
||||||
|
group = SettingGroup.WEB_UI,
|
||||||
|
defaultValue = "",
|
||||||
|
mustExist = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
val webUIChannel: MutableStateFlow<WebUIChannel> by EnumSetting(
|
||||||
|
protoNumber = 13,
|
||||||
|
group = SettingGroup.WEB_UI,
|
||||||
|
defaultValue = WebUIChannel.STABLE,
|
||||||
|
enumClass = WebUIChannel::class,
|
||||||
|
typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.WebUIChannel")),
|
||||||
|
)
|
||||||
|
|
||||||
|
val webUIUpdateCheckInterval: MutableStateFlow<Double> by DisableableDoubleSetting(
|
||||||
|
protoNumber = 14,
|
||||||
|
group = SettingGroup.WEB_UI,
|
||||||
|
defaultValue = 23.hours.inWholeHours.toDouble(),
|
||||||
|
min = 0.0,
|
||||||
|
max = 23.0,
|
||||||
|
description = "Time in hours",
|
||||||
|
)
|
||||||
|
|
||||||
|
val downloadAsCbz: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 15,
|
||||||
|
defaultValue = false,
|
||||||
|
group = SettingGroup.DOWNLOADER,
|
||||||
|
)
|
||||||
|
|
||||||
|
val downloadsPath: MutableStateFlow<String> by PathSetting(
|
||||||
|
protoNumber = 16,
|
||||||
|
group = SettingGroup.DOWNLOADER,
|
||||||
|
defaultValue = "",
|
||||||
|
mustExist = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
val autoDownloadNewChapters: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 17,
|
||||||
|
defaultValue = false,
|
||||||
|
group = SettingGroup.DOWNLOADER,
|
||||||
|
)
|
||||||
|
|
||||||
|
val excludeEntryWithUnreadChapters: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 18,
|
||||||
|
group = SettingGroup.DOWNLOADER,
|
||||||
|
defaultValue = true,
|
||||||
|
description = "Exclude entries with unread chapters from auto-download",
|
||||||
|
)
|
||||||
|
|
||||||
|
val autoDownloadAheadLimit: MutableStateFlow<Int> by MigratedConfigValue(
|
||||||
|
protoNumber = 19,
|
||||||
|
defaultValue = 0,
|
||||||
|
group = SettingGroup.DOWNLOADER,
|
||||||
|
deprecated =
|
||||||
|
SettingsRegistry.SettingDeprecated(
|
||||||
|
replaceWith = "autoDownloadNewChaptersLimit",
|
||||||
|
message = "Replaced with autoDownloadNewChaptersLimit",
|
||||||
|
),
|
||||||
|
readMigrated = { autoDownloadNewChaptersLimit.value },
|
||||||
|
setMigrated = { autoDownloadNewChaptersLimit.value = it },
|
||||||
|
)
|
||||||
|
|
||||||
|
val autoDownloadNewChaptersLimit: MutableStateFlow<Int> by DisableableIntSetting(
|
||||||
|
protoNumber = 20,
|
||||||
|
group = SettingGroup.DOWNLOADER,
|
||||||
|
defaultValue = 0,
|
||||||
|
min = 0,
|
||||||
|
description = "Maximum number of new chapters to auto-download",
|
||||||
|
)
|
||||||
|
|
||||||
|
val autoDownloadIgnoreReUploads: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 21,
|
||||||
|
group = SettingGroup.DOWNLOADER,
|
||||||
|
defaultValue = false,
|
||||||
|
description = "Ignore re-uploaded chapters from auto-download",
|
||||||
|
)
|
||||||
|
|
||||||
|
val extensionRepos: MutableStateFlow<List<String>> by ListSetting<String>(
|
||||||
|
protoNumber = 22,
|
||||||
|
group = SettingGroup.EXTENSION,
|
||||||
|
defaultValue = emptyList(),
|
||||||
|
itemValidator = { url ->
|
||||||
|
if (url.matches(repoMatchRegex)) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
"Invalid repository URL format"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemToValidValue = { url ->
|
||||||
|
if (url.matches(repoMatchRegex)) {
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
typeInfo =
|
||||||
|
SettingsRegistry.PartialTypeInfo(
|
||||||
|
specificType = "List<String>",
|
||||||
|
),
|
||||||
|
description = "example: [\"https://github.com/MY_ACCOUNT/MY_REPO/tree/repo\"]",
|
||||||
|
)
|
||||||
|
|
||||||
|
val maxSourcesInParallel: MutableStateFlow<Int> by IntSetting(
|
||||||
|
protoNumber = 23,
|
||||||
|
group = SettingGroup.EXTENSION,
|
||||||
|
defaultValue = 6,
|
||||||
|
min = 1,
|
||||||
|
max = 20,
|
||||||
|
description =
|
||||||
|
"How many different sources can do requests (library update, downloads) in parallel. " +
|
||||||
|
"Library update/downloads are grouped by source and all manga of a source are updated/downloaded synchronously",
|
||||||
|
)
|
||||||
|
|
||||||
|
val excludeUnreadChapters: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 24,
|
||||||
|
defaultValue = true,
|
||||||
|
group = SettingGroup.LIBRARY_UPDATES,
|
||||||
|
)
|
||||||
|
|
||||||
|
val excludeNotStarted: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 25,
|
||||||
|
defaultValue = true,
|
||||||
|
group = SettingGroup.LIBRARY_UPDATES,
|
||||||
|
)
|
||||||
|
|
||||||
|
val excludeCompleted: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 26,
|
||||||
|
defaultValue = true,
|
||||||
|
group = SettingGroup.LIBRARY_UPDATES,
|
||||||
|
)
|
||||||
|
|
||||||
|
val globalUpdateInterval: MutableStateFlow<Double> by DisableableDoubleSetting(
|
||||||
|
protoNumber = 27,
|
||||||
|
group = SettingGroup.LIBRARY_UPDATES,
|
||||||
|
defaultValue = 12.hours.inWholeHours.toDouble(),
|
||||||
|
min = 6.0,
|
||||||
|
description = "Time in hours",
|
||||||
|
)
|
||||||
|
|
||||||
|
val updateMangas: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 28,
|
||||||
|
group = SettingGroup.LIBRARY_UPDATES,
|
||||||
|
defaultValue = false,
|
||||||
|
description = "Update manga metadata and thumbnail along with the chapter list update during the library update.",
|
||||||
|
)
|
||||||
|
|
||||||
|
val basicAuthEnabled: MutableStateFlow<Boolean> by MigratedConfigValue(
|
||||||
|
protoNumber = 29,
|
||||||
|
defaultValue = false,
|
||||||
|
group = SettingGroup.AUTH,
|
||||||
|
deprecated =
|
||||||
|
SettingsRegistry.SettingDeprecated(
|
||||||
|
replaceWith = "authMode",
|
||||||
|
message = "Removed - prefer authMode",
|
||||||
|
),
|
||||||
|
readMigrated = { authMode.value == AuthMode.BASIC_AUTH },
|
||||||
|
setMigrated = { authMode.value = if (it) AuthMode.BASIC_AUTH else AuthMode.NONE },
|
||||||
|
typeInfo =
|
||||||
|
SettingsRegistry.PartialTypeInfo(
|
||||||
|
restoreLegacy = { value ->
|
||||||
|
value.takeIf { authMode.value == AuthMode.NONE }
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val authUsername: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 30,
|
||||||
|
group = SettingGroup.AUTH,
|
||||||
|
defaultValue = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val authPassword: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 31,
|
||||||
|
group = SettingGroup.AUTH,
|
||||||
|
defaultValue = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val debugLogsEnabled: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 32,
|
||||||
|
defaultValue = false,
|
||||||
|
group = SettingGroup.MISC,
|
||||||
|
)
|
||||||
|
|
||||||
|
val gqlDebugLogsEnabled: MutableStateFlow<Boolean> by MigratedConfigValue(
|
||||||
|
protoNumber = 33,
|
||||||
|
defaultValue = false,
|
||||||
|
group = SettingGroup.MISC,
|
||||||
|
deprecated =
|
||||||
|
SettingsRegistry.SettingDeprecated(
|
||||||
|
message = "Removed - does not do anything",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val systemTrayEnabled: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 34,
|
||||||
|
defaultValue = true,
|
||||||
|
group = SettingGroup.MISC,
|
||||||
|
)
|
||||||
|
|
||||||
|
val maxLogFiles: MutableStateFlow<Int> by IntSetting(
|
||||||
|
protoNumber = 35,
|
||||||
|
group = SettingGroup.MISC,
|
||||||
|
defaultValue = 31,
|
||||||
|
min = 0,
|
||||||
|
description = "The max number of days to keep files before they get deleted",
|
||||||
|
)
|
||||||
|
|
||||||
|
private val logbackSizePattern = "^[0-9]+(|kb|KB|mb|MB|gb|GB)$".toRegex()
|
||||||
|
val maxLogFileSize: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 36,
|
||||||
|
group = SettingGroup.MISC,
|
||||||
|
defaultValue = "10mb",
|
||||||
|
pattern = logbackSizePattern,
|
||||||
|
description = "Maximum log file size - values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)",
|
||||||
|
)
|
||||||
|
|
||||||
|
val maxLogFolderSize: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 37,
|
||||||
|
group = SettingGroup.MISC,
|
||||||
|
defaultValue = "100mb",
|
||||||
|
pattern = logbackSizePattern,
|
||||||
|
description = "Maximum log folder size - values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)",
|
||||||
|
)
|
||||||
|
|
||||||
|
val backupPath: MutableStateFlow<String> by PathSetting(
|
||||||
|
protoNumber = 38,
|
||||||
|
group = SettingGroup.BACKUP,
|
||||||
|
defaultValue = "",
|
||||||
|
mustExist = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
val backupTime: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 39,
|
||||||
|
group = SettingGroup.BACKUP,
|
||||||
|
defaultValue = "00:00",
|
||||||
|
pattern = "^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$".toRegex(),
|
||||||
|
description = "Daily backup time (HH:MM) ; range: [00:00, 23:59]",
|
||||||
|
)
|
||||||
|
|
||||||
|
val backupInterval: MutableStateFlow<Int> by DisableableIntSetting(
|
||||||
|
protoNumber = 40,
|
||||||
|
group = SettingGroup.BACKUP,
|
||||||
|
defaultValue = 1,
|
||||||
|
min = 0,
|
||||||
|
description = "Time in days",
|
||||||
|
)
|
||||||
|
|
||||||
|
val backupTTL: MutableStateFlow<Int> by DisableableIntSetting(
|
||||||
|
protoNumber = 41,
|
||||||
|
group = SettingGroup.BACKUP,
|
||||||
|
defaultValue = 14.days.inWholeDays.toInt(),
|
||||||
|
min = 0,
|
||||||
|
description = "Backup retention in days",
|
||||||
|
)
|
||||||
|
|
||||||
|
val localSourcePath: MutableStateFlow<String> by PathSetting(
|
||||||
|
protoNumber = 42,
|
||||||
|
group = SettingGroup.LOCAL_SOURCE,
|
||||||
|
defaultValue = "",
|
||||||
|
mustExist = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
val flareSolverrEnabled: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 43,
|
||||||
|
defaultValue = false,
|
||||||
|
group = SettingGroup.CLOUDFLARE,
|
||||||
|
)
|
||||||
|
|
||||||
|
val flareSolverrUrl: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 44,
|
||||||
|
group = SettingGroup.CLOUDFLARE,
|
||||||
|
defaultValue = "http://localhost:8191",
|
||||||
|
)
|
||||||
|
|
||||||
|
val flareSolverrTimeout: MutableStateFlow<Int> by IntSetting(
|
||||||
|
protoNumber = 45,
|
||||||
|
group = SettingGroup.CLOUDFLARE,
|
||||||
|
defaultValue = 60.seconds.inWholeSeconds.toInt(),
|
||||||
|
min = 0,
|
||||||
|
description = "Time in seconds",
|
||||||
|
)
|
||||||
|
|
||||||
|
val flareSolverrSessionName: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 46,
|
||||||
|
group = SettingGroup.CLOUDFLARE,
|
||||||
|
defaultValue = "suwayomi",
|
||||||
|
)
|
||||||
|
|
||||||
|
val flareSolverrSessionTtl: MutableStateFlow<Int> by IntSetting(
|
||||||
|
protoNumber = 47,
|
||||||
|
group = SettingGroup.CLOUDFLARE,
|
||||||
|
defaultValue = 15.minutes.inWholeMinutes.toInt(),
|
||||||
|
min = 0,
|
||||||
|
description = "Time in minutes",
|
||||||
|
)
|
||||||
|
|
||||||
|
val flareSolverrAsResponseFallback: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 48,
|
||||||
|
defaultValue = false,
|
||||||
|
group = SettingGroup.CLOUDFLARE,
|
||||||
|
)
|
||||||
|
|
||||||
|
val opdsUseBinaryFileSizes: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 49,
|
||||||
|
group = SettingGroup.OPDS,
|
||||||
|
defaultValue = false,
|
||||||
|
description = "Display file size in binary (KiB, MiB, GiB) instead of decimal (KB, MB, GB)",
|
||||||
|
)
|
||||||
|
|
||||||
|
val opdsItemsPerPage: MutableStateFlow<Int> by IntSetting(
|
||||||
|
protoNumber = 50,
|
||||||
|
group = SettingGroup.OPDS,
|
||||||
|
defaultValue = 100,
|
||||||
|
min = 10,
|
||||||
|
max = 5000,
|
||||||
|
)
|
||||||
|
|
||||||
|
val opdsEnablePageReadProgress: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 51,
|
||||||
|
defaultValue = true,
|
||||||
|
group = SettingGroup.OPDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
val opdsMarkAsReadOnDownload: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 52,
|
||||||
|
defaultValue = false,
|
||||||
|
group = SettingGroup.OPDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
val opdsShowOnlyUnreadChapters: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 53,
|
||||||
|
defaultValue = false,
|
||||||
|
group = SettingGroup.OPDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
val opdsShowOnlyDownloadedChapters: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 54,
|
||||||
|
defaultValue = false,
|
||||||
|
group = SettingGroup.OPDS,
|
||||||
|
)
|
||||||
|
|
||||||
|
val opdsChapterSortOrder: MutableStateFlow<SortOrder> by EnumSetting(
|
||||||
|
protoNumber = 55,
|
||||||
|
group = SettingGroup.OPDS,
|
||||||
|
defaultValue = SortOrder.DESC,
|
||||||
|
enumClass = SortOrder::class,
|
||||||
|
typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("org.jetbrains.exposed.sql.SortOrder")),
|
||||||
|
)
|
||||||
|
|
||||||
|
val authMode: MutableStateFlow<AuthMode> by EnumSetting(
|
||||||
|
protoNumber = 56,
|
||||||
|
group = SettingGroup.AUTH,
|
||||||
|
defaultValue = AuthMode.NONE,
|
||||||
|
enumClass = AuthMode::class,
|
||||||
|
typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.AuthMode")),
|
||||||
|
)
|
||||||
|
|
||||||
|
val downloadConversions: MutableStateFlow<Map<String, DownloadConversion>> by MapSetting<String, DownloadConversion>(
|
||||||
|
protoNumber = 57,
|
||||||
|
defaultValue = emptyMap(),
|
||||||
|
group = SettingGroup.DOWNLOADER,
|
||||||
|
typeInfo =
|
||||||
|
SettingsRegistry.PartialTypeInfo(
|
||||||
|
specificType = "List<SettingsDownloadConversionType>",
|
||||||
|
interfaceType = "List<SettingsDownloadConversion>",
|
||||||
|
backupType = "List<BackupSettingsDownloadConversionType>",
|
||||||
|
imports =
|
||||||
|
listOf(
|
||||||
|
"suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSettingsDownloadConversionType",
|
||||||
|
),
|
||||||
|
convertToGqlType = { value ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val castedValue = value as Map<String, DownloadConversion>
|
||||||
|
|
||||||
|
castedValue.map {
|
||||||
|
SettingsDownloadConversionType(
|
||||||
|
it.key,
|
||||||
|
it.value.target,
|
||||||
|
it.value.compressionLevel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
convertToInternalType = { list ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val castedList = list as List<SettingsDownloadConversionType>
|
||||||
|
|
||||||
|
castedList.associate {
|
||||||
|
it.mimeType to
|
||||||
|
DownloadConversion(
|
||||||
|
target = it.target,
|
||||||
|
compressionLevel = it.compressionLevel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
convertToBackupType = { value ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val castedValue = value as Map<String, DownloadConversion>
|
||||||
|
|
||||||
|
castedValue.map {
|
||||||
|
BackupSettingsDownloadConversionType(
|
||||||
|
it.key,
|
||||||
|
it.value.target,
|
||||||
|
it.value.compressionLevel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
description =
|
||||||
|
"""
|
||||||
|
map input mime type to conversion information, or "default" for others
|
||||||
|
server.downloadConversions."image/webp" = {
|
||||||
|
target = "image/jpeg" # image type to convert to
|
||||||
|
compressionLevel = 0.8 # quality in range [0,1], leave away to use default compression
|
||||||
|
}
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
val jwtAudience: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 58,
|
||||||
|
group = SettingGroup.AUTH,
|
||||||
|
defaultValue = "suwayomi-server-api",
|
||||||
|
)
|
||||||
|
|
||||||
|
val koreaderSyncServerUrl: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 59,
|
||||||
|
group = SettingGroup.KOREADER_SYNC,
|
||||||
|
defaultValue = "http://localhost:17200",
|
||||||
|
)
|
||||||
|
|
||||||
|
val koreaderSyncUsername: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 60,
|
||||||
|
group = SettingGroup.KOREADER_SYNC,
|
||||||
|
defaultValue = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val koreaderSyncUserkey: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 61,
|
||||||
|
group = SettingGroup.KOREADER_SYNC,
|
||||||
|
defaultValue = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val koreaderSyncDeviceId: MutableStateFlow<String> by StringSetting(
|
||||||
|
protoNumber = 62,
|
||||||
|
group = SettingGroup.KOREADER_SYNC,
|
||||||
|
defaultValue = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
val koreaderSyncChecksumMethod: MutableStateFlow<KoreaderSyncChecksumMethod> by EnumSetting(
|
||||||
|
protoNumber = 63,
|
||||||
|
group = SettingGroup.KOREADER_SYNC,
|
||||||
|
defaultValue = KoreaderSyncChecksumMethod.BINARY,
|
||||||
|
enumClass = KoreaderSyncChecksumMethod::class,
|
||||||
|
typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.KoreaderSyncChecksumMethod")),
|
||||||
|
)
|
||||||
|
|
||||||
|
val koreaderSyncStrategy: MutableStateFlow<KoreaderSyncStrategy> by EnumSetting(
|
||||||
|
protoNumber = 64,
|
||||||
|
group = SettingGroup.KOREADER_SYNC,
|
||||||
|
defaultValue = KoreaderSyncStrategy.DISABLED,
|
||||||
|
enumClass = KoreaderSyncStrategy::class,
|
||||||
|
typeInfo = SettingsRegistry.PartialTypeInfo(imports = listOf("suwayomi.tachidesk.graphql.types.KoreaderSyncStrategy")),
|
||||||
|
)
|
||||||
|
|
||||||
|
val koreaderSyncPercentageTolerance: MutableStateFlow<Double> by DoubleSetting(
|
||||||
|
protoNumber = 65,
|
||||||
|
group = SettingGroup.KOREADER_SYNC,
|
||||||
|
defaultValue = 0.000000000000001,
|
||||||
|
min = 0.000000000000001,
|
||||||
|
max = 1.0,
|
||||||
|
description = "Absolute tolerance for progress comparison",
|
||||||
|
)
|
||||||
|
|
||||||
|
val jwtTokenExpiry: MutableStateFlow<Duration> by DurationSetting(
|
||||||
|
protoNumber = 66,
|
||||||
|
group = SettingGroup.AUTH,
|
||||||
|
defaultValue = 5.minutes,
|
||||||
|
min = 0.seconds,
|
||||||
|
)
|
||||||
|
|
||||||
|
val jwtRefreshExpiry: MutableStateFlow<Duration> by DurationSetting(
|
||||||
|
protoNumber = 67,
|
||||||
|
group = SettingGroup.AUTH,
|
||||||
|
defaultValue = 60.days,
|
||||||
|
min = 0.seconds,
|
||||||
|
)
|
||||||
|
|
||||||
|
val webUIEnabled: MutableStateFlow<Boolean> by BooleanSetting(
|
||||||
|
protoNumber = 68,
|
||||||
|
group = SettingGroup.WEB_UI,
|
||||||
|
defaultValue = true,
|
||||||
|
requiresRestart = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
/** ****************************************************************** **/
|
||||||
|
/** **/
|
||||||
|
/** Renamed settings **/
|
||||||
|
/** **/
|
||||||
|
|
||||||
|
/** ****************************************************************** **/
|
||||||
|
val basicAuthUsername: MutableStateFlow<String> by MigratedConfigValue(
|
||||||
|
protoNumber = 99991,
|
||||||
|
defaultValue = "",
|
||||||
|
group = SettingGroup.AUTH,
|
||||||
|
deprecated =
|
||||||
|
SettingsRegistry.SettingDeprecated(
|
||||||
|
replaceWith = "authUsername",
|
||||||
|
message = "Removed - prefer authUsername",
|
||||||
|
),
|
||||||
|
readMigrated = { authUsername.value },
|
||||||
|
setMigrated = { authUsername.value = it },
|
||||||
|
)
|
||||||
|
|
||||||
|
val basicAuthPassword: MutableStateFlow<String> by MigratedConfigValue(
|
||||||
|
protoNumber = 99992,
|
||||||
|
defaultValue = "",
|
||||||
|
group = SettingGroup.AUTH,
|
||||||
|
deprecated =
|
||||||
|
SettingsRegistry.SettingDeprecated(
|
||||||
|
replaceWith = "authPassword",
|
||||||
|
message = "Removed - prefer authPassword",
|
||||||
|
),
|
||||||
|
readMigrated = { authPassword.value },
|
||||||
|
setMigrated = { authPassword.value = it },
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun <T> subscribeTo(
|
||||||
|
flow: Flow<T>,
|
||||||
|
onChange: suspend (value: T) -> Unit,
|
||||||
|
ignoreInitialValue: Boolean = true,
|
||||||
|
) {
|
||||||
|
val actualFlow =
|
||||||
|
if (ignoreInitialValue) {
|
||||||
|
flow.drop(1)
|
||||||
|
} else {
|
||||||
|
flow
|
||||||
|
}
|
||||||
|
|
||||||
|
val sharedFlow = MutableSharedFlow<T>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||||
|
actualFlow.distinctUntilChanged().mapLatest { sharedFlow.emit(it) }.launchIn(mutableConfigValueScope)
|
||||||
|
sharedFlow.onEach { onChange(it) }.launchIn(mutableConfigValueScope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> subscribeTo(
|
||||||
|
flow: Flow<T>,
|
||||||
|
onChange: suspend () -> Unit,
|
||||||
|
ignoreInitialValue: Boolean = true,
|
||||||
|
) {
|
||||||
|
subscribeTo(flow, { _ -> onChange() }, ignoreInitialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> subscribeTo(
|
||||||
|
mutableStateFlow: MutableStateFlow<T>,
|
||||||
|
onChange: suspend (value: T) -> Unit,
|
||||||
|
ignoreInitialValue: Boolean = true,
|
||||||
|
) {
|
||||||
|
subscribeTo(mutableStateFlow.asStateFlow(), onChange, ignoreInitialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> subscribeTo(
|
||||||
|
mutableStateFlow: MutableStateFlow<T>,
|
||||||
|
onChange: suspend () -> Unit,
|
||||||
|
ignoreInitialValue: Boolean = true,
|
||||||
|
) {
|
||||||
|
subscribeTo(mutableStateFlow.asStateFlow(), { _ -> onChange() }, ignoreInitialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun register(getConfig: () -> Config) =
|
||||||
|
ServerConfig {
|
||||||
|
getConfig().getConfig(
|
||||||
|
SERVER_CONFIG_MODULE_NAME,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,581 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings
|
||||||
|
|
||||||
|
import io.github.config4k.getValue
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.drop
|
||||||
|
import kotlinx.coroutines.flow.filter
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import suwayomi.tachidesk.server.ServerConfig
|
||||||
|
import suwayomi.tachidesk.server.mutableConfigValueScope
|
||||||
|
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||||
|
import java.io.File
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base delegate for settings to read values from the config file with automatic setting registration and validation
|
||||||
|
*/
|
||||||
|
open class SettingDelegate<T : Any>(
|
||||||
|
protected val protoNumber: Int,
|
||||||
|
val defaultValue: T,
|
||||||
|
val validator: ((T) -> String?)? = null,
|
||||||
|
val toValidValue: ((T) -> T)? = null,
|
||||||
|
protected val group: SettingGroup,
|
||||||
|
protected val requiresRestart: Boolean? = null,
|
||||||
|
protected val typeInfo: SettingsRegistry.PartialTypeInfo? = null,
|
||||||
|
protected val deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
protected val description: String? = null,
|
||||||
|
) {
|
||||||
|
var flow: MutableStateFlow<T>? = null
|
||||||
|
lateinit var propertyName: String
|
||||||
|
lateinit var moduleName: String
|
||||||
|
|
||||||
|
operator fun provideDelegate(
|
||||||
|
thisRef: ServerConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
): SettingDelegate<T> {
|
||||||
|
propertyName = property.name
|
||||||
|
moduleName = thisRef.moduleName
|
||||||
|
|
||||||
|
SettingsRegistry.register(
|
||||||
|
SettingsRegistry.SettingMetadata(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
name = propertyName,
|
||||||
|
typeInfo =
|
||||||
|
SettingsRegistry.TypeInfo(
|
||||||
|
type = typeInfo?.type ?: defaultValue::class,
|
||||||
|
specificType = typeInfo?.specificType,
|
||||||
|
interfaceType = typeInfo?.interfaceType,
|
||||||
|
backupType = typeInfo?.backupType,
|
||||||
|
imports = typeInfo?.imports,
|
||||||
|
convertToGqlType = typeInfo?.convertToGqlType,
|
||||||
|
convertToInternalType = typeInfo?.convertToInternalType,
|
||||||
|
convertToBackupType = typeInfo?.convertToBackupType,
|
||||||
|
),
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
validator =
|
||||||
|
validator?.let { validate ->
|
||||||
|
{ value ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
validate(value as T)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
group = group.value,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart ?: false,
|
||||||
|
description =
|
||||||
|
run {
|
||||||
|
val defaultValueString =
|
||||||
|
when (defaultValue) {
|
||||||
|
is String -> "\"$defaultValue\""
|
||||||
|
else -> defaultValue
|
||||||
|
}
|
||||||
|
val defaultValueComment = "default: $defaultValueString"
|
||||||
|
|
||||||
|
if (description != null) {
|
||||||
|
"$defaultValueComment ; $description"
|
||||||
|
} else {
|
||||||
|
defaultValueComment
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
inline operator fun <reified ReifiedT : MutableStateFlow<R>, reified R> getValue(
|
||||||
|
thisRef: ServerConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
): ReifiedT {
|
||||||
|
if (flow != null) {
|
||||||
|
return flow as ReifiedT
|
||||||
|
}
|
||||||
|
|
||||||
|
val stateFlow = thisRef.overridableConfig.getValue<ServerConfig, ReifiedT>(thisRef, property)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
flow = stateFlow as MutableStateFlow<T>
|
||||||
|
|
||||||
|
// Validate config value and optionally fallback to default value
|
||||||
|
validator?.let { validate ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val initialValue = stateFlow.value
|
||||||
|
val error = validate(initialValue)
|
||||||
|
if (error != null) {
|
||||||
|
KotlinLogging.logger { }.warn {
|
||||||
|
"Invalid config value ($initialValue) for $moduleName.$propertyName: $error. Using default value: $defaultValue"
|
||||||
|
}
|
||||||
|
|
||||||
|
stateFlow.value = toValidValue?.let { it(initialValue) } ?: defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stateFlow
|
||||||
|
.drop(1)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.filter { it != thisRef.overridableConfig.getConfig().getValue<ServerConfig, R>(thisRef, property) }
|
||||||
|
.onEach { value ->
|
||||||
|
validator?.let { validate ->
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val error = validate(value as T)
|
||||||
|
if (error != null) {
|
||||||
|
throw IllegalArgumentException("Setting $propertyName: $error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
GlobalConfigManager.updateValue("$moduleName.$propertyName", value as Any)
|
||||||
|
}.launchIn(mutableConfigValueScope)
|
||||||
|
|
||||||
|
return stateFlow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MigratedConfigValue<T : Any>(
|
||||||
|
private val protoNumber: Int,
|
||||||
|
private val defaultValue: T,
|
||||||
|
private val group: SettingGroup,
|
||||||
|
private val requiresRestart: Boolean? = null,
|
||||||
|
private val typeInfo: SettingsRegistry.PartialTypeInfo? = null,
|
||||||
|
private val deprecated: SettingsRegistry.SettingDeprecated,
|
||||||
|
private val readMigrated: (() -> T) = { defaultValue },
|
||||||
|
private val setMigrated: ((T) -> Unit) = {},
|
||||||
|
) {
|
||||||
|
var flow: MutableStateFlow<T>? = null
|
||||||
|
lateinit var propertyName: String
|
||||||
|
lateinit var moduleName: String
|
||||||
|
|
||||||
|
operator fun provideDelegate(
|
||||||
|
thisRef: ServerConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
): MigratedConfigValue<T> {
|
||||||
|
propertyName = property.name
|
||||||
|
moduleName = thisRef.moduleName
|
||||||
|
|
||||||
|
SettingsRegistry.register(
|
||||||
|
SettingsRegistry.SettingMetadata(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
name = propertyName,
|
||||||
|
typeInfo =
|
||||||
|
SettingsRegistry.TypeInfo(
|
||||||
|
type = typeInfo?.type ?: defaultValue::class,
|
||||||
|
specificType = typeInfo?.specificType,
|
||||||
|
backupType = typeInfo?.backupType,
|
||||||
|
imports = typeInfo?.imports,
|
||||||
|
restoreLegacy = typeInfo?.restoreLegacy,
|
||||||
|
),
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
group = group.value,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart ?: false,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun getValue(
|
||||||
|
thisRef: ServerConfig,
|
||||||
|
property: KProperty<*>,
|
||||||
|
): MutableStateFlow<T> {
|
||||||
|
if (flow != null) {
|
||||||
|
return flow!!
|
||||||
|
}
|
||||||
|
|
||||||
|
val value = readMigrated()
|
||||||
|
|
||||||
|
val stateFlow = MutableStateFlow(value)
|
||||||
|
flow = stateFlow
|
||||||
|
|
||||||
|
stateFlow
|
||||||
|
.drop(1)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.filter { it != readMigrated() }
|
||||||
|
.onEach(setMigrated)
|
||||||
|
.launchIn(mutableConfigValueScope)
|
||||||
|
|
||||||
|
return stateFlow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specialized delegates for common types
|
||||||
|
class StringSetting(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: String,
|
||||||
|
pattern: Regex? = null,
|
||||||
|
maxLength: Int? = null,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : SettingDelegate<String>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
validator = { value ->
|
||||||
|
when {
|
||||||
|
pattern != null && !value.matches(pattern) ->
|
||||||
|
"Value must match pattern: ${pattern.pattern}"
|
||||||
|
maxLength != null && value.length > maxLength ->
|
||||||
|
"Value must not exceed $maxLength characters"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toValidValue = { value ->
|
||||||
|
if (pattern != null && !value.matches(pattern)) {
|
||||||
|
defaultValue
|
||||||
|
} else {
|
||||||
|
maxLength?.let { value.take(it) } ?: value
|
||||||
|
}
|
||||||
|
},
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
|
|
||||||
|
abstract class RangeSetting<T : Comparable<T>>(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: T,
|
||||||
|
min: T? = null,
|
||||||
|
max: T? = null,
|
||||||
|
validator: ((T) -> String?)? = null,
|
||||||
|
toValidValue: ((T) -> T)? = null,
|
||||||
|
group: SettingGroup,
|
||||||
|
typeInfo: SettingsRegistry.PartialTypeInfo? = null,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : SettingDelegate<T>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
validator =
|
||||||
|
validator ?: { value ->
|
||||||
|
when {
|
||||||
|
min != null && value < min -> "Value must be at least $min"
|
||||||
|
max != null && value > max -> "Value must not exceed $max"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toValidValue =
|
||||||
|
toValidValue ?: { value ->
|
||||||
|
val coerceAtLeast = min?.let { value.coerceAtLeast(min) } ?: value
|
||||||
|
val coerceAtMost = max?.let { coerceAtLeast.coerceAtMost(max) } ?: value
|
||||||
|
|
||||||
|
coerceAtMost
|
||||||
|
},
|
||||||
|
group = group,
|
||||||
|
typeInfo = typeInfo,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description =
|
||||||
|
run {
|
||||||
|
val defaultDescription = "range: [${min ?: "-∞"}, ${max ?: "+∞"}]"
|
||||||
|
|
||||||
|
if (description != null) {
|
||||||
|
"$defaultDescription ; $description"
|
||||||
|
} else {
|
||||||
|
defaultDescription
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
class IntSetting(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: Int,
|
||||||
|
min: Int? = null,
|
||||||
|
max: Int? = null,
|
||||||
|
customValidator: ((Int) -> String?)? = null,
|
||||||
|
customToValidValue: ((Int) -> Int)? = null,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : RangeSetting<Int>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
min = min,
|
||||||
|
max = max,
|
||||||
|
validator = customValidator,
|
||||||
|
toValidValue = customToValidValue,
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
|
|
||||||
|
class DisableableIntSetting(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: Int,
|
||||||
|
min: Int? = null,
|
||||||
|
max: Int? = null,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : RangeSetting<Int>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
min = min,
|
||||||
|
max = max,
|
||||||
|
validator = { value ->
|
||||||
|
when {
|
||||||
|
value == 0 -> null
|
||||||
|
min != null && value < min -> "Value must be 0.0 or at least $min"
|
||||||
|
max != null && value > max -> "Value must be 0.0 or not exceed $max"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toValidValue = { value ->
|
||||||
|
if (value == 0) {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
val coerceAtLeast = min?.let { value.coerceAtLeast(min) } ?: value
|
||||||
|
val coerceAtMost = max?.let { coerceAtLeast.coerceAtMost(max) } ?: value
|
||||||
|
|
||||||
|
coerceAtMost
|
||||||
|
}
|
||||||
|
},
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description =
|
||||||
|
run {
|
||||||
|
if (description != null) {
|
||||||
|
"0 == disabled ; $description"
|
||||||
|
} else {
|
||||||
|
description
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
class DoubleSetting(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: Double,
|
||||||
|
min: Double? = null,
|
||||||
|
max: Double? = null,
|
||||||
|
customValidator: ((Double) -> String?)? = null,
|
||||||
|
customToValidValue: ((Double) -> Double)? = null,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : RangeSetting<Double>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
min = min,
|
||||||
|
max = max,
|
||||||
|
validator = customValidator,
|
||||||
|
toValidValue = customToValidValue,
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
|
|
||||||
|
class DisableableDoubleSetting(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: Double,
|
||||||
|
min: Double? = null,
|
||||||
|
max: Double? = null,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : RangeSetting<Double>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
min = min,
|
||||||
|
max = max,
|
||||||
|
validator = { value ->
|
||||||
|
when {
|
||||||
|
value == 0.0 -> null
|
||||||
|
min != null && value < min -> "Value must 0.0 or be at least $min"
|
||||||
|
max != null && value > max -> "Value must 0.0 or not exceed $max"
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toValidValue = { value ->
|
||||||
|
if (value == 0.0) {
|
||||||
|
value
|
||||||
|
} else {
|
||||||
|
val coerceAtLeast = min?.let { value.coerceAtLeast(min) } ?: value
|
||||||
|
val coerceAtMost = max?.let { coerceAtLeast.coerceAtMost(max) } ?: value
|
||||||
|
|
||||||
|
coerceAtMost
|
||||||
|
}
|
||||||
|
},
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description =
|
||||||
|
run {
|
||||||
|
if (description != null) {
|
||||||
|
"0.0 == disabled ; $description"
|
||||||
|
} else {
|
||||||
|
description
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
class BooleanSetting(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: Boolean,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : SettingDelegate<Boolean>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
validator = null,
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
|
|
||||||
|
class PathSetting(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: String,
|
||||||
|
mustExist: Boolean = false,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : SettingDelegate<String>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
validator = { value ->
|
||||||
|
if (mustExist && value.isNotEmpty() && !File(value).exists()) {
|
||||||
|
"Path does not exist: $value"
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
|
|
||||||
|
class EnumSetting<T : Enum<T>>(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: T,
|
||||||
|
enumClass: KClass<T>,
|
||||||
|
typeInfo: SettingsRegistry.PartialTypeInfo? = null,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : SettingDelegate<T>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
validator = { value ->
|
||||||
|
if (!enumClass.java.isInstance(value)) {
|
||||||
|
"Invalid enum value for ${enumClass.simpleName}"
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
typeInfo = typeInfo,
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description =
|
||||||
|
run {
|
||||||
|
val defaultDescription = "options: ${enumClass.java.enumConstants.joinToString()}"
|
||||||
|
|
||||||
|
if (description != null) {
|
||||||
|
"$description ; $defaultDescription"
|
||||||
|
} else {
|
||||||
|
defaultDescription
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
class DurationSetting(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: Duration,
|
||||||
|
min: Duration? = null,
|
||||||
|
max: Duration? = null,
|
||||||
|
customValidator: ((Duration) -> String?)? = null,
|
||||||
|
customToValidValue: ((Duration) -> Duration)? = null,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : RangeSetting<Duration>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
min = min,
|
||||||
|
max = max,
|
||||||
|
validator = customValidator,
|
||||||
|
toValidValue = customToValidValue,
|
||||||
|
typeInfo =
|
||||||
|
SettingsRegistry.PartialTypeInfo(
|
||||||
|
imports = listOf("kotlin.time.Duration"),
|
||||||
|
),
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ListSetting<T>(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: List<T>,
|
||||||
|
itemValidator: ((T) -> String?)? = null,
|
||||||
|
itemToValidValue: ((T) -> T?)? = null,
|
||||||
|
typeInfo: SettingsRegistry.PartialTypeInfo? = null,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : SettingDelegate<List<T>>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
validator = { list ->
|
||||||
|
if (itemValidator != null) {
|
||||||
|
list.firstNotNullOfOrNull { item ->
|
||||||
|
itemValidator(item)?.let { error -> "Invalid item: $error" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
toValidValue = { list ->
|
||||||
|
if (itemToValidValue != null) {
|
||||||
|
list.mapNotNull(itemToValidValue)
|
||||||
|
} else {
|
||||||
|
defaultValue
|
||||||
|
}
|
||||||
|
},
|
||||||
|
typeInfo = typeInfo,
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
|
|
||||||
|
class MapSetting<K, V>(
|
||||||
|
protoNumber: Int,
|
||||||
|
defaultValue: Map<K, V>,
|
||||||
|
validator: ((Map<K, V>) -> String?)? = null,
|
||||||
|
typeInfo: SettingsRegistry.PartialTypeInfo? = null,
|
||||||
|
group: SettingGroup,
|
||||||
|
deprecated: SettingsRegistry.SettingDeprecated? = null,
|
||||||
|
requiresRestart: Boolean? = null,
|
||||||
|
description: String? = null,
|
||||||
|
) : SettingDelegate<Map<K, V>>(
|
||||||
|
protoNumber = protoNumber,
|
||||||
|
defaultValue = defaultValue,
|
||||||
|
validator = validator,
|
||||||
|
typeInfo = typeInfo,
|
||||||
|
group = group,
|
||||||
|
deprecated = deprecated,
|
||||||
|
requiresRestart = requiresRestart,
|
||||||
|
description = description,
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings
|
||||||
|
|
||||||
|
enum class SettingGroup(
|
||||||
|
val value: String,
|
||||||
|
) {
|
||||||
|
NETWORK("Network"),
|
||||||
|
PROXY("Proxy"),
|
||||||
|
WEB_UI("WebUI"),
|
||||||
|
DOWNLOADER("Downloader"),
|
||||||
|
EXTENSION("Extension/Source"),
|
||||||
|
LIBRARY_UPDATES("Library updates"),
|
||||||
|
AUTH("Authentication"),
|
||||||
|
MISC("Misc"),
|
||||||
|
BACKUP("Backup"),
|
||||||
|
LOCAL_SOURCE("Local source"),
|
||||||
|
CLOUDFLARE("Cloudflare"),
|
||||||
|
OPDS("OPDS"),
|
||||||
|
KOREADER_SYNC("KOReader sync"),
|
||||||
|
;
|
||||||
|
|
||||||
|
override fun toString(): String = value
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings
|
||||||
|
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Registry to track all settings for automatic updating and validation
|
||||||
|
*/
|
||||||
|
object SettingsRegistry {
|
||||||
|
data class SettingDeprecated(
|
||||||
|
val replaceWith: String? = null,
|
||||||
|
val message: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface ITypeInfo {
|
||||||
|
val type: KClass<*>?
|
||||||
|
val specificType: String?
|
||||||
|
val interfaceType: String?
|
||||||
|
val backupType: String?
|
||||||
|
val imports: List<String>?
|
||||||
|
val convertToGqlType: ((configValue: Any) -> Any)?
|
||||||
|
val convertToInternalType: ((gqlValue: Any) -> Any)?
|
||||||
|
val convertToBackupType: ((gqlValue: Any) -> Any)?
|
||||||
|
val restoreLegacy: ((backupValue: Any?) -> Any?)?
|
||||||
|
}
|
||||||
|
|
||||||
|
data class TypeInfo(
|
||||||
|
override val type: KClass<*>,
|
||||||
|
override val specificType: String? = null,
|
||||||
|
override val interfaceType: String? = null,
|
||||||
|
override val backupType: String? = null,
|
||||||
|
override val imports: List<String>? = null,
|
||||||
|
override val convertToGqlType: ((configValue: Any) -> Any)? = null,
|
||||||
|
override val convertToInternalType: ((gqlValue: Any) -> Any)? = null,
|
||||||
|
override val convertToBackupType: ((gqlValue: Any) -> Any)? = null,
|
||||||
|
override val restoreLegacy: ((backupValue: Any?) -> Any?)? = null,
|
||||||
|
) : ITypeInfo
|
||||||
|
|
||||||
|
data class PartialTypeInfo(
|
||||||
|
override val type: KClass<*>? = null,
|
||||||
|
override val specificType: String? = null,
|
||||||
|
override val interfaceType: String? = null,
|
||||||
|
override val backupType: String? = null,
|
||||||
|
override val imports: List<String>? = null,
|
||||||
|
override val convertToGqlType: ((configValue: Any) -> Any)? = null,
|
||||||
|
override val convertToInternalType: ((gqlValue: Any) -> Any)? = null,
|
||||||
|
override val convertToBackupType: ((gqlValue: Any) -> Any)? = null,
|
||||||
|
override val restoreLegacy: ((backupValue: Any?) -> Any?)? = null,
|
||||||
|
) : ITypeInfo
|
||||||
|
|
||||||
|
data class SettingMetadata(
|
||||||
|
val protoNumber: Int,
|
||||||
|
val name: String,
|
||||||
|
val typeInfo: TypeInfo,
|
||||||
|
val defaultValue: Any,
|
||||||
|
val validator: ((Any?) -> String?)? = null,
|
||||||
|
val convertGqlToInternalType: ((Any?) -> Any?)? = null,
|
||||||
|
val group: String,
|
||||||
|
val deprecated: SettingDeprecated? = null,
|
||||||
|
val requiresRestart: Boolean,
|
||||||
|
val description: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val settings = mutableMapOf<String, SettingMetadata>()
|
||||||
|
|
||||||
|
fun register(metadata: SettingMetadata) {
|
||||||
|
settings[metadata.name] = metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
fun get(name: String): SettingMetadata? = settings[name]
|
||||||
|
|
||||||
|
fun getAll(): Map<String, SettingMetadata> = settings.toMap()
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package suwayomi.tachidesk.server.util
|
||||||
|
|
||||||
|
import io.github.config4k.registerCustomType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central place for registering custom types for config serialization/deserialization
|
||||||
|
* This ensures consistency between runtime config handling and config file generation
|
||||||
|
*/
|
||||||
|
object ConfigTypeRegistration {
|
||||||
|
private var registered = false
|
||||||
|
|
||||||
|
fun registerCustomTypes() {
|
||||||
|
if (registered) return
|
||||||
|
|
||||||
|
registerCustomType(MutableStateFlowType())
|
||||||
|
registerCustomType(DurationType())
|
||||||
|
|
||||||
|
registered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,54 +2,17 @@ package suwayomi.tachidesk.graphql.mutations
|
|||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
|
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
|
||||||
import graphql.schema.DataFetchingEnvironment
|
import graphql.schema.DataFetchingEnvironment
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import suwayomi.tachidesk.graphql.server.getAttribute
|
import suwayomi.tachidesk.graphql.server.getAttribute
|
||||||
import suwayomi.tachidesk.graphql.types.PartialSettingsType
|
import suwayomi.tachidesk.graphql.types.PartialSettingsType
|
||||||
import suwayomi.tachidesk.graphql.types.Settings
|
import suwayomi.tachidesk.graphql.types.Settings
|
||||||
import suwayomi.tachidesk.graphql.types.SettingsType
|
import suwayomi.tachidesk.graphql.types.SettingsType
|
||||||
import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.repoMatchRegex
|
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
import suwayomi.tachidesk.server.JavalinSetup.Attribute
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.getAttribute
|
|
||||||
import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME
|
import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME
|
||||||
import suwayomi.tachidesk.server.ServerConfig
|
import suwayomi.tachidesk.server.ServerConfig
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.settings.SettingsUpdater
|
||||||
|
import suwayomi.tachidesk.server.settings.SettingsValidator
|
||||||
import suwayomi.tachidesk.server.user.requireUser
|
import suwayomi.tachidesk.server.user.requireUser
|
||||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
import xyz.nulldev.ts.config.GlobalConfigManager
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
private fun validateValue(
|
|
||||||
exception: Exception,
|
|
||||||
validate: () -> Boolean,
|
|
||||||
) {
|
|
||||||
if (!validate()) {
|
|
||||||
throw exception
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> validateValue(
|
|
||||||
value: T?,
|
|
||||||
exception: Exception,
|
|
||||||
validate: (value: T) -> Boolean,
|
|
||||||
) {
|
|
||||||
if (value != null) {
|
|
||||||
validateValue(exception) { validate(value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> validateValue(
|
|
||||||
value: T?,
|
|
||||||
name: String,
|
|
||||||
validate: (value: T) -> Boolean,
|
|
||||||
) {
|
|
||||||
validateValue(value, Exception("Invalid value for \"$name\" [$value]"), validate)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun validateFilePath(
|
|
||||||
value: String?,
|
|
||||||
name: String,
|
|
||||||
) {
|
|
||||||
validateValue(value, name) { File(it).exists() }
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsMutation {
|
class SettingsMutation {
|
||||||
data class SetSettingsInput(
|
data class SetSettingsInput(
|
||||||
@@ -62,176 +25,14 @@ class SettingsMutation {
|
|||||||
val settings: SettingsType,
|
val settings: SettingsType,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun validateSettings(settings: Settings) {
|
|
||||||
validateValue(settings.ip, "ip") { it.matches("^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$".toRegex()) }
|
|
||||||
|
|
||||||
// proxy
|
|
||||||
validateValue(settings.socksProxyVersion, "socksProxyVersion") { it == 4 || it == 5 }
|
|
||||||
|
|
||||||
// webUI
|
|
||||||
validateFilePath(settings.electronPath, "electronPath")
|
|
||||||
validateValue(settings.webUIUpdateCheckInterval, "webUIUpdateCheckInterval") { it == 0.0 || it in 1.0..23.0 }
|
|
||||||
|
|
||||||
// downloader
|
|
||||||
validateFilePath(settings.downloadsPath, "downloadsPath")
|
|
||||||
validateValue(settings.autoDownloadNewChaptersLimit, "autoDownloadNewChaptersLimit") { it >= 0 }
|
|
||||||
|
|
||||||
// extensions
|
|
||||||
validateValue(settings.extensionRepos, "extensionRepos") { it.all { repoUrl -> repoUrl.matches(repoMatchRegex) } }
|
|
||||||
|
|
||||||
// requests
|
|
||||||
validateValue(settings.maxSourcesInParallel, "maxSourcesInParallel") { it in 1..20 }
|
|
||||||
|
|
||||||
// updater
|
|
||||||
validateValue(settings.globalUpdateInterval, "globalUpdateInterval") { it == 0.0 || it >= 6 }
|
|
||||||
|
|
||||||
// misc
|
|
||||||
validateValue(settings.maxLogFiles, "maxLogFiles") { it >= 0 }
|
|
||||||
|
|
||||||
val logbackSizePattern = "^[0-9]+(|kb|KB|mb|MB|gb|GB)$".toRegex()
|
|
||||||
validateValue(settings.maxLogFileSize, "maxLogFolderSize") { it.matches(logbackSizePattern) }
|
|
||||||
validateValue(settings.maxLogFolderSize, "maxLogFolderSize") { it.matches(logbackSizePattern) }
|
|
||||||
|
|
||||||
// backup
|
|
||||||
validateFilePath(settings.backupPath, "backupPath")
|
|
||||||
validateValue(settings.backupTime, "backupTime") { it.matches("^([0-1]?[0-9]|2[0-3]):[0-5][0-9]$".toRegex()) }
|
|
||||||
validateValue(settings.backupInterval, "backupInterval") { it == 0 || it >= 1 }
|
|
||||||
validateValue(settings.backupTTL, "backupTTL") { it == 0 || it >= 1 }
|
|
||||||
|
|
||||||
// local source
|
|
||||||
validateFilePath(settings.localSourcePath, "localSourcePath")
|
|
||||||
|
|
||||||
// opds
|
|
||||||
validateValue(settings.opdsItemsPerPage, "opdsItemsPerPage") { it in 10..5000 }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <SettingType : Any> updateSetting(
|
|
||||||
newSetting: SettingType?,
|
|
||||||
configSetting: MutableStateFlow<SettingType>,
|
|
||||||
) {
|
|
||||||
if (newSetting == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
configSetting.value = newSetting
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <SettingType : Any, RealSettingType : Any> updateSetting(
|
|
||||||
newSetting: RealSettingType?,
|
|
||||||
configSetting: MutableStateFlow<SettingType>,
|
|
||||||
mapper: (RealSettingType) -> SettingType,
|
|
||||||
) {
|
|
||||||
if (newSetting == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
configSetting.value = mapper(newSetting)
|
|
||||||
}
|
|
||||||
|
|
||||||
@GraphQLIgnore
|
@GraphQLIgnore
|
||||||
fun updateSettings(settings: Settings) {
|
fun updateSettings(settings: Settings) {
|
||||||
updateSetting(settings.ip, serverConfig.ip)
|
val validationErrors = SettingsValidator.validate(settings, true)
|
||||||
updateSetting(settings.port, serverConfig.port)
|
if (validationErrors.isNotEmpty()) {
|
||||||
|
throw Exception("Validation errors: ${validationErrors.joinToString("; ")}")
|
||||||
// proxy
|
|
||||||
updateSetting(settings.socksProxyEnabled, serverConfig.socksProxyEnabled)
|
|
||||||
updateSetting(settings.socksProxyVersion, serverConfig.socksProxyVersion)
|
|
||||||
updateSetting(settings.socksProxyHost, serverConfig.socksProxyHost)
|
|
||||||
updateSetting(settings.socksProxyPort, serverConfig.socksProxyPort)
|
|
||||||
updateSetting(settings.socksProxyUsername, serverConfig.socksProxyUsername)
|
|
||||||
updateSetting(settings.socksProxyPassword, serverConfig.socksProxyPassword)
|
|
||||||
|
|
||||||
// webUI
|
|
||||||
updateSetting(settings.webUIFlavor, serverConfig.webUIFlavor)
|
|
||||||
updateSetting(settings.initialOpenInBrowserEnabled, serverConfig.initialOpenInBrowserEnabled)
|
|
||||||
updateSetting(settings.webUIInterface, serverConfig.webUIInterface)
|
|
||||||
updateSetting(settings.electronPath, serverConfig.electronPath)
|
|
||||||
updateSetting(settings.webUIChannel, serverConfig.webUIChannel)
|
|
||||||
updateSetting(settings.webUIUpdateCheckInterval, serverConfig.webUIUpdateCheckInterval)
|
|
||||||
|
|
||||||
// downloader
|
|
||||||
updateSetting(settings.downloadAsCbz, serverConfig.downloadAsCbz)
|
|
||||||
updateSetting(settings.downloadsPath, serverConfig.downloadsPath)
|
|
||||||
updateSetting(settings.autoDownloadNewChapters, serverConfig.autoDownloadNewChapters)
|
|
||||||
updateSetting(settings.excludeEntryWithUnreadChapters, serverConfig.excludeEntryWithUnreadChapters)
|
|
||||||
updateSetting(settings.autoDownloadAheadLimit, serverConfig.autoDownloadNewChaptersLimit) // deprecated
|
|
||||||
updateSetting(settings.autoDownloadNewChaptersLimit, serverConfig.autoDownloadNewChaptersLimit)
|
|
||||||
updateSetting(settings.autoDownloadIgnoreReUploads, serverConfig.autoDownloadIgnoreReUploads)
|
|
||||||
updateSetting(settings.downloadConversions, serverConfig.downloadConversions) { list ->
|
|
||||||
list.associate {
|
|
||||||
it.mimeType to
|
|
||||||
ServerConfig.DownloadConversion(
|
|
||||||
target = it.target,
|
|
||||||
compressionLevel = it.compressionLevel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// extension
|
SettingsUpdater.updateAll(settings)
|
||||||
updateSetting(settings.extensionRepos, serverConfig.extensionRepos)
|
|
||||||
|
|
||||||
// requests
|
|
||||||
updateSetting(settings.maxSourcesInParallel, serverConfig.maxSourcesInParallel)
|
|
||||||
|
|
||||||
// updater
|
|
||||||
updateSetting(settings.excludeUnreadChapters, serverConfig.excludeUnreadChapters)
|
|
||||||
updateSetting(settings.excludeNotStarted, serverConfig.excludeNotStarted)
|
|
||||||
updateSetting(settings.excludeCompleted, serverConfig.excludeCompleted)
|
|
||||||
updateSetting(settings.globalUpdateInterval, serverConfig.globalUpdateInterval)
|
|
||||||
updateSetting(settings.updateMangas, serverConfig.updateMangas)
|
|
||||||
|
|
||||||
// Authentication
|
|
||||||
updateSetting(settings.authMode, serverConfig.authMode)
|
|
||||||
updateSetting(settings.jwtAudience, serverConfig.jwtAudience)
|
|
||||||
updateSetting(settings.jwtTokenExpiry, serverConfig.jwtTokenExpiry)
|
|
||||||
updateSetting(settings.jwtRefreshExpiry, serverConfig.jwtRefreshExpiry)
|
|
||||||
updateSetting(settings.authUsername, serverConfig.authUsername)
|
|
||||||
updateSetting(settings.authPassword, serverConfig.authPassword)
|
|
||||||
updateSetting(settings.basicAuthEnabled, serverConfig.basicAuthEnabled)
|
|
||||||
updateSetting(settings.basicAuthUsername, serverConfig.basicAuthUsername)
|
|
||||||
updateSetting(settings.basicAuthPassword, serverConfig.basicAuthPassword)
|
|
||||||
|
|
||||||
// misc
|
|
||||||
updateSetting(settings.debugLogsEnabled, serverConfig.debugLogsEnabled)
|
|
||||||
updateSetting(settings.systemTrayEnabled, serverConfig.systemTrayEnabled)
|
|
||||||
updateSetting(settings.maxLogFiles, serverConfig.maxLogFiles)
|
|
||||||
updateSetting(settings.maxLogFileSize, serverConfig.maxLogFileSize)
|
|
||||||
updateSetting(settings.maxLogFolderSize, serverConfig.maxLogFolderSize)
|
|
||||||
|
|
||||||
// backup
|
|
||||||
updateSetting(settings.backupPath, serverConfig.backupPath)
|
|
||||||
updateSetting(settings.backupTime, serverConfig.backupTime)
|
|
||||||
updateSetting(settings.backupInterval, serverConfig.backupInterval)
|
|
||||||
updateSetting(settings.backupTTL, serverConfig.backupTTL)
|
|
||||||
|
|
||||||
// local source
|
|
||||||
updateSetting(settings.localSourcePath, serverConfig.localSourcePath)
|
|
||||||
|
|
||||||
// cloudflare bypass
|
|
||||||
updateSetting(settings.flareSolverrEnabled, serverConfig.flareSolverrEnabled)
|
|
||||||
updateSetting(settings.flareSolverrUrl, serverConfig.flareSolverrUrl)
|
|
||||||
updateSetting(settings.flareSolverrTimeout, serverConfig.flareSolverrTimeout)
|
|
||||||
updateSetting(settings.flareSolverrSessionName, serverConfig.flareSolverrSessionName)
|
|
||||||
updateSetting(settings.flareSolverrSessionTtl, serverConfig.flareSolverrSessionTtl)
|
|
||||||
updateSetting(settings.flareSolverrAsResponseFallback, serverConfig.flareSolverrAsResponseFallback)
|
|
||||||
|
|
||||||
// opds
|
|
||||||
updateSetting(settings.opdsUseBinaryFileSizes, serverConfig.opdsUseBinaryFileSizes)
|
|
||||||
updateSetting(settings.opdsItemsPerPage, serverConfig.opdsItemsPerPage)
|
|
||||||
updateSetting(settings.opdsEnablePageReadProgress, serverConfig.opdsEnablePageReadProgress)
|
|
||||||
updateSetting(settings.opdsMarkAsReadOnDownload, serverConfig.opdsMarkAsReadOnDownload)
|
|
||||||
updateSetting(settings.opdsShowOnlyUnreadChapters, serverConfig.opdsShowOnlyUnreadChapters)
|
|
||||||
updateSetting(settings.opdsShowOnlyDownloadedChapters, serverConfig.opdsShowOnlyDownloadedChapters)
|
|
||||||
updateSetting(settings.opdsChapterSortOrder, serverConfig.opdsChapterSortOrder)
|
|
||||||
|
|
||||||
// koreader sync
|
|
||||||
updateSetting(settings.koreaderSyncServerUrl, serverConfig.koreaderSyncServerUrl)
|
|
||||||
updateSetting(settings.koreaderSyncUsername, serverConfig.koreaderSyncUsername)
|
|
||||||
updateSetting(settings.koreaderSyncUserkey, serverConfig.koreaderSyncUserkey)
|
|
||||||
updateSetting(settings.koreaderSyncDeviceId, serverConfig.koreaderSyncDeviceId)
|
|
||||||
updateSetting(settings.koreaderSyncChecksumMethod, serverConfig.koreaderSyncChecksumMethod)
|
|
||||||
updateSetting(settings.koreaderSyncStrategy, serverConfig.koreaderSyncStrategy)
|
|
||||||
updateSetting(settings.koreaderSyncPercentageTolerance, serverConfig.koreaderSyncPercentageTolerance)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSettings(
|
fun setSettings(
|
||||||
@@ -241,7 +42,6 @@ class SettingsMutation {
|
|||||||
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
|
||||||
val (clientMutationId, settings) = input
|
val (clientMutationId, settings) = input
|
||||||
|
|
||||||
validateSettings(settings)
|
|
||||||
updateSettings(settings)
|
updateSettings(settings)
|
||||||
|
|
||||||
return SetSettingsPayload(clientMutationId, SettingsType())
|
return SetSettingsPayload(clientMutationId, SettingsType())
|
||||||
|
|||||||
@@ -1,18 +1,5 @@
|
|||||||
package suwayomi.tachidesk.graphql.types
|
package suwayomi.tachidesk.graphql.types
|
||||||
|
|
||||||
enum class KoreaderSyncChecksumMethod {
|
|
||||||
BINARY,
|
|
||||||
FILENAME,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum class KoreaderSyncStrategy {
|
|
||||||
PROMPT, // Ask on conflict
|
|
||||||
SILENT, // Always use latest
|
|
||||||
SEND, // Send changes only
|
|
||||||
RECEIVE, // Receive changes only
|
|
||||||
DISABLED,
|
|
||||||
}
|
|
||||||
|
|
||||||
data class KoSyncStatusPayload(
|
data class KoSyncStatusPayload(
|
||||||
val isLoggedIn: Boolean,
|
val isLoggedIn: Boolean,
|
||||||
val username: String?,
|
val username: String?,
|
||||||
@@ -1,417 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
package suwayomi.tachidesk.graphql.types
|
|
||||||
|
|
||||||
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
|
||||||
import suwayomi.tachidesk.graphql.server.primitives.Node
|
|
||||||
import suwayomi.tachidesk.server.ServerConfig
|
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
interface Settings : Node {
|
|
||||||
val ip: String?
|
|
||||||
val port: Int?
|
|
||||||
|
|
||||||
// proxy
|
|
||||||
val socksProxyEnabled: Boolean?
|
|
||||||
val socksProxyVersion: Int?
|
|
||||||
val socksProxyHost: String?
|
|
||||||
val socksProxyPort: String?
|
|
||||||
val socksProxyUsername: String?
|
|
||||||
val socksProxyPassword: String?
|
|
||||||
|
|
||||||
// webUI
|
|
||||||
// requires restart (found no way to mutate (serve + "unserve") served files during runtime), exclude for now
|
|
||||||
// val webUIEnabled: Boolean,
|
|
||||||
val webUIFlavor: WebUIFlavor?
|
|
||||||
val initialOpenInBrowserEnabled: Boolean?
|
|
||||||
val webUIInterface: WebUIInterface?
|
|
||||||
val electronPath: String?
|
|
||||||
val webUIChannel: WebUIChannel?
|
|
||||||
val webUIUpdateCheckInterval: Double?
|
|
||||||
|
|
||||||
// downloader
|
|
||||||
val downloadAsCbz: Boolean?
|
|
||||||
val downloadsPath: String?
|
|
||||||
val autoDownloadNewChapters: Boolean?
|
|
||||||
val excludeEntryWithUnreadChapters: Boolean?
|
|
||||||
|
|
||||||
@GraphQLDeprecated(
|
|
||||||
"Replaced with autoDownloadNewChaptersLimit",
|
|
||||||
replaceWith = ReplaceWith("autoDownloadNewChaptersLimit"),
|
|
||||||
)
|
|
||||||
val autoDownloadAheadLimit: Int?
|
|
||||||
val autoDownloadNewChaptersLimit: Int?
|
|
||||||
val autoDownloadIgnoreReUploads: Boolean?
|
|
||||||
val downloadConversions: List<SettingsDownloadConversion>?
|
|
||||||
|
|
||||||
// extension
|
|
||||||
val extensionRepos: List<String>?
|
|
||||||
|
|
||||||
// requests
|
|
||||||
val maxSourcesInParallel: Int?
|
|
||||||
|
|
||||||
// updater
|
|
||||||
val excludeUnreadChapters: Boolean?
|
|
||||||
val excludeNotStarted: Boolean?
|
|
||||||
val excludeCompleted: Boolean?
|
|
||||||
val globalUpdateInterval: Double?
|
|
||||||
val updateMangas: Boolean?
|
|
||||||
|
|
||||||
// Authentication
|
|
||||||
val authMode: AuthMode?
|
|
||||||
val jwtAudience: String?
|
|
||||||
val jwtTokenExpiry: Duration?
|
|
||||||
val jwtRefreshExpiry: Duration?
|
|
||||||
val authUsername: String?
|
|
||||||
val authPassword: String?
|
|
||||||
|
|
||||||
@GraphQLDeprecated("Removed - prefer authMode")
|
|
||||||
val basicAuthEnabled: Boolean?
|
|
||||||
|
|
||||||
@GraphQLDeprecated("Removed - prefer authUsername")
|
|
||||||
val basicAuthUsername: String?
|
|
||||||
|
|
||||||
@GraphQLDeprecated("Removed - prefer authPassword")
|
|
||||||
val basicAuthPassword: String?
|
|
||||||
|
|
||||||
// misc
|
|
||||||
val debugLogsEnabled: Boolean?
|
|
||||||
|
|
||||||
@GraphQLDeprecated("Removed - does not do anything")
|
|
||||||
val gqlDebugLogsEnabled: Boolean?
|
|
||||||
val systemTrayEnabled: Boolean?
|
|
||||||
val maxLogFiles: Int?
|
|
||||||
val maxLogFileSize: String?
|
|
||||||
val maxLogFolderSize: String?
|
|
||||||
|
|
||||||
// backup
|
|
||||||
val backupPath: String?
|
|
||||||
val backupTime: String?
|
|
||||||
val backupInterval: Int?
|
|
||||||
val backupTTL: Int?
|
|
||||||
|
|
||||||
// local source
|
|
||||||
val localSourcePath: String?
|
|
||||||
|
|
||||||
// cloudflare bypass
|
|
||||||
val flareSolverrEnabled: Boolean?
|
|
||||||
val flareSolverrUrl: String?
|
|
||||||
val flareSolverrTimeout: Int?
|
|
||||||
val flareSolverrSessionName: String?
|
|
||||||
val flareSolverrSessionTtl: Int?
|
|
||||||
val flareSolverrAsResponseFallback: Boolean?
|
|
||||||
|
|
||||||
// opds
|
|
||||||
val opdsUseBinaryFileSizes: Boolean?
|
|
||||||
val opdsItemsPerPage: Int?
|
|
||||||
val opdsEnablePageReadProgress: Boolean?
|
|
||||||
val opdsMarkAsReadOnDownload: Boolean?
|
|
||||||
val opdsShowOnlyUnreadChapters: Boolean?
|
|
||||||
val opdsShowOnlyDownloadedChapters: Boolean?
|
|
||||||
val opdsChapterSortOrder: SortOrder?
|
|
||||||
|
|
||||||
// koreader sync
|
|
||||||
val koreaderSyncServerUrl: String?
|
|
||||||
val koreaderSyncUsername: String?
|
|
||||||
val koreaderSyncUserkey: String?
|
|
||||||
val koreaderSyncDeviceId: String?
|
|
||||||
val koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod?
|
|
||||||
val koreaderSyncStrategy: KoreaderSyncStrategy?
|
|
||||||
val koreaderSyncPercentageTolerance: Double?
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SettingsDownloadConversion {
|
|
||||||
val mimeType: String
|
|
||||||
val target: String
|
|
||||||
val compressionLevel: Double?
|
|
||||||
}
|
|
||||||
|
|
||||||
class SettingsDownloadConversionType(
|
|
||||||
override val mimeType: String,
|
|
||||||
override val target: String,
|
|
||||||
override val compressionLevel: Double?,
|
|
||||||
) : SettingsDownloadConversion
|
|
||||||
|
|
||||||
data class PartialSettingsType(
|
|
||||||
override val ip: String?,
|
|
||||||
override val port: Int?,
|
|
||||||
// proxy
|
|
||||||
override val socksProxyEnabled: Boolean?,
|
|
||||||
override val socksProxyVersion: Int?,
|
|
||||||
override val socksProxyHost: String?,
|
|
||||||
override val socksProxyPort: String?,
|
|
||||||
override val socksProxyUsername: String?,
|
|
||||||
override val socksProxyPassword: String?,
|
|
||||||
// webUI
|
|
||||||
override val webUIFlavor: WebUIFlavor?,
|
|
||||||
override val initialOpenInBrowserEnabled: Boolean?,
|
|
||||||
override val webUIInterface: WebUIInterface?,
|
|
||||||
override val electronPath: String?,
|
|
||||||
override val webUIChannel: WebUIChannel?,
|
|
||||||
override val webUIUpdateCheckInterval: Double?,
|
|
||||||
// downloader
|
|
||||||
override val downloadAsCbz: Boolean?,
|
|
||||||
override val downloadsPath: String?,
|
|
||||||
override val autoDownloadNewChapters: Boolean?,
|
|
||||||
override val excludeEntryWithUnreadChapters: Boolean?,
|
|
||||||
@GraphQLDeprecated(
|
|
||||||
"Replaced with autoDownloadNewChaptersLimit",
|
|
||||||
replaceWith = ReplaceWith("autoDownloadNewChaptersLimit"),
|
|
||||||
)
|
|
||||||
override val autoDownloadAheadLimit: Int?,
|
|
||||||
override val autoDownloadNewChaptersLimit: Int?,
|
|
||||||
override val autoDownloadIgnoreReUploads: Boolean?,
|
|
||||||
override val downloadConversions: List<SettingsDownloadConversionType>?,
|
|
||||||
// extension
|
|
||||||
override val extensionRepos: List<String>?,
|
|
||||||
// requests
|
|
||||||
override val maxSourcesInParallel: Int?,
|
|
||||||
// updater
|
|
||||||
override val excludeUnreadChapters: Boolean?,
|
|
||||||
override val excludeNotStarted: Boolean?,
|
|
||||||
override val excludeCompleted: Boolean?,
|
|
||||||
override val globalUpdateInterval: Double?,
|
|
||||||
override val updateMangas: Boolean?,
|
|
||||||
// Authentication
|
|
||||||
override val authMode: AuthMode?,
|
|
||||||
override val jwtAudience: String?,
|
|
||||||
override val jwtTokenExpiry: Duration?,
|
|
||||||
override val jwtRefreshExpiry: Duration?,
|
|
||||||
override val authUsername: String?,
|
|
||||||
override val authPassword: String?,
|
|
||||||
@GraphQLDeprecated("Removed - prefer authMode")
|
|
||||||
override val basicAuthEnabled: Boolean?,
|
|
||||||
@GraphQLDeprecated("Removed - prefer authUsername")
|
|
||||||
override val basicAuthUsername: String?,
|
|
||||||
@GraphQLDeprecated("Removed - prefer authPassword")
|
|
||||||
override val basicAuthPassword: String?,
|
|
||||||
// misc
|
|
||||||
override val debugLogsEnabled: Boolean?,
|
|
||||||
@GraphQLDeprecated("Removed - does not do anything")
|
|
||||||
override val gqlDebugLogsEnabled: Boolean?,
|
|
||||||
override val systemTrayEnabled: Boolean?,
|
|
||||||
override val maxLogFiles: Int?,
|
|
||||||
override val maxLogFileSize: String?,
|
|
||||||
override val maxLogFolderSize: String?,
|
|
||||||
// backup
|
|
||||||
override val backupPath: String?,
|
|
||||||
override val backupTime: String?,
|
|
||||||
override val backupInterval: Int?,
|
|
||||||
override val backupTTL: Int?,
|
|
||||||
// local source
|
|
||||||
override val localSourcePath: String?,
|
|
||||||
// cloudflare bypass
|
|
||||||
override val flareSolverrEnabled: Boolean?,
|
|
||||||
override val flareSolverrUrl: String?,
|
|
||||||
override val flareSolverrTimeout: Int?,
|
|
||||||
override val flareSolverrSessionName: String?,
|
|
||||||
override val flareSolverrSessionTtl: Int?,
|
|
||||||
override val flareSolverrAsResponseFallback: Boolean?,
|
|
||||||
// opds
|
|
||||||
override val opdsUseBinaryFileSizes: Boolean?,
|
|
||||||
override val opdsItemsPerPage: Int?,
|
|
||||||
override val opdsEnablePageReadProgress: Boolean?,
|
|
||||||
override val opdsMarkAsReadOnDownload: Boolean?,
|
|
||||||
override val opdsShowOnlyUnreadChapters: Boolean?,
|
|
||||||
override val opdsShowOnlyDownloadedChapters: Boolean?,
|
|
||||||
override val opdsChapterSortOrder: SortOrder?,
|
|
||||||
// koreader sync
|
|
||||||
override val koreaderSyncServerUrl: String?,
|
|
||||||
override val koreaderSyncUsername: String?,
|
|
||||||
override val koreaderSyncUserkey: String?,
|
|
||||||
override val koreaderSyncDeviceId: String?,
|
|
||||||
override val koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod?,
|
|
||||||
override val koreaderSyncStrategy: KoreaderSyncStrategy?,
|
|
||||||
override val koreaderSyncPercentageTolerance: Double?,
|
|
||||||
) : Settings
|
|
||||||
|
|
||||||
class SettingsType(
|
|
||||||
override val ip: String,
|
|
||||||
override val port: Int,
|
|
||||||
// proxy
|
|
||||||
override val socksProxyEnabled: Boolean,
|
|
||||||
override val socksProxyVersion: Int,
|
|
||||||
override val socksProxyHost: String,
|
|
||||||
override val socksProxyPort: String,
|
|
||||||
override val socksProxyUsername: String,
|
|
||||||
override val socksProxyPassword: String,
|
|
||||||
// webUI
|
|
||||||
override val webUIFlavor: WebUIFlavor,
|
|
||||||
override val initialOpenInBrowserEnabled: Boolean,
|
|
||||||
override val webUIInterface: WebUIInterface,
|
|
||||||
override val electronPath: String,
|
|
||||||
override val webUIChannel: WebUIChannel,
|
|
||||||
override val webUIUpdateCheckInterval: Double,
|
|
||||||
// downloader
|
|
||||||
override val downloadAsCbz: Boolean,
|
|
||||||
override val downloadsPath: String,
|
|
||||||
override val autoDownloadNewChapters: Boolean,
|
|
||||||
override val excludeEntryWithUnreadChapters: Boolean,
|
|
||||||
@GraphQLDeprecated(
|
|
||||||
"Replaced with autoDownloadNewChaptersLimit",
|
|
||||||
replaceWith = ReplaceWith("autoDownloadNewChaptersLimit"),
|
|
||||||
)
|
|
||||||
override val autoDownloadAheadLimit: Int,
|
|
||||||
override val autoDownloadNewChaptersLimit: Int,
|
|
||||||
override val autoDownloadIgnoreReUploads: Boolean,
|
|
||||||
override val downloadConversions: List<SettingsDownloadConversionType>,
|
|
||||||
// extension
|
|
||||||
override val extensionRepos: List<String>,
|
|
||||||
// requests
|
|
||||||
override val maxSourcesInParallel: Int,
|
|
||||||
// updater
|
|
||||||
override val excludeUnreadChapters: Boolean,
|
|
||||||
override val excludeNotStarted: Boolean,
|
|
||||||
override val excludeCompleted: Boolean,
|
|
||||||
override val globalUpdateInterval: Double,
|
|
||||||
override val updateMangas: Boolean,
|
|
||||||
// Authentication
|
|
||||||
override val authMode: AuthMode,
|
|
||||||
override val jwtAudience: String,
|
|
||||||
override val jwtTokenExpiry: Duration,
|
|
||||||
override val jwtRefreshExpiry: Duration,
|
|
||||||
override val authUsername: String,
|
|
||||||
override val authPassword: String,
|
|
||||||
@GraphQLDeprecated("Removed - prefer authMode")
|
|
||||||
override val basicAuthEnabled: Boolean,
|
|
||||||
@GraphQLDeprecated("Removed - prefer authUsername")
|
|
||||||
override val basicAuthUsername: String,
|
|
||||||
@GraphQLDeprecated("Removed - prefer authPassword")
|
|
||||||
override val basicAuthPassword: String,
|
|
||||||
// misc
|
|
||||||
override val debugLogsEnabled: Boolean,
|
|
||||||
@GraphQLDeprecated("Removed - does not do anything")
|
|
||||||
override val gqlDebugLogsEnabled: Boolean,
|
|
||||||
override val systemTrayEnabled: Boolean,
|
|
||||||
override val maxLogFiles: Int,
|
|
||||||
override val maxLogFileSize: String,
|
|
||||||
override val maxLogFolderSize: String,
|
|
||||||
// backup
|
|
||||||
override val backupPath: String,
|
|
||||||
override val backupTime: String,
|
|
||||||
override val backupInterval: Int,
|
|
||||||
override val backupTTL: Int,
|
|
||||||
// local source
|
|
||||||
override val localSourcePath: String,
|
|
||||||
// cloudflare bypass
|
|
||||||
override val flareSolverrEnabled: Boolean,
|
|
||||||
override val flareSolverrUrl: String,
|
|
||||||
override val flareSolverrTimeout: Int,
|
|
||||||
override val flareSolverrSessionName: String,
|
|
||||||
override val flareSolverrSessionTtl: Int,
|
|
||||||
override val flareSolverrAsResponseFallback: Boolean,
|
|
||||||
// opds
|
|
||||||
override val opdsUseBinaryFileSizes: Boolean,
|
|
||||||
override val opdsItemsPerPage: Int,
|
|
||||||
override val opdsEnablePageReadProgress: Boolean,
|
|
||||||
override val opdsMarkAsReadOnDownload: Boolean,
|
|
||||||
override val opdsShowOnlyUnreadChapters: Boolean,
|
|
||||||
override val opdsShowOnlyDownloadedChapters: Boolean,
|
|
||||||
override val opdsChapterSortOrder: SortOrder,
|
|
||||||
// koreader sync
|
|
||||||
override val koreaderSyncServerUrl: String,
|
|
||||||
override val koreaderSyncUsername: String,
|
|
||||||
override val koreaderSyncUserkey: String,
|
|
||||||
override val koreaderSyncDeviceId: String,
|
|
||||||
override val koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod,
|
|
||||||
override val koreaderSyncStrategy: KoreaderSyncStrategy,
|
|
||||||
override val koreaderSyncPercentageTolerance: Double,
|
|
||||||
) : Settings {
|
|
||||||
constructor(config: ServerConfig = serverConfig) : this(
|
|
||||||
config.ip.value,
|
|
||||||
config.port.value,
|
|
||||||
// proxy
|
|
||||||
config.socksProxyEnabled.value,
|
|
||||||
config.socksProxyVersion.value,
|
|
||||||
config.socksProxyHost.value,
|
|
||||||
config.socksProxyPort.value,
|
|
||||||
config.socksProxyUsername.value,
|
|
||||||
config.socksProxyPassword.value,
|
|
||||||
// webUI
|
|
||||||
config.webUIFlavor.value,
|
|
||||||
config.initialOpenInBrowserEnabled.value,
|
|
||||||
config.webUIInterface.value,
|
|
||||||
config.electronPath.value,
|
|
||||||
config.webUIChannel.value,
|
|
||||||
config.webUIUpdateCheckInterval.value,
|
|
||||||
// downloader
|
|
||||||
config.downloadAsCbz.value,
|
|
||||||
config.downloadsPath.value,
|
|
||||||
config.autoDownloadNewChapters.value,
|
|
||||||
config.excludeEntryWithUnreadChapters.value,
|
|
||||||
config.autoDownloadNewChaptersLimit.value, // deprecated
|
|
||||||
config.autoDownloadNewChaptersLimit.value,
|
|
||||||
config.autoDownloadIgnoreReUploads.value,
|
|
||||||
config.downloadConversions.value.map {
|
|
||||||
SettingsDownloadConversionType(
|
|
||||||
it.key,
|
|
||||||
it.value.target,
|
|
||||||
it.value.compressionLevel,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
// extension
|
|
||||||
config.extensionRepos.value,
|
|
||||||
// requests
|
|
||||||
config.maxSourcesInParallel.value,
|
|
||||||
// updater
|
|
||||||
config.excludeUnreadChapters.value,
|
|
||||||
config.excludeNotStarted.value,
|
|
||||||
config.excludeCompleted.value,
|
|
||||||
config.globalUpdateInterval.value,
|
|
||||||
config.updateMangas.value,
|
|
||||||
// Authentication
|
|
||||||
config.authMode.value,
|
|
||||||
config.jwtAudience.value,
|
|
||||||
config.jwtTokenExpiry.value,
|
|
||||||
config.jwtRefreshExpiry.value,
|
|
||||||
config.authUsername.value,
|
|
||||||
config.authPassword.value,
|
|
||||||
config.basicAuthEnabled.value,
|
|
||||||
config.basicAuthUsername.value,
|
|
||||||
config.basicAuthPassword.value,
|
|
||||||
// misc
|
|
||||||
config.debugLogsEnabled.value,
|
|
||||||
false,
|
|
||||||
config.systemTrayEnabled.value,
|
|
||||||
config.maxLogFiles.value,
|
|
||||||
config.maxLogFileSize.value,
|
|
||||||
config.maxLogFolderSize.value,
|
|
||||||
// backup
|
|
||||||
config.backupPath.value,
|
|
||||||
config.backupTime.value,
|
|
||||||
config.backupInterval.value,
|
|
||||||
config.backupTTL.value,
|
|
||||||
// local source
|
|
||||||
config.localSourcePath.value,
|
|
||||||
// cloudflare bypass
|
|
||||||
config.flareSolverrEnabled.value,
|
|
||||||
config.flareSolverrUrl.value,
|
|
||||||
config.flareSolverrTimeout.value,
|
|
||||||
config.flareSolverrSessionName.value,
|
|
||||||
config.flareSolverrSessionTtl.value,
|
|
||||||
config.flareSolverrAsResponseFallback.value,
|
|
||||||
// opds
|
|
||||||
config.opdsUseBinaryFileSizes.value,
|
|
||||||
config.opdsItemsPerPage.value,
|
|
||||||
config.opdsEnablePageReadProgress.value,
|
|
||||||
config.opdsMarkAsReadOnDownload.value,
|
|
||||||
config.opdsShowOnlyUnreadChapters.value,
|
|
||||||
config.opdsShowOnlyDownloadedChapters.value,
|
|
||||||
config.opdsChapterSortOrder.value,
|
|
||||||
// koreader sync
|
|
||||||
config.koreaderSyncServerUrl.value,
|
|
||||||
config.koreaderSyncUsername.value,
|
|
||||||
config.koreaderSyncUserkey.value,
|
|
||||||
config.koreaderSyncDeviceId.value,
|
|
||||||
config.koreaderSyncChecksumMethod.value,
|
|
||||||
config.koreaderSyncStrategy.value,
|
|
||||||
config.koreaderSyncPercentageTolerance.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -31,13 +31,12 @@ import suwayomi.tachidesk.manga.impl.Chapter
|
|||||||
import suwayomi.tachidesk.manga.impl.Manga
|
import suwayomi.tachidesk.manga.impl.Manga
|
||||||
import suwayomi.tachidesk.manga.impl.Source
|
import suwayomi.tachidesk.manga.impl.Source
|
||||||
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
import suwayomi.tachidesk.manga.impl.backup.BackupFlags
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.handlers.BackupSettingsHandler
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupChapter
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupChapter
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings.BackupSettingsDownloadConversionType
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
|
||||||
import suwayomi.tachidesk.manga.impl.track.Track
|
import suwayomi.tachidesk.manga.impl.track.Track
|
||||||
@@ -97,15 +96,11 @@ object ProtoBackupExport : ProtoBackupBase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val (hour, minute) =
|
val (backupHour, backupMinute) =
|
||||||
serverConfig.backupTime.value
|
serverConfig.backupTime.value
|
||||||
.split(":")
|
.split(":")
|
||||||
.map { it.toInt() }
|
.map { it.toInt() }
|
||||||
val backupHour = hour.coerceAtLeast(0).coerceAtMost(23)
|
val backupInterval = serverConfig.backupInterval.value.days
|
||||||
val backupMinute = minute.coerceAtLeast(0).coerceAtMost(59)
|
|
||||||
val backupInterval =
|
|
||||||
serverConfig.backupInterval.value.days
|
|
||||||
.coerceAtLeast(1.days)
|
|
||||||
|
|
||||||
// trigger last backup in case the server wasn't running on the scheduled time
|
// trigger last backup in case the server wasn't running on the scheduled time
|
||||||
val lastAutomatedBackup = preferences.getLong(LAST_AUTOMATED_BACKUP_KEY, 0)
|
val lastAutomatedBackup = preferences.getLong(LAST_AUTOMATED_BACKUP_KEY, 0)
|
||||||
@@ -193,7 +188,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
|||||||
backupCategories(flags),
|
backupCategories(flags),
|
||||||
backupExtensionInfo(databaseManga, flags),
|
backupExtensionInfo(databaseManga, flags),
|
||||||
backupGlobalMeta(flags),
|
backupGlobalMeta(flags),
|
||||||
backupServerSettings(flags),
|
BackupSettingsHandler.backup(flags),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,102 +373,4 @@ object ProtoBackupExport : ProtoBackupBase() {
|
|||||||
|
|
||||||
return GlobalMeta.getMetaMap()
|
return GlobalMeta.getMetaMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun backupServerSettings(flags: BackupFlags): BackupServerSettings? {
|
|
||||||
if (!flags.includeServerSettings) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return BackupServerSettings(
|
|
||||||
ip = serverConfig.ip.value,
|
|
||||||
port = serverConfig.port.value,
|
|
||||||
// socks
|
|
||||||
socksProxyEnabled = serverConfig.socksProxyEnabled.value,
|
|
||||||
socksProxyVersion = serverConfig.socksProxyVersion.value,
|
|
||||||
socksProxyHost = serverConfig.socksProxyHost.value,
|
|
||||||
socksProxyPort = serverConfig.socksProxyPort.value,
|
|
||||||
socksProxyUsername = serverConfig.socksProxyUsername.value,
|
|
||||||
socksProxyPassword = serverConfig.socksProxyPassword.value,
|
|
||||||
// webUI
|
|
||||||
webUIFlavor = serverConfig.webUIFlavor.value,
|
|
||||||
initialOpenInBrowserEnabled = serverConfig.initialOpenInBrowserEnabled.value,
|
|
||||||
webUIInterface = serverConfig.webUIInterface.value,
|
|
||||||
electronPath = serverConfig.electronPath.value,
|
|
||||||
webUIChannel = serverConfig.webUIChannel.value,
|
|
||||||
webUIUpdateCheckInterval = serverConfig.webUIUpdateCheckInterval.value,
|
|
||||||
// downloader
|
|
||||||
downloadAsCbz = serverConfig.downloadAsCbz.value,
|
|
||||||
downloadsPath = serverConfig.downloadsPath.value,
|
|
||||||
autoDownloadNewChapters = serverConfig.autoDownloadNewChapters.value,
|
|
||||||
excludeEntryWithUnreadChapters = serverConfig.excludeEntryWithUnreadChapters.value,
|
|
||||||
autoDownloadAheadLimit = 0, // deprecated
|
|
||||||
autoDownloadNewChaptersLimit = serverConfig.autoDownloadNewChaptersLimit.value,
|
|
||||||
autoDownloadIgnoreReUploads = serverConfig.autoDownloadIgnoreReUploads.value,
|
|
||||||
downloadConversions =
|
|
||||||
serverConfig.downloadConversions.value.map {
|
|
||||||
BackupSettingsDownloadConversionType(
|
|
||||||
it.key,
|
|
||||||
it.value.target,
|
|
||||||
it.value.compressionLevel,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
// extension
|
|
||||||
extensionRepos = serverConfig.extensionRepos.value,
|
|
||||||
// requests
|
|
||||||
maxSourcesInParallel = serverConfig.maxSourcesInParallel.value,
|
|
||||||
// updater
|
|
||||||
excludeUnreadChapters = serverConfig.excludeUnreadChapters.value,
|
|
||||||
excludeNotStarted = serverConfig.excludeNotStarted.value,
|
|
||||||
excludeCompleted = serverConfig.excludeCompleted.value,
|
|
||||||
globalUpdateInterval = serverConfig.globalUpdateInterval.value,
|
|
||||||
updateMangas = serverConfig.updateMangas.value,
|
|
||||||
// Authentication
|
|
||||||
authMode = serverConfig.authMode.value,
|
|
||||||
jwtAudience = serverConfig.jwtAudience.value,
|
|
||||||
jwtTokenExpiry = serverConfig.jwtTokenExpiry.value,
|
|
||||||
jwtRefreshExpiry = serverConfig.jwtRefreshExpiry.value,
|
|
||||||
authUsername = serverConfig.authUsername.value,
|
|
||||||
authPassword = serverConfig.authPassword.value,
|
|
||||||
basicAuthEnabled = false,
|
|
||||||
basicAuthUsername = null,
|
|
||||||
basicAuthPassword = null,
|
|
||||||
// misc
|
|
||||||
debugLogsEnabled = serverConfig.debugLogsEnabled.value,
|
|
||||||
gqlDebugLogsEnabled = false, // deprecated
|
|
||||||
systemTrayEnabled = serverConfig.systemTrayEnabled.value,
|
|
||||||
maxLogFiles = serverConfig.maxLogFiles.value,
|
|
||||||
maxLogFileSize = serverConfig.maxLogFileSize.value,
|
|
||||||
maxLogFolderSize = serverConfig.maxLogFolderSize.value,
|
|
||||||
// backup
|
|
||||||
backupPath = serverConfig.backupPath.value,
|
|
||||||
backupTime = serverConfig.backupTime.value,
|
|
||||||
backupInterval = serverConfig.backupInterval.value,
|
|
||||||
backupTTL = serverConfig.backupTTL.value,
|
|
||||||
// local source
|
|
||||||
localSourcePath = serverConfig.localSourcePath.value,
|
|
||||||
// cloudflare bypass
|
|
||||||
flareSolverrEnabled = serverConfig.flareSolverrEnabled.value,
|
|
||||||
flareSolverrUrl = serverConfig.flareSolverrUrl.value,
|
|
||||||
flareSolverrTimeout = serverConfig.flareSolverrTimeout.value,
|
|
||||||
flareSolverrSessionName = serverConfig.flareSolverrSessionName.value,
|
|
||||||
flareSolverrSessionTtl = serverConfig.flareSolverrSessionTtl.value,
|
|
||||||
flareSolverrAsResponseFallback = serverConfig.flareSolverrAsResponseFallback.value,
|
|
||||||
// opds
|
|
||||||
opdsUseBinaryFileSizes = serverConfig.opdsUseBinaryFileSizes.value,
|
|
||||||
opdsItemsPerPage = serverConfig.opdsItemsPerPage.value,
|
|
||||||
opdsEnablePageReadProgress = serverConfig.opdsEnablePageReadProgress.value,
|
|
||||||
opdsMarkAsReadOnDownload = serverConfig.opdsMarkAsReadOnDownload.value,
|
|
||||||
opdsShowOnlyUnreadChapters = serverConfig.opdsShowOnlyUnreadChapters.value,
|
|
||||||
opdsShowOnlyDownloadedChapters = serverConfig.opdsShowOnlyDownloadedChapters.value,
|
|
||||||
opdsChapterSortOrder = serverConfig.opdsChapterSortOrder.value,
|
|
||||||
// koreader sync
|
|
||||||
koreaderSyncServerUrl = serverConfig.koreaderSyncServerUrl.value,
|
|
||||||
koreaderSyncUsername = serverConfig.koreaderSyncUsername.value,
|
|
||||||
koreaderSyncUserkey = serverConfig.koreaderSyncUserkey.value,
|
|
||||||
koreaderSyncDeviceId = serverConfig.koreaderSyncDeviceId.value,
|
|
||||||
koreaderSyncChecksumMethod = serverConfig.koreaderSyncChecksumMethod.value,
|
|
||||||
koreaderSyncStrategy = serverConfig.koreaderSyncStrategy.value,
|
|
||||||
koreaderSyncPercentageTolerance = serverConfig.koreaderSyncPercentageTolerance.value,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,8 +31,6 @@ import org.jetbrains.exposed.sql.statements.BatchUpdateStatement
|
|||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
import suwayomi.tachidesk.global.impl.GlobalMeta
|
import suwayomi.tachidesk.global.impl.GlobalMeta
|
||||||
import suwayomi.tachidesk.graphql.mutations.SettingsMutation
|
|
||||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
|
||||||
import suwayomi.tachidesk.graphql.types.toStatus
|
import suwayomi.tachidesk.graphql.types.toStatus
|
||||||
import suwayomi.tachidesk.manga.impl.Category
|
import suwayomi.tachidesk.manga.impl.Category
|
||||||
import suwayomi.tachidesk.manga.impl.Category.modifyCategoriesMetas
|
import suwayomi.tachidesk.manga.impl.Category.modifyCategoriesMetas
|
||||||
@@ -43,12 +41,12 @@ import suwayomi.tachidesk.manga.impl.Manga.modifyMangasMetas
|
|||||||
import suwayomi.tachidesk.manga.impl.Source.modifySourceMetas
|
import suwayomi.tachidesk.manga.impl.Source.modifySourceMetas
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.ValidationResult
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.validate
|
import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator.validate
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.handlers.BackupSettingsHandler
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.Backup
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupCategory
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupChapter
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupChapter
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings
|
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
|
||||||
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
import suwayomi.tachidesk.manga.impl.track.tracker.TrackerManager
|
||||||
@@ -58,7 +56,6 @@ import suwayomi.tachidesk.manga.model.dataclass.TrackRecordDataClass
|
|||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.server.database.dbTransaction
|
import suwayomi.tachidesk.server.database.dbTransaction
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Timer
|
import java.util.Timer
|
||||||
@@ -215,7 +212,7 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
BackupRestoreState.RestoringSettings(restoreCategories + restoreMeta + restoreSettings, restoreAmount),
|
BackupRestoreState.RestoringSettings(restoreCategories + restoreMeta + restoreSettings, restoreAmount),
|
||||||
)
|
)
|
||||||
|
|
||||||
restoreServerSettings(backup.serverSettings)
|
BackupSettingsHandler.restore(backup.serverSettings)
|
||||||
|
|
||||||
// Store source mapping for error messages
|
// Store source mapping for error messages
|
||||||
val sourceMapping = backup.getSourceMap()
|
val sourceMapping = backup.getSourceMap()
|
||||||
@@ -522,21 +519,5 @@ object ProtoBackupImport : ProtoBackupBase() {
|
|||||||
modifySourceMetas(backupSources.associateBy { it.sourceId }.mapValues { it.value.meta })
|
modifySourceMetas(backupSources.associateBy { it.sourceId }.mapValues { it.value.meta })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun restoreServerSettings(backupServerSettings: BackupServerSettings?) {
|
|
||||||
if (backupServerSettings == null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsMutation().updateSettings(
|
|
||||||
backupServerSettings.copy(
|
|
||||||
// legacy settings cannot overwrite new settings
|
|
||||||
basicAuthEnabled =
|
|
||||||
backupServerSettings.basicAuthEnabled.takeIf {
|
|
||||||
serverConfig.authMode.value == AuthMode.NONE
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun TrackRecordDataClass.forComparison() = this.copy(id = 0, mangaId = 0)
|
private fun TrackRecordDataClass.forComparison() = this.copy(id = 0, mangaId = 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
|
||||||
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.protobuf.ProtoNumber
|
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
|
||||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
|
||||||
import suwayomi.tachidesk.graphql.types.KoreaderSyncChecksumMethod
|
|
||||||
import suwayomi.tachidesk.graphql.types.KoreaderSyncStrategy
|
|
||||||
import suwayomi.tachidesk.graphql.types.Settings
|
|
||||||
import suwayomi.tachidesk.graphql.types.SettingsDownloadConversion
|
|
||||||
import suwayomi.tachidesk.graphql.types.WebUIChannel
|
|
||||||
import suwayomi.tachidesk.graphql.types.WebUIFlavor
|
|
||||||
import suwayomi.tachidesk.graphql.types.WebUIInterface
|
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class BackupServerSettings(
|
|
||||||
@ProtoNumber(1) override var ip: String,
|
|
||||||
@ProtoNumber(2) override var port: Int,
|
|
||||||
@ProtoNumber(3) override var socksProxyEnabled: Boolean,
|
|
||||||
@ProtoNumber(4) override var socksProxyVersion: Int,
|
|
||||||
@ProtoNumber(5) override var socksProxyHost: String,
|
|
||||||
@ProtoNumber(6) override var socksProxyPort: String,
|
|
||||||
@ProtoNumber(7) override var socksProxyUsername: String,
|
|
||||||
@ProtoNumber(8) override var socksProxyPassword: String,
|
|
||||||
@ProtoNumber(9) override var webUIFlavor: WebUIFlavor,
|
|
||||||
@ProtoNumber(10) override var initialOpenInBrowserEnabled: Boolean,
|
|
||||||
@ProtoNumber(11) override var webUIInterface: WebUIInterface,
|
|
||||||
@ProtoNumber(12) override var electronPath: String,
|
|
||||||
@ProtoNumber(13) override var webUIChannel: WebUIChannel,
|
|
||||||
@ProtoNumber(14) override var webUIUpdateCheckInterval: Double,
|
|
||||||
@ProtoNumber(15) override var downloadAsCbz: Boolean,
|
|
||||||
@ProtoNumber(16) override var downloadsPath: String,
|
|
||||||
@ProtoNumber(17) override var autoDownloadNewChapters: Boolean,
|
|
||||||
@ProtoNumber(18) override var excludeEntryWithUnreadChapters: Boolean,
|
|
||||||
@ProtoNumber(19) override var autoDownloadAheadLimit: Int,
|
|
||||||
@ProtoNumber(20) override var autoDownloadNewChaptersLimit: Int,
|
|
||||||
@ProtoNumber(21) override var autoDownloadIgnoreReUploads: Boolean,
|
|
||||||
@ProtoNumber(22) override var extensionRepos: List<String>,
|
|
||||||
@ProtoNumber(23) override var maxSourcesInParallel: Int,
|
|
||||||
@ProtoNumber(24) override var excludeUnreadChapters: Boolean,
|
|
||||||
@ProtoNumber(25) override var excludeNotStarted: Boolean,
|
|
||||||
@ProtoNumber(26) override var excludeCompleted: Boolean,
|
|
||||||
@ProtoNumber(27) override var globalUpdateInterval: Double,
|
|
||||||
@ProtoNumber(28) override var updateMangas: Boolean,
|
|
||||||
@ProtoNumber(29) override var basicAuthEnabled: Boolean?,
|
|
||||||
@ProtoNumber(30) override var authUsername: String,
|
|
||||||
@ProtoNumber(31) override var authPassword: String,
|
|
||||||
@ProtoNumber(32) override var debugLogsEnabled: Boolean,
|
|
||||||
@ProtoNumber(33) override var gqlDebugLogsEnabled: Boolean,
|
|
||||||
@ProtoNumber(34) override var systemTrayEnabled: Boolean,
|
|
||||||
@ProtoNumber(35) override var maxLogFiles: Int,
|
|
||||||
@ProtoNumber(36) override var maxLogFileSize: String,
|
|
||||||
@ProtoNumber(37) override var maxLogFolderSize: String,
|
|
||||||
@ProtoNumber(38) override var backupPath: String,
|
|
||||||
@ProtoNumber(39) override var backupTime: String,
|
|
||||||
@ProtoNumber(40) override var backupInterval: Int,
|
|
||||||
@ProtoNumber(41) override var backupTTL: Int,
|
|
||||||
@ProtoNumber(42) override var localSourcePath: String,
|
|
||||||
@ProtoNumber(43) override var flareSolverrEnabled: Boolean,
|
|
||||||
@ProtoNumber(44) override var flareSolverrUrl: String,
|
|
||||||
@ProtoNumber(45) override var flareSolverrTimeout: Int,
|
|
||||||
@ProtoNumber(46) override var flareSolverrSessionName: String,
|
|
||||||
@ProtoNumber(47) override var flareSolverrSessionTtl: Int,
|
|
||||||
@ProtoNumber(48) override var flareSolverrAsResponseFallback: Boolean,
|
|
||||||
@ProtoNumber(49) override var opdsUseBinaryFileSizes: Boolean,
|
|
||||||
@ProtoNumber(50) override var opdsItemsPerPage: Int,
|
|
||||||
@ProtoNumber(51) override var opdsEnablePageReadProgress: Boolean,
|
|
||||||
@ProtoNumber(52) override var opdsMarkAsReadOnDownload: Boolean,
|
|
||||||
@ProtoNumber(53) override var opdsShowOnlyUnreadChapters: Boolean,
|
|
||||||
@ProtoNumber(54) override var opdsShowOnlyDownloadedChapters: Boolean,
|
|
||||||
@ProtoNumber(55) override var opdsChapterSortOrder: SortOrder,
|
|
||||||
@ProtoNumber(56) override var authMode: AuthMode,
|
|
||||||
@ProtoNumber(57) override val downloadConversions: List<BackupSettingsDownloadConversionType>?,
|
|
||||||
@ProtoNumber(58) override var jwtAudience: String?,
|
|
||||||
@ProtoNumber(59) override var koreaderSyncServerUrl: String,
|
|
||||||
@ProtoNumber(60) override var koreaderSyncUsername: String,
|
|
||||||
@ProtoNumber(61) override var koreaderSyncUserkey: String,
|
|
||||||
@ProtoNumber(62) override var koreaderSyncDeviceId: String,
|
|
||||||
@ProtoNumber(63) override var koreaderSyncChecksumMethod: KoreaderSyncChecksumMethod,
|
|
||||||
@ProtoNumber(64) override var koreaderSyncStrategy: KoreaderSyncStrategy,
|
|
||||||
@ProtoNumber(65) override var koreaderSyncPercentageTolerance: Double,
|
|
||||||
@ProtoNumber(66) override var jwtTokenExpiry: Duration?,
|
|
||||||
@ProtoNumber(67) override var jwtRefreshExpiry: Duration?,
|
|
||||||
// Deprecated settings
|
|
||||||
@ProtoNumber(99991) override var basicAuthUsername: String?,
|
|
||||||
@ProtoNumber(99992) override var basicAuthPassword: String?,
|
|
||||||
) : Settings {
|
|
||||||
@Serializable
|
|
||||||
class BackupSettingsDownloadConversionType(
|
|
||||||
@ProtoNumber(1) override val mimeType: String,
|
|
||||||
@ProtoNumber(2) override val target: String,
|
|
||||||
@ProtoNumber(3) override val compressionLevel: Double?,
|
|
||||||
) : SettingsDownloadConversion
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package suwayomi.tachidesk.manga.impl.backup.proto.models
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.protobuf.ProtoNumber
|
||||||
|
import suwayomi.tachidesk.graphql.types.SettingsDownloadConversion
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
class BackupSettingsDownloadConversionType(
|
||||||
|
@ProtoNumber(1) override val mimeType: String,
|
||||||
|
@ProtoNumber(2) override val target: String,
|
||||||
|
@ProtoNumber(3) override val compressionLevel: Double?,
|
||||||
|
) : SettingsDownloadConversion
|
||||||
@@ -261,7 +261,7 @@ object DownloadManager {
|
|||||||
"Failed: ${downloadQueue.size - availableDownloads.size}"
|
"Failed: ${downloadQueue.size - availableDownloads.size}"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value.coerceAtLeast(1)) {
|
if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value) {
|
||||||
availableDownloads
|
availableDownloads
|
||||||
.asSequence()
|
.asSequence()
|
||||||
.map { it.manga.sourceId }
|
.map { it.manga.sourceId }
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
|||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
import org.jetbrains.exposed.sql.update
|
import org.jetbrains.exposed.sql.update
|
||||||
|
import suwayomi.tachidesk.graphql.types.DownloadConversion
|
||||||
import suwayomi.tachidesk.manga.impl.Page
|
import suwayomi.tachidesk.manga.impl.Page
|
||||||
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
|
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
|
||||||
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
|
import suwayomi.tachidesk.manga.impl.download.model.DownloadChapter
|
||||||
@@ -25,7 +26,6 @@ import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
|
|||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||||
import suwayomi.tachidesk.server.ServerConfig
|
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
import suwayomi.tachidesk.util.ConversionUtil
|
import suwayomi.tachidesk.util.ConversionUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -261,7 +261,7 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
|||||||
|
|
||||||
private fun convertPage(
|
private fun convertPage(
|
||||||
page: File,
|
page: File,
|
||||||
conversion: ServerConfig.DownloadConversion,
|
conversion: DownloadConversion,
|
||||||
) {
|
) {
|
||||||
val (targetMime, compressionLevel) = conversion
|
val (targetMime, compressionLevel) = conversion
|
||||||
|
|
||||||
|
|||||||
@@ -96,8 +96,7 @@ class Updater : IUpdater {
|
|||||||
serverConfig.subscribeTo(serverConfig.globalUpdateInterval, ::scheduleUpdateTask)
|
serverConfig.subscribeTo(serverConfig.globalUpdateInterval, ::scheduleUpdateTask)
|
||||||
serverConfig.subscribeTo(
|
serverConfig.subscribeTo(
|
||||||
serverConfig.maxSourcesInParallel,
|
serverConfig.maxSourcesInParallel,
|
||||||
{ value ->
|
{ newMaxPermits ->
|
||||||
val newMaxPermits = value.coerceAtLeast(1).coerceAtMost(20)
|
|
||||||
val permitDifference = maxSourcesInParallel - newMaxPermits
|
val permitDifference = maxSourcesInParallel - newMaxPermits
|
||||||
maxSourcesInParallel = newMaxPermits
|
maxSourcesInParallel = newMaxPermits
|
||||||
|
|
||||||
@@ -160,10 +159,7 @@ class Updater : IUpdater {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val updateInterval =
|
val updateInterval = serverConfig.globalUpdateInterval.value.hours.inWholeMilliseconds
|
||||||
serverConfig.globalUpdateInterval.value.hours
|
|
||||||
.coerceAtLeast(6.hours)
|
|
||||||
.inWholeMilliseconds
|
|
||||||
val lastAutomatedUpdate = getLastAutomatedUpdateTimestamp()
|
val lastAutomatedUpdate = getLastAutomatedUpdateTimestamp()
|
||||||
val isInitialScheduling = lastAutomatedUpdate == 0L
|
val isInitialScheduling = lastAutomatedUpdate == 0L
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ class FeedBuilderInternal(
|
|||||||
private val isSearchFeed: Boolean = false,
|
private val isSearchFeed: Boolean = false,
|
||||||
) {
|
) {
|
||||||
private val opdsItemsPerPageBounded: Int
|
private val opdsItemsPerPageBounded: Int
|
||||||
get() = serverConfig.opdsItemsPerPage.value.coerceIn(10, 5000)
|
get() = serverConfig.opdsItemsPerPage.value
|
||||||
|
|
||||||
private val feedAuthor = OpdsAuthorXml("Suwayomi", "https://suwayomi.org/")
|
private val feedAuthor = OpdsAuthorXml("Suwayomi", "https://suwayomi.org/")
|
||||||
private val feedGeneratedAt: String = OpdsDateUtil.formatCurrentInstantForOpds()
|
private val feedGeneratedAt: String = OpdsDateUtil.formatCurrentInstantForOpds()
|
||||||
|
|||||||
@@ -597,7 +597,7 @@ object OpdsFeedBuilder {
|
|||||||
"desc", "number_desc" -> ChapterTable.sourceOrder to SortOrder.DESC
|
"desc", "number_desc" -> ChapterTable.sourceOrder to SortOrder.DESC
|
||||||
"date_asc" -> ChapterTable.date_upload to SortOrder.ASC
|
"date_asc" -> ChapterTable.date_upload to SortOrder.ASC
|
||||||
"date_desc" -> ChapterTable.date_upload to SortOrder.DESC
|
"date_desc" -> ChapterTable.date_upload to SortOrder.DESC
|
||||||
else -> ChapterTable.sourceOrder to (serverConfig.opdsChapterSortOrder.value ?: SortOrder.ASC)
|
else -> ChapterTable.sourceOrder to (serverConfig.opdsChapterSortOrder.value)
|
||||||
}
|
}
|
||||||
val currentFilter = filterParam?.lowercase() ?: if (serverConfig.opdsShowOnlyUnreadChapters.value) "unread" else "all"
|
val currentFilter = filterParam?.lowercase() ?: if (serverConfig.opdsShowOnlyUnreadChapters.value) "unread" else "all"
|
||||||
var (chapterEntries, totalChapters) =
|
var (chapterEntries, totalChapters) =
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import org.jetbrains.exposed.sql.Op
|
|||||||
import org.jetbrains.exposed.sql.ResultRow
|
import org.jetbrains.exposed.sql.ResultRow
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.greater
|
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.andWhere
|
import org.jetbrains.exposed.sql.andWhere
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.sql.transactions.transaction
|
||||||
@@ -22,7 +21,7 @@ import suwayomi.tachidesk.server.serverConfig
|
|||||||
|
|
||||||
object ChapterRepository {
|
object ChapterRepository {
|
||||||
private val opdsItemsPerPageBounded: Int
|
private val opdsItemsPerPageBounded: Int
|
||||||
get() = serverConfig.opdsItemsPerPage.value.coerceIn(10, 5000)
|
get() = serverConfig.opdsItemsPerPage.value
|
||||||
|
|
||||||
private fun ResultRow.toOpdsChapterListAcqEntry(): OpdsChapterListAcqEntry =
|
private fun ResultRow.toOpdsChapterListAcqEntry(): OpdsChapterListAcqEntry =
|
||||||
OpdsChapterListAcqEntry(
|
OpdsChapterListAcqEntry(
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import suwayomi.tachidesk.server.serverConfig
|
|||||||
*/
|
*/
|
||||||
object MangaRepository {
|
object MangaRepository {
|
||||||
private val opdsItemsPerPageBounded: Int
|
private val opdsItemsPerPageBounded: Int
|
||||||
get() = serverConfig.opdsItemsPerPage.value.coerceIn(10, 5000)
|
get() = serverConfig.opdsItemsPerPage.value
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maps a database [ResultRow] to an [OpdsMangaAcqEntry] data transfer object.
|
* Maps a database [ResultRow] to an [OpdsMangaAcqEntry] data transfer object.
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ import java.util.Locale
|
|||||||
|
|
||||||
object NavigationRepository {
|
object NavigationRepository {
|
||||||
private val opdsItemsPerPageBounded: Int
|
private val opdsItemsPerPageBounded: Int
|
||||||
get() = serverConfig.opdsItemsPerPage.value.coerceIn(10, 5000)
|
get() = serverConfig.opdsItemsPerPage.value
|
||||||
|
|
||||||
private val rootSectionDetails: Map<String, Triple<String, StringResource, StringResource>> =
|
private val rootSectionDetails: Map<String, Triple<String, StringResource, StringResource>> =
|
||||||
mapOf(
|
mapOf(
|
||||||
|
|||||||
@@ -1,263 +0,0 @@
|
|||||||
package suwayomi.tachidesk.server
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Copyright (C) Contributors to the Suwayomi project
|
|
||||||
*
|
|
||||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
|
||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
||||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
|
||||||
|
|
||||||
import com.typesafe.config.Config
|
|
||||||
import io.github.config4k.getValue
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.channels.BufferOverflow
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.drop
|
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
|
||||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
|
||||||
import suwayomi.tachidesk.graphql.types.KoreaderSyncChecksumMethod
|
|
||||||
import suwayomi.tachidesk.graphql.types.KoreaderSyncStrategy
|
|
||||||
import suwayomi.tachidesk.graphql.types.WebUIChannel
|
|
||||||
import suwayomi.tachidesk.graphql.types.WebUIFlavor
|
|
||||||
import suwayomi.tachidesk.graphql.types.WebUIInterface
|
|
||||||
import xyz.nulldev.ts.config.GlobalConfigManager
|
|
||||||
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule
|
|
||||||
import kotlin.reflect.KProperty
|
|
||||||
import kotlin.time.Duration
|
|
||||||
|
|
||||||
val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
|
||||||
|
|
||||||
const val SERVER_CONFIG_MODULE_NAME = "server"
|
|
||||||
|
|
||||||
class ServerConfig(
|
|
||||||
getConfig: () -> Config,
|
|
||||||
) : SystemPropertyOverridableConfigModule(
|
|
||||||
getConfig,
|
|
||||||
SERVER_CONFIG_MODULE_NAME,
|
|
||||||
) {
|
|
||||||
open inner class OverrideConfigValue {
|
|
||||||
var flow: MutableStateFlow<Any>? = null
|
|
||||||
|
|
||||||
inline operator fun <reified T : MutableStateFlow<R>, reified R> getValue(
|
|
||||||
thisRef: ServerConfig,
|
|
||||||
property: KProperty<*>,
|
|
||||||
): T {
|
|
||||||
if (flow != null) {
|
|
||||||
return flow as T
|
|
||||||
}
|
|
||||||
|
|
||||||
val stateFlow = overridableConfig.getValue<ServerConfig, T>(thisRef, property)
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
flow = stateFlow as MutableStateFlow<Any>
|
|
||||||
|
|
||||||
stateFlow
|
|
||||||
.drop(1)
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.filter { it != thisRef.overridableConfig.getConfig().getValue<ServerConfig, R>(thisRef, property) }
|
|
||||||
.onEach { GlobalConfigManager.updateValue("$moduleName.${property.name}", it as Any) }
|
|
||||||
.launchIn(mutableConfigValueScope)
|
|
||||||
|
|
||||||
return stateFlow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open inner class MigratedConfigValue<T>(
|
|
||||||
private val readMigrated: () -> T,
|
|
||||||
private val setMigrated: (T) -> Unit,
|
|
||||||
) {
|
|
||||||
private var flow: MutableStateFlow<T>? = null
|
|
||||||
|
|
||||||
operator fun getValue(
|
|
||||||
thisRef: ServerConfig,
|
|
||||||
property: KProperty<*>,
|
|
||||||
): MutableStateFlow<T> {
|
|
||||||
if (flow != null) {
|
|
||||||
return flow!!
|
|
||||||
}
|
|
||||||
|
|
||||||
val value = readMigrated()
|
|
||||||
|
|
||||||
val stateFlow = MutableStateFlow(value)
|
|
||||||
flow = stateFlow
|
|
||||||
|
|
||||||
stateFlow
|
|
||||||
.drop(1)
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.filter { it != readMigrated() }
|
|
||||||
.onEach(setMigrated)
|
|
||||||
.launchIn(mutableConfigValueScope)
|
|
||||||
|
|
||||||
return stateFlow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val ip: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val port: MutableStateFlow<Int> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// proxy
|
|
||||||
val socksProxyEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val socksProxyVersion: MutableStateFlow<Int> by OverrideConfigValue()
|
|
||||||
val socksProxyHost: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val socksProxyPort: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val socksProxyUsername: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val socksProxyPassword: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// webUI
|
|
||||||
val webUIEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val webUIFlavor: MutableStateFlow<WebUIFlavor> by OverrideConfigValue()
|
|
||||||
val initialOpenInBrowserEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val webUIInterface: MutableStateFlow<WebUIInterface> by OverrideConfigValue()
|
|
||||||
val electronPath: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val webUIChannel: MutableStateFlow<WebUIChannel> by OverrideConfigValue()
|
|
||||||
val webUIUpdateCheckInterval: MutableStateFlow<Double> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// downloader
|
|
||||||
val downloadAsCbz: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val downloadsPath: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val autoDownloadNewChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val excludeEntryWithUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val autoDownloadNewChaptersLimit: MutableStateFlow<Int> by OverrideConfigValue()
|
|
||||||
val autoDownloadIgnoreReUploads: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val downloadConversions: MutableStateFlow<Map<String, DownloadConversion>> by OverrideConfigValue()
|
|
||||||
|
|
||||||
data class DownloadConversion(
|
|
||||||
val target: String,
|
|
||||||
val compressionLevel: Double? = null,
|
|
||||||
)
|
|
||||||
|
|
||||||
// extensions
|
|
||||||
val extensionRepos: MutableStateFlow<List<String>> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// requests
|
|
||||||
val maxSourcesInParallel: MutableStateFlow<Int> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// updater
|
|
||||||
val excludeUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val excludeNotStarted: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val excludeCompleted: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val globalUpdateInterval: MutableStateFlow<Double> by OverrideConfigValue()
|
|
||||||
val updateMangas: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// Authentication
|
|
||||||
val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue()
|
|
||||||
val authUsername: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val authPassword: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val jwtAudience: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val jwtTokenExpiry: MutableStateFlow<Duration> by OverrideConfigValue()
|
|
||||||
val jwtRefreshExpiry: MutableStateFlow<Duration> by OverrideConfigValue()
|
|
||||||
val basicAuthEnabled: MutableStateFlow<Boolean> by MigratedConfigValue({
|
|
||||||
authMode.value == AuthMode.BASIC_AUTH
|
|
||||||
}) {
|
|
||||||
authMode.value = if (it) AuthMode.BASIC_AUTH else AuthMode.NONE
|
|
||||||
}
|
|
||||||
val basicAuthUsername: MutableStateFlow<String> by MigratedConfigValue({ authUsername.value }) {
|
|
||||||
authUsername.value = it
|
|
||||||
}
|
|
||||||
val basicAuthPassword: MutableStateFlow<String> by MigratedConfigValue({ authPassword.value }) {
|
|
||||||
authPassword.value = it
|
|
||||||
}
|
|
||||||
|
|
||||||
// misc
|
|
||||||
val debugLogsEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val systemTrayEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val maxLogFiles: MutableStateFlow<Int> by OverrideConfigValue()
|
|
||||||
val maxLogFileSize: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val maxLogFolderSize: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// backup
|
|
||||||
val backupPath: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val backupTime: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val backupInterval: MutableStateFlow<Int> by OverrideConfigValue()
|
|
||||||
val backupTTL: MutableStateFlow<Int> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// local source
|
|
||||||
val localSourcePath: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// cloudflare bypass
|
|
||||||
val flareSolverrEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val flareSolverrUrl: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val flareSolverrTimeout: MutableStateFlow<Int> by OverrideConfigValue()
|
|
||||||
val flareSolverrSessionName: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val flareSolverrSessionTtl: MutableStateFlow<Int> by OverrideConfigValue()
|
|
||||||
val flareSolverrAsResponseFallback: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// opds settings
|
|
||||||
val opdsUseBinaryFileSizes: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val opdsItemsPerPage: MutableStateFlow<Int> by OverrideConfigValue()
|
|
||||||
val opdsEnablePageReadProgress: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val opdsMarkAsReadOnDownload: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val opdsShowOnlyUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val opdsShowOnlyDownloadedChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
|
||||||
val opdsChapterSortOrder: MutableStateFlow<SortOrder> by OverrideConfigValue()
|
|
||||||
|
|
||||||
// koreader sync
|
|
||||||
val koreaderSyncServerUrl: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val koreaderSyncUsername: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val koreaderSyncUserkey: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val koreaderSyncDeviceId: MutableStateFlow<String> by OverrideConfigValue()
|
|
||||||
val koreaderSyncChecksumMethod: MutableStateFlow<KoreaderSyncChecksumMethod> by OverrideConfigValue()
|
|
||||||
val koreaderSyncStrategy: MutableStateFlow<KoreaderSyncStrategy> by OverrideConfigValue()
|
|
||||||
val koreaderSyncPercentageTolerance: MutableStateFlow<Double> by OverrideConfigValue()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
fun <T> subscribeTo(
|
|
||||||
flow: Flow<T>,
|
|
||||||
onChange: suspend (value: T) -> Unit,
|
|
||||||
ignoreInitialValue: Boolean = true,
|
|
||||||
) {
|
|
||||||
val actualFlow =
|
|
||||||
if (ignoreInitialValue) {
|
|
||||||
flow.drop(1)
|
|
||||||
} else {
|
|
||||||
flow
|
|
||||||
}
|
|
||||||
|
|
||||||
val sharedFlow = MutableSharedFlow<T>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
|
||||||
actualFlow.distinctUntilChanged().mapLatest { sharedFlow.emit(it) }.launchIn(mutableConfigValueScope)
|
|
||||||
sharedFlow.onEach { onChange(it) }.launchIn(mutableConfigValueScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> subscribeTo(
|
|
||||||
flow: Flow<T>,
|
|
||||||
onChange: suspend () -> Unit,
|
|
||||||
ignoreInitialValue: Boolean = true,
|
|
||||||
) {
|
|
||||||
subscribeTo(flow, { _ -> onChange() }, ignoreInitialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> subscribeTo(
|
|
||||||
mutableStateFlow: MutableStateFlow<T>,
|
|
||||||
onChange: suspend (value: T) -> Unit,
|
|
||||||
ignoreInitialValue: Boolean = true,
|
|
||||||
) {
|
|
||||||
subscribeTo(mutableStateFlow.asStateFlow(), onChange, ignoreInitialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun <T> subscribeTo(
|
|
||||||
mutableStateFlow: MutableStateFlow<T>,
|
|
||||||
onChange: suspend () -> Unit,
|
|
||||||
ignoreInitialValue: Boolean = true,
|
|
||||||
) {
|
|
||||||
subscribeTo(mutableStateFlow.asStateFlow(), { _ -> onChange() }, ignoreInitialValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
fun register(getConfig: () -> Config) =
|
|
||||||
ServerConfig {
|
|
||||||
getConfig().getConfig(
|
|
||||||
SERVER_CONFIG_MODULE_NAME,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,6 @@ import eu.kanade.tachiyomi.App
|
|||||||
import eu.kanade.tachiyomi.createAppModule
|
import eu.kanade.tachiyomi.createAppModule
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
import io.github.config4k.registerCustomType
|
|
||||||
import io.github.config4k.toConfig
|
import io.github.config4k.toConfig
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.javalin.json.JavalinJackson
|
import io.javalin.json.JavalinJackson
|
||||||
@@ -48,8 +47,7 @@ import suwayomi.tachidesk.manga.impl.util.lang.renameTo
|
|||||||
import suwayomi.tachidesk.server.database.databaseUp
|
import suwayomi.tachidesk.server.database.databaseUp
|
||||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||||
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
||||||
import suwayomi.tachidesk.server.util.DurationType
|
import suwayomi.tachidesk.server.util.ConfigTypeRegistration
|
||||||
import suwayomi.tachidesk.server.util.MutableStateFlowType
|
|
||||||
import suwayomi.tachidesk.server.util.SystemTray
|
import suwayomi.tachidesk.server.util.SystemTray
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -176,8 +174,7 @@ fun applicationSetup() {
|
|||||||
mainLoop.start()
|
mainLoop.start()
|
||||||
|
|
||||||
// register Tachidesk's config which is dubbed "ServerConfig"
|
// register Tachidesk's config which is dubbed "ServerConfig"
|
||||||
registerCustomType(MutableStateFlowType())
|
ConfigTypeRegistration.registerCustomTypes()
|
||||||
registerCustomType(DurationType())
|
|
||||||
GlobalConfigManager.registerModule(
|
GlobalConfigManager.registerModule(
|
||||||
ServerConfig.register { GlobalConfigManager.config },
|
ServerConfig.register { GlobalConfigManager.config },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.graphql.types.Settings
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
|
internal fun Settings.asMap(): Map<String, Any?> {
|
||||||
|
val map = mutableMapOf<String, Any?>()
|
||||||
|
|
||||||
|
this::class.memberProperties.forEach { property ->
|
||||||
|
try {
|
||||||
|
// Skip the 'id' property from Node interface
|
||||||
|
if (property.name == "id") return@forEach
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val value = (property as KProperty1<Settings, *>).get(this)
|
||||||
|
map[property.name] = value
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Skip properties that can't be accessed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import suwayomi.tachidesk.graphql.types.Settings
|
||||||
|
import suwayomi.tachidesk.server.ServerConfig
|
||||||
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
|
import kotlin.reflect.KProperty1
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
|
object SettingsUpdater {
|
||||||
|
private fun updateSetting(
|
||||||
|
name: String,
|
||||||
|
value: Any,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val property =
|
||||||
|
serverConfig::class
|
||||||
|
.memberProperties
|
||||||
|
.find { it.name == name } as? KProperty1<ServerConfig, MutableStateFlow<*>>
|
||||||
|
|
||||||
|
if (property != null) {
|
||||||
|
val stateFlow = property.get(serverConfig)
|
||||||
|
|
||||||
|
val maybeConvertedValue =
|
||||||
|
SettingsRegistry
|
||||||
|
.get(name)
|
||||||
|
?.typeInfo
|
||||||
|
?.convertToInternalType
|
||||||
|
?.invoke(value) ?: value
|
||||||
|
|
||||||
|
// Normal update - MigratedConfigValue handles deprecated mappings automatically
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
(stateFlow as MutableStateFlow<Any>).value = maybeConvertedValue
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
KotlinLogging.logger { }.error(e) { "Failed to update setting $name due to" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateAll(settings: Settings) {
|
||||||
|
settings
|
||||||
|
.asMap()
|
||||||
|
.forEach { (name, value) ->
|
||||||
|
if (value != null) {
|
||||||
|
updateSetting(name, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package suwayomi.tachidesk.server.settings
|
||||||
|
|
||||||
|
import suwayomi.tachidesk.graphql.types.Settings
|
||||||
|
|
||||||
|
object SettingsValidator {
|
||||||
|
private fun validateSingle(
|
||||||
|
name: String,
|
||||||
|
value: Any?,
|
||||||
|
): String? {
|
||||||
|
val metadata = SettingsRegistry.get(name) ?: return null
|
||||||
|
return metadata.validator?.invoke(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun validateAll(
|
||||||
|
values: Map<String, Any?>,
|
||||||
|
ignoreNull: Boolean?,
|
||||||
|
): List<String> =
|
||||||
|
values
|
||||||
|
.filterValues { value -> ignoreNull == false || value != null }
|
||||||
|
.mapNotNull { (name, value) -> validateSingle(name, value)?.let { error -> "$name: $error" } }
|
||||||
|
|
||||||
|
fun validate(
|
||||||
|
settings: Settings,
|
||||||
|
ignoreNull: Boolean = false,
|
||||||
|
): List<String> = validateAll(settings.asMap(), ignoreNull)
|
||||||
|
}
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
# Server ip and port bindings
|
|
||||||
server.ip = "0.0.0.0"
|
|
||||||
server.port = 4567
|
|
||||||
|
|
||||||
# Socks5 proxy
|
|
||||||
server.socksProxyEnabled = false
|
|
||||||
server.socksProxyVersion = 5 # 4 or 5
|
|
||||||
server.socksProxyHost = ""
|
|
||||||
server.socksProxyPort = ""
|
|
||||||
server.socksProxyUsername = ""
|
|
||||||
server.socksProxyPassword = ""
|
|
||||||
|
|
||||||
# webUI
|
|
||||||
server.webUIEnabled = true
|
|
||||||
server.webUIFlavor = "WebUI" # "WebUI", "VUI" or "Custom"
|
|
||||||
server.initialOpenInBrowserEnabled = true
|
|
||||||
server.webUIInterface = "browser" # "browser" or "electron"
|
|
||||||
server.electronPath = ""
|
|
||||||
server.webUIChannel = "stable" # "bundled" (the version bundled with the server release), "stable" or "preview" - the webUI version that should be used
|
|
||||||
server.webUIUpdateCheckInterval = 23 # time in hours - 0 to disable auto update - range: 1 <= n < 24 - default 23 hours - how often the server should check for webUI updates
|
|
||||||
|
|
||||||
# downloader
|
|
||||||
server.downloadAsCbz = false
|
|
||||||
server.downloadsPath = ""
|
|
||||||
server.autoDownloadNewChapters = false # if new chapters that have been retrieved should get automatically downloaded
|
|
||||||
server.excludeEntryWithUnreadChapters = true # ignore automatic chapter downloads of entries with unread chapters
|
|
||||||
server.autoDownloadNewChaptersLimit = 0 # 0 to disable it - how many unread downloaded chapters should be available - if the limit is reached, new chapters won't be downloaded automatically. this limit will also be applied to the auto download of new chapters on an update
|
|
||||||
server.autoDownloadIgnoreReUploads = false # decides if re-uploads should be ignored during auto download of new chapters
|
|
||||||
server.downloadConversions = {}
|
|
||||||
# map input mime type to conversion information, or "default" for others
|
|
||||||
# server.downloadConversions."image/webp" = {
|
|
||||||
# target = "image/jpeg" # image type to convert to
|
|
||||||
# compressionLevel = 0.8 # quality in range [0,1], leave away to use default compression
|
|
||||||
# }
|
|
||||||
|
|
||||||
# extension repos
|
|
||||||
server.extensionRepos = [
|
|
||||||
# an example: https://github.com/MY_ACCOUNT/MY_REPO/tree/repo
|
|
||||||
]
|
|
||||||
|
|
||||||
# requests
|
|
||||||
server.maxSourcesInParallel = 6 # range: 1 <= n <= 20 - default: 6 - sets how many sources can do requests (updates, downloads) in parallel. updates/downloads are grouped by source and all mangas of a source are updated/downloaded synchronously
|
|
||||||
|
|
||||||
# updater
|
|
||||||
server.excludeUnreadChapters = true
|
|
||||||
server.excludeNotStarted = true
|
|
||||||
server.excludeCompleted = true
|
|
||||||
server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't have to be full hours e.g. 12.5) - range: 6 <= n < ∞ - default: 12 hours - interval in which the global update will be automatically triggered
|
|
||||||
server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update
|
|
||||||
|
|
||||||
# Authentication
|
|
||||||
server.authMode = "none" # none, basic_auth, simple_login or ui_login
|
|
||||||
server.authUsername = ""
|
|
||||||
server.authPassword = ""
|
|
||||||
server.jwtAudience = "suwayomi-server-api"
|
|
||||||
server.jwtTokenExpiry = "5m"
|
|
||||||
server.jwtRefreshExpiry = "60d"
|
|
||||||
|
|
||||||
# misc
|
|
||||||
server.debugLogsEnabled = false
|
|
||||||
server.systemTrayEnabled = true
|
|
||||||
server.maxLogFiles = 31 # the max number of days to keep files before they get deleted
|
|
||||||
server.maxLogFileSize = "10mb" # the max size of a log file - possible values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)
|
|
||||||
server.maxLogFolderSize = "100mb" # the max size of all saved log files - possible values: 1 (bytes), 1KB (kilobytes), 1MB (megabytes), 1GB (gigabytes)
|
|
||||||
|
|
||||||
# backup
|
|
||||||
server.backupPath = ""
|
|
||||||
server.backupTime = "00:00" # range: hour: 0-23, minute: 0-59 - default: "00:00" - time of day at which the automated backup should be triggered
|
|
||||||
server.backupInterval = 1 # time in days - 0 to disable it - range: 1 <= n < ∞ - default: 1 day - interval in which the server will automatically create a backup
|
|
||||||
server.backupTTL = 14 # time in days - 0 to disable it - range: 1 <= n < ∞ - default: 14 days - how long backup files will be kept before they will get deleted
|
|
||||||
|
|
||||||
# local source
|
|
||||||
server.localSourcePath = ""
|
|
||||||
|
|
||||||
# Cloudflare bypass
|
|
||||||
server.flareSolverrEnabled = false
|
|
||||||
server.flareSolverrUrl = "http://localhost:8191"
|
|
||||||
server.flareSolverrTimeout = 60 # time in seconds
|
|
||||||
server.flareSolverrSessionName = "suwayomi"
|
|
||||||
server.flareSolverrSessionTtl = 15 # time in minutes
|
|
||||||
server.flareSolverrAsResponseFallback = false
|
|
||||||
|
|
||||||
# OPDS
|
|
||||||
server.opdsUseBinaryFileSizes = false # if the file sizes should be displayed in binary (KiB, MiB, GiB) or decimal (KB, MB, GB)
|
|
||||||
server.opdsItemsPerPage = 50 # Range (10 - 5000)
|
|
||||||
server.opdsEnablePageReadProgress = true
|
|
||||||
server.opdsMarkAsReadOnDownload = false
|
|
||||||
server.opdsShowOnlyUnreadChapters = false
|
|
||||||
server.opdsShowOnlyDownloadedChapters = false
|
|
||||||
server.opdsChapterSortOrder = "DESC" # "ASC", "DESC"
|
|
||||||
|
|
||||||
# Koreader Sync
|
|
||||||
server.koreaderSyncServerUrl = "http://localhost:17200"
|
|
||||||
server.koreaderSyncUsername = ""
|
|
||||||
server.koreaderSyncUserkey = ""
|
|
||||||
server.koreaderSyncDeviceId = ""
|
|
||||||
server.koreaderSyncChecksumMethod = "binary" # "binary" or "filename"
|
|
||||||
server.koreaderSyncStrategy = "disabled" # "prompt", "silent", "send", "receive", "disabled"
|
|
||||||
server.koreaderSyncPercentageTolerance = 0.00000000000001 # absolute tolerance for progress comparison from 1 (widest) to 1e-15 (strict)
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
# Server ip and port bindings
|
|
||||||
server.ip = "0.0.0.0"
|
|
||||||
server.port = 4567
|
|
||||||
|
|
||||||
# Socks5 proxy
|
|
||||||
server.socksProxyEnabled = false
|
|
||||||
server.socksProxyVersion = 5 # 4 or 5
|
|
||||||
server.socksProxyHost = ""
|
|
||||||
server.socksProxyPort = ""
|
|
||||||
server.socksProxyUsername = ""
|
|
||||||
server.socksProxyPassword = ""
|
|
||||||
|
|
||||||
# webUI
|
|
||||||
server.webUIEnabled = true
|
|
||||||
server.webUIFlavor = "WebUI" # "WebUI", "VUI" or "Custom"
|
|
||||||
server.initialOpenInBrowserEnabled = true
|
|
||||||
server.webUIInterface = "browser" # "browser" or "electron"
|
|
||||||
server.electronPath = ""
|
|
||||||
server.webUIChannel = "stable" # "bundled" (the version bundled with the server release), "stable" or "preview" - the webUI version that should be used
|
|
||||||
server.webUIUpdateCheckInterval = 23 # time in hours - 0 to disable auto update - range: 1 <= n < 24 - default 23 hours - how often the server should check for webUI updates
|
|
||||||
|
|
||||||
# downloader
|
|
||||||
server.downloadAsCbz = false
|
|
||||||
server.downloadsPath = ""
|
|
||||||
server.autoDownloadNewChapters = false # if new chapters that have been retrieved should get automatically downloaded
|
|
||||||
server.excludeEntryWithUnreadChapters = true # ignore automatic chapter downloads of entries with unread chapters
|
|
||||||
server.autoDownloadNewChaptersLimit = 0 # 0 to disable it - how many unread downloaded chapters should be available - if the limit is reached, new chapters won't be downloaded automatically. this limit will also be applied to the auto download of new chapters on an update
|
|
||||||
server.autoDownloadIgnoreReUploads = false # decides if re-uploads should be ignored during auto download of new chapters
|
|
||||||
server.downloadConversions = {}
|
|
||||||
# map input mime type to conversion information, or "default" for others
|
|
||||||
# server.downloadConversions."image/webp" = {
|
|
||||||
# target = "image/jpeg" # image type to convert to
|
|
||||||
# compressionLevel = 0.8 # quality in range [0,1], leave away to use default compression
|
|
||||||
# }
|
|
||||||
|
|
||||||
# extension repos
|
|
||||||
server.extensionRepos = [
|
|
||||||
# an example: https://github.com/MY_ACCOUNT/MY_REPO/tree/repo
|
|
||||||
]
|
|
||||||
|
|
||||||
# requests
|
|
||||||
server.maxSourcesInParallel = 6 # range: 1 <= n <= 20 - default: 6 - sets how many sources can do requests (updates, downloads) in parallel. updates/downloads are grouped by source and all mangas of a source are updated/downloaded synchronously
|
|
||||||
|
|
||||||
# updater
|
|
||||||
server.excludeUnreadChapters = true
|
|
||||||
server.excludeNotStarted = true
|
|
||||||
server.excludeCompleted = true
|
|
||||||
server.globalUpdateInterval = 12 # time in hours - 0 to disable it - (doesn't have to be full hours e.g. 12.5) - range: 6 <= n < ∞ - default: 12 hours - interval in which the global update will be automatically triggered
|
|
||||||
server.updateMangas = false # if the mangas should be updated along with the chapter list during a library/category update
|
|
||||||
|
|
||||||
# Authentication
|
|
||||||
server.authMode = "none" # none, basic_auth, simple_login or ui_login
|
|
||||||
server.authUsername = ""
|
|
||||||
server.authPassword = ""
|
|
||||||
server.jwtAudience = "suwayomi-server-api"
|
|
||||||
server.jwtTokenExpiry = "5m"
|
|
||||||
server.jwtRefreshExpiry = "60d"
|
|
||||||
|
|
||||||
# misc
|
|
||||||
server.debugLogsEnabled = false
|
|
||||||
server.systemTrayEnabled = true
|
|
||||||
|
|
||||||
# backup
|
|
||||||
server.backupPath = ""
|
|
||||||
server.backupTime = "00:00" # range: hour: 0-23, minute: 0-59 - default: "00:00" - time of day at which the automated backup should be triggered
|
|
||||||
server.backupInterval = 1 # time in days - 0 to disable it - range: 1 <= n < ∞ - default: 1 day - interval in which the server will automatically create a backup
|
|
||||||
server.backupTTL = 14 # time in days - 0 to disable it - range: 1 <= n < ∞ - default: 14 days - how long backup files will be kept before they will get deleted
|
|
||||||
|
|
||||||
# local source
|
|
||||||
server.localSourcePath = ""
|
|
||||||
|
|
||||||
# Cloudflare bypass
|
|
||||||
server.flareSolverrEnabled = false
|
|
||||||
server.flareSolverrUrl = "http://localhost:8191"
|
|
||||||
server.flareSolverrTimeout = 60 # time in seconds
|
|
||||||
server.flareSolverrSessionName = "suwayomi"
|
|
||||||
server.flareSolverrSessionTtl = 15 # time in minutes
|
|
||||||
server.flareSolverrAsResponseFallback = false
|
|
||||||
|
|
||||||
# OPDS
|
|
||||||
server.opdsItemsPerPage = 50 # Range (10 - 5000)
|
|
||||||
server.opdsEnablePageReadProgress = true
|
|
||||||
server.opdsMarkAsReadOnDownload = false
|
|
||||||
server.opdsShowOnlyUnreadChapters = false
|
|
||||||
server.opdsShowOnlyDownloadedChapters = false
|
|
||||||
server.opdsChapterSortOrder = "DESC" # "ASC", "DESC"
|
|
||||||
@@ -2,6 +2,8 @@ rootProject.name = System.getenv("ProductName") ?: "Suwayomi-Server"
|
|||||||
|
|
||||||
include("server")
|
include("server")
|
||||||
include("server:i18n")
|
include("server:i18n")
|
||||||
|
include("server:server-config")
|
||||||
|
include("server:server-config-generate")
|
||||||
|
|
||||||
include("AndroidCompat")
|
include("AndroidCompat")
|
||||||
include("AndroidCompat:Config")
|
include("AndroidCompat:Config")
|
||||||
|
|||||||
Reference in New Issue
Block a user