mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01:00
Feature/listen to server config value changes (#617)
* Make server config value changes subscribable * Make server config value changes subscribable - Update usage * Add util functions to listen to server config value changes * Listen to server config value changes - Auto backups * Listen to server config value changes - Auto global update * Listen to server config value changes - WebUI auto updates * Listen to server config value changes - Javalin update ip and port * Listen to server config value changes - Update socks proxy * Listen to server config value changes - Update debug log level * Listen to server config value changes - Update system tray icon * Update config values one at a time In case settings are changed in quick succession it's possible that each setting update reverts the change of the previous changed setting because the internal config hasn't been updated yet. E.g. 1. settingA changed 2. settingB changed 3. settingA updates config file 4. settingB updates config file (internal config hasn't been updated yet with change from settingA) 5. settingA updates internal config (settingA updated) 6. settingB updates internal config (settingB updated, settingA outdated) now settingA is unchanged because settingB reverted its change while updating the config with its new value * Always add log interceptor to OkHttpClient In case debug logs are disabled then the KotlinLogging log level will be set to level > debug and thus, these logs won't get logged * Rename "maxParallelUpdateRequests" to "maxSourcesInParallel" * Use server setting "maxSourcesInParallel" for downloads * Listen to server config value changes - downloads * Always use latest server settings - Browser * Always use latest server settings - folders * [Test] Fix type error
This commit is contained in:
@@ -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<Class<out ConfigModule>, ConfigModule>
|
||||
get() = generatedModules
|
||||
|
||||
private val mutex = Mutex()
|
||||
|
||||
/**
|
||||
* Get a config module
|
||||
*/
|
||||
@@ -98,12 +102,14 @@ open class ConfigManager {
|
||||
userConfigFile.writeText(newFileContent)
|
||||
}
|
||||
|
||||
fun updateValue(path: String, value: Any) {
|
||||
suspend fun updateValue(path: String, value: Any) {
|
||||
mutex.withLock {
|
||||
val configValue = ConfigValueFactory.fromAnyRef(value)
|
||||
|
||||
updateUserConfigFile(path, configValue)
|
||||
internalConfig = internalConfig.withValue(path, configValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure the "UserConfig" is up-to-date.
|
||||
|
||||
@@ -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 <R> setValue(thisRef: R, property: KProperty<*>, value: Any) {
|
||||
GlobalConfigManager.updateValue("$moduleName.${property.name}", value)
|
||||
}
|
||||
|
||||
inline operator fun <R, reified T> getValue(thisRef: R, property: KProperty<*>): T {
|
||||
val configValue: T = getConfig().getValue(thisRef, property)
|
||||
|
||||
|
||||
@@ -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,7 +52,6 @@ class NetworkHelper(context: Context) {
|
||||
.callTimeout(2, TimeUnit.MINUTES)
|
||||
.addInterceptor(UserAgentInterceptor())
|
||||
|
||||
if (serverConfig.debugLogsEnabled) {
|
||||
val httpLoggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
|
||||
val logger = KotlinLogging.logger { }
|
||||
|
||||
@@ -64,7 +62,6 @@ class NetworkHelper(context: Context) {
|
||||
level = HttpLoggingInterceptor.Level.BASIC
|
||||
}
|
||||
builder.addInterceptor(httpLoggingInterceptor)
|
||||
}
|
||||
|
||||
// when (preferences.dohProvider()) {
|
||||
// PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -55,7 +55,7 @@ class InfoQuery {
|
||||
return future {
|
||||
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable()
|
||||
WebUIUpdateInfo(
|
||||
channel = serverConfig.webUIChannel,
|
||||
channel = serverConfig.webUIChannel.value,
|
||||
tag = version,
|
||||
updateAvailable
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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<String, WsContext>()
|
||||
@@ -169,6 +168,22 @@ object DownloadManager {
|
||||
|
||||
private val downloaderWatch = MutableSharedFlow<Unit>(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()
|
||||
|
||||
@@ -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<Int, UpdateJob>()
|
||||
private val updateChannels = ConcurrentHashMap<String, Channel<UpdateJob>>()
|
||||
|
||||
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()
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package suwayomi.tachidesk.server
|
||||
|
||||
interface ConfigAdapter<T> {
|
||||
fun toType(configValue: String): T
|
||||
}
|
||||
|
||||
object StringConfigAdapter : ConfigAdapter<String> {
|
||||
override fun toType(configValue: String): String {
|
||||
return configValue
|
||||
}
|
||||
}
|
||||
|
||||
object IntConfigAdapter : ConfigAdapter<Int> {
|
||||
override fun toType(configValue: String): Int {
|
||||
return configValue.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
object BooleanConfigAdapter : ConfigAdapter<Boolean> {
|
||||
override fun toType(configValue: String): Boolean {
|
||||
return configValue.toBoolean()
|
||||
}
|
||||
}
|
||||
|
||||
object DoubleConfigAdapter : ConfigAdapter<Double> {
|
||||
override fun toType(configValue: String): Double {
|
||||
return configValue.toDouble()
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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<T>(private val configAdapter: ConfigAdapter<T>) {
|
||||
private var flow: MutableStateFlow<T>? = null
|
||||
|
||||
operator fun getValue(thisRef: ServerConfig, property: KProperty<*>): MutableStateFlow<T> {
|
||||
if (flow != null) {
|
||||
return flow!!
|
||||
}
|
||||
|
||||
val value = configAdapter.toType(overridableConfig.getValue<ServerConfig, String>(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<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val port: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
|
||||
// proxy
|
||||
var socksProxyEnabled: Boolean by overridableConfig
|
||||
var socksProxyHost: String by overridableConfig
|
||||
var socksProxyPort: String by overridableConfig
|
||||
val socksProxyEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val socksProxyHost: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val socksProxyPort: MutableStateFlow<String> 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<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val webUIFlavor: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val initialOpenInBrowserEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val webUIInterface: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val electronPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val webUIChannel: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val webUIUpdateCheckInterval: MutableStateFlow<Double> by OverrideConfigValue(DoubleConfigAdapter)
|
||||
|
||||
// downloader
|
||||
var downloadAsCbz: Boolean by overridableConfig
|
||||
var downloadsPath: String by overridableConfig
|
||||
var autoDownloadNewChapters: Boolean by overridableConfig
|
||||
val downloadAsCbz: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val downloadsPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val autoDownloadNewChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
|
||||
// requests
|
||||
val maxSourcesInParallel: MutableStateFlow<Int> 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<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val excludeNotStarted: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val excludeCompleted: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val globalUpdateInterval: MutableStateFlow<Double> by OverrideConfigValue(DoubleConfigAdapter)
|
||||
|
||||
// Authentication
|
||||
var basicAuthEnabled: Boolean by overridableConfig
|
||||
var basicAuthUsername: String by overridableConfig
|
||||
var basicAuthPassword: String by overridableConfig
|
||||
val basicAuthEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val basicAuthUsername: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val basicAuthPassword: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
|
||||
// misc
|
||||
var debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config)
|
||||
var systemTrayEnabled: Boolean by overridableConfig
|
||||
val debugLogsEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val systemTrayEnabled: MutableStateFlow<Boolean> 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<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val backupTime: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val backupInterval: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val backupTTL: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
|
||||
// local source
|
||||
var localSourcePath: String by overridableConfig
|
||||
val localSourcePath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
|
||||
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().onEach { 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(MODULE_NAME) })
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<JsonMapper>()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Any>()
|
||||
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user