Use singleton in various classes

This commit is contained in:
Alex Ling 2020-05-04 16:18:16 +00:00
parent 09b297cd8e
commit 1bec9f0108
16 changed files with 178 additions and 117 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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]

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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!

View File

@ -1,6 +1,3 @@
require "../context"
class Router
def initialize(@context : Context)
end
@context : Context = Context.default
end

View File

@ -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

View File

@ -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

View File

@ -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