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:
schroda
2023-08-12 17:47:41 +02:00
committed by GitHub
parent 01ab912bd9
commit 321fbe22dd
22 changed files with 368 additions and 143 deletions

View File

@@ -14,6 +14,8 @@ import com.typesafe.config.ConfigValue
import com.typesafe.config.ConfigValueFactory import com.typesafe.config.ConfigValueFactory
import com.typesafe.config.parser.ConfigDocument import com.typesafe.config.parser.ConfigDocument
import com.typesafe.config.parser.ConfigDocumentFactory import com.typesafe.config.parser.ConfigDocumentFactory
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import mu.KotlinLogging import mu.KotlinLogging
import java.io.File import java.io.File
@@ -32,6 +34,8 @@ open class ConfigManager {
val loadedModules: Map<Class<out ConfigModule>, ConfigModule> val loadedModules: Map<Class<out ConfigModule>, ConfigModule>
get() = generatedModules get() = generatedModules
private val mutex = Mutex()
/** /**
* Get a config module * Get a config module
*/ */
@@ -98,11 +102,13 @@ open class ConfigManager {
userConfigFile.writeText(newFileContent) userConfigFile.writeText(newFileContent)
} }
fun updateValue(path: String, value: Any) { suspend fun updateValue(path: String, value: Any) {
val configValue = ConfigValueFactory.fromAnyRef(value) mutex.withLock {
val configValue = ConfigValueFactory.fromAnyRef(value)
updateUserConfigFile(path, configValue) updateUserConfigFile(path, configValue)
internalConfig = internalConfig.withValue(path, configValue) internalConfig = internalConfig.withValue(path, configValue)
}
} }
/** /**

View File

@@ -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] */ /** Defines a config property that is overridable with jvm `-D` commandline arguments prefixed with [CONFIG_PREFIX] */
class SystemPropertyOverrideDelegate(val getConfig: () -> Config, val moduleName: String) { 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 { inline operator fun <R, reified T> getValue(thisRef: R, property: KProperty<*>): T {
val configValue: T = getConfig().getValue(thisRef, property) val configValue: T = getConfig().getValue(thisRef, property)

View File

@@ -20,7 +20,6 @@ import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import mu.KotlinLogging import mu.KotlinLogging
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import suwayomi.tachidesk.server.serverConfig
import java.net.CookieHandler import java.net.CookieHandler
import java.net.CookieManager import java.net.CookieManager
import java.net.CookiePolicy import java.net.CookiePolicy
@@ -53,18 +52,16 @@ class NetworkHelper(context: Context) {
.callTimeout(2, TimeUnit.MINUTES) .callTimeout(2, TimeUnit.MINUTES)
.addInterceptor(UserAgentInterceptor()) .addInterceptor(UserAgentInterceptor())
if (serverConfig.debugLogsEnabled) { val httpLoggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
val httpLoggingInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger { val logger = KotlinLogging.logger { }
val logger = KotlinLogging.logger { }
override fun log(message: String) { override fun log(message: String) {
logger.debug { message } logger.debug { message }
}
}).apply {
level = HttpLoggingInterceptor.Level.BASIC
} }
builder.addInterceptor(httpLoggingInterceptor) }).apply {
level = HttpLoggingInterceptor.Level.BASIC
} }
builder.addInterceptor(httpLoggingInterceptor)
// when (preferences.dohProvider()) { // when (preferences.dohProvider()) {
// PREF_DOH_CLOUDFLARE -> builder.dohCloudflare() // PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()

View File

@@ -87,8 +87,8 @@ object CFClearance {
LaunchOptions() LaunchOptions()
.setHeadless(false) .setHeadless(false)
.apply { .apply {
if (serverConfig.socksProxyEnabled) { if (serverConfig.socksProxyEnabled.value) {
setProxy("socks5://${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") setProxy("socks5://${serverConfig.socksProxyHost.value}:${serverConfig.socksProxyPort.value}")
} }
} }
).use { browser -> ).use { browser ->

View File

@@ -3,7 +3,6 @@ package suwayomi.tachidesk.graphql.mutations
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout import kotlinx.coroutines.withTimeout
import suwayomi.tachidesk.graphql.types.UpdateState.DOWNLOADING 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.UpdateState.STOPPED
import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo import suwayomi.tachidesk.graphql.types.WebUIUpdateInfo
import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus import suwayomi.tachidesk.graphql.types.WebUIUpdateStatus
@@ -37,7 +36,7 @@ class InfoMutation {
input.clientMutationId, input.clientMutationId,
WebUIUpdateStatus( WebUIUpdateStatus(
info = WebUIUpdateInfo( info = WebUIUpdateInfo(
channel = serverConfig.webUIChannel, channel = serverConfig.webUIChannel.value,
tag = version, tag = version,
updateAvailable updateAvailable
), ),

View File

@@ -55,7 +55,7 @@ class InfoQuery {
return future { return future {
val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable() val (version, updateAvailable) = WebInterfaceManager.isUpdateAvailable()
WebUIUpdateInfo( WebUIUpdateInfo(
channel = serverConfig.webUIChannel, channel = serverConfig.webUIChannel.value,
tag = version, tag = version,
updateAvailable updateAvailable
) )

View File

@@ -210,7 +210,7 @@ object Chapter {
val wasInitialFetch = currentNumberOfChapters == 0 val wasInitialFetch = currentNumberOfChapters == 0
// make sure to ignore initial fetch // make sure to ignore initial fetch
val downloadNewChapters = serverConfig.autoDownloadNewChapters && !wasInitialFetch && areNewChaptersAvailable val downloadNewChapters = serverConfig.autoDownloadNewChapters.value && !wasInitialFetch && areNewChaptersAvailable
if (!downloadNewChapters) { if (!downloadNewChapters) {
return return
} }

View File

@@ -35,7 +35,7 @@ object ChapterDownloadHelper {
val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId)) val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
val cbzFile = File(getChapterCbzPath(mangaId, chapterId)) val cbzFile = File(getChapterCbzPath(mangaId, chapterId))
if (cbzFile.exists()) return ArchiveProvider(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) return FolderProvider(mangaId, chapterId)
} }
} }

View File

@@ -8,6 +8,7 @@ package suwayomi.tachidesk.manga.impl.backup.proto
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import eu.kanade.tachiyomi.source.model.UpdateStrategy import eu.kanade.tachiyomi.source.model.UpdateStrategy
import kotlinx.coroutines.flow.combine
import mu.KotlinLogging import mu.KotlinLogging
import okio.buffer import okio.buffer
import okio.gzip import okio.gzip
@@ -53,10 +54,22 @@ object ProtoBackupExport : ProtoBackupBase() {
private const val lastAutomatedBackupKey = "lastAutomatedBackupKey" private const val lastAutomatedBackupKey = "lastAutomatedBackupKey"
private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java) private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java)
init {
serverConfig.subscribeTo(
combine(serverConfig.backupInterval, serverConfig.backupTime) { interval, timeOfDay ->
Pair(
interval,
timeOfDay
)
},
::scheduleAutomatedBackupTask
)
}
fun scheduleAutomatedBackupTask() { fun scheduleAutomatedBackupTask() {
HAScheduler.descheduleCron(backupSchedulerJobId) HAScheduler.descheduleCron(backupSchedulerJobId)
val areAutomatedBackupsDisabled = serverConfig.backupInterval == 0 val areAutomatedBackupsDisabled = serverConfig.backupInterval.value == 0
if (areAutomatedBackupsDisabled) { if (areAutomatedBackupsDisabled) {
return return
} }
@@ -67,10 +80,10 @@ object ProtoBackupExport : ProtoBackupBase() {
preferences.putLong(lastAutomatedBackupKey, System.currentTimeMillis()) 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 backupHour = hour.coerceAtLeast(0).coerceAtMost(23)
val backupMinute = minute.coerceAtLeast(0).coerceAtMost(59) 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 // trigger last backup in case the server wasn't running on the scheduled time
val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, System.currentTimeMillis()) val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, System.currentTimeMillis())
@@ -105,9 +118,9 @@ object ProtoBackupExport : ProtoBackupBase() {
} }
private fun cleanupAutomatedBackups() { 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) { if (isCleanupDisabled) {
return return
} }
@@ -133,7 +146,7 @@ object ProtoBackupExport : ProtoBackupBase() {
val lastAccessTime = file.lastModified() val lastAccessTime = file.lastModified()
val isTTLReached = 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) { if (isTTLReached) {
file.delete() file.delete()
} }

View File

@@ -42,6 +42,7 @@ import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.manga.model.table.ChapterTable import suwayomi.tachidesk.manga.model.table.ChapterTable
import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.MangaTable
import suwayomi.tachidesk.manga.model.table.toDataClass import suwayomi.tachidesk.manga.model.table.toDataClass
import suwayomi.tachidesk.server.serverConfig
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@@ -51,8 +52,6 @@ import kotlin.time.Duration.Companion.seconds
private val logger = KotlinLogging.logger {} private val logger = KotlinLogging.logger {}
private const val MAX_SOURCES_IN_PARAllEL = 5
object DownloadManager { object DownloadManager {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
private val clients = ConcurrentHashMap<String, WsContext>() private val clients = ConcurrentHashMap<String, WsContext>()
@@ -169,6 +168,22 @@ object DownloadManager {
private val downloaderWatch = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val downloaderWatch = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
init { 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 { scope.launch {
downloaderWatch.sample(1.seconds).collect { downloaderWatch.sample(1.seconds).collect {
val runningDownloaders = downloaders.values.filter { it.isActive } 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}" } 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() availableDownloads.asSequence()
.map { it.manga.sourceId } .map { it.manga.sourceId }
.distinct() .distinct()
.minus( .minus(
runningDownloaders.map { it.sourceId }.toSet() runningDownloaders.map { it.sourceId }.toSet()
) )
.take(MAX_SOURCES_IN_PARAllEL - runningDownloaders.size) .take(serverConfig.maxSourcesInParallel.value - runningDownloaders.size)
.map { getDownloader(it) } .map { getDownloader(it) }
.forEach { .forEach {
it.start() it.start()

View File

@@ -33,6 +33,7 @@ import suwayomi.tachidesk.util.HAScheduler
import java.util.Date import java.util.Date
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.prefs.Preferences import java.util.prefs.Preferences
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.hours
class Updater : IUpdater { class Updater : IUpdater {
@@ -45,13 +46,36 @@ class Updater : IUpdater {
private val tracker = ConcurrentHashMap<Int, UpdateJob>() private val tracker = ConcurrentHashMap<Int, UpdateJob>()
private val updateChannels = ConcurrentHashMap<String, Channel<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 lastAutomatedUpdateKey = "lastAutomatedUpdateKey"
private val preferences = Preferences.userNodeForPackage(Updater::class.java) private val preferences = Preferences.userNodeForPackage(Updater::class.java)
private var currentUpdateTaskId = "" 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() { private fun autoUpdateTask() {
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0) val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis()) preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis())
@@ -61,19 +85,19 @@ class Updater : IUpdater {
return 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) addCategoriesToUpdateQueue(Category.getCategoryList(), clear = true, forceAll = false)
} }
fun scheduleUpdateTask() { fun scheduleUpdateTask() {
HAScheduler.deschedule(currentUpdateTaskId) HAScheduler.deschedule(currentUpdateTaskId)
val isAutoUpdateDisabled = serverConfig.globalUpdateInterval == 0.0 val isAutoUpdateDisabled = serverConfig.globalUpdateInterval.value == 0.0
if (isAutoUpdateDisabled) { if (isAutoUpdateDisabled) {
return 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 lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
val timeToNextExecution = (updateInterval - (System.currentTimeMillis() - lastAutomatedUpdate)).mod(updateInterval) val timeToNextExecution = (updateInterval - (System.currentTimeMillis() - lastAutomatedUpdate)).mod(updateInterval)
@@ -150,9 +174,9 @@ class Updater : IUpdater {
val mangasToUpdate = categoriesToUpdateMangas val mangasToUpdate = categoriesToUpdateMangas
.asSequence() .asSequence()
.filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE } .filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE }
.filter { if (serverConfig.excludeUnreadChapters) { (it.unreadCount ?: 0L) == 0L } else true } .filter { if (serverConfig.excludeUnreadChapters.value) { (it.unreadCount ?: 0L) == 0L } else true }
.filter { if (serverConfig.excludeNotStarted) { it.lastReadAt != null } else true } .filter { if (serverConfig.excludeNotStarted.value) { it.lastReadAt != null } else true }
.filter { if (serverConfig.excludeCompleted) { it.status != MangaStatus.COMPLETED.name } 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 } } .filter { forceAll || !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } }
.toList() .toList()

View File

@@ -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()
}
}

View File

@@ -18,9 +18,12 @@ import io.swagger.v3.oas.models.info.Info
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.future.future import kotlinx.coroutines.future.future
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import mu.KotlinLogging import mu.KotlinLogging
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.conf.global import org.kodein.di.conf.global
import org.kodein.di.instance import org.kodein.di.instance
@@ -46,27 +49,48 @@ object JavalinSetup {
} }
fun 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 -> val app = Javalin.create { config ->
if (serverConfig.webUIEnabled) { if (serverConfig.webUIEnabled.value) {
runBlocking { runBlocking {
WebInterfaceManager.setupWebUI() 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.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL)
config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL) config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL)
config.registerPlugin(OpenApiPlugin(getOpenApiOptions())) config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
} }
config.server { server }
config.enableCorsForAllOrigins() config.enableCorsForAllOrigins()
config.accessManager { handler, ctx, _ -> config.accessManager { handler, ctx, _ ->
fun credentialsValid(): Boolean { fun credentialsValid(): Boolean {
val (username, password) = ctx.basicAuthCredentials() 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.header("WWW-Authenticate", "Basic")
ctx.status(401).json("Unauthorized") ctx.status(401).json("Unauthorized")
} else { } else {
@@ -75,11 +99,11 @@ object JavalinSetup {
} }
}.events { event -> }.events { event ->
event.serverStarted { event.serverStarted {
if (serverConfig.initialOpenInBrowserEnabled) { if (serverConfig.initialOpenInBrowserEnabled.value) {
Browser.openInBrowser() Browser.openInBrowser()
} }
} }
}.start(serverConfig.ip, serverConfig.port) }.start()
// when JVM is prompted to shutdown, stop javalin gracefully // when JVM is prompted to shutdown, stop javalin gracefully
Runtime.getRuntime().addShutdownHook( Runtime.getRuntime().addShutdownHook(

View File

@@ -8,58 +8,127 @@ package suwayomi.tachidesk.server
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import com.typesafe.config.Config 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.GlobalConfigManager
import xyz.nulldev.ts.config.SystemPropertyOverridableConfigModule 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" private const val MODULE_NAME = "server"
class ServerConfig(getConfig: () -> Config, moduleName: String = MODULE_NAME) : SystemPropertyOverridableConfigModule(getConfig, moduleName) { class ServerConfig(getConfig: () -> Config, val moduleName: String = MODULE_NAME) : SystemPropertyOverridableConfigModule(getConfig, moduleName) {
var ip: String by overridableConfig inner class OverrideConfigValue<T>(private val configAdapter: ConfigAdapter<T>) {
var port: Int by overridableConfig 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 // proxy
var socksProxyEnabled: Boolean by overridableConfig val socksProxyEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
var socksProxyHost: String by overridableConfig val socksProxyHost: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
var socksProxyPort: String by overridableConfig val socksProxyPort: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
// webUI // webUI
var webUIEnabled: Boolean by overridableConfig val webUIEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
var webUIFlavor: String by overridableConfig val webUIFlavor: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
var initialOpenInBrowserEnabled: Boolean by overridableConfig val initialOpenInBrowserEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
var webUIInterface: String by overridableConfig val webUIInterface: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
var electronPath: String by overridableConfig val electronPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
var webUIChannel: String by overridableConfig val webUIChannel: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
var webUIUpdateCheckInterval: Double by overridableConfig val webUIUpdateCheckInterval: MutableStateFlow<Double> by OverrideConfigValue(DoubleConfigAdapter)
// downloader // downloader
var downloadAsCbz: Boolean by overridableConfig val downloadAsCbz: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
var downloadsPath: String by overridableConfig val downloadsPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
var autoDownloadNewChapters: Boolean by overridableConfig val autoDownloadNewChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
// requests
val maxSourcesInParallel: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
// updater // updater
var maxParallelUpdateRequests: Int by overridableConfig val excludeUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
var excludeUnreadChapters: Boolean by overridableConfig val excludeNotStarted: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
var excludeNotStarted: Boolean by overridableConfig val excludeCompleted: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
var excludeCompleted: Boolean by overridableConfig val globalUpdateInterval: MutableStateFlow<Double> by OverrideConfigValue(DoubleConfigAdapter)
var globalUpdateInterval: Double by overridableConfig
// Authentication // Authentication
var basicAuthEnabled: Boolean by overridableConfig val basicAuthEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
var basicAuthUsername: String by overridableConfig val basicAuthUsername: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
var basicAuthPassword: String by overridableConfig val basicAuthPassword: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
// misc // misc
var debugLogsEnabled: Boolean = debugLogsEnabled(GlobalConfigManager.config) val debugLogsEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
var systemTrayEnabled: Boolean by overridableConfig val systemTrayEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
// backup // backup
var backupPath: String by overridableConfig val backupPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
var backupTime: String by overridableConfig val backupTime: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
var backupInterval: Int by overridableConfig val backupInterval: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
var backupTTL: Int by overridableConfig val backupTTL: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
// local source // 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 { companion object {
fun register(getConfig: () -> Config) = ServerConfig({ getConfig().getConfig(MODULE_NAME) }) fun register(getConfig: () -> Config) = ServerConfig({ getConfig().getConfig(MODULE_NAME) })

View File

@@ -7,11 +7,13 @@ package suwayomi.tachidesk.server
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
import ch.qos.logback.classic.Level
import com.typesafe.config.ConfigRenderOptions import com.typesafe.config.ConfigRenderOptions
import eu.kanade.tachiyomi.App import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.source.local.LocalSource import eu.kanade.tachiyomi.source.local.LocalSource
import io.javalin.plugin.json.JavalinJackson import io.javalin.plugin.json.JavalinJackson
import io.javalin.plugin.json.JsonMapper import io.javalin.plugin.json.JsonMapper
import kotlinx.coroutines.flow.combine
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import mu.KotlinLogging import mu.KotlinLogging
import org.bouncycastle.jce.provider.BouncyCastleProvider 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.manga.impl.util.lang.renameTo
import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.database.databaseUp
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex 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.AndroidCompat
import xyz.nulldev.androidcompat.AndroidCompatInitializer import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.ApplicationRootDir import xyz.nulldev.ts.config.ApplicationRootDir
import xyz.nulldev.ts.config.ConfigKodeinModule import xyz.nulldev.ts.config.ConfigKodeinModule
import xyz.nulldev.ts.config.GlobalConfigManager import xyz.nulldev.ts.config.GlobalConfigManager
import xyz.nulldev.ts.config.initLoggerConfig import xyz.nulldev.ts.config.initLoggerConfig
import xyz.nulldev.ts.config.setLogLevel
import java.io.File import java.io.File
import java.security.Security import java.security.Security
import java.util.Locale import java.util.Locale
@@ -43,29 +46,25 @@ class ApplicationDirs(
val dataRoot: String = ApplicationRootDir, val dataRoot: String = ApplicationRootDir,
val tempRoot: String = "${System.getProperty("java.io.tmpdir")}/Tachidesk" val tempRoot: String = "${System.getProperty("java.io.tmpdir")}/Tachidesk"
) { ) {
val cacheRoot = System.getProperty("java.io.tmpdir") + "/tachidesk"
val extensionsRoot = "$dataRoot/extensions" val extensionsRoot = "$dataRoot/extensions"
val downloadsRoot = serverConfig.downloadsPath.ifBlank { "$dataRoot/downloads" } val downloadsRoot get() = serverConfig.downloadsPath.value.ifBlank { "$dataRoot/downloads" }
val localMangaRoot = serverConfig.localSourcePath.ifBlank { "$dataRoot/local" } val localMangaRoot get() = serverConfig.localSourcePath.value.ifBlank { "$dataRoot/local" }
val webUIRoot = "$dataRoot/webUI" 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 tempThumbnailCacheRoot = "$tempRoot/thumbnails"
val tempMangaCacheRoot = "$tempRoot/manga-cache" val tempMangaCacheRoot = "$tempRoot/manga-cache"
val thumbnailDownloadsRoot = "$downloadsRoot/thumbnails" val thumbnailDownloadsRoot get() = "$downloadsRoot/thumbnails"
val mangaDownloadsRoot = "$downloadsRoot/mangas" val mangaDownloadsRoot get() = "$downloadsRoot/mangas"
} }
val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() } val serverConfig: ServerConfig by lazy { GlobalConfigManager.module() }
val systemTrayInstance by lazy { systemTray() }
val androidCompat by lazy { AndroidCompat() } val androidCompat by lazy { AndroidCompat() }
fun applicationSetup() { fun applicationSetup() {
Thread.setDefaultUncaughtExceptionHandler { Thread.setDefaultUncaughtExceptionHandler { _, throwable ->
_, throwable ->
KotlinLogging.logger { }.error(throwable) { "unhandled exception" } KotlinLogging.logger { }.error(throwable) { "unhandled exception" }
} }
@@ -74,6 +73,14 @@ fun applicationSetup() {
ServerConfig.register { GlobalConfigManager.config } ServerConfig.register { GlobalConfigManager.config }
) )
serverConfig.subscribeTo(serverConfig.debugLogsEnabled, { debugLogsEnabled ->
if (debugLogsEnabled) {
setLogLevel(Level.DEBUG)
} else {
setLogLevel(Level.INFO)
}
})
// Application dirs // Application dirs
val applicationDirs = ApplicationDirs() val applicationDirs = ApplicationDirs()
@@ -164,13 +171,17 @@ fun applicationSetup() {
LocalSource.register() LocalSource.register()
// create system tray // create system tray
if (serverConfig.systemTrayEnabled) { serverConfig.subscribeTo(serverConfig.systemTrayEnabled, { systemTrayEnabled ->
try { try {
systemTrayInstance if (systemTrayEnabled) {
SystemTray.create()
} else {
SystemTray.remove()
}
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
e.printStackTrace() e.printStackTrace()
} }
} }, ignoreInitialValue = false)
// Disable jetty's logging // Disable jetty's logging
System.setProperty("org.eclipse.jetty.util.log.announce", "false") System.setProperty("org.eclipse.jetty.util.log.announce", "false")
@@ -178,11 +189,25 @@ fun applicationSetup() {
System.setProperty("org.eclipse.jetty.LEVEL", "OFF") System.setProperty("org.eclipse.jetty.LEVEL", "OFF")
// socks proxy settings // socks proxy settings
if (serverConfig.socksProxyEnabled) { serverConfig.subscribeTo(
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost combine(
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort serverConfig.socksProxyEnabled,
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") 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 // AES/CBC/PKCS7Padding Cypher provider for zh.copymanga
Security.addProvider(BouncyCastleProvider()) Security.addProvider(BouncyCastleProvider())

View File

@@ -31,7 +31,7 @@ object AppMutex {
OtherApplicationRunning(2) 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>() private val jsonMapper by DI.global.instance<JsonMapper>()
@@ -41,7 +41,7 @@ object AppMutex {
.build() .build()
val request = Builder() val request = Builder()
.url("http://$appIP:${serverConfig.port}/api/v1/settings/about/") .url("http://$appIP:${serverConfig.port.value}/api/v1/settings/about/")
.build() .build()
val response = try { val response = try {
@@ -64,7 +64,7 @@ object AppMutex {
logger.info("Mutex status is clear, Resuming startup.") logger.info("Mutex status is clear, Resuming startup.")
} }
AppMutexState.TachideskInstanceRunning -> { 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.") logger.info("Probably user thought tachidesk is closed so, opening webUI in browser again.")
openInBrowser() openInBrowser()
@@ -74,7 +74,7 @@ object AppMutex {
shutdownApp(MutexCheckFailedTachideskRunning) shutdownApp(MutexCheckFailedTachideskRunning)
} }
AppMutexState.OtherApplicationRunning -> { 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) shutdownApp(MutexCheckFailedAnotherAppRunning)
} }
} }

View File

@@ -11,16 +11,20 @@ import dorkbox.desktop.Desktop
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
object Browser { 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 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() { fun openInBrowser() {
if (serverConfig.webUIEnabled) { if (serverConfig.webUIEnabled.value) {
if (serverConfig.webUIInterface == ("electron")) { val appBaseUrl = getAppBaseUrl()
if (serverConfig.webUIInterface.value == ("electron")) {
try { try {
val electronPath = serverConfig.electronPath val electronPath = serverConfig.electronPath.value
electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start()) electronInstances.add(ProcessBuilder(electronPath, appBaseUrl).start())
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
e.printStackTrace() e.printStackTrace()

View File

@@ -17,10 +17,16 @@ import suwayomi.tachidesk.server.util.Browser.openInBrowser
import suwayomi.tachidesk.server.util.ExitCode.Success import suwayomi.tachidesk.server.util.ExitCode.Success
object SystemTray { object SystemTray {
fun systemTray(): SystemTray? { private var instance: SystemTray? = null
try {
fun create() {
instance = try {
// ref: https://github.com/dorkbox/SystemTray/blob/master/test/dorkbox/TestTray.java // 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) CacheUtil.clear(BuildConfig.NAME)
@@ -28,7 +34,7 @@ object SystemTray {
SystemTray.FORCE_TRAY_TYPE = SystemTray.TrayType.Awt 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 val mainMenu = systemTray.menu
mainMenu.add( mainMenu.add(
@@ -51,10 +57,15 @@ object SystemTray {
} }
) )
return systemTray systemTray
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
return null null
} }
} }
fun remove() {
instance?.remove()
instance = null
}
} }

View File

@@ -16,11 +16,11 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST import kotlinx.coroutines.channels.BufferOverflow.DROP_OLDEST
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
@@ -70,7 +70,7 @@ enum class WebUIChannel {
companion object { companion object {
fun doesConfigChannelEqual(channel: WebUIChannel): Boolean { 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, SharingStarted.Eagerly,
WebUIUpdateStatus( WebUIUpdateStatus(
info = WebUIUpdateInfo( info = WebUIUpdateInfo(
channel = serverConfig.webUIChannel, channel = serverConfig.webUIChannel.value,
tag = "", tag = "",
updateAvailable = false updateAvailable = false
), ),
@@ -122,27 +122,36 @@ object WebInterfaceManager {
) )
init { init {
scheduleWebUIUpdateCheck() serverConfig.subscribeTo(
combine(serverConfig.webUIUpdateCheckInterval, serverConfig.webUIFlavor) { interval, flavor ->
Pair(
interval,
flavor
)
},
::scheduleWebUIUpdateCheck,
ignoreInitialValue = false
)
} }
private fun isAutoUpdateEnabled(): Boolean { private fun isAutoUpdateEnabled(): Boolean {
return serverConfig.webUIUpdateCheckInterval.toInt() != 0 return serverConfig.webUIUpdateCheckInterval.value.toInt() != 0
} }
private fun scheduleWebUIUpdateCheck() { private fun scheduleWebUIUpdateCheck() {
HAScheduler.descheduleCron(currentUpdateTaskId) HAScheduler.descheduleCron(currentUpdateTaskId)
val isAutoUpdateDisabled = !isAutoUpdateEnabled() || serverConfig.webUIFlavor == "Custom" val isAutoUpdateDisabled = !isAutoUpdateEnabled() || serverConfig.webUIFlavor.value == "Custom"
if (isAutoUpdateDisabled) { if (isAutoUpdateDisabled) {
return 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 lastAutomatedUpdate = preferences.getLong(lastWebUIUpdateCheckKey, System.currentTimeMillis())
val task = { val task = {
logger.debug { 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( Date(
lastAutomatedUpdate lastAutomatedUpdate
) )
@@ -165,14 +174,14 @@ object WebInterfaceManager {
} }
suspend fun setupWebUI() { suspend fun setupWebUI() {
if (serverConfig.webUIFlavor == "Custom") { if (serverConfig.webUIFlavor.value == "Custom") {
return return
} }
if (doesLocalWebUIExist(applicationDirs.webUIRoot)) { if (doesLocalWebUIExist(applicationDirs.webUIRoot)) {
val currentVersion = getLocalVersion() 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)) { if (!isLocalWebUIValid(applicationDirs.webUIRoot)) {
doInitialSetup() doInitialSetup()
@@ -186,7 +195,7 @@ object WebInterfaceManager {
// check if the bundled webUI version is a newer version than the current used version // 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 // this could be the case in case no compatible webUI version is available and a newer server version was installed
val shouldUpdateToBundledVersion = val shouldUpdateToBundledVersion =
serverConfig.webUIFlavor == DEFAULT_WEB_UI && extractVersion(getLocalVersion()) < extractVersion( serverConfig.webUIFlavor.value == DEFAULT_WEB_UI && extractVersion(getLocalVersion()) < extractVersion(
BuildConfig.WEBUI_TAG BuildConfig.WEBUI_TAG
) )
if (shouldUpdateToBundledVersion) { if (shouldUpdateToBundledVersion) {
@@ -232,10 +241,10 @@ object WebInterfaceManager {
return return
} }
if (serverConfig.webUIFlavor != DEFAULT_WEB_UI) { if (serverConfig.webUIFlavor.value != DEFAULT_WEB_UI) {
logger.warn { "doInitialSetup: fallback to default webUI \"$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() } val fallbackToBundledVersion = !doDownload() { getLatestCompatibleVersion() }
if (!fallbackToBundledVersion) { if (!fallbackToBundledVersion) {
@@ -287,11 +296,11 @@ object WebInterfaceManager {
val localVersion = getLocalVersion() val localVersion = getLocalVersion()
if (!isUpdateAvailable(localVersion).second) { 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 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 { try {
downloadVersion(getLatestCompatibleVersion()) downloadVersion(getLatestCompatibleVersion())
} catch (e: Exception) { } catch (e: Exception) {
@@ -416,7 +425,7 @@ object WebInterfaceManager {
val currentServerVersionNumber = extractVersion(BuildConfig.REVISION) val currentServerVersionNumber = extractVersion(BuildConfig.REVISION)
val webUIToServerVersionMappings = fetchServerMappingFile() 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) { for (i in 0 until webUIToServerVersionMappings.size) {
val webUIToServerVersionEntry = webUIToServerVersionMappings[i].jsonObject val webUIToServerVersionEntry = webUIToServerVersionMappings[i].jsonObject
@@ -446,7 +455,7 @@ object WebInterfaceManager {
notifyFlow.emit( notifyFlow.emit(
WebUIUpdateStatus( WebUIUpdateStatus(
info = WebUIUpdateInfo( info = WebUIUpdateInfo(
channel = serverConfig.webUIChannel, channel = serverConfig.webUIChannel.value,
tag = version, tag = version,
updateAvailable = true updateAvailable = true
), ),
@@ -472,7 +481,7 @@ object WebInterfaceManager {
val webUIZipURL = "${getDownloadUrlFor(version)}/$webUIZip" val webUIZipURL = "${getDownloadUrlFor(version)}/$webUIZip"
val log = 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..." } log.info { "Downloading WebUI zip from the Internet..." }
executeWithRetry(log, { executeWithRetry(log, {

View File

@@ -21,8 +21,10 @@ server.downloadAsCbz = false
server.downloadsPath = "" server.downloadsPath = ""
server.autoDownloadNewChapters = false # if new chapters that have been retrieved should get automatically downloaded 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 # 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.excludeUnreadChapters = true
server.excludeNotStarted = true server.excludeNotStarted = true
server.excludeCompleted = true server.excludeCompleted = true

View File

@@ -26,8 +26,8 @@ import suwayomi.tachidesk.server.ServerConfig
import suwayomi.tachidesk.server.androidCompat import suwayomi.tachidesk.server.androidCompat
import suwayomi.tachidesk.server.database.databaseUp import suwayomi.tachidesk.server.database.databaseUp
import suwayomi.tachidesk.server.serverConfig import suwayomi.tachidesk.server.serverConfig
import suwayomi.tachidesk.server.systemTrayInstance
import suwayomi.tachidesk.server.util.AppMutex import suwayomi.tachidesk.server.util.AppMutex
import suwayomi.tachidesk.server.util.SystemTray
import xyz.nulldev.androidcompat.AndroidCompatInitializer import xyz.nulldev.androidcompat.AndroidCompatInitializer
import xyz.nulldev.ts.config.CONFIG_PREFIX import xyz.nulldev.ts.config.CONFIG_PREFIX
import xyz.nulldev.ts.config.ConfigKodeinModule import xyz.nulldev.ts.config.ConfigKodeinModule
@@ -83,7 +83,7 @@ open class ApplicationTest {
// register Tachidesk's config which is dubbed "ServerConfig" // register Tachidesk's config which is dubbed "ServerConfig"
GlobalConfigManager.registerModule( GlobalConfigManager.registerModule(
ServerConfig.register(GlobalConfigManager.config) ServerConfig.register { GlobalConfigManager.config }
) )
// Make sure only one instance of the app is running // Make sure only one instance of the app is running
@@ -125,9 +125,9 @@ open class ApplicationTest {
} }
// create system tray // create system tray
if (serverConfig.systemTrayEnabled) { if (serverConfig.systemTrayEnabled.value) {
try { try {
systemTrayInstance SystemTray.create()
} catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error } catch (e: Throwable) { // cover both java.lang.Exception and java.lang.Error
e.printStackTrace() e.printStackTrace()
} }
@@ -139,10 +139,10 @@ open class ApplicationTest {
System.setProperty("org.eclipse.jetty.LEVEL", "OFF") System.setProperty("org.eclipse.jetty.LEVEL", "OFF")
// socks proxy settings // socks proxy settings
if (serverConfig.socksProxyEnabled) { if (serverConfig.socksProxyEnabled.value) {
System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost System.getProperties()["socksProxyHost"] = serverConfig.socksProxyHost.value
System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort System.getProperties()["socksProxyPort"] = serverConfig.socksProxyPort.value
logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost}:${serverConfig.socksProxyPort}") logger.info("Socks Proxy is enabled to ${serverConfig.socksProxyHost.value}:${serverConfig.socksProxyPort.value}")
} }
} }

View File

@@ -11,8 +11,10 @@ server.socksProxyPort = ""
server.downloadAsCbz = false server.downloadAsCbz = false
server.autoDownloadNewChapters = false server.autoDownloadNewChapters = false
# requests
server.maxSourcesInParallel = 10
# updater # updater
server.maxParallelUpdateRequests = 10
server.excludeUnreadChapters = true server.excludeUnreadChapters = true
server.excludeNotStarted = true server.excludeNotStarted = true
server.excludeCompleted = true server.excludeCompleted = true