@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, ) data class UpdateMangasInput( val clientMutationId: String? = null, val ids: List, val patch: UpdateMangaPatch, ) private suspend fun updateMangas( ids: List, 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 { 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 { 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 { 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, ) @RequireAuth fun fetchMangaAndChapters(input: FetchMangaAndChaptersInput): CompletableFuture { 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, val metas: List, ) data class SetMangaMetasInput( val clientMutationId: String? = null, val items: List, ) data class SetMangaMetasPayload( val clientMutationId: String?, val metas: List, val mangas: List, ) @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, val keys: List? = null, val prefixes: List? = null, ) data class DeleteMangaMetasInput( val clientMutationId: String? = null, val items: List, ) data class DeleteMangaMetasPayload( val clientMutationId: String?, val metas: List, val mangas: List, ) @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() val mangaIds = mutableSetOf() items.forEach { item -> val keyCondition: Op? = item.keys?.takeIf { it.isNotEmpty() }?.let { MangaMetaTable.key inList it } val prefixCondition: Op? = item.prefixes ?.filter { it.isNotEmpty() } ?.map { (MangaMetaTable.key like LikePattern("$it%")) as Op } ?.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) } }