diff --git a/server/src/main/kotlin/suwayomi/tachidesk/global/GlobalAPI.kt b/server/src/main/kotlin/suwayomi/tachidesk/global/GlobalAPI.kt index b98ddb67..9585b82d 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/GlobalAPI.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/GlobalAPI.kt @@ -7,18 +7,15 @@ package suwayomi.tachidesk.global * 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.Javalin import io.javalin.apibuilder.ApiBuilder.get import io.javalin.apibuilder.ApiBuilder.path import suwayomi.tachidesk.global.controller.SettingsController object GlobalAPI { - fun defineEndpoints(app: Javalin) { - app.routes { - path("api/v1/settings") { - get("about", SettingsController::about) - get("check-update", SettingsController::checkUpdate) - } + fun defineEndpoints() { + path("settings") { + 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 cdd337df..0f782d87 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/global/controller/SettingsController.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/global/controller/SettingsController.kt @@ -10,19 +10,19 @@ package suwayomi.tachidesk.global.controller import io.javalin.http.Context import suwayomi.tachidesk.global.impl.About import suwayomi.tachidesk.global.impl.AppUpdate -import suwayomi.tachidesk.server.JavalinSetup +import suwayomi.tachidesk.server.JavalinSetup.future /** Settings Page/Screen */ object SettingsController { /** returns some static info about the current app build */ - fun about(ctx: Context): Context { - return ctx.json(About.getAbout()) + fun about(ctx: Context) { + ctx.json(About.getAbout()) } /** check for app updates */ - fun checkUpdate(ctx: Context): Context { - return ctx.json( - JavalinSetup.future { AppUpdate.checkUpdate() } + fun checkUpdate(ctx: Context) { + ctx.json( + future { AppUpdate.checkUpdate() } ) } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt index 6d5ed5cf..1fb48af6 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/MangaAPI.kt @@ -8,469 +8,103 @@ package suwayomi.tachidesk.manga * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import io.javalin.Javalin -import suwayomi.tachidesk.manga.impl.Category -import suwayomi.tachidesk.manga.impl.CategoryManga.addMangaToCategory -import suwayomi.tachidesk.manga.impl.CategoryManga.getCategoryMangaList -import suwayomi.tachidesk.manga.impl.CategoryManga.getMangaCategories -import suwayomi.tachidesk.manga.impl.CategoryManga.removeMangaFromCategory -import suwayomi.tachidesk.manga.impl.Chapter.getChapter -import suwayomi.tachidesk.manga.impl.Chapter.getChapterList -import suwayomi.tachidesk.manga.impl.Chapter.modifyChapter -import suwayomi.tachidesk.manga.impl.Chapter.modifyChapterMeta -import suwayomi.tachidesk.manga.impl.Library.addMangaToLibrary -import suwayomi.tachidesk.manga.impl.Library.getLibraryMangas -import suwayomi.tachidesk.manga.impl.Library.removeMangaFromLibrary -import suwayomi.tachidesk.manga.impl.Manga.getManga -import suwayomi.tachidesk.manga.impl.Manga.getMangaThumbnail -import suwayomi.tachidesk.manga.impl.Manga.modifyMangaMeta -import suwayomi.tachidesk.manga.impl.MangaList.getMangaList -import suwayomi.tachidesk.manga.impl.Page.getPageImage -import suwayomi.tachidesk.manga.impl.Search.sourceFilters -import suwayomi.tachidesk.manga.impl.Search.sourceGlobalSearch -import suwayomi.tachidesk.manga.impl.Search.sourceSearch -import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange -import suwayomi.tachidesk.manga.impl.Source.getSource -import suwayomi.tachidesk.manga.impl.Source.getSourceList -import suwayomi.tachidesk.manga.impl.Source.getSourcePreferences -import suwayomi.tachidesk.manga.impl.Source.setSourcePreference -import suwayomi.tachidesk.manga.impl.backup.BackupFlags -import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupExport.createLegacyBackup -import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup -import suwayomi.tachidesk.manga.impl.download.DownloadManager -import suwayomi.tachidesk.manga.impl.extension.Extension.getExtensionIcon -import suwayomi.tachidesk.manga.impl.extension.Extension.installExtension -import suwayomi.tachidesk.manga.impl.extension.Extension.uninstallExtension -import suwayomi.tachidesk.manga.impl.extension.Extension.updateExtension -import suwayomi.tachidesk.manga.impl.extension.ExtensionsList.getExtensionList -import suwayomi.tachidesk.server.JavalinSetup.future -import java.text.SimpleDateFormat -import java.util.Date +import io.javalin.apibuilder.ApiBuilder.delete +import io.javalin.apibuilder.ApiBuilder.get +import io.javalin.apibuilder.ApiBuilder.patch +import io.javalin.apibuilder.ApiBuilder.path +import io.javalin.apibuilder.ApiBuilder.post +import io.javalin.apibuilder.ApiBuilder.ws +import suwayomi.tachidesk.manga.controller.BackupController +import suwayomi.tachidesk.manga.controller.DownloadController +import suwayomi.tachidesk.manga.controller.ExtensionController +import suwayomi.tachidesk.manga.controller.LibraryController +import suwayomi.tachidesk.manga.controller.MangaController +import suwayomi.tachidesk.manga.controller.SourceController object MangaAPI { fun defineEndpoints(app: Javalin) { - // list all extensions - app.get("/api/v1/extension/list") { ctx -> - ctx.json( - future { - getExtensionList() - } - ) + path("extension") { + get("list", ExtensionController::list) + + get("install/:pkgName", ExtensionController::install) + get("update/:pkgName", ExtensionController::update) + get("uninstall/:pkgName", ExtensionController::uninstall) + + get("icon/:apkName", ExtensionController::icon) } - // install extension identified with "pkgName" - app.get("/api/v1/extension/install/:pkgName") { ctx -> - val pkgName = ctx.pathParam("pkgName") + path("source") { + get("list", SourceController::list) + get(":sourceId", SourceController::retrieve) - ctx.json( - future { - installExtension(pkgName) - } - ) + get(":sourceId/popular/:pageNum", SourceController::popular) + get(":sourceId/latest/:pageNum", SourceController::latest) + + get(":sourceId/preferences", SourceController::getPreferences) + post(":sourceId/preferences", SourceController::setPreference) + + post(":sourceId/filters", SourceController::filters) // TODO + + get(":sourceId/search/:searchTerm/:pageNum", SourceController::searchSingle) + get("search/:searchTerm/:pageNum", SourceController::searchSingle) // TODO } - // update extension identified with "pkgName" - app.get("/api/v1/extension/update/:pkgName") { ctx -> - val pkgName = ctx.pathParam("pkgName") + path("manga") { + get(":mangaId", MangaController::retrieve) + get(":mangaId/thumbnail", MangaController::thumbnail) - ctx.json( - future { - updateExtension(pkgName) - } - ) + get(":mangaId/category", MangaController::categoryList) + get(":mangaId/category/:categoryId", MangaController::addToCategory) + delete(":mangaId/category/:categoryId", MangaController::removeFromCategory) + + get(":mangaId/library", MangaController::addToLibrary) + delete(":mangaId/library", MangaController::removeFromLibrary) + + patch(":mangaId/meta", MangaController::meta) + + get(":mangaId/chapters", MangaController::chapterList) + get(":mangaId/chapter/:chapterIndex", MangaController::chapterRetrieve) + patch(":mangaId/chapter/:chapterIndex", MangaController::chapterModify) + + patch(":mangaId/chapter/:chapterIndex/meta", MangaController::chapterMeta) + + get(":mangaId/chapter/:chapterIndex/page/:index", MangaController::chapterList) } - // uninstall extension identified with "pkgName" - app.get("/api/v1/extension/uninstall/:pkgName") { ctx -> - val pkgName = ctx.pathParam("pkgName") + path("") { + get("library", LibraryController::list) - uninstallExtension(pkgName) - ctx.status(200) - } + path("category") { + get("", LibraryController::categoryList) + post("", LibraryController::categoryCreate) - // icon for extension named `apkName` - app.get("/api/v1/extension/icon/:apkName") { ctx -> - val apkName = ctx.pathParam("apkName") + get(":categoryId", LibraryController::categoryMangas) + patch(":categoryId", LibraryController::categoryModify) + delete(":categoryId", LibraryController::categoryDelete) - ctx.result( - future { getExtensionIcon(apkName) } - .thenApply { - ctx.header("content-type", it.second) - it.first - } - ) - } - - // list of sources - app.get("/api/v1/source/list") { ctx -> - ctx.json(getSourceList()) - } - - // fetch source with id `sourceId` - app.get("/api/v1/source/:sourceId") { ctx -> - val sourceId = ctx.pathParam("sourceId").toLong() - ctx.json(getSource(sourceId)) - } - - // fetch preferences of source with id `sourceId` - app.get("/api/v1/source/:sourceId/preferences") { ctx -> - val sourceId = ctx.pathParam("sourceId").toLong() - ctx.json(getSourcePreferences(sourceId)) - } - - // fetch preferences of source with id `sourceId` - app.post("/api/v1/source/:sourceId/preferences") { ctx -> - val sourceId = ctx.pathParam("sourceId").toLong() - val preferenceChange = ctx.bodyAsClass(SourcePreferenceChange::class.java) - ctx.json(setSourcePreference(sourceId, preferenceChange)) - } - - // popular mangas from source with id `sourceId` - app.get("/api/v1/source/:sourceId/popular/:pageNum") { ctx -> - val sourceId = ctx.pathParam("sourceId").toLong() - val pageNum = ctx.pathParam("pageNum").toInt() - ctx.json( - future { - getMangaList(sourceId, pageNum, popular = true) - } - ) - } - - // latest mangas from source with id `sourceId` - app.get("/api/v1/source/:sourceId/latest/:pageNum") { ctx -> - val sourceId = ctx.pathParam("sourceId").toLong() - val pageNum = ctx.pathParam("pageNum").toInt() - ctx.json( - future { - getMangaList(sourceId, pageNum, popular = false) - } - ) - } - - // get manga info - app.get("/api/v1/manga/:mangaId/") { ctx -> - val mangaId = ctx.pathParam("mangaId").toInt() - val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean() - - ctx.json( - future { - getManga(mangaId, onlineFetch) - } - ) - } - - // manga thumbnail - app.get("api/v1/manga/:mangaId/thumbnail") { ctx -> - val mangaId = ctx.pathParam("mangaId").toInt() - - ctx.result( - future { getMangaThumbnail(mangaId) } - .thenApply { - ctx.header("content-type", it.second) - it.first - } - ) - } - - // list manga's categories - app.get("api/v1/manga/:mangaId/category/") { ctx -> - val mangaId = ctx.pathParam("mangaId").toInt() - ctx.json(getMangaCategories(mangaId)) - } - - // adds the manga to category - app.get("api/v1/manga/:mangaId/category/:categoryId") { ctx -> - val mangaId = ctx.pathParam("mangaId").toInt() - val categoryId = ctx.pathParam("categoryId").toInt() - addMangaToCategory(mangaId, categoryId) - ctx.status(200) - } - - // removes the manga from the category - app.delete("api/v1/manga/:mangaId/category/:categoryId") { ctx -> - val mangaId = ctx.pathParam("mangaId").toInt() - val categoryId = ctx.pathParam("categoryId").toInt() - removeMangaFromCategory(mangaId, categoryId) - ctx.status(200) - } - - // get chapter list when showing a manga - app.get("/api/v1/manga/:mangaId/chapters") { ctx -> - val mangaId = ctx.pathParam("mangaId").toInt() - - val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean() - - ctx.json(future { getChapterList(mangaId, onlineFetch) }) - } - - // used to modify a manga's meta parameters - app.patch("/api/v1/manga/:mangaId/meta") { ctx -> - val mangaId = ctx.pathParam("mangaId").toInt() - - val key = ctx.formParam("key")!! - val value = ctx.formParam("value")!! - - modifyMangaMeta(mangaId, key, value) - - ctx.status(200) - } - - // used to display a chapter, get a chapter in order to show it's pages - app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx -> - val chapterIndex = ctx.pathParam("chapterIndex").toInt() - val mangaId = ctx.pathParam("mangaId").toInt() - ctx.json(future { getChapter(chapterIndex, mangaId) }) - } - - // used to modify a chapter's parameters - app.patch("/api/v1/manga/:mangaId/chapter/:chapterIndex") { ctx -> - val chapterIndex = ctx.pathParam("chapterIndex").toInt() - val mangaId = ctx.pathParam("mangaId").toInt() - - val read = ctx.formParam("read")?.toBoolean() - val bookmarked = ctx.formParam("bookmarked")?.toBoolean() - val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean() - val lastPageRead = ctx.formParam("lastPageRead")?.toInt() - - modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead) - - ctx.status(200) - } - - // used to modify a chapter's meta parameters - app.patch("/api/v1/manga/:mangaId/chapter/:chapterIndex/meta") { ctx -> - val chapterIndex = ctx.pathParam("chapterIndex").toInt() - val mangaId = ctx.pathParam("mangaId").toInt() - - val key = ctx.formParam("key")!! - val value = ctx.formParam("value")!! - - modifyChapterMeta(mangaId, chapterIndex, key, value) - - ctx.status(200) - } - - // get page at index "index" - app.get("/api/v1/manga/:mangaId/chapter/:chapterIndex/page/:index") { ctx -> - val mangaId = ctx.pathParam("mangaId").toInt() - val chapterIndex = ctx.pathParam("chapterIndex").toInt() - val index = ctx.pathParam("index").toInt() - - ctx.result( - future { getPageImage(mangaId, chapterIndex, index) } - .thenApply { - ctx.header("content-type", it.second) - it.first - } - ) - } - - // submit a chapter for download - app.put("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx -> - // TODO - } - - // cancel a chapter download - app.delete("/api/v1/manga/:mangaId/chapter/:chapterIndex/download") { ctx -> - // TODO - } - - // global search, Not implemented yet - app.get("/api/v1/search/:searchTerm") { ctx -> - val searchTerm = ctx.pathParam("searchTerm") - ctx.json(sourceGlobalSearch(searchTerm)) - } - - // single source search - app.get("/api/v1/source/:sourceId/search/:searchTerm/:pageNum") { ctx -> - val sourceId = ctx.pathParam("sourceId").toLong() - val searchTerm = ctx.pathParam("searchTerm") - val pageNum = ctx.pathParam("pageNum").toInt() - ctx.json(future { sourceSearch(sourceId, searchTerm, pageNum) }) - } - - // source filter list - app.get("/api/v1/source/:sourceId/filters/") { ctx -> - val sourceId = ctx.pathParam("sourceId").toLong() - ctx.json(sourceFilters(sourceId)) - } - - // adds the manga to library - app.get("api/v1/manga/:mangaId/library") { ctx -> - val mangaId = ctx.pathParam("mangaId").toInt() - - ctx.result( - future { addMangaToLibrary(mangaId) } - ) - } - - // removes the manga from the library - app.delete("api/v1/manga/:mangaId/library") { ctx -> - val mangaId = ctx.pathParam("mangaId").toInt() - - ctx.result( - future { removeMangaFromLibrary(mangaId) } - ) - } - - // lists mangas that have no category assigned - app.get("/api/v1/library/") { ctx -> - ctx.json(getLibraryMangas()) - } - - // category list - app.get("/api/v1/category/") { ctx -> - ctx.json(Category.getCategoryList()) - } - - // category create - app.post("/api/v1/category/") { ctx -> - val name = ctx.formParam("name")!! - Category.createCategory(name) - ctx.status(200) - } - - // category modification - app.patch("/api/v1/category/:categoryId") { ctx -> - 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) - } - - // category re-ordering - app.patch("/api/v1/category/:categoryId/reorder") { ctx -> - val categoryId = ctx.pathParam("categoryId").toInt() - val from = ctx.formParam("from")!!.toInt() - val to = ctx.formParam("to")!!.toInt() - Category.reorderCategory(categoryId, from, to) - ctx.status(200) - } - - // category delete - app.delete("/api/v1/category/:categoryId") { ctx -> - val categoryId = ctx.pathParam("categoryId").toInt() - Category.removeCategory(categoryId) - ctx.status(200) - } - - // returns the manga list associated with a category - app.get("/api/v1/category/:categoryId") { ctx -> - val categoryId = ctx.pathParam("categoryId").toInt() - ctx.json(getCategoryMangaList(categoryId)) - } - - // expects a Tachiyomi legacy backup json in the body - app.post("/api/v1/backup/legacy/import") { ctx -> - ctx.result( - future { - restoreLegacyBackup(ctx.bodyAsInputStream()) - } - ) - } - - // expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json" - app.post("/api/v1/backup/legacy/import/file") { ctx -> - ctx.result( - future { - restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content) - } - ) - } - - // returns a Tachiyomi legacy backup json created from the current database as a json body - app.get("/api/v1/backup/legacy/export") { ctx -> - ctx.contentType("application/json") - ctx.result( - future { - createLegacyBackup( - BackupFlags( - includeManga = true, - includeCategories = true, - includeChapters = true, - includeTracking = true, - includeHistory = true, - ) - ) - } - ) - } - - // returns a Tachiyomi legacy backup json created from the current database as a file - app.get("/api/v1/backup/legacy/export/file") { ctx -> - ctx.contentType("application/json") - val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm") - val currentDate = sdf.format(Date()) - - ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"") - ctx.result( - future { - createLegacyBackup( - BackupFlags( - includeManga = true, - includeCategories = true, - includeChapters = true, - includeTracking = true, - includeHistory = true, - ) - ) - } - ) - } - - // Download queue stats - app.ws("/api/v1/downloads") { ws -> - ws.onConnect { ctx -> - DownloadManager.addClient(ctx) - DownloadManager.notifyClient(ctx) - } - ws.onMessage { ctx -> - DownloadManager.handleRequest(ctx) - } - ws.onClose { ctx -> - DownloadManager.removeClient(ctx) + patch(":categoryId/reorder", LibraryController::categoryReorder) } } - // Start the downloader - app.get("/api/v1/downloads/start") { ctx -> - DownloadManager.start() + path("backup") { + post("legacy/import", BackupController::legacyImport) + post("legacy/import/file", BackupController::legacyImportFile) - ctx.status(200) + get("legacy/export", BackupController::legacyExport) + get("legacy/export/file", BackupController::legacyExportFile) } - // Stop the downloader - app.get("/api/v1/downloads/stop") { ctx -> - DownloadManager.stop() + path("downloads") { + ws("", DownloadController::downloadsWS) - ctx.status(200) + get("start", DownloadController::start) + get("stop", DownloadController::stop) + get("clear", DownloadController::stop) } - // clear download queue - app.get("/api/v1/downloads/clear") { ctx -> - DownloadManager.clear() - - ctx.status(200) - } - - // Queue chapter for download - app.get("/api/v1/download/:mangaId/chapter/:chapterIndex") { ctx -> - val chapterIndex = ctx.pathParam("chapterIndex").toInt() - val mangaId = ctx.pathParam("mangaId").toInt() - - DownloadManager.enqueue(chapterIndex, mangaId) - - ctx.status(200) - } - - // delete chapter from download queue - app.delete("/api/v1/download/:mangaId/chapter/:chapterIndex") { ctx -> - val chapterIndex = ctx.pathParam("chapterIndex").toInt() - val mangaId = ctx.pathParam("mangaId").toInt() - - DownloadManager.unqueue(chapterIndex, mangaId) - - ctx.status(200) + path("download") { + get(":mangaId/chapter/:chapterIndex", DownloadController::queueChapter) + delete(":mangaId/chapter/:chapterIndex", DownloadController::unqueueChapter) } } } diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt new file mode 100644 index 00000000..84d1dde8 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/BackupController.kt @@ -0,0 +1,76 @@ +package suwayomi.tachidesk.manga.controller + +import io.javalin.http.Context +import suwayomi.tachidesk.manga.impl.backup.BackupFlags +import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupExport +import suwayomi.tachidesk.manga.impl.backup.legacy.LegacyBackupImport +import suwayomi.tachidesk.server.JavalinSetup +import java.text.SimpleDateFormat +import java.util.Date + +/* + * 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/. */ + +object BackupController { + /** expects a Tachiyomi legacy backup json in the body */ + fun legacyImport(ctx: Context) { + ctx.result( + JavalinSetup.future { + LegacyBackupImport.restoreLegacyBackup(ctx.bodyAsInputStream()) + } + ) + } + + /** expects a Tachiyomi legacy backup json as a file upload, the file must be named "backup.json" */ + fun legacyImportFile(ctx: Context) { + ctx.result( + JavalinSetup.future { + LegacyBackupImport.restoreLegacyBackup(ctx.uploadedFile("backup.json")!!.content) + } + ) + } + + /** returns a Tachiyomi legacy backup json created from the current database as a json body */ + fun legacyExport(ctx: Context) { + ctx.contentType("application/json") + ctx.result( + JavalinSetup.future { + LegacyBackupExport.createLegacyBackup( + BackupFlags( + includeManga = true, + includeCategories = true, + includeChapters = true, + includeTracking = true, + includeHistory = true, + ) + ) + } + ) + } + + /** returns a Tachiyomi legacy backup json created from the current database as a file */ + fun legacyExportFile(ctx: Context) { + ctx.contentType("application/json") + val sdf = SimpleDateFormat("yyyy-MM-dd_HH-mm") + val currentDate = sdf.format(Date()) + + ctx.header("Content-Disposition", "attachment; filename=\"tachidesk_$currentDate.json\"") + ctx.result( + JavalinSetup.future { + LegacyBackupExport.createLegacyBackup( + BackupFlags( + includeManga = true, + includeCategories = true, + includeChapters = true, + includeTracking = true, + includeHistory = true, + ) + ) + } + ) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt new file mode 100644 index 00000000..0a51134d --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/DownloadController.kt @@ -0,0 +1,69 @@ +package suwayomi.tachidesk.manga.controller + +/* + * 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 io.javalin.http.Context +import io.javalin.websocket.WsHandler +import suwayomi.tachidesk.manga.impl.download.DownloadManager + +object DownloadController { + /** Download queue stats */ + fun downloadsWS(ws: WsHandler) { + ws.onConnect { ctx -> + DownloadManager.addClient(ctx) + DownloadManager.notifyClient(ctx) + } + ws.onMessage { ctx -> + DownloadManager.handleRequest(ctx) + } + ws.onClose { ctx -> + DownloadManager.removeClient(ctx) + } + } + + /** Start the downloader */ + fun start(ctx: Context) { + DownloadManager.start() + + ctx.status(200) + } + + /** Stop the downloader */ + fun stop(ctx: Context) { + DownloadManager.stop() + + ctx.status(200) + } + + /** clear download queue */ + fun clear(ctx: Context) { + DownloadManager.clear() + + ctx.status(200) + } + + /** Queue chapter for download */ + fun queueChapter(ctx: Context) { + val chapterIndex = ctx.pathParam("chapterIndex").toInt() + val mangaId = ctx.pathParam("mangaId").toInt() + + DownloadManager.enqueue(chapterIndex, mangaId) + + ctx.status(200) + } + + /** delete chapter from download queue */ + fun unqueueChapter(ctx: Context) { + val chapterIndex = ctx.pathParam("chapterIndex").toInt() + val mangaId = ctx.pathParam("mangaId").toInt() + + DownloadManager.unqueue(chapterIndex, mangaId) + + ctx.status(200) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt new file mode 100644 index 00000000..e63eb265 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/ExtensionController.kt @@ -0,0 +1,67 @@ +package suwayomi.tachidesk.manga.controller + +/* + * 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 io.javalin.http.Context +import suwayomi.tachidesk.manga.impl.extension.Extension +import suwayomi.tachidesk.manga.impl.extension.ExtensionsList +import suwayomi.tachidesk.server.JavalinSetup.future + +object ExtensionController { + /** list all extensions */ + fun list(ctx: Context) { + ctx.json( + future { + ExtensionsList.getExtensionList() + } + ) + } + + /** install extension identified with "pkgName" */ + fun install(ctx: Context) { + val pkgName = ctx.pathParam("pkgName") + + ctx.json( + future { + Extension.installExtension(pkgName) + } + ) + } + + /** update extension identified with "pkgName" */ + fun update(ctx: Context) { + val pkgName = ctx.pathParam("pkgName") + + ctx.json( + future { + Extension.updateExtension(pkgName) + } + ) + } + + /** uninstall extension identified with "pkgName" */ + fun uninstall(ctx: Context) { + val pkgName = ctx.pathParam("pkgName") + + Extension.uninstallExtension(pkgName) + ctx.status(200) + } + + /** icon for extension named `apkName` */ + fun icon(ctx: Context) { + val apkName = ctx.pathParam("apkName") + + ctx.result( + future { Extension.getExtensionIcon(apkName) } + .thenApply { + ctx.header("content-type", it.second) + it.first + } + ) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/LibraryController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/LibraryController.kt new file mode 100644 index 00000000..10215214 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/LibraryController.kt @@ -0,0 +1,63 @@ +package suwayomi.tachidesk.manga.controller + +/* + * 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 io.javalin.http.Context +import suwayomi.tachidesk.manga.impl.Category +import suwayomi.tachidesk.manga.impl.CategoryManga +import suwayomi.tachidesk.manga.impl.Library + +object LibraryController { + /** lists mangas that have no category assigned */ + fun list(ctx: Context) { + ctx.json(Library.getLibraryMangas()) + } + + /** category list */ + fun categoryList(ctx: Context) { + ctx.json(Category.getCategoryList()) + } + + /** category create */ + fun categoryCreate(ctx: Context) { + val name = ctx.formParam("name")!! + Category.createCategory(name) + ctx.status(200) + } + + /** 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) + } + + /** category delete */ + fun categoryDelete(ctx: Context) { + val categoryId = ctx.pathParam("categoryId").toInt() + Category.removeCategory(categoryId) + ctx.status(200) + } + + /** returns the manga list associated with a category */ + fun categoryMangas(ctx: Context) { + val categoryId = ctx.pathParam("categoryId").toInt() + ctx.json(CategoryManga.getCategoryMangaList(categoryId)) + } + + /** category re-ordering */ + fun categoryReorder(ctx: Context) { + val categoryId = ctx.pathParam("categoryId").toInt() + val from = ctx.formParam("from")!!.toInt() + val to = ctx.formParam("to")!!.toInt() + Category.reorderCategory(categoryId, from, to) + ctx.status(200) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt new file mode 100644 index 00000000..a8dfb7d7 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/MangaController.kt @@ -0,0 +1,154 @@ +package suwayomi.tachidesk.manga.controller + +/* + * 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 io.javalin.http.Context +import suwayomi.tachidesk.manga.impl.CategoryManga +import suwayomi.tachidesk.manga.impl.Chapter +import suwayomi.tachidesk.manga.impl.Library +import suwayomi.tachidesk.manga.impl.Manga +import suwayomi.tachidesk.manga.impl.Page +import suwayomi.tachidesk.server.JavalinSetup.future + +object MangaController { + /** get manga info */ + fun retrieve(ctx: Context) { + val mangaId = ctx.pathParam("mangaId").toInt() + val onlineFetch = ctx.queryParam("onlineFetch", "false").toBoolean() + + ctx.json( + future { + Manga.getManga(mangaId, onlineFetch) + } + ) + } + + /** manga thumbnail */ + fun thumbnail(ctx: Context) { + val mangaId = ctx.pathParam("mangaId").toInt() + + ctx.result( + future { Manga.getMangaThumbnail(mangaId) } + .thenApply { + ctx.header("content-type", it.second) + it.first + } + ) + } + + /** adds the manga to library */ + fun addToLibrary(ctx: Context) { + val mangaId = ctx.pathParam("mangaId").toInt() + + ctx.result( + future { Library.addMangaToLibrary(mangaId) } + ) + } + + /** removes the manga from the library */ + fun removeFromLibrary(ctx: Context) { + val mangaId = ctx.pathParam("mangaId").toInt() + + ctx.result( + future { Library.removeMangaFromLibrary(mangaId) } + ) + } + + /** list manga's categories */ + fun categoryList(ctx: Context) { + val mangaId = ctx.pathParam("mangaId").toInt() + ctx.json(CategoryManga.getMangaCategories(mangaId)) + } + + /** adds the manga to category */ + fun addToCategory(ctx: Context) { + val mangaId = ctx.pathParam("mangaId").toInt() + val categoryId = ctx.pathParam("categoryId").toInt() + CategoryManga.addMangaToCategory(mangaId, categoryId) + ctx.status(200) + } + + /** removes the manga from the category */ + fun removeFromCategory(ctx: Context) { + val mangaId = ctx.pathParam("mangaId").toInt() + val categoryId = ctx.pathParam("categoryId").toInt() + CategoryManga.removeMangaFromCategory(mangaId, categoryId) + ctx.status(200) + } + + /** used to modify a manga's meta parameters */ + fun meta(ctx: Context) { + val mangaId = ctx.pathParam("mangaId").toInt() + + val key = ctx.formParam("key")!! + val value = ctx.formParam("value")!! + + Manga.modifyMangaMeta(mangaId, key, value) + + ctx.status(200) + } + + /** get chapter list when showing a manga */ + fun chapterList(ctx: Context) { + val mangaId = ctx.pathParam("mangaId").toInt() + + val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean() + + ctx.json(future { Chapter.getChapterList(mangaId, onlineFetch) }) + } + + /** used to display a chapter, get a chapter in order to show its pages */ + fun chapterRetrieve(ctx: Context) { + val chapterIndex = ctx.pathParam("chapterIndex").toInt() + val mangaId = ctx.pathParam("mangaId").toInt() + ctx.json(future { Chapter.getChapter(chapterIndex, mangaId) }) + } + + /** used to modify a chapter's parameters */ + fun chapterModify(ctx: Context) { + val chapterIndex = ctx.pathParam("chapterIndex").toInt() + val mangaId = ctx.pathParam("mangaId").toInt() + + val read = ctx.formParam("read")?.toBoolean() + val bookmarked = ctx.formParam("bookmarked")?.toBoolean() + val markPrevRead = ctx.formParam("markPrevRead")?.toBoolean() + val lastPageRead = ctx.formParam("lastPageRead")?.toInt() + + Chapter.modifyChapter(mangaId, chapterIndex, read, bookmarked, markPrevRead, lastPageRead) + + ctx.status(200) + } + + /** used to modify a chapter's meta parameters */ + fun chapterMeta(ctx: Context) { + val chapterIndex = ctx.pathParam("chapterIndex").toInt() + val mangaId = ctx.pathParam("mangaId").toInt() + + val key = ctx.formParam("key")!! + val value = ctx.formParam("value")!! + + Chapter.modifyChapterMeta(mangaId, chapterIndex, key, value) + + ctx.status(200) + } + + /** get page at index "index" */ + fun pageRetrieve(ctx: Context) { + val mangaId = ctx.pathParam("mangaId").toInt() + val chapterIndex = ctx.pathParam("chapterIndex").toInt() + val index = ctx.pathParam("index").toInt() + + ctx.result( + future { Page.getPageImage(mangaId, chapterIndex, index) } + .thenApply { + ctx.header("content-type", it.second) + it.first + } + ) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt new file mode 100644 index 00000000..a55b79e9 --- /dev/null +++ b/server/src/main/kotlin/suwayomi/tachidesk/manga/controller/SourceController.kt @@ -0,0 +1,84 @@ +package suwayomi.tachidesk.manga.controller + +/* + * 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 io.javalin.http.Context +import suwayomi.tachidesk.manga.impl.MangaList +import suwayomi.tachidesk.manga.impl.Search +import suwayomi.tachidesk.manga.impl.Source +import suwayomi.tachidesk.manga.impl.Source.SourcePreferenceChange +import suwayomi.tachidesk.server.JavalinSetup +import suwayomi.tachidesk.server.JavalinSetup.future + +object SourceController { + /** list of sources */ + fun list(ctx: Context) { + ctx.json(Source.getSourceList()) + } + + /** fetch source with id `sourceId` */ + fun retrieve(ctx: Context) { + val sourceId = ctx.pathParam("sourceId").toLong() + ctx.json(Source.getSource(sourceId)) + } + + /** popular mangas from source with id `sourceId` */ + fun popular(ctx: Context) { + val sourceId = ctx.pathParam("sourceId").toLong() + val pageNum = ctx.pathParam("pageNum").toInt() + ctx.json( + future { + MangaList.getMangaList(sourceId, pageNum, popular = true) + } + ) + } + + /** latest mangas from source with id `sourceId` */ + fun latest(ctx: Context) { + val sourceId = ctx.pathParam("sourceId").toLong() + val pageNum = ctx.pathParam("pageNum").toInt() + ctx.json( + future { + MangaList.getMangaList(sourceId, pageNum, popular = false) + } + ) + } + + /** fetch preferences of source with id `sourceId` */ + fun getPreferences(ctx: Context) { + val sourceId = ctx.pathParam("sourceId").toLong() + ctx.json(Source.getSourcePreferences(sourceId)) + } + + /** fetch preferences 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)) + } + + /** fetch filters of source with id `sourceId` */ + fun filters(ctx: Context) { // TODO + val sourceId = ctx.pathParam("sourceId").toLong() + ctx.json(Search.sourceFilters(sourceId)) + } + + /** single source search */ + fun searchSingle(ctx: Context) { + val sourceId = ctx.pathParam("sourceId").toLong() + val searchTerm = ctx.pathParam("searchTerm") + val pageNum = ctx.pathParam("pageNum").toInt() + ctx.json(JavalinSetup.future { Search.sourceSearch(sourceId, searchTerm, pageNum) }) + } + + /** all source search */ + fun searchAll(ctx: Context) { // TODO + val searchTerm = ctx.pathParam("searchTerm") + ctx.json(Search.sourceGlobalSearch(searchTerm)) + } +} diff --git a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt index 1c08e88c..b0ebbbb9 100644 --- a/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt +++ b/server/src/main/kotlin/suwayomi/tachidesk/server/JavalinSetup.kt @@ -8,6 +8,7 @@ package suwayomi.tachidesk.server * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ import io.javalin.Javalin +import io.javalin.apibuilder.ApiBuilder.path import io.javalin.http.staticfiles.Location import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -77,8 +78,12 @@ object JavalinSetup { ctx.result(e.message ?: "Internal Server Error") } - GlobalAPI.defineEndpoints(app) - MangaAPI.defineEndpoints(app) - AnimeAPI.defineEndpoints(app) + app.routes { + path("api/v1/") { + GlobalAPI.defineEndpoints() + MangaAPI.defineEndpoints(app) + AnimeAPI.defineEndpoints(app) // TODO: migrate Anime endpoints + } + } } }