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:
schroda
2025-09-01 23:02:58 +02:00
committed by GitHub
parent 11b2a6b616
commit 8ef2877040
48 changed files with 2443 additions and 1330 deletions
@@ -2,54 +2,17 @@ package suwayomi.tachidesk.graphql.mutations
import com.expediagroup.graphql.generator.annotations.GraphQLIgnore
import graphql.schema.DataFetchingEnvironment
import kotlinx.coroutines.flow.MutableStateFlow
import suwayomi.tachidesk.graphql.server.getAttribute
import suwayomi.tachidesk.graphql.types.PartialSettingsType
import suwayomi.tachidesk.graphql.types.Settings
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.getAttribute
import suwayomi.tachidesk.server.SERVER_CONFIG_MODULE_NAME
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 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 {
data class SetSettingsInput(
@@ -62,176 +25,14 @@ class SettingsMutation {
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
fun updateSettings(settings: Settings) {
updateSetting(settings.ip, serverConfig.ip)
updateSetting(settings.port, serverConfig.port)
// 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,
)
}
val validationErrors = SettingsValidator.validate(settings, true)
if (validationErrors.isNotEmpty()) {
throw Exception("Validation errors: ${validationErrors.joinToString("; ")}")
}
// extension
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)
SettingsUpdater.updateAll(settings)
}
fun setSettings(
@@ -241,7 +42,6 @@ class SettingsMutation {
dataFetchingEnvironment.getAttribute(Attribute.TachideskUser).requireUser()
val (clientMutationId, settings) = input
validateSettings(settings)
updateSettings(settings)
return SetSettingsPayload(clientMutationId, SettingsType())
@@ -1,14 +0,0 @@
package suwayomi.tachidesk.graphql.types
enum class AuthMode {
NONE,
BASIC_AUTH,
SIMPLE_LOGIN,
UI_LOGIN,
// TODO: ACCOUNT for #623
;
companion object {
fun from(channel: String): AuthMode = entries.find { it.name.lowercase() == channel.lowercase() } ?: NONE
}
}
@@ -1,18 +1,5 @@
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(
val isLoggedIn: Boolean,
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.Source
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.BackupCategory
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.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.BackupTracking
import suwayomi.tachidesk.manga.impl.track.Track
@@ -97,15 +96,11 @@ object ProtoBackupExport : ProtoBackupBase() {
}
}
val (hour, minute) =
val (backupHour, backupMinute) =
serverConfig.backupTime.value
.split(":")
.map { it.toInt() }
val backupHour = hour.coerceAtLeast(0).coerceAtMost(23)
val backupMinute = minute.coerceAtLeast(0).coerceAtMost(59)
val backupInterval =
serverConfig.backupInterval.value.days
.coerceAtLeast(1.days)
val backupInterval = serverConfig.backupInterval.value.days
// trigger last backup in case the server wasn't running on the scheduled time
val lastAutomatedBackup = preferences.getLong(LAST_AUTOMATED_BACKUP_KEY, 0)
@@ -193,7 +188,7 @@ object ProtoBackupExport : ProtoBackupBase() {
backupCategories(flags),
backupExtensionInfo(databaseManga, flags),
backupGlobalMeta(flags),
backupServerSettings(flags),
BackupSettingsHandler.backup(flags),
)
}
@@ -378,102 +373,4 @@ object ProtoBackupExport : ProtoBackupBase() {
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.update
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.manga.impl.Category
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.backup.proto.ProtoBackupValidator.ValidationResult
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.BackupCategory
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.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.BackupTracking
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.MangaTable
import suwayomi.tachidesk.server.database.dbTransaction
import suwayomi.tachidesk.server.serverConfig
import java.io.InputStream
import java.util.Date
import java.util.Timer
@@ -215,7 +212,7 @@ object ProtoBackupImport : ProtoBackupBase() {
BackupRestoreState.RestoringSettings(restoreCategories + restoreMeta + restoreSettings, restoreAmount),
)
restoreServerSettings(backup.serverSettings)
BackupSettingsHandler.restore(backup.serverSettings)
// Store source mapping for error messages
val sourceMapping = backup.getSourceMap()
@@ -522,21 +519,5 @@ object ProtoBackupImport : ProtoBackupBase() {
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)
}
@@ -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}"
}
if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value.coerceAtLeast(1)) {
if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value) {
availableDownloads
.asSequence()
.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.transactions.transaction
import org.jetbrains.exposed.sql.update
import suwayomi.tachidesk.graphql.types.DownloadConversion
import suwayomi.tachidesk.manga.impl.Page
import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReady
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.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.server.ServerConfig
import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.util.ConversionUtil
import java.io.File
@@ -261,7 +261,7 @@ abstract class ChaptersFilesProvider<Type : FileType>(
private fun convertPage(
page: File,
conversion: ServerConfig.DownloadConversion,
conversion: DownloadConversion,
) {
val (targetMime, compressionLevel) = conversion
@@ -96,8 +96,7 @@ class Updater : IUpdater {
serverConfig.subscribeTo(serverConfig.globalUpdateInterval, ::scheduleUpdateTask)
serverConfig.subscribeTo(
serverConfig.maxSourcesInParallel,
{ value ->
val newMaxPermits = value.coerceAtLeast(1).coerceAtMost(20)
{ newMaxPermits ->
val permitDifference = maxSourcesInParallel - newMaxPermits
maxSourcesInParallel = newMaxPermits
@@ -160,10 +159,7 @@ class Updater : IUpdater {
return
}
val updateInterval =
serverConfig.globalUpdateInterval.value.hours
.coerceAtLeast(6.hours)
.inWholeMilliseconds
val updateInterval = serverConfig.globalUpdateInterval.value.hours.inWholeMilliseconds
val lastAutomatedUpdate = getLastAutomatedUpdateTimestamp()
val isInitialScheduling = lastAutomatedUpdate == 0L
@@ -27,7 +27,7 @@ class FeedBuilderInternal(
private val isSearchFeed: Boolean = false,
) {
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 feedGeneratedAt: String = OpdsDateUtil.formatCurrentInstantForOpds()
@@ -597,7 +597,7 @@ object OpdsFeedBuilder {
"desc", "number_desc" -> ChapterTable.sourceOrder to SortOrder.DESC
"date_asc" -> ChapterTable.date_upload to SortOrder.ASC
"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"
var (chapterEntries, totalChapters) =
@@ -6,7 +6,6 @@ import org.jetbrains.exposed.sql.Op
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.SortOrder
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.andWhere
import org.jetbrains.exposed.sql.transactions.transaction
@@ -22,7 +21,7 @@ import suwayomi.tachidesk.server.serverConfig
object ChapterRepository {
private val opdsItemsPerPageBounded: Int
get() = serverConfig.opdsItemsPerPage.value.coerceIn(10, 5000)
get() = serverConfig.opdsItemsPerPage.value
private fun ResultRow.toOpdsChapterListAcqEntry(): OpdsChapterListAcqEntry =
OpdsChapterListAcqEntry(
@@ -40,7 +40,7 @@ import suwayomi.tachidesk.server.serverConfig
*/
object MangaRepository {
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.
@@ -28,7 +28,7 @@ import java.util.Locale
object NavigationRepository {
private val opdsItemsPerPageBounded: Int
get() = serverConfig.opdsItemsPerPage.value.coerceIn(10, 5000)
get() = serverConfig.opdsItemsPerPage.value
private val rootSectionDetails: Map<String, Triple<String, StringResource, StringResource>> =
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.network.NetworkHelper
import eu.kanade.tachiyomi.source.local.LocalSource
import io.github.config4k.registerCustomType
import io.github.config4k.toConfig
import io.github.oshai.kotlinlogging.KotlinLogging
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.generated.BuildConfig
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
import suwayomi.tachidesk.server.util.DurationType
import suwayomi.tachidesk.server.util.MutableStateFlowType
import suwayomi.tachidesk.server.util.ConfigTypeRegistration
import suwayomi.tachidesk.server.util.SystemTray
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@@ -176,8 +174,7 @@ fun applicationSetup() {
mainLoop.start()
// register Tachidesk's config which is dubbed "ServerConfig"
registerCustomType(MutableStateFlowType())
registerCustomType(DurationType())
ConfigTypeRegistration.registerCustomTypes()
GlobalConfigManager.registerModule(
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,31 +0,0 @@
package suwayomi.tachidesk.server.util
import com.typesafe.config.Config
import io.github.config4k.ClassContainer
import io.github.config4k.CustomType
import io.github.config4k.readers.SelectReader
import io.github.config4k.toConfig
import kotlin.time.Duration
class DurationType : CustomType {
override fun parse(
clazz: ClassContainer,
config: Config,
name: String,
): Any? {
val clazz = ClassContainer(String::class)
val reader = SelectReader.getReader(clazz)
val path = name
val result = reader(config, path) as String
return Duration.parse(result)
}
override fun testParse(clazz: ClassContainer): Boolean = clazz.mapperClass.qualifiedName == "kotlin.time.Duration"
override fun testToConfig(obj: Any): Boolean = obj as? Duration != null
override fun toConfig(
obj: Any,
name: String,
): Config = (obj as Duration).toString().toConfig(name)
}
@@ -1,39 +0,0 @@
package suwayomi.tachidesk.server.util
import com.typesafe.config.Config
import io.github.config4k.ClassContainer
import io.github.config4k.CustomType
import io.github.config4k.readers.SelectReader
import io.github.config4k.toConfig
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class MutableStateFlowType : CustomType {
override fun parse(
clazz: ClassContainer,
config: Config,
name: String,
): Any? {
val reader =
SelectReader.getReader(
clazz.typeArguments.entries
.first()
.value,
)
val path = name
val result = reader(config, path)
return MutableStateFlow(result)
}
override fun testParse(clazz: ClassContainer): Boolean =
clazz.mapperClass.qualifiedName == "kotlinx.coroutines.flow.MutableStateFlow" ||
clazz.mapperClass.qualifiedName == "kotlinx.coroutines.flow.StateFlow" ||
clazz.mapperClass.qualifiedName == "kotlinx.coroutines.flow.StateFlowImpl"
override fun testToConfig(obj: Any): Boolean = (obj as? StateFlow<*>)?.value != null
override fun toConfig(
obj: Any,
name: String,
): Config = (obj as StateFlow<*>).value!!.toConfig(name)
}
@@ -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"