mirror of
https://github.com/hkalexling/Mango.git
synced 2026-03-20 00:00:48 -04:00
See https://ux.stackexchange.com/questions/95431/how-should-sorting-work-when-numeric-is-mixed-with-alpha-numeric
212 lines
5.6 KiB
Crystal
212 lines
5.6 KiB
Crystal
require "zip"
|
|
require "mime"
|
|
require "json"
|
|
require "uri"
|
|
require "./util"
|
|
|
|
struct Image
|
|
property data : Bytes
|
|
property mime : String
|
|
property filename : String
|
|
property size : Int32
|
|
|
|
def initialize(@data, @mime, @filename, @size)
|
|
end
|
|
end
|
|
|
|
class Entry
|
|
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, @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
|
|
.select { |e|
|
|
["image/jpeg", "image/png"].includes? \
|
|
MIME.from_filename? e.filename
|
|
}
|
|
.size
|
|
file.close
|
|
@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)
|
|
Zip::File.open @zip_path do |file|
|
|
page = file.entries
|
|
.select { |e|
|
|
["image/jpeg", "image/png"].includes? \
|
|
MIME.from_filename? e.filename
|
|
}
|
|
.sort { |a, b| compare_alphanumerically(split_by_alphanumeric(a.filename), split_by_alphanumeric(b.filename)) }
|
|
.[page_num - 1]
|
|
page.open do |io|
|
|
slice = Bytes.new page.uncompressed_size
|
|
bytes_read = io.read_fully? slice
|
|
unless bytes_read
|
|
return nil
|
|
end
|
|
return Image.new slice, MIME.from_filename(page.filename),\
|
|
page.filename, bytes_read
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
class Title
|
|
JSON.mapping dir: String, entries: Array(Entry), title: String,
|
|
id: String, encoded_title: String, mtime: Time, logger: MLogger
|
|
|
|
def initialize(dir : String, storage, @logger : MLogger)
|
|
@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| File.join dir, path }
|
|
.select { |path| valid_zip path }
|
|
.map { |path|
|
|
Entry.new 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
|
|
# When downloading from MangaDex, the zip/cbz file would not be valid
|
|
# before the download is completed. If we scan the zip file,
|
|
# Entry.new would throw, so we use this method to check before
|
|
# constructing Entry
|
|
private def valid_zip(path : String)
|
|
begin
|
|
file = Zip::File.new path
|
|
file.close
|
|
return true
|
|
rescue
|
|
@logger.warn "File #{path} is corrupted or is not a valid zip "\
|
|
"archive. Ignoring it."
|
|
return false
|
|
end
|
|
end
|
|
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?
|
|
info.progress[username] = {entry => page}
|
|
info.save @dir
|
|
return
|
|
end
|
|
info.progress[username][entry] = page
|
|
info.save @dir
|
|
end
|
|
def load_progress(username, entry)
|
|
info = TitleInfo.new @dir
|
|
if info.progress[username]?.nil?
|
|
return 0
|
|
end
|
|
if info.progress[username][entry]?.nil?
|
|
return 0
|
|
end
|
|
info.progress[username][entry]
|
|
end
|
|
def load_percetage(username, entry)
|
|
info = TitleInfo.new @dir
|
|
page = load_progress username, entry
|
|
entry_obj = @entries.find{|e| e.title == entry}
|
|
return 0 if entry_obj.nil?
|
|
page / entry_obj.pages
|
|
end
|
|
def load_percetage(username)
|
|
read_pages = total_pages = 0
|
|
@entries.each do |e|
|
|
read_pages += load_progress username, e.title
|
|
total_pages += e.pages
|
|
end
|
|
read_pages / total_pages
|
|
end
|
|
def next_entry(current_entry_obj)
|
|
idx = @entries.index current_entry_obj
|
|
return nil if idx.nil? || idx == @entries.size - 1
|
|
@entries[idx + 1]
|
|
end
|
|
end
|
|
|
|
class TitleInfo
|
|
# { user1: { entry1: 10, entry2: 0 } }
|
|
include JSON::Serializable
|
|
|
|
@[JSON::Field(key: "comment")]
|
|
property comment = "Generated by Mango. DO NOT EDIT!"
|
|
|
|
@[JSON::Field(key: "progress")]
|
|
property progress : Hash(String, Hash(String, Int32))
|
|
|
|
def initialize(title_dir)
|
|
info = nil
|
|
|
|
json_path = File.join title_dir, "info.json"
|
|
if File.exists? json_path
|
|
info = TitleInfo.from_json File.read json_path
|
|
else
|
|
info = TitleInfo.from_json "{\"progress\": {}}"
|
|
end
|
|
|
|
@progress = info.progress.clone
|
|
end
|
|
def save(title_dir)
|
|
json_path = File.join title_dir, "info.json"
|
|
File.write json_path, self.to_pretty_json
|
|
end
|
|
end
|
|
|
|
class Library
|
|
JSON.mapping dir: String, titles: Array(Title), scan_interval: Int32,
|
|
logger: MLogger, storage: Storage
|
|
|
|
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
|
|
|
|
return scan if @scan_interval < 1
|
|
spawn do
|
|
loop do
|
|
start = Time.local
|
|
scan
|
|
ms = (Time.local - start).total_milliseconds
|
|
@logger.info "Scanned #{@titles.size} titles in #{ms}ms"
|
|
sleep @scan_interval * 60
|
|
end
|
|
end
|
|
end
|
|
def get_title(tid)
|
|
@titles.find { |t| t.id == 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
|
|
@titles = (Dir.entries @dir)
|
|
.select { |path| File.directory? File.join @dir, path }
|
|
.map { |path| Title.new File.join(@dir, path), @storage, @logger }
|
|
.select { |title| !title.entries.empty? }
|
|
.sort { |a, b| a.title <=> b.title }
|
|
@logger.debug "Scan completed"
|
|
end
|
|
end
|