Skip to content
This repository was archived by the owner on Mar 23, 2025. It is now read-only.

Commit 8a73280

Browse files
authored
Merge pull request #232 from hkalexling/rc/0.24.0
v0.24.0
2 parents e408398 + 9df372f commit 8a73280

File tree

18 files changed

+659
-119
lines changed

18 files changed

+659
-119
lines changed

.dockerignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,9 @@
11
node_modules
22
lib
3+
Dockerfile
4+
Dockerfile.arm32v7
5+
Dockerfile.arm64v8
6+
README.md
7+
.all-contributorsrc
8+
env.example
9+
.github/

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
5151
### CLI
5252

5353
```
54-
Mango - Manga Server and Web Reader. Version 0.23.0
54+
Mango - Manga Server and Web Reader. Version 0.24.0
5555
5656
Usage:
5757
@@ -86,6 +86,10 @@ log_level: info
8686
upload_path: ~/mango/uploads
8787
plugin_path: ~/mango/plugins
8888
download_timeout_seconds: 30
89+
library_cache_path: ~/mango/library.yml.gz
90+
cache_enabled: false
91+
cache_size_mbs: 50
92+
cache_log_enabled: true
8993
disable_login: false
9094
default_username: ""
9195
auth_proxy_header_name: ""
@@ -97,12 +101,12 @@ mangadex:
97101
download_queue_db_path: ~/mango/queue.db
98102
chapter_rename_rule: '[Vol.{volume} ][Ch.{chapter} ]{title|id}'
99103
manga_rename_rule: '{title}'
100-
subscription_update_interval_hours: 24
101104
```
102105
103106
- `scan_interval_minutes`, `thumbnail_generation_interval_hours` and `db_optimization_interval_hours` can be any non-negative integer. Setting them to `0` disables the periodic tasks
104107
- `log_level` can be `debug`, `info`, `warn`, `error`, `fatal` or `off`. Setting it to `off` disables the logging
105108
- You can disable authentication by setting `disable_login` to true. Note that `default_username` must be set to an existing username for this to work.
109+
- By setting `cache_enabled` to `true`, you can enable an experimental feature where Mango caches library metadata to improve page load time. You can further fine-tune the feature with `cache_size_mbs` and `cache_log_enabled`.
106110

107111
### Library Structure
108112

shard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: mango
2-
version: 0.23.0
2+
version: 0.24.0
33

44
authors:
55
- Alex Ling <hkalexling@gmail.com>

spec/util_spec.cr

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,13 @@ describe "chapter_sort" do
6161
end.should eq ary
6262
end
6363
end
64+
65+
describe "sanitize_filename" do
66+
it "returns a random string for empty sanitized string" do
67+
sanitize_filename("..").should_not eq sanitize_filename("..")
68+
end
69+
it "sanitizes correctly" do
70+
sanitize_filename(".. \n\v.\rマンゴー/|*()<[1/2] 3.14 hello world ")
71+
.should eq "マンゴー_()[1_2] 3.14 hello world"
72+
end
73+
end

src/config.cr

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ class Config
1111
property session_secret : String = "mango-session-secret"
1212
property library_path : String = File.expand_path "~/mango/library",
1313
home: true
14+
property library_cache_path = File.expand_path "~/mango/library.yml.gz",
15+
home: true
1416
property db_path : String = File.expand_path "~/mango/mango.db", home: true
1517
property scan_interval_minutes : Int32 = 5
1618
property thumbnail_generation_interval_hours : Int32 = 24
@@ -20,6 +22,9 @@ class Config
2022
property plugin_path : String = File.expand_path "~/mango/plugins",
2123
home: true
2224
property download_timeout_seconds : Int32 = 30
25+
property cache_enabled = false
26+
property cache_size_mbs = 50
27+
property cache_log_enabled = true
2328
property disable_login = false
2429
property default_username = ""
2530
property auth_proxy_header_name = ""

src/library/cache.cr

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
require "digest"
2+
3+
require "./entry"
4+
require "./types"
5+
6+
# Base class for an entry in the LRU cache.
7+
# There are two ways to use it:
8+
# 1. Use it as it is by instantiating with the appropriate `SaveT` and
9+
# `ReturnT`. Note that in this case, `SaveT` and `ReturnT` must be the
10+
# same type. That is, the input value will be stored as it is without
11+
# any transformation.
12+
# 2. You can also subclass it and provide custom implementations for
13+
# `to_save_t` and `to_return_t`. This allows you to transform and store
14+
# the input value to a different type. See `SortedEntriesCacheEntry` as
15+
# an example.
16+
private class CacheEntry(SaveT, ReturnT)
17+
getter key : String, atime : Time
18+
19+
@value : SaveT
20+
21+
def initialize(@key : String, value : ReturnT)
22+
@atime = @ctime = Time.utc
23+
@value = self.class.to_save_t value
24+
end
25+
26+
def value
27+
@atime = Time.utc
28+
self.class.to_return_t @value
29+
end
30+
31+
def self.to_save_t(value : ReturnT)
32+
value
33+
end
34+
35+
def self.to_return_t(value : SaveT)
36+
value
37+
end
38+
39+
def instance_size
40+
instance_sizeof(CacheEntry(SaveT, ReturnT)) + # sizeof itself
41+
instance_sizeof(String) + @key.bytesize + # allocated memory for @key
42+
@value.instance_size
43+
end
44+
end
45+
46+
class SortedEntriesCacheEntry < CacheEntry(Array(String), Array(Entry))
47+
def self.to_save_t(value : Array(Entry))
48+
value.map &.id
49+
end
50+
51+
def self.to_return_t(value : Array(String))
52+
ids_to_entries value
53+
end
54+
55+
private def self.ids_to_entries(ids : Array(String))
56+
e_map = Library.default.deep_entries.to_h { |entry| {entry.id, entry} }
57+
entries = [] of Entry
58+
begin
59+
ids.each do |id|
60+
entries << e_map[id]
61+
end
62+
return entries if ids.size == entries.size
63+
rescue
64+
end
65+
end
66+
67+
def instance_size
68+
instance_sizeof(SortedEntriesCacheEntry) + # sizeof itself
69+
instance_sizeof(String) + @key.bytesize + # allocated memory for @key
70+
@value.size * (instance_sizeof(String) + sizeof(String)) +
71+
@value.sum(&.bytesize) # elements in Array(String)
72+
end
73+
74+
def self.gen_key(book_id : String, username : String,
75+
entries : Array(Entry), opt : SortOptions?)
76+
entries_sig = Digest::SHA1.hexdigest (entries.map &.id).to_s
77+
user_context = opt && opt.method == SortMethod::Progress ? username : ""
78+
sig = Digest::SHA1.hexdigest (book_id + entries_sig + user_context +
79+
(opt ? opt.to_tuple.to_s : "nil"))
80+
"#{sig}:sorted_entries"
81+
end
82+
end
83+
84+
class String
85+
def instance_size
86+
instance_sizeof(String) + bytesize
87+
end
88+
end
89+
90+
struct Tuple(*T)
91+
def instance_size
92+
sizeof(T) + # total size of non-reference types
93+
self.sum do |e|
94+
next 0 unless e.is_a? Reference
95+
if e.responds_to? :instance_size
96+
e.instance_size
97+
else
98+
instance_sizeof(typeof(e))
99+
end
100+
end
101+
end
102+
end
103+
104+
alias CacheableType = Array(Entry) | String | Tuple(String, Int32)
105+
alias CacheEntryType = SortedEntriesCacheEntry |
106+
CacheEntry(String, String) |
107+
CacheEntry(Tuple(String, Int32), Tuple(String, Int32))
108+
109+
def generate_cache_entry(key : String, value : CacheableType)
110+
if value.is_a? Array(Entry)
111+
SortedEntriesCacheEntry.new key, value
112+
else
113+
CacheEntry(typeof(value), typeof(value)).new key, value
114+
end
115+
end
116+
117+
# LRU Cache
118+
class LRUCache
119+
@@limit : Int128 = Int128.new 0
120+
@@should_log = true
121+
# key => entry
122+
@@cache = {} of String => CacheEntryType
123+
124+
def self.enabled
125+
Config.current.cache_enabled
126+
end
127+
128+
def self.init
129+
cache_size = Config.current.cache_size_mbs
130+
@@limit = Int128.new cache_size * 1024 * 1024 if enabled
131+
@@should_log = Config.current.cache_log_enabled
132+
end
133+
134+
def self.get(key : String)
135+
return unless enabled
136+
entry = @@cache[key]?
137+
if @@should_log
138+
Logger.debug "LRUCache #{entry.nil? ? "miss" : "hit"} #{key}"
139+
end
140+
return entry.value unless entry.nil?
141+
end
142+
143+
def self.set(cache_entry : CacheEntryType)
144+
return unless enabled
145+
key = cache_entry.key
146+
@@cache[key] = cache_entry
147+
Logger.debug "LRUCache cached #{key}" if @@should_log
148+
remove_least_recent_access
149+
end
150+
151+
def self.invalidate(key : String)
152+
return unless enabled
153+
@@cache.delete key
154+
end
155+
156+
def self.print
157+
return unless @@should_log
158+
sum = @@cache.sum { |_, entry| entry.instance_size }
159+
Logger.debug "---- LRU Cache ----"
160+
Logger.debug "Size: #{sum} Bytes"
161+
Logger.debug "List:"
162+
@@cache.each do |k, v|
163+
Logger.debug "#{k} | #{v.atime} | #{v.instance_size}"
164+
end
165+
Logger.debug "-------------------"
166+
end
167+
168+
private def self.is_cache_full
169+
sum = @@cache.sum { |_, entry| entry.instance_size }
170+
sum > @@limit
171+
end
172+
173+
private def self.remove_least_recent_access
174+
if @@should_log && is_cache_full
175+
Logger.debug "Removing entries from LRUCache"
176+
end
177+
while is_cache_full && @@cache.size > 0
178+
min_tuple = @@cache.min_by { |_, entry| entry.atime }
179+
min_key = min_tuple[0]
180+
min_entry = min_tuple[1]
181+
182+
Logger.debug " \
183+
Target: #{min_key}, \
184+
Last Access Time: #{min_entry.atime}" if @@should_log
185+
invalidate min_key
186+
end
187+
end
188+
end

src/library/entry.cr

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
require "image_size"
2+
require "yaml"
23

34
class Entry
5+
include YAML::Serializable
6+
47
getter zip_path : String, book : Title, title : String,
58
size : String, pages : Int32, id : String, encoded_path : String,
69
encoded_title : String, mtime : Time, err_msg : String?
@@ -46,31 +49,23 @@ class Entry
4649
file.close
4750
end
4851

49-
def to_slim_json : String
52+
def build_json(*, slim = false)
5053
JSON.build do |json|
5154
json.object do
5255
{% for str in ["zip_path", "title", "size", "id"] %}
5356
json.field {{str}}, @{{str.id}}
5457
{% end %}
5558
json.field "title_id", @book.id
5659
json.field "pages" { json.number @pages }
60+
unless slim
61+
json.field "display_name", @book.display_name @title
62+
json.field "cover_url", cover_url
63+
json.field "mtime" { json.number @mtime.to_unix }
64+
end
5765
end
5866
end
5967
end
6068

61-
def to_json(json : JSON::Builder)
62-
json.object do
63-
{% for str in ["zip_path", "title", "size", "id"] %}
64-
json.field {{str}}, @{{str.id}}
65-
{% end %}
66-
json.field "title_id", @book.id
67-
json.field "display_name", @book.display_name @title
68-
json.field "cover_url", cover_url
69-
json.field "pages" { json.number @pages }
70-
json.field "mtime" { json.number @mtime.to_unix }
71-
end
72-
end
73-
7469
def display_name
7570
@book.display_name @title
7671
end
@@ -81,9 +76,17 @@ class Entry
8176

8277
def cover_url
8378
return "#{Config.current.base_url}img/icon.png" if @err_msg
79+
80+
unless @book.entry_cover_url_cache
81+
TitleInfo.new @book.dir do |info|
82+
@book.entry_cover_url_cache = info.entry_cover_url
83+
end
84+
end
85+
entry_cover_url = @book.entry_cover_url_cache
86+
8487
url = "#{Config.current.base_url}api/cover/#{@book.id}/#{@id}"
85-
TitleInfo.new @book.dir do |info|
86-
info_url = info.entry_cover_url[@title]?
88+
if entry_cover_url
89+
info_url = entry_cover_url[@title]?
8790
unless info_url.nil? || info_url.empty?
8891
url = File.join Config.current.base_url, info_url
8992
end
@@ -170,6 +173,16 @@ class Entry
170173
# For backward backward compatibility with v0.1.0, we save entry titles
171174
# instead of IDs in info.json
172175
def save_progress(username, page)
176+
LRUCache.invalidate "#{@book.id}:#{username}:progress_sum"
177+
@book.parents.each do |parent|
178+
LRUCache.invalidate "#{parent.id}:#{username}:progress_sum"
179+
end
180+
[false, true].each do |ascend|
181+
sorted_entries_cache_key = SortedEntriesCacheEntry.gen_key @book.id,
182+
username, @book.entries, SortOptions.new(SortMethod::Progress, ascend)
183+
LRUCache.invalidate sorted_entries_cache_key
184+
end
185+
173186
TitleInfo.new @book.dir do |info|
174187
if info.progress[username]?.nil?
175188
info.progress[username] = {@title => page}

0 commit comments

Comments
 (0)