Compare commits

..

42 Commits

Author SHA1 Message Date
Alex Ling
cdfc9f3a93 Show manga title on subscription manager 2022-03-19 11:39:20 +00:00
Alex Ling
2cc1a06b4e Reset table sort options 2022-03-19 11:23:23 +00:00
Alex Ling
95eb208901 Confirmation before deleting subscriptions 2022-03-19 11:13:48 +00:00
Alex Ling
acefa00b12 Merge branch 'dev' into feature/plugin-v2 2022-03-18 11:57:49 +00:00
Alex Ling
8dfe580e39 Merge branch 'dev' into feature/plugin-v2 2022-03-15 13:58:48 +00:00
Alex Ling
c290eee90b Fix BigFloat conversion issue 2022-03-15 13:58:16 +00:00
Alex Ling
2ade6ebd8c Document the subscription endpoints 2022-02-20 06:09:16 +00:00
Alex Ling
b56a9cb50c Clean up 2022-02-20 04:48:05 +00:00
Alex Ling
fd650bdf45 Fix null pid 2022-02-20 03:58:36 +00:00
Alex Ling
c80855bb5d Merge branch 'dev' into feature/plugin-v2 2022-02-20 02:46:53 +00:00
Alex Ling
0adcd3a634 Update last checked even when no chapters found 2022-01-23 12:24:19 +00:00
Alex Ling
f6bd3fa15d Update last checked from manager page 2022-01-23 12:23:58 +00:00
Alex Ling
be3babda37 Show target API version 2022-01-23 11:06:07 +00:00
Alex Ling
cae832911d Fix timestamp precision issue in plugin 2022-01-23 09:47:00 +00:00
Alex Ling
2c7c29fef9 Trigger subscription update from manager page 2022-01-23 09:45:45 +00:00
Alex Ling
968f6246de Fix actions on download manager 2022-01-23 08:46:19 +00:00
Alex Ling
d862c386f1 Base64 encode chapter IDs 2022-01-23 06:08:20 +00:00
Alex Ling
031df3a2bc Finish plugin updater 2022-01-22 14:50:37 +00:00
Alex Ling
5fac8c6a60 Merge branch 'dev' into feature/plugin-v2 2022-01-21 13:20:23 +00:00
Alex Ling
748386f0af WIP 2021-12-31 14:49:37 +00:00
Alex Ling
031ea7ef16 Finish subscription manager page 2021-11-25 12:31:17 +00:00
Alex Ling
b76d4645cc Add subscription manager page (WIP) 2021-11-21 04:08:17 +00:00
Alex Ling
fe6f884d94 Store manga ID with subscriptions 2021-11-20 11:08:36 +00:00
Alex Ling
5442d124af Subscription management API endpoints 2021-11-20 09:47:18 +00:00
Alex Ling
352236ab65 Delete unnecessary raise for debugging 2021-11-20 08:50:06 +00:00
Alex Ling
e44213960f Refactor date filtering and use native date picker 2021-11-20 08:10:51 +00:00
Alex Ling
87e54aa89c Merge branch 'dev' into feature/plugin-v2
Fixes #244 again in this branch
2021-11-18 14:55:15 +00:00
Alex Ling
a45bea01c9 Fix linter 2021-09-04 02:32:46 +00:00
Alex Ling
198913db3e Remove MangaDex files that are no longer needed 2021-09-04 02:31:30 +00:00
Alex Ling
238860d52d Simplify subscription JSON parsing 2021-09-04 02:26:18 +00:00
Alex Ling
ae1c36263b Merge branch 'dev' into feature/plugin-v2 2021-09-04 02:25:19 +00:00
Alex Ling
b7aee1e903 WIP 2021-08-17 04:11:58 +00:00
Alex Ling
f56ce2313c WIP 2021-07-11 11:19:08 +00:00
Alex Ling
259f6cb285 Add endpoint for plugin subscription 2021-07-11 02:45:14 +00:00
Alex Ling
3b19883dde Use auto overflow tables
cherry-picked from a612500b0f
2021-06-07 07:35:44 +00:00
Alex Ling
6844860065 Revert "Subscription manager"
This reverts commit a612500b0f.
2021-06-07 07:32:32 +00:00
Alex Ling
9eb699ea3b Add plugin subscription types 2021-06-07 07:04:49 +00:00
Alex Ling
59bcb4db3b WIP 2021-06-05 10:53:45 +00:00
Alex Ling
87c479bf42 WIP 2021-05-30 10:54:27 +00:00
Alex Ling
e0713ccde8 WIP 2021-05-22 07:24:30 +00:00
Alex Ling
a571d21cba WIP 2021-05-15 13:37:11 +00:00
Alex Ling
23541f457e Add "title_title" to slim JSON 2021-05-15 06:54:12 +00:00
14 changed files with 45 additions and 418 deletions

View File

@@ -51,7 +51,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
### CLI
```
Mango - Manga Server and Web Reader. Version 0.26.2
Mango - Manga Server and Web Reader. Version 0.25.0
Usage:
@@ -88,16 +88,15 @@ upload_path: ~/mango/uploads
plugin_path: ~/mango/plugins
download_timeout_seconds: 30
library_cache_path: ~/mango/library.yml.gz
cache_enabled: true
cache_enabled: false
cache_size_mbs: 50
cache_log_enabled: true
disable_login: false
default_username: ""
auth_proxy_header_name: ""
plugin_update_interval_hours: 24
```
- `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
- `scan_interval_minutes`, `thumbnail_generation_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
- 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`.

View File

@@ -50,7 +50,7 @@ shards:
koa:
git: https://github.com/hkalexling/koa.git
version: 0.9.0
version: 0.8.0
mg:
git: https://github.com/hkalexling/mg.git
@@ -68,10 +68,6 @@ shards:
git: https://github.com/luislavena/radix.git
version: 0.4.1
sanitize:
git: https://github.com/hkalexling/sanitize.git
version: 0.1.0+git.commit.e09520e972d0d9b70b71bb003e6831f7c2c59dce
sqlite3:
git: https://github.com/crystal-lang/crystal-sqlite3.git
version: 0.18.0

View File

@@ -1,5 +1,5 @@
name: mango
version: 0.26.2
version: 0.25.0
authors:
- Alex Ling <hkalexling@gmail.com>
@@ -42,5 +42,3 @@ dependencies:
branch: master
mg:
github: hkalexling/mg
sanitize:
github: hkalexling/sanitize

View File

@@ -6,7 +6,6 @@ class AuthHandler < Kemal::Handler
# Some of the code is copied form kemalcr/kemal-basic-auth on GitHub
BASIC = "Basic"
BEARER = "Bearer"
AUTH = "Authorization"
AUTH_MESSAGE = "Could not verify your access level for that URL.\n" \
"You have to login with proper credentials"
@@ -19,14 +18,8 @@ class AuthHandler < Kemal::Handler
end
def require_auth(env)
if request_path_startswith env, ["/api"]
# 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
env.session.string "callback", env.request.path
redirect env, "/login"
end
def validate_token(env)
@@ -42,18 +35,13 @@ class AuthHandler < Kemal::Handler
def validate_auth_header(env)
if env.request.headers[AUTH]?
if value = env.request.headers[AUTH]
if value.starts_with? BASIC
if value.size > 0 && value.starts_with?(BASIC)
token = verify_user value
return false if token.nil?
env.session.string "token", token
return true
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
false
@@ -66,10 +54,6 @@ class AuthHandler < Kemal::Handler
end
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,
# or a static file
if request_path_startswith(env, ["/login", "/logout", "/api/login"]) ||
@@ -78,8 +62,8 @@ class AuthHandler < Kemal::Handler
end
# Check user is logged in
if validate_token(env) || validate_auth_header(env)
# Skip if the request has a valid token (either from cookies or header)
if validate_token env
# Skip if the request has a valid token
elsif Config.current.disable_login
# Check default username if login is disabled
unless Storage.default.username_exists Config.current.default_username

View File

@@ -1,8 +0,0 @@
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

View File

@@ -55,12 +55,9 @@ class Entry
def build_json(*, slim = false)
JSON.build do |json|
json.object do
{% for str in %w(zip_path title size id) %}
{% for str in ["zip_path", "title", "size", "id"] %}
json.field {{str}}, @{{str.id}}
{% end %}
if err_msg
json.field "err_msg", err_msg
end
json.field "title_id", @book.id
json.field "title_title", @book.title
json.field "sort_title", sort_title

View File

@@ -139,31 +139,14 @@ class Library
titles.flat_map &.deep_entries
end
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
def build_json(*, slim = false, depth = -1)
JSON.build do |json|
json.object do
json.field "dir", @dir
json.field "titles" do
json.array do
_titles.each do |title|
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
self.titles.each do |title|
json.raw title.build_json(slim: slim, depth: depth)
end
end
end

View File

@@ -202,21 +202,7 @@ class Title
alias SortContext = NamedTuple(username: String, opt: SortOptions)
def build_json(*, slim = false, depth = -1,
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
sort_context : SortContext? = nil)
JSON.build do |json|
json.object do
{% for str in ["dir", "title", "id"] %}
@@ -232,39 +218,25 @@ class Title
unless depth == 0
json.field "titles" do
json.array do
_titles.each do |title|
self.titles.each do |title|
json.raw title.build_json(slim: slim,
depth: depth > 0 ? depth - 1 : depth,
sort_context: sort_context, percentage: percentage)
depth: depth > 0 ? depth - 1 : depth)
end
end
end
json.field "entries" do
json.array do
_entries = if sort_context
sorted_entries sort_context[:username],
sort_context[:opt]
else
@entries
end
_entries.each do |entry|
json.raw entry.build_json(slim: slim)
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
json.field "parents" do
json.array do

View File

@@ -55,13 +55,6 @@ class SortOptions
def to_tuple
{@method.to_s.underscore, ascend}
end
def to_json
{
"method" => method.to_s.underscore,
"ascend" => ascend,
}.to_json
end
end
struct Image

View File

@@ -7,7 +7,7 @@ require "option_parser"
require "clim"
require "tallboy"
MANGO_VERSION = "0.26.2"
MANGO_VERSION = "0.25.0"
# From http://www.network-science.de/ascii/
BANNER = %{

View File

@@ -1,5 +1,3 @@
require "sanitize"
struct AdminRouter
def initialize
get "/admin" do |env|
@@ -16,13 +14,13 @@ struct AdminRouter
end
get "/admin/user/edit" do |env|
sanitizer = Sanitize::Policy::Text.new
username = env.params.query["username"]?.try { |s| sanitizer.process s }
username = env.params.query["username"]?
admin = env.params.query["admin"]?
if admin
admin = admin == "true"
end
error = env.params.query["error"]?.try { |s| sanitizer.process s }
error = env.params.query["error"]?
current_user = get_username env
new_user = username.nil? && admin.nil?
layout "user-edit"
end

View File

@@ -47,12 +47,7 @@ struct APIRouter
"mtime" => Int64,
"entries" => ["entry"],
"titles" => ["title"],
"parents" => [{
"title" => String,
"id" => String,
}],
"title_percentages" => [Float64?],
"entry_percentages" => [Float64?],
"parents" => [String],
}.merge(s %w(dir title id display_name cover_url)),
desc: "A manga title (a collection of entries and sub-titles)"
@@ -85,12 +80,6 @@ struct APIRouter
"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
@@ -99,18 +88,11 @@ struct APIRouter
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
"Authenticated"
rescue e
Logger.error e
env.response.status_code = 403
send_json env, {
"success" => false,
"error" => e.message,
}.to_json
e.message
end
end
@@ -149,7 +131,7 @@ struct APIRouter
rescue e
Logger.error e
env.response.status_code = 500
send_text env, e.message
e.message
end
end
@@ -186,13 +168,11 @@ struct APIRouter
rescue e
Logger.error e
env.response.status_code = 500
send_text env, e.message
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
@@ -203,7 +183,8 @@ struct APIRouter
Koa.path "tid", desc: "Title ID"
Koa.query "slim"
Koa.query "depth"
Koa.query "percentage"
Koa.query "sort", desc: "Sorting option for entries. Can be one of 'auto', 'title', 'progress', 'time_added' and 'time_modified'"
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 404, "Title not found"
Koa.tag "library"
@@ -211,104 +192,29 @@ struct APIRouter
begin
username = get_username env
sort_opt = SortOptions.new
get_sort_opt
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)
opt: sort_opt})
rescue e
Logger.error e
env.response.status_code = 404
send_text env, e.message
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
@@ -316,162 +222,16 @@ struct APIRouter
MD
Koa.query "slim"
Koa.query "depth"
Koa.query "percentage"
Koa.response 200, schema: {
"dir" => String,
"titles" => ["title"],
"title_percentage" => [Float64?],
"dir" => String,
"titles" => ["title"],
}
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
send_json env, Library.default.build_json(slim: slim, depth: depth)
end
Koa.describe "Triggers a library scan"
@@ -507,7 +267,6 @@ struct APIRouter
spawn do
Library.default.generate_thumbnails
end
send_text env, ""
end
Koa.describe "Deletes a user with `username`"
@@ -1142,7 +901,7 @@ struct APIRouter
e_tag = "W/#{file_hash}"
if e_tag == prev_e_tag
env.response.status_code = 304
send_text env, ""
""
else
sizes = entry.page_dimensions
env.response.headers["ETag"] = e_tag
@@ -1176,7 +935,6 @@ struct APIRouter
rescue e
Logger.error e
env.response.status_code = 404
send_text env, e.message
end
end

View File

@@ -25,17 +25,6 @@ class Server
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
add_handler LogHandler.new
add_handler AuthHandler.new

View File

@@ -39,28 +39,13 @@ macro send_error_page(msg)
end
macro send_img(env, img)
cors
send_file {{env}}, {{img}}.data, {{img}}.mime
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)
begin
# Check if we can get the session id from the cookie
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!
token = env.session.string "token"
(Storage.default.verify_token token).not_nil!
rescue e
if Config.current.disable_login
Config.current.default_username
@@ -72,29 +57,12 @@ macro get_username(env)
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)
cors
env.response.content_type = "application/json"
env.response.print json
end
def send_text(env, text)
cors
env.response.content_type = "text/plain"
env.response.print text
end
def send_attachment(env, path)
cors
send_file env, path, filename: File.basename(path), disposition: "attachment"
end