mirror of
https://github.com/Suwayomi/Suwayomi-Server.git
synced 2026-07-04 19:34:35 -05:00
* Non-Extension Index changes for 1.6 * Changelog * Minor fixes * Implement extension store * Test build fix * Docs * Simplify fetching manga and chapters * Use EMPTY JsonObject * Update docs/Configuring-Suwayomi‐Server.md Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com> * Improve Fetch Extension Store * Fixes * Simplify deprecated isNsfw in SourceQuery * Simplify ContentRating in Source.kt * Simplify isNsfw in SourceType * No magic numbers for ContentRating, improves safety for future versions of extension api * Fix SearchTest * Lint * Lint * Optimize imports and fix unchecked cast warning * Proper extension store queries * Optimize import fixes * Add ContentRatingFilter * Improve extension store sync * fix: re-sync (#2121) * Lint * Add ExtenionStores to the fetchExtensions result since its possible for the stores to change. * Use a single version of ContentRating * Exclude ServerConfig.extensionStores from GraphQL * Use syncDbToPrefs in ExtensionStoreMutation * Optimize Imports * Update server/server-config/src/main/kotlin/suwayomi/tachidesk/server/ServerConfig.kt Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com> * Remove replaceWith and add specific description for GQL APIs * Include OkHttp ZSTD * Update to latest Mihon extension lib * Fix latest Mihon Extension Lib * Lint * Optimize imports * Lint * Review fixes * Add a index to extesnion table store url * Lint --------- Co-authored-by: Constantin Piber <59023762+cpiber@users.noreply.github.com>
403 lines
13 KiB
Kotlin
403 lines
13 KiB
Kotlin
@file:Suppress("RedundantNullableReturnType", "unused")
|
|
|
|
package suwayomi.tachidesk.graphql.mutations
|
|
|
|
import com.expediagroup.graphql.generator.annotations.GraphQLDeprecated
|
|
import org.jetbrains.exposed.v1.core.LikePattern
|
|
import org.jetbrains.exposed.v1.core.Op
|
|
import org.jetbrains.exposed.v1.core.and
|
|
import org.jetbrains.exposed.v1.core.eq
|
|
import org.jetbrains.exposed.v1.core.inList
|
|
import org.jetbrains.exposed.v1.core.like
|
|
import org.jetbrains.exposed.v1.core.or
|
|
import org.jetbrains.exposed.v1.jdbc.deleteWhere
|
|
import org.jetbrains.exposed.v1.jdbc.selectAll
|
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
|
import org.jetbrains.exposed.v1.jdbc.update
|
|
import suwayomi.tachidesk.graphql.directives.RequireAuth
|
|
import suwayomi.tachidesk.graphql.types.ChapterType
|
|
import suwayomi.tachidesk.graphql.types.MangaMetaType
|
|
import suwayomi.tachidesk.graphql.types.MangaType
|
|
import suwayomi.tachidesk.graphql.types.MetaInput
|
|
import suwayomi.tachidesk.manga.impl.Library
|
|
import suwayomi.tachidesk.manga.impl.Manga
|
|
import suwayomi.tachidesk.manga.impl.update.IUpdater
|
|
import suwayomi.tachidesk.manga.model.table.ChapterTable
|
|
import suwayomi.tachidesk.manga.model.table.MangaMetaTable
|
|
import suwayomi.tachidesk.manga.model.table.MangaTable
|
|
import suwayomi.tachidesk.manga.model.table.toDataClass
|
|
import suwayomi.tachidesk.server.JavalinSetup.future
|
|
import uy.kohesive.injekt.injectLazy
|
|
import java.time.Instant
|
|
import java.util.concurrent.CompletableFuture
|
|
|
|
/**
|
|
* TODO Mutations
|
|
* - Download x(all = -1) chapters
|
|
* - Delete read/all downloaded chapters
|
|
*/
|
|
class MangaMutation {
|
|
private val updater: IUpdater by injectLazy()
|
|
|
|
data class UpdateMangaPatch(
|
|
val inLibrary: Boolean? = null,
|
|
)
|
|
|
|
data class UpdateMangaPayload(
|
|
val clientMutationId: String?,
|
|
val manga: MangaType,
|
|
)
|
|
|
|
data class UpdateMangaInput(
|
|
val clientMutationId: String? = null,
|
|
val id: Int,
|
|
val patch: UpdateMangaPatch,
|
|
)
|
|
|
|
data class UpdateMangasPayload(
|
|
val clientMutationId: String?,
|
|
val mangas: List<MangaType>,
|
|
)
|
|
|
|
data class UpdateMangasInput(
|
|
val clientMutationId: String? = null,
|
|
val ids: List<Int>,
|
|
val patch: UpdateMangaPatch,
|
|
)
|
|
|
|
private suspend fun updateMangas(
|
|
ids: List<Int>,
|
|
patch: UpdateMangaPatch,
|
|
) {
|
|
transaction {
|
|
if (patch.inLibrary != null) {
|
|
MangaTable.update({ MangaTable.id inList ids }) { update ->
|
|
patch.inLibrary.also {
|
|
update[inLibrary] = it
|
|
if (it) update[inLibraryAt] = Instant.now().epochSecond
|
|
}
|
|
}
|
|
}
|
|
}.apply {
|
|
if (patch.inLibrary != null) {
|
|
transaction {
|
|
// try to initialize uninitialized in library manga to ensure that the expected data is available (chapter list, metadata, ...)
|
|
val mangas =
|
|
transaction {
|
|
MangaTable
|
|
.selectAll()
|
|
.where { (MangaTable.id inList ids) and (MangaTable.initialized eq false) }
|
|
.map { MangaTable.toDataClass(it) }
|
|
}
|
|
|
|
updater.addMangasToQueue(mangas)
|
|
}
|
|
|
|
ids.forEach {
|
|
Library.handleMangaThumbnail(it, patch.inLibrary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@RequireAuth
|
|
fun updateManga(input: UpdateMangaInput): CompletableFuture<UpdateMangaPayload?> {
|
|
val (clientMutationId, id, patch) = input
|
|
|
|
return future {
|
|
updateMangas(listOf(id), patch)
|
|
|
|
val manga =
|
|
transaction {
|
|
MangaType(MangaTable.selectAll().where { MangaTable.id eq id }.first())
|
|
}
|
|
|
|
UpdateMangaPayload(
|
|
clientMutationId = clientMutationId,
|
|
manga = manga,
|
|
)
|
|
}
|
|
}
|
|
|
|
@RequireAuth
|
|
fun updateMangas(input: UpdateMangasInput): CompletableFuture<UpdateMangasPayload?> {
|
|
val (clientMutationId, ids, patch) = input
|
|
|
|
return future {
|
|
updateMangas(ids, patch)
|
|
|
|
val mangas =
|
|
transaction {
|
|
MangaTable.selectAll().where { MangaTable.id inList ids }.map { MangaType(it) }
|
|
}
|
|
|
|
UpdateMangasPayload(
|
|
clientMutationId = clientMutationId,
|
|
mangas = mangas,
|
|
)
|
|
}
|
|
}
|
|
|
|
data class FetchMangaInput(
|
|
val clientMutationId: String? = null,
|
|
val id: Int,
|
|
)
|
|
|
|
data class FetchMangaPayload(
|
|
val clientMutationId: String?,
|
|
val manga: MangaType,
|
|
)
|
|
|
|
@RequireAuth
|
|
@GraphQLDeprecated("Deprecated in Tachiyomix 1.6", ReplaceWith("fetchMangaAndChapters"))
|
|
fun fetchManga(input: FetchMangaInput): CompletableFuture<FetchMangaPayload?> {
|
|
val (clientMutationId, id) = input
|
|
|
|
return future {
|
|
Manga.updateMangaAndChapters(id, updateChapters = false)
|
|
|
|
val manga =
|
|
transaction {
|
|
MangaTable.selectAll().where { MangaTable.id eq id }.first()
|
|
}
|
|
FetchMangaPayload(
|
|
clientMutationId = clientMutationId,
|
|
manga = MangaType(manga),
|
|
)
|
|
}
|
|
}
|
|
|
|
data class FetchMangaAndChaptersInput(
|
|
val clientMutationId: String? = null,
|
|
val id: Int,
|
|
val fetchManga: Boolean,
|
|
val fetchChapters: Boolean,
|
|
)
|
|
|
|
data class FetchMangaAndChaptersPayload(
|
|
val clientMutationId: String?,
|
|
val manga: MangaType,
|
|
val chapters: List<ChapterType>,
|
|
)
|
|
|
|
@RequireAuth
|
|
fun fetchMangaAndChapters(input: FetchMangaAndChaptersInput): CompletableFuture<FetchMangaAndChaptersPayload?> {
|
|
val (clientMutationId, id, fetchManga, fetchChapters) = input
|
|
|
|
return future {
|
|
Manga.updateMangaAndChapters(
|
|
mangaId = id,
|
|
updateManga = fetchManga,
|
|
updateChapters = fetchChapters,
|
|
)
|
|
|
|
val (manga, chapters) =
|
|
transaction {
|
|
Pair(
|
|
MangaTable.selectAll().where { MangaTable.id eq id }.first(),
|
|
ChapterTable
|
|
.selectAll()
|
|
.where { ChapterTable.manga eq id }
|
|
.orderBy(ChapterTable.sourceOrder)
|
|
.map { ChapterType(it) },
|
|
)
|
|
}
|
|
FetchMangaAndChaptersPayload(
|
|
clientMutationId = clientMutationId,
|
|
manga = MangaType(manga),
|
|
chapters = chapters,
|
|
)
|
|
}
|
|
}
|
|
|
|
data class SetMangaMetaInput(
|
|
val clientMutationId: String? = null,
|
|
val meta: MangaMetaType,
|
|
)
|
|
|
|
data class SetMangaMetaPayload(
|
|
val clientMutationId: String?,
|
|
val meta: MangaMetaType,
|
|
)
|
|
|
|
@RequireAuth
|
|
fun setMangaMeta(input: SetMangaMetaInput): SetMangaMetaPayload? {
|
|
val (clientMutationId, meta) = input
|
|
|
|
Manga.modifyMangaMeta(meta.mangaId, meta.key, meta.value)
|
|
|
|
return SetMangaMetaPayload(clientMutationId, meta)
|
|
}
|
|
|
|
data class DeleteMangaMetaInput(
|
|
val clientMutationId: String? = null,
|
|
val mangaId: Int,
|
|
val key: String,
|
|
)
|
|
|
|
data class DeleteMangaMetaPayload(
|
|
val clientMutationId: String?,
|
|
val meta: MangaMetaType?,
|
|
val manga: MangaType,
|
|
)
|
|
|
|
@RequireAuth
|
|
fun deleteMangaMeta(input: DeleteMangaMetaInput): DeleteMangaMetaPayload? {
|
|
val (clientMutationId, mangaId, key) = input
|
|
|
|
val (meta, manga) =
|
|
transaction {
|
|
val meta =
|
|
MangaMetaTable
|
|
.selectAll()
|
|
.where { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
|
.firstOrNull()
|
|
|
|
MangaMetaTable.deleteWhere { (MangaMetaTable.ref eq mangaId) and (MangaMetaTable.key eq key) }
|
|
|
|
val manga =
|
|
transaction {
|
|
MangaType(MangaTable.selectAll().where { MangaTable.id eq mangaId }.first())
|
|
}
|
|
|
|
if (meta != null) {
|
|
MangaMetaType(meta)
|
|
} else {
|
|
null
|
|
} to manga
|
|
}
|
|
|
|
return DeleteMangaMetaPayload(clientMutationId, meta, manga)
|
|
}
|
|
|
|
data class SetMangaMetasItem(
|
|
val mangaIds: List<Int>,
|
|
val metas: List<MetaInput>,
|
|
)
|
|
|
|
data class SetMangaMetasInput(
|
|
val clientMutationId: String? = null,
|
|
val items: List<SetMangaMetasItem>,
|
|
)
|
|
|
|
data class SetMangaMetasPayload(
|
|
val clientMutationId: String?,
|
|
val metas: List<MangaMetaType>,
|
|
val mangas: List<MangaType>,
|
|
)
|
|
|
|
@RequireAuth
|
|
fun setMangaMetas(input: SetMangaMetasInput): SetMangaMetasPayload? {
|
|
val (clientMutationId, items) = input
|
|
|
|
val metaByMangaId =
|
|
items
|
|
.flatMap { item ->
|
|
val metaMap = item.metas.associate { it.key to it.value }
|
|
item.mangaIds.map { mangaId -> mangaId to metaMap }
|
|
}.groupBy({ it.first }, { it.second })
|
|
.mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } }
|
|
|
|
Manga.modifyMangasMetas(metaByMangaId)
|
|
|
|
val allMangaIds = metaByMangaId.keys
|
|
val allMetaKeys = metaByMangaId.values.flatMap { it.keys }.distinct()
|
|
|
|
val (updatedMetas, mangas) =
|
|
transaction {
|
|
val updatedMetas =
|
|
MangaMetaTable
|
|
.selectAll()
|
|
.where { (MangaMetaTable.ref inList allMangaIds) and (MangaMetaTable.key inList allMetaKeys) }
|
|
.map { MangaMetaType(it) }
|
|
|
|
val mangas =
|
|
MangaTable
|
|
.selectAll()
|
|
.where { MangaTable.id inList allMangaIds }
|
|
.map { MangaType(it) }
|
|
.distinctBy { it.id }
|
|
|
|
updatedMetas to mangas
|
|
}
|
|
|
|
return SetMangaMetasPayload(clientMutationId, updatedMetas, mangas)
|
|
}
|
|
|
|
data class DeleteMangaMetasItem(
|
|
val mangaIds: List<Int>,
|
|
val keys: List<String>? = null,
|
|
val prefixes: List<String>? = null,
|
|
)
|
|
|
|
data class DeleteMangaMetasInput(
|
|
val clientMutationId: String? = null,
|
|
val items: List<DeleteMangaMetasItem>,
|
|
)
|
|
|
|
data class DeleteMangaMetasPayload(
|
|
val clientMutationId: String?,
|
|
val metas: List<MangaMetaType>,
|
|
val mangas: List<MangaType>,
|
|
)
|
|
|
|
@RequireAuth
|
|
fun deleteMangaMetas(input: DeleteMangaMetasInput): DeleteMangaMetasPayload? {
|
|
val (clientMutationId, items) = input
|
|
|
|
items.forEach { item ->
|
|
require(!item.keys.isNullOrEmpty() || !item.prefixes.isNullOrEmpty()) {
|
|
"Either 'keys' or 'prefixes' must be provided for each item"
|
|
}
|
|
}
|
|
|
|
val (allDeletedMetas, allMangaIds) =
|
|
transaction {
|
|
val deletedMetas = mutableListOf<MangaMetaType>()
|
|
val mangaIds = mutableSetOf<Int>()
|
|
|
|
items.forEach { item ->
|
|
val keyCondition: Op<Boolean>? =
|
|
item.keys?.takeIf { it.isNotEmpty() }?.let { MangaMetaTable.key inList it }
|
|
|
|
val prefixCondition: Op<Boolean>? =
|
|
item.prefixes
|
|
?.filter { it.isNotEmpty() }
|
|
?.map { (MangaMetaTable.key like LikePattern("$it%")) as Op<Boolean> }
|
|
?.reduceOrNull { acc, op -> acc or op }
|
|
|
|
val metaKeyCondition =
|
|
if (keyCondition != null && prefixCondition != null) {
|
|
keyCondition or prefixCondition
|
|
} else {
|
|
keyCondition ?: prefixCondition!!
|
|
}
|
|
|
|
val condition = (MangaMetaTable.ref inList item.mangaIds) and metaKeyCondition
|
|
|
|
deletedMetas +=
|
|
MangaMetaTable
|
|
.selectAll()
|
|
.where { condition }
|
|
.map { MangaMetaType(it) }
|
|
|
|
MangaMetaTable.deleteWhere { condition }
|
|
mangaIds += item.mangaIds
|
|
}
|
|
|
|
deletedMetas to mangaIds
|
|
}
|
|
|
|
val mangas =
|
|
transaction {
|
|
MangaTable
|
|
.selectAll()
|
|
.where { MangaTable.id inList allMangaIds }
|
|
.map { MangaType(it) }
|
|
.distinctBy { it.id }
|
|
}
|
|
|
|
return DeleteMangaMetasPayload(clientMutationId, allDeletedMetas, mangas)
|
|
}
|
|
}
|