mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-02 02:45:29 -04:00
1440 lines
43 KiB
Crystal
1440 lines
43 KiB
Crystal
require "../upload"
|
|
require "koa"
|
|
require "digest"
|
|
|
|
struct APIRouter
|
|
@@api_json : String?
|
|
|
|
API_VERSION = "0.1.0"
|
|
|
|
macro s(fields)
|
|
{
|
|
{% for field in fields %}
|
|
{{field}} => String,
|
|
{% end %}
|
|
}
|
|
end
|
|
|
|
def initialize
|
|
Koa.init "Mango API", version: API_VERSION, desc: <<-MD
|
|
# A Word of Caution
|
|
|
|
This API was designed for internal use only, and the design doesn't comply with the resources convention of a RESTful API. Because of this, most of the API endpoints listed here will soon be updated and removed in future versions of Mango, so use them at your own risk!
|
|
|
|
# Authentication
|
|
|
|
All endpoints except `/api/login` require authentication. After logging in, your session ID would be stored as a cookie named `mango-sessid-#{Config.current.port}`, which can be used to authenticate the API access. Note that all admin API endpoints (`/api/admin/...`) require the logged-in user to have admin access.
|
|
|
|
# Terminologies
|
|
|
|
- Entry: An entry is a `cbz`/`cbr` file in your library. Depending on how you organize your manga collection, an entry can contain a chapter, a volume or even an entire manga.
|
|
- Title: A title contains a list of entries and optionally some sub-titles. For example, you can have a title to store a manga, and it contains a list of sub-titles representing the volumes in the manga. Each sub-title would then contain a list of entries representing the chapters in the volume.
|
|
- Library: The library is a collection of top-level titles, and it does not contain entries (though the titles do). A Mango instance can only have one library.
|
|
MD
|
|
|
|
Koa.cookie_auth "cookie", "mango-sessid-#{Config.current.port}"
|
|
Koa.define_tag "admin", desc: <<-MD
|
|
These are the admin endpoints only accessible for users with admin access. A non-admin user will get HTTP 403 when calling the endpoints.
|
|
MD
|
|
|
|
Koa.schema "entry", {
|
|
"pages" => Int32,
|
|
"mtime" => Int64,
|
|
}.merge(s %w(path title size id title_id display_name cover_url)),
|
|
desc: "An entry in a book"
|
|
|
|
Koa.schema "title", {
|
|
"mtime" => Int64,
|
|
"entries" => ["entry"],
|
|
"titles" => ["title"],
|
|
"parents" => [{
|
|
"title" => String,
|
|
"id" => String,
|
|
}],
|
|
"title_percentages" => [Float64?],
|
|
"entry_percentages" => [Float64?],
|
|
}.merge(s %w(dir title id display_name cover_url)),
|
|
desc: "A manga title (a collection of entries and sub-titles)"
|
|
|
|
Koa.schema "result", {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
}
|
|
|
|
Koa.schema "filter", {
|
|
"key" => String,
|
|
"type" => String,
|
|
"value" => String | Int32 | Int64 | Float32,
|
|
}
|
|
|
|
Koa.schema "subscription", {
|
|
"id" => String,
|
|
"plugin_id" => String,
|
|
"manga_id" => String,
|
|
"manga_title" => String,
|
|
"name" => String,
|
|
"created_at" => Int64,
|
|
"last_checked" => Int64,
|
|
"filters" => ["filter"],
|
|
}
|
|
|
|
Koa.describe "Authenticates a user", <<-MD
|
|
After successful login, the cookie `mango-sessid-#{Config.current.port}` will contain a valid session ID that can be used for subsequent requests
|
|
MD
|
|
Koa.body schema: {
|
|
"username" => String,
|
|
"password" => String,
|
|
}
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"session_id" => String?,
|
|
"is_admin" => Bool?,
|
|
}
|
|
Koa.tag "users"
|
|
post "/api/login" do |env|
|
|
begin
|
|
username = env.params.json["username"].as String
|
|
password = env.params.json["password"].as String
|
|
token = Storage.default.verify_user(username, password).not_nil!
|
|
|
|
env.session.string "token", token
|
|
send_json env, {
|
|
"success" => true,
|
|
"session_id" => env.session.id,
|
|
"is_admin" => Storage.default.username_is_admin username,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
env.response.status_code = 403
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Returns a page in a manga entry"
|
|
Koa.path "tid", desc: "Title ID"
|
|
Koa.path "eid", desc: "Entry ID"
|
|
Koa.path "page", schema: Int32, desc: "The page number to return (starts from 1)"
|
|
Koa.response 200, schema: Bytes, media_type: "image/*"
|
|
Koa.response 500, "Page not found or not readable"
|
|
Koa.response 304, "Page not modified (only available when `If-None-Match` is set)"
|
|
Koa.tag "reader"
|
|
get "/api/page/:tid/:eid/:page" do |env|
|
|
begin
|
|
tid = env.params.url["tid"]
|
|
eid = env.params.url["eid"]
|
|
page = env.params.url["page"].to_i
|
|
prev_e_tag = env.request.headers["If-None-Match"]?
|
|
|
|
title = Library.default.get_title tid
|
|
raise "Title ID `#{tid}` not found" if title.nil?
|
|
entry = title.get_entry eid
|
|
raise "Entry ID `#{eid}` of `#{title.title}` not found" if entry.nil?
|
|
img = entry.read_page page
|
|
raise "Failed to load page #{page} of " \
|
|
"`#{title.title}/#{entry.title}`" if img.nil?
|
|
|
|
e_tag = Digest::SHA1.hexdigest img.data
|
|
if prev_e_tag == e_tag
|
|
env.response.status_code = 304
|
|
""
|
|
else
|
|
if entry.is_a? DirEntry
|
|
cache_control = "no-cache, max-age=86400"
|
|
else
|
|
cache_control = "public, max-age=86400"
|
|
end
|
|
env.response.headers["ETag"] = e_tag
|
|
env.response.headers["Cache-Control"] = cache_control
|
|
send_img env, img
|
|
end
|
|
rescue e
|
|
Logger.error e
|
|
env.response.status_code = 500
|
|
send_text env, e.message
|
|
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, schema: Bytes, media_type: "image/*"
|
|
Koa.response 304, "Page not modified (only available when `If-None-Match` is set)"
|
|
Koa.response 500, "Page not found or not readable"
|
|
Koa.tag "library"
|
|
get "/api/cover/:tid/:eid" do |env|
|
|
begin
|
|
tid = env.params.url["tid"]
|
|
eid = env.params.url["eid"]
|
|
prev_e_tag = env.request.headers["If-None-Match"]?
|
|
|
|
title = Library.default.get_title tid
|
|
raise "Title ID `#{tid}` not found" if title.nil?
|
|
entry = title.get_entry eid
|
|
raise "Entry ID `#{eid}` of `#{title.title}` not found" if entry.nil?
|
|
|
|
img = entry.get_thumbnail || entry.read_page 1
|
|
raise "Failed to get cover of `#{title.title}/#{entry.title}`" \
|
|
if img.nil?
|
|
|
|
e_tag = Digest::SHA1.hexdigest img.data
|
|
if prev_e_tag == e_tag
|
|
env.response.status_code = 304
|
|
""
|
|
else
|
|
env.response.headers["ETag"] = e_tag
|
|
send_img env, img
|
|
end
|
|
rescue e
|
|
Logger.error e
|
|
env.response.status_code = 500
|
|
send_text env, e.message
|
|
end
|
|
end
|
|
|
|
Koa.describe "Returns the book with title `tid`", <<-MD
|
|
The entries and titles will be sorted by the default sorting method for the logged-in user.
|
|
- Supply the `percentage` query parameter to include the reading progress
|
|
- Supply the `slim` query parameter to strip away "display_name", "cover_url", and "mtime" from the returned object to speed up the loading time
|
|
- Supply the `depth` query parameter to control the depth of nested titles to return.
|
|
- When `depth` is 1, returns the top-level titles and sub-titles/entries one level in them
|
|
- When `depth` is 0, returns the top-level titles without their sub-titles/entries
|
|
- When `depth` is N, returns the top-level titles and sub-titles/entries N levels in them
|
|
- When `depth` is negative, returns the entire library
|
|
MD
|
|
Koa.path "tid", desc: "Title ID"
|
|
Koa.query "slim"
|
|
Koa.query "depth"
|
|
Koa.query "percentage"
|
|
Koa.response 200, schema: "title"
|
|
Koa.response 404, "Title not found"
|
|
Koa.tag "library"
|
|
get "/api/book/:tid" do |env|
|
|
begin
|
|
username = get_username env
|
|
|
|
tid = env.params.url["tid"]
|
|
title = Library.default.get_title tid
|
|
raise "Title ID `#{tid}` not found" if title.nil?
|
|
|
|
sort_opt = SortOptions.from_info_json title.dir, username
|
|
|
|
slim = !env.params.query["slim"]?.nil?
|
|
depth = env.params.query["depth"]?.try(&.to_i?) || -1
|
|
percentage = !env.params.query["percentage"]?.nil?
|
|
|
|
send_json env, title.build_json(slim: slim, depth: depth,
|
|
sort_context: {username: username,
|
|
opt: sort_opt}, percentage: percentage)
|
|
rescue e
|
|
Logger.error e
|
|
env.response.status_code = 404
|
|
send_text env, e.message
|
|
end
|
|
end
|
|
|
|
Koa.describe "Returns the sorting option of a title or the library", <<-MD
|
|
- If the query parameter `tid` is supplied, returns the sorting option of the title identified by the `tid`.
|
|
- If the query parameter `tid` is missing, returns the sorting option of the library.
|
|
MD
|
|
Koa.query "tid"
|
|
Koa.response 200, schema: {
|
|
"method" => String?,
|
|
"ascend" => Bool?,
|
|
"error" => String?,
|
|
}
|
|
Koa.tag "library"
|
|
get "/api/sort_opt" do |env|
|
|
username = get_username env
|
|
|
|
tid = env.params.query["tid"]?
|
|
dir = if tid
|
|
(Library.default.get_title tid).not_nil!.dir
|
|
else
|
|
Library.default.dir
|
|
end
|
|
sort_opt = SortOptions.from_info_json dir, username
|
|
send_json env, sort_opt.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
|
|
Koa.describe "Updates the sorting option of a title or the library", <<-MD
|
|
- When the `tid` field is supplied in the body, updates the sorting option of the title identified by the `tid`.
|
|
- When the `tid` field is missing in the body, updates the sorting option of the library.
|
|
MD
|
|
Koa.body schema: {
|
|
"tid" => String?,
|
|
"method" => String,
|
|
"ascend" => Bool,
|
|
}
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
}
|
|
Koa.tag "library"
|
|
put "/api/sort_opt" do |env|
|
|
username = get_username env
|
|
|
|
tid = env.params.json["tid"]?.try &.as String
|
|
dir = if tid
|
|
(Library.default.get_title tid).not_nil!.dir
|
|
else
|
|
Library.default.dir
|
|
end
|
|
|
|
method = env.params.json["sort"].as String
|
|
ascend = env.params.json["ascend"].as Bool
|
|
sort_opt = SortOptions.new method, ascend
|
|
|
|
TitleInfo.new dir do |info|
|
|
info.sort_by[username] = sort_opt.to_tuple
|
|
info.save
|
|
end
|
|
send_json env, {
|
|
"success" => true,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
|
|
Koa.describe "Returns the entire library with all titles and entries", <<-MD
|
|
The titles will be sorted by the default sorting method for the logged-in user.
|
|
- Supply the `slim` query parameter to strip away "display_name", "cover_url", and "mtime" from the returned object to speed up the loading time
|
|
- Supply the `dpeth` query parameter to control the depth of nested titles to return.
|
|
- Supply the `percentage` query parameter to include the reading progress
|
|
- When `depth` is 1, returns the requested title and sub-titles/entries one level in it
|
|
- When `depth` is 0, returns the requested title without its sub-titles/entries
|
|
- When `depth` is N, returns the requested title and sub-titles/entries N levels in it
|
|
- When `depth` is negative, returns the requested title and all sub-titles/entries in it
|
|
MD
|
|
Koa.query "slim"
|
|
Koa.query "depth"
|
|
Koa.query "percentage"
|
|
Koa.response 200, schema: {
|
|
"dir" => String,
|
|
"titles" => ["title"],
|
|
"title_percentage" => [Float64?],
|
|
}
|
|
Koa.tag "library"
|
|
get "/api/library" do |env|
|
|
username = get_username env
|
|
|
|
sort_opt = SortOptions.from_info_json Library.default.dir, username
|
|
|
|
slim = !env.params.query["slim"]?.nil?
|
|
depth = env.params.query["depth"]?.try(&.to_i?) || -1
|
|
percentage = !env.params.query["percentage"]?.nil?
|
|
|
|
send_json env, Library.default.build_json(slim: slim, depth: depth,
|
|
sort_context: {username: username,
|
|
opt: sort_opt}, percentage: percentage)
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
|
|
Koa.describe "Returns the continue reading entries"
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"entries" => ["entry"],
|
|
"entry_percentages" => [Float64],
|
|
}
|
|
Koa.tag "library"
|
|
get "/api/library/continue_reading" do |env|
|
|
username = get_username env
|
|
cr_entries = Library.default.get_continue_reading_entries username
|
|
|
|
json = JSON.build do |j|
|
|
j.object do
|
|
j.field "success" do
|
|
j.bool true
|
|
end
|
|
j.field "entries" do
|
|
j.array do
|
|
cr_entries.each do |e|
|
|
j.raw e[:entry].build_json
|
|
end
|
|
end
|
|
end
|
|
j.field "entry_percentages" do
|
|
j.array do
|
|
cr_entries.each do |e|
|
|
j.number e[:percentage]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
send_json env, json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
|
|
Koa.describe "Returns the start reading titles"
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"titles" => ["title"],
|
|
}
|
|
Koa.tag "library"
|
|
get "/api/library/start_reading" do |env|
|
|
username = get_username env
|
|
titles = Library.default.get_start_reading_titles username
|
|
|
|
json = JSON.build do |j|
|
|
j.object do
|
|
j.field "success" do
|
|
j.bool true
|
|
end
|
|
j.field "titles" do
|
|
j.array do
|
|
titles.each do |t|
|
|
j.raw t.build_json depth: 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
send_json env, json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
|
|
Koa.describe "Returns the recently added items"
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"items" => [{
|
|
"item" => "title | entry",
|
|
"percentage" => Float64,
|
|
"count" => Int32,
|
|
}],
|
|
}
|
|
Koa.tag "library"
|
|
get "/api/library/recently_added" do |env|
|
|
username = get_username env
|
|
ra_entries = Library.default.get_recently_added_entries username
|
|
|
|
json = JSON.build do |j|
|
|
j.object do
|
|
j.field "success" do
|
|
j.bool true
|
|
end
|
|
j.field "items" do
|
|
j.array do
|
|
ra_entries.each do |e|
|
|
j.object do
|
|
j.field "item" do
|
|
if e[:grouped_count] === 1
|
|
j.raw e[:entry].build_json
|
|
else
|
|
j.raw e[:entry].book.build_json depth: 0
|
|
end
|
|
end
|
|
j.field "percentage" do
|
|
j.number e[:percentage]
|
|
end
|
|
j.field "count" do
|
|
j.number e[:grouped_count]
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
send_json env, json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
|
|
Koa.describe "Triggers a library scan"
|
|
Koa.tags ["admin", "library"]
|
|
Koa.response 200, schema: {
|
|
"milliseconds" => Float64,
|
|
"titles" => Int32,
|
|
}
|
|
post "/api/admin/scan" do |env|
|
|
start = Time.utc
|
|
Library.default.scan
|
|
ms = (Time.utc - start).total_milliseconds
|
|
send_json env, {
|
|
"milliseconds" => ms,
|
|
"titles" => Library.default.titles.size,
|
|
}.to_json
|
|
end
|
|
|
|
Koa.describe "Returns the thumbnail generation progress between 0 and 1"
|
|
Koa.tags ["admin", "library"]
|
|
Koa.response 200, schema: {
|
|
"progress" => Float64,
|
|
}
|
|
get "/api/admin/thumbnail_progress" do |env|
|
|
send_json env, {
|
|
"progress" => Library.default.thumbnail_ctx.progress,
|
|
}.to_json
|
|
end
|
|
|
|
Koa.describe "Triggers a thumbnail generation"
|
|
Koa.tags ["admin", "library"]
|
|
post "/api/admin/generate_thumbnails" do |env|
|
|
spawn do
|
|
Library.default.generate_thumbnails
|
|
end
|
|
send_text env, ""
|
|
end
|
|
|
|
Koa.describe "Deletes a user with `username`"
|
|
Koa.tags ["admin", "users"]
|
|
Koa.response 200, schema: "result"
|
|
delete "/api/admin/user/delete/:username" do |env|
|
|
begin
|
|
username = env.params.url["username"]
|
|
Storage.default.delete_user username
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
else
|
|
send_json env, {"success" => true}.to_json
|
|
end
|
|
end
|
|
|
|
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 of 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, schema: "result"
|
|
Koa.tag "progress"
|
|
put "/api/progress/:tid/:page" do |env|
|
|
begin
|
|
username = get_username env
|
|
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
|
page = env.params.url["page"].to_i
|
|
entry_id = env.params.query["eid"]?
|
|
|
|
if !entry_id.nil?
|
|
entry = title.get_entry(entry_id).not_nil!
|
|
raise "incorrect page value" if page < 0 || page > entry.pages
|
|
entry.save_progress username, page
|
|
elsif page == 0
|
|
title.unread_all username
|
|
else
|
|
title.read_all username
|
|
end
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
else
|
|
send_json env, {"success" => true}.to_json
|
|
end
|
|
end
|
|
|
|
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 schema: {
|
|
"ids" => [String],
|
|
}, desc: "An array of entry IDs"
|
|
Koa.response 200, schema: "result"
|
|
Koa.tag "progress"
|
|
put "/api/bulk_progress/:action/:tid" do |env|
|
|
begin
|
|
username = get_username env
|
|
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
|
action = env.params.url["action"]
|
|
ids = env.params.json["ids"].as(Array).map &.as_s
|
|
|
|
unless action.in? ["read", "unread"]
|
|
raise "Unknow action #{action}"
|
|
end
|
|
title.bulk_progress action, ids, username
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
else
|
|
send_json env, {"success" => true}.to_json
|
|
end
|
|
end
|
|
|
|
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.tags ["admin", "library"]
|
|
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, schema: "result"
|
|
put "/api/admin/display_name/:tid/:name" do |env|
|
|
begin
|
|
title = (Library.default.get_title env.params.url["tid"])
|
|
.not_nil!
|
|
name = env.params.url["name"]
|
|
entry = env.params.query["eid"]?
|
|
if entry.nil?
|
|
title.set_display_name name
|
|
else
|
|
eobj = title.get_entry entry
|
|
title.set_display_name eobj.not_nil!.title, name
|
|
end
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
else
|
|
send_json env, {"success" => true}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Sets the sort title of a title or an entry", <<-MD
|
|
When `eid` is provided, apply the sort title to the entry. Otherwise, apply the sort title to the title identified by `tid`.
|
|
MD
|
|
Koa.tags ["admin", "library"]
|
|
Koa.path "tid", desc: "Title ID"
|
|
Koa.query "eid", desc: "Entry ID", required: false
|
|
Koa.query "name", desc: "The new sort title"
|
|
Koa.response 200, schema: "result"
|
|
put "/api/admin/sort_title/:tid" do |env|
|
|
username = get_username env
|
|
begin
|
|
title = (Library.default.get_title env.params.url["tid"])
|
|
.not_nil!
|
|
name = env.params.query["name"]?
|
|
entry = env.params.query["eid"]?
|
|
if entry.nil?
|
|
title.set_sort_title name, username
|
|
else
|
|
eobj = title.get_entry entry
|
|
eobj.set_sort_title name, username unless eobj.nil?
|
|
end
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
else
|
|
send_json env, {"success" => true}.to_json
|
|
end
|
|
end
|
|
|
|
ws "/api/admin/mangadex/queue" do |socket, env|
|
|
interval_raw = env.params.query["interval"]?
|
|
interval = (interval_raw.to_i? if interval_raw) || 5
|
|
loop do
|
|
socket.send({
|
|
"jobs" => Queue.default.get_all.reverse,
|
|
"paused" => Queue.default.paused?,
|
|
}.to_json)
|
|
sleep interval.seconds
|
|
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.tags ["admin", "downloader"]
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"paused" => Bool?,
|
|
"jobs?" => [{
|
|
"pages" => Int32,
|
|
"success_count" => Int32,
|
|
"fail_count" => Int32,
|
|
"time" => Int64,
|
|
}.merge(s %w(id manga_id title manga_title status_message status))],
|
|
}
|
|
get "/api/admin/mangadex/queue" do |env|
|
|
begin
|
|
send_json env, {
|
|
"jobs" => Queue.default.get_all.reverse,
|
|
"paused" => Queue.default.paused?,
|
|
"success" => true,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
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 behavior 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.tags ["admin", "downloader"]
|
|
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, schema: "result"
|
|
post "/api/admin/mangadex/queue/:action" do |env|
|
|
begin
|
|
action = env.params.url["action"]
|
|
id = env.params.query["id"]?
|
|
case action
|
|
when "delete"
|
|
if id.nil?
|
|
Queue.default.delete_status Queue::JobStatus::Completed
|
|
else
|
|
Queue.default.delete id
|
|
end
|
|
when "retry"
|
|
if id.nil?
|
|
Queue.default.reset
|
|
else
|
|
Queue.default.reset id
|
|
end
|
|
when "pause"
|
|
Queue.default.pause
|
|
when "resume"
|
|
Queue.default.resume
|
|
else
|
|
raise "Unknown queue action #{action}"
|
|
end
|
|
|
|
send_json env, {"success" => true}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Uploads a file to the server", <<-MD
|
|
Currently the only supported value for the `target` 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 media_type: "multipart/form-data", schema: {
|
|
"file" => Bytes,
|
|
}
|
|
Koa.response 200, schema: "result"
|
|
post "/api/admin/upload/:target" do |env|
|
|
begin
|
|
target = env.params.url["target"]
|
|
|
|
HTTP::FormData.parse env.request do |part|
|
|
next if part.name != "file"
|
|
|
|
filename = part.filename
|
|
if filename.nil?
|
|
raise "No file uploaded"
|
|
end
|
|
|
|
case target
|
|
when "cover"
|
|
title_id = env.params.query["tid"]
|
|
entry_id = env.params.query["eid"]?
|
|
title = Library.default.get_title(title_id).not_nil!
|
|
|
|
unless SUPPORTED_IMG_TYPES.includes? \
|
|
MIME.from_filename? filename
|
|
raise "The uploaded image must be either JPEG or PNG"
|
|
end
|
|
|
|
ext = File.extname filename
|
|
upload = Upload.new Config.current.upload_path
|
|
url = upload.path_to_url upload.save "img", ext, part.body
|
|
|
|
if url.nil?
|
|
raise "Failed to generate a public URL for the uploaded file"
|
|
end
|
|
|
|
if entry_id.nil?
|
|
title.set_cover_url url
|
|
else
|
|
entry_name = title.get_entry(entry_id).not_nil!.title
|
|
title.set_cover_url entry_name, url
|
|
end
|
|
else
|
|
raise "Unkown upload target #{target}"
|
|
end
|
|
|
|
send_json env, {"success" => true}.to_json
|
|
env.response.close
|
|
end
|
|
|
|
raise "No part with name `file` found"
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Returns a list of available plugins"
|
|
Koa.tags ["admin", "downloader"]
|
|
Koa.query "plugin", schema: String
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"plugins" => [{
|
|
"id" => String,
|
|
"title" => String,
|
|
}],
|
|
}
|
|
get "/api/admin/plugin" do |env|
|
|
begin
|
|
send_json env, {
|
|
"success" => true,
|
|
"plugins" => Plugin.list,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Returns the metadata of a plugin"
|
|
Koa.tags ["admin", "downloader"]
|
|
Koa.query "plugin", schema: String
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"info" => {
|
|
"dir" => String,
|
|
"id" => String,
|
|
"title" => String,
|
|
"placeholder" => String,
|
|
"wait_seconds" => Int32,
|
|
"version" => Int32,
|
|
"settings" => {} of String => String,
|
|
},
|
|
}
|
|
get "/api/admin/plugin/info" do |env|
|
|
begin
|
|
plugin = Plugin.new env.params.query["plugin"].as String
|
|
send_json env, {
|
|
"success" => true,
|
|
"info" => plugin.info,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Searches for manga matching the given query from a plugin", <<-MD
|
|
Only available for plugins targeting API v2 or above.
|
|
MD
|
|
Koa.tags ["admin", "downloader"]
|
|
Koa.query "plugin", schema: String
|
|
Koa.query "query", schema: String
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"manga" => [{
|
|
"id" => String,
|
|
"title" => String,
|
|
}],
|
|
}
|
|
get "/api/admin/plugin/search" do |env|
|
|
begin
|
|
query = env.params.query["query"].as String
|
|
plugin = Plugin.new env.params.query["plugin"].as String
|
|
|
|
manga_ary = plugin.search_manga(query).as_a
|
|
send_json env, {
|
|
"success" => true,
|
|
"manga" => manga_ary,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Creates a new subscription"
|
|
Koa.tags ["admin", "downloader", "subscription"]
|
|
Koa.body schema: {
|
|
"plugin" => String,
|
|
"manga" => String,
|
|
"manga_id" => String,
|
|
"name" => String,
|
|
"filters" => ["filter"],
|
|
}
|
|
Koa.response 200, schema: "result"
|
|
post "/api/admin/plugin/subscriptions" do |env|
|
|
begin
|
|
plugin_id = env.params.json["plugin"].as String
|
|
manga_title = env.params.json["manga"].as String
|
|
manga_id = env.params.json["manga_id"].as String
|
|
filters = env.params.json["filters"].as(Array(JSON::Any)).map do |f|
|
|
Filter.from_json f.to_json
|
|
end
|
|
name = env.params.json["name"].as String
|
|
|
|
sub = Subscription.new plugin_id, manga_id, manga_title, name
|
|
sub.filters = filters
|
|
|
|
plugin = Plugin.new plugin_id
|
|
plugin.subscribe sub
|
|
|
|
send_json env, {
|
|
"success" => true,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Returns the list of subscriptions for a plugin"
|
|
Koa.tags ["admin", "downloader", "subscription"]
|
|
Koa.query "plugin", desc: "The ID of the plugin"
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"subscriptions" => ["subscription"],
|
|
}
|
|
get "/api/admin/plugin/subscriptions" do |env|
|
|
begin
|
|
pid = env.params.query["plugin"].as String
|
|
send_json env, {
|
|
"success" => true,
|
|
"subscriptions" => Plugin.new(pid).list_subscriptions,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Deletes a subscription"
|
|
Koa.tags ["admin", "downloader", "subscription"]
|
|
Koa.body schema: {
|
|
"plugin" => String,
|
|
"subscription" => String,
|
|
}
|
|
Koa.response 200, schema: "result"
|
|
delete "/api/admin/plugin/subscriptions" do |env|
|
|
begin
|
|
pid = env.params.query["plugin"].as String
|
|
sid = env.params.query["subscription"].as String
|
|
|
|
Plugin.new(pid).unsubscribe sid
|
|
|
|
send_json env, {
|
|
"success" => true,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Checks for updates for a subscription"
|
|
Koa.tags ["admin", "downloader", "subscription"]
|
|
Koa.body schema: {
|
|
"plugin" => String,
|
|
"subscription" => String,
|
|
}
|
|
Koa.response 200, schema: "result"
|
|
post "/api/admin/plugin/subscriptions/update" do |env|
|
|
pid = env.params.query["plugin"].as String
|
|
sid = env.params.query["subscription"].as String
|
|
|
|
Plugin.new(pid).check_subscription sid
|
|
|
|
send_json env, {
|
|
"success" => true,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
|
|
Koa.describe "Lists the chapters in a title from a plugin"
|
|
Koa.tags ["admin", "downloader"]
|
|
Koa.query "plugin", schema: String
|
|
Koa.query "query", schema: String
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"chapters?" => [{
|
|
"id" => String,
|
|
"title?" => String,
|
|
}],
|
|
"title" => String?,
|
|
}
|
|
get "/api/admin/plugin/list" do |env|
|
|
begin
|
|
query = env.params.query["query"].as String
|
|
plugin = Plugin.new env.params.query["plugin"].as String
|
|
|
|
json = plugin.list_chapters query
|
|
|
|
if plugin.info.version == 1
|
|
chapters = json["chapters"]
|
|
title = json["title"]
|
|
else
|
|
chapters = json
|
|
title = nil
|
|
end
|
|
|
|
send_json env, {
|
|
"success" => true,
|
|
"chapters" => chapters,
|
|
"title" => title,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Adds a list of chapters from a plugin to the download queue"
|
|
Koa.tags ["admin", "downloader"]
|
|
Koa.body schema: {
|
|
"plugin" => String,
|
|
"title" => String,
|
|
"chapters" => [{
|
|
"id" => String,
|
|
"title" => String,
|
|
}],
|
|
}
|
|
Koa.response 200, schema: {
|
|
"success" => Int32,
|
|
"fail" => Int32,
|
|
}
|
|
post "/api/admin/plugin/download" do |env|
|
|
begin
|
|
plugin = Plugin.new env.params.json["plugin"].as String
|
|
chapters = env.params.json["chapters"].as Array(JSON::Any)
|
|
manga_title = env.params.json["title"].as String
|
|
|
|
jobs = chapters.map { |ch|
|
|
Queue::Job.new(
|
|
"#{plugin.info.id}-#{Base64.encode ch["id"].as_s}",
|
|
"", # manga_id
|
|
ch["title"].as_s,
|
|
manga_title,
|
|
Queue::JobStatus::Pending,
|
|
Time.utc
|
|
)
|
|
}
|
|
inserted_count = Queue.default.push jobs
|
|
send_json env, {
|
|
"success": inserted_count,
|
|
"fail": jobs.size - inserted_count,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Returns the image dimensions of all pages in an entry"
|
|
Koa.path "tid", desc: "A title ID"
|
|
Koa.path "eid", desc: "An entry ID"
|
|
Koa.tag "reader"
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"dimensions?" => [{
|
|
"width" => Int32,
|
|
"height" => Int32,
|
|
}],
|
|
}
|
|
Koa.response 304, "Not modified (only available when `If-None-Match` is set)"
|
|
get "/api/dimensions/:tid/:eid" do |env|
|
|
begin
|
|
tid = env.params.url["tid"]
|
|
eid = env.params.url["eid"]
|
|
prev_e_tag = env.request.headers["If-None-Match"]?
|
|
|
|
title = Library.default.get_title tid
|
|
raise "Title ID `#{tid}` not found" if title.nil?
|
|
entry = title.get_entry eid
|
|
raise "Entry ID `#{eid}` of `#{title.title}` not found" if entry.nil?
|
|
|
|
if entry.is_a? DirEntry
|
|
file_hash = Digest::SHA1.hexdigest(entry.path + entry.mtime.to_s + entry.size)
|
|
else
|
|
file_hash = Digest::SHA1.hexdigest(entry.path + entry.mtime.to_s)
|
|
end
|
|
e_tag = "W/#{file_hash}"
|
|
if e_tag == prev_e_tag
|
|
env.response.status_code = 304
|
|
send_text env, ""
|
|
else
|
|
sizes = entry.page_dimensions
|
|
if entry.is_a? DirEntry
|
|
cache_control = "no-cache, max-age=86400"
|
|
else
|
|
cache_control = "public, max-age=86400"
|
|
end
|
|
env.response.headers["ETag"] = e_tag
|
|
env.response.headers["Cache-Control"] = cache_control
|
|
send_json env, {
|
|
"success" => true,
|
|
"dimensions" => sizes,
|
|
}.to_json
|
|
end
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Downloads an entry"
|
|
Koa.path "tid", desc: "A title ID"
|
|
Koa.path "eid", desc: "An entry ID"
|
|
Koa.response 200, schema: Bytes
|
|
Koa.response 404, "Entry not found"
|
|
Koa.tags ["library", "reader"]
|
|
get "/api/download/:tid/:eid" do |env|
|
|
begin
|
|
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
|
entry = (title.get_entry env.params.url["eid"]).not_nil!
|
|
|
|
send_attachment env, entry.path
|
|
rescue e
|
|
Logger.error e
|
|
env.response.status_code = 404
|
|
send_text env, e.message
|
|
end
|
|
end
|
|
|
|
Koa.describe "Gets the tags of a title"
|
|
Koa.path "tid", desc: "A title ID"
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"tags" => [String?],
|
|
}
|
|
Koa.tags ["library", "tags"]
|
|
get "/api/tags/:tid" do |env|
|
|
begin
|
|
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
|
tags = title.tags
|
|
|
|
send_json env, {
|
|
"success" => true,
|
|
"tags" => tags,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Returns all tags"
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"tags" => [String?],
|
|
}
|
|
Koa.tags ["library", "tags"]
|
|
get "/api/tags" do |env|
|
|
begin
|
|
tags = Storage.default.list_tags
|
|
send_json env, {
|
|
"success" => true,
|
|
"tags" => tags,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Adds a new tag to a title"
|
|
Koa.path "tid", desc: "A title ID"
|
|
Koa.response 200, schema: "result"
|
|
Koa.tags ["admin", "library", "tags"]
|
|
put "/api/admin/tags/:tid/:tag" do |env|
|
|
begin
|
|
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
|
tag = env.params.url["tag"]
|
|
|
|
title.add_tag tag
|
|
send_json env, {
|
|
"success" => true,
|
|
"error" => nil,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Deletes a tag from a title"
|
|
Koa.path "tid", desc: "A title ID"
|
|
Koa.response 200, schema: "result"
|
|
Koa.tags ["admin", "library", "tags"]
|
|
delete "/api/admin/tags/:tid/:tag" do |env|
|
|
begin
|
|
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
|
tag = env.params.url["tag"]
|
|
|
|
title.delete_tag tag
|
|
send_json env, {
|
|
"success" => true,
|
|
"error" => nil,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Lists all missing titles"
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"titles?" => [{
|
|
"path" => String,
|
|
"id" => String,
|
|
"signature" => String,
|
|
}],
|
|
}
|
|
Koa.tags ["admin", "library"]
|
|
get "/api/admin/titles/missing" do |env|
|
|
begin
|
|
send_json env, {
|
|
"success" => true,
|
|
"error" => nil,
|
|
"titles" => Storage.default.missing_titles,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Lists all missing entries"
|
|
Koa.response 200, schema: {
|
|
"success" => Bool,
|
|
"error" => String?,
|
|
"entries?" => [{
|
|
"path" => String,
|
|
"id" => String,
|
|
"signature" => String,
|
|
}],
|
|
}
|
|
Koa.tags ["admin", "library"]
|
|
get "/api/admin/entries/missing" do |env|
|
|
begin
|
|
send_json env, {
|
|
"success" => true,
|
|
"error" => nil,
|
|
"entries" => Storage.default.missing_entries,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Deletes all missing titles"
|
|
Koa.response 200, schema: "result"
|
|
Koa.tags ["admin", "library"]
|
|
delete "/api/admin/titles/missing" do |env|
|
|
begin
|
|
Storage.default.delete_missing_title
|
|
send_json env, {
|
|
"success" => true,
|
|
"error" => nil,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Deletes all missing entries"
|
|
Koa.response 200, schema: "result"
|
|
Koa.tags ["admin", "library"]
|
|
delete "/api/admin/entries/missing" do |env|
|
|
begin
|
|
Storage.default.delete_missing_entry
|
|
send_json env, {
|
|
"success" => true,
|
|
"error" => nil,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Deletes a missing title identified by `tid`", <<-MD
|
|
Does nothing if the given `tid` is not found or if the title is not missing.
|
|
MD
|
|
Koa.response 200, schema: "result"
|
|
Koa.tags ["admin", "library"]
|
|
delete "/api/admin/titles/missing/:tid" do |env|
|
|
begin
|
|
tid = env.params.url["tid"]
|
|
Storage.default.delete_missing_title tid
|
|
send_json env, {
|
|
"success" => true,
|
|
"error" => nil,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.to_json
|
|
end
|
|
end
|
|
|
|
Koa.describe "Deletes a missing entry identified by `eid`", <<-MD
|
|
Does nothing if the given `eid` is not found or if the entry is not missing.
|
|
MD
|
|
Koa.response 200, schema: "result"
|
|
Koa.tags ["admin", "library"]
|
|
delete "/api/admin/entries/missing/:eid" do |env|
|
|
begin
|
|
eid = env.params.url["eid"]
|
|
Storage.default.delete_missing_entry eid
|
|
send_json env, {
|
|
"success" => true,
|
|
"error" => nil,
|
|
}.to_json
|
|
rescue e
|
|
Logger.error e
|
|
send_json env, {
|
|
"success" => false,
|
|
"error" => e.message,
|
|
}.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
|