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:
@@ -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
|
||||
}
|
||||
}
|
||||
-13
@@ -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,
|
||||
)
|
||||
}
|
||||
+4
-107
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-21
@@ -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)
|
||||
}
|
||||
|
||||
-95
@@ -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
|
||||
}
|
||||
+12
@@ -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 }
|
||||
|
||||
+2
-2
@@ -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"
|
||||
Reference in New Issue
Block a user