mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-02 02:45:29 -04:00
242 lines
6.7 KiB
Crystal
242 lines
6.7 KiB
Crystal
class Library
|
|
getter dir : String, title_ids : Array(String),
|
|
title_hash : Hash(String, Title)
|
|
|
|
use_default
|
|
|
|
def initialize
|
|
register_mime_types
|
|
|
|
@dir = Config.current.library_path
|
|
# explicitly initialize @titles to bypass the compiler check. it will
|
|
# be filled with actual Titles in the `scan` call below
|
|
@title_ids = [] of String
|
|
@title_hash = {} of String => Title
|
|
|
|
@entries_count = 0
|
|
@thumbnails_count = 0
|
|
|
|
scan_interval = Config.current.scan_interval_minutes
|
|
if scan_interval < 1
|
|
scan
|
|
else
|
|
spawn do
|
|
loop do
|
|
start = Time.local
|
|
scan
|
|
ms = (Time.local - start).total_milliseconds
|
|
Logger.info "Scanned #{@title_ids.size} titles in #{ms}ms"
|
|
sleep scan_interval.minutes
|
|
end
|
|
end
|
|
end
|
|
|
|
thumbnail_interval = Config.current.thumbnail_generation_interval_hours
|
|
unless thumbnail_interval < 1
|
|
spawn do
|
|
loop do
|
|
# Wait for scan to complete (in most cases)
|
|
sleep 1.minutes
|
|
generate_thumbnails
|
|
sleep thumbnail_interval.hours
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def titles
|
|
@title_ids.map { |tid| self.get_title!(tid) }
|
|
end
|
|
|
|
def sorted_titles(username, opt : SortOptions? = nil)
|
|
if opt.nil?
|
|
opt = SortOptions.from_info_json @dir, username
|
|
else
|
|
TitleInfo.new @dir do |info|
|
|
info.sort_by[username] = opt.to_tuple
|
|
info.save
|
|
end
|
|
end
|
|
|
|
# Helper function from src/util/util.cr
|
|
sort_titles titles, opt.not_nil!, username
|
|
end
|
|
|
|
def deep_titles
|
|
titles + titles.map { |t| t.deep_titles }.flatten
|
|
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)
|
|
@title_hash[tid]?
|
|
end
|
|
|
|
def get_title!(tid)
|
|
@title_hash[tid]
|
|
end
|
|
|
|
def scan
|
|
unless Dir.exists? @dir
|
|
Logger.info "The library directory #{@dir} does not exist. " \
|
|
"Attempting to create it"
|
|
Dir.mkdir_p @dir
|
|
end
|
|
|
|
storage = Storage.new auto_close: false
|
|
|
|
(Dir.entries @dir)
|
|
.select { |fn| !fn.starts_with? "." }
|
|
.map { |fn| File.join @dir, fn }
|
|
.select { |path| File.directory? path }
|
|
.map { |path| Title.new path, "" }
|
|
.select { |title| !(title.entries.empty? && title.titles.empty?) }
|
|
.sort { |a, b| a.title <=> b.title }
|
|
.tap { |_| @title_ids.clear }
|
|
.each do |title|
|
|
@title_hash[title.id] = title
|
|
@title_ids << title.id
|
|
end
|
|
|
|
storage.bulk_insert_ids
|
|
storage.close
|
|
|
|
Logger.debug "Scan completed"
|
|
Storage.default.optimize
|
|
end
|
|
|
|
def get_continue_reading_entries(username)
|
|
cr_entries = deep_titles
|
|
.map { |t| t.get_last_read_entry username }
|
|
# Select elements with type `Entry` from the array and ignore all `Nil`s
|
|
.select(Entry)[0...ENTRIES_IN_HOME_SECTIONS]
|
|
.map { |e|
|
|
# Get the last read time of the entry. If it hasn't been started, get
|
|
# the last read time of the previous entry
|
|
last_read = e.load_last_read username
|
|
pe = e.previous_entry
|
|
if last_read.nil? && pe
|
|
last_read = pe.load_last_read username
|
|
end
|
|
{
|
|
entry: e,
|
|
percentage: e.load_percentage(username),
|
|
last_read: last_read,
|
|
}
|
|
}
|
|
|
|
# Sort by by last_read, most recent first (nils at the end)
|
|
cr_entries.sort { |a, b|
|
|
next 0 if a[:last_read].nil? && b[:last_read].nil?
|
|
next 1 if a[:last_read].nil?
|
|
next -1 if b[:last_read].nil?
|
|
b[:last_read].not_nil! <=> a[:last_read].not_nil!
|
|
}
|
|
end
|
|
|
|
alias RA = NamedTuple(
|
|
entry: Entry,
|
|
percentage: Float64,
|
|
grouped_count: Int32)
|
|
|
|
def get_recently_added_entries(username)
|
|
recently_added = [] of RA
|
|
last_date_added = nil
|
|
|
|
titles.map { |t| t.deep_entries_with_date_added }.flatten
|
|
.select { |e| e[:date_added] > 1.month.ago }
|
|
.sort { |a, b| b[:date_added] <=> a[:date_added] }
|
|
.each do |e|
|
|
break if recently_added.size > 12
|
|
last = recently_added.last?
|
|
if last && e[:entry].book.id == last[:entry].book.id &&
|
|
(e[:date_added] - last_date_added.not_nil!).duration < 1.day
|
|
# A NamedTuple is immutable, so we have to cast it to a Hash first
|
|
last_hash = last.to_h
|
|
count = last_hash[:grouped_count].as(Int32)
|
|
last_hash[:grouped_count] = count + 1
|
|
# Setting the percentage to a negative value will hide the
|
|
# percentage badge on the card
|
|
last_hash[:percentage] = -1.0
|
|
recently_added[recently_added.size - 1] = RA.from last_hash
|
|
else
|
|
last_date_added = e[:date_added]
|
|
recently_added << {
|
|
entry: e[:entry],
|
|
percentage: e[:entry].load_percentage(username),
|
|
grouped_count: 1,
|
|
}
|
|
end
|
|
end
|
|
|
|
recently_added[0...ENTRIES_IN_HOME_SECTIONS]
|
|
end
|
|
|
|
def get_start_reading_titles(username)
|
|
# Here we are not using `deep_titles` as it may cause unexpected behaviors
|
|
# For example, consider the following nested titles:
|
|
# - One Puch Man
|
|
# - Vol. 1
|
|
# - Vol. 2
|
|
# If we use `deep_titles`, the start reading section might include `Vol. 2`
|
|
# when the user hasn't started `Vol. 1` yet
|
|
titles
|
|
.select { |t| t.load_percentage(username) == 0 }
|
|
.sample(ENTRIES_IN_HOME_SECTIONS)
|
|
.shuffle
|
|
end
|
|
|
|
def thumbnail_generation_progress
|
|
return 0 if @entries_count == 0
|
|
@thumbnails_count / @entries_count
|
|
end
|
|
|
|
def generate_thumbnails
|
|
if @thumbnails_count > 0
|
|
Logger.debug "Thumbnail generation in progress"
|
|
return
|
|
end
|
|
|
|
Logger.info "Starting thumbnail generation"
|
|
entries = deep_titles.map(&.deep_entries).flatten.reject &.err_msg
|
|
@entries_count = entries.size
|
|
@thumbnails_count = 0
|
|
|
|
# Report generation progress regularly
|
|
spawn do
|
|
loop do
|
|
unless @thumbnails_count == 0
|
|
Logger.debug "Thumbnail generation progress: " \
|
|
"#{(thumbnail_generation_progress * 100).round 1}%"
|
|
end
|
|
# Generation is completed. We reset the count to 0 to allow subsequent
|
|
# calls to the function, and break from the loop to stop the progress
|
|
# report fiber
|
|
if thumbnail_generation_progress.to_i == 1
|
|
@thumbnails_count = 0
|
|
break
|
|
end
|
|
sleep 10.seconds
|
|
end
|
|
end
|
|
|
|
entries.each do |e|
|
|
unless e.get_thumbnail
|
|
e.generate_thumbnail
|
|
# Sleep after each generation to minimize the impact on disk IO
|
|
# and CPU
|
|
sleep 0.5.seconds
|
|
end
|
|
@thumbnails_count += 1
|
|
end
|
|
Logger.info "Thumbnail generation finished"
|
|
end
|
|
end
|