mirror of
https://github.com/hkalexling/Mango.git
synced 2026-04-25 00:00:52 -04:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac3df03d88 | |||
| 7c9728683c | |||
| d921d04abf | |||
| 5400c8c8ef | |||
| 58e96cd4fe | |||
| aa09f3a86f | |||
| a5daded453 | |||
| 4968cb8e18 | |||
| 27c6e02da8 | |||
| 68d1b55aea | |||
| 32dc3e84b9 | |||
| 460fcdf2f5 | |||
| c6369f9f26 | |||
| aa147602fc | |||
| d58c83fbd8 | |||
| 1a0c3d81ce | |||
| 33c61fd8c1 | |||
| 6eba3fe351 | |||
| da2708abe5 | |||
| febf344d33 | |||
| ae15398b6c | |||
| b28f6046dd | |||
| 91b823450c | |||
| 085fba611c | |||
| f8d633c751 | |||
| f5e6f42fc2 | |||
| 3ca6d3d338 | |||
| 750a28eccb | |||
| 88b16445e2 | |||
| 7774efa471 | |||
| 4aeda53806 | |||
| 5d62a87720 | |||
| e902e1dff0 | |||
| 9fe32b5011 | |||
| e65d701e0a | |||
| 5a500364fc | |||
| 3e42266955 | |||
| 6407cea7bf | |||
| 7e22cc5f57 | |||
| e68678f2fb | |||
| 82fb45b242 | |||
| 46dfc2f712 | |||
| 8c7ced87f1 |
@@ -0,0 +1,3 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
patreon: hkalexling
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master, dev ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master, dev ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
container:
|
||||||
|
image: crystallang/crystal:0.32.1-alpine
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install dependencies
|
||||||
|
run: apk add --no-cache yarn yaml sqlite-static
|
||||||
|
- name: Build
|
||||||
|
run: make
|
||||||
|
- name: Run tests
|
||||||
|
run: make test
|
||||||
+9
-11
@@ -1,18 +1,16 @@
|
|||||||
FROM crystallang/crystal:0.32.0
|
FROM crystallang/crystal:0.32.1-alpine AS builder
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y curl
|
|
||||||
|
|
||||||
RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
|
|
||||||
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
|
|
||||||
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
|
|
||||||
|
|
||||||
RUN apt-get update && apt-get install -y nodejs yarn libsqlite3-dev
|
|
||||||
|
|
||||||
WORKDIR /Mango
|
WORKDIR /Mango
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
COPY package*.json .
|
COPY package*.json .
|
||||||
|
RUN apk add --no-cache yarn yaml sqlite-static \
|
||||||
|
&& make static
|
||||||
|
|
||||||
RUN make && make install
|
FROM library/alpine
|
||||||
|
|
||||||
CMD ["mango"]
|
WORKDIR /
|
||||||
|
|
||||||
|
COPY --from=builder /Mango/mango .
|
||||||
|
|
||||||
|
CMD ["./mango"]
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
|
|
||||||
# Mango
|
# Mango
|
||||||
|
|
||||||
[](https://gitter.im/mango-cr/mango?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
[](https://www.patreon.com/hkalexling)  [](https://gitter.im/mango-cr/mango?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||||
|
|
||||||
Mango is a self-hosted manga server and reader. Its features include
|
Mango is a self-hosted manga server and reader. Its features include
|
||||||
|
|
||||||
- Multi-user support
|
- Multi-user support
|
||||||
- Dark/light mode switch
|
- Dark/light mode switch
|
||||||
- Supports both `.zip` and `.cbz` formats
|
- Supports both `.zip` and `.cbz` formats
|
||||||
|
- Supports nested folders in library
|
||||||
- Automatically stores reading progress
|
- Automatically stores reading progress
|
||||||
- Built-in [MangaDex](https://mangadex.org/) downloader
|
- Built-in [MangaDex](https://mangadex.org/) downloader
|
||||||
- The web reader is responsive and works well on mobile, so there is no need for a mobile app
|
- The web reader is responsive and works well on mobile, so there is no need for a mobile app
|
||||||
@@ -74,20 +75,23 @@ mangadex:
|
|||||||
- `scan_interval_minutes` can be any non-negative integer. Setting it to `0` disables the periodic scan
|
- `scan_interval_minutes` can be any non-negative integer. Setting it to `0` disables the periodic scan
|
||||||
- `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
|
||||||
|
|
||||||
### Required Library Structure
|
### Library Structure
|
||||||
|
|
||||||
Please make sure that your library directory has the following structure:
|
You can organize your `.cbz/.zip` files in nested folders in the library directory. Here's an example:
|
||||||
|
|
||||||
```
|
```
|
||||||
.
|
.
|
||||||
├── Manga 1
|
├── Manga 1
|
||||||
│  └── Manga 1.cbz
|
│  ├── Volume 1.cbz
|
||||||
|
│  ├── Volume 2.cbz
|
||||||
|
│  ├── Volume 3.cbz
|
||||||
|
│  └── Volume 4.zip
|
||||||
└── Manga 2
|
└── Manga 2
|
||||||
├── Vol 0001.zip
|
  └── Vol. 1
|
||||||
├── Vol 0002.zip
|
  └── Ch.1 - Ch.3
|
||||||
├── Vol 0003.zip
|
  ├── 1.zip
|
||||||
├── Vol 0004.zip
|
  ├── 2.zip
|
||||||
└── Vol 0005.zip
|
  └── 3.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Initial Login
|
### Initial Login
|
||||||
|
|||||||
+15
-3
@@ -5,8 +5,20 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.uk-card-media-top {
|
.uk-card-media-top {
|
||||||
max-height: 350px;
|
height: 250px;
|
||||||
overflow: hidden;
|
}
|
||||||
|
@media (min-width: 600px) {
|
||||||
|
.uk-card-media-top {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.uk-card-media-top > img {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
.uk-card-title {
|
||||||
|
height: 3em;
|
||||||
}
|
}
|
||||||
.acard:hover {
|
.acard:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@@ -20,7 +32,7 @@
|
|||||||
#scan-status {
|
#scan-status {
|
||||||
cursor: auto;
|
cursor: auto;
|
||||||
}
|
}
|
||||||
.uk-card-title {
|
.break-word {
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
}
|
}
|
||||||
.uk-logo > img {
|
.uk-logo > img {
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
const truncate = () => {
|
||||||
|
$('.acard .uk-card-title').each((i, e) => {
|
||||||
|
$(e).dotdotdot({
|
||||||
|
truncate: 'letter',
|
||||||
|
watch: true,
|
||||||
|
callback: (truncated) => {
|
||||||
|
if (truncated) {
|
||||||
|
$(e).attr('uk-tooltip', $(e).attr('data-title'));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$(e).removeAttr('uk-tooltip');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
truncate();
|
||||||
@@ -93,8 +93,8 @@ $(() => {
|
|||||||
else if (by === 'date')
|
else if (by === 'date')
|
||||||
res = $(a).attr('data-mtime') > $(b).attr('data-mtime');
|
res = $(a).attr('data-mtime') > $(b).attr('data-mtime');
|
||||||
else if (by === 'progress') {
|
else if (by === 'progress') {
|
||||||
const ap = $(a).attr('data-progress');
|
const ap = parseFloat($(a).attr('data-progress'));
|
||||||
const bp = $(b).attr('data-progress');
|
const bp = parseFloat($(b).attr('data-progress'));
|
||||||
if (ap === bp)
|
if (ap === bp)
|
||||||
// if progress is the same, we compare by name
|
// if progress is the same, we compare by name
|
||||||
res = $(a).find('.uk-card-title').text() > $(b).find('.uk-card-title').text();
|
res = $(a).find('.uk-card-title').text() > $(b).find('.uk-card-title').text();
|
||||||
|
|||||||
+10
-45
@@ -15,48 +15,20 @@ const toggleTheme = () => {
|
|||||||
saveTheme(newTheme);
|
saveTheme(newTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
// https://stackoverflow.com/a/28344281
|
|
||||||
const hasClass = (ele,cls) => {
|
|
||||||
return !!ele.className.match(new RegExp('(\\s|^)'+cls+'(\\s|$)'));
|
|
||||||
};
|
|
||||||
const addClass = (ele,cls) => {
|
|
||||||
if (!hasClass(ele,cls)) ele.className += " "+cls;
|
|
||||||
};
|
|
||||||
const removeClass = (ele,cls) => {
|
|
||||||
if (hasClass(ele,cls)) {
|
|
||||||
var reg = new RegExp('(\\s|^)'+cls+'(\\s|$)');
|
|
||||||
ele.className=ele.className.replace(reg,' ');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addClassToClass = (targetCls, newCls) => {
|
|
||||||
const elements = document.getElementsByClassName(targetCls);
|
|
||||||
for (let i = 0; i < elements.length; i++) {
|
|
||||||
addClass(elements[i], newCls);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeClassFromClass = (targetCls, newCls) => {
|
|
||||||
const elements = document.getElementsByClassName(targetCls);
|
|
||||||
for (let i = 0; i < elements.length; i++) {
|
|
||||||
removeClass(elements[i], newCls);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setTheme = themeStr => {
|
const setTheme = themeStr => {
|
||||||
if (themeStr === 'dark') {
|
if (themeStr === 'dark') {
|
||||||
document.getElementsByTagName('html')[0].style.background = 'rgb(20, 20, 20)';
|
$('html').css('background', 'rgb(20, 20, 20)');
|
||||||
addClass(document.getElementsByTagName('body')[0], 'uk-light');
|
$('body').addClass('uk-light');
|
||||||
addClassToClass('uk-card', 'uk-card-secondary');
|
$('.uk-card').addClass('uk-card-secondary');
|
||||||
removeClassFromClass('uk-card', 'uk-card-default');
|
$('.uk-card').removeClass('uk-card-default');
|
||||||
addClassToClass('ui-widget-content', 'dark');
|
$('.ui-widget-content').addClass('dark');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
document.getElementsByTagName('html')[0].style.background = '';
|
$('html').css('background', '');
|
||||||
removeClass(document.getElementsByTagName('body')[0], 'uk-light');
|
$('body').removeClass('uk-light');
|
||||||
removeClassFromClass('uk-card', 'uk-card-secondary');
|
$('.uk-card').removeClass('uk-card-secondary');
|
||||||
addClassToClass('uk-card', 'uk-card-default');
|
$('.uk-card').addClass('uk-card-default');
|
||||||
removeClassFromClass('ui-widget-content', 'dark');
|
$('.ui-widget-content').removeClass('dark');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -69,10 +41,3 @@ const styleModal = () => {
|
|||||||
|
|
||||||
// do it before document is ready to prevent the initial flash of white
|
// do it before document is ready to prevent the initial flash of white
|
||||||
setTheme(getTheme());
|
setTheme(getTheme());
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// because this script is attached at the top of HTML, the style on uk-card
|
|
||||||
// won't be applied because the elements are not available yet. We have to
|
|
||||||
// apply the theme again for it to take effect
|
|
||||||
setTheme(getTheme());
|
|
||||||
}, false);
|
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ function showModal(encodedPath, pages, percentage, encodedeTitle, encodedEntryTi
|
|||||||
if (percentage === 100) {
|
if (percentage === 100) {
|
||||||
$('#read-btn').attr('hidden', '');
|
$('#read-btn').attr('hidden', '');
|
||||||
}
|
}
|
||||||
$('#modal-title').text(title);
|
$('#modal-title').text(entry);
|
||||||
$('#path-text').text(zipPath);
|
$('#path-text').text(zipPath);
|
||||||
$('#pages-text').text(pages + ' pages');
|
$('#pages-text').text(pages + ' pages');
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: mango
|
name: mango
|
||||||
version: 0.1.0
|
version: 0.2.4
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Alex Ling <hkalexling@gmail.com>
|
- Alex Ling <hkalexling@gmail.com>
|
||||||
|
|||||||
@@ -25,4 +25,12 @@ describe "compare_alphanumerically" do
|
|||||||
compare_alphanumerically a, b
|
compare_alphanumerically a, b
|
||||||
}.should eq ary
|
}.should eq ary
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# https://github.com/hkalexling/Mango/issues/22
|
||||||
|
it "handles numbers larger than Int32" do
|
||||||
|
ary = ["14410155591588.jpg", "21410155591588.png", "104410155591588.jpg"]
|
||||||
|
ary.reverse.sort {|a, b|
|
||||||
|
compare_alphanumerically a, b
|
||||||
|
}.should eq ary
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
+129
-29
@@ -15,10 +15,10 @@ struct Image
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Entry
|
class Entry
|
||||||
JSON.mapping zip_path: String, book_title: String, title: String,
|
property zip_path : String, book_title : String, title : String,
|
||||||
size: String, pages: Int32, cover_url: String, id: String,
|
size : String, pages : Int32, cover_url : String, id : String,
|
||||||
title_id: String, encoded_path: String, encoded_title: String,
|
title_id : String, encoded_path : String, encoded_title : String,
|
||||||
mtime: Time
|
mtime : Time
|
||||||
|
|
||||||
def initialize(path, @book_title, @title_id, storage)
|
def initialize(path, @book_title, @title_id, storage)
|
||||||
@zip_path = path
|
@zip_path = path
|
||||||
@@ -38,6 +38,19 @@ class Entry
|
|||||||
@cover_url = "/api/page/#{@title_id}/#{@id}/1"
|
@cover_url = "/api/page/#{@title_id}/#{@id}/1"
|
||||||
@mtime = File.info(@zip_path).modification_time
|
@mtime = File.info(@zip_path).modification_time
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_json(json : JSON::Builder)
|
||||||
|
json.object do
|
||||||
|
{% for str in ["zip_path", "book_title", "title", "size",
|
||||||
|
"cover_url", "id", "title_id", "encoded_path",
|
||||||
|
"encoded_title"] %}
|
||||||
|
json.field {{str}}, @{{str.id}}
|
||||||
|
{% end %}
|
||||||
|
json.field "pages" {json.number @pages}
|
||||||
|
json.field "mtime" {json.number @mtime.to_unix}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def read_page(page_num)
|
def read_page(page_num)
|
||||||
Zip::File.open @zip_path do |file|
|
Zip::File.open @zip_path do |file|
|
||||||
page = file.entries
|
page = file.entries
|
||||||
@@ -63,27 +76,94 @@ class Entry
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Title
|
class Title
|
||||||
JSON.mapping dir: String, entries: Array(Entry), title: String,
|
property dir : String, parent_id : String, title_ids : Array(String),
|
||||||
id: String, encoded_title: String, mtime: Time, logger: MLogger
|
entries : Array(Entry), title : String, id : String,
|
||||||
|
encoded_title : String, mtime : Time
|
||||||
|
|
||||||
def initialize(dir : String, storage, @logger : MLogger)
|
def initialize(dir : String, @parent_id, storage,
|
||||||
|
@logger : MLogger, @library : Library)
|
||||||
@dir = dir
|
@dir = dir
|
||||||
@id = storage.get_id @dir, true
|
@id = storage.get_id @dir, true
|
||||||
@title = File.basename dir
|
@title = File.basename dir
|
||||||
@encoded_title = URI.encode @title
|
@encoded_title = URI.encode @title
|
||||||
@entries = (Dir.entries dir)
|
@title_ids = [] of String
|
||||||
.select { |path| [".zip", ".cbz"].includes? File.extname path }
|
@entries = [] of Entry
|
||||||
.map { |path| File.join dir, path }
|
|
||||||
.select { |path| valid_zip path }
|
Dir.entries(dir).each do |fn|
|
||||||
.map { |path|
|
next if fn.starts_with? "."
|
||||||
Entry.new path, @title, @id, storage
|
path = File.join dir, fn
|
||||||
}
|
if File.directory? path
|
||||||
.select { |e| e.pages > 0 }
|
title = Title.new path, @id, storage, @logger, library
|
||||||
.sort { |a, b| a.title <=> b.title }
|
next if title.entries.size == 0 && title.titles.size == 0
|
||||||
|
@library.title_hash[title.id] = title
|
||||||
|
@title_ids << title.id
|
||||||
|
next
|
||||||
|
end
|
||||||
|
if [".zip", ".cbz"].includes? File.extname path
|
||||||
|
next if !valid_zip path
|
||||||
|
entry = Entry.new path, @title, @id, storage
|
||||||
|
@entries << entry if entry.pages > 0
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
@title_ids.sort! do |a, b|
|
||||||
|
compare_alphanumerically @library.title_hash[a].title,
|
||||||
|
@library.title_hash[b].title
|
||||||
|
end
|
||||||
|
@entries.sort! do |a, b|
|
||||||
|
compare_alphanumerically a.title, b.title
|
||||||
|
end
|
||||||
|
|
||||||
mtimes = [File.info(dir).modification_time]
|
mtimes = [File.info(dir).modification_time]
|
||||||
|
mtimes += @title_ids.map{|e| @library.title_hash[e].mtime}
|
||||||
mtimes += @entries.map{|e| e.mtime}
|
mtimes += @entries.map{|e| e.mtime}
|
||||||
@mtime = mtimes.max
|
@mtime = mtimes.max
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def to_json(json : JSON::Builder)
|
||||||
|
json.object do
|
||||||
|
{% for str in ["dir", "title", "id", "encoded_title"] %}
|
||||||
|
json.field {{str}}, @{{str.id}}
|
||||||
|
{% end %}
|
||||||
|
json.field "mtime" {json.number @mtime.to_unix}
|
||||||
|
json.field "titles" do
|
||||||
|
json.raw self.titles.to_json
|
||||||
|
end
|
||||||
|
json.field "entries" do
|
||||||
|
json.raw @entries.to_json
|
||||||
|
end
|
||||||
|
json.field "parents" do
|
||||||
|
json.array do
|
||||||
|
self.parents.each do |title|
|
||||||
|
json.object do
|
||||||
|
json.field "title", title.title
|
||||||
|
json.field "id", title.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def titles
|
||||||
|
@title_ids.map {|tid| @library.get_title! tid}
|
||||||
|
end
|
||||||
|
|
||||||
|
def parents
|
||||||
|
ary = [] of Title
|
||||||
|
tid = @parent_id
|
||||||
|
while !tid.empty?
|
||||||
|
title = @library.get_title! tid
|
||||||
|
ary << title
|
||||||
|
tid = title.parent_id
|
||||||
|
end
|
||||||
|
ary
|
||||||
|
end
|
||||||
|
|
||||||
|
def size
|
||||||
|
@entries.size + @title_ids.size
|
||||||
|
end
|
||||||
|
|
||||||
# When downloading from MangaDex, the zip/cbz file would not be valid
|
# When downloading from MangaDex, the zip/cbz file would not be valid
|
||||||
# before the download is completed. If we scan the zip file,
|
# before the download is completed. If we scan the zip file,
|
||||||
# Entry.new would throw, so we use this method to check before
|
# Entry.new would throw, so we use this method to check before
|
||||||
@@ -128,10 +208,11 @@ class Title
|
|||||||
info = TitleInfo.new @dir
|
info = TitleInfo.new @dir
|
||||||
page = load_progress username, entry
|
page = load_progress username, entry
|
||||||
entry_obj = @entries.find{|e| e.title == entry}
|
entry_obj = @entries.find{|e| e.title == entry}
|
||||||
return 0 if entry_obj.nil?
|
return 0.0 if entry_obj.nil?
|
||||||
page / entry_obj.pages
|
page / entry_obj.pages
|
||||||
end
|
end
|
||||||
def load_percetage(username)
|
def load_percetage(username)
|
||||||
|
return 0.0 if @entries.empty?
|
||||||
read_pages = total_pages = 0
|
read_pages = total_pages = 0
|
||||||
@entries.each do |e|
|
@entries.each do |e|
|
||||||
read_pages += load_progress username, e.title
|
read_pages += load_progress username, e.title
|
||||||
@@ -150,10 +231,7 @@ class TitleInfo
|
|||||||
# { user1: { entry1: 10, entry2: 0 } }
|
# { user1: { entry1: 10, entry2: 0 } }
|
||||||
include JSON::Serializable
|
include JSON::Serializable
|
||||||
|
|
||||||
@[JSON::Field(key: "comment")]
|
|
||||||
property comment = "Generated by Mango. DO NOT EDIT!"
|
property comment = "Generated by Mango. DO NOT EDIT!"
|
||||||
|
|
||||||
@[JSON::Field(key: "progress")]
|
|
||||||
property progress : Hash(String, Hash(String, Int32))
|
property progress : Hash(String, Hash(String, Int32))
|
||||||
|
|
||||||
def initialize(title_dir)
|
def initialize(title_dir)
|
||||||
@@ -175,13 +253,14 @@ class TitleInfo
|
|||||||
end
|
end
|
||||||
|
|
||||||
class Library
|
class Library
|
||||||
JSON.mapping dir: String, titles: Array(Title), scan_interval: Int32,
|
property dir : String, title_ids : Array(String), scan_interval : Int32,
|
||||||
logger: MLogger, storage: Storage
|
logger : MLogger, storage : Storage, title_hash : Hash(String, Title)
|
||||||
|
|
||||||
def initialize(@dir, @scan_interval, @logger, @storage)
|
def initialize(@dir, @scan_interval, @logger, @storage)
|
||||||
# explicitly initialize @titles to bypass the compiler check. it will
|
# explicitly initialize @titles to bypass the compiler check. it will
|
||||||
# be filled with actual Titles in the `scan` call below
|
# be filled with actual Titles in the `scan` call below
|
||||||
@titles = [] of Title
|
@title_ids = [] of String
|
||||||
|
@title_hash = {} of String => Title
|
||||||
|
|
||||||
return scan if @scan_interval < 1
|
return scan if @scan_interval < 1
|
||||||
spawn do
|
spawn do
|
||||||
@@ -189,13 +268,27 @@ class Library
|
|||||||
start = Time.local
|
start = Time.local
|
||||||
scan
|
scan
|
||||||
ms = (Time.local - start).total_milliseconds
|
ms = (Time.local - start).total_milliseconds
|
||||||
@logger.info "Scanned #{@titles.size} titles in #{ms}ms"
|
@logger.info "Scanned #{@title_ids.size} titles in #{ms}ms"
|
||||||
sleep @scan_interval * 60
|
sleep @scan_interval * 60
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
def titles
|
||||||
|
@title_ids.map {|tid| self.get_title!(tid) }
|
||||||
|
end
|
||||||
|
def to_json(json : JSON::Builder)
|
||||||
|
json.object do
|
||||||
|
json.field "dir", @dir
|
||||||
|
json.field "titles" do
|
||||||
|
json.raw self.titles.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
def get_title(tid)
|
def get_title(tid)
|
||||||
@titles.find { |t| t.id == tid }
|
@title_hash[tid]?
|
||||||
|
end
|
||||||
|
def get_title!(tid)
|
||||||
|
@title_hash[tid]
|
||||||
end
|
end
|
||||||
def scan
|
def scan
|
||||||
unless Dir.exists? @dir
|
unless Dir.exists? @dir
|
||||||
@@ -203,11 +296,18 @@ class Library
|
|||||||
"Attempting to create it"
|
"Attempting to create it"
|
||||||
Dir.mkdir_p @dir
|
Dir.mkdir_p @dir
|
||||||
end
|
end
|
||||||
@titles = (Dir.entries @dir)
|
@title_ids.clear
|
||||||
.select { |path| File.directory? File.join @dir, path }
|
(Dir.entries @dir)
|
||||||
.map { |path| Title.new File.join(@dir, path), @storage, @logger }
|
.select { |fn| !fn.starts_with? "." }
|
||||||
.select { |title| !title.entries.empty? }
|
.map { |fn| File.join @dir, fn }
|
||||||
|
.select { |path| File.directory? path }
|
||||||
|
.map { |path| Title.new path, "", @storage, @logger, self }
|
||||||
|
.select { |title| !(title.entries.empty? && title.titles.empty?) }
|
||||||
.sort { |a, b| a.title <=> b.title }
|
.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
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -249,6 +249,7 @@ module MangaDex
|
|||||||
|
|
||||||
class Downloader
|
class Downloader
|
||||||
property stopped = false
|
property stopped = false
|
||||||
|
@downloading = false
|
||||||
|
|
||||||
def initialize(@queue : Queue, @api : API, @library_path : String,
|
def initialize(@queue : Queue, @api : API, @library_path : String,
|
||||||
@wait_seconds : Int32, @retries : Int32,
|
@wait_seconds : Int32, @retries : Int32,
|
||||||
@@ -258,7 +259,7 @@ module MangaDex
|
|||||||
spawn do
|
spawn do
|
||||||
loop do
|
loop do
|
||||||
sleep 1.second
|
sleep 1.second
|
||||||
next if @stopped
|
next if @stopped || @downloading
|
||||||
begin
|
begin
|
||||||
job = @queue.pop
|
job = @queue.pop
|
||||||
next if job.nil?
|
next if job.nil?
|
||||||
@@ -271,7 +272,7 @@ module MangaDex
|
|||||||
end
|
end
|
||||||
|
|
||||||
private def download(job : Job)
|
private def download(job : Job)
|
||||||
@stopped = true
|
@downloading = true
|
||||||
@queue.set_status JobStatus::Downloading, job
|
@queue.set_status JobStatus::Downloading, job
|
||||||
begin
|
begin
|
||||||
chapter = @api.get_chapter(job.id)
|
chapter = @api.get_chapter(job.id)
|
||||||
@@ -281,7 +282,7 @@ module MangaDex
|
|||||||
unless e.message.nil?
|
unless e.message.nil?
|
||||||
@queue.add_message e.message.not_nil!, job
|
@queue.add_message e.message.not_nil!, job
|
||||||
end
|
end
|
||||||
@stopped = false
|
@downloading = false
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@queue.set_pages chapter.pages.size, job
|
@queue.set_pages chapter.pages.size, job
|
||||||
@@ -346,7 +347,7 @@ module MangaDex
|
|||||||
else
|
else
|
||||||
@queue.set_status JobStatus::MissingPages, job
|
@queue.set_status JobStatus::MissingPages, job
|
||||||
end
|
end
|
||||||
@stopped = false
|
@downloading = false
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ require "./context"
|
|||||||
require "./mangadex/*"
|
require "./mangadex/*"
|
||||||
require "option_parser"
|
require "option_parser"
|
||||||
|
|
||||||
VERSION = "0.2.0"
|
VERSION = "0.2.4"
|
||||||
|
|
||||||
config_path = nil
|
config_path = nil
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ class APIRouter < Router
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/api/book/:title" do |env|
|
get "/api/book/:tid" do |env|
|
||||||
begin
|
begin
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
title = @context.library.get_title tid
|
title = @context.library.get_title tid
|
||||||
|
|||||||
@@ -26,12 +26,14 @@ class MainRouter < Router
|
|||||||
.not_nil!
|
.not_nil!
|
||||||
|
|
||||||
cookie = HTTP::Cookie.new "token", token
|
cookie = HTTP::Cookie.new "token", token
|
||||||
|
cookie.expires = Time.local.shift years: 1
|
||||||
env.response.cookies << cookie
|
env.response.cookies << cookie
|
||||||
env.redirect "/"
|
env.redirect "/"
|
||||||
rescue
|
rescue
|
||||||
env.redirect "/login"
|
env.redirect "/login"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/" do |env|
|
get "/" do |env|
|
||||||
titles = @context.library.titles
|
titles = @context.library.titles
|
||||||
username = get_username env
|
username = get_username env
|
||||||
|
|||||||
+3
-1
@@ -1,3 +1,5 @@
|
|||||||
|
require "big"
|
||||||
|
|
||||||
IMGS_PER_PAGE = 5
|
IMGS_PER_PAGE = 5
|
||||||
|
|
||||||
macro layout(name)
|
macro layout(name)
|
||||||
@@ -56,7 +58,7 @@ def compare_alphanumerically(c, d)
|
|||||||
return -1 if a.nil?
|
return -1 if a.nil?
|
||||||
return 1 if b.nil?
|
return 1 if b.nil?
|
||||||
if is_numeric(a) && is_numeric(b)
|
if is_numeric(a) && is_numeric(b)
|
||||||
compare = a.to_i <=> b.to_i
|
compare = a.to_big_i <=> b.to_big_i
|
||||||
return compare if compare != 0
|
return compare if compare != 0
|
||||||
else
|
else
|
||||||
compare = a <=> b
|
compare = a <=> b
|
||||||
|
|||||||
+11
-3
@@ -26,12 +26,18 @@
|
|||||||
<a class="acard" href="/book/<%= t.id %>">
|
<a class="acard" href="/book/<%= t.id %>">
|
||||||
<div class="uk-card uk-card-default">
|
<div class="uk-card uk-card-default">
|
||||||
<div class="uk-card-media-top">
|
<div class="uk-card-media-top">
|
||||||
<img src="<%= t.entries[0].cover_url %>" alt="">
|
<%- if t.entries.size > 0 -%>
|
||||||
|
<img data-src="<%= t.entries[0].cover_url %>" data-width data-height alt="" uk-img>
|
||||||
|
<%- else -%>
|
||||||
|
<img data-src="/img/icon.png" data-width data-height alt="" uk-img>
|
||||||
|
<%- end -%>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-card-body">
|
<div class="uk-card-body">
|
||||||
|
<%- if t.entries.size > 0 -%>
|
||||||
<div class="uk-card-badge uk-label"><%= (percentage[i] * 100).round(1) %>%</div>
|
<div class="uk-card-badge uk-label"><%= (percentage[i] * 100).round(1) %>%</div>
|
||||||
<h3 class="uk-card-title"><%= t.title %></h3>
|
<%- end -%>
|
||||||
<p><%= t.entries.size %> entries</p>
|
<h3 class="uk-card-title break-word" data-title="<%= t.title.gsub("\"", """) %>"><%= t.title %></h3>
|
||||||
|
<p><%= t.size %> entries</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -40,6 +46,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% 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/search.js"></script>
|
||||||
<script src="/js/sort-items.js"></script>
|
<script src="/js/sort-items.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -10,10 +10,11 @@
|
|||||||
<link rel="stylesheet" href="/css/mango.css" />
|
<link rel="stylesheet" href="/css/mango.css" />
|
||||||
<script defer src="/js/fontawesome.min.js"></script>
|
<script defer src="/js/fontawesome.min.js"></script>
|
||||||
<script defer src="/js/solid.min.js"></script>
|
<script defer src="/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>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<script src="/js/theme.js"></script>
|
|
||||||
<div class="uk-offcanvas-content">
|
<div class="uk-offcanvas-content">
|
||||||
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
|
<div class="uk-navbar-container uk-navbar-transparent" uk-navbar="uk-navbar">
|
||||||
<div id="mobile-nav" uk-offcanvas="overlay: true">
|
<div id="mobile-nav" uk-offcanvas="overlay: true">
|
||||||
@@ -59,7 +60,9 @@
|
|||||||
<%= content %>
|
<%= content %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
|
<script>
|
||||||
|
setTheme(getTheme());
|
||||||
|
</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.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/uikit@3.3.1/dist/js/uikit-icons.min.js"></script>
|
||||||
|
|
||||||
|
|||||||
+34
-5
@@ -1,5 +1,12 @@
|
|||||||
<h2 class=uk-title><%= title.title %></h2>
|
<h2 class=uk-title><%= title.title %></h2>
|
||||||
<p class="uk-text-meta"><%= title.entries.size %> entries found</p>
|
<ul class="uk-breadcrumb">
|
||||||
|
<li><a href="/">Library</a></li>
|
||||||
|
<%- title.parents.each do |t| -%>
|
||||||
|
<li><a href="/book/<%= t.id %>"><%= t.title %></a></li>
|
||||||
|
<%- end -%>
|
||||||
|
<li class="uk-disabled"><a><%= title.title %></a></li>
|
||||||
|
</ul>
|
||||||
|
<p class="uk-text-meta"><%= title.size %> entries found</p>
|
||||||
<div class="uk-grid-small" uk-grid>
|
<div class="uk-grid-small" uk-grid>
|
||||||
<div class="uk-margin-bottom uk-width-3-4@s">
|
<div class="uk-margin-bottom uk-width-3-4@s">
|
||||||
<form class="uk-search uk-search-default">
|
<form class="uk-search uk-search-default">
|
||||||
@@ -23,16 +30,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="item-container" class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
<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 %>">
|
||||||
|
<div class="uk-card uk-card-default">
|
||||||
|
<div class="uk-card-media-top">
|
||||||
|
<%- if t.entries.size > 0 -%>
|
||||||
|
<img data-src="<%= t.entries[0].cover_url %>" data-width data-height alt="" uk-img>
|
||||||
|
<%- else -%>
|
||||||
|
<img data-src="/img/icon.png" data-width data-height alt="" uk-img>
|
||||||
|
<%- end -%>
|
||||||
|
</div>
|
||||||
|
<div class="uk-card-body">
|
||||||
|
<h3 class="uk-card-title break-word" data-title="<%= t.title.gsub("\"", """) %>"><%= t.title %></h3>
|
||||||
|
<p><%= t.size %> entries</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<%- end -%>
|
||||||
<%- title.entries.each_with_index do |e, i| -%>
|
<%- title.entries.each_with_index do |e, i| -%>
|
||||||
<div class="item" data-mtime="<%= e.mtime.to_unix %>" data-progress="<%= percentage[i] %>">
|
<div class="item" data-mtime="<%= e.mtime.to_unix %>" data-progress="<%= percentage[i] %>">
|
||||||
<a class="acard">
|
<a class="acard">
|
||||||
<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 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">
|
<div class="uk-card-media-top">
|
||||||
<img src="<%= e.cover_url %>" alt="">
|
<img data-src="<%= e.cover_url %>" alt="" data-width data-height uk-img>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-card-body">
|
<div class="uk-card-body">
|
||||||
<div class="uk-card-badge uk-label"><%= (percentage[i] * 100).round(1) %>%</div>
|
<div class="uk-card-badge uk-label"><%= (percentage[i] * 100).round(1) %>%</div>
|
||||||
<h3 class="uk-card-title"><%= e.title %></h3>
|
<h3 class="uk-card-title break-word" data-title="<%= e.title.gsub("\"", """) %>"><%= e.title %></h3>
|
||||||
|
<p><%= e.pages %> pages</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -44,8 +71,8 @@
|
|||||||
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
||||||
<button class="uk-modal-close-default" type="button" uk-close></button>
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
<div class="uk-modal-header">
|
<div class="uk-modal-header">
|
||||||
<h3 class="uk-modal-title" id="modal-title"></h3>
|
<h3 class="uk-modal-title break-word" id="modal-title"></h3>
|
||||||
<p class="uk-text-meta uk-margin-remove-bottom" id="path-text"></p>
|
<p class="uk-text-meta uk-margin-remove-bottom break-word" id="path-text"></p>
|
||||||
<p class="uk-text-meta uk-margin-remove-top" id="pages-text"></p>
|
<p class="uk-text-meta uk-margin-remove-top" id="pages-text"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-modal-body">
|
<div class="uk-modal-body">
|
||||||
@@ -64,6 +91,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% 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/alert.js"></script>
|
||||||
<script src="/js/title.js"></script>
|
<script src="/js/title.js"></script>
|
||||||
<script src="/js/search.js"></script>
|
<script src="/js/search.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user