mirror of
https://github.com/Suwayomi/Tachidesk.git
synced 2025-12-10 06:42:07 +01:00
Library Update Queries and Mutations (#609)
* Add library update GraphQL endpoints * No need for data classes * UpdateLibraryManga
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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())
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user