mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 11:24:35 -05:00
* [#1496] First conversion attempt * [#1496] Configurable conversion * Fix: allow nested configs (map) * [#1496] Support explicit `none` conversion * Use MimeUtils for provided download * [1496] Support image conversion on load for downloaded images * Lint * [#1496] Support conversion on fresh download as well Previous commit was only for already downloaded images, now also for fresh and cached * [#1496] Refactor: Move where conversion for download happens * Rewrite config handling, improve custom types * Lint * Add format to pages mutation * Lint * Standardize url encode * Lint * Config: Allow additional conversion parameters * Implement conversion quality parameter * Lint * Implement a conversion util to allow fallback readers * Add downloadConversions to api and backup, fix updateValue issues * Lint * Minor cleanup * Update libs.versions.toml --------- Co-authored-by: Syer10 <syer10@users.noreply.github.com>
This commit is contained in:
@@ -10,10 +10,11 @@ package xyz.nulldev.ts.config
|
|||||||
import ch.qos.logback.classic.Level
|
import ch.qos.logback.classic.Level
|
||||||
import com.typesafe.config.Config
|
import com.typesafe.config.Config
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
|
import com.typesafe.config.ConfigObject
|
||||||
import com.typesafe.config.ConfigValue
|
import com.typesafe.config.ConfigValue
|
||||||
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 io.github.config4k.toConfig
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
import kotlinx.coroutines.sync.withLock
|
import kotlinx.coroutines.sync.withLock
|
||||||
@@ -113,7 +114,7 @@ open class ConfigManager {
|
|||||||
) {
|
) {
|
||||||
mutex.withLock {
|
mutex.withLock {
|
||||||
val actualValue = if (value is Enum<*>) value.name else value
|
val actualValue = if (value is Enum<*>) value.name else value
|
||||||
val configValue = ConfigValueFactory.fromAnyRef(actualValue)
|
val configValue = actualValue.toConfig("internal").getValue("internal")
|
||||||
|
|
||||||
updateUserConfigFile(path, configValue)
|
updateUserConfigFile(path, configValue)
|
||||||
internalConfig = internalConfig.withValue(path, configValue)
|
internalConfig = internalConfig.withValue(path, configValue)
|
||||||
@@ -142,8 +143,13 @@ open class ConfigManager {
|
|||||||
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
||||||
val userConfig = getUserConfig()
|
val userConfig = getUserConfig()
|
||||||
|
|
||||||
val hasMissingSettings = serverConfig.entrySet().any { !userConfig.hasPath(it.key) }
|
// NOTE: if more than 1 dot is included, that's a nested setting, which we need to filter out here
|
||||||
val hasOutdatedSettings = userConfig.entrySet().any { !serverConfig.hasPath(it.key) }
|
val refKeys =
|
||||||
|
serverConfig.root().entries.flatMap {
|
||||||
|
(it.value as? ConfigObject)?.entries?.map { e -> "${it.key}.${e.key}" }.orEmpty()
|
||||||
|
}
|
||||||
|
val hasMissingSettings = refKeys.any { !userConfig.hasPath(it) }
|
||||||
|
val hasOutdatedSettings = userConfig.entrySet().any { !refKeys.contains(it.key) && it.key.count { c -> c == '.' } <= 1 }
|
||||||
val isUserConfigOutdated = hasMissingSettings || hasOutdatedSettings
|
val isUserConfigOutdated = hasMissingSettings || hasOutdatedSettings
|
||||||
if (!isUserConfigOutdated) {
|
if (!isUserConfigOutdated) {
|
||||||
return
|
return
|
||||||
@@ -159,7 +165,8 @@ open class ConfigManager {
|
|||||||
.filter {
|
.filter {
|
||||||
serverConfig.hasPath(
|
serverConfig.hasPath(
|
||||||
it.key,
|
it.key,
|
||||||
)
|
) ||
|
||||||
|
it.key.count { c -> c == '.' } > 1
|
||||||
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) }
|
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) }
|
||||||
|
|
||||||
newUserConfigDoc =
|
newUserConfigDoc =
|
||||||
|
|||||||
@@ -8,9 +8,13 @@ package xyz.nulldev.ts.config
|
|||||||
* 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 com.typesafe.config.ConfigException
|
||||||
import com.typesafe.config.ConfigFactory
|
import com.typesafe.config.ConfigFactory
|
||||||
import com.typesafe.config.ConfigValueFactory
|
import io.github.config4k.ClassContainer
|
||||||
|
import io.github.config4k.TypeReference
|
||||||
import io.github.config4k.getValue
|
import io.github.config4k.getValue
|
||||||
|
import io.github.config4k.readers.SelectReader
|
||||||
|
import io.github.config4k.toConfig
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,7 +30,7 @@ abstract class ConfigModule(
|
|||||||
*/
|
*/
|
||||||
abstract class SystemPropertyOverridableConfigModule(
|
abstract class SystemPropertyOverridableConfigModule(
|
||||||
getConfig: () -> Config,
|
getConfig: () -> Config,
|
||||||
moduleName: String,
|
val moduleName: String,
|
||||||
) : ConfigModule(getConfig) {
|
) : ConfigModule(getConfig) {
|
||||||
val overridableConfig = SystemPropertyOverrideDelegate(getConfig, moduleName)
|
val overridableConfig = SystemPropertyOverrideDelegate(getConfig, moduleName)
|
||||||
}
|
}
|
||||||
@@ -46,20 +50,31 @@ class SystemPropertyOverrideDelegate(
|
|||||||
val combined =
|
val combined =
|
||||||
System.getProperty(
|
System.getProperty(
|
||||||
"$CONFIG_PREFIX.$moduleName.${property.name}",
|
"$CONFIG_PREFIX.$moduleName.${property.name}",
|
||||||
if (T::class.simpleName == "List") {
|
configValue!!
|
||||||
ConfigValueFactory.fromAnyRef(configValue).render()
|
.toConfig("internal")
|
||||||
} else {
|
.root()
|
||||||
configValue.toString()
|
.render()
|
||||||
},
|
.removePrefix("internal="),
|
||||||
)
|
)
|
||||||
|
val combinedConfig =
|
||||||
|
try {
|
||||||
|
ConfigFactory.parseString(combined)
|
||||||
|
} catch (_: ConfigException) {
|
||||||
|
ConfigFactory.parseString("internal=$combined")
|
||||||
|
}
|
||||||
|
|
||||||
return when (T::class.simpleName) {
|
val genericType = object : TypeReference<T>() {}.genericType()
|
||||||
"Int" -> combined.toInt()
|
val clazz = ClassContainer(T::class, genericType)
|
||||||
"Boolean" -> combined.toBoolean()
|
val reader = SelectReader.getReader(clazz)
|
||||||
"Double" -> combined.toDouble()
|
val path = property.name
|
||||||
"List" -> ConfigFactory.parseString("internal=" + combined).getStringList("internal").orEmpty()
|
|
||||||
// add more types as needed
|
val result = reader(combinedConfig, "internal")
|
||||||
else -> combined // covers String
|
return try {
|
||||||
} as T
|
result as T
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw result
|
||||||
|
?.let { e }
|
||||||
|
?: ConfigException.BadPath(path, "take a look at your config")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,5 +47,5 @@ dependencies {
|
|||||||
|
|
||||||
// OpenJDK lacks native JPEG encoder and native WEBP decoder
|
// OpenJDK lacks native JPEG encoder and native WEBP decoder
|
||||||
implementation(libs.bundles.twelvemonkeys)
|
implementation(libs.bundles.twelvemonkeys)
|
||||||
implementation(libs.sejda.webp)
|
implementation(libs.imageio.webp)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ twelvemonkeys-imageio-metadata = { module = "com.twelvemonkeys.imageio:imageio-m
|
|||||||
twelvemonkeys-imageio-jpeg = { module = "com.twelvemonkeys.imageio:imageio-jpeg", version.ref = "twelvemonkeys" }
|
twelvemonkeys-imageio-jpeg = { module = "com.twelvemonkeys.imageio:imageio-jpeg", version.ref = "twelvemonkeys" }
|
||||||
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
|
twelvemonkeys-imageio-webp = { module = "com.twelvemonkeys.imageio:imageio-webp", version.ref = "twelvemonkeys" }
|
||||||
|
|
||||||
sejda-webp = "com.github.usefulness:webp-imageio:0.10.2"
|
imageio-webp = "com.github.usefulness:webp-imageio:0.10.2"
|
||||||
|
|
||||||
# Testing
|
# Testing
|
||||||
mockk = "io.mockk:mockk:1.14.4"
|
mockk = "io.mockk:mockk:1.14.4"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import suwayomi.tachidesk.manga.impl.chapter.getChapterDownloadReadyById
|
|||||||
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
|
import suwayomi.tachidesk.manga.model.table.ChapterMetaTable
|
||||||
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
||||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||||
|
import java.net.URLEncoder
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
@@ -226,7 +227,15 @@ class ChapterMutation {
|
|||||||
data class FetchChapterPagesInput(
|
data class FetchChapterPagesInput(
|
||||||
val clientMutationId: String? = null,
|
val clientMutationId: String? = null,
|
||||||
val chapterId: Int,
|
val chapterId: Int,
|
||||||
)
|
val format: String? = null,
|
||||||
|
) {
|
||||||
|
fun toParams(): Map<String, String> =
|
||||||
|
buildMap {
|
||||||
|
if (!format.isNullOrBlank()) {
|
||||||
|
put("format", format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data class FetchChapterPagesPayload(
|
data class FetchChapterPagesPayload(
|
||||||
val clientMutationId: String?,
|
val clientMutationId: String?,
|
||||||
@@ -236,16 +245,32 @@ class ChapterMutation {
|
|||||||
|
|
||||||
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<DataFetcherResult<FetchChapterPagesPayload?>> {
|
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<DataFetcherResult<FetchChapterPagesPayload?>> {
|
||||||
val (clientMutationId, chapterId) = input
|
val (clientMutationId, chapterId) = input
|
||||||
|
val paramsMap = input.toParams()
|
||||||
|
|
||||||
return future {
|
return future {
|
||||||
asDataFetcherResult {
|
asDataFetcherResult {
|
||||||
val chapter = getChapterDownloadReadyById(chapterId)
|
val chapter = getChapterDownloadReadyById(chapterId)
|
||||||
|
|
||||||
|
val params =
|
||||||
|
buildString {
|
||||||
|
if (paramsMap.isNotEmpty()) {
|
||||||
|
append("?")
|
||||||
|
paramsMap.entries.forEach { entry ->
|
||||||
|
if (length > 1) {
|
||||||
|
append("&")
|
||||||
|
}
|
||||||
|
append(entry.key)
|
||||||
|
append("=")
|
||||||
|
append(URLEncoder.encode(entry.value, Charsets.UTF_8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
FetchChapterPagesPayload(
|
FetchChapterPagesPayload(
|
||||||
clientMutationId = clientMutationId,
|
clientMutationId = clientMutationId,
|
||||||
pages =
|
pages =
|
||||||
List(chapter.pageCount) { index ->
|
List(chapter.pageCount) { index ->
|
||||||
"/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/$index"
|
"/api/v1/manga/${chapter.mangaId}/chapter/${chapter.index}/page/${index}$params"
|
||||||
},
|
},
|
||||||
chapter = ChapterType(chapter),
|
chapter = ChapterType(chapter),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -111,6 +111,18 @@ class SettingsMutation {
|
|||||||
configSetting.value = newSetting
|
configSetting.value = newSetting
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun <SettingType : Any, RealSettingType : Any> updateSetting(
|
||||||
|
newSetting: RealSettingType?,
|
||||||
|
configSetting: MutableStateFlow<SettingType>,
|
||||||
|
mapper: (RealSettingType) -> SettingType,
|
||||||
|
) {
|
||||||
|
if (newSetting == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
configSetting.value = mapper(newSetting)
|
||||||
|
}
|
||||||
|
|
||||||
@GraphQLIgnore
|
@GraphQLIgnore
|
||||||
fun updateSettings(settings: Settings) {
|
fun updateSettings(settings: Settings) {
|
||||||
updateSetting(settings.ip, serverConfig.ip)
|
updateSetting(settings.ip, serverConfig.ip)
|
||||||
@@ -140,6 +152,15 @@ class SettingsMutation {
|
|||||||
updateSetting(settings.autoDownloadAheadLimit, serverConfig.autoDownloadNewChaptersLimit) // deprecated
|
updateSetting(settings.autoDownloadAheadLimit, serverConfig.autoDownloadNewChaptersLimit) // deprecated
|
||||||
updateSetting(settings.autoDownloadNewChaptersLimit, serverConfig.autoDownloadNewChaptersLimit)
|
updateSetting(settings.autoDownloadNewChaptersLimit, serverConfig.autoDownloadNewChaptersLimit)
|
||||||
updateSetting(settings.autoDownloadIgnoreReUploads, serverConfig.autoDownloadIgnoreReUploads)
|
updateSetting(settings.autoDownloadIgnoreReUploads, serverConfig.autoDownloadIgnoreReUploads)
|
||||||
|
updateSetting(settings.downloadConversions, serverConfig.downloadConversions) { list ->
|
||||||
|
list.associate {
|
||||||
|
it.mimeType to
|
||||||
|
ServerConfig.DownloadConversion(
|
||||||
|
target = it.target,
|
||||||
|
compressionLevel = it.compressionLevel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// extension
|
// extension
|
||||||
updateSetting(settings.extensionRepos, serverConfig.extensionRepos)
|
updateSetting(settings.extensionRepos, serverConfig.extensionRepos)
|
||||||
@@ -218,7 +239,12 @@ class SettingsMutation {
|
|||||||
val (clientMutationId) = input
|
val (clientMutationId) = input
|
||||||
|
|
||||||
GlobalConfigManager.resetUserConfig()
|
GlobalConfigManager.resetUserConfig()
|
||||||
val defaultServerConfig = ServerConfig({ GlobalConfigManager.config.getConfig(SERVER_CONFIG_MODULE_NAME) })
|
val defaultServerConfig =
|
||||||
|
ServerConfig {
|
||||||
|
GlobalConfigManager.config.getConfig(
|
||||||
|
SERVER_CONFIG_MODULE_NAME,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val settings = SettingsType(defaultServerConfig)
|
val settings = SettingsType(defaultServerConfig)
|
||||||
updateSettings(settings)
|
updateSettings(settings)
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ interface Settings : Node {
|
|||||||
val autoDownloadAheadLimit: Int?
|
val autoDownloadAheadLimit: Int?
|
||||||
val autoDownloadNewChaptersLimit: Int?
|
val autoDownloadNewChaptersLimit: Int?
|
||||||
val autoDownloadIgnoreReUploads: Boolean?
|
val autoDownloadIgnoreReUploads: Boolean?
|
||||||
|
val downloadConversions: List<SettingsDownloadConversion>?
|
||||||
|
|
||||||
// extension
|
// extension
|
||||||
val extensionRepos: List<String>?
|
val extensionRepos: List<String>?
|
||||||
@@ -113,6 +114,18 @@ interface Settings : Node {
|
|||||||
val opdsChapterSortOrder: SortOrder?
|
val opdsChapterSortOrder: SortOrder?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SettingsDownloadConversion {
|
||||||
|
val mimeType: String
|
||||||
|
val target: String
|
||||||
|
val compressionLevel: Float?
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsDownloadConversionType(
|
||||||
|
override val mimeType: String,
|
||||||
|
override val target: String,
|
||||||
|
override val compressionLevel: Float?,
|
||||||
|
) : SettingsDownloadConversion
|
||||||
|
|
||||||
data class PartialSettingsType(
|
data class PartialSettingsType(
|
||||||
override val ip: String?,
|
override val ip: String?,
|
||||||
override val port: Int?,
|
override val port: Int?,
|
||||||
@@ -142,6 +155,7 @@ data class PartialSettingsType(
|
|||||||
override val autoDownloadAheadLimit: Int?,
|
override val autoDownloadAheadLimit: Int?,
|
||||||
override val autoDownloadNewChaptersLimit: Int?,
|
override val autoDownloadNewChaptersLimit: Int?,
|
||||||
override val autoDownloadIgnoreReUploads: Boolean?,
|
override val autoDownloadIgnoreReUploads: Boolean?,
|
||||||
|
override val downloadConversions: List<SettingsDownloadConversionType>?,
|
||||||
// extension
|
// extension
|
||||||
override val extensionRepos: List<String>?,
|
override val extensionRepos: List<String>?,
|
||||||
// requests
|
// requests
|
||||||
@@ -222,7 +236,8 @@ class SettingsType(
|
|||||||
)
|
)
|
||||||
override val autoDownloadAheadLimit: Int,
|
override val autoDownloadAheadLimit: Int,
|
||||||
override val autoDownloadNewChaptersLimit: Int,
|
override val autoDownloadNewChaptersLimit: Int,
|
||||||
override val autoDownloadIgnoreReUploads: Boolean?,
|
override val autoDownloadIgnoreReUploads: Boolean,
|
||||||
|
override val downloadConversions: List<SettingsDownloadConversionType>,
|
||||||
// extension
|
// extension
|
||||||
override val extensionRepos: List<String>,
|
override val extensionRepos: List<String>,
|
||||||
// requests
|
// requests
|
||||||
@@ -299,6 +314,13 @@ class SettingsType(
|
|||||||
config.autoDownloadNewChaptersLimit.value, // deprecated
|
config.autoDownloadNewChaptersLimit.value, // deprecated
|
||||||
config.autoDownloadNewChaptersLimit.value,
|
config.autoDownloadNewChaptersLimit.value,
|
||||||
config.autoDownloadIgnoreReUploads.value,
|
config.autoDownloadIgnoreReUploads.value,
|
||||||
|
config.downloadConversions.value.map {
|
||||||
|
SettingsDownloadConversionType(
|
||||||
|
it.key,
|
||||||
|
it.value.target,
|
||||||
|
it.value.compressionLevel,
|
||||||
|
)
|
||||||
|
},
|
||||||
// extension
|
// extension
|
||||||
config.extensionRepos.value,
|
config.extensionRepos.value,
|
||||||
// requests
|
// requests
|
||||||
|
|||||||
@@ -403,6 +403,7 @@ object MangaController {
|
|||||||
pathParam<Int>("chapterIndex"),
|
pathParam<Int>("chapterIndex"),
|
||||||
pathParam<Int>("index"),
|
pathParam<Int>("index"),
|
||||||
queryParam<Boolean?>("updateProgress"),
|
queryParam<Boolean?>("updateProgress"),
|
||||||
|
queryParam<String?>("format"),
|
||||||
documentWith = {
|
documentWith = {
|
||||||
withOperation {
|
withOperation {
|
||||||
summary("Get a chapter page")
|
summary("Get a chapter page")
|
||||||
@@ -411,9 +412,9 @@ object MangaController {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
behaviorOf = { ctx, mangaId, chapterIndex, index, updateProgress ->
|
behaviorOf = { ctx, mangaId, chapterIndex, index, updateProgress, format ->
|
||||||
ctx.future {
|
ctx.future {
|
||||||
future { Page.getPageImage(mangaId, chapterIndex, index, null) }
|
future { Page.getPageImage(mangaId, chapterIndex, index, format, null) }
|
||||||
.thenApply {
|
.thenApply {
|
||||||
ctx.header("content-type", it.second)
|
ctx.header("content-type", it.second)
|
||||||
val httpCacheSeconds = 1.days.inWholeSeconds
|
val httpCacheSeconds = 1.days.inWholeSeconds
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.source.local.LocalSource
|
|||||||
import eu.kanade.tachiyomi.source.model.Page
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import libcore.net.MimeUtils
|
||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import org.jetbrains.exposed.sql.and
|
import org.jetbrains.exposed.sql.and
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
@@ -23,8 +24,12 @@ import suwayomi.tachidesk.manga.impl.util.storage.ImageUtil
|
|||||||
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.PageTable
|
import suwayomi.tachidesk.manga.model.table.PageTable
|
||||||
|
import suwayomi.tachidesk.util.ConversionUtil
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
|
||||||
object Page {
|
object Page {
|
||||||
/**
|
/**
|
||||||
@@ -45,6 +50,7 @@ object Page {
|
|||||||
mangaId: Int,
|
mangaId: Int,
|
||||||
chapterIndex: Int,
|
chapterIndex: Int,
|
||||||
index: Int,
|
index: Int,
|
||||||
|
format: String? = null,
|
||||||
progressFlow: ((StateFlow<Int>) -> Unit)? = null,
|
progressFlow: ((StateFlow<Int>) -> Unit)? = null,
|
||||||
): Pair<InputStream, String> {
|
): Pair<InputStream, String> {
|
||||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||||
@@ -61,7 +67,7 @@ object Page {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (chapterEntry[ChapterTable.isDownloaded]) {
|
if (chapterEntry[ChapterTable.isDownloaded]) {
|
||||||
return ChapterDownloadHelper.getImage(mangaId, chapterId, index)
|
return convertImageResponse(ChapterDownloadHelper.getImage(mangaId, chapterId, index), format)
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// ignore and fetch again
|
// ignore and fetch again
|
||||||
@@ -90,12 +96,15 @@ object Page {
|
|||||||
// is of archive format
|
// is of archive format
|
||||||
if (LocalSource.pageCache.containsKey(chapterEntry[ChapterTable.url])) {
|
if (LocalSource.pageCache.containsKey(chapterEntry[ChapterTable.url])) {
|
||||||
val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]
|
val pageStream = LocalSource.pageCache[chapterEntry[ChapterTable.url]]!![index]
|
||||||
return pageStream() to (ImageUtil.findImageType { pageStream() }?.mime ?: "image/jpeg")
|
return convertImageResponse(pageStream() to (ImageUtil.findImageType { pageStream() }?.mime ?: "image/jpeg"), format)
|
||||||
}
|
}
|
||||||
|
|
||||||
// is of directory format
|
// is of directory format
|
||||||
val imageFile = File(tachiyomiPage.imageUrl!!)
|
val imageFile = File(tachiyomiPage.imageUrl!!)
|
||||||
return imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg")
|
return convertImageResponse(
|
||||||
|
imageFile.inputStream() to (ImageUtil.findImageType { imageFile.inputStream() }?.mime ?: "image/jpeg"),
|
||||||
|
format,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
val source = getCatalogueSourceOrStub(mangaEntry[MangaTable.sourceReference])
|
||||||
@@ -115,9 +124,38 @@ object Page {
|
|||||||
val cacheSaveDir = getChapterCachePath(mangaId, chapterId)
|
val cacheSaveDir = getChapterCachePath(mangaId, chapterId)
|
||||||
|
|
||||||
// Note: don't care about invalidating cache because OS cache is not permanent
|
// Note: don't care about invalidating cache because OS cache is not permanent
|
||||||
return getImageResponse(cacheSaveDir, fileName) {
|
return convertImageResponse(
|
||||||
|
getImageResponse(cacheSaveDir, fileName) {
|
||||||
source.getImage(tachiyomiPage)
|
source.getImage(tachiyomiPage)
|
||||||
|
},
|
||||||
|
format,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun convertImageResponse(
|
||||||
|
image: Pair<InputStream, String>,
|
||||||
|
format: String? = null,
|
||||||
|
): Pair<InputStream, String> {
|
||||||
|
val imageExtension = MimeUtils.guessExtensionFromMimeType(image.second) ?: image.second.removePrefix("image/")
|
||||||
|
|
||||||
|
val targetExtension =
|
||||||
|
(if (format != imageExtension) format else null)
|
||||||
|
?: return image
|
||||||
|
|
||||||
|
val outStream = ByteArrayOutputStream()
|
||||||
|
val writers = ImageIO.getImageWritersBySuffix(targetExtension)
|
||||||
|
val writer = writers.next()
|
||||||
|
ImageIO.createImageOutputStream(outStream).use { o ->
|
||||||
|
writer.setOutput(o)
|
||||||
|
|
||||||
|
val inImage =
|
||||||
|
ConversionUtil.readImage(image.first, image.second)
|
||||||
|
?: throw NoSuchElementException("No conversion to $targetExtension possible")
|
||||||
|
writer.write(inImage)
|
||||||
|
}
|
||||||
|
writer.dispose()
|
||||||
|
val inStream = ByteArrayInputStream(outStream.toByteArray())
|
||||||
|
return Pair(inStream.buffered(), MimeUtils.guessMimeTypeFromExtension(targetExtension) ?: "image/$targetExtension")
|
||||||
}
|
}
|
||||||
|
|
||||||
/** converts 0 to "001" */
|
/** converts 0 to "001" */
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupChapter
|
|||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupHistory
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupManga
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings
|
||||||
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupServerSettings.BackupSettingsDownloadConversionType
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupSource
|
||||||
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
|
import suwayomi.tachidesk.manga.impl.backup.proto.models.BackupTracking
|
||||||
import suwayomi.tachidesk.manga.impl.track.Track
|
import suwayomi.tachidesk.manga.impl.track.Track
|
||||||
@@ -408,6 +409,14 @@ object ProtoBackupExport : ProtoBackupBase() {
|
|||||||
autoDownloadAheadLimit = 0, // deprecated
|
autoDownloadAheadLimit = 0, // deprecated
|
||||||
autoDownloadNewChaptersLimit = serverConfig.autoDownloadNewChaptersLimit.value,
|
autoDownloadNewChaptersLimit = serverConfig.autoDownloadNewChaptersLimit.value,
|
||||||
autoDownloadIgnoreReUploads = serverConfig.autoDownloadIgnoreReUploads.value,
|
autoDownloadIgnoreReUploads = serverConfig.autoDownloadIgnoreReUploads.value,
|
||||||
|
downloadConversions =
|
||||||
|
serverConfig.downloadConversions.value.map {
|
||||||
|
BackupSettingsDownloadConversionType(
|
||||||
|
it.key,
|
||||||
|
it.value.target,
|
||||||
|
it.value.compressionLevel,
|
||||||
|
)
|
||||||
|
},
|
||||||
// extension
|
// extension
|
||||||
extensionRepos = serverConfig.extensionRepos.value,
|
extensionRepos = serverConfig.extensionRepos.value,
|
||||||
// requests
|
// requests
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
|||||||
import org.jetbrains.exposed.sql.SortOrder
|
import org.jetbrains.exposed.sql.SortOrder
|
||||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||||
import suwayomi.tachidesk.graphql.types.Settings
|
import suwayomi.tachidesk.graphql.types.Settings
|
||||||
|
import suwayomi.tachidesk.graphql.types.SettingsDownloadConversion
|
||||||
import suwayomi.tachidesk.graphql.types.WebUIChannel
|
import suwayomi.tachidesk.graphql.types.WebUIChannel
|
||||||
import suwayomi.tachidesk.graphql.types.WebUIFlavor
|
import suwayomi.tachidesk.graphql.types.WebUIFlavor
|
||||||
import suwayomi.tachidesk.graphql.types.WebUIInterface
|
import suwayomi.tachidesk.graphql.types.WebUIInterface
|
||||||
@@ -35,6 +36,7 @@ data class BackupServerSettings(
|
|||||||
@ProtoNumber(19) override var autoDownloadAheadLimit: Int,
|
@ProtoNumber(19) override var autoDownloadAheadLimit: Int,
|
||||||
@ProtoNumber(20) override var autoDownloadNewChaptersLimit: Int,
|
@ProtoNumber(20) override var autoDownloadNewChaptersLimit: Int,
|
||||||
@ProtoNumber(21) override var autoDownloadIgnoreReUploads: Boolean,
|
@ProtoNumber(21) override var autoDownloadIgnoreReUploads: Boolean,
|
||||||
|
@ProtoNumber(57) override val downloadConversions: List<BackupSettingsDownloadConversionType>?,
|
||||||
// extension
|
// extension
|
||||||
@ProtoNumber(22) override var extensionRepos: List<String>,
|
@ProtoNumber(22) override var extensionRepos: List<String>,
|
||||||
// requests
|
// requests
|
||||||
@@ -82,4 +84,11 @@ data class BackupServerSettings(
|
|||||||
@ProtoNumber(53) override var opdsShowOnlyUnreadChapters: Boolean,
|
@ProtoNumber(53) override var opdsShowOnlyUnreadChapters: Boolean,
|
||||||
@ProtoNumber(54) override var opdsShowOnlyDownloadedChapters: Boolean,
|
@ProtoNumber(54) override var opdsShowOnlyDownloadedChapters: Boolean,
|
||||||
@ProtoNumber(55) override var opdsChapterSortOrder: SortOrder,
|
@ProtoNumber(55) override var opdsChapterSortOrder: SortOrder,
|
||||||
) : Settings
|
) : Settings {
|
||||||
|
@Serializable
|
||||||
|
class BackupSettingsDownloadConversionType(
|
||||||
|
@ProtoNumber(1) override val mimeType: String,
|
||||||
|
@ProtoNumber(2) override val target: String,
|
||||||
|
@ProtoNumber(3) override val compressionLevel: Float?,
|
||||||
|
) : SettingsDownloadConversion
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package suwayomi.tachidesk.manga.impl.download.fileProvider
|
package suwayomi.tachidesk.manga.impl.download.fileProvider
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.source.local.metadata.COMIC_INFO_FILE
|
import eu.kanade.tachiyomi.source.local.metadata.COMIC_INFO_FILE
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -8,6 +9,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.flow.sample
|
import kotlinx.coroutines.flow.sample
|
||||||
|
import libcore.net.MimeUtils
|
||||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||||
import org.jetbrains.exposed.sql.selectAll
|
import org.jetbrains.exposed.sql.selectAll
|
||||||
@@ -21,8 +23,14 @@ import suwayomi.tachidesk.manga.impl.util.getChapterDownloadPath
|
|||||||
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
|
import suwayomi.tachidesk.manga.impl.util.storage.ImageResponse
|
||||||
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.server.serverConfig
|
||||||
|
import suwayomi.tachidesk.util.ConversionUtil
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
import javax.imageio.IIOImage
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
import javax.imageio.ImageWriteParam
|
||||||
|
|
||||||
sealed class FileType {
|
sealed class FileType {
|
||||||
data class RegularFile(
|
data class RegularFile(
|
||||||
@@ -61,6 +69,8 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
|||||||
val mangaId: Int,
|
val mangaId: Int,
|
||||||
val chapterId: Int,
|
val chapterId: Int,
|
||||||
) : DownloadedFilesProvider {
|
) : DownloadedFilesProvider {
|
||||||
|
protected val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
protected abstract fun getImageFiles(): List<Type>
|
protected abstract fun getImageFiles(): List<Type>
|
||||||
|
|
||||||
protected abstract fun getImageInputStream(image: Type): InputStream
|
protected abstract fun getImageInputStream(image: Type): InputStream
|
||||||
@@ -75,7 +85,7 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
|||||||
val image = images[index]
|
val image = images[index]
|
||||||
val imageFileType = image.getExtension()
|
val imageFileType = image.getExtension()
|
||||||
|
|
||||||
return Pair(getImageInputStream(image).buffered(), "image/$imageFileType")
|
return Pair(getImageInputStream(image).buffered(), MimeUtils.guessMimeTypeFromExtension(imageFileType) ?: "image/$imageFileType")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getImageCount(): Int = getImageFiles().filter { it.getName() != COMIC_INFO_FILE }.size
|
fun getImageCount(): Int = getImageFiles().filter { it.getName() != COMIC_INFO_FILE }.size
|
||||||
@@ -166,6 +176,8 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
maybeConvertChapterImages(downloadCacheFolder)
|
||||||
|
|
||||||
handleSuccessfulDownload()
|
handleSuccessfulDownload()
|
||||||
|
|
||||||
transaction {
|
transaction {
|
||||||
@@ -185,4 +197,64 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
|||||||
abstract override fun delete(): Boolean
|
abstract override fun delete(): Boolean
|
||||||
|
|
||||||
abstract fun getAsArchiveStream(): Pair<InputStream, Long>
|
abstract fun getAsArchiveStream(): Pair<InputStream, Long>
|
||||||
|
|
||||||
|
private fun maybeConvertChapterImages(chapterCacheFolder: File) {
|
||||||
|
if (chapterCacheFolder.isDirectory) {
|
||||||
|
val conv = serverConfig.downloadConversions.value
|
||||||
|
chapterCacheFolder
|
||||||
|
.listFiles()
|
||||||
|
.orEmpty()
|
||||||
|
.filter { it.name != COMIC_INFO_FILE }
|
||||||
|
.forEach {
|
||||||
|
val imageType = MimeUtils.guessMimeTypeFromExtension(it.extension) ?: return@forEach
|
||||||
|
val targetConversion =
|
||||||
|
conv.getOrElse(imageType) {
|
||||||
|
conv.getOrElse("default") {
|
||||||
|
logger.debug { "Skipping conversion of $it since no conversion specified" }
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val targetMime = targetConversion.target
|
||||||
|
if (imageType == targetMime || targetMime == "none") return@forEach // nothing to do
|
||||||
|
logger.debug { "Converting $it to $targetMime" }
|
||||||
|
val targetExtension = MimeUtils.guessExtensionFromMimeType(targetMime) ?: targetMime.removePrefix("image/")
|
||||||
|
|
||||||
|
val outFile = File(it.parentFile, it.nameWithoutExtension + "." + targetExtension)
|
||||||
|
|
||||||
|
val writers = ImageIO.getImageWritersByMIMEType(targetMime)
|
||||||
|
val writer =
|
||||||
|
try {
|
||||||
|
writers.next()
|
||||||
|
} catch (_: NoSuchElementException) {
|
||||||
|
logger.warn { "Conversion aborted: No reader for target format $targetMime" }
|
||||||
|
return@forEach
|
||||||
|
}
|
||||||
|
val writerParams = writer.defaultWriteParam
|
||||||
|
targetConversion.compressionLevel?.let {
|
||||||
|
writerParams.compressionMode = ImageWriteParam.MODE_EXPLICIT
|
||||||
|
writerParams.compressionQuality = it
|
||||||
|
}
|
||||||
|
val success =
|
||||||
|
try {
|
||||||
|
ImageIO.createImageOutputStream(outFile)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
logger.warn(e) { "Conversion aborted" }
|
||||||
|
return@forEach
|
||||||
|
}.use { outStream ->
|
||||||
|
writer.setOutput(outStream)
|
||||||
|
|
||||||
|
val inImage = ConversionUtil.readImage(it) ?: return@use false
|
||||||
|
writer.write(null, IIOImage(inImage, null, null), writerParams)
|
||||||
|
return@use true
|
||||||
|
}
|
||||||
|
writer.dispose()
|
||||||
|
if (success) {
|
||||||
|
it.delete()
|
||||||
|
} else {
|
||||||
|
logger.warn { "Conversion aborted: No reader for image $it" }
|
||||||
|
outFile.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ fun interface RetrieveFile1Args<A> : RetrieveFile {
|
|||||||
override fun executeGetImage(vararg args: Any): Pair<InputStream, String> = execute(args[0] as A)
|
override fun executeGetImage(vararg args: Any): Pair<InputStream, String> = execute(args[0] as A)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun interface RetrieveFile2Args<A, B> : RetrieveFile {
|
||||||
|
fun execute(
|
||||||
|
a: A,
|
||||||
|
b: B,
|
||||||
|
): Pair<InputStream, String>
|
||||||
|
|
||||||
|
override fun executeGetImage(vararg args: Any): Pair<InputStream, String> = execute(args[0] as A, args[1] as B)
|
||||||
|
}
|
||||||
|
|
||||||
fun interface FileRetriever {
|
fun interface FileRetriever {
|
||||||
fun getImage(): RetrieveFile
|
fun getImage(): RetrieveFile
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import suwayomi.tachidesk.manga.impl.track.tracker.model.Track
|
|||||||
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
import suwayomi.tachidesk.manga.impl.track.tracker.model.TrackSearch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
|
|
||||||
class KitsuApi(
|
class KitsuApi(
|
||||||
private val client: OkHttpClient,
|
private val client: OkHttpClient,
|
||||||
@@ -151,7 +150,7 @@ class KitsuApi(
|
|||||||
withIOContext {
|
withIOContext {
|
||||||
val jsonObject =
|
val jsonObject =
|
||||||
buildJsonObject {
|
buildJsonObject {
|
||||||
put("params", "query=${URLEncoder.encode(query, StandardCharsets.UTF_8.name())}$ALGOLIA_FILTER")
|
put("params", "query=${URLEncoder.encode(query, Charsets.UTF_8)}$ALGOLIA_FILTER")
|
||||||
}
|
}
|
||||||
|
|
||||||
with(json) {
|
with(json) {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package suwayomi.tachidesk.opds.util
|
|||||||
|
|
||||||
import suwayomi.tachidesk.server.serverConfig
|
import suwayomi.tachidesk.server.serverConfig
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.text.Normalizer
|
import java.text.Normalizer
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -15,7 +14,7 @@ object OpdsStringUtil {
|
|||||||
* Encodes a string to be used in OPDS URLs.
|
* Encodes a string to be used in OPDS URLs.
|
||||||
* @return The URL-encoded string
|
* @return The URL-encoded string
|
||||||
*/
|
*/
|
||||||
fun String.encodeForOpdsURL(): String = URLEncoder.encode(this, StandardCharsets.UTF_8.toString())
|
fun String.encodeForOpdsURL(): String = URLEncoder.encode(this, Charsets.UTF_8)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts a string into a URL-friendly slug.
|
* Converts a string into a URL-friendly slug.
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
package suwayomi.tachidesk.server
|
|
||||||
|
|
||||||
interface ConfigAdapter<T> {
|
|
||||||
fun toType(configValue: String): T
|
|
||||||
}
|
|
||||||
|
|
||||||
object StringConfigAdapter : ConfigAdapter<String> {
|
|
||||||
override fun toType(configValue: String): String = configValue
|
|
||||||
}
|
|
||||||
|
|
||||||
object IntConfigAdapter : ConfigAdapter<Int> {
|
|
||||||
override fun toType(configValue: String): Int = configValue.toInt()
|
|
||||||
}
|
|
||||||
|
|
||||||
object BooleanConfigAdapter : ConfigAdapter<Boolean> {
|
|
||||||
override fun toType(configValue: String): Boolean = configValue.toBoolean()
|
|
||||||
}
|
|
||||||
|
|
||||||
object DoubleConfigAdapter : ConfigAdapter<Double> {
|
|
||||||
override fun toType(configValue: String): Double = configValue.toDouble()
|
|
||||||
}
|
|
||||||
|
|
||||||
class EnumConfigAdapter<T : Enum<T>>(
|
|
||||||
private val enumClass: Class<T>,
|
|
||||||
) : ConfigAdapter<T> {
|
|
||||||
override fun toType(configValue: String): T = java.lang.Enum.valueOf(enumClass, configValue.uppercase())
|
|
||||||
}
|
|
||||||
@@ -33,7 +33,6 @@ import suwayomi.tachidesk.server.util.WebInterfaceManager
|
|||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
import java.nio.charset.StandardCharsets
|
|
||||||
import java.util.concurrent.CompletableFuture
|
import java.util.concurrent.CompletableFuture
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.time.Duration.Companion.days
|
import kotlin.time.Duration.Companion.days
|
||||||
@@ -168,7 +167,7 @@ object JavalinSetup {
|
|||||||
return@beforeMatched
|
return@beforeMatched
|
||||||
}
|
}
|
||||||
|
|
||||||
val authMode = serverConfig.authMode.value ?: AuthMode.NONE
|
val authMode = serverConfig.authMode.value
|
||||||
|
|
||||||
fun credentialsValid(): Boolean {
|
fun credentialsValid(): Boolean {
|
||||||
val basicAuthCredentials = ctx.basicAuthCredentials() ?: return false
|
val basicAuthCredentials = ctx.basicAuthCredentials() ?: return false
|
||||||
@@ -187,7 +186,7 @@ object JavalinSetup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (authMode == AuthMode.SIMPLE_LOGIN && !cookieValid()) {
|
if (authMode == AuthMode.SIMPLE_LOGIN && !cookieValid()) {
|
||||||
val url = "/login.html?redirect=" + URLEncoder.encode(ctx.fullUrl(), StandardCharsets.UTF_8)
|
val url = "/login.html?redirect=" + URLEncoder.encode(ctx.fullUrl(), Charsets.UTF_8)
|
||||||
ctx.header("Location", url)
|
ctx.header("Location", url)
|
||||||
throw RedirectResponse(HttpStatus.SEE_OTHER)
|
throw RedirectResponse(HttpStatus.SEE_OTHER)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ 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 io.github.config4k.getValue
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
@@ -38,39 +39,29 @@ const val SERVER_CONFIG_MODULE_NAME = "server"
|
|||||||
|
|
||||||
class ServerConfig(
|
class ServerConfig(
|
||||||
getConfig: () -> Config,
|
getConfig: () -> Config,
|
||||||
val moduleName: String = SERVER_CONFIG_MODULE_NAME,
|
|
||||||
) : SystemPropertyOverridableConfigModule(
|
) : SystemPropertyOverridableConfigModule(
|
||||||
getConfig,
|
getConfig,
|
||||||
moduleName,
|
SERVER_CONFIG_MODULE_NAME,
|
||||||
) {
|
) {
|
||||||
open inner class OverrideConfigValue<T>(
|
open inner class OverrideConfigValue {
|
||||||
private val configAdapter: ConfigAdapter<out Any>,
|
var flow: MutableStateFlow<Any>? = null
|
||||||
) {
|
|
||||||
private var flow: MutableStateFlow<T>? = null
|
|
||||||
|
|
||||||
open fun getValueFromConfig(
|
inline operator fun <reified T : MutableStateFlow<R>, reified R> getValue(
|
||||||
thisRef: ServerConfig,
|
thisRef: ServerConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
): Any = configAdapter.toType(overridableConfig.getValue<ServerConfig, String>(thisRef, property))
|
): T {
|
||||||
|
|
||||||
operator fun getValue(
|
|
||||||
thisRef: ServerConfig,
|
|
||||||
property: KProperty<*>,
|
|
||||||
): MutableStateFlow<T> {
|
|
||||||
if (flow != null) {
|
if (flow != null) {
|
||||||
return flow!!
|
return flow as T
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val stateFlow = overridableConfig.getValue<ServerConfig, T>(thisRef, property)
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
val value = getValueFromConfig(thisRef, property) as T
|
flow = stateFlow as MutableStateFlow<Any>
|
||||||
|
|
||||||
val stateFlow = MutableStateFlow(value)
|
|
||||||
flow = stateFlow
|
|
||||||
|
|
||||||
stateFlow
|
stateFlow
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.filter { it != getValueFromConfig(thisRef, property) }
|
.filter { it != thisRef.overridableConfig.getConfig().getValue<ServerConfig, R>(thisRef, property) }
|
||||||
.onEach { GlobalConfigManager.updateValue("$moduleName.${property.name}", it as Any) }
|
.onEach { GlobalConfigManager.updateValue("$moduleName.${property.name}", it as Any) }
|
||||||
.launchIn(mutableConfigValueScope)
|
.launchIn(mutableConfigValueScope)
|
||||||
|
|
||||||
@@ -78,29 +69,12 @@ class ServerConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class OverrideConfigValues<T>(
|
|
||||||
private val configAdapter: ConfigAdapter<out Any>,
|
|
||||||
) : OverrideConfigValue<T>(configAdapter) {
|
|
||||||
override fun getValueFromConfig(
|
|
||||||
thisRef: ServerConfig,
|
|
||||||
property: KProperty<*>,
|
|
||||||
): Any =
|
|
||||||
overridableConfig
|
|
||||||
.getValue<ServerConfig, List<String>>(thisRef, property)
|
|
||||||
.map { configAdapter.toType(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
open inner class MigratedConfigValue<T>(
|
open inner class MigratedConfigValue<T>(
|
||||||
private val readMigrated: () -> Any,
|
private val readMigrated: () -> T,
|
||||||
private val setMigrated: (T) -> Unit,
|
private val setMigrated: (T) -> Unit,
|
||||||
) {
|
) {
|
||||||
private var flow: MutableStateFlow<T>? = null
|
private var flow: MutableStateFlow<T>? = null
|
||||||
|
|
||||||
open fun getValueFromConfig(
|
|
||||||
thisRef: ServerConfig,
|
|
||||||
property: KProperty<*>,
|
|
||||||
): Any = readMigrated()
|
|
||||||
|
|
||||||
operator fun getValue(
|
operator fun getValue(
|
||||||
thisRef: ServerConfig,
|
thisRef: ServerConfig,
|
||||||
property: KProperty<*>,
|
property: KProperty<*>,
|
||||||
@@ -109,8 +83,7 @@ class ServerConfig(
|
|||||||
return flow!!
|
return flow!!
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
val value = readMigrated()
|
||||||
val value = getValueFromConfig(thisRef, property) as T
|
|
||||||
|
|
||||||
val stateFlow = MutableStateFlow(value)
|
val stateFlow = MutableStateFlow(value)
|
||||||
flow = stateFlow
|
flow = stateFlow
|
||||||
@@ -118,7 +91,7 @@ class ServerConfig(
|
|||||||
stateFlow
|
stateFlow
|
||||||
.drop(1)
|
.drop(1)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.filter { it != getValueFromConfig(thisRef, property) }
|
.filter { it != readMigrated() }
|
||||||
.onEach(setMigrated)
|
.onEach(setMigrated)
|
||||||
.launchIn(mutableConfigValueScope)
|
.launchIn(mutableConfigValueScope)
|
||||||
|
|
||||||
@@ -126,51 +99,57 @@ class ServerConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val ip: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val ip: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val port: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
val port: MutableStateFlow<Int> by OverrideConfigValue()
|
||||||
|
|
||||||
// proxy
|
// proxy
|
||||||
val socksProxyEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val socksProxyEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val socksProxyVersion: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
val socksProxyVersion: MutableStateFlow<Int> by OverrideConfigValue()
|
||||||
val socksProxyHost: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val socksProxyHost: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val socksProxyPort: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val socksProxyPort: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val socksProxyUsername: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val socksProxyUsername: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val socksProxyPassword: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val socksProxyPassword: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
|
|
||||||
// webUI
|
// webUI
|
||||||
val webUIEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val webUIEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val webUIFlavor: MutableStateFlow<WebUIFlavor> by OverrideConfigValue(EnumConfigAdapter(WebUIFlavor::class.java))
|
val webUIFlavor: MutableStateFlow<WebUIFlavor> by OverrideConfigValue()
|
||||||
val initialOpenInBrowserEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val initialOpenInBrowserEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val webUIInterface: MutableStateFlow<WebUIInterface> by OverrideConfigValue(EnumConfigAdapter(WebUIInterface::class.java))
|
val webUIInterface: MutableStateFlow<WebUIInterface> by OverrideConfigValue()
|
||||||
val electronPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val electronPath: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val webUIChannel: MutableStateFlow<WebUIChannel> by OverrideConfigValue(EnumConfigAdapter(WebUIChannel::class.java))
|
val webUIChannel: MutableStateFlow<WebUIChannel> by OverrideConfigValue()
|
||||||
val webUIUpdateCheckInterval: MutableStateFlow<Double> by OverrideConfigValue(DoubleConfigAdapter)
|
val webUIUpdateCheckInterval: MutableStateFlow<Double> by OverrideConfigValue()
|
||||||
|
|
||||||
// downloader
|
// downloader
|
||||||
val downloadAsCbz: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val downloadAsCbz: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val downloadsPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val downloadsPath: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val autoDownloadNewChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val autoDownloadNewChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val excludeEntryWithUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val excludeEntryWithUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val autoDownloadNewChaptersLimit: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
val autoDownloadNewChaptersLimit: MutableStateFlow<Int> by OverrideConfigValue()
|
||||||
val autoDownloadIgnoreReUploads: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val autoDownloadIgnoreReUploads: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
|
val downloadConversions: MutableStateFlow<Map<String, DownloadConversion>> by OverrideConfigValue()
|
||||||
|
|
||||||
|
data class DownloadConversion(
|
||||||
|
val target: String,
|
||||||
|
val compressionLevel: Float? = null,
|
||||||
|
)
|
||||||
|
|
||||||
// extensions
|
// extensions
|
||||||
val extensionRepos: MutableStateFlow<List<String>> by OverrideConfigValues(StringConfigAdapter)
|
val extensionRepos: MutableStateFlow<List<String>> by OverrideConfigValue()
|
||||||
|
|
||||||
// requests
|
// requests
|
||||||
val maxSourcesInParallel: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
val maxSourcesInParallel: MutableStateFlow<Int> by OverrideConfigValue()
|
||||||
|
|
||||||
// updater
|
// updater
|
||||||
val excludeUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val excludeUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val excludeNotStarted: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val excludeNotStarted: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val excludeCompleted: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val excludeCompleted: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val globalUpdateInterval: MutableStateFlow<Double> by OverrideConfigValue(DoubleConfigAdapter)
|
val globalUpdateInterval: MutableStateFlow<Double> by OverrideConfigValue()
|
||||||
val updateMangas: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val updateMangas: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue(EnumConfigAdapter(AuthMode::class.java))
|
val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue()
|
||||||
val authUsername: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val authUsername: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val authPassword: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val authPassword: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val basicAuthEnabled: MutableStateFlow<Boolean> by MigratedConfigValue({
|
val basicAuthEnabled: MutableStateFlow<Boolean> by MigratedConfigValue({
|
||||||
authMode.value == AuthMode.BASIC_AUTH
|
authMode.value == AuthMode.BASIC_AUTH
|
||||||
}) {
|
}) {
|
||||||
@@ -184,37 +163,37 @@ class ServerConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// misc
|
// misc
|
||||||
val debugLogsEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val debugLogsEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val systemTrayEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val systemTrayEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val maxLogFiles: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
val maxLogFiles: MutableStateFlow<Int> by OverrideConfigValue()
|
||||||
val maxLogFileSize: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val maxLogFileSize: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val maxLogFolderSize: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val maxLogFolderSize: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
|
|
||||||
// backup
|
// backup
|
||||||
val backupPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val backupPath: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val backupTime: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val backupTime: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val backupInterval: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
val backupInterval: MutableStateFlow<Int> by OverrideConfigValue()
|
||||||
val backupTTL: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
val backupTTL: MutableStateFlow<Int> by OverrideConfigValue()
|
||||||
|
|
||||||
// local source
|
// local source
|
||||||
val localSourcePath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val localSourcePath: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
|
|
||||||
// cloudflare bypass
|
// cloudflare bypass
|
||||||
val flareSolverrEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val flareSolverrEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val flareSolverrUrl: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val flareSolverrUrl: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val flareSolverrTimeout: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
val flareSolverrTimeout: MutableStateFlow<Int> by OverrideConfigValue()
|
||||||
val flareSolverrSessionName: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
val flareSolverrSessionName: MutableStateFlow<String> by OverrideConfigValue()
|
||||||
val flareSolverrSessionTtl: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
val flareSolverrSessionTtl: MutableStateFlow<Int> by OverrideConfigValue()
|
||||||
val flareSolverrAsResponseFallback: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val flareSolverrAsResponseFallback: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
|
|
||||||
// opds settings
|
// opds settings
|
||||||
val opdsUseBinaryFileSizes: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val opdsUseBinaryFileSizes: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val opdsItemsPerPage: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
val opdsItemsPerPage: MutableStateFlow<Int> by OverrideConfigValue()
|
||||||
val opdsEnablePageReadProgress: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val opdsEnablePageReadProgress: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val opdsMarkAsReadOnDownload: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val opdsMarkAsReadOnDownload: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val opdsShowOnlyUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val opdsShowOnlyUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val opdsShowOnlyDownloadedChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
val opdsShowOnlyDownloadedChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||||
val opdsChapterSortOrder: MutableStateFlow<SortOrder> by OverrideConfigValue(EnumConfigAdapter(SortOrder::class.java))
|
val opdsChapterSortOrder: MutableStateFlow<SortOrder> by OverrideConfigValue()
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
fun <T> subscribeTo(
|
fun <T> subscribeTo(
|
||||||
@@ -259,6 +238,11 @@ class ServerConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun register(getConfig: () -> Config) = ServerConfig({ getConfig().getConfig(SERVER_CONFIG_MODULE_NAME) })
|
fun register(getConfig: () -> Config) =
|
||||||
|
ServerConfig {
|
||||||
|
getConfig().getConfig(
|
||||||
|
SERVER_CONFIG_MODULE_NAME,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import com.typesafe.config.Config
|
|||||||
import com.typesafe.config.ConfigException
|
import com.typesafe.config.ConfigException
|
||||||
import com.typesafe.config.ConfigRenderOptions
|
import com.typesafe.config.ConfigRenderOptions
|
||||||
import com.typesafe.config.ConfigValue
|
import com.typesafe.config.ConfigValue
|
||||||
import com.typesafe.config.ConfigValueFactory
|
|
||||||
import com.typesafe.config.parser.ConfigDocument
|
import com.typesafe.config.parser.ConfigDocument
|
||||||
import dev.datlag.kcef.KCEF
|
import dev.datlag.kcef.KCEF
|
||||||
import eu.kanade.tachiyomi.App
|
import eu.kanade.tachiyomi.App
|
||||||
import eu.kanade.tachiyomi.createAppModule
|
import eu.kanade.tachiyomi.createAppModule
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.source.local.LocalSource
|
import eu.kanade.tachiyomi.source.local.LocalSource
|
||||||
|
import io.github.config4k.registerCustomType
|
||||||
|
import io.github.config4k.toConfig
|
||||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.javalin.json.JavalinJackson
|
import io.javalin.json.JavalinJackson
|
||||||
import io.javalin.json.JsonMapper
|
import io.javalin.json.JsonMapper
|
||||||
@@ -44,10 +45,10 @@ import suwayomi.tachidesk.manga.impl.download.DownloadManager
|
|||||||
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
||||||
import suwayomi.tachidesk.manga.impl.update.Updater
|
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.BooleanConfigAdapter
|
|
||||||
import suwayomi.tachidesk.server.database.databaseUp
|
import suwayomi.tachidesk.server.database.databaseUp
|
||||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||||
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
||||||
|
import suwayomi.tachidesk.server.util.MutableStateFlowType
|
||||||
import suwayomi.tachidesk.server.util.SystemTray
|
import suwayomi.tachidesk.server.util.SystemTray
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
@@ -147,7 +148,7 @@ fun <T : Any> migrateConfig(
|
|||||||
if (typedValue != null) {
|
if (typedValue != null) {
|
||||||
return configDocument.withValue(
|
return configDocument.withValue(
|
||||||
toConfigKey,
|
toConfigKey,
|
||||||
ConfigValueFactory.fromAnyRef(typedValue),
|
typedValue.toConfig("internal").getValue("internal"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (_: ConfigException) {
|
} catch (_: ConfigException) {
|
||||||
@@ -174,6 +175,7 @@ fun applicationSetup() {
|
|||||||
mainLoop.start()
|
mainLoop.start()
|
||||||
|
|
||||||
// register Tachidesk's config which is dubbed "ServerConfig"
|
// register Tachidesk's config which is dubbed "ServerConfig"
|
||||||
|
registerCustomType(MutableStateFlowType())
|
||||||
GlobalConfigManager.registerModule(
|
GlobalConfigManager.registerModule(
|
||||||
ServerConfig.register { GlobalConfigManager.config },
|
ServerConfig.register { GlobalConfigManager.config },
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package suwayomi.tachidesk.server.util
|
||||||
|
|
||||||
|
import com.typesafe.config.Config
|
||||||
|
import io.github.config4k.ClassContainer
|
||||||
|
import io.github.config4k.CustomType
|
||||||
|
import io.github.config4k.readers.SelectReader
|
||||||
|
import io.github.config4k.toConfig
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
|
class MutableStateFlowType : CustomType {
|
||||||
|
override fun parse(
|
||||||
|
clazz: ClassContainer,
|
||||||
|
config: Config,
|
||||||
|
name: String,
|
||||||
|
): Any? {
|
||||||
|
val reader =
|
||||||
|
SelectReader.getReader(
|
||||||
|
clazz.typeArguments.entries
|
||||||
|
.first()
|
||||||
|
.value,
|
||||||
|
)
|
||||||
|
val path = name
|
||||||
|
val result = reader(config, path)
|
||||||
|
return MutableStateFlow(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun testParse(clazz: ClassContainer): Boolean =
|
||||||
|
clazz.mapperClass.qualifiedName == "kotlinx.coroutines.flow.MutableStateFlow" ||
|
||||||
|
clazz.mapperClass.qualifiedName == "kotlinx.coroutines.flow.StateFlow" ||
|
||||||
|
clazz.mapperClass.qualifiedName == "kotlinx.coroutines.flow.StateFlowImpl"
|
||||||
|
|
||||||
|
override fun testToConfig(obj: Any): Boolean = (obj as? StateFlow<*>)?.value != null
|
||||||
|
|
||||||
|
override fun toConfig(
|
||||||
|
obj: Any,
|
||||||
|
name: String,
|
||||||
|
): Config = (obj as StateFlow<*>).value!!.toConfig(name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package suwayomi.tachidesk.util
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import java.awt.image.BufferedImage
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
import javax.imageio.ImageIO
|
||||||
|
|
||||||
|
object ConversionUtil {
|
||||||
|
val logger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
public fun readImage(image: File): BufferedImage? {
|
||||||
|
val readers = ImageIO.getImageReadersBySuffix(image.extension)
|
||||||
|
image.inputStream().use {
|
||||||
|
ImageIO.createImageInputStream(it).use { inputStream ->
|
||||||
|
for (reader in readers) {
|
||||||
|
try {
|
||||||
|
reader.setInput(inputStream)
|
||||||
|
return reader.read(0)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logger.debug(e) { "Reader ${reader.javaClass.name} not suitable" }
|
||||||
|
} finally {
|
||||||
|
reader.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info { "No suitable image converter found for ${image.name}" }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
public fun readImage(
|
||||||
|
image: InputStream,
|
||||||
|
mimeType: String,
|
||||||
|
): BufferedImage? {
|
||||||
|
val readers = ImageIO.getImageReadersByMIMEType(mimeType)
|
||||||
|
ImageIO.createImageInputStream(image).use { inputStream ->
|
||||||
|
for (reader in readers) {
|
||||||
|
try {
|
||||||
|
reader.setInput(inputStream)
|
||||||
|
return reader.read(0)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
logger.debug(e) { "Reader ${reader.javaClass.name} not suitable" }
|
||||||
|
} finally {
|
||||||
|
reader.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info { "No suitable image converter found for $mimeType" }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -26,6 +26,12 @@ server.autoDownloadNewChapters = false # if new chapters that have been retrieve
|
|||||||
server.excludeEntryWithUnreadChapters = true # ignore automatic chapter downloads of entries with unread chapters
|
server.excludeEntryWithUnreadChapters = true # ignore automatic chapter downloads of entries with unread chapters
|
||||||
server.autoDownloadNewChaptersLimit = 0 # 0 to disable it - how many unread downloaded chapters should be available - if the limit is reached, new chapters won't be downloaded automatically. this limit will also be applied to the auto download of new chapters on an update
|
server.autoDownloadNewChaptersLimit = 0 # 0 to disable it - how many unread downloaded chapters should be available - if the limit is reached, new chapters won't be downloaded automatically. this limit will also be applied to the auto download of new chapters on an update
|
||||||
server.autoDownloadIgnoreReUploads = false # decides if re-uploads should be ignored during auto download of new chapters
|
server.autoDownloadIgnoreReUploads = false # decides if re-uploads should be ignored during auto download of new chapters
|
||||||
|
server.downloadConversions = {}
|
||||||
|
# map input mime type to conversion information, or "default" for others
|
||||||
|
# server.downloadConversions."image/webp" = {
|
||||||
|
# target = "image/jpeg" # image type to convert to
|
||||||
|
# compressionLevel = 0.8 # quality in range [0,1], leave away to use default compression
|
||||||
|
# }
|
||||||
|
|
||||||
# extension repos
|
# extension repos
|
||||||
server.extensionRepos = [
|
server.extensionRepos = [
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ class TestUpdater : IUpdater {
|
|||||||
TODO("Not yet implemented")
|
TODO("Not yet implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun deleteLastAutomatedUpdateTimestamp() {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
override fun addCategoriesToUpdateQueue(
|
override fun addCategoriesToUpdateQueue(
|
||||||
categories: List<CategoryDataClass>,
|
categories: List<CategoryDataClass>,
|
||||||
clear: Boolean?,
|
clear: Boolean?,
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ server.autoDownloadNewChapters = false # if new chapters that have been retrieve
|
|||||||
server.excludeEntryWithUnreadChapters = true # ignore automatic chapter downloads of entries with unread chapters
|
server.excludeEntryWithUnreadChapters = true # ignore automatic chapter downloads of entries with unread chapters
|
||||||
server.autoDownloadNewChaptersLimit = 0 # 0 to disable it - how many unread downloaded chapters should be available - if the limit is reached, new chapters won't be downloaded automatically. this limit will also be applied to the auto download of new chapters on an update
|
server.autoDownloadNewChaptersLimit = 0 # 0 to disable it - how many unread downloaded chapters should be available - if the limit is reached, new chapters won't be downloaded automatically. this limit will also be applied to the auto download of new chapters on an update
|
||||||
server.autoDownloadIgnoreReUploads = false # decides if re-uploads should be ignored during auto download of new chapters
|
server.autoDownloadIgnoreReUploads = false # decides if re-uploads should be ignored during auto download of new chapters
|
||||||
|
server.downloadConversions = {}
|
||||||
|
# map input mime type to conversion information, or "default" for others
|
||||||
|
# server.downloadConversions."image/webp" = {
|
||||||
|
# target = "image/jpeg" # image type to convert to
|
||||||
|
# compressionLevel = 0.8 # quality in range [0,1], leave away to use default compression
|
||||||
|
# }
|
||||||
|
|
||||||
# extension repos
|
# extension repos
|
||||||
server.extensionRepos = [
|
server.extensionRepos = [
|
||||||
|
|||||||
Reference in New Issue
Block a user