From fe17176b31360f0466229f168453221275ab82c6 Mon Sep 17 00:00:00 2001 From: Mitchell Syer Date: Wed, 27 Apr 2022 07:31:39 -0400 Subject: [PATCH] document all endpoints (#350) * Document all endpoints * Forgot about global endpoints --- .../suwayomi/tachidesk/global/GlobalAPI.kt | 4 +- .../global/controller/SettingsController.kt | 44 +++- .../suwayomi/tachidesk/manga/MangaAPI.kt | 72 +++--- .../manga/controller/BackupController.kt | 199 ++++++++++----- .../manga/controller/CategoryController.kt | 139 ++++++++--- .../manga/controller/DownloadController.kt | 116 ++++++--- .../manga/controller/ExtensionController.kt | 184 ++++++++++---- .../manga/controller/SourceController.kt | 232 +++++++++++++----- .../manga/controller/UpdateController.kt | 104 +++++--- 9 files changed, 787 insertions(+), 307 deletions(-) diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/GlobalAPI.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/GlobalAPI.kt index 9585b82d..97b57a55 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/GlobalAPI.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/GlobalAPI.kt @@ -14,8 +14,8 @@ import suwayomi.tachidesk.global.controller.SettingsController object GlobalAPI { fun defineEndpoints() { path("settings") { - get("about", SettingsController::about) - get("check-update", SettingsController::checkUpdate) + get("about", SettingsController.about) + get("check-update", SettingsController.checkUpdate) } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/controller/SettingsController.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/controller/SettingsController.kt index 0f782d87..2ce76161 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/controller/SettingsController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/controller/SettingsController.kt @@ -7,22 +7,48 @@ package suwayomi.tachidesk.global.controller * 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 io.javalin.http.Context +import io.javalin.http.HttpCode import suwayomi.tachidesk.global.impl.About +import suwayomi.tachidesk.global.impl.AboutDataClass import suwayomi.tachidesk.global.impl.AppUpdate +import suwayomi.tachidesk.global.impl.UpdateDataClass import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.util.handler +import suwayomi.tachidesk.server.util.withOperation /** Settings Page/Screen */ object SettingsController { /** returns some static info about the current app build */ - fun about(ctx: Context) { - ctx.json(About.getAbout()) - } + val about = handler( + documentWith = { + withOperation { + summary("About Tachidesk") + description("Returns some static info about the current app build") + } + }, + behaviorOf = { ctx -> + ctx.json(About.getAbout()) + }, + withResults = { + json(HttpCode.OK) + } + ) /** check for app updates */ - fun checkUpdate(ctx: Context) { - ctx.json( - future { AppUpdate.checkUpdate() } - ) - } + val checkUpdate = handler( + documentWith = { + withOperation { + summary("Tachidesk update check") + description("Check for app updates") + } + }, + behaviorOf = { ctx -> + ctx.json( + future { AppUpdate.checkUpdate() } + ) + }, + withResults = { + json(HttpCode.OK) + } + ) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt index 3631c725..29bf03e7 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt @@ -24,31 +24,31 @@ import suwayomi.tachidesk.manga.controller.UpdateController object MangaAPI { fun defineEndpoints() { path("extension") { - get("list", ExtensionController::list) + get("list", ExtensionController.list) - get("install/{pkgName}", ExtensionController::install) - post("install", ExtensionController::installFile) - get("update/{pkgName}", ExtensionController::update) - get("uninstall/{pkgName}", ExtensionController::uninstall) + get("install/{pkgName}", ExtensionController.install) + post("install", ExtensionController.installFile) + get("update/{pkgName}", ExtensionController.update) + get("uninstall/{pkgName}", ExtensionController.uninstall) - get("icon/{apkName}", ExtensionController::icon) + get("icon/{apkName}", ExtensionController.icon) } path("source") { - get("list", SourceController::list) - get("{sourceId}", SourceController::retrieve) + get("list", SourceController.list) + get("{sourceId}", SourceController.retrieve) - get("{sourceId}/popular/{pageNum}", SourceController::popular) - get("{sourceId}/latest/{pageNum}", SourceController::latest) + get("{sourceId}/popular/{pageNum}", SourceController.popular) + get("{sourceId}/latest/{pageNum}", SourceController.latest) - get("{sourceId}/preferences", SourceController::getPreferences) - post("{sourceId}/preferences", SourceController::setPreference) + get("{sourceId}/preferences", SourceController.getPreferences) + post("{sourceId}/preferences", SourceController.setPreference) - get("{sourceId}/filters", SourceController::getFilters) - post("{sourceId}/filters", SourceController::setFilters) + get("{sourceId}/filters", SourceController.getFilters) + post("{sourceId}/filters", SourceController.setFilters) - get("{sourceId}/search", SourceController::searchSingle) -// get("all/search", SourceController::searchGlobal) // TODO + get("{sourceId}/search", SourceController.searchSingle) +// get("all/search", SourceController.searchGlobal) // TODO } path("manga") { @@ -75,47 +75,47 @@ object MangaAPI { } path("category") { - get("", CategoryController::categoryList) - post("", CategoryController::categoryCreate) + get("", CategoryController.categoryList) + post("", CategoryController.categoryCreate) // The order here is important {categoryId} needs to be applied last // or throws a NumberFormatException - patch("reorder", CategoryController::categoryReorder) + patch("reorder", CategoryController.categoryReorder) - get("{categoryId}", CategoryController::categoryMangas) - patch("{categoryId}", CategoryController::categoryModify) - delete("{categoryId}", CategoryController::categoryDelete) + get("{categoryId}", CategoryController.categoryMangas) + patch("{categoryId}", CategoryController.categoryModify) + delete("{categoryId}", CategoryController.categoryDelete) } path("backup") { - post("import", BackupController::protobufImport) - post("import/file", BackupController::protobufImportFile) + post("import", BackupController.protobufImport) + post("import/file", BackupController.protobufImportFile) - post("validate", BackupController::protobufValidate) - post("validate/file", BackupController::protobufValidateFile) + post("validate", BackupController.protobufValidate) + post("validate/file", BackupController.protobufValidateFile) - get("export", BackupController::protobufExport) - get("export/file", BackupController::protobufExportFile) + get("export", BackupController.protobufExport) + get("export/file", BackupController.protobufExportFile) } path("downloads") { ws("", DownloadController::downloadsWS) - get("start", DownloadController::start) - get("stop", DownloadController::stop) - get("clear", DownloadController::stop) + get("start", DownloadController.start) + get("stop", DownloadController.stop) + get("clear", DownloadController.stop) } path("download") { - get("{mangaId}/chapter/{chapterIndex}", DownloadController::queueChapter) - delete("{mangaId}/chapter/{chapterIndex}", DownloadController::unqueueChapter) + get("{mangaId}/chapter/{chapterIndex}", DownloadController.queueChapter) + delete("{mangaId}/chapter/{chapterIndex}", DownloadController.unqueueChapter) } path("update") { - get("recentChapters/{pageNum}", UpdateController::recentChapters) - post("fetch", UpdateController::categoryUpdate) + get("recentChapters/{pageNum}", UpdateController.recentChapters) + post("fetch", UpdateController.categoryUpdate) post("reset", UpdateController.reset) - get("summary", UpdateController::updateSummary) + get("summary", UpdateController.updateSummary) ws("", UpdateController::categoryUpdateWS) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt index d60a437d..1c7ecc2a 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt @@ -1,11 +1,14 @@ package suwayomi.tachidesk.manga.controller -import io.javalin.http.Context +import io.javalin.http.HttpCode +import suwayomi.tachidesk.manga.impl.backup.AbstractBackupValidator import suwayomi.tachidesk.manga.impl.backup.BackupFlags import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupExport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupImport import suwayomi.tachidesk.manga.impl.backup.proto.ProtoBackupValidator import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.util.handler +import suwayomi.tachidesk.server.util.withOperation import java.text.SimpleDateFormat import java.util.Date @@ -19,78 +22,156 @@ import java.util.Date object BackupController { /** expects a Tachiyomi protobuf backup in the body */ - fun protobufImport(ctx: Context) { - ctx.future( - future { - ProtoBackupImport.performRestore(ctx.bodyAsInputStream()) + val protobufImport = handler( + documentWith = { + withOperation { + summary("Restore a backup") + description("Expects a Tachiyomi protobuf backup in the body") } - ) - } + }, + behaviorOf = { ctx -> + ctx.future( + future { + ProtoBackupImport.performRestore(ctx.bodyAsInputStream()) + } + ) + }, + withResults = { + httpCode(HttpCode.OK) + } + ) /** expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */ - fun protobufImportFile(ctx: Context) { - // TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz" - ctx.future( - future { - ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content) + val protobufImportFile = handler( + documentWith = { + withOperation { + summary("Restore a backup file") + description("Expects a Tachiyomi protobuf backup as a file upload, the file must be named \"backup.proto.gz\"") } - ) - } + uploadedFile("backup.proto.gz") { + it.description("Protobuf backup") + it.required(true) + } + }, + behaviorOf = { ctx -> + // TODO: rewrite this with ctx.uploadedFiles(), don't call the multipart field "backup.proto.gz" + ctx.future( + future { + ProtoBackupImport.performRestore(ctx.uploadedFile("backup.proto.gz")!!.content) + } + ) + }, + withResults = { + httpCode(HttpCode.OK) + httpCode(HttpCode.NOT_FOUND) + } + ) /** returns a Tachiyomi protobuf backup created from the current database as a body */ - fun protobufExport(ctx: Context) { - ctx.contentType("application/octet-stream") - ctx.future( - future { - ProtoBackupExport.createBackup( - BackupFlags( - includeManga = true, - includeCategories = true, - includeChapters = true, - includeTracking = true, - includeHistory = true, - ) - ) + val protobufExport = handler( + documentWith = { + withOperation { + summary("Create a backup") + description("Returns a Tachiyomi protobuf backup created from the current database as a body") } - ) - } + }, + behaviorOf = { ctx -> + ctx.contentType("application/octet-stream") + ctx.future( + future { + ProtoBackupExport.createBackup( + BackupFlags( + includeManga = true, + includeCategories = true, + includeChapters = true, + includeTracking = true, + includeHistory = true, + ) + ) + } + ) + }, + withResults = { + mime(HttpCode.OK, "application/octet-stream") + } + ) /** returns a Tachiyomi protobuf backup created from the current database as a file */ - fun protobufExportFile(ctx: Context) { - ctx.contentType("application/octet-stream") - val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date()) - - ctx.header("Content-Disposition", """attachment; filename="tachidesk_$currentDate.proto.gz"""") - ctx.future( - future { - ProtoBackupExport.createBackup( - BackupFlags( - includeManga = true, - includeCategories = true, - includeChapters = true, - includeTracking = true, - includeHistory = true, - ) - ) + val protobufExportFile = handler( + documentWith = { + withOperation { + summary("Create a backup file") + description("Returns a Tachiyomi protobuf backup created from the current database as a file") } - ) - } + }, + behaviorOf = { ctx -> + ctx.contentType("application/octet-stream") + val currentDate = SimpleDateFormat("yyyy-MM-dd_HH-mm").format(Date()) + + ctx.header("Content-Disposition", """attachment; filename="tachidesk_$currentDate.proto.gz"""") + ctx.future( + future { + ProtoBackupExport.createBackup( + BackupFlags( + includeManga = true, + includeCategories = true, + includeChapters = true, + includeTracking = true, + includeHistory = true, + ) + ) + } + ) + }, + withResults = { + mime(HttpCode.OK, "application/octet-stream") + } + ) /** Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body */ - fun protobufValidate(ctx: Context) { - ctx.future( - future { - ProtoBackupValidator.validate(ctx.bodyAsInputStream()) + val protobufValidate = handler( + documentWith = { + withOperation { + summary("Validate a backup") + description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup in the body") } - ) - } + body("") { + + } + }, + behaviorOf = { ctx -> + ctx.future( + future { + ProtoBackupValidator.validate(ctx.bodyAsInputStream()) + } + ) + }, + withResults = { + json(HttpCode.OK) + } + ) /** Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named "backup.proto.gz" */ - fun protobufValidateFile(ctx: Context) { - ctx.future( - future { - ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content) + val protobufValidateFile = handler( + documentWith = { + withOperation { + summary("Validate a backup file") + description("Reports missing sources and trackers, expects a Tachiyomi protobuf backup as a file upload, the file must be named \"backup.proto.gz\"") } - ) - } + uploadedFile("backup.proto.gz") { + it.description("Protobuf backup") + it.required(true) + } + }, + behaviorOf = { ctx -> + ctx.future( + future { + ProtoBackupValidator.validate(ctx.uploadedFile("backup.proto.gz")!!.content) + } + ) + }, + withResults = { + json(HttpCode.OK) + } + ) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt index cb498eae..6b7daabe 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/CategoryController.kt @@ -7,50 +7,127 @@ package suwayomi.tachidesk.manga.controller * 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 io.javalin.http.Context +import io.javalin.http.HttpCode import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.CategoryManga +import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass +import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass +import suwayomi.tachidesk.server.util.formParam +import suwayomi.tachidesk.server.util.handler +import suwayomi.tachidesk.server.util.pathParam +import suwayomi.tachidesk.server.util.withOperation object CategoryController { /** category list */ - fun categoryList(ctx: Context) { - ctx.json(Category.getCategoryList()) - } + val categoryList = handler( + documentWith = { + withOperation { + summary("Category list") + description("get a list of categories") + } + }, + behaviorOf = { ctx -> + ctx.json(Category.getCategoryList()) + }, + withResults = { + json>(HttpCode.OK) + } + ) /** category create */ - fun categoryCreate(ctx: Context) { - val name = ctx.formParam("name")!! - Category.createCategory(name) - ctx.status(200) - } + val categoryCreate = handler( + formParam("name"), + documentWith = { + withOperation { + summary("Category create") + description("Create a category") + } + }, + behaviorOf = { ctx, name -> + if (Category.createCategory(name) != -1) { + ctx.status(200) + } else { + ctx.status(HttpCode.BAD_REQUEST) + } + + }, + withResults = { + httpCode(HttpCode.OK) + httpCode(HttpCode.BAD_REQUEST) + } + ) /** category modification */ - fun categoryModify(ctx: Context) { - val categoryId = ctx.pathParam("categoryId").toInt() - val name = ctx.formParam("name") - val isDefault = ctx.formParam("default")?.toBoolean() - Category.updateCategory(categoryId, name, isDefault) - ctx.status(200) - } + val categoryModify = handler( + pathParam("categoryId"), + formParam("name"), + formParam("default"), + documentWith = { + withOperation { + summary("Category modify") + description("Modify a category") + } + }, + behaviorOf = { ctx, categoryId, name, isDefault -> + Category.updateCategory(categoryId, name, isDefault) + ctx.status(200) + }, + withResults = { + httpCode(HttpCode.OK) + } + ) /** category delete */ - fun categoryDelete(ctx: Context) { - val categoryId = ctx.pathParam("categoryId").toInt() - Category.removeCategory(categoryId) - ctx.status(200) - } + val categoryDelete = handler( + pathParam("categoryId"), + documentWith = { + withOperation { + summary("Category delete") + description("Delete a category") + } + }, + behaviorOf = { ctx, categoryId -> + Category.removeCategory(categoryId) + ctx.status(200) + }, + withResults = { + httpCode(HttpCode.OK) + } + ) /** returns the manga list associated with a category */ - fun categoryMangas(ctx: Context) { - val categoryId = ctx.pathParam("categoryId").toInt() - ctx.json(CategoryManga.getCategoryMangaList(categoryId)) - } + val categoryMangas = handler( + pathParam("categoryId"), + documentWith = { + withOperation { + summary("Category manga") + description("Returns the manga list associated with a category") + } + }, + behaviorOf = { ctx, categoryId -> + ctx.json(CategoryManga.getCategoryMangaList(categoryId)) + }, + withResults = { + json>(HttpCode.OK) + } + ) /** category re-ordering */ - fun categoryReorder(ctx: Context) { - val from = ctx.formParam("from")!!.toInt() - val to = ctx.formParam("to")!!.toInt() - Category.reorderCategory(from, to) - ctx.status(200) - } + val categoryReorder = handler( + formParam("from"), + formParam("to"), + documentWith = { + withOperation { + summary("Category re-ordering") + description("Re-order a category") + } + }, + behaviorOf = { ctx, from, to -> + Category.reorderCategory(from, to) + ctx.status(200) + }, + withResults = { + httpCode(HttpCode.OK) + } + ) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt index 44f9da20..9f89dd60 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt @@ -7,10 +7,13 @@ package suwayomi.tachidesk.manga.controller * 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 io.javalin.http.Context +import io.javalin.http.HttpCode import io.javalin.websocket.WsConfig import suwayomi.tachidesk.manga.impl.download.DownloadManager import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.util.handler +import suwayomi.tachidesk.server.util.pathParam +import suwayomi.tachidesk.server.util.withOperation object DownloadController { /** Download queue stats */ @@ -28,45 +31,100 @@ object DownloadController { } /** Start the downloader */ - fun start(ctx: Context) { - DownloadManager.start() + val start = handler( + documentWith = { + withOperation { + summary("Downloader start") + description("Start the downloader") + } + }, + behaviorOf = { ctx -> + DownloadManager.start() - ctx.status(200) - } + ctx.status(200) + }, + withResults = { + httpCode(HttpCode.OK) + } + ) /** Stop the downloader */ - fun stop(ctx: Context) { - DownloadManager.stop() + val stop = handler( + documentWith = { + withOperation { + summary("Downloader stop") + description("Stop the downloader") + } + }, + behaviorOf = { ctx -> + DownloadManager.stop() - ctx.status(200) - } + ctx.status(200) + }, + withResults = { + httpCode(HttpCode.OK) + } + ) /** clear download queue */ - fun clear(ctx: Context) { - DownloadManager.clear() + val clear = handler( + documentWith = { + withOperation { + summary("Downloader clear") + description("Clear download queue") + } + }, + behaviorOf = { ctx -> + DownloadManager.clear() - ctx.status(200) - } + ctx.status(200) + + }, + withResults = { + httpCode(HttpCode.OK) + } + ) /** Queue chapter for download */ - fun queueChapter(ctx: Context) { - val chapterIndex = ctx.pathParam("chapterIndex").toInt() - val mangaId = ctx.pathParam("mangaId").toInt() - - ctx.future( - future { - DownloadManager.enqueue(chapterIndex, mangaId) + val queueChapter = handler( + pathParam("chapterIndex"), + pathParam("mangaId"), + documentWith = { + withOperation { + summary("Downloader add chapter") + description("Queue chapter for download") } - ) - } + }, + behaviorOf = { ctx, chapterIndex, mangaId -> + ctx.future( + future { + DownloadManager.enqueue(chapterIndex, mangaId) + } + ) + }, + withResults = { + httpCode(HttpCode.OK) + httpCode(HttpCode.NOT_FOUND) + } + ) /** delete chapter from download queue */ - fun unqueueChapter(ctx: Context) { - val chapterIndex = ctx.pathParam("chapterIndex").toInt() - val mangaId = ctx.pathParam("mangaId").toInt() + val unqueueChapter = handler( + pathParam("chapterIndex"), + pathParam("mangaId"), + documentWith = { + withOperation { + summary("Downloader remove chapter") + description("Delete chapter from download queue") + } + }, + behaviorOf = { ctx, chapterIndex, mangaId -> + DownloadManager.unqueue(chapterIndex, mangaId) - DownloadManager.unqueue(chapterIndex, mangaId) - - ctx.status(200) - } + ctx.status(200) + }, + withResults = { + httpCode(HttpCode.OK) + } + ) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt index 7ca59cb4..f82eddb8 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt @@ -7,78 +7,160 @@ package suwayomi.tachidesk.manga.controller * 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 io.javalin.http.Context +import io.javalin.http.HttpCode import mu.KotlinLogging import suwayomi.tachidesk.manga.impl.extension.Extension import suwayomi.tachidesk.manga.impl.extension.ExtensionsList +import suwayomi.tachidesk.manga.model.dataclass.ExtensionDataClass import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.util.handler +import suwayomi.tachidesk.server.util.pathParam +import suwayomi.tachidesk.server.util.queryParam +import suwayomi.tachidesk.server.util.withOperation object ExtensionController { private val logger = KotlinLogging.logger {} /** list all extensions */ - fun list(ctx: Context) { - ctx.future( - future { - ExtensionsList.getExtensionList() + val list = handler( + documentWith = { + withOperation { + summary("Extension list") + description("List all extensions") } - ) - } + }, + behaviorOf = { ctx -> + ctx.future( + future { + ExtensionsList.getExtensionList() + } + ) + }, + withResults = { + json>(HttpCode.OK) + } + ) /** install extension identified with "pkgName" */ - fun install(ctx: Context) { - val pkgName = ctx.pathParam("pkgName") - - ctx.future( - future { - Extension.installExtension(pkgName) + val install = handler( + pathParam("pkgName"), + documentWith = { + withOperation { + summary("Extension install") + description("install extension identified with \"pkgName\"") } - ) - } + }, + behaviorOf = { ctx, pkgName -> + ctx.future( + future { + Extension.installExtension(pkgName) + } + ) + }, + withResults = { + httpCode(HttpCode.CREATED) + httpCode(HttpCode.FOUND) + httpCode(HttpCode.INTERNAL_SERVER_ERROR) + } + ) /** install the uploaded apk file */ - fun installFile(ctx: Context) { - - val uploadedFile = ctx.uploadedFile("file")!! - logger.debug { "Uploaded extension file name: " + uploadedFile.filename } - - ctx.future( - future { - Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename) + val installFile = handler( + documentWith = { + withOperation { + summary("Extension install apk") + description("Install the uploaded apk file") } - ) - } + uploadedFile("file") { + it.description("Extension apk") + it.required(true) + } + }, + behaviorOf = { ctx -> + val uploadedFile = ctx.uploadedFile("file")!! + logger.debug { "Uploaded extension file name: " + uploadedFile.filename } + + ctx.future( + future { + Extension.installExternalExtension(uploadedFile.content, uploadedFile.filename) + } + ) + + }, + withResults = { + httpCode(HttpCode.CREATED) + httpCode(HttpCode.FOUND) + httpCode(HttpCode.INTERNAL_SERVER_ERROR) + } + ) /** update extension identified with "pkgName" */ - fun update(ctx: Context) { - val pkgName = ctx.pathParam("pkgName") - - ctx.future( - future { - Extension.updateExtension(pkgName) + val update = handler( + pathParam("pkgName"), + documentWith = { + withOperation { + summary("Extension update") + description("Update extension identified with \"pkgName\"") } - ) - } + }, + behaviorOf = { ctx, pkgName -> + ctx.future( + future { + Extension.updateExtension(pkgName) + } + ) + }, + withResults = { + httpCode(HttpCode.CREATED) + httpCode(HttpCode.FOUND) + httpCode(HttpCode.NOT_FOUND) + httpCode(HttpCode.INTERNAL_SERVER_ERROR) + } + ) /** uninstall extension identified with "pkgName" */ - fun uninstall(ctx: Context) { - val pkgName = ctx.pathParam("pkgName") - - Extension.uninstallExtension(pkgName) - ctx.status(200) - } + val uninstall = handler( + pathParam("pkgName"), + documentWith = { + withOperation { + summary("Extension uninstall") + description("Uninstall extension identified with \"pkgName\"") + } + }, + behaviorOf = { ctx, pkgName -> + Extension.uninstallExtension(pkgName) + ctx.status(200) + }, + withResults = { + httpCode(HttpCode.CREATED) + httpCode(HttpCode.FOUND) + httpCode(HttpCode.NOT_FOUND) + httpCode(HttpCode.INTERNAL_SERVER_ERROR) + } + ) /** icon for extension named `apkName` */ - fun icon(ctx: Context) { - val apkName = ctx.pathParam("apkName") - val useCache = ctx.queryParam("useCache")?.toBoolean() ?: true - - ctx.future( - future { Extension.getExtensionIcon(apkName, useCache) } - .thenApply { - ctx.header("content-type", it.second) - it.first - } - ) - } + val icon = handler( + pathParam("apkName"), + queryParam("useCache", true), + documentWith = { + withOperation { + summary("Extension icon") + description("Icon for extension named `apkName`") + } + }, + behaviorOf = { ctx, apkName, useCache -> + ctx.future( + future { Extension.getExtensionIcon(apkName, useCache) } + .thenApply { + ctx.header("content-type", it.second) + it.first + } + ) + }, + withResults = { + httpCode(HttpCode.OK) + httpCode(HttpCode.NOT_FOUND) + } + ) } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt index b51a1783..7a64ec5d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt @@ -7,7 +7,7 @@ package suwayomi.tachidesk.manga.controller * 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 io.javalin.http.Context +import io.javalin.http.HttpCode import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import org.kodein.di.DI @@ -18,87 +18,205 @@ import suwayomi.tachidesk.manga.impl.Search import suwayomi.tachidesk.manga.impl.Search.FilterChange import suwayomi.tachidesk.manga.impl.Source import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange +import suwayomi.tachidesk.manga.model.dataclass.PagedMangaListDataClass import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.util.handler +import suwayomi.tachidesk.server.util.pathParam +import suwayomi.tachidesk.server.util.queryParam +import suwayomi.tachidesk.server.util.withOperation +import javax.sound.sampled.SourceDataLine object SourceController { /** list of sources */ - fun list(ctx: Context) { - ctx.json(Source.getSourceList()) - } + val list = handler( + documentWith = { + withOperation { + summary("Sources list") + description("List of sources") + } + }, + behaviorOf = { ctx -> + ctx.json(Source.getSourceList()) + }, + withResults = { + json>(HttpCode.OK) + } + ) /** fetch source with id `sourceId` */ - fun retrieve(ctx: Context) { - val sourceId = ctx.pathParam("sourceId").toLong() - ctx.json(Source.getSource(sourceId)!!) - } + val retrieve = handler( + pathParam("sourceId"), + documentWith = { + withOperation { + summary("Source fetch") + description("Fetch source with id `sourceId`") + } + }, + behaviorOf = { ctx, sourceId -> + ctx.json(Source.getSource(sourceId)!!) + }, + withResults = { + json(HttpCode.OK) + httpCode(HttpCode.NOT_FOUND) + } + ) /** popular mangas from source with id `sourceId` */ - fun popular(ctx: Context) { - val sourceId = ctx.pathParam("sourceId").toLong() - val pageNum = ctx.pathParam("pageNum").toInt() - ctx.future( - future { - MangaList.getMangaList(sourceId, pageNum, popular = true) + val popular = handler( + pathParam("sourceId"), + pathParam("pageNum"), + documentWith = { + withOperation { + summary("Source popular manga") + description("Popular mangas from source with id `sourceId`") } - ) - } + }, + behaviorOf = { ctx, sourceId, pageNum -> + ctx.future( + future { + MangaList.getMangaList(sourceId, pageNum, popular = true) + } + ) + }, + withResults = { + json(HttpCode.OK) + } + ) /** latest mangas from source with id `sourceId` */ - fun latest(ctx: Context) { - val sourceId = ctx.pathParam("sourceId").toLong() - val pageNum = ctx.pathParam("pageNum").toInt() - ctx.future( - future { - MangaList.getMangaList(sourceId, pageNum, popular = false) + val latest = handler( + pathParam("sourceId"), + pathParam("pageNum"), + documentWith = { + withOperation { + summary("Source latest manga") + description("Latest mangas from source with id `sourceId`") } - ) - } + }, + behaviorOf = { ctx, sourceId, pageNum -> + ctx.future( + future { + MangaList.getMangaList(sourceId, pageNum, popular = false) + } + ) + }, + withResults = { + json(HttpCode.OK) + } + ) /** fetch preferences of source with id `sourceId` */ - fun getPreferences(ctx: Context) { - val sourceId = ctx.pathParam("sourceId").toLong() - ctx.json(Source.getSourcePreferences(sourceId)) - } + val getPreferences = handler( + pathParam("sourceId"), + documentWith = { + withOperation { + summary("Source preferences") + description("Fetch preferences of source with id `sourceId`") + } + }, + behaviorOf = { ctx, sourceId -> + ctx.json(Source.getSourcePreferences(sourceId)) + + }, + withResults = { + json>(HttpCode.OK) + } + ) /** set one preference of source with id `sourceId` */ - fun setPreference(ctx: Context) { - val sourceId = ctx.pathParam("sourceId").toLong() - val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java) - ctx.json(Source.setSourcePreference(sourceId, preferenceChange)) - } + val setPreference = handler( + pathParam("sourceId"), + documentWith = { + withOperation { + summary("Source preference set") + description("Set one preference of source with id `sourceId`") + } + }, + behaviorOf = { ctx, sourceId -> + val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java) + ctx.json(Source.setSourcePreference(sourceId, preferenceChange)) + }, + withResults = { + httpCode(HttpCode.OK) + } + ) /** fetch filters of source with id `sourceId` */ - fun getFilters(ctx: Context) { - val sourceId = ctx.pathParam("sourceId").toLong() - val reset = ctx.queryParam("reset")?.toBoolean() ?: false - ctx.json(Search.getFilterList(sourceId, reset)) - } + val getFilters = handler( + pathParam("sourceId"), + queryParam("reset", false), + documentWith = { + withOperation { + summary("Source filters") + description("Fetch filters of source with id `sourceId`") + } + }, + behaviorOf = { ctx, sourceId, reset -> + ctx.json(Search.getFilterList(sourceId, reset)) + }, + withResults = { + json>(HttpCode.OK) + } + ) private val json by DI.global.instance() /** change filters of source with id `sourceId` */ - fun setFilters(ctx: Context) { - val sourceId = ctx.pathParam("sourceId").toLong() - val filterChange = try { - json.decodeFromString>(ctx.body()) - } catch (e: Exception) { - listOf(json.decodeFromString(ctx.body())) - } + val setFilters = handler( + pathParam("sourceId"), + documentWith = { + withOperation { + summary("Source filters set") + description("Change filters of source with id `sourceId`") + } + }, + behaviorOf = { ctx, sourceId -> + val filterChange = try { + json.decodeFromString>(ctx.body()) + } catch (e: Exception) { + listOf(json.decodeFromString(ctx.body())) + } - ctx.json(Search.setFilter(sourceId, filterChange)) - } + ctx.json(Search.setFilter(sourceId, filterChange)) + }, + withResults = { + httpCode(HttpCode.OK) + } + ) /** single source search */ - fun searchSingle(ctx: Context) { - val sourceId = ctx.pathParam("sourceId").toLong() - val searchTerm = ctx.queryParam("searchTerm") ?: "" - val pageNum = ctx.queryParam("pageNum")?.toInt() ?: 1 - ctx.future(future { Search.sourceSearch(sourceId, searchTerm, pageNum) }) - } + val searchSingle = handler( + pathParam("sourceId"), + queryParam("searchTerm", ""), + queryParam("pageNum", 1), + documentWith = { + withOperation { + summary("Source search") + description("Single source search") + } + }, + behaviorOf = { ctx, sourceId, searchTerm, pageNum -> + ctx.future(future { Search.sourceSearch(sourceId, searchTerm, pageNum) }) + }, + withResults = { + json(HttpCode.OK) + } + ) /** all source search */ - fun searchAll(ctx: Context) { // TODO - val searchTerm = ctx.pathParam("searchTerm") - ctx.json(Search.sourceGlobalSearch(searchTerm)) - } + val searchAll = handler( + pathParam("searchTerm"), + documentWith = { + withOperation { + summary("Source global search") + description("All source search") + } + }, + behaviorOf = { ctx, searchTerm -> // TODO + ctx.json(Search.sourceGlobalSearch(searchTerm)) + }, + withResults = { + httpCode(HttpCode.OK) + } + ) } 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 2b8db0d0..ad4e62cc 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/UpdateController.kt @@ -1,6 +1,5 @@ package suwayomi.tachidesk.manga.controller -import io.javalin.http.Context import io.javalin.http.HttpCode import io.javalin.websocket.WsConfig import kotlinx.coroutines.runBlocking @@ -12,10 +11,15 @@ import suwayomi.tachidesk.manga.impl.Category import suwayomi.tachidesk.manga.impl.CategoryManga import suwayomi.tachidesk.manga.impl.Chapter import suwayomi.tachidesk.manga.impl.update.IUpdater +import suwayomi.tachidesk.manga.impl.update.UpdateStatus import suwayomi.tachidesk.manga.impl.update.UpdaterSocket import suwayomi.tachidesk.manga.model.dataclass.CategoryDataClass +import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass +import suwayomi.tachidesk.manga.model.dataclass.PaginatedList import suwayomi.tachidesk.server.JavalinSetup.future +import suwayomi.tachidesk.server.util.formParam import suwayomi.tachidesk.server.util.handler +import suwayomi.tachidesk.server.util.pathParam import suwayomi.tachidesk.server.util.withOperation /* @@ -29,35 +33,57 @@ object UpdateController { private val logger = KotlinLogging.logger { } /** get recently updated manga chapters */ - fun recentChapters(ctx: Context) { - val pageNum = ctx.pathParam("pageNum").toInt() - - ctx.future( - future { - Chapter.getRecentChapters(pageNum) - } - ) - } - - fun categoryUpdate(ctx: Context) { - val categoryId = ctx.formParam("category")?.toIntOrNull() - val categoriesForUpdate = ArrayList() - if (categoryId == null) { - logger.info { "Adding Library to Update Queue" } - categoriesForUpdate.addAll(Category.getCategoryList()) - } else { - val category = Category.getCategoryById(categoryId) - if (category != null) { - categoriesForUpdate.add(category) - } else { - logger.info { "No Category found" } - ctx.status(HttpCode.BAD_REQUEST) - return + val recentChapters = handler( + pathParam("pageNum"), + documentWith = { + withOperation { + summary("Updates fetch") + description("Get recently updated manga chapters") } + }, + behaviorOf = { ctx, pageNum -> + ctx.future( + future { + Chapter.getRecentChapters(pageNum) + } + ) + }, + withResults = { + json>(HttpCode.OK) } - addCategoriesToUpdateQueue(categoriesForUpdate, true) - ctx.status(HttpCode.OK) - } + ) + + val categoryUpdate = handler( + formParam("categoryId"), + documentWith = { + withOperation { + summary("Updater start") + description("Starts the updater") + } + }, + behaviorOf = { ctx, categoryId -> + val categoriesForUpdate = ArrayList() + if (categoryId == null) { + logger.info { "Adding Library to Update Queue" } + categoriesForUpdate.addAll(Category.getCategoryList()) + } else { + val category = Category.getCategoryById(categoryId) + if (category != null) { + categoriesForUpdate.add(category) + } else { + logger.info { "No Category found" } + ctx.status(HttpCode.BAD_REQUEST) + return@handler + } + } + addCategoriesToUpdateQueue(categoriesForUpdate, true) + ctx.status(HttpCode.OK) + }, + withResults = { + httpCode(HttpCode.OK) + httpCode(HttpCode.BAD_REQUEST) + } + ) private fun addCategoriesToUpdateQueue(categories: List, clear: Boolean = false) { val updater by DI.global.instance() @@ -84,15 +110,27 @@ object UpdateController { } } - fun updateSummary(ctx: Context) { - val updater by DI.global.instance() - ctx.json(updater.getStatus().value.getJsonSummary()) - } + val updateSummary = handler( + documentWith = { + withOperation { + summary("Updater summary") + description("Gets the latest updater summary") + } + }, + behaviorOf = { ctx -> + val updater by DI.global.instance() + ctx.json(updater.getStatus().value.getJsonSummary()) + }, + withResults = { + json(HttpCode.OK) + } + ) val reset = handler( documentWith = { withOperation { - summary("Stops and resets the Updater") + summary("Updater reset") + description("Stops and resets the Updater") } }, behaviorOf = { ctx ->