mirror of
https://github.com/hkalexling/Mango.git
synced 2026-03-20 00:00:48 -04:00
Compare commits
25 Commits
feature/pl
...
rc/0.26.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
171b44643c | ||
|
|
a353029fcd | ||
|
|
75e26d8624 | ||
|
|
ebe2c8efed | ||
|
|
b8ce1cc7f1 | ||
|
|
24c90e7283 | ||
|
|
9ffc34e8e6 | ||
|
|
d1de8b7a4e | ||
|
|
7ae0577e4e | ||
|
|
e9b1bccbc9 | ||
|
|
293fb84e1d | ||
|
|
9c07944390 | ||
|
|
173d69eb26 | ||
|
|
21d8d0e8a7 | ||
|
|
61e85dd49f | ||
|
|
c778364ca2 | ||
|
|
7ecdb1c0dd | ||
|
|
a5a7396edd | ||
|
|
461398d219 | ||
|
|
0d52544617 | ||
|
|
c3736d222c | ||
|
|
2091053221 | ||
|
|
703e6d076b | ||
|
|
a101526672 | ||
|
|
eca47e3d32 |
@@ -51,7 +51,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
|
|||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
Mango - Manga Server and Web Reader. Version 0.25.0
|
Mango - Manga Server and Web Reader. Version 0.26.1
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
@@ -88,15 +88,16 @@ upload_path: ~/mango/uploads
|
|||||||
plugin_path: ~/mango/plugins
|
plugin_path: ~/mango/plugins
|
||||||
download_timeout_seconds: 30
|
download_timeout_seconds: 30
|
||||||
library_cache_path: ~/mango/library.yml.gz
|
library_cache_path: ~/mango/library.yml.gz
|
||||||
cache_enabled: false
|
cache_enabled: true
|
||||||
cache_size_mbs: 50
|
cache_size_mbs: 50
|
||||||
cache_log_enabled: true
|
cache_log_enabled: true
|
||||||
disable_login: false
|
disable_login: false
|
||||||
default_username: ""
|
default_username: ""
|
||||||
auth_proxy_header_name: ""
|
auth_proxy_header_name: ""
|
||||||
|
plugin_update_interval_hours: 24
|
||||||
```
|
```
|
||||||
|
|
||||||
- `scan_interval_minutes`, `thumbnail_generation_interval_hours` can be any non-negative integer. Setting them to `0` disables the periodic tasks
|
- `scan_interval_minutes`, `thumbnail_generation_interval_hours`, and `plugin_update_interval_hours` can be any non-negative integer. Setting them to `0` disables the periodic tasks
|
||||||
- `log_level` can be `debug`, `info`, `warn`, `error`, `fatal` or `off`. Setting it to `off` disables the logging
|
- `log_level` can be `debug`, `info`, `warn`, `error`, `fatal` or `off`. Setting it to `off` disables the logging
|
||||||
- You can disable authentication by setting `disable_login` to true. Note that `default_username` must be set to an existing username for this to work.
|
- You can disable authentication by setting `disable_login` to true. Note that `default_username` must be set to an existing username for this to work.
|
||||||
- By setting `cache_enabled` to `true`, you can enable an experimental feature where Mango caches library metadata to improve page load time. You can further fine-tune the feature with `cache_size_mbs` and `cache_log_enabled`.
|
- By setting `cache_enabled` to `true`, you can enable an experimental feature where Mango caches library metadata to improve page load time. You can further fine-tune the feature with `cache_size_mbs` and `cache_log_enabled`.
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ shards:
|
|||||||
|
|
||||||
koa:
|
koa:
|
||||||
git: https://github.com/hkalexling/koa.git
|
git: https://github.com/hkalexling/koa.git
|
||||||
version: 0.8.0
|
version: 0.9.0
|
||||||
|
|
||||||
mg:
|
mg:
|
||||||
git: https://github.com/hkalexling/mg.git
|
git: https://github.com/hkalexling/mg.git
|
||||||
@@ -68,6 +68,10 @@ shards:
|
|||||||
git: https://github.com/luislavena/radix.git
|
git: https://github.com/luislavena/radix.git
|
||||||
version: 0.4.1
|
version: 0.4.1
|
||||||
|
|
||||||
|
sanitize:
|
||||||
|
git: https://github.com/hkalexling/sanitize.git
|
||||||
|
version: 0.1.0+git.commit.e09520e972d0d9b70b71bb003e6831f7c2c59dce
|
||||||
|
|
||||||
sqlite3:
|
sqlite3:
|
||||||
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
git: https://github.com/crystal-lang/crystal-sqlite3.git
|
||||||
version: 0.18.0
|
version: 0.18.0
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: mango
|
name: mango
|
||||||
version: 0.25.0
|
version: 0.26.1
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Alex Ling <hkalexling@gmail.com>
|
- Alex Ling <hkalexling@gmail.com>
|
||||||
@@ -42,3 +42,5 @@ dependencies:
|
|||||||
branch: master
|
branch: master
|
||||||
mg:
|
mg:
|
||||||
github: hkalexling/mg
|
github: hkalexling/mg
|
||||||
|
sanitize:
|
||||||
|
github: hkalexling/sanitize
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ class AuthHandler < Kemal::Handler
|
|||||||
# Some of the code is copied form kemalcr/kemal-basic-auth on GitHub
|
# Some of the code is copied form kemalcr/kemal-basic-auth on GitHub
|
||||||
|
|
||||||
BASIC = "Basic"
|
BASIC = "Basic"
|
||||||
|
BEARER = "Bearer"
|
||||||
AUTH = "Authorization"
|
AUTH = "Authorization"
|
||||||
AUTH_MESSAGE = "Could not verify your access level for that URL.\n" \
|
AUTH_MESSAGE = "Could not verify your access level for that URL.\n" \
|
||||||
"You have to login with proper credentials"
|
"You have to login with proper credentials"
|
||||||
@@ -18,8 +19,14 @@ class AuthHandler < Kemal::Handler
|
|||||||
end
|
end
|
||||||
|
|
||||||
def require_auth(env)
|
def require_auth(env)
|
||||||
env.session.string "callback", env.request.path
|
if request_path_startswith env, ["/api"]
|
||||||
redirect env, "/login"
|
# Do not redirect API requests
|
||||||
|
env.response.status_code = 401
|
||||||
|
send_text env, "Unauthorized"
|
||||||
|
else
|
||||||
|
env.session.string "callback", env.request.path
|
||||||
|
redirect env, "/login"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_token(env)
|
def validate_token(env)
|
||||||
@@ -35,13 +42,18 @@ class AuthHandler < Kemal::Handler
|
|||||||
def validate_auth_header(env)
|
def validate_auth_header(env)
|
||||||
if env.request.headers[AUTH]?
|
if env.request.headers[AUTH]?
|
||||||
if value = env.request.headers[AUTH]
|
if value = env.request.headers[AUTH]
|
||||||
if value.size > 0 && value.starts_with?(BASIC)
|
if value.starts_with? BASIC
|
||||||
token = verify_user value
|
token = verify_user value
|
||||||
return false if token.nil?
|
return false if token.nil?
|
||||||
|
|
||||||
env.session.string "token", token
|
env.session.string "token", token
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
if value.starts_with? BEARER
|
||||||
|
session_id = value.split(" ")[1]
|
||||||
|
token = Kemal::Session.get(session_id).try &.string? "token"
|
||||||
|
return !token.nil? && Storage.default.verify_token token
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
false
|
false
|
||||||
@@ -54,6 +66,10 @@ class AuthHandler < Kemal::Handler
|
|||||||
end
|
end
|
||||||
|
|
||||||
def call(env)
|
def call(env)
|
||||||
|
# OPTIONS requests do not require authentication
|
||||||
|
if env.request.method === "OPTIONS"
|
||||||
|
return call_next(env)
|
||||||
|
end
|
||||||
# Skip all authentication if requesting /login, /logout, /api/login,
|
# Skip all authentication if requesting /login, /logout, /api/login,
|
||||||
# or a static file
|
# or a static file
|
||||||
if request_path_startswith(env, ["/login", "/logout", "/api/login"]) ||
|
if request_path_startswith(env, ["/login", "/logout", "/api/login"]) ||
|
||||||
@@ -62,8 +78,8 @@ class AuthHandler < Kemal::Handler
|
|||||||
end
|
end
|
||||||
|
|
||||||
# Check user is logged in
|
# Check user is logged in
|
||||||
if validate_token env
|
if validate_token(env) || validate_auth_header(env)
|
||||||
# Skip if the request has a valid token
|
# Skip if the request has a valid token (either from cookies or header)
|
||||||
elsif Config.current.disable_login
|
elsif Config.current.disable_login
|
||||||
# Check default username if login is disabled
|
# Check default username if login is disabled
|
||||||
unless Storage.default.username_exists Config.current.default_username
|
unless Storage.default.username_exists Config.current.default_username
|
||||||
|
|||||||
8
src/handlers/cors_handler.cr
Normal file
8
src/handlers/cors_handler.cr
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
class CORSHandler < Kemal::Handler
|
||||||
|
def call(env)
|
||||||
|
if request_path_startswith env, ["/api"]
|
||||||
|
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
end
|
||||||
|
call_next env
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -55,9 +55,12 @@ class Entry
|
|||||||
def build_json(*, slim = false)
|
def build_json(*, slim = false)
|
||||||
JSON.build do |json|
|
JSON.build do |json|
|
||||||
json.object do
|
json.object do
|
||||||
{% for str in ["zip_path", "title", "size", "id"] %}
|
{% for str in %w(zip_path title size id) %}
|
||||||
json.field {{str}}, @{{str.id}}
|
json.field {{str}}, @{{str.id}}
|
||||||
{% end %}
|
{% end %}
|
||||||
|
if err_msg
|
||||||
|
json.field "err_msg", err_msg
|
||||||
|
end
|
||||||
json.field "title_id", @book.id
|
json.field "title_id", @book.id
|
||||||
json.field "title_title", @book.title
|
json.field "title_title", @book.title
|
||||||
json.field "sort_title", sort_title
|
json.field "sort_title", sort_title
|
||||||
|
|||||||
@@ -139,14 +139,31 @@ class Library
|
|||||||
titles.flat_map &.deep_entries
|
titles.flat_map &.deep_entries
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_json(*, slim = false, depth = -1)
|
def build_json(*, slim = false, depth = -1, sort_context = nil,
|
||||||
|
percentage = false)
|
||||||
|
_titles = if sort_context
|
||||||
|
sorted_titles sort_context[:username],
|
||||||
|
sort_context[:opt]
|
||||||
|
else
|
||||||
|
self.titles
|
||||||
|
end
|
||||||
JSON.build do |json|
|
JSON.build do |json|
|
||||||
json.object do
|
json.object do
|
||||||
json.field "dir", @dir
|
json.field "dir", @dir
|
||||||
json.field "titles" do
|
json.field "titles" do
|
||||||
json.array do
|
json.array do
|
||||||
self.titles.each do |title|
|
_titles.each do |title|
|
||||||
json.raw title.build_json(slim: slim, depth: depth)
|
json.raw title.build_json(slim: slim, depth: depth,
|
||||||
|
sort_context: sort_context, percentage: percentage)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if percentage && sort_context
|
||||||
|
json.field "title_percentages" do
|
||||||
|
json.array do
|
||||||
|
_titles.each do |title|
|
||||||
|
json.number title.load_percentage sort_context[:username]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -202,7 +202,21 @@ class Title
|
|||||||
alias SortContext = NamedTuple(username: String, opt: SortOptions)
|
alias SortContext = NamedTuple(username: String, opt: SortOptions)
|
||||||
|
|
||||||
def build_json(*, slim = false, depth = -1,
|
def build_json(*, slim = false, depth = -1,
|
||||||
sort_context : SortContext? = nil)
|
sort_context : SortContext? = nil,
|
||||||
|
percentage = false)
|
||||||
|
_titles = if sort_context
|
||||||
|
sorted_titles sort_context[:username],
|
||||||
|
sort_context[:opt]
|
||||||
|
else
|
||||||
|
self.titles
|
||||||
|
end
|
||||||
|
_entries = if sort_context
|
||||||
|
sorted_entries sort_context[:username],
|
||||||
|
sort_context[:opt]
|
||||||
|
else
|
||||||
|
@entries
|
||||||
|
end
|
||||||
|
|
||||||
JSON.build do |json|
|
JSON.build do |json|
|
||||||
json.object do
|
json.object do
|
||||||
{% for str in ["dir", "title", "id"] %}
|
{% for str in ["dir", "title", "id"] %}
|
||||||
@@ -218,25 +232,39 @@ class Title
|
|||||||
unless depth == 0
|
unless depth == 0
|
||||||
json.field "titles" do
|
json.field "titles" do
|
||||||
json.array do
|
json.array do
|
||||||
self.titles.each do |title|
|
_titles.each do |title|
|
||||||
json.raw title.build_json(slim: slim,
|
json.raw title.build_json(slim: slim,
|
||||||
depth: depth > 0 ? depth - 1 : depth)
|
depth: depth > 0 ? depth - 1 : depth,
|
||||||
|
sort_context: sort_context, percentage: percentage)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
json.field "entries" do
|
json.field "entries" do
|
||||||
json.array do
|
json.array do
|
||||||
_entries = if sort_context
|
|
||||||
sorted_entries sort_context[:username],
|
|
||||||
sort_context[:opt]
|
|
||||||
else
|
|
||||||
@entries
|
|
||||||
end
|
|
||||||
_entries.each do |entry|
|
_entries.each do |entry|
|
||||||
json.raw entry.build_json(slim: slim)
|
json.raw entry.build_json(slim: slim)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
if percentage && sort_context
|
||||||
|
json.field "title_percentages" do
|
||||||
|
json.array do
|
||||||
|
_titles.each do |t|
|
||||||
|
json.number t.load_percentage sort_context[:username]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
json.field "entry_percentages" do
|
||||||
|
json.array do
|
||||||
|
load_percentage_for_all_entries(
|
||||||
|
sort_context[:username],
|
||||||
|
sort_context[:opt]
|
||||||
|
).each do |p|
|
||||||
|
json.number p.nan? ? 0 : p
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
json.field "parents" do
|
json.field "parents" do
|
||||||
json.array do
|
json.array do
|
||||||
|
|||||||
@@ -55,6 +55,13 @@ class SortOptions
|
|||||||
def to_tuple
|
def to_tuple
|
||||||
{@method.to_s.underscore, ascend}
|
{@method.to_s.underscore, ascend}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_json
|
||||||
|
{
|
||||||
|
"method" => method.to_s.underscore,
|
||||||
|
"ascend" => ascend,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
struct Image
|
struct Image
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ require "option_parser"
|
|||||||
require "clim"
|
require "clim"
|
||||||
require "tallboy"
|
require "tallboy"
|
||||||
|
|
||||||
MANGO_VERSION = "0.25.0"
|
MANGO_VERSION = "0.26.1"
|
||||||
|
|
||||||
# From http://www.network-science.de/ascii/
|
# From http://www.network-science.de/ascii/
|
||||||
BANNER = %{
|
BANNER = %{
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
require "sanitize"
|
||||||
|
|
||||||
struct AdminRouter
|
struct AdminRouter
|
||||||
def initialize
|
def initialize
|
||||||
get "/admin" do |env|
|
get "/admin" do |env|
|
||||||
@@ -14,13 +16,13 @@ struct AdminRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
get "/admin/user/edit" do |env|
|
get "/admin/user/edit" do |env|
|
||||||
username = env.params.query["username"]?
|
sanitizer = Sanitize::Policy::Text.new
|
||||||
|
username = env.params.query["username"]?.try { |s| sanitizer.process s }
|
||||||
admin = env.params.query["admin"]?
|
admin = env.params.query["admin"]?
|
||||||
if admin
|
if admin
|
||||||
admin = admin == "true"
|
admin = admin == "true"
|
||||||
end
|
end
|
||||||
error = env.params.query["error"]?
|
error = env.params.query["error"]?.try { |s| sanitizer.process s }
|
||||||
current_user = get_username env
|
|
||||||
new_user = username.nil? && admin.nil?
|
new_user = username.nil? && admin.nil?
|
||||||
layout "user-edit"
|
layout "user-edit"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -47,7 +47,12 @@ struct APIRouter
|
|||||||
"mtime" => Int64,
|
"mtime" => Int64,
|
||||||
"entries" => ["entry"],
|
"entries" => ["entry"],
|
||||||
"titles" => ["title"],
|
"titles" => ["title"],
|
||||||
"parents" => [String],
|
"parents" => [{
|
||||||
|
"title" => String,
|
||||||
|
"id" => String,
|
||||||
|
}],
|
||||||
|
"title_percentages" => [Float64?],
|
||||||
|
"entry_percentages" => [Float64?],
|
||||||
}.merge(s %w(dir title id display_name cover_url)),
|
}.merge(s %w(dir title id display_name cover_url)),
|
||||||
desc: "A manga title (a collection of entries and sub-titles)"
|
desc: "A manga title (a collection of entries and sub-titles)"
|
||||||
|
|
||||||
@@ -80,6 +85,12 @@ struct APIRouter
|
|||||||
"username" => String,
|
"username" => String,
|
||||||
"password" => String,
|
"password" => String,
|
||||||
}
|
}
|
||||||
|
Koa.response 200, schema: {
|
||||||
|
"success" => Bool,
|
||||||
|
"error" => String?,
|
||||||
|
"session_id" => String?,
|
||||||
|
"is_admin" => Bool?,
|
||||||
|
}
|
||||||
Koa.tag "users"
|
Koa.tag "users"
|
||||||
post "/api/login" do |env|
|
post "/api/login" do |env|
|
||||||
begin
|
begin
|
||||||
@@ -88,11 +99,18 @@ struct APIRouter
|
|||||||
token = Storage.default.verify_user(username, password).not_nil!
|
token = Storage.default.verify_user(username, password).not_nil!
|
||||||
|
|
||||||
env.session.string "token", token
|
env.session.string "token", token
|
||||||
"Authenticated"
|
send_json env, {
|
||||||
|
"success" => true,
|
||||||
|
"session_id" => env.session.id,
|
||||||
|
"is_admin" => Storage.default.username_is_admin username,
|
||||||
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
env.response.status_code = 403
|
env.response.status_code = 403
|
||||||
e.message
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -131,7 +149,7 @@ struct APIRouter
|
|||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
env.response.status_code = 500
|
env.response.status_code = 500
|
||||||
e.message
|
send_text env, e.message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -168,11 +186,13 @@ struct APIRouter
|
|||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
env.response.status_code = 500
|
env.response.status_code = 500
|
||||||
e.message
|
send_text env, e.message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Returns the book with title `tid`", <<-MD
|
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 `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.
|
- 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 1, returns the top-level titles and sub-titles/entries one level in them
|
||||||
@@ -183,8 +203,7 @@ struct APIRouter
|
|||||||
Koa.path "tid", desc: "Title ID"
|
Koa.path "tid", desc: "Title ID"
|
||||||
Koa.query "slim"
|
Koa.query "slim"
|
||||||
Koa.query "depth"
|
Koa.query "depth"
|
||||||
Koa.query "sort", desc: "Sorting option for entries. Can be one of 'auto', 'title', 'progress', 'time_added' and 'time_modified'"
|
Koa.query "percentage"
|
||||||
Koa.query "ascend", desc: "Sorting direction for entries. Set to 0 for the descending order. Doesn't work without specifying 'sort'"
|
|
||||||
Koa.response 200, schema: "title"
|
Koa.response 200, schema: "title"
|
||||||
Koa.response 404, "Title not found"
|
Koa.response 404, "Title not found"
|
||||||
Koa.tag "library"
|
Koa.tag "library"
|
||||||
@@ -192,29 +211,104 @@ struct APIRouter
|
|||||||
begin
|
begin
|
||||||
username = get_username env
|
username = get_username env
|
||||||
|
|
||||||
sort_opt = SortOptions.new
|
|
||||||
get_sort_opt
|
|
||||||
|
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
title = Library.default.get_title tid
|
title = Library.default.get_title tid
|
||||||
raise "Title ID `#{tid}` not found" if title.nil?
|
raise "Title ID `#{tid}` not found" if title.nil?
|
||||||
|
|
||||||
|
sort_opt = SortOptions.from_info_json title.dir, username
|
||||||
|
|
||||||
slim = !env.params.query["slim"]?.nil?
|
slim = !env.params.query["slim"]?.nil?
|
||||||
depth = env.params.query["depth"]?.try(&.to_i?) || -1
|
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,
|
send_json env, title.build_json(slim: slim, depth: depth,
|
||||||
sort_context: {username: username,
|
sort_context: {username: username,
|
||||||
opt: sort_opt})
|
opt: sort_opt}, percentage: percentage)
|
||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
env.response.status_code = 404
|
env.response.status_code = 404
|
||||||
e.message
|
send_text env, e.message
|
||||||
end
|
end
|
||||||
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
|
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 `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 `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 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 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 N, returns the requested title and sub-titles/entries N levels in it
|
||||||
@@ -222,16 +316,162 @@ struct APIRouter
|
|||||||
MD
|
MD
|
||||||
Koa.query "slim"
|
Koa.query "slim"
|
||||||
Koa.query "depth"
|
Koa.query "depth"
|
||||||
|
Koa.query "percentage"
|
||||||
Koa.response 200, schema: {
|
Koa.response 200, schema: {
|
||||||
"dir" => String,
|
"dir" => String,
|
||||||
"titles" => ["title"],
|
"titles" => ["title"],
|
||||||
|
"title_percentage" => [Float64?],
|
||||||
}
|
}
|
||||||
Koa.tag "library"
|
Koa.tag "library"
|
||||||
get "/api/library" do |env|
|
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?
|
slim = !env.params.query["slim"]?.nil?
|
||||||
depth = env.params.query["depth"]?.try(&.to_i?) || -1
|
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)
|
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
|
end
|
||||||
|
|
||||||
Koa.describe "Triggers a library scan"
|
Koa.describe "Triggers a library scan"
|
||||||
@@ -267,6 +507,7 @@ struct APIRouter
|
|||||||
spawn do
|
spawn do
|
||||||
Library.default.generate_thumbnails
|
Library.default.generate_thumbnails
|
||||||
end
|
end
|
||||||
|
send_text env, ""
|
||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Deletes a user with `username`"
|
Koa.describe "Deletes a user with `username`"
|
||||||
@@ -901,7 +1142,7 @@ struct APIRouter
|
|||||||
e_tag = "W/#{file_hash}"
|
e_tag = "W/#{file_hash}"
|
||||||
if e_tag == prev_e_tag
|
if e_tag == prev_e_tag
|
||||||
env.response.status_code = 304
|
env.response.status_code = 304
|
||||||
""
|
send_text env, ""
|
||||||
else
|
else
|
||||||
sizes = entry.page_dimensions
|
sizes = entry.page_dimensions
|
||||||
env.response.headers["ETag"] = e_tag
|
env.response.headers["ETag"] = e_tag
|
||||||
@@ -935,6 +1176,7 @@ struct APIRouter
|
|||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
env.response.status_code = 404
|
env.response.status_code = 404
|
||||||
|
send_text env, e.message
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,17 @@ class Server
|
|||||||
AdminRouter.new
|
AdminRouter.new
|
||||||
ReaderRouter.new
|
ReaderRouter.new
|
||||||
APIRouter.new
|
APIRouter.new
|
||||||
OPDSRouter.new
|
|
||||||
|
{% for path in %w(/api/* /uploads/* /img/*) %}
|
||||||
|
options {{path}} do |env|
|
||||||
|
cors
|
||||||
|
halt env
|
||||||
|
end
|
||||||
|
{% end %}
|
||||||
|
|
||||||
|
static_headers do |response|
|
||||||
|
response.headers.add("Access-Control-Allow-Origin", "*")
|
||||||
|
end
|
||||||
|
|
||||||
Kemal.config.logging = false
|
Kemal.config.logging = false
|
||||||
add_handler LogHandler.new
|
add_handler LogHandler.new
|
||||||
|
|||||||
@@ -39,13 +39,28 @@ macro send_error_page(msg)
|
|||||||
end
|
end
|
||||||
|
|
||||||
macro send_img(env, img)
|
macro send_img(env, img)
|
||||||
|
cors
|
||||||
send_file {{env}}, {{img}}.data, {{img}}.mime
|
send_file {{env}}, {{img}}.data, {{img}}.mime
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def get_token_from_auth_header(env) : String?
|
||||||
|
value = env.request.headers["Authorization"]
|
||||||
|
if value && value.starts_with? "Bearer"
|
||||||
|
session_id = value.split(" ")[1]
|
||||||
|
return Kemal::Session.get(session_id).try &.string? "token"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
macro get_username(env)
|
macro get_username(env)
|
||||||
begin
|
begin
|
||||||
token = env.session.string "token"
|
# Check if we can get the session id from the cookie
|
||||||
(Storage.default.verify_token token).not_nil!
|
token = env.session.string? "token"
|
||||||
|
if token.nil?
|
||||||
|
# If not, check if we can get the session id from the auth header
|
||||||
|
token = get_token_from_auth_header env
|
||||||
|
end
|
||||||
|
# If we still don't have a token, we handle it in `resuce` with `not_nil!`
|
||||||
|
(Storage.default.verify_token token.not_nil!).not_nil!
|
||||||
rescue e
|
rescue e
|
||||||
if Config.current.disable_login
|
if Config.current.disable_login
|
||||||
Config.current.default_username
|
Config.current.default_username
|
||||||
@@ -57,12 +72,29 @@ macro get_username(env)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
macro cors
|
||||||
|
env.response.headers["Access-Control-Allow-Methods"] = "HEAD,GET,PUT,POST," \
|
||||||
|
"DELETE,OPTIONS"
|
||||||
|
env.response.headers["Access-Control-Allow-Headers"] = "X-Requested-With," \
|
||||||
|
"X-HTTP-Method-Override, Content-Type, Cache-Control, Accept," \
|
||||||
|
"Authorization"
|
||||||
|
env.response.headers["Access-Control-Allow-Origin"] = "*"
|
||||||
|
end
|
||||||
|
|
||||||
def send_json(env, json)
|
def send_json(env, json)
|
||||||
|
cors
|
||||||
env.response.content_type = "application/json"
|
env.response.content_type = "application/json"
|
||||||
env.response.print json
|
env.response.print json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def send_text(env, text)
|
||||||
|
cors
|
||||||
|
env.response.content_type = "text/plain"
|
||||||
|
env.response.print text
|
||||||
|
end
|
||||||
|
|
||||||
def send_attachment(env, path)
|
def send_attachment(env, path)
|
||||||
|
cors
|
||||||
send_file env, path, filename: File.basename(path), disposition: "attachment"
|
send_file env, path, filename: File.basename(path), disposition: "attachment"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user