mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01: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 com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import com.typesafe.config.ConfigObject
|
||||
import com.typesafe.config.ConfigValue
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import com.typesafe.config.parser.ConfigDocument
|
||||
import com.typesafe.config.parser.ConfigDocumentFactory
|
||||
import io.github.config4k.toConfig
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
@@ -113,7 +114,7 @@ open class ConfigManager {
|
||||
) {
|
||||
mutex.withLock {
|
||||
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)
|
||||
internalConfig = internalConfig.withValue(path, configValue)
|
||||
@@ -142,8 +143,13 @@ open class ConfigManager {
|
||||
val serverConfig = ConfigFactory.parseResources("server-reference.conf")
|
||||
val userConfig = getUserConfig()
|
||||
|
||||
val hasMissingSettings = serverConfig.entrySet().any { !userConfig.hasPath(it.key) }
|
||||
val hasOutdatedSettings = userConfig.entrySet().any { !serverConfig.hasPath(it.key) }
|
||||
// NOTE: if more than 1 dot is included, that's a nested setting, which we need to filter out here
|
||||
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
|
||||
if (!isUserConfigOutdated) {
|
||||
return
|
||||
@@ -159,7 +165,8 @@ open class ConfigManager {
|
||||
.filter {
|
||||
serverConfig.hasPath(
|
||||
it.key,
|
||||
)
|
||||
) ||
|
||||
it.key.count { c -> c == '.' } > 1
|
||||
}.forEach { newUserConfigDoc = newUserConfigDoc.withValue(it.key, it.value) }
|
||||
|
||||
newUserConfigDoc =
|
||||
|
||||
@@ -8,9 +8,13 @@ package xyz.nulldev.ts.config
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import com.typesafe.config.ConfigException
|
||||
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.readers.SelectReader
|
||||
import io.github.config4k.toConfig
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
/**
|
||||
@@ -26,7 +30,7 @@ abstract class ConfigModule(
|
||||
*/
|
||||
abstract class SystemPropertyOverridableConfigModule(
|
||||
getConfig: () -> Config,
|
||||
moduleName: String,
|
||||
val moduleName: String,
|
||||
) : ConfigModule(getConfig) {
|
||||
val overridableConfig = SystemPropertyOverrideDelegate(getConfig, moduleName)
|
||||
}
|
||||
@@ -46,20 +50,31 @@ class SystemPropertyOverrideDelegate(
|
||||
val combined =
|
||||
System.getProperty(
|
||||
"$CONFIG_PREFIX.$moduleName.${property.name}",
|
||||
if (T::class.simpleName == "List") {
|
||||
ConfigValueFactory.fromAnyRef(configValue).render()
|
||||
} else {
|
||||
configValue.toString()
|
||||
},
|
||||
configValue!!
|
||||
.toConfig("internal")
|
||||
.root()
|
||||
.render()
|
||||
.removePrefix("internal="),
|
||||
)
|
||||
val combinedConfig =
|
||||
try {
|
||||
ConfigFactory.parseString(combined)
|
||||
} catch (_: ConfigException) {
|
||||
ConfigFactory.parseString("internal=$combined")
|
||||
}
|
||||
|
||||
return when (T::class.simpleName) {
|
||||
"Int" -> combined.toInt()
|
||||
"Boolean" -> combined.toBoolean()
|
||||
"Double" -> combined.toDouble()
|
||||
"List" -> ConfigFactory.parseString("internal=" + combined).getStringList("internal").orEmpty()
|
||||
// add more types as needed
|
||||
else -> combined // covers String
|
||||
} as T
|
||||
val genericType = object : TypeReference<T>() {}.genericType()
|
||||
val clazz = ClassContainer(T::class, genericType)
|
||||
val reader = SelectReader.getReader(clazz)
|
||||
val path = property.name
|
||||
|
||||
val result = reader(combinedConfig, "internal")
|
||||
return try {
|
||||
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
|
||||
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-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
|
||||
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.ChapterTable
|
||||
import suwayomi.tachidesk.server.JavalinSetup.future
|
||||
import java.net.URLEncoder
|
||||
import java.time.Instant
|
||||
import java.util.concurrent.CompletableFuture
|
||||
|
||||
@@ -226,7 +227,15 @@ class ChapterMutation {
|
||||
data class FetchChapterPagesInput(
|
||||
val clientMutationId: String? = null,
|
||||
val chapterId: Int,
|
||||
)
|
||||
val format: String? = null,
|
||||
) {
|
||||
fun toParams(): Map<String, String> =
|
||||
buildMap {
|
||||
if (!format.isNullOrBlank()) {
|
||||
put("format", format)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class FetchChapterPagesPayload(
|
||||
val clientMutationId: String?,
|
||||
@@ -236,16 +245,32 @@ class ChapterMutation {
|
||||
|
||||
fun fetchChapterPages(input: FetchChapterPagesInput): CompletableFuture<DataFetcherResult<FetchChapterPagesPayload?>> {
|
||||
val (clientMutationId, chapterId) = input
|
||||
val paramsMap = input.toParams()
|
||||
|
||||
return future {
|
||||
asDataFetcherResult {
|
||||
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(
|
||||
clientMutationId = clientMutationId,
|
||||
pages =
|
||||
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),
|
||||
)
|
||||
|
||||
@@ -111,6 +111,18 @@ class SettingsMutation {
|
||||
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
|
||||
fun updateSettings(settings: Settings) {
|
||||
updateSetting(settings.ip, serverConfig.ip)
|
||||
@@ -140,6 +152,15 @@ class SettingsMutation {
|
||||
updateSetting(settings.autoDownloadAheadLimit, serverConfig.autoDownloadNewChaptersLimit) // deprecated
|
||||
updateSetting(settings.autoDownloadNewChaptersLimit, serverConfig.autoDownloadNewChaptersLimit)
|
||||
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
|
||||
updateSetting(settings.extensionRepos, serverConfig.extensionRepos)
|
||||
@@ -218,7 +239,12 @@ class SettingsMutation {
|
||||
val (clientMutationId) = input
|
||||
|
||||
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)
|
||||
updateSettings(settings)
|
||||
|
||||
@@ -48,6 +48,7 @@ interface Settings : Node {
|
||||
val autoDownloadAheadLimit: Int?
|
||||
val autoDownloadNewChaptersLimit: Int?
|
||||
val autoDownloadIgnoreReUploads: Boolean?
|
||||
val downloadConversions: List<SettingsDownloadConversion>?
|
||||
|
||||
// extension
|
||||
val extensionRepos: List<String>?
|
||||
@@ -113,6 +114,18 @@ interface Settings : Node {
|
||||
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(
|
||||
override val ip: String?,
|
||||
override val port: Int?,
|
||||
@@ -142,6 +155,7 @@ data class PartialSettingsType(
|
||||
override val autoDownloadAheadLimit: Int?,
|
||||
override val autoDownloadNewChaptersLimit: Int?,
|
||||
override val autoDownloadIgnoreReUploads: Boolean?,
|
||||
override val downloadConversions: List<SettingsDownloadConversionType>?,
|
||||
// extension
|
||||
override val extensionRepos: List<String>?,
|
||||
// requests
|
||||
@@ -222,7 +236,8 @@ class SettingsType(
|
||||
)
|
||||
override val autoDownloadAheadLimit: Int,
|
||||
override val autoDownloadNewChaptersLimit: Int,
|
||||
override val autoDownloadIgnoreReUploads: Boolean?,
|
||||
override val autoDownloadIgnoreReUploads: Boolean,
|
||||
override val downloadConversions: List<SettingsDownloadConversionType>,
|
||||
// extension
|
||||
override val extensionRepos: List<String>,
|
||||
// requests
|
||||
@@ -299,6 +314,13 @@ class SettingsType(
|
||||
config.autoDownloadNewChaptersLimit.value, // deprecated
|
||||
config.autoDownloadNewChaptersLimit.value,
|
||||
config.autoDownloadIgnoreReUploads.value,
|
||||
config.downloadConversions.value.map {
|
||||
SettingsDownloadConversionType(
|
||||
it.key,
|
||||
it.value.target,
|
||||
it.value.compressionLevel,
|
||||
)
|
||||
},
|
||||
// extension
|
||||
config.extensionRepos.value,
|
||||
// requests
|
||||
|
||||
@@ -403,6 +403,7 @@ object MangaController {
|
||||
pathParam<Int>("chapterIndex"),
|
||||
pathParam<Int>("index"),
|
||||
queryParam<Boolean?>("updateProgress"),
|
||||
queryParam<String?>("format"),
|
||||
documentWith = {
|
||||
withOperation {
|
||||
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 {
|
||||
future { Page.getPageImage(mangaId, chapterIndex, index, null) }
|
||||
future { Page.getPageImage(mangaId, chapterIndex, index, format, null) }
|
||||
.thenApply {
|
||||
ctx.header("content-type", it.second)
|
||||
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.online.HttpSource
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import libcore.net.MimeUtils
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import org.jetbrains.exposed.sql.and
|
||||
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.MangaTable
|
||||
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.InputStream
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
object Page {
|
||||
/**
|
||||
@@ -45,6 +50,7 @@ object Page {
|
||||
mangaId: Int,
|
||||
chapterIndex: Int,
|
||||
index: Int,
|
||||
format: String? = null,
|
||||
progressFlow: ((StateFlow<Int>) -> Unit)? = null,
|
||||
): Pair<InputStream, String> {
|
||||
val mangaEntry = transaction { MangaTable.selectAll().where { MangaTable.id eq mangaId }.first() }
|
||||
@@ -61,7 +67,7 @@ object Page {
|
||||
|
||||
try {
|
||||
if (chapterEntry[ChapterTable.isDownloaded]) {
|
||||
return ChapterDownloadHelper.getImage(mangaId, chapterId, index)
|
||||
return convertImageResponse(ChapterDownloadHelper.getImage(mangaId, chapterId, index), format)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
// ignore and fetch again
|
||||
@@ -90,12 +96,15 @@ object Page {
|
||||
// is of archive format
|
||||
if (LocalSource.pageCache.containsKey(chapterEntry[ChapterTable.url])) {
|
||||
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
|
||||
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])
|
||||
@@ -115,9 +124,38 @@ object Page {
|
||||
val cacheSaveDir = getChapterCachePath(mangaId, chapterId)
|
||||
|
||||
// Note: don't care about invalidating cache because OS cache is not permanent
|
||||
return getImageResponse(cacheSaveDir, fileName) {
|
||||
source.getImage(tachiyomiPage)
|
||||
return convertImageResponse(
|
||||
getImageResponse(cacheSaveDir, fileName) {
|
||||
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" */
|
||||
|
||||
@@ -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.BackupManga
|
||||
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.BackupTracking
|
||||
import suwayomi.tachidesk.manga.impl.track.Track
|
||||
@@ -408,6 +409,14 @@ object ProtoBackupExport : ProtoBackupBase() {
|
||||
autoDownloadAheadLimit = 0, // deprecated
|
||||
autoDownloadNewChaptersLimit = serverConfig.autoDownloadNewChaptersLimit.value,
|
||||
autoDownloadIgnoreReUploads = serverConfig.autoDownloadIgnoreReUploads.value,
|
||||
downloadConversions =
|
||||
serverConfig.downloadConversions.value.map {
|
||||
BackupSettingsDownloadConversionType(
|
||||
it.key,
|
||||
it.value.target,
|
||||
it.value.compressionLevel,
|
||||
)
|
||||
},
|
||||
// extension
|
||||
extensionRepos = serverConfig.extensionRepos.value,
|
||||
// requests
|
||||
|
||||
@@ -5,6 +5,7 @@ import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import org.jetbrains.exposed.sql.SortOrder
|
||||
import suwayomi.tachidesk.graphql.types.AuthMode
|
||||
import suwayomi.tachidesk.graphql.types.Settings
|
||||
import suwayomi.tachidesk.graphql.types.SettingsDownloadConversion
|
||||
import suwayomi.tachidesk.graphql.types.WebUIChannel
|
||||
import suwayomi.tachidesk.graphql.types.WebUIFlavor
|
||||
import suwayomi.tachidesk.graphql.types.WebUIInterface
|
||||
@@ -35,6 +36,7 @@ data class BackupServerSettings(
|
||||
@ProtoNumber(19) override var autoDownloadAheadLimit: Int,
|
||||
@ProtoNumber(20) override var autoDownloadNewChaptersLimit: Int,
|
||||
@ProtoNumber(21) override var autoDownloadIgnoreReUploads: Boolean,
|
||||
@ProtoNumber(57) override val downloadConversions: List<BackupSettingsDownloadConversionType>?,
|
||||
// extension
|
||||
@ProtoNumber(22) override var extensionRepos: List<String>,
|
||||
// requests
|
||||
@@ -82,4 +84,11 @@ data class BackupServerSettings(
|
||||
@ProtoNumber(53) override var opdsShowOnlyUnreadChapters: Boolean,
|
||||
@ProtoNumber(54) override var opdsShowOnlyDownloadedChapters: Boolean,
|
||||
@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
|
||||
|
||||
import eu.kanade.tachiyomi.source.local.metadata.COMIC_INFO_FILE
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -8,6 +9,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.sample
|
||||
import libcore.net.MimeUtils
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
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.model.table.ChapterTable
|
||||
import suwayomi.tachidesk.manga.model.table.MangaTable
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import suwayomi.tachidesk.util.ConversionUtil
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import javax.imageio.IIOImage
|
||||
import javax.imageio.ImageIO
|
||||
import javax.imageio.ImageWriteParam
|
||||
|
||||
sealed class FileType {
|
||||
data class RegularFile(
|
||||
@@ -61,6 +69,8 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
||||
val mangaId: Int,
|
||||
val chapterId: Int,
|
||||
) : DownloadedFilesProvider {
|
||||
protected val logger = KotlinLogging.logger {}
|
||||
|
||||
protected abstract fun getImageFiles(): List<Type>
|
||||
|
||||
protected abstract fun getImageInputStream(image: Type): InputStream
|
||||
@@ -75,7 +85,7 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
||||
val image = images[index]
|
||||
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
|
||||
@@ -166,6 +176,8 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
||||
},
|
||||
)
|
||||
|
||||
maybeConvertChapterImages(downloadCacheFolder)
|
||||
|
||||
handleSuccessfulDownload()
|
||||
|
||||
transaction {
|
||||
@@ -185,4 +197,64 @@ abstract class ChaptersFilesProvider<Type : FileType>(
|
||||
abstract override fun delete(): Boolean
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@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 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 uy.kohesive.injekt.injectLazy
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
class KitsuApi(
|
||||
private val client: OkHttpClient,
|
||||
@@ -151,7 +150,7 @@ class KitsuApi(
|
||||
withIOContext {
|
||||
val jsonObject =
|
||||
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) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package suwayomi.tachidesk.opds.util
|
||||
|
||||
import suwayomi.tachidesk.server.serverConfig
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.text.Normalizer
|
||||
|
||||
/**
|
||||
@@ -15,7 +14,7 @@ object OpdsStringUtil {
|
||||
* Encodes a string to be used in OPDS URLs.
|
||||
* @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.
|
||||
|
||||
@@ -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 java.io.IOException
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.time.Duration.Companion.days
|
||||
@@ -168,7 +167,7 @@ object JavalinSetup {
|
||||
return@beforeMatched
|
||||
}
|
||||
|
||||
val authMode = serverConfig.authMode.value ?: AuthMode.NONE
|
||||
val authMode = serverConfig.authMode.value
|
||||
|
||||
fun credentialsValid(): Boolean {
|
||||
val basicAuthCredentials = ctx.basicAuthCredentials() ?: return false
|
||||
@@ -187,7 +186,7 @@ object JavalinSetup {
|
||||
}
|
||||
|
||||
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)
|
||||
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/. */
|
||||
|
||||
import com.typesafe.config.Config
|
||||
import io.github.config4k.getValue
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
@@ -38,39 +39,29 @@ const val SERVER_CONFIG_MODULE_NAME = "server"
|
||||
|
||||
class ServerConfig(
|
||||
getConfig: () -> Config,
|
||||
val moduleName: String = SERVER_CONFIG_MODULE_NAME,
|
||||
) : SystemPropertyOverridableConfigModule(
|
||||
getConfig,
|
||||
moduleName,
|
||||
SERVER_CONFIG_MODULE_NAME,
|
||||
) {
|
||||
open inner class OverrideConfigValue<T>(
|
||||
private val configAdapter: ConfigAdapter<out Any>,
|
||||
) {
|
||||
private var flow: MutableStateFlow<T>? = null
|
||||
open inner class OverrideConfigValue {
|
||||
var flow: MutableStateFlow<Any>? = null
|
||||
|
||||
open fun getValueFromConfig(
|
||||
inline operator fun <reified T : MutableStateFlow<R>, reified R> getValue(
|
||||
thisRef: ServerConfig,
|
||||
property: KProperty<*>,
|
||||
): Any = configAdapter.toType(overridableConfig.getValue<ServerConfig, String>(thisRef, property))
|
||||
|
||||
operator fun getValue(
|
||||
thisRef: ServerConfig,
|
||||
property: KProperty<*>,
|
||||
): MutableStateFlow<T> {
|
||||
): T {
|
||||
if (flow != null) {
|
||||
return flow!!
|
||||
return flow as T
|
||||
}
|
||||
|
||||
val stateFlow = overridableConfig.getValue<ServerConfig, T>(thisRef, property)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val value = getValueFromConfig(thisRef, property) as T
|
||||
|
||||
val stateFlow = MutableStateFlow(value)
|
||||
flow = stateFlow
|
||||
flow = stateFlow as MutableStateFlow<Any>
|
||||
|
||||
stateFlow
|
||||
.drop(1)
|
||||
.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) }
|
||||
.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>(
|
||||
private val readMigrated: () -> Any,
|
||||
private val readMigrated: () -> T,
|
||||
private val setMigrated: (T) -> Unit,
|
||||
) {
|
||||
private var flow: MutableStateFlow<T>? = null
|
||||
|
||||
open fun getValueFromConfig(
|
||||
thisRef: ServerConfig,
|
||||
property: KProperty<*>,
|
||||
): Any = readMigrated()
|
||||
|
||||
operator fun getValue(
|
||||
thisRef: ServerConfig,
|
||||
property: KProperty<*>,
|
||||
@@ -109,8 +83,7 @@ class ServerConfig(
|
||||
return flow!!
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val value = getValueFromConfig(thisRef, property) as T
|
||||
val value = readMigrated()
|
||||
|
||||
val stateFlow = MutableStateFlow(value)
|
||||
flow = stateFlow
|
||||
@@ -118,7 +91,7 @@ class ServerConfig(
|
||||
stateFlow
|
||||
.drop(1)
|
||||
.distinctUntilChanged()
|
||||
.filter { it != getValueFromConfig(thisRef, property) }
|
||||
.filter { it != readMigrated() }
|
||||
.onEach(setMigrated)
|
||||
.launchIn(mutableConfigValueScope)
|
||||
|
||||
@@ -126,51 +99,57 @@ class ServerConfig(
|
||||
}
|
||||
}
|
||||
|
||||
val ip: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val port: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val ip: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val port: MutableStateFlow<Int> by OverrideConfigValue()
|
||||
|
||||
// proxy
|
||||
val socksProxyEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val socksProxyVersion: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val socksProxyHost: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val socksProxyPort: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val socksProxyUsername: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val socksProxyPassword: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val socksProxyEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val socksProxyVersion: MutableStateFlow<Int> by OverrideConfigValue()
|
||||
val socksProxyHost: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val socksProxyPort: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val socksProxyUsername: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val socksProxyPassword: MutableStateFlow<String> by OverrideConfigValue()
|
||||
|
||||
// webUI
|
||||
val webUIEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val webUIFlavor: MutableStateFlow<WebUIFlavor> by OverrideConfigValue(EnumConfigAdapter(WebUIFlavor::class.java))
|
||||
val initialOpenInBrowserEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val webUIInterface: MutableStateFlow<WebUIInterface> by OverrideConfigValue(EnumConfigAdapter(WebUIInterface::class.java))
|
||||
val electronPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val webUIChannel: MutableStateFlow<WebUIChannel> by OverrideConfigValue(EnumConfigAdapter(WebUIChannel::class.java))
|
||||
val webUIUpdateCheckInterval: MutableStateFlow<Double> by OverrideConfigValue(DoubleConfigAdapter)
|
||||
val webUIEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val webUIFlavor: MutableStateFlow<WebUIFlavor> by OverrideConfigValue()
|
||||
val initialOpenInBrowserEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val webUIInterface: MutableStateFlow<WebUIInterface> by OverrideConfigValue()
|
||||
val electronPath: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val webUIChannel: MutableStateFlow<WebUIChannel> by OverrideConfigValue()
|
||||
val webUIUpdateCheckInterval: MutableStateFlow<Double> by OverrideConfigValue()
|
||||
|
||||
// downloader
|
||||
val downloadAsCbz: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val downloadsPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val autoDownloadNewChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val excludeEntryWithUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val autoDownloadNewChaptersLimit: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val autoDownloadIgnoreReUploads: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val downloadAsCbz: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val downloadsPath: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val autoDownloadNewChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val excludeEntryWithUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val autoDownloadNewChaptersLimit: MutableStateFlow<Int> by OverrideConfigValue()
|
||||
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
|
||||
val extensionRepos: MutableStateFlow<List<String>> by OverrideConfigValues(StringConfigAdapter)
|
||||
val extensionRepos: MutableStateFlow<List<String>> by OverrideConfigValue()
|
||||
|
||||
// requests
|
||||
val maxSourcesInParallel: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val maxSourcesInParallel: MutableStateFlow<Int> by OverrideConfigValue()
|
||||
|
||||
// updater
|
||||
val excludeUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val excludeNotStarted: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val excludeCompleted: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val globalUpdateInterval: MutableStateFlow<Double> by OverrideConfigValue(DoubleConfigAdapter)
|
||||
val updateMangas: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val excludeUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val excludeNotStarted: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val excludeCompleted: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val globalUpdateInterval: MutableStateFlow<Double> by OverrideConfigValue()
|
||||
val updateMangas: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
|
||||
// Authentication
|
||||
val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue(EnumConfigAdapter(AuthMode::class.java))
|
||||
val authUsername: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val authPassword: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val authMode: MutableStateFlow<AuthMode> by OverrideConfigValue()
|
||||
val authUsername: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val authPassword: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val basicAuthEnabled: MutableStateFlow<Boolean> by MigratedConfigValue({
|
||||
authMode.value == AuthMode.BASIC_AUTH
|
||||
}) {
|
||||
@@ -184,37 +163,37 @@ class ServerConfig(
|
||||
}
|
||||
|
||||
// misc
|
||||
val debugLogsEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val systemTrayEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val maxLogFiles: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val maxLogFileSize: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val maxLogFolderSize: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val debugLogsEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val systemTrayEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val maxLogFiles: MutableStateFlow<Int> by OverrideConfigValue()
|
||||
val maxLogFileSize: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val maxLogFolderSize: MutableStateFlow<String> by OverrideConfigValue()
|
||||
|
||||
// backup
|
||||
val backupPath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val backupTime: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val backupInterval: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val backupTTL: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val backupPath: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val backupTime: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val backupInterval: MutableStateFlow<Int> by OverrideConfigValue()
|
||||
val backupTTL: MutableStateFlow<Int> by OverrideConfigValue()
|
||||
|
||||
// local source
|
||||
val localSourcePath: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val localSourcePath: MutableStateFlow<String> by OverrideConfigValue()
|
||||
|
||||
// cloudflare bypass
|
||||
val flareSolverrEnabled: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val flareSolverrUrl: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val flareSolverrTimeout: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val flareSolverrSessionName: MutableStateFlow<String> by OverrideConfigValue(StringConfigAdapter)
|
||||
val flareSolverrSessionTtl: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val flareSolverrAsResponseFallback: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val flareSolverrEnabled: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val flareSolverrUrl: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val flareSolverrTimeout: MutableStateFlow<Int> by OverrideConfigValue()
|
||||
val flareSolverrSessionName: MutableStateFlow<String> by OverrideConfigValue()
|
||||
val flareSolverrSessionTtl: MutableStateFlow<Int> by OverrideConfigValue()
|
||||
val flareSolverrAsResponseFallback: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
|
||||
// opds settings
|
||||
val opdsUseBinaryFileSizes: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val opdsItemsPerPage: MutableStateFlow<Int> by OverrideConfigValue(IntConfigAdapter)
|
||||
val opdsEnablePageReadProgress: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val opdsMarkAsReadOnDownload: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val opdsShowOnlyUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val opdsShowOnlyDownloadedChapters: MutableStateFlow<Boolean> by OverrideConfigValue(BooleanConfigAdapter)
|
||||
val opdsChapterSortOrder: MutableStateFlow<SortOrder> by OverrideConfigValue(EnumConfigAdapter(SortOrder::class.java))
|
||||
val opdsUseBinaryFileSizes: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val opdsItemsPerPage: MutableStateFlow<Int> by OverrideConfigValue()
|
||||
val opdsEnablePageReadProgress: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val opdsMarkAsReadOnDownload: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val opdsShowOnlyUnreadChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val opdsShowOnlyDownloadedChapters: MutableStateFlow<Boolean> by OverrideConfigValue()
|
||||
val opdsChapterSortOrder: MutableStateFlow<SortOrder> by OverrideConfigValue()
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun <T> subscribeTo(
|
||||
@@ -259,6 +238,11 @@ class ServerConfig(
|
||||
}
|
||||
|
||||
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.ConfigRenderOptions
|
||||
import com.typesafe.config.ConfigValue
|
||||
import com.typesafe.config.ConfigValueFactory
|
||||
import com.typesafe.config.parser.ConfigDocument
|
||||
import dev.datlag.kcef.KCEF
|
||||
import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.createAppModule
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
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.javalin.json.JavalinJackson
|
||||
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.Updater
|
||||
import suwayomi.tachidesk.manga.impl.util.lang.renameTo
|
||||
import suwayomi.tachidesk.server.BooleanConfigAdapter
|
||||
import suwayomi.tachidesk.server.database.databaseUp
|
||||
import suwayomi.tachidesk.server.generated.BuildConfig
|
||||
import suwayomi.tachidesk.server.util.AppMutex.handleAppMutex
|
||||
import suwayomi.tachidesk.server.util.MutableStateFlowType
|
||||
import suwayomi.tachidesk.server.util.SystemTray
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
@@ -147,7 +148,7 @@ fun <T : Any> migrateConfig(
|
||||
if (typedValue != null) {
|
||||
return configDocument.withValue(
|
||||
toConfigKey,
|
||||
ConfigValueFactory.fromAnyRef(typedValue),
|
||||
typedValue.toConfig("internal").getValue("internal"),
|
||||
)
|
||||
}
|
||||
} catch (_: ConfigException) {
|
||||
@@ -174,6 +175,7 @@ fun applicationSetup() {
|
||||
mainLoop.start()
|
||||
|
||||
// register Tachidesk's config which is dubbed "ServerConfig"
|
||||
registerCustomType(MutableStateFlowType())
|
||||
GlobalConfigManager.registerModule(
|
||||
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.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.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
|
||||
server.extensionRepos = [
|
||||
|
||||
@@ -31,6 +31,10 @@ class TestUpdater : IUpdater {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun deleteLastAutomatedUpdateTimestamp() {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun addCategoriesToUpdateQueue(
|
||||
categories: List<CategoryDataClass>,
|
||||
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.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.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
|
||||
server.extensionRepos = [
|
||||
|
||||
Reference in New Issue
Block a user