diff --git a/src/config.cr b/src/config.cr index a1eebf0..98d72a5 100644 --- a/src/config.cr +++ b/src/config.cr @@ -26,6 +26,16 @@ class Config "manga_rename_rule" => "{title}", } + @@singlet : Config? + + def self.current + @@singlet.not_nil! + end + + def set_current + @@singlet = self + end + def self.load(path : String?) path = "~/.config/mango/config.yml" if path.nil? cfg_path = File.expand_path path, home: true diff --git a/src/context.cr b/src/context.cr deleted file mode 100644 index a4e4d4b..0000000 --- a/src/context.cr +++ /dev/null @@ -1,21 +0,0 @@ -require "./config" -require "./library" -require "./storage" -require "./logger" - -class Context - property config : Config - property library : Library - property storage : Storage - property logger : Logger - property queue : MangaDex::Queue - - def initialize(@config, @logger, @library, @storage, @queue) - end - - {% for lvl in Logger::LEVELS %} - def {{lvl.id}}(msg) - @logger.{{lvl.id}} msg - end - {% end %} -end diff --git a/src/handlers/log_handler.cr b/src/handlers/log_handler.cr index 105c9f1..2363ce6 100644 --- a/src/handlers/log_handler.cr +++ b/src/handlers/log_handler.cr @@ -2,20 +2,17 @@ require "kemal" require "../logger" class LogHandler < Kemal::BaseLogHandler - def initialize(@logger : Logger) - end - def call(env) elapsed_time = Time.measure { call_next env } elapsed_text = elapsed_text elapsed_time msg = "#{env.response.status_code} #{env.request.method}" \ " #{env.request.resource} #{elapsed_text}" - @logger.debug msg + Logger.debug msg env end def write(msg) - @logger.debug msg + Logger.debug msg end private def elapsed_text(elapsed) diff --git a/src/library.cr b/src/library.cr index 058b04a..58cc86b 100644 --- a/src/library.cr +++ b/src/library.cr @@ -97,7 +97,7 @@ class Title encoded_title : String, mtime : Time def initialize(@dir : String, @parent_id, storage, - @logger : Logger, @library : Library) + @library : Library) @id = storage.get_id @dir, true @title = File.basename dir @encoded_title = URI.encode @title @@ -109,7 +109,7 @@ class Title next if fn.starts_with? "." path = File.join dir, fn if File.directory? path - title = Title.new path, @id, storage, @logger, library + title = Title.new path, @id, storage, library next if title.entries.size == 0 && title.titles.size == 0 @library.title_hash[title.id] = title @title_ids << title.id @@ -118,9 +118,9 @@ class Title if [".zip", ".cbz"].includes? File.extname path zip_exception = validate_zip path unless zip_exception.nil? - @logger.warn "File #{path} is corrupted or is not a valid zip " \ - "archive. Ignoring it." - @logger.debug "Zip error: #{zip_exception}" + Logger.warn "File #{path} is corrupted or is not a valid zip " \ + "archive. Ignoring it." + Logger.debug "Zip error: #{zip_exception}" next end entry = Entry.new path, self, @id, storage @@ -367,9 +367,19 @@ end class Library property dir : String, title_ids : Array(String), scan_interval : Int32, - logger : Logger, storage : Storage, title_hash : Hash(String, Title) + storage : Storage, title_hash : Hash(String, Title) - def initialize(@dir, @scan_interval, @logger, @storage) + def self.default + unless @@default + @@default = new + end + @@default.not_nil! + end + + def initialize + @storage = Storage.default + @dir = Config.current.library_path + @scan_interval = Config.current.scan_interval # explicitly initialize @titles to bypass the compiler check. it will # be filled with actual Titles in the `scan` call below @title_ids = [] of String @@ -381,7 +391,7 @@ class Library start = Time.local scan ms = (Time.local - start).total_milliseconds - @logger.info "Scanned #{@title_ids.size} titles in #{ms}ms" + Logger.info "Scanned #{@title_ids.size} titles in #{ms}ms" sleep @scan_interval * 60 end end @@ -410,8 +420,8 @@ class Library def scan unless Dir.exists? @dir - @logger.info "The library directory #{@dir} does not exist. " \ - "Attempting to create it" + Logger.info "The library directory #{@dir} does not exist. " \ + "Attempting to create it" Dir.mkdir_p @dir end @title_ids.clear @@ -419,13 +429,13 @@ class Library .select { |fn| !fn.starts_with? "." } .map { |fn| File.join @dir, fn } .select { |path| File.directory? path } - .map { |path| Title.new path, "", @storage, @logger, self } + .map { |path| Title.new path, "", @storage, self } .select { |title| !(title.entries.empty? && title.titles.empty?) } .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 diff --git a/src/logger.cr b/src/logger.cr index 6a7536a..b54d495 100644 --- a/src/logger.cr +++ b/src/logger.cr @@ -8,7 +8,15 @@ class Logger @@severity : Log::Severity = :info - def initialize(level : String) + def self.default + unless @@default + @@default = new + end + @@default.not_nil! + end + + def initialize + level = Config.current.log_level {% begin %} case level.downcase when "off" @@ -50,9 +58,16 @@ class Logger @backend.write Log::Entry.new "", Log::Severity::None, msg, nil end + def self.log(msg) + default.log msg + end + {% for lvl in LEVELS %} def {{lvl.id}}(msg) @log.{{lvl.id}} { msg } end + def self.{{lvl.id}}(msg) + default.not_nil!.{{lvl.id}} msg + end {% end %} end diff --git a/src/mangadex/api.cr b/src/mangadex/api.cr index 225a94a..8161ac5 100644 --- a/src/mangadex/api.cr +++ b/src/mangadex/api.cr @@ -133,7 +133,15 @@ module MangaDex end class API - def initialize(@base_url = "https://mangadex.org/api/") + def self.default + unless @@default + @@default = new + end + @@default.not_nil! + end + + def initialize + @base_url = Config.current.mangadex["api_url"].to_s || "https://mangadex.org/api/" @lang = {} of String => String CSV.each_row {{read_file "src/assets/lang_codes.csv"}} do |row| @lang[row[1]] = row[0] diff --git a/src/mangadex/downloader.cr b/src/mangadex/downloader.cr index 2561f6b..cb74fa8 100644 --- a/src/mangadex/downloader.cr +++ b/src/mangadex/downloader.cr @@ -1,5 +1,6 @@ require "./api" require "sqlite3" +require "zip" module MangaDex class PageJob @@ -79,12 +80,20 @@ module MangaDex class Queue property downloader : Downloader? + @path : String = Config.current.mangadex["download_queue_db_path"].to_s - def initialize(@path : String, @logger : Logger) - dir = File.dirname path + def self.default + unless @@default + @@default = new + end + @@default.not_nil! + end + + def initialize + dir = File.dirname @path unless Dir.exists? dir - @logger.info "The queue DB directory #{dir} does not exist. " \ - "Attepmting to create it" + Logger.info "The queue DB directory #{dir} does not exist. " \ + "Attepmting to create it" Dir.mkdir_p dir end DB.open "sqlite3://#{@path}" do |db| @@ -101,7 +110,7 @@ module MangaDex db.exec "create index if not exists status_idx " \ "on queue (status)" rescue e - @logger.error "Error when checking tables in DB: #{e}" + Logger.error "Error when checking tables in DB: #{e}" raise e end end @@ -254,11 +263,22 @@ module MangaDex class Downloader property stopped = false + @wait_seconds : Int32 = Config.current.mangadex["download_wait_seconds"] + .to_i32 + @retries : Int32 = Config.current.mangadex["download_retries"].to_i32 + @library_path : String = Config.current.library_path @downloading = false - def initialize(@queue : Queue, @api : API, @library_path : String, - @wait_seconds : Int32, @retries : Int32, - @logger : Logger) + def self.default + unless @@default + @@default = new + end + @@default.not_nil! + end + + def initialize + @queue = Queue.default + @api = API.default @queue.downloader = self spawn do @@ -270,7 +290,7 @@ module MangaDex next if job.nil? download job rescue e - @logger.error e + Logger.error e end end end @@ -282,7 +302,7 @@ module MangaDex begin chapter = @api.get_chapter(job.id) rescue e - @logger.error e + Logger.error e @queue.set_status JobStatus::Error, job unless e.message.nil? @queue.add_message e.message.not_nil!, job @@ -310,16 +330,16 @@ module MangaDex ext = File.extname fn fn = "#{i.to_s.rjust len, '0'}#{ext}" page_job = PageJob.new url, fn, writer, @retries - @logger.debug "Downloading #{url}" + Logger.debug "Downloading #{url}" loop do sleep @wait_seconds.seconds download_page page_job break if page_job.success || page_job.tries_remaning <= 0 page_job.tries_remaning -= 1 - @logger.warn "Failed to download page #{url}. " \ - "Retrying... Remaining retries: " \ - "#{page_job.tries_remaning}" + Logger.warn "Failed to download page #{url}. " \ + "Retrying... Remaining retries: " \ + "#{page_job.tries_remaning}" end channel.send page_job @@ -330,8 +350,8 @@ module MangaDex page_jobs = [] of PageJob chapter.pages.size.times do page_job = channel.receive - @logger.debug "[#{page_job.success ? "success" : "failed"}] " \ - "#{page_job.url}" + Logger.debug "[#{page_job.success ? "success" : "failed"}] " \ + "#{page_job.url}" page_jobs << page_job if page_job.success @queue.add_success job @@ -339,14 +359,14 @@ module MangaDex @queue.add_fail job msg = "Failed to download page #{page_job.url}" @queue.add_message msg, job - @logger.error msg + Logger.error msg end end fail_count = page_jobs.count { |j| !j.success } - @logger.debug "Download completed. " \ - "#{fail_count}/#{page_jobs.size} failed" + Logger.debug "Download completed. " \ + "#{fail_count}/#{page_jobs.size} failed" writer.close - @logger.debug "cbz File created at #{zip_path}" + Logger.debug "cbz File created at #{zip_path}" zip_exception = validate_zip zip_path if !zip_exception.nil? @@ -363,7 +383,7 @@ module MangaDex end private def download_page(job : PageJob) - @logger.debug "downloading #{job.url}" + Logger.debug "downloading #{job.url}" headers = HTTP::Headers{ "User-agent" => "Mangadex.cr", } @@ -377,7 +397,7 @@ module MangaDex end job.success = true rescue e - @logger.error e + Logger.error e job.success = false end end diff --git a/src/mango.cr b/src/mango.cr index 8266471..4c3a100 100644 --- a/src/mango.cr +++ b/src/mango.cr @@ -1,5 +1,5 @@ +require "./config" require "./server" -require "./context" require "./mangadex/*" require "option_parser" @@ -24,18 +24,7 @@ OptionParser.parse do |parser| end end -config = Config.load config_path -logger = Logger.new config.log_level -storage = Storage.new config.db_path, logger -library = Library.new config.library_path, config.scan_interval, logger, storage -queue = MangaDex::Queue.new config.mangadex["download_queue_db_path"].to_s, - logger -api = MangaDex::API.new config.mangadex["api_url"].to_s -MangaDex::Downloader.new queue, api, config.library_path, - config.mangadex["download_wait_seconds"].to_i, - config.mangadex["download_retries"].to_i, logger +Config.load(config_path).set_current -context = Context.new config, logger, library, storage, queue - -server = Server.new context +server = Server.new server.start diff --git a/src/routes/admin.cr b/src/routes/admin.cr index 37cf656..343d499 100644 --- a/src/routes/admin.cr +++ b/src/routes/admin.cr @@ -1,7 +1,7 @@ require "./router" class AdminRouter < Router - def setup + def initialize get "/admin" do |env| layout "admin" end @@ -96,7 +96,7 @@ class AdminRouter < Router end get "/admin/downloads" do |env| - base_url = @context.config.mangadex["base_url"] + base_url = Config.current.mangadex["base_url"] layout "download-manager" end end diff --git a/src/routes/api.cr b/src/routes/api.cr index e888239..54bfb1b 100644 --- a/src/routes/api.cr +++ b/src/routes/api.cr @@ -3,7 +3,7 @@ require "../mangadex/*" require "../upload" class APIRouter < Router - def setup + def initialize get "/api/page/:tid/:eid/:page" do |env| begin tid = env.params.url["tid"] @@ -123,7 +123,7 @@ class APIRouter < Router get "/api/admin/mangadex/manga/:id" do |env| begin id = env.params.url["id"] - api = MangaDex::API.new @context.config.mangadex["api_url"].to_s + api = MangaDex::API.default manga = api.get_manga id send_json env, manga.to_info_json rescue e @@ -230,7 +230,7 @@ class APIRouter < Router end ext = File.extname filename - upload = Upload.new @context.config.upload_path, @context.logger + upload = Upload.new Config.current.upload_path url = upload.path_to_url upload.save "img", ext, part.body if url.nil? diff --git a/src/routes/main.cr b/src/routes/main.cr index 3e21fc1..f8db978 100644 --- a/src/routes/main.cr +++ b/src/routes/main.cr @@ -1,7 +1,7 @@ require "./router" class MainRouter < Router - def setup + def initialize get "/login" do |env| render "src/views/login.ecr" end @@ -59,7 +59,7 @@ class MainRouter < Router end get "/download" do |env| - base_url = @context.config.mangadex["base_url"] + base_url = Config.current.mangadex["base_url"] layout "download" end end diff --git a/src/routes/reader.cr b/src/routes/reader.cr index 49d9a27..c949e34 100644 --- a/src/routes/reader.cr +++ b/src/routes/reader.cr @@ -1,7 +1,7 @@ require "./router" class ReaderRouter < Router - def setup + def initialize get "/reader/:title/:entry" do |env| begin title = (@context.library.get_title env.params.url["title"]).not_nil! diff --git a/src/routes/router.cr b/src/routes/router.cr index 97fb698..a6d7ace 100644 --- a/src/routes/router.cr +++ b/src/routes/router.cr @@ -1,6 +1,3 @@ -require "../context" - class Router - def initialize(@context : Context) - end + @context : Context = Context.default end diff --git a/src/server.cr b/src/server.cr index fef8412..98a8f97 100644 --- a/src/server.cr +++ b/src/server.cr @@ -1,11 +1,38 @@ require "kemal" -require "./context" +require "./library" require "./handlers/*" require "./util" require "./routes/*" +class Context + property library : Library + property storage : Storage + property queue : MangaDex::Queue + + def self.default + unless @@default + @@default = new + end + @@default.not_nil! + end + + def initialize + @storage = Storage.default + @library = Library.default + @queue = MangaDex::Queue.default + end + + {% for lvl in Logger::LEVELS %} + def {{lvl.id}}(msg) + Logger.{{lvl.id}} msg + end + {% end %} +end + class Server - def initialize(@context : Context) + @context : Context = Context.default + + def initialize error 403 do |env| message = "HTTP 403: You are not authorized to visit #{env.request.path}" layout "message" @@ -19,15 +46,15 @@ class Server layout "message" end - MainRouter.new(@context).setup - AdminRouter.new(@context).setup - ReaderRouter.new(@context).setup - APIRouter.new(@context).setup + MainRouter.new + AdminRouter.new + ReaderRouter.new + APIRouter.new Kemal.config.logging = false - add_handler LogHandler.new @context.logger + add_handler LogHandler.new add_handler AuthHandler.new @context.storage - add_handler UploadHandler.new @context.config.upload_path + add_handler UploadHandler.new Config.current.upload_path {% if flag?(:release) %} # when building for relase, embed the static files in binary @context.debug "We are in release mode. Using embedded static files." @@ -41,7 +68,7 @@ class Server {% if flag?(:release) %} Kemal.config.env = "production" {% end %} - Kemal.config.port = @context.config.port + Kemal.config.port = Config.current.port Kemal.run end end diff --git a/src/storage.cr b/src/storage.cr index 382709f..21cd5d3 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -13,14 +13,23 @@ def verify_password(hash, pw) end class Storage - def initialize(@path : String, @logger : Logger) - dir = File.dirname path + @path : String = Config.current.db_path + + def self.default + unless @@default + @@default = new + end + @@default.not_nil! + end + + def initialize + dir = File.dirname @path unless Dir.exists? dir - @logger.info "The DB directory #{dir} does not exist. " \ - "Attepmting to create it" + Logger.info "The DB directory #{dir} does not exist. " \ + "Attepmting to create it" Dir.mkdir_p dir end - DB.open "sqlite3://#{path}" do |db| + DB.open "sqlite3://#{@path}" do |db| begin # We create the `ids` table first. even if the uses has an # early version installed and has the `user` table only, @@ -34,19 +43,19 @@ class Storage "(username text, password text, token text, admin integer)" rescue e unless e.message.not_nil!.ends_with? "already exists" - @logger.fatal "Error when checking tables in DB: #{e}" + Logger.fatal "Error when checking tables in DB: #{e}" raise e end else - @logger.debug "Creating DB file at #{@path}" + Logger.debug "Creating DB file at #{@path}" db.exec "create unique index username_idx on users (username)" db.exec "create unique index token_idx on users (token)" random_pw = random_str hash = hash_password random_pw db.exec "insert into users values (?, ?, ?, ?)", "admin", hash, nil, 1 - @logger.log "Initial user created. You can log in with " \ - "#{{"username" => "admin", "password" => random_pw}}" + Logger.log "Initial user created. You can log in with " \ + "#{{"username" => "admin", "password" => random_pw}}" end end end @@ -58,18 +67,18 @@ class Storage "users where username = (?)", username, as: {String, String?} unless verify_password hash, password - @logger.debug "Password does not match the hash" + Logger.debug "Password does not match the hash" return nil end - @logger.debug "User #{username} verified" + Logger.debug "User #{username} verified" return token if token token = random_str - @logger.debug "Updating token for #{username}" + Logger.debug "Updating token for #{username}" db.exec "update users set token = (?) where username = (?)", token, username return token rescue e - @logger.error "Error when verifying user #{username}: #{e}" + Logger.error "Error when verifying user #{username}: #{e}" return nil end end @@ -82,7 +91,7 @@ class Storage username = db.query_one "select username from users where " \ "token = (?)", token, as: String rescue e - @logger.debug "Unable to verify token" + Logger.debug "Unable to verify token" end end username @@ -95,7 +104,7 @@ class Storage is_admin = db.query_one "select admin from users where " \ "token = (?)", token, as: Bool rescue e - @logger.debug "Unable to verify user as admin" + Logger.debug "Unable to verify user as admin" end end is_admin diff --git a/src/upload.cr b/src/upload.cr index c6d27be..04fe51f 100644 --- a/src/upload.cr +++ b/src/upload.cr @@ -1,10 +1,10 @@ require "./util" class Upload - def initialize(@dir : String, @logger : Logger) + def initialize(@dir : String) unless Dir.exists? @dir - @logger.info "The uploads directory #{@dir} does not exist. " \ - "Attempting to create it" + Logger.info "The uploads directory #{@dir} does not exist. " \ + "Attempting to create it" Dir.mkdir_p @dir end end @@ -19,7 +19,7 @@ class Upload file_path = File.join full_dir, filename unless Dir.exists? full_dir - @logger.debug "creating directory #{full_dir}" + Logger.debug "creating directory #{full_dir}" Dir.mkdir_p full_dir end @@ -50,7 +50,7 @@ class Upload end if ary.empty? - @logger.warn "File #{path} is not in the upload directory #{@dir}" + Logger.warn "File #{path} is not in the upload directory #{@dir}" return end