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.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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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 ->
|
||||||
|
|||||||
@@ -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
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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.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(
|
||||||
|
|||||||
@@ -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) })
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user