diff --git a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt index 2aeb83d8..ab9b5576 100644 --- a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt +++ b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigManager.kt @@ -14,6 +14,8 @@ import com.typesafe.config.ConfigValue import com.typesafe.config.ConfigValueFactory import com.typesafe.config.parser.ConfigDocument import com.typesafe.config.parser.ConfigDocumentFactory +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import mu.KotlinLogging import java.io.File @@ -32,6 +34,8 @@ open class ConfigManager { val loadedModules: Map, ConfigModule> get() = generatedModules + private val mutex = Mutex() + /** * Get a config module */ @@ -98,11 +102,13 @@ open class ConfigManager { userConfigFile.writeText(newFileContent) } - fun updateValue(path: String, value: Any) { - val configValue = ConfigValueFactory.fromAnyRef(value) + suspend fun updateValue(path: String, value: Any) { + mutex.withLock { + val configValue = ConfigValueFactory.fromAnyRef(value) - updateUserConfigFile(path, configValue) - internalConfig = internalConfig.withValue(path, configValue) + updateUserConfigFile(path, configValue) + internalConfig = internalConfig.withValue(path, configValue) + } } /** diff --git a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigModule.kt b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigModule.kt index 9005e514..643adbfa 100644 --- a/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigModule.kt +++ b/AndroidCompat/Config/src/main/java/xyz/nulldev/ts/config/ConfigModule.kt @@ -26,10 +26,6 @@ abstract class SystemPropertyOverridableConfigModule(getConfig: () -> Config, mo /** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */ class SystemPropertyOverrideDelegate(val getConfig: () -> Config, val moduleName: String) { - operator fun setValue(thisRef: R, property: KProperty<*>, value: Any) { - GlobalConfigManager.updateValue("$moduleName.${property.name}", value) - } - inline operator fun getValue(thisRef: R, property: KProperty<*>): T { val configValue: T = getConfig().getValue(thisRef, property) diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt index 068d125a..387521f0 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -20,7 +20,6 @@ import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor import mu.KotlinLogging import okhttp3.OkHttpClient import okhttp3.logging.HttpLoggingInterceptor -import suwayomi.tachidesk.server.serverConfig import java.net.CookieHandler import java.net.CookieManager import java.net.CookiePolicy @@ -53,18 +52,16 @@ class NetworkHelper(context: Context) { .callTimeout(2, TimeUnit.MINUTES) .addInterceptor(UserAgentInterceptor()) - if (serverConfig.debugLogsEnabled) { - val httpLoggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger { - val logger = KotlinLogging.logger { } + val httpLoggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger { + val logger = KotlinLogging.logger { } - override fun log(message: String) { - logger.debug { message } - } - }).apply { - level = HttpLoggingInterceptor.Level.BASIC + override fun log(message: String) { + logger.debug { message } } - builder.addInterceptor(httpLoggingInterceptor) + }).apply { + level = HttpLoggingInterceptor.Level.BASIC } + builder.addInterceptor(httpLoggingInterceptor) // when (preferences.dohProvider()) { // PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() diff --git a/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index 9960720d..421eecc9 100644 --- a/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/server/src/main/kotlin/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -87,8 +87,8 @@ object CFClearance { LaunchOptions() .setHeadless(false) .apply { - if (serverConfig.socksProxyEnabled) { - setProxy("socks5://${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") + if (serverConfig.socksProxyEnabled.value) { + setProxy("socks5://${serverConfig.socksProxyHost.value}:${serverConfig.socksProxyPort.value}") } } ).use { browser -> diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt index 76e57ba9..f484cb13 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/InfoMutation.kt @@ -3,7 +3,6 @@ package suwayomi.tachidesk.graphql.mutations import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING -import suwayomi.tachidesk.graphql.types.UpdateState.FINISHED import suwayomi.tachidesk.graphql.types.UpdateState.STOPPED import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus @@ -37,7 +36,7 @@ class InfoMutation { input.clientMutationId, WebUIUpdateStatus( info = WebUIUpdateInfo( - channel = serverConfig.webUIChannel, + channel = serverConfig.webUIChannel.value, tag = version, updateAvailable ), diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt index c00d68fc..45ed95fc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/InfoQuery.kt @@ -55,7 +55,7 @@ class InfoQuery { return future { val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable() WebUIUpdateInfo( - channel = serverConfig.webUIChannel, + channel = serverConfig.webUIChannel.value, tag = version, updateAvailable ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt index 391e4b28..3637a71f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Chapter.kt @@ -210,7 +210,7 @@ object Chapter { val wasInitialFetch = currentNumberOfChapters == 0 // make sure to ignore initial fetch - val downloadNewChapters = serverConfig.autoDownloadNewChapters && !wasInitialFetch && areNewChaptersAvailable + val downloadNewChapters = serverConfig.autoDownloadNewChapters.value && !wasInitialFetch && areNewChaptersAvailable if (!downloadNewChapters) { return } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt index 907a217a..4c61a6c0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/ChapterDownloadHelper.kt @@ -35,7 +35,7 @@ object ChapterDownloadHelper { val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId)) val cbzFile = File(getChapterCbzPath(mangaId, chapterId)) if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId) - if (!chapterFolder.exists() && serverConfig.downloadAsCbz) return ArchiveProvider(mangaId, chapterId) + if (!chapterFolder.exists() && serverConfig.downloadAsCbz.value) return ArchiveProvider(mangaId, chapterId) return FolderProvider(mangaId, chapterId) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt index 474a72ee..2ebb4aa7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/backup/proto/ProtoBackupExport.kt @@ -8,6 +8,7 @@ package suwayomi.tachidesk.manga.impl.backup.proto * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import eu.kanade.tachiyomi.source.model.UpdateStrategy +import kotlinx.coroutines.flow.combine import mu.KotlinLogging import okio.buffer import okio.gzip @@ -53,10 +54,22 @@ object ProtoBackupExport : ProtoBackupBase() { private const val lastAutomatedBackupKey = "lastAutomatedBackupKey" private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java) + init { + serverConfig.subscribeTo( + combine(serverConfig.backupInterval, serverConfig.backupTime) { interval, timeOfDay -> + Pair( + interval, + timeOfDay + ) + }, + ::scheduleAutomatedBackupTask + ) + } + fun scheduleAutomatedBackupTask() { HAScheduler.descheduleCron(backupSchedulerJobId) - val areAutomatedBackupsDisabled = serverConfig.backupInterval == 0 + val areAutomatedBackupsDisabled = serverConfig.backupInterval.value == 0 if (areAutomatedBackupsDisabled) { return } @@ -67,10 +80,10 @@ object ProtoBackupExport : ProtoBackupBase() { preferences.putLong(lastAutomatedBackupKey, System.currentTimeMillis()) } - val (hour, minute) = serverConfig.backupTime.split(":").map { it.toInt() } + val (hour, minute) = 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.days.coerceAtLeast(1.days) + val backupInterval = serverConfig.backupInterval.value.days.coerceAtLeast(1.days) // trigger last backup in case the server wasn't running on the scheduled time val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, System.currentTimeMillis()) @@ -105,9 +118,9 @@ object ProtoBackupExport : ProtoBackupBase() { } private fun cleanupAutomatedBackups() { - logger.debug { "Cleanup automated backups (ttl= ${serverConfig.backupTTL})" } + logger.debug { "Cleanup automated backups (ttl= ${serverConfig.backupTTL.value})" } - val isCleanupDisabled = serverConfig.backupTTL == 0 + val isCleanupDisabled = serverConfig.backupTTL.value == 0 if (isCleanupDisabled) { return } @@ -133,7 +146,7 @@ object ProtoBackupExport : ProtoBackupBase() { val lastAccessTime = file.lastModified() val isTTLReached = - System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.days.coerceAtLeast(1.days).inWholeMilliseconds + System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.value.days.coerceAtLeast(1.days).inWholeMilliseconds if (isTTLReached) { file.delete() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt index ca5d885f..4d44c9f6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/download/DownloadManager.kt @@ -42,6 +42,7 @@ import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.toDataClass +import suwayomi.tachidesk.server.serverConfig import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.util.concurrent.ConcurrentHashMap @@ -51,8 +52,6 @@ import kotlin.time.Duration.Companion.seconds private val logger = KotlinLogging.logger {} -private const val MAX_SOURCES_IN_PARAllEL = 5 - object DownloadManager { private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val clients = ConcurrentHashMap() @@ -169,6 +168,22 @@ object DownloadManager { private val downloaderWatch = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) init { + serverConfig.subscribeTo(serverConfig.maxSourcesInParallel, { maxSourcesInParallel -> + val runningDownloaders = downloaders.values.filter { it.isActive } + var downloadersToStop = runningDownloaders.size - maxSourcesInParallel + + logger.debug { "Max sources in parallel changed to $maxSourcesInParallel (running downloaders ${runningDownloaders.size})" } + + if (downloadersToStop > 0) { + runningDownloaders.takeWhile { + it.stop() + --downloadersToStop > 0 + } + } else { + downloaderWatch.emit(Unit) + } + }) + scope.launch { downloaderWatch.sample(1.seconds).collect { val runningDownloaders = downloaders.values.filter { it.isActive } @@ -176,14 +191,14 @@ object DownloadManager { logger.info { "Running: ${runningDownloaders.size}, Queued: ${availableDownloads.size}, Failed: ${downloadQueue.size - availableDownloads.size}" } - if (runningDownloaders.size < MAX_SOURCES_IN_PARAllEL) { + if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value) { availableDownloads.asSequence() .map { it.manga.sourceId } .distinct() .minus( runningDownloaders.map { it.sourceId }.toSet() ) - .take(MAX_SOURCES_IN_PARAllEL - runningDownloaders.size) + .take(serverConfig.maxSourcesInParallel.value - runningDownloaders.size) .map { getDownloader(it) } .forEach { it.start() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt index 4b23ebf0..6883267a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt @@ -33,6 +33,7 @@ import suwayomi.tachidesk.util.HAScheduler import java.util.Date import java.util.concurrent.ConcurrentHashMap import java.util.prefs.Preferences +import kotlin.math.absoluteValue import kotlin.time.Duration.Companion.hours class Updater : IUpdater { @@ -45,13 +46,36 @@ class Updater : IUpdater { private val tracker = ConcurrentHashMap() private val updateChannels = ConcurrentHashMap>() - private val semaphore = Semaphore(serverConfig.maxParallelUpdateRequests) + private var maxSourcesInParallel = 20 // max permits, necessary to be set to be able to release up to 20 permits + private val semaphore = Semaphore(maxSourcesInParallel) private val lastAutomatedUpdateKey = "lastAutomatedUpdateKey" private val preferences = Preferences.userNodeForPackage(Updater::class.java) private var currentUpdateTaskId = "" + init { + serverConfig.subscribeTo(serverConfig.globalUpdateInterval, ::scheduleUpdateTask) + serverConfig.subscribeTo( + serverConfig.maxSourcesInParallel, + { value -> + val newMaxPermits = value.coerceAtLeast(1).coerceAtMost(20) + val permitDifference = maxSourcesInParallel - newMaxPermits + maxSourcesInParallel = newMaxPermits + + val addMorePermits = permitDifference < 0 + for (i in 1..permitDifference.absoluteValue) { + if (addMorePermits) { + semaphore.release() + } else { + semaphore.acquire() + } + } + }, + ignoreInitialValue = false + ) + } + private fun autoUpdateTask() { val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0) preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis()) @@ -61,19 +85,19 @@ class Updater : IUpdater { return } - logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" } + logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval.value}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" } addCategoriesToUpdateQueue(Category.getCategoryList(), clear = true, forceAll = false) } fun scheduleUpdateTask() { HAScheduler.deschedule(currentUpdateTaskId) - val isAutoUpdateDisabled = serverConfig.globalUpdateInterval == 0.0 + val isAutoUpdateDisabled = serverConfig.globalUpdateInterval.value == 0.0 if (isAutoUpdateDisabled) { return } - val updateInterval = serverConfig.globalUpdateInterval.hours.coerceAtLeast(6.hours).inWholeMilliseconds + val updateInterval = serverConfig.globalUpdateInterval.value.hours.coerceAtLeast(6.hours).inWholeMilliseconds val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0) val timeToNextExecution = (updateInterval - (System.currentTimeMillis() - lastAutomatedUpdate)).mod(updateInterval) @@ -150,9 +174,9 @@ class Updater : IUpdater { val mangasToUpdate = categoriesToUpdateMangas .asSequence() .filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE } - .filter { if (serverConfig.excludeUnreadChapters) { (it.unreadCount ?: 0L) == 0L } else true } - .filter { if (serverConfig.excludeNotStarted) { it.lastReadAt != null } else true } - .filter { if (serverConfig.excludeCompleted) { it.status != MangaStatus.COMPLETED.name } else true } + .filter { if (serverConfig.excludeUnreadChapters.value) { (it.unreadCount ?: 0L) == 0L } else true } + .filter { if (serverConfig.excludeNotStarted.value) { it.lastReadAt != null } else true } + .filter { if (serverConfig.excludeCompleted.value) { it.status != MangaStatus.COMPLETED.name } else true } .filter { forceAll || !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } } .toList() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt new file mode 100644 index 00000000..e7668f8d --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ConfigAdapters.kt @@ -0,0 +1,29 @@ +package suwayomi.tachidesk.server + +interface ConfigAdapter { + fun toType(configValue: String): T +} + +object StringConfigAdapter : ConfigAdapter { + override fun toType(configValue: String): String { + return configValue + } +} + +object IntConfigAdapter : ConfigAdapter { + override fun toType(configValue: String): Int { + return configValue.toInt() + } +} + +object BooleanConfigAdapter : ConfigAdapter { + override fun toType(configValue: String): Boolean { + return configValue.toBoolean() + } +} + +object DoubleConfigAdapter : ConfigAdapter { + override fun toType(configValue: String): Double { + return configValue.toDouble() + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index be26cb0a..9e52f95c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -18,9 +18,12 @@ import io.swagger.v3.oas.models.info.Info import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.future.future import kotlinx.coroutines.runBlocking import mu.KotlinLogging +import org.eclipse.jetty.server.Server +import org.eclipse.jetty.server.ServerConnector import org.kodein.di.DI import org.kodein.di.conf.global import org.kodein.di.instance @@ -46,27 +49,48 @@ object JavalinSetup { } fun javalinSetup() { + val server = Server() + val connector = ServerConnector(server).apply { + host = serverConfig.ip.value + port = serverConfig.port.value + } + server.addConnector(connector) + + serverConfig.subscribeTo(combine(serverConfig.ip, serverConfig.port) { ip, port -> Pair(ip, port) }, { (newIp, newPort) -> + val oldIp = connector.host + val oldPort = connector.port + + connector.host = newIp + connector.port = newPort + connector.stop() + connector.start() + + logger.info { "Server ip and/or port changed from $oldIp:$oldPort to $newIp:$newPort " } + }) + val app = Javalin.create { config -> - if (serverConfig.webUIEnabled) { + if (serverConfig.webUIEnabled.value) { runBlocking { WebInterfaceManager.setupWebUI() } - logger.info { "Serving web static files for ${serverConfig.webUIFlavor}" } + logger.info { "Serving web static files for ${serverConfig.webUIFlavor.value}" } config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL) config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL) config.registerPlugin(OpenApiPlugin(getOpenApiOptions())) } + config.server { server } + config.enableCorsForAllOrigins() config.accessManager { handler, ctx, _ -> fun credentialsValid(): Boolean { val (username, password) = ctx.basicAuthCredentials() - return username == serverConfig.basicAuthUsername && password == serverConfig.basicAuthPassword + return username == serverConfig.basicAuthUsername.value && password == serverConfig.basicAuthPassword.value } - if (serverConfig.basicAuthEnabled && !(ctx.basicAuthCredentialsExist() && credentialsValid())) { + if (serverConfig.basicAuthEnabled.value && !(ctx.basicAuthCredentialsExist() && credentialsValid())) { ctx.header("WWW-Authenticate", "Basic") ctx.status(401).json("Unauthorized") } else { @@ -75,11 +99,11 @@ object JavalinSetup { } }.events { event -> event.serverStarted { - if (serverConfig.initialOpenInBrowserEnabled) { + if (serverConfig.initialOpenInBrowserEnabled.value) { Browser.openInBrowser() } } - }.start(serverConfig.ip, serverConfig.port) + }.start() // when JVM is prompted to shutdown, stop javalin gracefully Runtime.getRuntime().addShutdownHook( diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt index c3f0ce01..c699a732 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt @@ -8,58 +8,127 @@ package suwayomi.tachidesk.server * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import com.typesafe.config.Config +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule -import xyz.nulldev.ts.config.debugLogsEnabled +import kotlin.reflect.KProperty + +val mutableConfigValueScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private const val MODULE_NAME = "server" -class ServerConfig(getConfig: () -> Config, moduleName: String = MODULE_NAME) : SystemPropertyOverridableConfigModule(getConfig, moduleName) { - var ip: String by overridableConfig - var port: Int by overridableConfig +class ServerConfig(getConfig: () -> Config, val moduleName: String = MODULE_NAME) : SystemPropertyOverridableConfigModule(getConfig, moduleName) { + inner class OverrideConfigValue(private val configAdapter: ConfigAdapter) { + private var flow: MutableStateFlow? = null + + operator fun getValue(thisRef: ServerConfig, property: KProperty<*>): MutableStateFlow { + if (flow != null) { + return flow!! + } + + val value = configAdapter.toType(overridableConfig.getValue(thisRef, property)) + + val stateFlow = MutableStateFlow(value) + flow = stateFlow + + stateFlow.drop(1).distinctUntilChanged().onEach { + GlobalConfigManager.updateValue("$moduleName.${property.name}", it as Any) + }.launchIn(mutableConfigValueScope) + + return stateFlow + } + } + + val ip: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val port: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) // proxy - var socksProxyEnabled: Boolean by overridableConfig - var socksProxyHost: String by overridableConfig - var socksProxyPort: String by overridableConfig + val socksProxyEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val socksProxyHost: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val socksProxyPort: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) // webUI - var webUIEnabled: Boolean by overridableConfig - var webUIFlavor: String by overridableConfig - var initialOpenInBrowserEnabled: Boolean by overridableConfig - var webUIInterface: String by overridableConfig - var electronPath: String by overridableConfig - var webUIChannel: String by overridableConfig - var webUIUpdateCheckInterval: Double by overridableConfig + val webUIEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val webUIFlavor: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val initialOpenInBrowserEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val webUIInterface: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val electronPath: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val webUIChannel: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val webUIUpdateCheckInterval: MutableStateFlow by OverrideConfigValue(DoubleConfigAdapter) // downloader - var downloadAsCbz: Boolean by overridableConfig - var downloadsPath: String by overridableConfig - var autoDownloadNewChapters: Boolean by overridableConfig + val downloadAsCbz: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val downloadsPath: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val autoDownloadNewChapters: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + + // requests + val maxSourcesInParallel: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) // updater - var maxParallelUpdateRequests: Int by overridableConfig - var excludeUnreadChapters: Boolean by overridableConfig - var excludeNotStarted: Boolean by overridableConfig - var excludeCompleted: Boolean by overridableConfig - var globalUpdateInterval: Double by overridableConfig + val excludeUnreadChapters: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val excludeNotStarted: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val excludeCompleted: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val globalUpdateInterval: MutableStateFlow by OverrideConfigValue(DoubleConfigAdapter) // Authentication - var basicAuthEnabled: Boolean by overridableConfig - var basicAuthUsername: String by overridableConfig - var basicAuthPassword: String by overridableConfig + val basicAuthEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val basicAuthUsername: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val basicAuthPassword: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) // misc - var debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config) - var systemTrayEnabled: Boolean by overridableConfig + val debugLogsEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) + val systemTrayEnabled: MutableStateFlow by OverrideConfigValue(BooleanConfigAdapter) // backup - var backupPath: String by overridableConfig - var backupTime: String by overridableConfig - var backupInterval: Int by overridableConfig - var backupTTL: Int by overridableConfig + val backupPath: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val backupTime: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + val backupInterval: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) + val backupTTL: MutableStateFlow by OverrideConfigValue(IntConfigAdapter) // local source - var localSourcePath: String by overridableConfig + val localSourcePath: MutableStateFlow by OverrideConfigValue(StringConfigAdapter) + + fun subscribeTo(flow: Flow, onChange: suspend (value: T) -> Unit, ignoreInitialValue: Boolean = true) { + val actualFlow = if (ignoreInitialValue) { + flow.drop(1) + } else { + flow + } + + val sharedFlow = MutableSharedFlow(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + actualFlow.distinctUntilChanged().onEach { sharedFlow.emit(it) }.launchIn(mutableConfigValueScope) + sharedFlow.onEach { onChange(it) }.launchIn(mutableConfigValueScope) + } + + fun subscribeTo(flow: Flow, onChange: suspend () -> Unit, ignoreInitialValue: Boolean = true) { + subscribeTo(flow, { _ -> onChange() }, ignoreInitialValue) + } + + fun subscribeTo( + mutableStateFlow: MutableStateFlow, + onChange: suspend (value: T) -> Unit, + ignoreInitialValue: Boolean = true + ) { + subscribeTo(mutableStateFlow.asStateFlow(), onChange, ignoreInitialValue) + } + + fun subscribeTo( + mutableStateFlow: MutableStateFlow, + onChange: suspend () -> Unit, + ignoreInitialValue: Boolean = true + ) { + subscribeTo(mutableStateFlow.asStateFlow(), { _ -> onChange() }, ignoreInitialValue) + } companion object { fun register(getConfig: () -> Config) = ServerConfig({ getConfig().getConfig(MODULE_NAME) }) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt index f504afb3..d5f33091 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/ServerSetup.kt @@ -7,11 +7,13 @@ package suwayomi.tachidesk.server * 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 ch.qos.logback.classic.Level import com.typesafe.config.ConfigRenderOptions import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.source.local.LocalSource import io.javalin.plugin.json.JavalinJackson import io.javalin.plugin.json.JsonMapper +import kotlinx.coroutines.flow.combine import kotlinx.serialization.json.Json import mu.KotlinLogging import org.bouncycastle.jce.provider.BouncyCastleProvider @@ -26,13 +28,14 @@ import suwayomi.tachidesk.manga.impl.update.Updater import suwayomi.tachidesk.manga.impl.util.lang.renameTo import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex -import suwayomi.tachidesk.server.util.SystemTray.systemTray +import suwayomi.tachidesk.server.util.SystemTray import xyz.nulldev.androidcompat.AndroidCompat import xyz.nulldev.androidcompat.AndroidCompatInitializer import xyz.nulldev.ts.config.ApplicationRootDir import xyz.nulldev.ts.config.ConfigKodeinModule import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.initLoggerConfig +import xyz.nulldev.ts.config.setLogLevel import java.io.File import java.security.Security import java.util.Locale @@ -43,29 +46,25 @@ class ApplicationDirs( val dataRoot: String = ApplicationRootDir, val tempRoot: String = "${System.getProperty("java.io.tmpdir")}/Tachidesk" ) { - val cacheRoot = System.getProperty("java.io.tmpdir") + "/tachidesk" val extensionsRoot = "$dataRoot/extensions" - val downloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" } - val localMangaRoot = serverConfig.localSourcePath.ifBlank { "$dataRoot/local" } + val downloadsRoot get() = serverConfig.downloadsPath.value.ifBlank { "$dataRoot/downloads" } + val localMangaRoot get() = serverConfig.localSourcePath.value.ifBlank { "$dataRoot/local" } val webUIRoot = "$dataRoot/webUI" - val automatedBackupRoot = serverConfig.backupPath.ifBlank { "$dataRoot/backups" } + val automatedBackupRoot get() = serverConfig.backupPath.value.ifBlank { "$dataRoot/backups" } val tempThumbnailCacheRoot = "$tempRoot/thumbnails" val tempMangaCacheRoot = "$tempRoot/manga-cache" - val thumbnailDownloadsRoot = "$downloadsRoot/thumbnails" - val mangaDownloadsRoot = "$downloadsRoot/mangas" + val thumbnailDownloadsRoot get() = "$downloadsRoot/thumbnails" + val mangaDownloadsRoot get() = "$downloadsRoot/mangas" } val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() } -val systemTrayInstance by lazy { systemTray() } - val androidCompat by lazy { AndroidCompat() } fun applicationSetup() { - Thread.setDefaultUncaughtExceptionHandler { - _, throwable -> + Thread.setDefaultUncaughtExceptionHandler { _, throwable -> KotlinLogging.logger { }.error(throwable) { "unhandled exception" } } @@ -74,6 +73,14 @@ fun applicationSetup() { ServerConfig.register { GlobalConfigManager.config } ) + serverConfig.subscribeTo(serverConfig.debugLogsEnabled, { debugLogsEnabled -> + if (debugLogsEnabled) { + setLogLevel(Level.DEBUG) + } else { + setLogLevel(Level.INFO) + } + }) + // Application dirs val applicationDirs = ApplicationDirs() @@ -164,13 +171,17 @@ fun applicationSetup() { LocalSource.register() // create system tray - if (serverConfig.systemTrayEnabled) { + serverConfig.subscribeTo(serverConfig.systemTrayEnabled, { systemTrayEnabled -> try { - systemTrayInstance + if (systemTrayEnabled) { + SystemTray.create() + } else { + SystemTray.remove() + } } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error e.printStackTrace() } - } + }, ignoreInitialValue = false) // Disable jetty's logging System.setProperty("org.eclipse.jetty.util.log.announce", "false") @@ -178,11 +189,25 @@ fun applicationSetup() { System.setProperty("org.eclipse.jetty.LEVEL", "OFF") // socks proxy settings - if (serverConfig.socksProxyEnabled) { - System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost - System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort - logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") - } + serverConfig.subscribeTo( + combine( + serverConfig.socksProxyEnabled, + serverConfig.socksProxyHost, + serverConfig.socksProxyPort + ) { proxyEnabled, proxyHost, proxyPort -> + Triple(proxyEnabled, proxyHost, proxyPort) + }, + { (proxyEnabled, proxyHost, proxyPort) -> + logger.info("Socks Proxy changed - enabled= $proxyEnabled, proxy= $proxyHost:$proxyPort") + if (proxyEnabled) { + System.getProperties()["socksProxyHost"] = proxyHost + System.getProperties()["socksProxyPort"] = proxyPort + } else { + System.getProperties()["socksProxyHost"] = "" + System.getProperties()["socksProxyPort"] = "" + } + } + ) // AES/CBC/PKCS7Padding Cypher provider for zh.copymanga Security.addProvider(BouncyCastleProvider()) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/AppMutex.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/AppMutex.kt index 7f0f3671..4b1120ae 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/AppMutex.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/AppMutex.kt @@ -31,7 +31,7 @@ object AppMutex { OtherApplicationRunning(2) } - private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip + private val appIP = if (serverConfig.ip.value == "0.0.0.0") "127.0.0.1" else serverConfig.ip.value private val jsonMapper by DI.global.instance() @@ -41,7 +41,7 @@ object AppMutex { .build() val request = Builder() - .url("http://$appIP:${serverConfig.port}/api/v1/settings/about/") + .url("http://$appIP:${serverConfig.port.value}/api/v1/settings/about/") .build() val response = try { @@ -64,7 +64,7 @@ object AppMutex { logger.info("Mutex status is clear, Resuming startup.") } AppMutexState.TachideskInstanceRunning -> { - logger.info("Another instance of Tachidesk is running on $appIP:${serverConfig.port}") + logger.info("Another instance of Tachidesk is running on $appIP:${serverConfig.port.value}") logger.info("Probably user thought tachidesk is closed so, opening webUI in browser again.") openInBrowser() @@ -74,7 +74,7 @@ object AppMutex { shutdownApp(MutexCheckFailedTachideskRunning) } AppMutexState.OtherApplicationRunning -> { - logger.error("A non Tachidesk application is running on $appIP:${serverConfig.port}, aborting startup.") + logger.error("A non Tachidesk application is running on $appIP:${serverConfig.port.value}, aborting startup.") shutdownApp(MutexCheckFailedAnotherAppRunning) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt index e055e64e..c87f3de4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/Browser.kt @@ -11,16 +11,20 @@ import dorkbox.desktop.Desktop import suwayomi.tachidesk.server.serverConfig object Browser { - private val appIP = if (serverConfig.ip == "0.0.0.0") "127.0.0.1" else serverConfig.ip - private val appBaseUrl = "http://$appIP:${serverConfig.port}" - private val electronInstances = mutableListOf() + private fun getAppBaseUrl(): String { + val appIP = if (serverConfig.ip.value == "0.0.0.0") "127.0.0.1" else serverConfig.ip.value + return "http://$appIP:${serverConfig.port.value}" + } + fun openInBrowser() { - if (serverConfig.webUIEnabled) { - if (serverConfig.webUIInterface == ("electron")) { + if (serverConfig.webUIEnabled.value) { + val appBaseUrl = getAppBaseUrl() + + if (serverConfig.webUIInterface.value == ("electron")) { try { - val electronPath = serverConfig.electronPath + val electronPath = serverConfig.electronPath.value electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start()) } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error e.printStackTrace() diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/SystemTray.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/SystemTray.kt index 978f6307..6bbc70be 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/SystemTray.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/SystemTray.kt @@ -17,10 +17,16 @@ import suwayomi.tachidesk.server.util.Browser.openInBrowser import suwayomi.tachidesk.server.util.ExitCode.Success object SystemTray { - fun systemTray(): SystemTray? { - try { + private var instance: SystemTray? = null + + fun create() { + instance = try { // ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java - SystemTray.DEBUG = serverConfig.debugLogsEnabled + serverConfig.subscribeTo( + serverConfig.debugLogsEnabled, + { debugLogsEnabled -> SystemTray.DEBUG = debugLogsEnabled }, + ignoreInitialValue = false + ) CacheUtil.clear(BuildConfig.NAME) @@ -28,7 +34,7 @@ object SystemTray { SystemTray.FORCE_TRAY_TYPE = SystemTray.TrayType.Awt } - val systemTray = SystemTray.get(BuildConfig.NAME) ?: return null + val systemTray = SystemTray.get(BuildConfig.NAME) val mainMenu = systemTray.menu mainMenu.add( @@ -51,10 +57,15 @@ object SystemTray { } ) - return systemTray + systemTray } catch (e: Exception) { e.printStackTrace() - return null + null } } + + fun remove() { + instance?.remove() + instance = null + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt index cafd2892..856e7341 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/util/WebInterfaceManager.kt @@ -16,11 +16,11 @@ import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonObject @@ -70,7 +70,7 @@ enum class WebUIChannel { companion object { fun doesConfigChannelEqual(channel: WebUIChannel): Boolean { - return serverConfig.webUIChannel.equals(channel.toString(), true) + return serverConfig.webUIChannel.value.equals(channel.toString(), true) } } } @@ -112,7 +112,7 @@ object WebInterfaceManager { SharingStarted.Eagerly, WebUIUpdateStatus( info = WebUIUpdateInfo( - channel = serverConfig.webUIChannel, + channel = serverConfig.webUIChannel.value, tag = "", updateAvailable = false ), @@ -122,27 +122,36 @@ object WebInterfaceManager { ) init { - scheduleWebUIUpdateCheck() + serverConfig.subscribeTo( + combine(serverConfig.webUIUpdateCheckInterval, serverConfig.webUIFlavor) { interval, flavor -> + Pair( + interval, + flavor + ) + }, + ::scheduleWebUIUpdateCheck, + ignoreInitialValue = false + ) } private fun isAutoUpdateEnabled(): Boolean { - return serverConfig.webUIUpdateCheckInterval.toInt() != 0 + return serverConfig.webUIUpdateCheckInterval.value.toInt() != 0 } private fun scheduleWebUIUpdateCheck() { HAScheduler.descheduleCron(currentUpdateTaskId) - val isAutoUpdateDisabled = !isAutoUpdateEnabled() || serverConfig.webUIFlavor == "Custom" + val isAutoUpdateDisabled = !isAutoUpdateEnabled() || serverConfig.webUIFlavor.value == "Custom" if (isAutoUpdateDisabled) { return } - val updateInterval = serverConfig.webUIUpdateCheckInterval.hours.coerceAtLeast(1.hours).coerceAtMost(23.hours) + val updateInterval = serverConfig.webUIUpdateCheckInterval.value.hours.coerceAtLeast(1.hours).coerceAtMost(23.hours) val lastAutomatedUpdate = preferences.getLong(lastWebUIUpdateCheckKey, System.currentTimeMillis()) val task = { logger.debug { - "Checking for webUI update (channel= ${serverConfig.webUIChannel}, interval= ${serverConfig.webUIUpdateCheckInterval}h, lastAutomatedUpdate= ${ + "Checking for webUI update (channel= ${serverConfig.webUIChannel.value}, interval= ${serverConfig.webUIUpdateCheckInterval.value}h, lastAutomatedUpdate= ${ Date( lastAutomatedUpdate ) @@ -165,14 +174,14 @@ object WebInterfaceManager { } suspend fun setupWebUI() { - if (serverConfig.webUIFlavor == "Custom") { + if (serverConfig.webUIFlavor.value == "Custom") { return } if (doesLocalWebUIExist(applicationDirs.webUIRoot)) { val currentVersion = getLocalVersion() - logger.info { "setupWebUI: found webUI files - flavor= ${serverConfig.webUIFlavor}, version= $currentVersion" } + logger.info { "setupWebUI: found webUI files - flavor= ${serverConfig.webUIFlavor.value}, version= $currentVersion" } if (!isLocalWebUIValid(applicationDirs.webUIRoot)) { doInitialSetup() @@ -186,7 +195,7 @@ object WebInterfaceManager { // check if the bundled webUI version is a newer version than the current used version // this could be the case in case no compatible webUI version is available and a newer server version was installed val shouldUpdateToBundledVersion = - serverConfig.webUIFlavor == DEFAULT_WEB_UI && extractVersion(getLocalVersion()) < extractVersion( + serverConfig.webUIFlavor.value == DEFAULT_WEB_UI && extractVersion(getLocalVersion()) < extractVersion( BuildConfig.WEBUI_TAG ) if (shouldUpdateToBundledVersion) { @@ -232,10 +241,10 @@ object WebInterfaceManager { return } - if (serverConfig.webUIFlavor != DEFAULT_WEB_UI) { + if (serverConfig.webUIFlavor.value != DEFAULT_WEB_UI) { logger.warn { "doInitialSetup: fallback to default webUI \"$DEFAULT_WEB_UI\"" } - serverConfig.webUIFlavor = DEFAULT_WEB_UI + serverConfig.webUIFlavor.value = DEFAULT_WEB_UI val fallbackToBundledVersion = !doDownload() { getLatestCompatibleVersion() } if (!fallbackToBundledVersion) { @@ -287,11 +296,11 @@ object WebInterfaceManager { val localVersion = getLocalVersion() if (!isUpdateAvailable(localVersion).second) { - logger.debug { "checkForUpdate(${serverConfig.webUIFlavor}, $localVersion): local version is the latest one" } + logger.debug { "checkForUpdate(${serverConfig.webUIFlavor.value}, $localVersion): local version is the latest one" } return } - logger.info { "checkForUpdate(${serverConfig.webUIFlavor}, $localVersion): An update is available, starting download..." } + logger.info { "checkForUpdate(${serverConfig.webUIFlavor.value}, $localVersion): An update is available, starting download..." } try { downloadVersion(getLatestCompatibleVersion()) } catch (e: Exception) { @@ -416,7 +425,7 @@ object WebInterfaceManager { val currentServerVersionNumber = extractVersion(BuildConfig.REVISION) val webUIToServerVersionMappings = fetchServerMappingFile() - logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" } + logger.debug { "getLatestCompatibleVersion: webUIChannel= ${serverConfig.webUIChannel.value}, currentServerVersion= ${BuildConfig.REVISION}, mappingFile= $webUIToServerVersionMappings" } for (i in 0 until webUIToServerVersionMappings.size) { val webUIToServerVersionEntry = webUIToServerVersionMappings[i].jsonObject @@ -446,7 +455,7 @@ object WebInterfaceManager { notifyFlow.emit( WebUIUpdateStatus( info = WebUIUpdateInfo( - channel = serverConfig.webUIChannel, + channel = serverConfig.webUIChannel.value, tag = version, updateAvailable = true ), @@ -472,7 +481,7 @@ object WebInterfaceManager { val webUIZipURL = "${getDownloadUrlFor(version)}/$webUIZip" val log = - KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${serverConfig.webUIFlavor})") + KotlinLogging.logger("${logger.name} downloadVersion(version= $version, flavor= ${serverConfig.webUIFlavor.value})") log.info { "Downloading WebUI zip from the Internet..." } executeWithRetry(log, { diff --git a/server/src/main/resources/server-reference.conf b/server/src/main/resources/server-reference.conf index 18df8771..db38de15 100644 --- a/server/src/main/resources/server-reference.conf +++ b/server/src/main/resources/server-reference.conf @@ -21,8 +21,10 @@ server.downloadAsCbz = false server.downloadsPath = "" server.autoDownloadNewChapters = false # if new chapters that have been retrieved should get automatically downloaded +# 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.maxParallelUpdateRequests = 10 # sets how many sources can be updated in parallel. updates are grouped by source and all mangas of a source are updated synchronously server.excludeUnreadChapters = true server.excludeNotStarted = true server.excludeCompleted = true diff --git a/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt b/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt index f723e0ff..cde63aa2 100644 --- a/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt +++ b/server/src/test/kotlin/suwayomi/tachidesk/test/ApplicationTest.kt @@ -26,8 +26,8 @@ import suwayomi.tachidesk.server.ServerConfig import suwayomi.tachidesk.server.androidCompat import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.serverConfig -import suwayomi.tachidesk.server.systemTrayInstance import suwayomi.tachidesk.server.util.AppMutex +import suwayomi.tachidesk.server.util.SystemTray import xyz.nulldev.androidcompat.AndroidCompatInitializer import xyz.nulldev.ts.config.CONFIG_PREFIX import xyz.nulldev.ts.config.ConfigKodeinModule @@ -83,7 +83,7 @@ open class ApplicationTest { // register Tachidesk's config which is dubbed "ServerConfig" GlobalConfigManager.registerModule( - ServerConfig.register(GlobalConfigManager.config) + ServerConfig.register { GlobalConfigManager.config } ) // Make sure only one instance of the app is running @@ -125,9 +125,9 @@ open class ApplicationTest { } // create system tray - if (serverConfig.systemTrayEnabled) { + if (serverConfig.systemTrayEnabled.value) { try { - systemTrayInstance + SystemTray.create() } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error e.printStackTrace() } @@ -139,10 +139,10 @@ open class ApplicationTest { System.setProperty("org.eclipse.jetty.LEVEL", "OFF") // socks proxy settings - if (serverConfig.socksProxyEnabled) { - System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost - System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort - logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") + if (serverConfig.socksProxyEnabled.value) { + System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost.value + System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort.value + logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost.value}:${serverConfig.socksProxyPort.value}") } } diff --git a/server/src/test/resources/server-reference.conf b/server/src/test/resources/server-reference.conf index 01057e9a..9d5dc26f 100644 --- a/server/src/test/resources/server-reference.conf +++ b/server/src/test/resources/server-reference.conf @@ -11,8 +11,10 @@ server.socksProxyPort = "" server.downloadAsCbz = false server.autoDownloadNewChapters = false +# requests +server.maxSourcesInParallel = 10 + # updater -server.maxParallelUpdateRequests = 10 server.excludeUnreadChapters = true server.excludeNotStarted = true server.excludeCompleted = true