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