diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt index d0b8c594b..94c3426ba 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MetaDataLoader.kt @@ -12,9 +12,11 @@ import suwayomi.tachidesk.graphql.types.CategoryMetaType import suwayomi.tachidesk.graphql.types.ChapterMetaType import suwayomi.tachidesk.graphql.types.GlobalMetaType import suwayomi.tachidesk.graphql.types.MangaMetaType +import suwayomi.tachidesk.graphql.types.SourceMetaType import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable +import suwayomi.tachidesk.manga.model.table.SourceMetaTable import suwayomi.tachidesk.server.JavalinSetup.future class GlobalMetaDataLoader : KotlinDataLoader { @@ -88,3 +90,21 @@ class CategoryMetaDataLoader : KotlinDataLoader> { } } } + +class SourceMetaDataLoader : KotlinDataLoader> { + override val dataLoaderName = "SourceMetaDataLoader" + + override fun getDataLoader(): DataLoader> = + DataLoaderFactory.newDataLoader> { ids -> + future { + transaction { + addLogger(Slf4jSqlDebugLogger) + val metasByRefId = + SourceMetaTable.select { SourceMetaTable.ref inList ids } + .map { SourceMetaType(it) } + .groupBy { it.sourceId } + ids.map { metasByRefId[it].orEmpty() } + } + } + } +} 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 ad6371b9e..a1cb4c37e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/SourceMutation.kt @@ -5,11 +5,15 @@ import androidx.preference.EditTextPreference import androidx.preference.ListPreference import androidx.preference.MultiSelectListPreference import androidx.preference.SwitchPreferenceCompat +import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.deleteWhere import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.transactions.transaction import suwayomi.tachidesk.graphql.types.FilterChange import suwayomi.tachidesk.graphql.types.MangaType 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 @@ -17,11 +21,69 @@ import suwayomi.tachidesk.manga.impl.MangaList.insertOrGet 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, + ) + + 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?, + ) + + fun deleteSourceMeta(input: DeleteSourceMetaInput): DeleteSourceMetaPayload { + val (clientMutationId, sourceId, key) = input + + val (meta, source) = + transaction { + val meta = + SourceMetaTable.select { (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.select { SourceTable.id eq sourceId }.firstOrNull() + ?.let { SourceType(it) } + } + + if (meta != null) { + SourceMetaType(meta) + } else { + null + } to source + } + + return DeleteSourceMetaPayload(clientMutationId, meta, source) + } + enum class FetchSourceMangaType { SEARCH, POPULAR, diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt index 8240d9061..5f9ab689c 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt @@ -30,6 +30,7 @@ import suwayomi.tachidesk.graphql.dataLoaders.MangaForIdsDataLoader import suwayomi.tachidesk.graphql.dataLoaders.MangaForSourceDataLoader import suwayomi.tachidesk.graphql.dataLoaders.MangaMetaDataLoader import suwayomi.tachidesk.graphql.dataLoaders.SourceDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.SourceMetaDataLoader import suwayomi.tachidesk.graphql.dataLoaders.SourcesForExtensionDataLoader import suwayomi.tachidesk.graphql.dataLoaders.TrackRecordDataLoader import suwayomi.tachidesk.graphql.dataLoaders.TrackRecordsForMangaIdDataLoader @@ -64,6 +65,7 @@ class TachideskDataLoaderRegistryFactory { CategoriesForMangaDataLoader(), SourceDataLoader(), SourcesForExtensionDataLoader(), + SourceMetaDataLoader(), ExtensionDataLoader(), ExtensionForSourceDataLoader(), TrackerDataLoader(), diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt index 8c2fe8593..be7669a78 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/MetaType.kt @@ -12,6 +12,7 @@ import suwayomi.tachidesk.graphql.server.primitives.PageInfo import suwayomi.tachidesk.manga.model.table.CategoryMetaTable import suwayomi.tachidesk.manga.model.table.ChapterMetaTable import suwayomi.tachidesk.manga.model.table.MangaMetaTable +import suwayomi.tachidesk.manga.model.table.SourceMetaTable import java.util.concurrent.CompletableFuture interface MetaType : Node { @@ -67,6 +68,22 @@ class CategoryMetaType( } } +class SourceMetaType( + override val key: String, + override val value: String, + val sourceId: Long, +) : MetaType { + constructor(row: ResultRow) : this( + key = row[SourceMetaTable.key], + value = row[SourceMetaTable.value], + sourceId = row[SourceMetaTable.ref], + ) + + fun source(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader("SourceDataLoader", sourceId) + } +} + class GlobalMetaType( override val key: String, override val value: String, 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 5e0203f0b..b00a1594f 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/SourceType.kt @@ -82,6 +82,10 @@ class SourceType( fun filters(): List { return getCatalogueSourceOrStub(id).getFilterList().map { filterOf(it) } } + + fun meta(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture> { + return dataFetchingEnvironment.getValueFromDataLoader>("SourceMetaDataLoader", id) + } } @Suppress("ktlint:standard:function-naming") diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt index d70f1dbda..71a175640 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/Source.kt @@ -13,9 +13,12 @@ import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.sourcePreferences import io.javalin.plugin.json.JsonMapper import mu.KotlinLogging +import org.jetbrains.exposed.sql.and +import org.jetbrains.exposed.sql.insert import org.jetbrains.exposed.sql.select import org.jetbrains.exposed.sql.selectAll import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.update import org.kodein.di.DI import org.kodein.di.conf.global import org.kodein.di.instance @@ -25,6 +28,7 @@ import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.getCatalogue import suwayomi.tachidesk.manga.impl.util.source.GetCatalogueSource.unregisterCatalogueSource import suwayomi.tachidesk.manga.model.dataclass.SourceDataClass import suwayomi.tachidesk.manga.model.table.ExtensionTable +import suwayomi.tachidesk.manga.model.table.SourceMetaTable import suwayomi.tachidesk.manga.model.table.SourceTable import uy.kohesive.injekt.api.get import xyz.nulldev.androidcompat.androidimpl.CustomContext @@ -150,4 +154,29 @@ object Source { // must reload the source because a preference was changed unregisterCatalogueSource(sourceId) } + + fun modifyMeta( + sourceId: Long, + key: String, + value: String, + ) { + transaction { + val meta = + transaction { + SourceMetaTable.select { (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) } + }.firstOrNull() + + if (meta == null) { + SourceMetaTable.insert { + it[SourceMetaTable.key] = key + it[SourceMetaTable.value] = value + it[SourceMetaTable.ref] = sourceId + } + } else { + SourceMetaTable.update({ (SourceMetaTable.ref eq sourceId) and (SourceMetaTable.key eq key) }) { + it[SourceMetaTable.value] = value + } + } + } + } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/SourceMetaTable.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/SourceMetaTable.kt new file mode 100644 index 000000000..3e6bc8a83 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/model/table/SourceMetaTable.kt @@ -0,0 +1,20 @@ +package suwayomi.tachidesk.manga.model.table + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import org.jetbrains.exposed.dao.id.IntIdTable +import suwayomi.tachidesk.manga.model.table.ChapterMetaTable.ref + +/** + * Metadata storage for clients, about Source with id == [ref]. + */ +object SourceMetaTable : IntIdTable() { + val key = varchar("key", 256) + val value = varchar("value", 4096) + val ref = long("source_ref") +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0036_SourceMeta.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0036_SourceMeta.kt new file mode 100644 index 000000000..7595d4636 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/database/migration/M0036_SourceMeta.kt @@ -0,0 +1,27 @@ +package suwayomi.tachidesk.server.database.migration + +/* + * Copyright (C) Contributors to the Suwayomi project + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +import de.neonew.exposed.migrations.helpers.AddTableMigration +import org.jetbrains.exposed.dao.id.IntIdTable +import org.jetbrains.exposed.sql.Table + +@Suppress("ClassName", "unused") +class M0036_SourceMeta : AddTableMigration() { + private class SourceMetaTable : IntIdTable() { + val key = varchar("key", 256) + val value = varchar("value", 4096) + val ref = long("source_ref") + } + + override val tables: Array + get() = + arrayOf( + SourceMetaTable(), + ) +}