[#1496] Image conversion (#1505)

* [#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:
Constantin Piber
2025-07-14 23:51:18 +02:00
committed by GitHub
parent 09c950a890
commit df0078b725
24 changed files with 464 additions and 167 deletions

View File

@@ -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),
)

View File

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

View File

@@ -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