package suwayomi.tachidesk /* * 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.Javalin import suwayomi.server.JavalinSetup import suwayomi.server.JavalinSetup.future import suwayomi.server.impl.About import suwayomi.tachidesk.impl.Category import suwayomi.tachidesk.impl.CategoryManga.addMangaToCategory import suwayomi.tachidesk.impl.CategoryManga.getCategoryMangaList import suwayomi.tachidesk.impl.CategoryManga.getMangaCategories import suwayomi.tachidesk.impl.CategoryManga.removeMangaFromCategory import suwayomi.tachidesk.impl.Chapter.getChapter import suwayomi.tachidesk.impl.Chapter.getChapterList import suwayomi.tachidesk.impl.Chapter.modifyChapter import suwayomi.tachidesk.impl.Library.addMangaToLibrary import suwayomi.tachidesk.impl.Library.getLibraryMangas import suwayomi.tachidesk.impl.Library.removeMangaFromLibrary import suwayomi.tachidesk.impl.Manga.getManga import suwayomi.tachidesk.impl.Manga.getMangaThumbnail import suwayomi.tachidesk.impl.MangaList.getMangaList import suwayomi.tachidesk.impl.Page.getPageImage import suwayomi.tachidesk.impl.Search.sourceFilters import suwayomi.tachidesk.impl.Search.sourceGlobalSearch import suwayomi.tachidesk.impl.Search.sourceSearch import suwayomi.tachidesk.impl.Source.getSource import suwayomi.tachidesk.impl.Source.getSourceList import suwayomi.tachidesk.impl.backup.BackupFlags import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupExport.createLegacyBackup import suwayomi.tachidesk.impl.backup.legacy.LegacyBackupImport.restoreLegacyBackup import suwayomi.tachidesk.impl.extension.Extension.getExtensionIcon import suwayomi.tachidesk.impl.extension.Extension.installExtension import suwayomi.tachidesk.impl.extension.Extension.uninstallExtension import suwayomi.tachidesk.impl.extension.Extension.updateExtension import suwayomi.tachidesk.impl.extension.ExtensionsList.getExtensionList import java.text.SimpleDateFormat import java.util.Date object TachideskAPI { fun defineEndpoints(app: Javalin) { // list all extensions app.get("/api/v1/extension/list") { ctx -> ctx.json( JavalinSetup.future { getExtensionList() } ) } // install extension identified with "pkgName" app.get("/api/v1/extension/install/:pkgName") { ctx -> val pkgName = ctx.pathParam("pkgName") ctx.json( JavalinSetup.future { installExtension(pkgName) } ) } // update extension identified with "pkgName" app.get("/api/v1/extension/update/:pkgName") { ctx -> val pkgName = ctx.pathParam("pkgName") ctx.json( JavalinSetup.future { updateExtension(pkgName) } ) } // uninstall extension identified with "pkgName" app.get("/api/v1/extension/uninstall/:pkgName") { ctx -> val pkgName = ctx.pathParam("pkgName") uninstallExtension(pkgName) ctx.status(200) } // icon for extension named `apkName` app.get("/api/v1/extension/icon/:apkName") { ctx -> // TODO: move to pkgName val apkName = ctx.pathParam("apkName") ctx.result( JavalinSetup.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)) } // 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( JavalinSetup.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( JavalinSetup.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( JavalinSetup.future { getManga(mangaId, onlineFetch) } ) } // manga thumbnail app.get("api/v1/manga/:mangaId/thumbnail") { ctx -> val mangaId = ctx.pathParam("mangaId").toInt() ctx.result( JavalinSetup.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(JavalinSetup.future { getChapterList(mangaId, onlineFetch) }) } // 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(JavalinSetup.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) } // 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( JavalinSetup.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(JavalinSetup.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( JavalinSetup.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( JavalinSetup.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) } // returns some static info of the current app build app.get("/api/v1/about/") { ctx -> ctx.json(About.getAbout()) } // 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( JavalinSetup.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( JavalinSetup.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( JavalinSetup.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 -> // TODO: send current stat // TODO: add to downlad subscribers } ws.onMessage { // TODO: send current stat } ws.onClose { ctx -> // TODO: remove from subscribers } } } }