From 7c3eff2ba72d37be67e31984c7579684f86fc879 Mon Sep 17 00:00:00 2001 From: Mitchell Syer Date: Mon, 5 Jun 2023 09:19:03 -0400 Subject: [PATCH] Complete source mutations (#567) --- .../graphql/mutations/SourceMutation.kt | 116 +++++++++++++++++- .../graphql/server/TachideskGraphQLSchema.kt | 2 + .../tachidesk/graphql/types/SourceType.kt | 20 +++ .../tachidesk/manga/impl/MangaList.kt | 28 +++++ .../suwayomi/tachidesk/manga/impl/Search.kt | 2 +- 5 files changed, 161 insertions(+), 7 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt index 0c8e1182..8eb02fb3 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt @@ -1,8 +1,112 @@ package suwayomi.tachidesk.graphql.mutations -/** - * TODO Mutations - * - Browse with filters - * - Configure settings - */ -class SourceMutation +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import suwayomi.tachidesk.graphql.types.MangaType +import suwayomi.tachidesk.graphql.types.PreferenceObject +import suwayomi.tachidesk.manga.impl.MangaList.insertOrGet +import suwayomi.tachidesk.manga.impl.Search +import suwayomi.tachidesk.manga.impl.Source +import suwayomi.tachidesk.manga.impl.util.lang.awaitSingle +import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource +import suwayomi.tachidesk.manga.model.table.MangaTable +import suwayomi.tachidesk.server.JavalinSetup.future +import java.util.concurrent.CompletableFuture + +class SourceMutation { + + enum class FetchSourceMangaType { + SEARCH, + POPULAR, + LATEST + } + data class FilterChange( + val position: Int, + val state: String + ) + 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 + ) + + 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.fetchSearchManga( + page = page, + query = query.orEmpty(), + filters = Search.buildFilterList( + sourceId = sourceId, + changes = filters?.map { Search.FilterChange(it.position, it.state) } + .orEmpty() + ) + ).awaitSingle() + } + FetchSourceMangaType.POPULAR -> { + source.fetchPopularManga(page).awaitSingle() + } + FetchSourceMangaType.LATEST -> { + if (!source.supportsLatest) throw Exception("Source does not support latest") + source.fetchLatestUpdates(page).awaitSingle() + } + } + + val mangaIds = mangasPage.insertOrGet(sourceId) + + val mangas = transaction { + MangaTable.select { 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 state: String + ) + data class UpdateSourcePreferenceInput( + val clientMutationId: String? = null, + val source: Long, + val change: SourcePreferenceChange + ) + data class UpdateSourcePreferencePayload( + val clientMutationId: String?, + val preferences: List + ) + + fun updateSourcePreference( + input: UpdateSourcePreferenceInput + ): UpdateSourcePreferencePayload { + val (clientMutationId, sourceId, change) = input + + Source.setSourcePreference(sourceId, Source.SourcePreferenceChange(change.position, change.state)) + + return UpdateSourcePreferencePayload( + clientMutationId = clientMutationId, + preferences = Source.getSourcePreferences(sourceId).map { PreferenceObject(it.type, it.props) } + ) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt index e42a0d45..3f36703f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -11,6 +11,7 @@ import com.expediagroup.graphql.generator.SchemaGeneratorConfig import com.expediagroup.graphql.generator.TopLevelObject import com.expediagroup.graphql.generator.hooks.FlowSubscriptionSchemaGeneratorHooks import com.expediagroup.graphql.generator.toSchema +import graphql.scalars.ExtendedScalars import graphql.schema.GraphQLType import suwayomi.tachidesk.graphql.mutations.CategoryMutation import suwayomi.tachidesk.graphql.mutations.ChapterMutation @@ -35,6 +36,7 @@ class CustomSchemaGeneratorHooks : FlowSubscriptionSchemaGeneratorHooks() { override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) { Long::class -> GraphQLLongAsString // encode to string for JS Cursor::class -> GraphQLCursor + Any::class -> ExtendedScalars.Json else -> super.willGenerateGraphQLType(type) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt index a3018c39..f41e7a59 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt @@ -18,6 +18,8 @@ import suwayomi.tachidesk.graphql.server.primitives.Edge import suwayomi.tachidesk.graphql.server.primitives.Node import suwayomi.tachidesk.graphql.server.primitives.NodeList import suwayomi.tachidesk.graphql.server.primitives.PageInfo +import suwayomi.tachidesk.manga.impl.Search +import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass @@ -64,6 +66,14 @@ class SourceType( fun extension(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { return dataFetchingEnvironment.getValueFromDataLoader("ExtensionForSourceDataLoader", id) } + + fun preferences(): List { + return Source.getSourcePreferences(id).map { PreferenceObject(it.type, it.props) } + } + + fun filters(): List { + return Search.getFilterList(id, false).map { FilterObject(it.type, it.filter) } + } } fun SourceType(row: ResultRow): SourceType? { @@ -122,3 +132,13 @@ data class SourceNodeList( } } } + +data class PreferenceObject( + val type: String, + val props: Any +) + +data class FilterObject( + val type: String, + val filter: Any +) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt index 3a720725..f750aeb1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/MangaList.kt @@ -44,6 +44,34 @@ object MangaList { return mangasPage.processEntries(sourceId) } + fun MangasPage.insertOrGet(sourceId: Long): List { + return transaction { + mangas.map { manga -> + val mangaEntry = MangaTable.select { + (MangaTable.url eq manga.url) and (MangaTable.sourceReference eq sourceId) + }.firstOrNull() + if (mangaEntry == null) { // create manga entry + MangaTable.insertAndGetId { + it[url] = manga.url + it[title] = manga.title + + it[artist] = manga.artist + it[author] = manga.author + it[description] = manga.description + it[genre] = manga.genre + it[status] = manga.status + it[thumbnail_url] = manga.thumbnail_url + it[updateStrategy] = manga.update_strategy.name + + it[sourceReference] = sourceId + }.value + } else { + mangaEntry[MangaTable.id].value + } + } + } + } + fun MangasPage.processEntries(sourceId: Long): PagedMangaListDataClass { val mangasPage = this val mangaList = transaction { diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt index cc3bdf5c..84813b31 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Search.kt @@ -125,7 +125,7 @@ object Search { return filterList } - private fun buildFilterList(sourceId: Long, changes: List): FilterList { + fun buildFilterList(sourceId: Long, changes: List): FilterList { val source = getCatalogueSourceOrStub(sourceId) val filterList = source.getFilterList() return updateFilterList(filterList, changes)