diff --git a/public/js/reader.js b/public/js/reader.js index 233e2aa..2b42b0a 100644 --- a/public/js/reader.js +++ b/public/js/reader.js @@ -219,7 +219,7 @@ const saveProgress = (idx, cb) => { lastSavedPage = idx; console.log('saving progress', idx); - const url = `${base_url}api/progress/${tid}/${idx}?${$.param({entry: eid})}`; + const url = `${base_url}api/progress/${tid}/${idx}?${$.param({eid: eid})}`; $.post(url) .then(data => { if (data.error) throw new Error(data.error); diff --git a/public/js/title.js b/public/js/title.js index 0942cd1..7f0e0bd 100644 --- a/public/js/title.js +++ b/public/js/title.js @@ -63,7 +63,7 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi const updateProgress = (tid, eid, page) => { let url = `${base_url}api/progress/${tid}/${page}` const query = $.param({ - entry: eid + eid: eid }); if (eid) url += `?${query}`; @@ -89,7 +89,7 @@ const renameSubmit = (name, eid) => { } const query = $.param({ - entry: eid + eid: eid }); let url = `${base_url}api/admin/display_name/${titleId}/${name}`; if (eid) @@ -150,10 +150,10 @@ const setupUpload = (eid) => { const bar = $('#upload-progress').get(0); const titleId = upload.attr('data-title-id'); const queryObj = { - title: titleId + tid: titleId }; if (eid) - queryObj['entry'] = eid; + queryObj['eid'] = eid; const query = $.param(queryObj); const url = `${base_url}api/admin/upload/cover?${query}`; console.log(url); diff --git a/shard.lock b/shard.lock index 065bd2a..6189fcd 100644 --- a/shard.lock +++ b/shard.lock @@ -48,10 +48,18 @@ shards: git: https://github.com/jeromegn/kilt.git version: 0.4.0 + koa: + git: https://github.com/hkalexling/koa.git + version: 0.5.0 + myhtml: git: https://github.com/kostya/myhtml.git version: 1.5.1 + open_api: + git: https://github.com/jreinert/open_api.cr.git + version: 1.2.1+git.commit.95e4df2ca10b1fe88b8b35c62a18b06a10267b6c + radix: git: https://github.com/luislavena/radix.git version: 0.3.9 diff --git a/shard.yml b/shard.yml index d52dabb..6e11021 100644 --- a/shard.yml +++ b/shard.yml @@ -37,3 +37,5 @@ dependencies: github: mamantoha/http_proxy image_size: github: hkalexling/image_size.cr + koa: + github: hkalexling/koa diff --git a/src/library/entry.cr b/src/library/entry.cr index 7a8cfae..810c6c8 100644 --- a/src/library/entry.cr +++ b/src/library/entry.cr @@ -47,8 +47,7 @@ class Entry def to_json(json : JSON::Builder) json.object do - {% for str in ["zip_path", "title", "size", "id", - "encoded_path", "encoded_title"] %} + {% for str in ["zip_path", "title", "size", "id"] %} json.field {{str}}, @{{str.id}} {% end %} json.field "title_id", @book.id diff --git a/src/library/title.cr b/src/library/title.cr index 6754504..f3d8b2f 100644 --- a/src/library/title.cr +++ b/src/library/title.cr @@ -56,7 +56,7 @@ class Title def to_json(json : JSON::Builder) json.object do - {% for str in ["dir", "title", "id", "encoded_title"] %} + {% for str in ["dir", "title", "id"] %} json.field {{str}}, @{{str.id}} {% end %} json.field "display_name", display_name diff --git a/src/routes/api.cr b/src/routes/api.cr index 11a6134..fab0b28 100644 --- a/src/routes/api.cr +++ b/src/routes/api.cr @@ -1,9 +1,151 @@ require "./router" require "../mangadex/*" require "../upload" +require "koa" class APIRouter < Router + @@api_json : String? + + macro s(fields) + { + {% for field in fields %} + {{field}} => "string", + {% end %} + } + end + def initialize + Koa.init "Mango API", version: MANGO_VERSION + + Koa.cookie_auth "cookie", "mango-sessid-#{Config.current.port}" + Koa.global_tag "admin", desc: <<-MD + These are the admin APIs only accessible for users with admin access. A non-admin user will get HTTP 403 when calling the APIs. + MD + + Koa.binary "binary", desc: "A binary file" + Koa.array "entryAry", "$entry", desc: "An array of entries" + Koa.array "titleAry", "$title", desc: "An array of titles" + Koa.array "strAry", "string", desc: "An array of strings" + + entry_schema = { + "pages" => "integer", + "mtime" => "integer", + }.merge s %w(zip_path title size id title_id display_name cover_url) + Koa.object "entry", entry_schema, desc: "An entry (aka. chapter) in a book" + + title_schema = { + "mtime" => "integer", + "entries" => "$entryAry", + "titles" => "$titleAry", + "parents" => "$strAry", + }.merge s %w(dir title id display_name cover_url) + Koa.object "title", title_schema, + desc: "A manga title (a collection of entries and sub-titles)" + + Koa.object "library", { + "dir" => "string", + "titles" => "$titleAry", + }, desc: "A library containing a list of top-level titles" + + Koa.object "scanResult", { + "milliseconds" => "integer", + "titles" => "integer", + } + + Koa.object "progressResult", { + "progress" => "number", + } + + Koa.object "result", { + "success" => "boolean", + "error" => "string?", + } + + mc_schema = { + "groups" => "object", + }.merge s %w(id title volume chapter language full_title time manga_title manga_id) + Koa.object "mangadexChapter", mc_schema, desc: "A MangaDex chapter" + + Koa.array "chapterAry", "$mangadexChapter" + + mm_schema = { + "chapers" => "$chapterAry", + }.merge s %w(id title description author artist cover_url) + Koa.object "mangadexManga", mm_schema, desc: "A MangaDex manga" + + Koa.object "chaptersObj", { + "chapters" => "$chapterAry", + } + + Koa.object "successFailCount", { + "success" => "integer", + "fail" => "integer", + } + + job_schema = { + "pages" => "integer", + "success_count" => "integer", + "fail_count" => "integer", + "time" => "integer", + }.merge s %w(id manga_id title manga_title status_message status) + Koa.object "job", job_schema, desc: "A download job in the queue" + + Koa.array "jobAry", "$job" + + Koa.object "jobs", { + "success" => "boolean", + "paused" => "boolean", + "jobs" => "$jobAry", + } + + Koa.object "binaryUpload", { + "file" => "$binary", + } + + Koa.object "pluginListBody", { + "plugin" => "string", + "query" => "string", + } + + Koa.object "pluginChapter", { + "id" => "string", + "title" => "string", + } + + Koa.array "pluginChapterAry", "$pluginChapter" + + Koa.object "pluginList", { + "success" => "boolean", + "chapters" => "$pluginChapterAry?", + "title" => "string?", + "error" => "string?", + } + + Koa.object "pluginDownload", { + "plugin" => "string", + "title" => "string", + "chapters" => "$pluginChapterAry", + } + + Koa.object "dimension", { + "width" => "integer", + "height" => "integer", + } + + Koa.array "dimensionAry", "$dimension" + + Koa.object "dimensionResult", { + "success" => "boolean", + "dimensions" => "$dimensionAry?", + "error" => "string?", + } + + Koa.describe "Returns a page in a manga entry" + Koa.path "tid", desc: "Title ID" + Koa.path "eid", desc: "Entry ID" + Koa.path "page", type: "integer", desc: "The page number to return (starts from 1)" + Koa.response 200, ref: "$binary", media_type: "image/*" + Koa.response 500, "Page not found or not readable" get "/api/page/:tid/:eid/:page" do |env| begin tid = env.params.url["tid"] @@ -26,6 +168,11 @@ class APIRouter < Router end end + Koa.describe "Returns the cover image of a manga entry" + Koa.path "tid", desc: "Title ID" + Koa.path "eid", desc: "Entry ID" + Koa.response 200, ref: "$binary", media_type: "image/*" + Koa.response 500, "Page not found or not readable" get "/api/cover/:tid/:eid" do |env| begin tid = env.params.url["tid"] @@ -48,6 +195,10 @@ class APIRouter < Router end end + Koa.describe "Returns the book with title `tid`" + Koa.path "tid", desc: "Title ID" + Koa.response 200, ref: "$title" + Koa.response 404, "Title not found" get "/api/book/:tid" do |env| begin tid = env.params.url["tid"] @@ -57,15 +208,20 @@ class APIRouter < Router send_json env, title.to_json rescue e @context.error e - env.response.status_code = 500 + env.response.status_code = 404 e.message end end - get "/api/book" do |env| + Koa.describe "Returns the entire library with all titles and entries" + Koa.response 200, ref: "$library" + get "/api/library" do |env| send_json env, @context.library.to_json end + Koa.describe "Triggers a library scan" + Koa.tag "admin" + Koa.response 200, ref: "$scanResult" post "/api/admin/scan" do |env| start = Time.utc @context.library.scan @@ -76,18 +232,26 @@ class APIRouter < Router }.to_json end + Koa.describe "Returns the thumbanil generation progress between 0 and 1" + Koa.tag "admin" + Koa.response 200, ref: "$progressResult" get "/api/admin/thumbnail_progress" do |env| send_json env, { "progress" => Library.default.thumbnail_generation_progress, }.to_json end + Koa.describe "Triggers a thumbanil generation" + Koa.tag "admin" post "/api/admin/generate_thumbnails" do |env| spawn do Library.default.generate_thumbnails end end + Koa.describe "Deletes a user with `username`" + Koa.tag "admin" + Koa.response 200, ref: "$result" post "/api/admin/user/delete/:username" do |env| begin username = env.params.url["username"] @@ -103,13 +267,24 @@ class APIRouter < Router end end - post "/api/progress/:title/:page" do |env| + Koa.describe "Updates the reading progress of an entry or the whole title for the current user", <<-MD + When `eid` is provided, sets the reading progress the the entry to `page`. + + When `eid` is omitted, updates the progress of the entire title. Specifically: + + - if `page` is 0, marks the entire title as unread + - otherwise, marks the entire title as read + MD + Koa.path "tid", desc: "Title ID" + Koa.query "eid", desc: "Entry ID", required: false + Koa.path "page", desc: "The new page number indicating the progress" + Koa.response 200, ref: "$result" + post "/api/progress/:tid/:page" do |env| begin username = get_username env - title = (@context.library.get_title env.params.url["title"]) - .not_nil! + title = (@context.library.get_title env.params.url["tid"]).not_nil! page = env.params.url["page"].to_i - entry_id = env.params.query["entry"]? + entry_id = env.params.query["eid"]? if !entry_id.nil? entry = title.get_entry(entry_id).not_nil! @@ -131,10 +306,15 @@ class APIRouter < Router end end - post "/api/bulk-progress/:action/:title" do |env| + Koa.describe "Updates the reading progress of multiple entries in a title" + Koa.path "action", desc: "The action to perform. Can be either `read` or `unread`" + Koa.path "tid", desc: "Title ID" + Koa.body ref: "$strAry", desc: "An array of entry IDs" + Koa.response 200, ref: "$result" + post "/api/bulk-progress/:action/:tid" do |env| begin username = get_username env - title = (@context.library.get_title env.params.url["title"]).not_nil! + title = (@context.library.get_title env.params.url["tid"]).not_nil! action = env.params.url["action"] ids = env.params.json["ids"].as(Array).map &.as_s @@ -153,12 +333,20 @@ class APIRouter < Router end end - post "/api/admin/display_name/:title/:name" do |env| + Koa.describe "Sets the display name of a title or an entry", <<-MD + When `eid` is provided, apply the display name to the entry. Otherwise, apply the display name to the title identified by `tid`. + MD + Koa.tag "admin" + Koa.path "tid", desc: "Title ID" + Koa.query "eid", desc: "Entry ID", required: false + Koa.path "name", desc: "The new display name" + Koa.response 200, ref: "$result" + post "/api/admin/display_name/:tid/:name" do |env| begin - title = (@context.library.get_title env.params.url["title"]) + title = (@context.library.get_title env.params.url["tid"]) .not_nil! name = env.params.url["name"] - entry = env.params.query["entry"]? + entry = env.params.query["eid"]? if entry.nil? title.set_display_name name else @@ -176,6 +364,12 @@ class APIRouter < Router end end + Koa.describe "Returns a MangaDex manga identified by `id`", <<-MD + On error, returns a JSON that contains the error message in the `error` field. + MD + Koa.tag "admin" + Koa.path "id", desc: "A MangaDex manga ID" + Koa.response 200, ref: "$mangadexManga" get "/api/admin/mangadex/manga/:id" do |env| begin id = env.params.url["id"] @@ -188,6 +382,12 @@ class APIRouter < Router end end + Koa.describe "Adds a list of MangaDex chapters to the download queue", <<-MD + On error, returns a JSON that contains the error message in the `error` field. + MD + Koa.tag "admin" + Koa.body ref: "$chaptersObj" + Koa.response 200, ref: "$successFailCount" post "/api/admin/mangadex/download" do |env| begin chapters = env.params.json["chapters"].as(Array).map { |c| c.as_h } @@ -224,6 +424,11 @@ class APIRouter < Router end end + Koa.describe "Returns the current download queue", <<-MD + On error, returns a JSON that contains the error message in the `error` field. + MD + Koa.tag "admin" + Koa.response 200, ref: "$jobs" get "/api/admin/mangadex/queue" do |env| begin jobs = @context.queue.get_all @@ -240,6 +445,19 @@ class APIRouter < Router end end + Koa.describe "Perform an action on a download job or all jobs in the queue", <<-MD + The `action` parameter can be `delete`, `retry`, `pause` or `resume`. + + When `action` is `pause` or `resume`, pauses or resumes the download queue, respectively. + + When `action` is set to `delete`, the behavior depends on `id`. If `id` is provided, deletes the specific job identified by the ID. Otherwise, deletes all **completed** jobs in the queue. + + When `action` is set to `retry`, the behaviro depends on `id`. If `id` is provided, restarts the job identified by the ID. Otherwise, retries all jobs in the `Error` or `MissingPages` status in the queue. + MD + Koa.tag "admin" + Koa.path "action", desc: "The action to perform. It should be one of the followins: `delete`, `retry`, `pause` and `resume`." + Koa.query "id", required: false, desc: "A job ID" + Koa.response 200, ref: "$result" post "/api/admin/mangadex/queue/:action" do |env| begin action = env.params.url["action"] @@ -274,6 +492,22 @@ class APIRouter < Router end end + Koa.describe "Uploads a file to the server", <<-MD + Currently the only supported value for the `targe` parameter is `cover`. + + ### Cover + + Uploads a cover image for a title or an entry. + + Query parameters: + - `tid`: A title ID + - `eid`: (Optional) An entry ID + + When `eid` is omitted, the new cover image will be applied to the title. Otherwise, applies the image to the specified entry. + MD + Koa.tag "admin" + Koa.body type: "multipart/form-data", ref: "$binaryUpload" + Koa.response 200, ref: "$result" post "/api/admin/upload/:target" do |env| begin target = env.params.url["target"] @@ -288,8 +522,8 @@ class APIRouter < Router case target when "cover" - title_id = env.params.query["title"] - entry_id = env.params.query["entry"]? + title_id = env.params.query["tid"] + entry_id = env.params.query["eid"]? title = @context.library.get_title(title_id).not_nil! unless SUPPORTED_IMG_TYPES.includes? \ @@ -328,6 +562,10 @@ class APIRouter < Router end end + Koa.describe "Lists the chapters in a title from a plugin" + Koa.tag "admin" + Koa.body ref: "$pluginListBody" + Koa.response 200, ref: "$pluginList" post "/api/admin/plugin/list" do |env| begin query = env.params.json["query"].as String @@ -350,6 +588,10 @@ class APIRouter < Router end end + Koa.describe "Adds a list of chapters from a plugin to the download queue" + Koa.tag "admin" + Koa.body ref: "$pluginDownload" + Koa.response 200, ref: "$successFailCount" post "/api/admin/plugin/download" do |env| begin plugin = Plugin.new env.params.json["plugin"].as String @@ -379,6 +621,10 @@ class APIRouter < Router end end + Koa.describe "Returns the image dimention of all pages in an entry" + Koa.path "tid", desc: "A title ID" + Koa.path "eid", desc: "An entry ID" + Koa.response 200, ref: "$dimensionResult" get "/api/dimensions/:tid/:eid" do |env| begin tid = env.params.url["tid"] @@ -401,5 +647,16 @@ class APIRouter < Router }.to_json end end + + doc = Koa.generate + @@api_json = doc.to_json if doc + + get "/openapi.json" do |env| + if @@api_json + send_json env, @@api_json + else + env.response.status_code = 404 + end + end end end diff --git a/src/routes/main.cr b/src/routes/main.cr index 4c55472..bf54c0f 100644 --- a/src/routes/main.cr +++ b/src/routes/main.cr @@ -113,5 +113,9 @@ class MainRouter < Router env.response.status_code = 500 end end + + get "/swagger" do |env| + render "src/views/swagger.html.ecr" + end end end diff --git a/src/views/swagger.html.ecr b/src/views/swagger.html.ecr new file mode 100644 index 0000000..9babf87 --- /dev/null +++ b/src/views/swagger.html.ecr @@ -0,0 +1,22 @@ + + +
+ + +