Compare commits

...

33 Commits

Author SHA1 Message Date
Alex Ling
3039031924 Merge branch 'master' of https://github.com/hkalexling/Mango 2020-05-24 06:47:19 +00:00
Alex Ling
8665616c2e Bump version to v0.4.0 2020-05-24 06:36:40 +00:00
Alex Ling
4453b0ee9f Link to development guideline in README [skip ci] 2020-05-24 14:34:19 +08:00
Alex Ling
487154e68c Add base url and rename rules to README [skip ci] 2020-05-24 14:33:05 +08:00
Alex Ling
60609263ab Explicitly set icon size (#40) [skip ci] 2020-05-23 14:47:35 +00:00
Alex Ling
4a245d2504 Check supplied base url has leading slash and append tailing slash if needed 2020-05-23 14:30:41 +00:00
Alex Ling
48c3a82078 Use base url when generating cover URLs 2020-05-23 14:16:56 +00:00
Alex Ling
4a59459773 Use base url in JS files 2020-05-23 14:18:14 +00:00
Alex Ling
eefa8c3982 Use base url in some hardcoded URLs 2020-05-23 14:17:11 +00:00
Alex Ling
8fe2f3b4cc Use base url in views 2020-05-23 14:16:56 +00:00
Alex Ling
60d4cee0a9 Respect base url setting when redirecting 2020-05-23 10:42:59 +00:00
Alex Ling
8658cb8306 Add base url to config 2020-05-23 10:42:39 +00:00
Alex Ling
d4e523c337 [skip ci] allow skip CI 2020-05-17 14:08:46 +00:00
Alex Ling
d49c0092c2 Generate artifact 2020-05-17 13:57:28 +00:00
Alex Ling
d75009f088 Rename scripts/ to dev/ 2020-05-17 13:44:10 +00:00
Alex Ling
d416dc6618 Use rename when downloading 2020-05-17 06:29:13 +00:00
Alex Ling
7233e6e5c3 Type annotate the self.default methods 2020-05-17 06:28:33 +00:00
Alex Ling
bd8ae9497f Initialize the downloader when started 2020-05-07 15:42:31 +00:00
Alex Ling
34b11dc2c7 Only hijack HTTP 500 when in release mode 2020-05-07 15:41:02 +00:00
Alex Ling
30dea57346 Use singleton in tests 2020-05-07 10:12:58 +00:00
Alex Ling
7448592216 Optionally pass in db path for testing 2020-05-07 10:12:58 +00:00
Alex Ling
049bd3ab2c Fix long lines 2020-05-07 10:12:58 +00:00
Alex Ling
c3608c101b Enforce 80 characters limit in make check 2020-05-07 10:12:58 +00:00
Alex Ling
1bec9f0108 Use singleton in various classes 2020-05-07 10:12:58 +00:00
Alex Ling
09b297cd8e Add rename method to Manga and Chapter 2020-05-07 10:12:06 +00:00
Alex Ling
b7cd55e692 Add rename rules to config 2020-05-07 10:11:45 +00:00
Alex Ling
986939ecb6 Add tests for the Rename module 2020-05-07 10:01:32 +00:00
Alex Ling
a5e97af3a3 Use abstract class in the Rename module 2020-05-03 16:31:00 +00:00
Alex Ling
4cee5faecd Allow | character outside of patterns 2020-05-03 16:30:35 +00:00
Alex Ling
711add74ef Allow spaces in patterns 2020-05-03 16:29:54 +00:00
Alex Ling
f6f09c54bc Add Rename module 2020-05-03 12:02:12 +00:00
Alex Ling
0f58ebb87b Ignore markdown files 2020-05-03 12:01:57 +00:00
Alex Ling
46347a8fe4 Update README.md 2020-04-23 14:49:06 +08:00
46 changed files with 575 additions and 230 deletions

View File

@@ -8,9 +8,9 @@ on:
jobs:
build:
if: "!contains(github.event.head_commit.message, 'skip ci')"
runs-on: ubuntu-latest
container:
image: crystallang/crystal:0.34.0-alpine
@@ -19,8 +19,13 @@ jobs:
- name: Install dependencies
run: apk add --no-cache yarn yaml sqlite-static
- name: Build
run: make
run: make static
- name: Linter
run: make check
- name: Run tests
run: make test
- name: Upload artifact
uses: actions/upload-artifact@v2
with:
name: mango
path: mango

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ yarn.lock
dist
mango
.env
*.md

View File

@@ -25,6 +25,7 @@ test:
check:
crystal tool format --check
./bin/ameba
./dev/linewidth.sh
install:
cp mango $(INSTALL_DIR)/mango

View File

@@ -35,7 +35,7 @@ Simply download the pre-built binary file `mango` for the latest [release](https
### Docker (via Dockerhub)
The official docker images are available on [Dockerhub](https://hub.docker.com/r/hkalexling/mango).
The official docker images are available on [Dockerhub](https://hub.docker.com/r/hkalexling/mango).
### Build from source
@@ -50,7 +50,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
### CLI
```
Mango e-manga server/reader. Version 0.3.0
Mango e-manga server/reader. Version 0.4.0
-v, --version Show version
-h, --help Show help
@@ -64,17 +64,19 @@ The default config file location is `~/.config/mango/config.yml`. It might be di
```yaml
---
port: 9000
library_path: /home/alex_ling/mango/library
upload_path: /home/alex_ling/mango/uploads
db_path: /home/alex_ling/mango/mango.db
base_url: /
db_path: ~/mango/mango.db
scan_interval_minutes: 5
log_level: info
upload_path: ~/mango/uploads
mangadex:
base_url: https://mangadex.org
api_url: https://mangadex.org/api
download_wait_seconds: 5
download_retries: 4
download_queue_db_path: /home/alex_ling/mango/queue.db
download_queue_db_path: ~/mango/queue.db
chapter_rename_rule: '[Vol.{volume} ][Ch.{chapter} ]{title|id}'
manga_rename_rule: '{title}'
```
- `scan_interval_minutes` can be any non-negative integer. Setting it to `0` disables the periodic scan
@@ -94,8 +96,8 @@ You can organize your `.cbz/.zip` files in nested folders in the library directo
└── Manga 2
   └── Vol. 1
   └── Ch.1 - Ch.3
   ├── 1.zip
   ├── 2.zip
   ├── 1.zip
   ├── 2.zip
   └── 3.zip
```
@@ -127,4 +129,6 @@ Mobile UI:
## Contributors
Please check the [development guideline](https://github.com/hkalexling/Mango/wiki/Development) if you are interest in code contributions.
[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/0)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/0)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/1)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/1)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/2)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/2)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/3)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/3)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/4)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/4)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/5)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/5)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/6)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/6)[![](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/images/7)](https://sourcerer.io/fame/hkalexling/hkalexling/Mango/links/7)

5
dev/linewidth.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
[ ! -z "$(grep '.\{80\}' --exclude-dir=lib --include="*.cr" -nr --color=always . | tee /dev/tty)" ] \
&& echo "The above lines exceed the 80 characters limit" \
|| exit 0

View File

@@ -36,7 +36,8 @@
word-wrap: break-word;
}
.uk-logo > img {
max-height: 90px;
height: 90px;
width: 90px;
}
.uk-search {
width: 100%;

View File

@@ -5,7 +5,7 @@ function scan() {
$('#scan-status > span').attr('hidden', '');
var color = $('#scan').css('color');
$('#scan').css('color', 'gray');
$.post('/api/admin/scan', function (data) {
$.post(base_url + 'api/admin/scan', function (data) {
var ms = data.milliseconds;
var titles = data.titles;
$('#scan-status > span').text('Scanned ' + titles + ' titles in ' + ms + 'ms');

View File

@@ -22,7 +22,7 @@ const loadConfig = () => {
globalConfig.autoRefresh = $('#auto-refresh').prop('checked');
};
const remove = (id) => {
var url = '/api/admin/mangadex/queue/delete';
var url = base_url + 'api/admin/mangadex/queue/delete';
if (id !== undefined)
url += '?' + $.param({id: id});
console.log(url);
@@ -43,7 +43,7 @@ const remove = (id) => {
});
};
const refresh = (id) => {
var url = '/api/admin/mangadex/queue/retry';
var url = base_url + 'api/admin/mangadex/queue/retry';
if (id !== undefined)
url += '?' + $.param({id: id});
console.log(url);
@@ -67,7 +67,7 @@ const toggle = () => {
$('#pause-resume-btn').attr('disabled', '');
const paused = $('#pause-resume-btn').text() === 'Resume download';
const action = paused ? 'resume' : 'pause';
const url = `/api/admin/mangadex/queue/${action}`;
const url = `${base_url}api/admin/mangadex/queue/${action}`;
$.ajax({
type: 'POST',
url: url,
@@ -87,7 +87,7 @@ const load = () => {
console.log('fetching');
$.ajax({
type: 'GET',
url: '/api/admin/mangadex/queue',
url: base_url + 'api/admin/mangadex/queue',
dataType: 'json'
})
.done(data => {

View File

@@ -33,7 +33,7 @@ const download = () => {
console.log(ids);
$.ajax({
type: 'POST',
url: '/api/admin/mangadex/download',
url: base_url + 'api/admin/mangadex/download',
data: JSON.stringify({chapters: chapters}),
contentType: "application/json",
dataType: 'json'
@@ -47,7 +47,7 @@ const download = () => {
const successCount = parseInt(data.success);
const failCount = parseInt(data.fail);
UIkit.modal.confirm(`${successCount} of ${successCount + failCount} chapters added to the download queue. Proceed to the download manager?`).then(() => {
window.location.href = '/admin/downloads';
window.location.href = base_url + 'admin/downloads';
});
styleModal();
})
@@ -109,7 +109,7 @@ const search = () => {
return;
}
$.getJSON("/api/admin/mangadex/manga/" + int_id)
$.getJSON(`${base_url}api/admin/mangadex/manga/${int_id}`)
.done((data) => {
if (data.error) {
alert('danger', 'Failed to get manga info. Error: ' + data.error);

View File

@@ -22,8 +22,8 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
$('#path-text').text(zipPath);
$('#pages-text').text(pages + ' pages');
$('#beginning-btn').attr('href', '/reader/' + titleID + '/' + entryID + '/1');
$('#continue-btn').attr('href', '/reader/' + titleID + '/' + entryID);
$('#beginning-btn').attr('href', `${base_url}reader/${titleID}/${entryID}/1`);
$('#continue-btn').attr('href', `${base_url}reader/${titleID}/${entryID}`);
$('#read-btn').click(function(){
updateProgress(titleID, entryID, pages);
@@ -39,7 +39,7 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
}
const updateProgress = (tid, eid, page) => {
let url = `/api/progress/${tid}/${page}`
let url = `${base_url}api/progress/${tid}/${page}`
const query = $.param({entry: eid});
if (eid)
url += `?${query}`;
@@ -66,7 +66,7 @@ const renameSubmit = (name, eid) => {
}
const query = $.param({ entry: eid });
let url = `/api/admin/display_name/${titleId}/${name}`;
let url = `${base_url}api/admin/display_name/${titleId}/${name}`;
if (eid)
url += `?${query}`;
@@ -130,7 +130,7 @@ const setupUpload = (eid) => {
if (eid)
queryObj['entry'] = eid;
const query = $.param(queryObj);
const url = `/api/admin/upload/cover?${query}`;
const url = `${base_url}api/admin/upload/cover?${query}`;
console.log(url);
UIkit.upload('.upload-field', {
url: url,

View File

@@ -1,5 +1,5 @@
$(() => {
var target = '/admin/user/edit';
var target = base_url + 'admin/user/edit';
if (username) target += username;
$('form').attr('action', target);
if (error) alert('danger', error);

View File

@@ -1,5 +1,5 @@
function remove(username) {
$.post('/api/admin/user/delete/' + username, function(data) {
$.post(base_url + 'api/admin/user/delete/' + username, function(data) {
if (data.success) {
location.reload();
}

View File

@@ -1,5 +1,5 @@
name: mango
version: 0.3.0
version: 0.4.0
authors:
- Alex Ling <hkalexling@gmail.com>

View File

@@ -2,7 +2,7 @@ require "./spec_helper"
describe Config do
it "creates config if it does not exist" do
with_default_config do |_, _, path|
with_default_config do |_, path|
File.exists?(path).should be_true
end
end

71
spec/rename_spec.cr Normal file
View File

@@ -0,0 +1,71 @@
require "./spec_helper"
require "../src/rename"
include Rename
describe Rule do
it "raises on nested brackets" do
expect_raises Exception do
Rule.new "[[]]"
end
expect_raises Exception do
Rule.new "{{}}"
end
end
it "raises on unclosed brackets" do
expect_raises Exception do
Rule.new "["
end
expect_raises Exception do
Rule.new "{"
end
expect_raises Exception do
Rule.new "[{]}"
end
end
it "raises when closing unopened brackets" do
expect_raises Exception do
Rule.new "]"
end
expect_raises Exception do
Rule.new "[}"
end
end
it "handles `|` in patterns" do
rule = Rule.new "{a|b|c}"
rule.render({"b" => "b"}).should eq "b"
rule.render({"a" => "a", "b" => "b"}).should eq "a"
end
it "allows `|` outside of patterns" do
rule = Rule.new "hello|world"
rule.render({} of String => String).should eq "hello|world"
end
it "raises on escaped characters" do
expect_raises Exception do
Rule.new "hello/world"
end
end
it "handles spaces in patterns" do
rule = Rule.new "{ a }"
rule.render({"a" => "a"}).should eq "a"
end
it "strips leading and tailing spaces" do
rule = Rule.new " hello "
rule.render({"a" => "a"}).should eq "hello"
end
it "renders a few examples correctly" do
rule = Rule.new "[Ch. {chapter }] {title | id} testing"
rule.render({"id" => "ID"}).should eq "ID testing"
rule.render({"chapter" => "CH", "id" => "ID"})
.should eq "Ch. CH ID testing"
rule.render({} of String => String).should eq "testing"
end
end

View File

@@ -1,6 +1,6 @@
require "spec"
require "../src/context"
require "../src/server"
require "../src/config"
class State
@@hash = {} of String => String
@@ -37,15 +37,15 @@ end
def with_default_config
temp_config = get_tempfile "mango-test-config"
config = Config.load temp_config.path
logger = Logger.new config.log_level
yield config, logger, temp_config.path
config.set_current
yield config, temp_config.path
temp_config.delete
end
def with_storage
with_default_config do |_, logger|
with_default_config do
temp_db = get_tempfile "mango-test-db"
storage = Storage.new temp_db.path, logger
storage = Storage.new temp_db.path
clear = yield storage, temp_db.path
if clear == true
temp_db.delete
@@ -54,9 +54,9 @@ def with_storage
end
def with_queue
with_default_config do |_, logger|
with_default_config do
temp_queue_db = get_tempfile "mango-test-queue-db"
queue = MangaDex::Queue.new temp_queue_db.path, logger
queue = MangaDex::Queue.new temp_queue_db.path
clear = yield queue, temp_queue_db.path
if clear == true
temp_queue_db.delete

View File

@@ -4,6 +4,7 @@ class Config
include YAML::Serializable
property port : Int32 = 9000
property base_url : String = "/"
property library_path : String = File.expand_path "~/mango/library",
home: true
property db_path : String = File.expand_path "~/mango/mango.db", home: true
@@ -22,13 +23,26 @@ class Config
"download_retries" => 4,
"download_queue_db_path" => File.expand_path("~/mango/queue.db",
home: true),
"chapter_rename_rule" => "[Vol.{volume} ][Ch.{chapter} ]{title|id}",
"manga_rename_rule" => "{title}",
}
@@singlet : Config?
def self.current
@@singlet.not_nil!
end
def set_current
@@singlet = self
end
def self.load(path : String?)
path = "~/.config/mango/config.yml" if path.nil?
cfg_path = File.expand_path path, home: true
if File.exists? cfg_path
config = self.from_yaml File.read cfg_path
config.preprocess
config.fill_defaults
return config
end
@@ -58,4 +72,13 @@ class Config
end
{% end %}
end
def preprocess
unless base_url.starts_with? "/"
raise "base url (#{base_url}) should start with `/`"
end
unless base_url.ends_with? "/"
@base_url += "/"
end
end
end

View File

@@ -1,21 +0,0 @@
require "./config"
require "./library"
require "./storage"
require "./logger"
class Context
property config : Config
property library : Library
property storage : Storage
property logger : Logger
property queue : MangaDex::Queue
def initialize(@config, @logger, @library, @storage, @queue)
end
{% for lvl in Logger::LEVELS %}
def {{lvl.id}}(msg)
@logger.{{lvl.id}} msg
end
{% end %}
end

View File

@@ -11,7 +11,7 @@ class AuthHandler < Kemal::Handler
cookie = env.request.cookies.find { |c| c.name == "token" }
if cookie.nil? || !@storage.verify_token cookie.value
return env.redirect "/login"
return redirect env, "/login"
end
if request_path_startswith env, ["/admin", "/api/admin", "/download"]

View File

@@ -2,20 +2,17 @@ require "kemal"
require "../logger"
class LogHandler < Kemal::BaseLogHandler
def initialize(@logger : Logger)
end
def call(env)
elapsed_time = Time.measure { call_next env }
elapsed_text = elapsed_text elapsed_time
msg = "#{env.response.status_code} #{env.request.method}" \
" #{env.request.resource} #{elapsed_text}"
@logger.debug msg
Logger.debug msg
env
end
def write(msg)
@logger.debug msg
Logger.debug msg
end
private def elapsed_text(elapsed)

View File

@@ -11,7 +11,9 @@ class UploadHandler < Kemal::Handler
return call_next env
end
ary = env.request.path.split(File::SEPARATOR).select { |part| !part.empty? }
ary = env.request.path.split(File::SEPARATOR).select do |part|
!part.empty?
end
ary[0] = @upload_dir
path = File.join ary

View File

@@ -57,7 +57,7 @@ class Entry
end
def cover_url
url = "/api/page/#{@title_id}/#{@id}/1"
url = "#{Config.current.base_url}api/page/#{@title_id}/#{@id}/1"
TitleInfo.new @book.dir do |info|
info_url = info.entry_cover_url[@title]?
unless info_url.nil? || info_url.empty?
@@ -97,7 +97,7 @@ class Title
encoded_title : String, mtime : Time
def initialize(@dir : String, @parent_id, storage,
@logger : Logger, @library : Library)
@library : Library)
@id = storage.get_id @dir, true
@title = File.basename dir
@encoded_title = URI.encode @title
@@ -109,7 +109,7 @@ class Title
next if fn.starts_with? "."
path = File.join dir, fn
if File.directory? path
title = Title.new path, @id, storage, @logger, library
title = Title.new path, @id, storage, library
next if title.entries.size == 0 && title.titles.size == 0
@library.title_hash[title.id] = title
@title_ids << title.id
@@ -118,9 +118,9 @@ class Title
if [".zip", ".cbz"].includes? File.extname path
zip_exception = validate_zip path
unless zip_exception.nil?
@logger.warn "File #{path} is corrupted or is not a valid zip " \
"archive. Ignoring it."
@logger.debug "Zip error: #{zip_exception}"
Logger.warn "File #{path} is corrupted or is not a valid zip " \
"archive. Ignoring it."
Logger.debug "Zip error: #{zip_exception}"
next
end
entry = Entry.new path, self, @id, storage
@@ -367,9 +367,19 @@ end
class Library
property dir : String, title_ids : Array(String), scan_interval : Int32,
logger : Logger, storage : Storage, title_hash : Hash(String, Title)
storage : Storage, title_hash : Hash(String, Title)
def initialize(@dir, @scan_interval, @logger, @storage)
def self.default : self
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
@storage = Storage.default
@dir = Config.current.library_path
@scan_interval = Config.current.scan_interval
# explicitly initialize @titles to bypass the compiler check. it will
# be filled with actual Titles in the `scan` call below
@title_ids = [] of String
@@ -381,7 +391,7 @@ class Library
start = Time.local
scan
ms = (Time.local - start).total_milliseconds
@logger.info "Scanned #{@title_ids.size} titles in #{ms}ms"
Logger.info "Scanned #{@title_ids.size} titles in #{ms}ms"
sleep @scan_interval * 60
end
end
@@ -410,8 +420,8 @@ class Library
def scan
unless Dir.exists? @dir
@logger.info "The library directory #{@dir} does not exist. " \
"Attempting to create it"
Logger.info "The library directory #{@dir} does not exist. " \
"Attempting to create it"
Dir.mkdir_p @dir
end
@title_ids.clear
@@ -419,13 +429,13 @@ class Library
.select { |fn| !fn.starts_with? "." }
.map { |fn| File.join @dir, fn }
.select { |path| File.directory? path }
.map { |path| Title.new path, "", @storage, @logger, self }
.map { |path| Title.new path, "", @storage, self }
.select { |title| !(title.entries.empty? && title.titles.empty?) }
.sort { |a, b| a.title <=> b.title }
.each do |title|
@title_hash[title.id] = title
@title_ids << title.id
end
@logger.debug "Scan completed"
Logger.debug "Scan completed"
end
end

View File

@@ -8,7 +8,15 @@ class Logger
@@severity : Log::Severity = :info
def initialize(level : String)
def self.default : self
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
level = Config.current.log_level
{% begin %}
case level.downcase
when "off"
@@ -50,9 +58,16 @@ class Logger
@backend.write Log::Entry.new "", Log::Severity::None, msg, nil
end
def self.log(msg)
default.log msg
end
{% for lvl in LEVELS %}
def {{lvl.id}}(msg)
@log.{{lvl.id}} { msg }
end
def self.{{lvl.id}}(msg)
default.not_nil!.{{lvl.id}} msg
end
{% end %}
end

View File

@@ -1,6 +1,7 @@
require "http/client"
require "json"
require "csv"
require "../rename"
macro string_properties(names)
{% for name in names %}
@@ -14,6 +15,14 @@ macro parse_strings_from_json(names)
{% end %}
end
macro properties_to_hash(names)
{
{% for name in names %}
"{{name.id}}" => @{{name.id}}.to_s,
{% end %}
}
end
module MangaDex
class Chapter
string_properties ["lang_code", "title", "volume", "chapter"]
@@ -64,16 +73,20 @@ module MangaDex
gname = obj["group_name#{s}"].as_s
@groups << {gid, gname}
end
@full_title = @title
unless @chapter.empty?
@full_title = "Ch.#{@chapter} " + @full_title
end
unless @volume.empty?
@full_title = "Vol.#{@volume} " + @full_title
end
rename_rule = Rename::Rule.new \
Config.current.mangadex["chapter_rename_rule"].to_s
@full_title = rename rename_rule
rescue e
raise "failed to parse json: #{e}"
end
def rename(rule : Rename::Rule)
hash = properties_to_hash ["id", "title", "volume", "chapter",
"lang_code", "language", "pages"]
hash["groups"] = @groups.map { |g| g[1] }.join ","
rule.render hash
end
end
class Manga
@@ -111,10 +124,23 @@ module MangaDex
rescue e
raise "failed to parse json: #{e}"
end
def rename(rule : Rename::Rule)
rule.render properties_to_hash ["id", "title", "author", "artist"]
end
end
class API
def initialize(@base_url = "https://mangadex.org/api/")
def self.default : self
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
@base_url = Config.current.mangadex["api_url"].to_s ||
"https://mangadex.org/api/"
@lang = {} of String => String
CSV.each_row {{read_file "src/assets/lang_codes.csv"}} do |row|
@lang[row[1]] = row[0]

View File

@@ -1,5 +1,6 @@
require "./api"
require "sqlite3"
require "zip"
module MangaDex
class PageJob
@@ -79,12 +80,21 @@ module MangaDex
class Queue
property downloader : Downloader?
@path : String
def initialize(@path : String, @logger : Logger)
dir = File.dirname path
def self.default : self
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize(db_path : String? = nil)
@path = db_path || Config.current.mangadex["download_queue_db_path"].to_s
dir = File.dirname @path
unless Dir.exists? dir
@logger.info "The queue DB directory #{dir} does not exist. " \
"Attepmting to create it"
Logger.info "The queue DB directory #{dir} does not exist. " \
"Attepmting to create it"
Dir.mkdir_p dir
end
DB.open "sqlite3://#{@path}" do |db|
@@ -101,7 +111,7 @@ module MangaDex
db.exec "create index if not exists status_idx " \
"on queue (status)"
rescue e
@logger.error "Error when checking tables in DB: #{e}"
Logger.error "Error when checking tables in DB: #{e}"
raise e
end
end
@@ -254,11 +264,22 @@ module MangaDex
class Downloader
property stopped = false
@wait_seconds : Int32 = Config.current.mangadex["download_wait_seconds"]
.to_i32
@retries : Int32 = Config.current.mangadex["download_retries"].to_i32
@library_path : String = Config.current.library_path
@downloading = false
def initialize(@queue : Queue, @api : API, @library_path : String,
@wait_seconds : Int32, @retries : Int32,
@logger : Logger)
def self.default : self
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
@queue = Queue.default
@api = API.default
@queue.downloader = self
spawn do
@@ -270,7 +291,7 @@ module MangaDex
next if job.nil?
download job
rescue e
@logger.error e
Logger.error e
end
end
end
@@ -282,7 +303,7 @@ module MangaDex
begin
chapter = @api.get_chapter(job.id)
rescue e
@logger.error e
Logger.error e
@queue.set_status JobStatus::Error, job
unless e.message.nil?
@queue.add_message e.message.not_nil!, job
@@ -292,7 +313,9 @@ module MangaDex
end
@queue.set_pages chapter.pages.size, job
lib_dir = @library_path
manga_dir = File.join lib_dir, chapter.manga.title
rename_rule = Rename::Rule.new \
Config.current.mangadex["manga_rename_rule"].to_s
manga_dir = File.join lib_dir, chapter.manga.rename rename_rule
unless File.exists? manga_dir
Dir.mkdir_p manga_dir
end
@@ -310,16 +333,16 @@ module MangaDex
ext = File.extname fn
fn = "#{i.to_s.rjust len, '0'}#{ext}"
page_job = PageJob.new url, fn, writer, @retries
@logger.debug "Downloading #{url}"
Logger.debug "Downloading #{url}"
loop do
sleep @wait_seconds.seconds
download_page page_job
break if page_job.success ||
page_job.tries_remaning <= 0
page_job.tries_remaning -= 1
@logger.warn "Failed to download page #{url}. " \
"Retrying... Remaining retries: " \
"#{page_job.tries_remaning}"
Logger.warn "Failed to download page #{url}. " \
"Retrying... Remaining retries: " \
"#{page_job.tries_remaning}"
end
channel.send page_job
@@ -330,8 +353,8 @@ module MangaDex
page_jobs = [] of PageJob
chapter.pages.size.times do
page_job = channel.receive
@logger.debug "[#{page_job.success ? "success" : "failed"}] " \
"#{page_job.url}"
Logger.debug "[#{page_job.success ? "success" : "failed"}] " \
"#{page_job.url}"
page_jobs << page_job
if page_job.success
@queue.add_success job
@@ -339,14 +362,14 @@ module MangaDex
@queue.add_fail job
msg = "Failed to download page #{page_job.url}"
@queue.add_message msg, job
@logger.error msg
Logger.error msg
end
end
fail_count = page_jobs.count { |j| !j.success }
@logger.debug "Download completed. " \
"#{fail_count}/#{page_jobs.size} failed"
Logger.debug "Download completed. " \
"#{fail_count}/#{page_jobs.size} failed"
writer.close
@logger.debug "cbz File created at #{zip_path}"
Logger.debug "cbz File created at #{zip_path}"
zip_exception = validate_zip zip_path
if !zip_exception.nil?
@@ -363,7 +386,7 @@ module MangaDex
end
private def download_page(job : PageJob)
@logger.debug "downloading #{job.url}"
Logger.debug "downloading #{job.url}"
headers = HTTP::Headers{
"User-agent" => "Mangadex.cr",
}
@@ -377,7 +400,7 @@ module MangaDex
end
job.success = true
rescue e
@logger.error e
Logger.error e
job.success = false
end
end

View File

@@ -1,9 +1,9 @@
require "./config"
require "./server"
require "./context"
require "./mangadex/*"
require "option_parser"
VERSION = "0.3.0"
VERSION = "0.4.0"
config_path = nil
@@ -19,23 +19,14 @@ OptionParser.parse do |parser|
exit
end
parser.on "-c PATH", "--config=PATH",
"Path to the config file. Default is `~/.config/mango/config.yml`" do |path|
"Path to the config file. " \
"Default is `~/.config/mango/config.yml`" do |path|
config_path = path
end
end
config = Config.load config_path
logger = Logger.new config.log_level
storage = Storage.new config.db_path, logger
library = Library.new config.library_path, config.scan_interval, logger, storage
queue = MangaDex::Queue.new config.mangadex["download_queue_db_path"].to_s,
logger
api = MangaDex::API.new config.mangadex["api_url"].to_s
MangaDex::Downloader.new queue, api, config.library_path,
config.mangadex["download_wait_seconds"].to_i,
config.mangadex["download_retries"].to_i, logger
Config.load(config_path).set_current
MangaDex::Downloader.default
context = Context.new config, logger, library, storage, queue
server = Server.new context
server = Server.new
server.start

141
src/rename.cr Normal file
View File

@@ -0,0 +1,141 @@
module Rename
alias VHash = Hash(String, String)
abstract class Base(T)
@ary = [] of T
def push(var)
@ary.push var
end
abstract def render(hash : VHash)
end
class Variable < Base(String)
property id : String
def initialize(@id)
end
def render(hash : VHash)
hash[@id]? || ""
end
end
class Pattern < Base(Variable)
def render(hash : VHash)
@ary.each do |v|
if hash.has_key? v.id
return v.render hash
end
end
""
end
end
class Group < Base(Pattern | String)
def render(hash : VHash)
return "" if @ary.select(&.is_a? Pattern)
.any? &.as(Pattern).render(hash).empty?
@ary.map do |e|
if e.is_a? Pattern
e.render hash
else
e
end
end.join
end
end
class Rule < Base(Group | String | Pattern)
ESCAPE = ['/']
def initialize(str : String)
parse! str
rescue e
raise "Failed to parse rename rule #{str}. Error: #{e}"
end
private def parse!(str : String)
chars = [] of Char
pattern : Pattern? = nil
group : Group? = nil
str.each_char_with_index do |char, i|
if ['[', ']', '{', '}', '|'].includes?(char) && !chars.empty?
string = chars.join
if !pattern.nil?
pattern.push Variable.new string.strip
elsif !group.nil?
group.push string
else
@ary.push string
end
chars = [] of Char
end
case char
when '['
if !group.nil? || !pattern.nil?
raise "nested groups are not allowed"
end
group = Group.new
when ']'
if group.nil?
raise "unmatched ] at position #{i}"
end
if !pattern.nil?
raise "patterns (`{}`) should be closed before closing the " \
"group (`[]`)"
end
@ary.push group
group = nil
when '{'
if !pattern.nil?
raise "nested patterns are not allowed"
end
pattern = Pattern.new
when '}'
if pattern.nil?
raise "unmatched } at position #{i}"
end
if !group.nil?
group.push pattern
else
@ary.push pattern
end
pattern = nil
when '|'
if pattern.nil?
chars.push char
end
else
if ESCAPE.includes? char
raise "the character #{char} at position #{i} is not allowed"
end
chars.push char
end
end
unless chars.empty?
@ary.push chars.join
end
if !pattern.nil?
raise "unclosed pattern {"
end
if !group.nil?
raise "unclosed group ["
end
end
def render(hash : VHash)
@ary.map do |e|
if e.is_a? String
e
else
e.render hash
end
end.join.strip
end
end
end

View File

@@ -1,7 +1,7 @@
require "./router"
class AdminRouter < Router
def setup
def initialize
get "/admin" do |env|
layout "admin"
end
@@ -48,13 +48,13 @@ class AdminRouter < Router
@context.storage.new_user username, password, admin
env.redirect "/admin/user"
redirect env, "/admin/user"
rescue e
@context.error e
redirect_url = URI.new \
path: "/admin/user/edit",
query: hash_to_query({"error" => e.message})
env.redirect redirect_url.to_s
redirect env, redirect_url.to_s
end
post "/admin/user/edit/:original_username" do |env|
@@ -85,18 +85,18 @@ class AdminRouter < Router
@context.storage.update_user \
original_username, username, password, admin
env.redirect "/admin/user"
redirect env, "/admin/user"
rescue e
@context.error e
redirect_url = URI.new \
path: "/admin/user/edit",
query: hash_to_query({"username" => original_username, \
"admin" => admin, "error" => e.message})
env.redirect redirect_url.to_s
redirect env, redirect_url.to_s
end
get "/admin/downloads" do |env|
base_url = @context.config.mangadex["base_url"]
mangadex_base_url = Config.current.mangadex["base_url"]
layout "download-manager"
end
end

View File

@@ -3,7 +3,7 @@ require "../mangadex/*"
require "../upload"
class APIRouter < Router
def setup
def initialize
get "/api/page/:tid/:eid/:page" do |env|
begin
tid = env.params.url["tid"]
@@ -123,7 +123,7 @@ class APIRouter < Router
get "/api/admin/mangadex/manga/:id" do |env|
begin
id = env.params.url["id"]
api = MangaDex::API.new @context.config.mangadex["api_url"].to_s
api = MangaDex::API.default
manga = api.get_manga id
send_json env, manga.to_info_json
rescue e
@@ -230,7 +230,7 @@ class APIRouter < Router
end
ext = File.extname filename
upload = Upload.new @context.config.upload_path, @context.logger
upload = Upload.new Config.current.upload_path
url = upload.path_to_url upload.save "img", ext, part.body
if url.nil?

View File

@@ -1,8 +1,9 @@
require "./router"
class MainRouter < Router
def setup
def initialize
get "/login" do |env|
base_url = Config.current.base_url
render "src/views/login.ecr"
end
@@ -13,7 +14,7 @@ class MainRouter < Router
rescue e
@context.error "Error when attempting to log out: #{e}"
ensure
env.redirect "/login"
redirect env, "/login"
end
end
@@ -26,9 +27,9 @@ class MainRouter < Router
cookie = HTTP::Cookie.new "token", token
cookie.expires = Time.local.shift years: 1
env.response.cookies << cookie
env.redirect "/"
redirect env, "/"
rescue
env.redirect "/login"
redirect env, "/login"
end
end
@@ -59,7 +60,7 @@ class MainRouter < Router
end
get "/download" do |env|
base_url = @context.config.mangadex["base_url"]
mangadex_base_url = Config.current.mangadex["base_url"]
layout "download"
end
end

View File

@@ -1,7 +1,7 @@
require "./router"
class ReaderRouter < Router
def setup
def initialize
get "/reader/:title/:entry" do |env|
begin
title = (@context.library.get_title env.params.url["title"]).not_nil!
@@ -15,7 +15,7 @@ class ReaderRouter < Router
# might not have actually read them
page = [page - 2 * IMGS_PER_PAGE, 1].max
env.redirect "/reader/#{title.id}/#{entry.id}/#{page}"
redirect env, "/reader/#{title.id}/#{entry.id}/#{page}"
rescue e
@context.error e
env.response.status_code = 404
@@ -24,6 +24,8 @@ class ReaderRouter < Router
get "/reader/:title/:entry/:page" do |env|
begin
base_url = Config.current.base_url
title = (@context.library.get_title env.params.url["title"]).not_nil!
entry = (title.get_entry env.params.url["entry"]).not_nil!
page = env.params.url["page"].to_i
@@ -35,20 +37,20 @@ class ReaderRouter < Router
pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min)
urls = pages.map { |idx|
"/api/page/#{title.id}/#{entry.id}/#{idx}"
"#{base_url}api/page/#{title.id}/#{entry.id}/#{idx}"
}
reader_urls = pages.map { |idx|
"/reader/#{title.id}/#{entry.id}/#{idx}"
"#{base_url}reader/#{title.id}/#{entry.id}/#{idx}"
}
next_page = page + IMGS_PER_PAGE
next_url = next_entry_url = nil
exit_url = "/book/#{title.id}"
exit_url = "#{base_url}book/#{title.id}"
next_entry = title.next_entry entry
unless next_page > entry.pages
next_url = "/reader/#{title.id}/#{entry.id}/#{next_page}"
next_url = "#{base_url}reader/#{title.id}/#{entry.id}/#{next_page}"
end
unless next_entry.nil?
next_entry_url = "/reader/#{title.id}/#{next_entry.id}"
next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}"
end
render "src/views/reader.ecr"

View File

@@ -1,6 +1,3 @@
require "../context"
class Router
def initialize(@context : Context)
end
@context : Context = Context.default
end

View File

@@ -1,11 +1,38 @@
require "kemal"
require "./context"
require "./library"
require "./handlers/*"
require "./util"
require "./routes/*"
class Context
property library : Library
property storage : Storage
property queue : MangaDex::Queue
def self.default : self
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize
@storage = Storage.default
@library = Library.default
@queue = MangaDex::Queue.default
end
{% for lvl in Logger::LEVELS %}
def {{lvl.id}}(msg)
Logger.{{lvl.id}} msg
end
{% end %}
end
class Server
def initialize(@context : Context)
@context : Context = Context.default
def initialize
error 403 do |env|
message = "HTTP 403: You are not authorized to visit #{env.request.path}"
layout "message"
@@ -14,20 +41,23 @@ class Server
message = "HTTP 404: Mango cannot find the page #{env.request.path}"
layout "message"
end
error 500 do |env|
message = "HTTP 500: Internal server error. Please try again later."
layout "message"
end
MainRouter.new(@context).setup
AdminRouter.new(@context).setup
ReaderRouter.new(@context).setup
APIRouter.new(@context).setup
{% if flag?(:release) %}
error 500 do |env|
message = "HTTP 500: Internal server error. Please try again later."
layout "message"
end
{% end %}
MainRouter.new
AdminRouter.new
ReaderRouter.new
APIRouter.new
Kemal.config.logging = false
add_handler LogHandler.new @context.logger
add_handler LogHandler.new
add_handler AuthHandler.new @context.storage
add_handler UploadHandler.new @context.config.upload_path
add_handler UploadHandler.new Config.current.upload_path
{% if flag?(:release) %}
# when building for relase, embed the static files in binary
@context.debug "We are in release mode. Using embedded static files."
@@ -41,7 +71,7 @@ class Server
{% if flag?(:release) %}
Kemal.config.env = "production"
{% end %}
Kemal.config.port = @context.config.port
Kemal.config.port = Config.current.port
Kemal.run
end
end

View File

@@ -13,14 +13,24 @@ def verify_password(hash, pw)
end
class Storage
def initialize(@path : String, @logger : Logger)
dir = File.dirname path
@path : String
def self.default : self
unless @@default
@@default = new
end
@@default.not_nil!
end
def initialize(db_path : String? = nil)
@path = db_path || Config.current.db_path
dir = File.dirname @path
unless Dir.exists? dir
@logger.info "The DB directory #{dir} does not exist. " \
"Attepmting to create it"
Logger.info "The DB directory #{dir} does not exist. " \
"Attepmting to create it"
Dir.mkdir_p dir
end
DB.open "sqlite3://#{path}" do |db|
DB.open "sqlite3://#{@path}" do |db|
begin
# We create the `ids` table first. even if the uses has an
# early version installed and has the `user` table only,
@@ -34,19 +44,19 @@ class Storage
"(username text, password text, token text, admin integer)"
rescue e
unless e.message.not_nil!.ends_with? "already exists"
@logger.fatal "Error when checking tables in DB: #{e}"
Logger.fatal "Error when checking tables in DB: #{e}"
raise e
end
else
@logger.debug "Creating DB file at #{@path}"
Logger.debug "Creating DB file at #{@path}"
db.exec "create unique index username_idx on users (username)"
db.exec "create unique index token_idx on users (token)"
random_pw = random_str
hash = hash_password random_pw
db.exec "insert into users values (?, ?, ?, ?)",
"admin", hash, nil, 1
@logger.log "Initial user created. You can log in with " \
"#{{"username" => "admin", "password" => random_pw}}"
Logger.log "Initial user created. You can log in with " \
"#{{"username" => "admin", "password" => random_pw}}"
end
end
end
@@ -58,18 +68,18 @@ class Storage
"users where username = (?)",
username, as: {String, String?}
unless verify_password hash, password
@logger.debug "Password does not match the hash"
Logger.debug "Password does not match the hash"
return nil
end
@logger.debug "User #{username} verified"
Logger.debug "User #{username} verified"
return token if token
token = random_str
@logger.debug "Updating token for #{username}"
Logger.debug "Updating token for #{username}"
db.exec "update users set token = (?) where username = (?)",
token, username
return token
rescue e
@logger.error "Error when verifying user #{username}: #{e}"
Logger.error "Error when verifying user #{username}: #{e}"
return nil
end
end
@@ -82,7 +92,7 @@ class Storage
username = db.query_one "select username from users where " \
"token = (?)", token, as: String
rescue e
@logger.debug "Unable to verify token"
Logger.debug "Unable to verify token"
end
end
username
@@ -95,7 +105,7 @@ class Storage
is_admin = db.query_one "select admin from users where " \
"token = (?)", token, as: Bool
rescue e
@logger.debug "Unable to verify user as admin"
Logger.debug "Unable to verify user as admin"
end
end
is_admin

View File

@@ -1,10 +1,10 @@
require "./util"
class Upload
def initialize(@dir : String, @logger : Logger)
def initialize(@dir : String)
unless Dir.exists? @dir
@logger.info "The uploads directory #{@dir} does not exist. " \
"Attempting to create it"
Logger.info "The uploads directory #{@dir} does not exist. " \
"Attempting to create it"
Dir.mkdir_p @dir
end
end
@@ -19,7 +19,7 @@ class Upload
file_path = File.join full_dir, filename
unless Dir.exists? full_dir
@logger.debug "creating directory #{full_dir}"
Logger.debug "creating directory #{full_dir}"
Dir.mkdir_p full_dir
end
@@ -50,7 +50,7 @@ class Upload
end
if ary.empty?
@logger.warn "File #{path} is not in the upload directory #{@dir}"
Logger.warn "File #{path} is not in the upload directory #{@dir}"
return
end

View File

@@ -4,6 +4,7 @@ IMGS_PER_PAGE = 5
UPLOAD_URL_PREFIX = "/uploads"
macro layout(name)
base_url = Config.current.base_url
begin
cookie = env.request.cookies.find { |c| c.name == "token" }
is_admin = false
@@ -99,3 +100,8 @@ end
def random_str
UUID.random.to_s.gsub "-", ""
end
def redirect(env, path)
base = Config.current.base_url
env.redirect File.join base, path
end

View File

@@ -1,5 +1,5 @@
<ul class="uk-list uk-list-large uk-list-divider">
<li data-url="/admin/user">User Managerment</li>
<li data-url="<%= base_url %>admin/user">User Managerment</li>
<li onclick="if(!scanning){scan()}">
<span id="scan">Scan Library Files</span>
<span id="scan-status" class="uk-align-right">
@@ -7,12 +7,12 @@
<span hidden></span>
</span>
</li>
<li data-url="/admin/downloads">Download Manager</li>
<li data-url="<%= base_url %>admin/downloads">Download Manager</li>
</ul>
<hr class="uk-divider-icon">
<a class="uk-button uk-button-danger" href="/logout">Log Out</a>
<a class="uk-button uk-button-danger" href="<%= base_url %>logout">Log Out</a>
<% content_for "script" do %>
<script src="/js/admin.js"></script>
<script src="<%= base_url %>js/admin.js"></script>
<% end %>

View File

@@ -24,9 +24,9 @@
<% content_for "script" do %>
<script>
var baseURL = "<%= base_url %>".replace(/\/$/, "");
var baseURL = "<%= mangadex_base_url %>".replace(/\/$/, "");
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<script src="/js/alert.js"></script>
<script src="/js/download-manager.js"></script>
<script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/download-manager.js"></script>
<% end %>

View File

@@ -74,10 +74,10 @@
<% content_for "script" do %>
<script>
var baseURL = "<%= base_url %>".replace(/\/$/, "");
var baseURL = "<%= mangadex_base_url %>".replace(/\/$/, "");
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
<script src="/js/alert.js"></script>
<script src="/js/download.js"></script>
<script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/download.js"></script>
<% end %>

View File

@@ -23,7 +23,7 @@
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
<%- titles.each_with_index do |t, i| -%>
<div class="item" data-mtime="<%= t.mtime.to_unix %>" data-progress="<%= percentage[i] %>">
<a class="acard" href="/book/<%= t.id %>">
<a class="acard" href="<%= base_url %>book/<%= t.id %>">
<div class="uk-card uk-card-default">
<div class="uk-card-media-top">
<img data-src="<%= t.cover_url %>" data-width data-height alt="" uk-img>
@@ -43,7 +43,7 @@
<% content_for "script" do %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script>
<script src="/js/dots.js"></script>
<script src="/js/search.js"></script>
<script src="/js/sort-items.js"></script>
<script src="<%= base_url %>js/dots.js"></script>
<script src="<%= base_url %>js/search.js"></script>
<script src="<%= base_url %>js/sort-items.js"></script>
<% end %>

View File

@@ -7,11 +7,11 @@
<meta name="description" content="Mango Manga Server">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/css/uikit.min.css" />
<link rel="stylesheet" href="/css/mango.css" />
<script defer src="/js/fontawesome.min.js"></script>
<script defer src="/js/solid.min.js"></script>
<link rel="stylesheet" href="<%= base_url %>css/mango.css" />
<script defer src="<%= base_url %>js/fontawesome.min.js"></script>
<script defer src="<%= base_url %>js/solid.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="/js/theme.js"></script>
<script src="<%= base_url %>js/theme.js"></script>
</head>
<body>
@@ -20,14 +20,14 @@
<div id="mobile-nav" uk-offcanvas="overlay: true">
<div class="uk-offcanvas-bar uk-flex uk-flex-column">
<ul class="uk-nav uk-nav-primary uk-nav-center uk-margin-auto-vertical">
<li><a href="/">Home</a></li>
<li><a href="<%= base_url %>">Home</a></li>
<% if is_admin %>
<li><a href="/admin">Admin</a></li>
<li><a href="/download">Download</a></li>
<li><a href="<%= base_url %>admin">Admin</a></li>
<li><a href="<%= base_url %>download">Download</a></li>
<% end %>
<hr uk-divider>
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
<li><a href="/logout">Logout</a></li>
<li><a href="<%= base_url %>logout">Logout</a></li>
</ul>
</div>
</div>
@@ -39,19 +39,19 @@
<div class="uk-navbar-toggle" uk-navbar-toggle-icon="uk-navbar-toggle-icon" uk-toggle="target: #mobile-nav"></div>
</div>
<div class="uk-navbar-left uk-visible@s">
<a class="uk-navbar-item uk-logo" href="/"><img src="/img/icon.png"></a>
<a class="uk-navbar-item uk-logo" href="<%= base_url %>"><img src="<%= base_url %>img/icon.png"></a>
<ul class="uk-navbar-nav">
<li><a href="/">Home</a></li>
<li><a href="<%= base_url %>">Home</a></li>
<% if is_admin %>
<li><a href="/admin">Admin</a></li>
<li><a href="/download">Download</a></li>
<li><a href="<%= base_url %>admin">Admin</a></li>
<li><a href="<%= base_url %>download">Download</a></li>
<% end %>
</ul>
</div>
<div class="uk-navbar-right uk-visible@s">
<ul class="uk-navbar-nav">
<li><a onclick="toggleTheme()"><i class="fas fa-adjust"></i></a></li>
<li><a href="/logout">Logout</a></li>
<li><a href="<%= base_url %>logout">Logout</a></li>
</ul>
</div>
</div>
@@ -66,6 +66,7 @@
</div>
<script>
setTheme(getTheme());
const base_url = "<%= base_url %>";
</script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>

View File

@@ -9,7 +9,7 @@
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/css/uikit.min.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="/js/theme.js"></script>
<script src="<%= base_url %>js/theme.js"></script>
</head>
<body>
<div class="uk-section uk-flex uk-flex-middle uk-animation-fade" uk-height-viewport="">
@@ -19,7 +19,7 @@
<div class="uk-width-1-1@m">
<div class="uk-margin uk-width-large uk-margin-auto uk-card uk-card-default uk-card-body uk-box-shadow-large">
<h3 class="uk-card-title uk-text-center">Log In</h3>
<form action="/login" method="post">
<form action="<%= base_url %>login" method="post">
<div class="uk-margin">
<div class="uk-inline uk-width-1-1"><span class="uk-form-icon" uk-icon="icon:user"></span><input class="uk-input uk-form-large" type="text" name="username"></div>
</div>

View File

@@ -7,11 +7,11 @@
<meta name="description" content="Mango Manga Server">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/css/uikit.min.css" />
<link rel="stylesheet" href="/css/mango.css" />
<link rel="stylesheet" href="<%= base_url %>css/mango.css" />
</head>
<body>
<script src="/js/theme.js"></script>
<script src="<%= base_url %>js/theme.js"></script>
<div class="uk-section uk-section-default uk-section-small reader-bg">
<div class="uk-container uk-container-small">
<%- urls.each_with_index do |url, i| -%>
@@ -56,10 +56,13 @@
</div>
</div>
</div>
<script>
const base_url = "<%= base_url %>"
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script>
<script src="/js/reader.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ScrollMagic/2.0.7/ScrollMagic.min.js"></script>
<script src="<%= base_url %>js/reader.js"></script>
</body>
</html>

View File

@@ -7,9 +7,9 @@
</h2>
</div>
<ul class="uk-breadcrumb">
<li><a href="/">Library</a></li>
<li><a href="<%= base_url %>">Library</a></li>
<%- title.parents.each do |t| -%>
<li><a href="/book/<%= t.id %>"><%= t.display_name %></a></li>
<li><a href="<%= base_url %>book/<%= t.id %>"><%= t.display_name %></a></li>
<%- end -%>
<li class="uk-disabled"><a><%= title.display_name %></a></li>
</ul>
@@ -39,7 +39,7 @@
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
<%- title.titles.each_with_index do |t, i| -%>
<div class="item" data-mtime="<%= t.mtime.to_unix %>" data-progress="0.0">
<a class="acard" href="/book/<%= t.id %>">
<a class="acard" href="<%= base_url %>book/<%= t.id %>">
<div class="uk-card uk-card-default">
<div class="uk-card-media-top">
<img data-src="<%= t.cover_url %>" data-width data-height alt="" uk-img>
@@ -151,9 +151,9 @@
<% content_for "script" do %>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jQuery.dotdotdot/4.0.11/dotdotdot.js"></script>
<script src="/js/dots.js"></script>
<script src="/js/alert.js"></script>
<script src="/js/title.js"></script>
<script src="/js/search.js"></script>
<script src="/js/sort-items.js"></script>
<script src="<%= base_url %>js/dots.js"></script>
<script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/title.js"></script>
<script src="<%= base_url %>js/search.js"></script>
<script src="<%= base_url %>js/sort-items.js"></script>
<% end %>

View File

@@ -1,4 +1,4 @@
<form action="/admin/user/edit" method="post" accept-charset="utf-8">
<form action="<%= base_url %>admin/user/edit" method="post" accept-charset="utf-8">
<div class="uk-margin">
<label class="uk-form-label" for="form-stacked-text">Username</label>
@@ -49,6 +49,6 @@
error = '<%= error %>';
<%- end -%>
</script>
<script src="/js/alert.js"></script>
<script src="/js/user-edit.js"></script>
<script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/user-edit.js"></script>
<% end %>

View File

@@ -12,7 +12,7 @@
<td><%= u[0] %></td>
<td><%= u[1] %></td>
<td>
<a href="/admin/user/edit?username=<%= u[0] %>&admin=<%= u[1] %>" uk-icon="file-edit"></a>
<a href="<%= base_url %>admin/user/edit?username=<%= u[0] %>&admin=<%= u[1] %>" uk-icon="file-edit"></a>
<%- if u[0] != username %>
<a href="#" onclick="remove('<%= u[0] %>');return false;" uk-icon="trash"></a>
<%- end %>
@@ -22,10 +22,10 @@
</tbody>
</table>
<a href="/admin/user/edit" class="uk-button uk-button-primary">New User</a>
<a href="<%= base_url %>admin/user/edit" class="uk-button uk-button-primary">New User</a>
<% content_for "script" do %>
<script src="/js/alert.js"></script>
<script src="/js/user.js"></script>
<script src="<%= base_url %>js/alert.js"></script>
<script src="<%= base_url %>js/user.js"></script>
<% end %>