mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-03 03:15:31 -04:00
Merge branch 'master' into v0.2.0
This commit is contained in:
commit
319967438b
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
32
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: I found a bug in Mango!
|
||||
title: "[Bug Report]"
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- OS: [e.g. Ubuntu 18.04]
|
||||
- Browser [e.g. chrome, safari, if applicable]
|
||||
- Mango Version [e.g. v0.1.0]
|
||||
|
||||
**Docker (if you are running Mango in a Docker container)**
|
||||
- The `docker-compose.yml` file you are using
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here. Add screenshots if applicable.
|
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature for Mango
|
||||
title: "[Feature Request]"
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
10
.github/ISSUE_TEMPLATE/general-question.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/general-question.md
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
---
|
||||
name: General Question
|
||||
about: I have a question about Mango
|
||||
title: "[Question]"
|
||||
labels: general question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
16
README.md
16
README.md
@ -12,18 +12,22 @@ Mango is a self-hosted manga server and reader. Its features include
|
||||
|
||||
## Installation
|
||||
|
||||
### Pre-built Binary
|
||||
|
||||
1. Simply download the pre-built binary file `mango` for the latest [release](https://github.com/hkalexling/Mango/releases). All the dependencies are statically linked, and it should work with most Linux systems on amd64.
|
||||
|
||||
### Docker
|
||||
|
||||
1. Make sure you have docker installed and running. You will also need `docker-compose`
|
||||
2. Clone the repository
|
||||
3. Copy `docker-compose.example.yml` to `docker-compose.yml` and modify it if necessary
|
||||
4. Run `docker-compose up`. This should build the docker image and start the container with Mango running inside
|
||||
5. Head over to `localhost:9000` to log in
|
||||
|
||||
3. Copy `docker-compose.example.yml` to `docker-compose.yml`
|
||||
4. Modify the `volumes` in `docker-compose.yml` to point the directories to desired locations on the host machine
|
||||
5. Run `docker-compose up`. This should build the docker image and start the container with Mango running inside
|
||||
6. Head over to `localhost:9000` to log in
|
||||
|
||||
### Build from source
|
||||
|
||||
1. Make sure you have Crystal, Node and Yarn installed
|
||||
1. Make sure you have Crystal, Node and Yarn installed. You might also need to install the development headers for `libsqlite3` and `libyaml`.
|
||||
2. Clone the repository
|
||||
3. `make && sudo make install`
|
||||
4. Start Mango by running the command `mango`
|
||||
@ -43,7 +47,7 @@ Mango e-manga server/reader. Version 0.1.0
|
||||
|
||||
### Config
|
||||
|
||||
The default config file location is `~/.config/mango/config.yml`. The config options and default values are given below
|
||||
The default config file location is `~/.config/mango/config.yml`. It might be different if you are running Mango in a docker container. The config options and default values are given below
|
||||
|
||||
```yaml
|
||||
---
|
||||
|
@ -11,8 +11,5 @@ services:
|
||||
ports:
|
||||
- 9000:9000
|
||||
volumes:
|
||||
- library:/root/mango/library
|
||||
- config:/root/.config/mango
|
||||
volumes:
|
||||
library:
|
||||
config:
|
||||
- ~/mango:/root/mango
|
||||
- ~/.config/mango:/root/.config/mango
|
||||
|
@ -1,4 +1,7 @@
|
||||
function showModal(title, zipPath, pages, percentage, title, entry) {
|
||||
function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTitle, titleID, entryID) {
|
||||
const zipPath = decodeURIComponent(encodedPath);
|
||||
const title = decodeURIComponent(encodedeTitle);
|
||||
const entry = decodeURIComponent(encodedEntryTitle);
|
||||
$('#modal button, #modal a').each(function(){
|
||||
$(this).removeAttr('hidden');
|
||||
});
|
||||
@ -16,20 +19,20 @@ function showModal(title, zipPath, pages, percentage, title, entry) {
|
||||
$('#path-text').text(zipPath);
|
||||
$('#pages-text').text(pages + ' pages');
|
||||
|
||||
$('#beginning-btn').attr('href', '/reader/' + title + '/' + entry + '/1');
|
||||
$('#continue-btn').attr('href', '/reader/' + title + '/' + entry);
|
||||
$('#beginning-btn').attr('href', '/reader/' + titleID + '/' + entryID + '/1');
|
||||
$('#continue-btn').attr('href', '/reader/' + titleID + '/' + entryID);
|
||||
|
||||
$('#read-btn').click(function(){
|
||||
updateProgress(title, entry, pages);
|
||||
updateProgress(titleID, entryID, pages);
|
||||
});
|
||||
$('#unread-btn').click(function(){
|
||||
updateProgress(title, entry, 0);
|
||||
updateProgress(titleID, entryID, 0);
|
||||
});
|
||||
|
||||
UIkit.modal($('#modal')).show();
|
||||
}
|
||||
function updateProgress(title, entry, page) {
|
||||
$.post('/api/progress/' + title + '/' + entry + '/' + page, function(data) {
|
||||
function updateProgress(titleID, entryID, page) {
|
||||
$.post('/api/progress/' + titleID + '/' + entryID + '/' + page, function(data) {
|
||||
if (data.success) {
|
||||
location.reload();
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
require "zip"
|
||||
require "mime"
|
||||
require "json"
|
||||
require "uri"
|
||||
|
||||
struct Image
|
||||
property data : Bytes
|
||||
@ -13,12 +14,16 @@ struct Image
|
||||
end
|
||||
|
||||
class Entry
|
||||
JSON.mapping zip_path: String, book_title: String, title: String, \
|
||||
size: String, pages: Int32, cover_url: String, mtime: Time
|
||||
JSON.mapping zip_path: String, book_title: String, title: String,
|
||||
size: String, pages: Int32, cover_url: String, id: String,
|
||||
title_id: String, encoded_path: String, encoded_title: String,
|
||||
mtime: Time
|
||||
|
||||
def initialize(path, @book_title)
|
||||
def initialize(path, @book_title, @title_id, storage)
|
||||
@zip_path = path
|
||||
@encoded_path = URI.encode path
|
||||
@title = File.basename path, File.extname path
|
||||
@encoded_title = URI.encode @title
|
||||
@size = (File.size path).humanize_bytes
|
||||
file = Zip::File.new path
|
||||
@pages = file.entries
|
||||
@ -28,7 +33,8 @@ class Entry
|
||||
}
|
||||
.size
|
||||
file.close
|
||||
@cover_url = "/api/page/#{@book_title}/#{title}/1"
|
||||
@id = storage.get_id @zip_path, false
|
||||
@cover_url = "/api/page/#{@title_id}/#{@id}/1"
|
||||
@mtime = File.info(@zip_path).modification_time
|
||||
end
|
||||
def read_page(page_num)
|
||||
@ -54,23 +60,30 @@ class Entry
|
||||
end
|
||||
|
||||
class Title
|
||||
JSON.mapping dir: String, entries: Array(Entry), title: String, mtime: Time
|
||||
JSON.mapping dir: String, entries: Array(Entry), title: String,
|
||||
id: String, encoded_title: String, mtime: Time
|
||||
|
||||
def initialize(dir : String)
|
||||
def initialize(dir : String, storage)
|
||||
@dir = dir
|
||||
@id = storage.get_id @dir, true
|
||||
@title = File.basename dir
|
||||
@encoded_title = URI.encode @title
|
||||
@entries = (Dir.entries dir)
|
||||
.select { |path| [".zip", ".cbz"].includes? File.extname path }
|
||||
.map { |path| Entry.new File.join(dir, path), @title }
|
||||
.map { |path|
|
||||
Entry.new File.join(dir, path), @title, @id, storage
|
||||
}
|
||||
.select { |e| e.pages > 0 }
|
||||
.sort { |a, b| a.title <=> b.title }
|
||||
mtimes = [File.info(dir).modification_time]
|
||||
mtimes += @entries.map{|e| e.mtime}
|
||||
@mtime = mtimes.max
|
||||
end
|
||||
def get_entry(name)
|
||||
@entries.find { |e| e.title == name }
|
||||
def get_entry(eid)
|
||||
@entries.find { |e| e.id == eid }
|
||||
end
|
||||
# For backward backward compatibility with v0.1.0, we save entry titles
|
||||
# instead of IDs in info.json
|
||||
def save_progress(username, entry, page)
|
||||
info = TitleInfo.new @dir
|
||||
if info.progress[username]?.nil?
|
||||
@ -81,7 +94,7 @@ class Title
|
||||
info.progress[username][entry] = page
|
||||
info.save @dir
|
||||
end
|
||||
def load_progress(username, entry : String)
|
||||
def load_progress(username, entry)
|
||||
info = TitleInfo.new @dir
|
||||
if info.progress[username]?.nil?
|
||||
return 0
|
||||
@ -91,10 +104,10 @@ class Title
|
||||
end
|
||||
info.progress[username][entry]
|
||||
end
|
||||
def load_percetage(username, entry : String)
|
||||
def load_percetage(username, entry)
|
||||
info = TitleInfo.new @dir
|
||||
page = load_progress username, entry
|
||||
entry_obj = get_entry entry
|
||||
entry_obj = @entries.find{|e| e.title == entry}
|
||||
return 0 if entry_obj.nil?
|
||||
page / entry_obj.pages
|
||||
end
|
||||
@ -142,9 +155,10 @@ class TitleInfo
|
||||
end
|
||||
|
||||
class Library
|
||||
JSON.mapping dir: String, titles: Array(Title), scan_interval: Int32, logger: MLogger
|
||||
JSON.mapping dir: String, titles: Array(Title), scan_interval: Int32,
|
||||
logger: MLogger, storage: Storage
|
||||
|
||||
def initialize(@dir, @scan_interval, @logger)
|
||||
def initialize(@dir, @scan_interval, @logger, @storage)
|
||||
# explicitly initialize @titles to bypass the compiler check. it will
|
||||
# be filled with actual Titles in the `scan` call below
|
||||
@titles = [] of Title
|
||||
@ -160,8 +174,8 @@ class Library
|
||||
end
|
||||
end
|
||||
end
|
||||
def get_title(name)
|
||||
@titles.find { |t| t.title == name }
|
||||
def get_title(tid)
|
||||
@titles.find { |t| t.id == tid }
|
||||
end
|
||||
def scan
|
||||
unless Dir.exists? @dir
|
||||
@ -171,7 +185,7 @@ class Library
|
||||
end
|
||||
@titles = (Dir.entries @dir)
|
||||
.select { |path| File.directory? File.join @dir, path }
|
||||
.map { |path| Title.new File.join @dir, path }
|
||||
.map { |path| Title.new File.join(@dir, path), @storage }
|
||||
.select { |title| !title.entries.empty? }
|
||||
.sort { |a, b| a.title <=> b.title }
|
||||
@logger.debug "Scan completed"
|
||||
|
@ -25,8 +25,8 @@ end
|
||||
|
||||
config = Config.load config_path
|
||||
logger = MLogger.new config
|
||||
library = Library.new config.library_path, config.scan_interval, logger
|
||||
storage = Storage.new config.db_path, logger
|
||||
library = Library.new config.library_path, config.scan_interval, logger, storage
|
||||
|
||||
context = Context.new config, logger, library, storage
|
||||
|
||||
|
@ -3,19 +3,20 @@ require "../mangadex/*"
|
||||
|
||||
class APIRouter < Router
|
||||
def setup
|
||||
get "/api/page/:title/:entry/:page" do |env|
|
||||
get "/api/page/:tid/:eid/:page" do |env|
|
||||
begin
|
||||
title = env.params.url["title"]
|
||||
entry = env.params.url["entry"]
|
||||
tid = env.params.url["tid"]
|
||||
eid = env.params.url["eid"]
|
||||
page = env.params.url["page"].to_i
|
||||
|
||||
t = @context.library.get_title title
|
||||
raise "Title `#{title}` not found" if t.nil?
|
||||
e = t.get_entry entry
|
||||
raise "Entry `#{entry}` of `#{title}` not found" if e.nil?
|
||||
img = e.read_page page
|
||||
raise "Failed to load page #{page} of `#{title}/#{entry}`"\
|
||||
if img.nil?
|
||||
title = @context.library.get_title tid
|
||||
raise "Title ID `#{tid}` not found" if title.nil?
|
||||
entry = title.get_entry eid
|
||||
raise "Entry ID `#{eid}` of `#{title.title}` not found" if \
|
||||
entry.nil?
|
||||
img = entry.read_page page
|
||||
raise "Failed to load page #{page} of " \
|
||||
"`#{title.title}/#{entry.title}`" if img.nil?
|
||||
|
||||
send_img env, img
|
||||
rescue e
|
||||
@ -27,12 +28,11 @@ class APIRouter < Router
|
||||
|
||||
get "/api/book/:title" do |env|
|
||||
begin
|
||||
title = env.params.url["title"]
|
||||
tid = env.params.url["tid"]
|
||||
title = @context.library.get_title tid
|
||||
raise "Title ID `#{tid}` not found" if title.nil?
|
||||
|
||||
t = @context.library.get_title title
|
||||
raise "Title `#{title}` not found" if t.nil?
|
||||
|
||||
send_json env, t.to_json
|
||||
send_json env, title.to_json
|
||||
rescue e
|
||||
@context.error e
|
||||
env.response.status_code = 500
|
||||
|
@ -16,7 +16,7 @@ class ReaderRouter < Router
|
||||
# might not have actually read them
|
||||
page = [page - 2 * IMGS_PER_PAGE, 1].max
|
||||
|
||||
env.redirect "/reader/#{title.title}/#{entry.title}/#{page}"
|
||||
env.redirect "/reader/#{title.id}/#{entry.id}/#{page}"
|
||||
rescue e
|
||||
@context.error e
|
||||
env.response.status_code = 404
|
||||
@ -37,16 +37,16 @@ class ReaderRouter < Router
|
||||
|
||||
pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min)
|
||||
urls = pages.map { |idx|
|
||||
"/api/page/#{title.title}/#{entry.title}/#{idx}" }
|
||||
"/api/page/#{title.id}/#{entry.id}/#{idx}" }
|
||||
reader_urls = pages.map { |idx|
|
||||
"/reader/#{title.title}/#{entry.title}/#{idx}" }
|
||||
"/reader/#{title.id}/#{entry.id}/#{idx}" }
|
||||
next_page = page + IMGS_PER_PAGE
|
||||
next_url = next_page > entry.pages ? nil :
|
||||
"/reader/#{title.title}/#{entry.title}/#{next_page}"
|
||||
exit_url = "/book/#{title.title}"
|
||||
"/reader/#{title.id}/#{entry.id}/#{next_page}"
|
||||
exit_url = "/book/#{title.id}"
|
||||
next_entry = title.next_entry entry
|
||||
next_entry_url = next_entry.nil? ? nil : \
|
||||
"/reader/#{title.title}/#{next_entry.title}"
|
||||
"/reader/#{title.id}/#{next_entry.id}"
|
||||
|
||||
render "src/views/reader.ecr"
|
||||
rescue e
|
||||
|
@ -12,7 +12,7 @@ def verify_password(hash, pw)
|
||||
end
|
||||
|
||||
def random_str
|
||||
Base64.strict_encode UUID.random().to_s
|
||||
UUID.random.to_s.gsub "-", ""
|
||||
end
|
||||
|
||||
class Storage
|
||||
@ -25,10 +25,18 @@ class Storage
|
||||
end
|
||||
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,
|
||||
# we will still be able to create `ids`
|
||||
db.exec "create table ids" \
|
||||
"(path text, id text, is_title integer)"
|
||||
db.exec "create unique index path_idx on ids (path)"
|
||||
db.exec "create unique index id_idx on ids (id)"
|
||||
|
||||
db.exec "create table users" \
|
||||
"(username text, password text, token text, admin integer)"
|
||||
rescue e
|
||||
unless e.message == "table users already exists"
|
||||
unless e.message.not_nil!.ends_with? "already exists"
|
||||
@logger.fatal "Error when checking tables in DB: #{e}"
|
||||
raise e
|
||||
end
|
||||
@ -147,4 +155,23 @@ class Storage
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def get_id(path, is_title)
|
||||
DB.open "sqlite3://#{@path}" do |db|
|
||||
begin
|
||||
id = db.query_one "select id from ids where path = (?)",
|
||||
path, as: {String}
|
||||
return id
|
||||
rescue
|
||||
id = random_str
|
||||
db.exec "insert into ids values (?, ?, ?)", path, id,
|
||||
is_title ? 1 : 0
|
||||
return id
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def to_json(json : JSON::Builder)
|
||||
json.string self
|
||||
end
|
||||
end
|
||||
|
@ -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.title %>">
|
||||
<a class="acard" href="/book/<%= t.id %>">
|
||||
<div class="uk-card uk-card-default">
|
||||
<div class="uk-card-media-top">
|
||||
<img src="<%= t.entries[0].cover_url %>" alt="">
|
||||
|
@ -25,7 +25,7 @@
|
||||
<%- title.entries.each_with_index do |e, i| -%>
|
||||
<div class="item" data-mtime="<%= e.mtime.to_unix %>" data-progress="<%= percentage[i] %>">
|
||||
<a class="acard">
|
||||
<div class="uk-card uk-card-default" onclick="showModal('<%= e.title %>', '<%= e.zip_path %>', '<%= e.pages %>', <%= (percentage[i] * 100).round(1) %>, '<%= title.title %>', '<%= e.title %>')">
|
||||
<div class="uk-card uk-card-default" onclick="showModal("<%= e.encoded_path %>", '<%= e.pages %>', <%= (percentage[i] * 100).round(1) %>, "<%= title.encoded_title %>", "<%= e.encoded_title %>", '<%= e.title_id %>', '<%= e.id %>')">
|
||||
<div class="uk-card-media-top">
|
||||
<img src="<%= e.cover_url %>" alt="">
|
||||
</div>
|
||||
|
Loading…
x
Reference in New Issue
Block a user