mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-12 07:14:35 -05: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:
@@ -210,7 +210,7 @@ object Chapter {
|
||||
val wasInitialFetch = currentNumberOfChapters == 0
|
||||
|
||||
// make sure to ignore initial fetch
|
||||
val downloadNewChapters = serverConfig.autoDownloadNewChapters && !wasInitialFetch && areNewChaptersAvailable
|
||||
val downloadNewChapters = serverConfig.autoDownloadNewChapters.value && !wasInitialFetch && areNewChaptersAvailable
|
||||
if (!downloadNewChapters) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ object ChapterDownloadHelper {
|
||||
val chapterFolder = File(getChapterDownloadPath(mangaId, chapterId))
|
||||
val cbzFile = File(getChapterCbzPath(mangaId, chapterId))
|
||||
if (cbzFile.exists()) return ArchiveProvider(mangaId, chapterId)
|
||||
if (!chapterFolder.exists() && serverConfig.downloadAsCbz) return ArchiveProvider(mangaId, chapterId)
|
||||
if (!chapterFolder.exists() && serverConfig.downloadAsCbz.value) return ArchiveProvider(mangaId, chapterId)
|
||||
return FolderProvider(mangaId, chapterId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ package suwayomi.tachidesk.manga.impl.backup.proto
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.UpdateStrategy
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import mu.KotlinLogging
|
||||
import okio.buffer
|
||||
import okio.gzip
|
||||
@@ -53,10 +54,22 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
private const val lastAutomatedBackupKey = "lastAutomatedBackupKey"
|
||||
private val preferences = Preferences.userNodeForPackage(ProtoBackupExport::class.java)
|
||||
|
||||
init {
|
||||
serverConfig.subscribeTo(
|
||||
combine(serverConfig.backupInterval, serverConfig.backupTime) { interval, timeOfDay ->
|
||||
Pair(
|
||||
interval,
|
||||
timeOfDay
|
||||
)
|
||||
},
|
||||
::scheduleAutomatedBackupTask
|
||||
)
|
||||
}
|
||||
|
||||
fun scheduleAutomatedBackupTask() {
|
||||
HAScheduler.descheduleCron(backupSchedulerJobId)
|
||||
|
||||
val areAutomatedBackupsDisabled = serverConfig.backupInterval == 0
|
||||
val areAutomatedBackupsDisabled = serverConfig.backupInterval.value == 0
|
||||
if (areAutomatedBackupsDisabled) {
|
||||
return
|
||||
}
|
||||
@@ -67,10 +80,10 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
preferences.putLong(lastAutomatedBackupKey, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
val (hour, minute) = serverConfig.backupTime.split(":").map { it.toInt() }
|
||||
val (hour, minute) = serverConfig.backupTime.value.split(":").map { it.toInt() }
|
||||
val backupHour = hour.coerceAtLeast(0).coerceAtMost(23)
|
||||
val backupMinute = minute.coerceAtLeast(0).coerceAtMost(59)
|
||||
val backupInterval = serverConfig.backupInterval.days.coerceAtLeast(1.days)
|
||||
val backupInterval = serverConfig.backupInterval.value.days.coerceAtLeast(1.days)
|
||||
|
||||
// trigger last backup in case the server wasn't running on the scheduled time
|
||||
val lastAutomatedBackup = preferences.getLong(lastAutomatedBackupKey, System.currentTimeMillis())
|
||||
@@ -105,9 +118,9 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
}
|
||||
|
||||
private fun cleanupAutomatedBackups() {
|
||||
logger.debug { "Cleanup automated backups (ttl= ${serverConfig.backupTTL})" }
|
||||
logger.debug { "Cleanup automated backups (ttl= ${serverConfig.backupTTL.value})" }
|
||||
|
||||
val isCleanupDisabled = serverConfig.backupTTL == 0
|
||||
val isCleanupDisabled = serverConfig.backupTTL.value == 0
|
||||
if (isCleanupDisabled) {
|
||||
return
|
||||
}
|
||||
@@ -133,7 +146,7 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
|
||||
val lastAccessTime = file.lastModified()
|
||||
val isTTLReached =
|
||||
System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.days.coerceAtLeast(1.days).inWholeMilliseconds
|
||||
System.currentTimeMillis() - lastAccessTime >= serverConfig.backupTTL.value.days.coerceAtLeast(1.days).inWholeMilliseconds
|
||||
if (isTTLReached) {
|
||||
file.delete()
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
|
||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.manga.model.table.toDataClass
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
@@ -51,8 +52,6 @@ import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
private const val MAX_SOURCES_IN_PARAllEL = 5
|
||||
|
||||
object DownloadManager {
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default)
|
||||
private val clients = ConcurrentHashMap<String, WsContext>()
|
||||
@@ -169,6 +168,22 @@ object DownloadManager {
|
||||
|
||||
private val downloaderWatch = MutableSharedFlow<Unit>(extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
|
||||
init {
|
||||
serverConfig.subscribeTo(serverConfig.maxSourcesInParallel, { maxSourcesInParallel ->
|
||||
val runningDownloaders = downloaders.values.filter { it.isActive }
|
||||
var downloadersToStop = runningDownloaders.size - maxSourcesInParallel
|
||||
|
||||
logger.debug { "Max sources in parallel changed to $maxSourcesInParallel (running downloaders ${runningDownloaders.size})" }
|
||||
|
||||
if (downloadersToStop > 0) {
|
||||
runningDownloaders.takeWhile {
|
||||
it.stop()
|
||||
--downloadersToStop > 0
|
||||
}
|
||||
} else {
|
||||
downloaderWatch.emit(Unit)
|
||||
}
|
||||
})
|
||||
|
||||
scope.launch {
|
||||
downloaderWatch.sample(1.seconds).collect {
|
||||
val runningDownloaders = downloaders.values.filter { it.isActive }
|
||||
@@ -176,14 +191,14 @@ object DownloadManager {
|
||||
|
||||
logger.info { "Running: ${runningDownloaders.size}, Queued: ${availableDownloads.size}, Failed: ${downloadQueue.size - availableDownloads.size}" }
|
||||
|
||||
if (runningDownloaders.size < MAX_SOURCES_IN_PARAllEL) {
|
||||
if (runningDownloaders.size < serverConfig.maxSourcesInParallel.value) {
|
||||
availableDownloads.asSequence()
|
||||
.map { it.manga.sourceId }
|
||||
.distinct()
|
||||
.minus(
|
||||
runningDownloaders.map { it.sourceId }.toSet()
|
||||
)
|
||||
.take(MAX_SOURCES_IN_PARAllEL - runningDownloaders.size)
|
||||
.take(serverConfig.maxSourcesInParallel.value - runningDownloaders.size)
|
||||
.map { getDownloader(it) }
|
||||
.forEach {
|
||||
it.start()
|
||||
|
||||
@@ -33,6 +33,7 @@ import suwayomi.tachidesk.util.HAScheduler
|
||||
import java.util.Date
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.prefs.Preferences
|
||||
import kotlin.math.absoluteValue
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
class Updater : IUpdater {
|
||||
@@ -45,13 +46,36 @@ class Updater : IUpdater {
|
||||
private val tracker = ConcurrentHashMap<Int, UpdateJob>()
|
||||
private val updateChannels = ConcurrentHashMap<String, Channel<UpdateJob>>()
|
||||
|
||||
private val semaphore = Semaphore(serverConfig.maxParallelUpdateRequests)
|
||||
private var maxSourcesInParallel = 20 // max permits, necessary to be set to be able to release up to 20 permits
|
||||
private val semaphore = Semaphore(maxSourcesInParallel)
|
||||
|
||||
private val lastAutomatedUpdateKey = "lastAutomatedUpdateKey"
|
||||
private val preferences = Preferences.userNodeForPackage(Updater::class.java)
|
||||
|
||||
private var currentUpdateTaskId = ""
|
||||
|
||||
init {
|
||||
serverConfig.subscribeTo(serverConfig.globalUpdateInterval, ::scheduleUpdateTask)
|
||||
serverConfig.subscribeTo(
|
||||
serverConfig.maxSourcesInParallel,
|
||||
{ value ->
|
||||
val newMaxPermits = value.coerceAtLeast(1).coerceAtMost(20)
|
||||
val permitDifference = maxSourcesInParallel - newMaxPermits
|
||||
maxSourcesInParallel = newMaxPermits
|
||||
|
||||
val addMorePermits = permitDifference < 0
|
||||
for (i in 1..permitDifference.absoluteValue) {
|
||||
if (addMorePermits) {
|
||||
semaphore.release()
|
||||
} else {
|
||||
semaphore.acquire()
|
||||
}
|
||||
}
|
||||
},
|
||||
ignoreInitialValue = false
|
||||
)
|
||||
}
|
||||
|
||||
private fun autoUpdateTask() {
|
||||
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
|
||||
preferences.putLong(lastAutomatedUpdateKey, System.currentTimeMillis())
|
||||
@@ -61,19 +85,19 @@ class Updater : IUpdater {
|
||||
return
|
||||
}
|
||||
|
||||
logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" }
|
||||
logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval.value}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" }
|
||||
addCategoriesToUpdateQueue(Category.getCategoryList(), clear = true, forceAll = false)
|
||||
}
|
||||
|
||||
fun scheduleUpdateTask() {
|
||||
HAScheduler.deschedule(currentUpdateTaskId)
|
||||
|
||||
val isAutoUpdateDisabled = serverConfig.globalUpdateInterval == 0.0
|
||||
val isAutoUpdateDisabled = serverConfig.globalUpdateInterval.value == 0.0
|
||||
if (isAutoUpdateDisabled) {
|
||||
return
|
||||
}
|
||||
|
||||
val updateInterval = serverConfig.globalUpdateInterval.hours.coerceAtLeast(6.hours).inWholeMilliseconds
|
||||
val updateInterval = serverConfig.globalUpdateInterval.value.hours.coerceAtLeast(6.hours).inWholeMilliseconds
|
||||
val lastAutomatedUpdate = preferences.getLong(lastAutomatedUpdateKey, 0)
|
||||
val timeToNextExecution = (updateInterval - (System.currentTimeMillis() - lastAutomatedUpdate)).mod(updateInterval)
|
||||
|
||||
@@ -150,9 +174,9 @@ class Updater : IUpdater {
|
||||
val mangasToUpdate = categoriesToUpdateMangas
|
||||
.asSequence()
|
||||
.filter { it.updateStrategy == UpdateStrategy.ALWAYS_UPDATE }
|
||||
.filter { if (serverConfig.excludeUnreadChapters) { (it.unreadCount ?: 0L) == 0L } else true }
|
||||
.filter { if (serverConfig.excludeNotStarted) { it.lastReadAt != null } else true }
|
||||
.filter { if (serverConfig.excludeCompleted) { it.status != MangaStatus.COMPLETED.name } else true }
|
||||
.filter { if (serverConfig.excludeUnreadChapters.value) { (it.unreadCount ?: 0L) == 0L } else true }
|
||||
.filter { if (serverConfig.excludeNotStarted.value) { it.lastReadAt != null } else true }
|
||||
.filter { if (serverConfig.excludeCompleted.value) { it.status != MangaStatus.COMPLETED.name } else true }
|
||||
.filter { forceAll || !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } }
|
||||
.toList()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user