@file:Suppress("RedundantNullableReturnType", "unused") package suwayomi.tachidesk.graphql.mutations import androidx.preference.CheckBoxPreference import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.MultiSelectListPreference import androidx.preference.SwitchPreferenceCompat 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 suwayomi.tachidesk.graphql.directives.RequireAuth import suwayomi.tachidesk.graphql.types.FilterChange import suwayomi.tachidesk.graphql.types.MangaType import suwayomi.tachidesk.graphql.types.MetaInput import suwayomi.tachidesk.graphql.types.Preference import suwayomi.tachidesk.graphql.types.SourceMetaType import suwayomi.tachidesk.graphql.types.SourceType import suwayomi.tachidesk.graphql.types.preferenceOf import suwayomi.tachidesk.graphql.types.updateFilterList import suwayomi.tachidesk.manga.impl.MangaList.insertOrUpdate import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.model.table.MangaTable import suwayomi.tachidesk.manga.model.table.SourceMetaTable import suwayomi.tachidesk.manga.model.table.SourceTable import suwayomi.tachidesk.server.JavalinSetup.future import java.util.concurrent.CompletableFuture class SourceMutation { data class SetSourceMetaInput( val clientMutationId: String? = null, val meta: SourceMetaType, ) data class SetSourceMetaPayload( val clientMutationId: String?, val meta: SourceMetaType, ) @RequireAuth fun setSourceMeta(input: SetSourceMetaInput): SetSourceMetaPayload? { val (clientMutationId, meta) = input Source.modifyMeta(meta.sourceId, meta.key, meta.value) return SetSourceMetaPayload(clientMutationId, meta) } data class DeleteSourceMetaInput( val clientMutationId: String? = null, val sourceId: Long, val key: String, ) data class DeleteSourceMetaPayload( val clientMutationId: String?, val meta: SourceMetaType?, val source: SourceType?, ) @RequireAuth fun deleteSourceMeta(input: DeleteSourceMetaInput): DeleteSourceMetaPayload? { val (clientMutationId, sourceId, key) = input val (meta, source) = transaction { val meta = SourceMetaTable .selectAll() .where { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) } .firstOrNull() SourceMetaTable.deleteWhere { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) } val source = transaction { SourceTable .selectAll() .where { SourceTable.id eq sourceId } .firstOrNull() ?.let { SourceType(it) } } if (meta != null) { SourceMetaType(meta) } else { null } to source } return DeleteSourceMetaPayload(clientMutationId, meta, source) } data class SetSourceMetasItem( val sourceIds: List, val metas: List, ) data class SetSourceMetasInput( val clientMutationId: String? = null, val items: List, ) data class SetSourceMetasPayload( val clientMutationId: String?, val metas: List, val sources: List, ) @RequireAuth fun setSourceMetas(input: SetSourceMetasInput): SetSourceMetasPayload? { val (clientMutationId, items) = input val metaBySourceId = items .flatMap { item -> val metaMap = item.metas.associate { it.key to it.value } item.sourceIds.map { sourceId -> sourceId to metaMap } }.groupBy({ it.first }, { it.second }) .mapValues { (_, maps) -> maps.reduce { acc, map -> acc + map } } Source.modifySourceMetas(metaBySourceId) val allSourceIds = metaBySourceId.keys val allMetaKeys = metaBySourceId.values.flatMap { it.keys }.distinct() val (updatedMetas, sources) = transaction { val updatedMetas = SourceMetaTable .selectAll() .where { (SourceMetaTable.ref inList allSourceIds) and (SourceMetaTable.key inList allMetaKeys) } .map { SourceMetaType(it) } val sources = SourceTable .selectAll() .where { SourceTable.id inList allSourceIds } .mapNotNull { SourceType(it) } .distinctBy { it.id } updatedMetas to sources } return SetSourceMetasPayload(clientMutationId, updatedMetas, sources) } data class DeleteSourceMetasItem( val sourceIds: List, val keys: List? = null, val prefixes: List? = null, ) data class DeleteSourceMetasInput( val clientMutationId: String? = null, val items: List, ) data class DeleteSourceMetasPayload( val clientMutationId: String?, val metas: List, val sources: List, ) @RequireAuth fun deleteSourceMetas(input: DeleteSourceMetasInput): DeleteSourceMetasPayload? { 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, allSourceIds) = transaction { val deletedMetas = mutableListOf() val sourceIds = mutableSetOf() items.forEach { item -> val keyCondition: Op? = item.keys?.takeIf { it.isNotEmpty() }?.let { SourceMetaTable.key inList it } val prefixCondition: Op? = item.prefixes ?.filter { it.isNotEmpty() } ?.map { (SourceMetaTable.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 = (SourceMetaTable.ref inList item.sourceIds) and metaKeyCondition deletedMetas += SourceMetaTable .selectAll() .where { condition } .map { SourceMetaType(it) } SourceMetaTable.deleteWhere { condition } sourceIds += item.sourceIds } deletedMetas to sourceIds } val sources = transaction { SourceTable .selectAll() .where { SourceTable.id inList allSourceIds } .mapNotNull { SourceType(it) } .distinctBy { it.id } } return DeleteSourceMetasPayload(clientMutationId, allDeletedMetas, sources) } enum class FetchSourceMangaType { SEARCH, POPULAR, LATEST, } data class FetchSourceMangaInput( val clientMutationId: String? = null, val source: Long, val type: FetchSourceMangaType, val page: Int, val query: String? = null, val filters: List? = null, ) data class FetchSourceMangaPayload( val clientMutationId: String?, val mangas: List, val hasNextPage: Boolean, ) @RequireAuth fun fetchSourceManga(input: FetchSourceMangaInput): CompletableFuture { val (clientMutationId, sourceId, type, page, query, filters) = input return future { val source = GetCatalogueSource.getCatalogueSourceOrNull(sourceId)!! val mangasPage = when (type) { FetchSourceMangaType.SEARCH -> { source.getSearchManga( page = page, query = query.orEmpty(), filters = updateFilterList(source, filters), ) } FetchSourceMangaType.POPULAR -> { source.getPopularManga(page) } FetchSourceMangaType.LATEST -> { if (!source.supportsLatest) throw Exception("Source does not support latest") source.getLatestUpdates(page) } } val mangaIds = mangasPage.insertOrUpdate(sourceId) val mangas = transaction { MangaTable .selectAll() .where { MangaTable.id inList mangaIds } .map { MangaType(it) } }.sortedBy { mangaIds.indexOf(it.id) } FetchSourceMangaPayload( clientMutationId = clientMutationId, mangas = mangas, hasNextPage = mangasPage.hasNextPage, ) } } data class SourcePreferenceChange( val position: Int, val switchState: Boolean? = null, val checkBoxState: Boolean? = null, val editTextState: String? = null, val listState: String? = null, val multiSelectState: List? = null, ) data class UpdateSourcePreferenceInput( val clientMutationId: String? = null, val source: Long, val change: SourcePreferenceChange, ) data class UpdateSourcePreferencePayload( val clientMutationId: String?, val preferences: List, val source: SourceType, ) @RequireAuth fun updateSourcePreference(input: UpdateSourcePreferenceInput): UpdateSourcePreferencePayload? { val (clientMutationId, sourceId, change) = input Source.setSourcePreference(sourceId, change.position, "") { preference -> when (preference) { is SwitchPreferenceCompat -> change.switchState is CheckBoxPreference -> change.checkBoxState is EditTextPreference -> change.editTextState is ListPreference -> change.listState is MultiSelectListPreference -> change.multiSelectState?.toSet() else -> throw RuntimeException("sealed class cannot have more subtypes!") } ?: throw Exception("Expected change to ${preference::class.simpleName}") } return UpdateSourcePreferencePayload( clientMutationId = clientMutationId, preferences = Source.getSourcePreferencesRaw(sourceId).map { preferenceOf(it) }, source = transaction { SourceType(SourceTable.selectAll().where { SourceTable.id eq sourceId }.first())!! }, ) } }