diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt index 49129180..ecd586b4 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/dataLoaders/MangaDataLoader.kt @@ -80,3 +80,20 @@ class MangaForSourceDataLoader : KotlinDataLoader { } } } + +class MangaForIdsDataLoader : KotlinDataLoader, MangaNodeList> { + override val dataLoaderName = "MangaForIdsDataLoader" + override fun getDataLoader(): DataLoader, MangaNodeList> = DataLoaderFactory.newDataLoader { mangaIds -> + future { + transaction { + addLogger(Slf4jSqlDebugLogger) + val ids = mangaIds.flatten().distinct() + val manga = MangaTable.select { MangaTable.id inList ids } + .map { MangaType(it) } + mangaIds.map { mangaIds -> + manga.filter { it.id in mangaIds }.toNodeList() + } + } + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UpdateMutation.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UpdateMutation.kt new file mode 100644 index 00000000..7a626c3d --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/mutations/UpdateMutation.kt @@ -0,0 +1,69 @@ +package suwayomi.tachidesk.graphql.mutations + +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.transactions.transaction +import org.kodein.di.DI +import org.kodein.di.conf.global +import org.kodein.di.instance +import suwayomi.tachidesk.graphql.types.UpdateStatus +import suwayomi.tachidesk.manga.impl.Category +import suwayomi.tachidesk.manga.impl.update.IUpdater +import suwayomi.tachidesk.manga.model.table.CategoryTable +import suwayomi.tachidesk.manga.model.table.toDataClass + +class UpdateMutation { + private val updater by DI.global.instance() + + data class UpdateLibraryMangaInput( + val clientMutationId: String? = null + ) + data class UpdateLibraryMangaPayload( + val clientMutationId: String?, + val updateStatus: UpdateStatus + ) + + fun updateLibraryManga(input: UpdateLibraryMangaInput): UpdateLibraryMangaPayload { + updater.addCategoriesToUpdateQueue( + Category.getCategoryList(), + clear = true, + forceAll = false + ) + + return UpdateLibraryMangaPayload(input.clientMutationId, UpdateStatus(updater.status.value)) + } + + data class UpdateCategoryMangaInput( + val clientMutationId: String? = null, + val categories: List + ) + data class UpdateCategoryMangaPayload( + val clientMutationId: String?, + val updateStatus: UpdateStatus + ) + + fun updateCategoryManga(input: UpdateCategoryMangaInput): UpdateCategoryMangaPayload { + val categories = transaction { + CategoryTable.select { CategoryTable.id inList input.categories }.map { + CategoryTable.toDataClass(it) + } + } + updater.addCategoriesToUpdateQueue(categories, clear = true, forceAll = true) + + return UpdateCategoryMangaPayload( + clientMutationId = input.clientMutationId, + updateStatus = UpdateStatus(updater.status.value) + ) + } + + data class UpdateStopInput( + val clientMutationId: String? = null + ) + data class UpdateStopPayload( + val clientMutationId: String? + ) + + fun updateStop(input: UpdateStopInput): UpdateStopPayload { + updater.reset() + return UpdateStopPayload(input.clientMutationId) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt new file mode 100644 index 00000000..88b50dd7 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdateQuery.kt @@ -0,0 +1,24 @@ +package suwayomi.tachidesk.graphql.queries + +import org.kodein.di.DI +import org.kodein.di.conf.global +import org.kodein.di.instance +import suwayomi.tachidesk.graphql.types.UpdateStatus +import suwayomi.tachidesk.graphql.types.UpdateStatusType +import suwayomi.tachidesk.manga.impl.update.IUpdater +import suwayomi.tachidesk.manga.impl.update.JobStatus + +class UpdateQuery { + private val updater by DI.global.instance() + + fun updateStatus(): UpdateStatus { + val status = updater.status.value + return UpdateStatus( + isRunning = status.running, + pendingJobs = UpdateStatusType(status.statusMap[JobStatus.PENDING]?.map { it.id }.orEmpty()), + runningJobs = UpdateStatusType(status.statusMap[JobStatus.RUNNING]?.map { it.id }.orEmpty()), + completeJobs = UpdateStatusType(status.statusMap[JobStatus.COMPLETE]?.map { it.id }.orEmpty()), + failedJobs = UpdateStatusType(status.statusMap[JobStatus.FAILED]?.map { it.id }.orEmpty()) + ) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdaterQuery.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdaterQuery.kt deleted file mode 100644 index 4235ebe9..00000000 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/queries/UpdaterQuery.kt +++ /dev/null @@ -1,42 +0,0 @@ -package suwayomi.tachidesk.graphql.queries - -import org.jetbrains.exposed.sql.select -import org.jetbrains.exposed.sql.transactions.transaction -import org.kodein.di.DI -import org.kodein.di.conf.global -import org.kodein.di.instance -import suwayomi.tachidesk.graphql.types.MangaType -import suwayomi.tachidesk.manga.impl.update.IUpdater -import suwayomi.tachidesk.manga.impl.update.JobStatus -import suwayomi.tachidesk.manga.impl.update.UpdaterSocket -import suwayomi.tachidesk.manga.model.table.MangaTable - -class UpdaterQuery { - sealed interface UpdaterStatus { - data class UpdaterJob(val status: JobStatus, val manga: MangaType) - - data class Running(val jobs: Map>) : UpdaterStatus - - // data class Idle - } - - private val updater by DI.global.instance() - - fun updaterStatus() { - val status = updater.status.value - if (status.running) { - val mangaIds = status.statusMap.values.flatMap { mangas -> mangas.map { it.id } } - val mangaMap = transaction { - MangaTable.select { MangaTable.id inList mangaIds } - .map { MangaType(it) } - .associateBy { it.id } - } - UpdaterStatus.Running( - status.statusMap.mapValues { (_, mangas) -> - mangas.mapNotNull { mangaMap[it.id] } - } - ) - } - UpdaterSocket - } -} 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 2a8ac68d..e362ea8e 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskDataLoaderRegistryFactory.kt @@ -19,6 +19,7 @@ import suwayomi.tachidesk.graphql.dataLoaders.ExtensionForSourceDataLoader import suwayomi.tachidesk.graphql.dataLoaders.GlobalMetaDataLoader import suwayomi.tachidesk.graphql.dataLoaders.MangaDataLoader import suwayomi.tachidesk.graphql.dataLoaders.MangaForCategoryDataLoader +import suwayomi.tachidesk.graphql.dataLoaders.MangaForIdsDataLoader import suwayomi.tachidesk.graphql.dataLoaders.MangaForSourceDataLoader import suwayomi.tachidesk.graphql.dataLoaders.MangaMetaDataLoader import suwayomi.tachidesk.graphql.dataLoaders.SourceDataLoader @@ -36,6 +37,7 @@ class TachideskDataLoaderRegistryFactory { MangaMetaDataLoader(), MangaForCategoryDataLoader(), MangaForSourceDataLoader(), + MangaForIdsDataLoader(), CategoryDataLoader(), CategoryMetaDataLoader(), CategoriesForMangaDataLoader(), 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 0c0c0db9..036f2882 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/server/TachideskGraphQLSchema.kt @@ -20,6 +20,7 @@ import suwayomi.tachidesk.graphql.mutations.ExtensionMutation import suwayomi.tachidesk.graphql.mutations.MangaMutation import suwayomi.tachidesk.graphql.mutations.MetaMutation import suwayomi.tachidesk.graphql.mutations.SourceMutation +import suwayomi.tachidesk.graphql.mutations.UpdateMutation import suwayomi.tachidesk.graphql.queries.BackupQuery import suwayomi.tachidesk.graphql.queries.CategoryQuery import suwayomi.tachidesk.graphql.queries.ChapterQuery @@ -27,11 +28,13 @@ import suwayomi.tachidesk.graphql.queries.ExtensionQuery import suwayomi.tachidesk.graphql.queries.MangaQuery import suwayomi.tachidesk.graphql.queries.MetaQuery import suwayomi.tachidesk.graphql.queries.SourceQuery +import suwayomi.tachidesk.graphql.queries.UpdateQuery import suwayomi.tachidesk.graphql.server.primitives.Cursor import suwayomi.tachidesk.graphql.server.primitives.GraphQLCursor import suwayomi.tachidesk.graphql.server.primitives.GraphQLLongAsString import suwayomi.tachidesk.graphql.server.primitives.GraphQLUpload import suwayomi.tachidesk.graphql.subscriptions.DownloadSubscription +import suwayomi.tachidesk.graphql.subscriptions.UpdateSubscription import kotlin.reflect.KClass import kotlin.reflect.KType @@ -57,7 +60,8 @@ val schema = toSchema( TopLevelObject(ExtensionQuery()), TopLevelObject(MangaQuery()), TopLevelObject(MetaQuery()), - TopLevelObject(SourceQuery()) + TopLevelObject(SourceQuery()), + TopLevelObject(UpdateQuery()) ), mutations = listOf( TopLevelObject(BackupMutation()), @@ -66,9 +70,11 @@ val schema = toSchema( TopLevelObject(ExtensionMutation()), TopLevelObject(MangaMutation()), TopLevelObject(MetaMutation()), - TopLevelObject(SourceMutation()) + TopLevelObject(SourceMutation()), + TopLevelObject(UpdateMutation()) ), subscriptions = listOf( - TopLevelObject(DownloadSubscription()) + TopLevelObject(DownloadSubscription()), + TopLevelObject(UpdateSubscription()) ) ) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/UpdateSubscription.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/UpdateSubscription.kt new file mode 100644 index 00000000..9386bbcc --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/subscriptions/UpdateSubscription.kt @@ -0,0 +1,26 @@ +/* + * 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/. */ + +package suwayomi.tachidesk.graphql.subscriptions + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.kodein.di.DI +import org.kodein.di.conf.global +import org.kodein.di.instance +import suwayomi.tachidesk.graphql.types.UpdateStatus +import suwayomi.tachidesk.manga.impl.update.IUpdater + +class UpdateSubscription { + private val updater by DI.global.instance() + + fun updateStatusChanged(): Flow { + return updater.status.map { updateStatus -> + UpdateStatus(updateStatus) + } + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdateType.kt b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdateType.kt new file mode 100644 index 00000000..713205c0 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/graphql/types/UpdateType.kt @@ -0,0 +1,33 @@ +package suwayomi.tachidesk.graphql.types + +import com.expediagroup.graphql.generator.annotations.GraphQLIgnore +import com.expediagroup.graphql.server.extensions.getValueFromDataLoader +import graphql.schema.DataFetchingEnvironment +import suwayomi.tachidesk.manga.impl.update.JobStatus +import suwayomi.tachidesk.manga.impl.update.UpdateStatus +import java.util.concurrent.CompletableFuture + +class UpdateStatus( + val isRunning: Boolean, + val pendingJobs: UpdateStatusType, + val runningJobs: UpdateStatusType, + val completeJobs: UpdateStatusType, + val failedJobs: UpdateStatusType +) { + constructor(status: UpdateStatus) : this( + isRunning = status.running, + pendingJobs = UpdateStatusType(status.statusMap[JobStatus.PENDING]?.map { it.id }.orEmpty()), + runningJobs = UpdateStatusType(status.statusMap[JobStatus.RUNNING]?.map { it.id }.orEmpty()), + completeJobs = UpdateStatusType(status.statusMap[JobStatus.COMPLETE]?.map { it.id }.orEmpty()), + failedJobs = UpdateStatusType(status.statusMap[JobStatus.FAILED]?.map { it.id }.orEmpty()) + ) +} + +class UpdateStatusType( + @get:GraphQLIgnore + val mangaIds: List +) { + fun mangas(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture { + return dataFetchingEnvironment.getValueFromDataLoader, MangaNodeList>("MangaForIdsDataLoader", mangaIds) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt index 8c16f1bb..72ed09f1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt @@ -68,11 +68,19 @@ object UpdateController { val updater by DI.global.instance() if (categoryId == null) { logger.info { "Adding Library to Update Queue" } - updater.addCategoriesToUpdateQueue(Category.getCategoryList(), true) + updater.addCategoriesToUpdateQueue( + Category.getCategoryList(), + clear = true, + forceAll = false + ) } else { val category = Category.getCategoryById(categoryId) if (category != null) { - updater.addCategoriesToUpdateQueue(listOf(category), true) + updater.addCategoriesToUpdateQueue( + listOf(category), + clear = true, + forceAll = true + ) } else { logger.info { "No Category found" } ctx.status(HttpCode.BAD_REQUEST) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/IUpdater.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/IUpdater.kt index b00f41d1..66dd25c1 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/IUpdater.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/IUpdater.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.StateFlow import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass interface IUpdater { - fun addCategoriesToUpdateQueue(categories: List, clear: Boolean?) + fun addCategoriesToUpdateQueue(categories: List, clear: Boolean?, forceAll: Boolean) val status: StateFlow fun reset() } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt index 3a526d0a..4b23ebf0 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/impl/update/Updater.kt @@ -62,7 +62,7 @@ class Updater : IUpdater { } logger.info { "Trigger global update (interval= ${serverConfig.globalUpdateInterval}h, lastAutomatedUpdate= ${Date(lastAutomatedUpdate)})" } - addCategoriesToUpdateQueue(Category.getCategoryList(), true) + addCategoriesToUpdateQueue(Category.getCategoryList(), clear = true, forceAll = false) } fun scheduleUpdateTask() { @@ -125,7 +125,7 @@ class Updater : IUpdater { return tracker.values.toList() } - override fun addCategoriesToUpdateQueue(categories: List, clear: Boolean?) { + override fun addCategoriesToUpdateQueue(categories: List, clear: Boolean?, forceAll: Boolean) { val updater by DI.global.instance() if (clear == true) { updater.reset() @@ -135,7 +135,11 @@ class Updater : IUpdater { val excludedCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.EXCLUDE].orEmpty() val includedCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.INCLUDE].orEmpty() val unsetCategories = includeInUpdateStatusToCategoryMap[IncludeInUpdate.UNSET].orEmpty() - val categoriesToUpdate = includedCategories.ifEmpty { unsetCategories } + val categoriesToUpdate = if (forceAll) { + categories + } else { + includedCategories.ifEmpty { unsetCategories } + } logger.debug { "Updating categories: '${categoriesToUpdate.joinToString("', '") { it.name }}'" } @@ -149,7 +153,7 @@ class Updater : IUpdater { .filter { if (serverConfig.excludeUnreadChapters) { (it.unreadCount ?: 0L) == 0L } else true } .filter { if (serverConfig.excludeNotStarted) { it.lastReadAt != null } else true } .filter { if (serverConfig.excludeCompleted) { it.status != MangaStatus.COMPLETED.name } else true } - .filter { !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } } + .filter { forceAll || !excludedCategories.any { category -> mangasToCategoriesMap[it.id]?.contains(category) == true } } .toList() // In case no manga gets updated and no update job was running before, the client would never receive an info about its update request