Library Update Queries and Mutations (#609)

* Add library update GraphQL endpoints

* No need for data classes

* UpdateLibraryManga
This commit is contained in:
Mitchell Syer
2023-08-03 18:08:35 -04:00
committed by GitHub
parent 78a167aacf
commit c3fb08d634
11 changed files with 199 additions and 52 deletions

View File

@@ -80,3 +80,20 @@ class MangaForSourceDataLoader : KotlinDataLoader<Long, MangaNodeList> {
}
}
}
class MangaForIdsDataLoader : KotlinDataLoader<List<Int>, MangaNodeList> {
override val dataLoaderName = "MangaForIdsDataLoader"
override fun getDataLoader(): DataLoader<List<Int>, 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()
}
}
}
}
}

View File

@@ -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<IUpdater>()
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<Int>
)
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)
}
}

View File

@@ -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<IUpdater>()
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())
)
}
}

View File

@@ -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<JobStatus, List<MangaType>>) : UpdaterStatus
// data class Idle
}
private val updater by DI.global.instance<IUpdater>()
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
}
}

View File

@@ -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(),

View File

@@ -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())
)
)

View File

@@ -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<IUpdater>()
fun updateStatusChanged(): Flow<UpdateStatus> {
return updater.status.map { updateStatus ->
UpdateStatus(updateStatus)
}
}
}

View File

@@ -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<Int>
) {
fun mangas(dataFetchingEnvironment: DataFetchingEnvironment): CompletableFuture<MangaNodeList> {
return dataFetchingEnvironment.getValueFromDataLoader<List<Int>, MangaNodeList>("MangaForIdsDataLoader", mangaIds)
}
}

View File

@@ -68,11 +68,19 @@ object UpdateController {
val updater by DI.global.instance<IUpdater>()
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)

View File

@@ -4,7 +4,7 @@ import kotlinx.coroutines.flow.StateFlow
import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass
interface IUpdater {
fun addCategoriesToUpdateQueue(categories: List<CategoryDataClass>, clear: Boolean?)
fun addCategoriesToUpdateQueue(categories: List<CategoryDataClass>, clear: Boolean?, forceAll: Boolean)
val status: StateFlow<UpdateStatus>
fun reset()
}

View File

@@ -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<CategoryDataClass>, clear: Boolean?) {
override fun addCategoriesToUpdateQueue(categories: List<CategoryDataClass>, clear: Boolean?, forceAll: Boolean) {
val updater by DI.global.instance<IUpdater>()
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