From 8b184ed48d48255b542e9ed27276517a84c5b876 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Wed, 8 Apr 2020 03:38:02 +0000 Subject: [PATCH] Project-wise code formatting --- spec/config_spec.cr | 18 +- spec/mangadex_spec.cr | 177 +++++----- spec/spec_helper.cr | 86 ++--- spec/storage_spec.cr | 154 ++++----- spec/util_spec.cr | 58 ++-- src/auth_handler.cr | 31 +- src/config.cr | 102 +++--- src/context.cr | 24 +- src/library.cr | 627 +++++++++++++++++----------------- src/log_handler.cr | 36 +- src/logger.cr | 90 ++--- src/mangadex/api.cr | 364 ++++++++++---------- src/mangadex/downloader.cr | 667 +++++++++++++++++++------------------ src/mango.cr | 34 +- src/routes/admin.cr | 183 +++++----- src/routes/api.cr | 364 ++++++++++---------- src/routes/main.cr | 104 +++--- src/routes/reader.cr | 99 +++--- src/routes/router.cr | 4 +- src/server.cr | 72 ++-- src/static_handler.cr | 42 +-- src/storage.cr | 298 ++++++++--------- src/util.cr | 100 +++--- 23 files changed, 1864 insertions(+), 1870 deletions(-) diff --git a/spec/config_spec.cr b/spec/config_spec.cr index c4f8700..c3aad08 100644 --- a/spec/config_spec.cr +++ b/spec/config_spec.cr @@ -1,14 +1,14 @@ require "./spec_helper" describe Config do - it "creates config if it does not exist" do - with_default_config do |config, logger, path| - File.exists?(path).should be_true - end - end + it "creates config if it does not exist" do + with_default_config do |config, logger, path| + File.exists?(path).should be_true + end + end - it "correctly loads config" do - config = Config.load "spec/asset/test-config.yml" - config.port.should eq 3000 - end + it "correctly loads config" do + config = Config.load "spec/asset/test-config.yml" + config.port.should eq 3000 + end end diff --git a/spec/mangadex_spec.cr b/spec/mangadex_spec.cr index 21e5853..79534ce 100644 --- a/spec/mangadex_spec.cr +++ b/spec/mangadex_spec.cr @@ -3,103 +3,102 @@ require "./spec_helper" include MangaDex describe Queue do - it "creates DB at given path" do - with_queue do |queue, path| - File.exists?(path).should be_true - end - end + it "creates DB at given path" do + with_queue do |queue, path| + File.exists?(path).should be_true + end + end - it "pops nil when empty" do - with_queue do |queue| - queue.pop.should be_nil - end - end + it "pops nil when empty" do + with_queue do |queue| + queue.pop.should be_nil + end + end - it "inserts multiple jobs" do - with_queue do |queue| - j1 = Job.new "1", "1", "title", "manga_title", JobStatus::Error, - Time.utc - j2 = Job.new "2", "2", "title", "manga_title", JobStatus::Completed, - Time.utc - j3 = Job.new "3", "3", "title", "manga_title", JobStatus::Pending, - Time.utc - j4 = Job.new "4", "4", "title", "manga_title", - JobStatus::Downloading, Time.utc - count = queue.push [j1, j2, j3, j4] - count.should eq 4 - end - end + it "inserts multiple jobs" do + with_queue do |queue| + j1 = Job.new "1", "1", "title", "manga_title", JobStatus::Error, + Time.utc + j2 = Job.new "2", "2", "title", "manga_title", JobStatus::Completed, + Time.utc + j3 = Job.new "3", "3", "title", "manga_title", JobStatus::Pending, + Time.utc + j4 = Job.new "4", "4", "title", "manga_title", + JobStatus::Downloading, Time.utc + count = queue.push [j1, j2, j3, j4] + count.should eq 4 + end + end - it "pops pending job" do - with_queue do |queue| - job = queue.pop - job.should_not be_nil - job.not_nil!.id.should eq "3" - end - end + it "pops pending job" do + with_queue do |queue| + job = queue.pop + job.should_not be_nil + job.not_nil!.id.should eq "3" + end + end - it "correctly counts jobs" do - with_queue do |queue| - queue.count.should eq 4 - end - end + it "correctly counts jobs" do + with_queue do |queue| + queue.count.should eq 4 + end + end - it "deletes job" do - with_queue do |queue| - queue.delete "4" - queue.count.should eq 3 - end - end + it "deletes job" do + with_queue do |queue| + queue.delete "4" + queue.count.should eq 3 + end + end - it "sets status" do - with_queue do |queue| - job = queue.pop.not_nil! - queue.set_status JobStatus::Downloading, job - job = queue.pop - job.should_not be_nil - job.not_nil!.status.should eq JobStatus::Downloading - end - end + it "sets status" do + with_queue do |queue| + job = queue.pop.not_nil! + queue.set_status JobStatus::Downloading, job + job = queue.pop + job.should_not be_nil + job.not_nil!.status.should eq JobStatus::Downloading + end + end - it "sets number of pages" do - with_queue do |queue| - job = queue.pop.not_nil! - queue.set_pages 100, job - job = queue.pop - job.should_not be_nil - job.not_nil!.pages.should eq 100 - end - end + it "sets number of pages" do + with_queue do |queue| + job = queue.pop.not_nil! + queue.set_pages 100, job + job = queue.pop + job.should_not be_nil + job.not_nil!.pages.should eq 100 + end + end - it "adds fail/success counts" do - with_queue do |queue| - job = queue.pop.not_nil! - queue.add_success job - queue.add_success job - queue.add_fail job - job = queue.pop - job.should_not be_nil - job.not_nil!.success_count.should eq 2 - job.not_nil!.fail_count.should eq 1 - end - end + it "adds fail/success counts" do + with_queue do |queue| + job = queue.pop.not_nil! + queue.add_success job + queue.add_success job + queue.add_fail job + job = queue.pop + job.should_not be_nil + job.not_nil!.success_count.should eq 2 + job.not_nil!.fail_count.should eq 1 + end + end - it "appends status message" do - with_queue do |queue| - job = queue.pop.not_nil! - queue.add_message "hello", job - queue.add_message "world", job - job = queue.pop - job.should_not be_nil - job.not_nil!.status_message.should eq "\nhello\nworld" - end - end + it "appends status message" do + with_queue do |queue| + job = queue.pop.not_nil! + queue.add_message "hello", job + queue.add_message "world", job + job = queue.pop + job.should_not be_nil + job.not_nil!.status_message.should eq "\nhello\nworld" + end + end - it "cleans up" do - with_queue do - true - end - State.reset - end + it "cleans up" do + with_queue do + true + end + State.reset + end end - diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr index 7755d02..67b5535 100644 --- a/spec/spec_helper.cr +++ b/spec/spec_helper.cr @@ -3,63 +3,63 @@ require "../src/context" require "../src/server" class State - @@hash = {} of String => String + @@hash = {} of String => String - def self.get(key) - @@hash[key]? - end + def self.get(key) + @@hash[key]? + end - def self.get!(key) - @@hash[key] - end + def self.get!(key) + @@hash[key] + end - def self.set(key, value) - return if value.nil? - @@hash[key] = value - end + def self.set(key, value) + return if value.nil? + @@hash[key] = value + end - def self.reset - @@hash.clear - end + def self.reset + @@hash.clear + end end def get_tempfile(name) - path = State.get name - if path.nil? || !File.exists? path - file = File.tempfile name - State.set name, file.path - return file - else - return File.new path - end + path = State.get name + if path.nil? || !File.exists? path + file = File.tempfile name + State.set name, file.path + return file + else + return File.new path + end end def with_default_config - temp_config = get_tempfile "mango-test-config" - config = Config.load temp_config.path - logger = Logger.new config.log_level - yield config, logger, temp_config.path - temp_config.delete + temp_config = get_tempfile "mango-test-config" + config = Config.load temp_config.path + logger = Logger.new config.log_level + yield config, logger, temp_config.path + temp_config.delete end def with_storage - with_default_config do |config, logger| - temp_db = get_tempfile "mango-test-db" - storage = Storage.new temp_db.path, logger - clear = yield storage, temp_db.path - if clear == true - temp_db.delete - end - end + with_default_config do |config, logger| + temp_db = get_tempfile "mango-test-db" + storage = Storage.new temp_db.path, logger + clear = yield storage, temp_db.path + if clear == true + temp_db.delete + end + end end def with_queue - with_default_config do |config, logger| - temp_queue_db = get_tempfile "mango-test-queue-db" - queue = MangaDex::Queue.new temp_queue_db.path, logger - clear = yield queue, temp_queue_db.path - if clear == true - temp_queue_db.delete - end - end + with_default_config do |config, logger| + temp_queue_db = get_tempfile "mango-test-queue-db" + queue = MangaDex::Queue.new temp_queue_db.path, logger + clear = yield queue, temp_queue_db.path + if clear == true + temp_queue_db.delete + end + end end diff --git a/spec/storage_spec.cr b/spec/storage_spec.cr index 8b80003..8092d9e 100644 --- a/spec/storage_spec.cr +++ b/spec/storage_spec.cr @@ -1,91 +1,91 @@ require "./spec_helper" describe Storage do - it "creates DB at given path" do - with_storage do |storage, path| - File.exists?(path).should be_true - end - end + it "creates DB at given path" do + with_storage do |storage, path| + File.exists?(path).should be_true + end + end - it "deletes user" do - with_storage do |storage| - storage.delete_user "admin" - end - end + it "deletes user" do + with_storage do |storage| + storage.delete_user "admin" + end + end - it "creates new user" do - with_storage do |storage| - storage.new_user "user", "123456", false - storage.new_user "admin", "123456", true - end - end + it "creates new user" do + with_storage do |storage| + storage.new_user "user", "123456", false + storage.new_user "admin", "123456", true + end + end - it "verifies username/password combination" do - with_storage do |storage| - user_token = storage.verify_user "user", "123456" - admin_token = storage.verify_user "admin", "123456" - user_token.should_not be_nil - admin_token.should_not be_nil - State.set "user_token", user_token - State.set "admin_token", admin_token - end - end + it "verifies username/password combination" do + with_storage do |storage| + user_token = storage.verify_user "user", "123456" + admin_token = storage.verify_user "admin", "123456" + user_token.should_not be_nil + admin_token.should_not be_nil + State.set "user_token", user_token + State.set "admin_token", admin_token + end + end - it "rejects duplicate username" do - with_storage do |storage| - expect_raises SQLite3::Exception, - "UNIQUE constraint failed: users.username" do - storage.new_user "admin", "123456", true - end - end - end + it "rejects duplicate username" do + with_storage do |storage| + expect_raises SQLite3::Exception, + "UNIQUE constraint failed: users.username" do + storage.new_user "admin", "123456", true + end + end + end - it "verifies token" do - with_storage do |storage| - user_token = State.get! "user_token" - user = storage.verify_token user_token - user.should eq "user" - end - end + it "verifies token" do + with_storage do |storage| + user_token = State.get! "user_token" + user = storage.verify_token user_token + user.should eq "user" + end + end - it "verfies admin token" do - with_storage do |storage| - admin_token = State.get! "admin_token" - storage.verify_admin(admin_token).should be_true - end - end + it "verfies admin token" do + with_storage do |storage| + admin_token = State.get! "admin_token" + storage.verify_admin(admin_token).should be_true + end + end - it "rejects non-admin token" do - with_storage do |storage| - user_token = State.get! "user_token" - storage.verify_admin(user_token).should be_false - end - end + it "rejects non-admin token" do + with_storage do |storage| + user_token = State.get! "user_token" + storage.verify_admin(user_token).should be_false + end + end - it "updates user" do - with_storage do |storage| - storage.update_user "admin", "admin", "654321", true - token = storage.verify_user "admin", "654321" - admin_token = State.get! "admin_token" - token.should eq admin_token - end - end + it "updates user" do + with_storage do |storage| + storage.update_user "admin", "admin", "654321", true + token = storage.verify_user "admin", "654321" + admin_token = State.get! "admin_token" + token.should eq admin_token + end + end - it "logs user out" do - with_storage do |storage| - user_token = State.get! "user_token" - admin_token = State.get! "admin_token" - storage.logout user_token - storage.logout admin_token - storage.verify_token(user_token).should be_nil - storage.verify_token(admin_token).should be_nil - end - end + it "logs user out" do + with_storage do |storage| + user_token = State.get! "user_token" + admin_token = State.get! "admin_token" + storage.logout user_token + storage.logout admin_token + storage.verify_token(user_token).should be_nil + storage.verify_token(admin_token).should be_nil + end + end - it "cleans up" do - with_storage do - true - end - State.reset - end + it "cleans up" do + with_storage do + true + end + State.reset + end end diff --git a/spec/util_spec.cr b/spec/util_spec.cr index 47e26d6..da2bb66 100644 --- a/spec/util_spec.cr +++ b/spec/util_spec.cr @@ -1,36 +1,36 @@ require "./spec_helper" describe "compare_alphanumerically" do - it "sorts filenames with leading zeros correctly" do - ary = ["010.jpg", "001.jpg", "002.png"] - ary.sort! {|a, b| - compare_alphanumerically a, b - } - ary.should eq ["001.jpg", "002.png", "010.jpg"] - end + it "sorts filenames with leading zeros correctly" do + ary = ["010.jpg", "001.jpg", "002.png"] + ary.sort! { |a, b| + compare_alphanumerically a, b + } + ary.should eq ["001.jpg", "002.png", "010.jpg"] + end - it "sorts filenames without leading zeros correctly" do - ary = ["10.jpg", "1.jpg", "0.png", "0100.jpg"] - ary.sort! {|a, b| - compare_alphanumerically a, b - } - ary.should eq ["0.png", "1.jpg", "10.jpg", "0100.jpg"] - end + it "sorts filenames without leading zeros correctly" do + ary = ["10.jpg", "1.jpg", "0.png", "0100.jpg"] + ary.sort! { |a, b| + compare_alphanumerically a, b + } + ary.should eq ["0.png", "1.jpg", "10.jpg", "0100.jpg"] + end - # https://ux.stackexchange.com/a/95441 - it "sorts like the stack exchange post" do - ary = ["2", "12", "200000", "1000000", "a", "a12", "b2", "text2", - "text2a", "text2a2", "text2a12", "text2ab", "text12", "text12a"] - ary.reverse.sort {|a, b| - compare_alphanumerically a, b - }.should eq ary - end + # https://ux.stackexchange.com/a/95441 + it "sorts like the stack exchange post" do + ary = ["2", "12", "200000", "1000000", "a", "a12", "b2", "text2", + "text2a", "text2a2", "text2a12", "text2ab", "text12", "text12a"] + ary.reverse.sort { |a, b| + compare_alphanumerically a, b + }.should eq ary + end - # https://github.com/hkalexling/Mango/issues/22 - it "handles numbers larger than Int32" do - ary = ["14410155591588.jpg", "21410155591588.png", "104410155591588.jpg"] - ary.reverse.sort {|a, b| - compare_alphanumerically a, b - }.should eq ary - end + # https://github.com/hkalexling/Mango/issues/22 + it "handles numbers larger than Int32" do + ary = ["14410155591588.jpg", "21410155591588.png", "104410155591588.jpg"] + ary.reverse.sort { |a, b| + compare_alphanumerically a, b + }.should eq ary + end end diff --git a/src/auth_handler.cr b/src/auth_handler.cr index b624b22..1a29b52 100644 --- a/src/auth_handler.cr +++ b/src/auth_handler.cr @@ -3,24 +3,23 @@ require "./storage" require "./util" class AuthHandler < Kemal::Handler - def initialize(@storage : Storage) - end + def initialize(@storage : Storage) + end - def call(env) - return call_next(env) \ - if request_path_startswith env, ["/login", "/logout"] + def call(env) + return call_next(env) if request_path_startswith env, ["/login", "/logout"] - cookie = env.request.cookies.find { |c| c.name == "token" } - if cookie.nil? || ! @storage.verify_token cookie.value - return env.redirect "/login" - end + cookie = env.request.cookies.find { |c| c.name == "token" } + if cookie.nil? || !@storage.verify_token cookie.value + return env.redirect "/login" + end - if request_path_startswith env, ["/admin", "/api/admin", "/download"] - unless @storage.verify_admin cookie.value - env.response.status_code = 403 - end - end + if request_path_startswith env, ["/admin", "/api/admin", "/download"] + unless @storage.verify_admin cookie.value + env.response.status_code = 403 + end + end - call_next env - end + call_next env + end end diff --git a/src/config.cr b/src/config.cr index 18be18b..b9caffc 100644 --- a/src/config.cr +++ b/src/config.cr @@ -1,60 +1,58 @@ require "yaml" class Config - include YAML::Serializable + include YAML::Serializable - property port : Int32 = 9000 - property library_path : String = \ - File.expand_path "~/mango/library", home: true - property db_path : String = \ - File.expand_path "~/mango/mango.db", home: true - @[YAML::Field(key: "scan_interval_minutes")] - property scan_interval : Int32 = 5 - property log_level : String = "info" - property mangadex = Hash(String, String|Int32).new + property port : Int32 = 9000 + property library_path : String = File.expand_path "~/mango/library", home: true + property db_path : String = File.expand_path "~/mango/mango.db", home: true + @[YAML::Field(key: "scan_interval_minutes")] + property scan_interval : Int32 = 5 + property log_level : String = "info" + property mangadex = Hash(String, String | Int32).new - @[YAML::Field(ignore: true)] - @mangadex_defaults = { - "base_url" => "https://mangadex.org", - "api_url" => "https://mangadex.org/api", - "download_wait_seconds" => 5, - "download_retries" => 4, - "download_queue_db_path" => File.expand_path "~/mango/queue.db", - home: true - } + @[YAML::Field(ignore: true)] + @mangadex_defaults = { + "base_url" => "https://mangadex.org", + "api_url" => "https://mangadex.org/api", + "download_wait_seconds" => 5, + "download_retries" => 4, + "download_queue_db_path" => File.expand_path("~/mango/queue.db", + home: true), + } - def self.load(path : String?) - path = "~/.config/mango/config.yml" if path.nil? - cfg_path = File.expand_path path, home: true - if File.exists? cfg_path - config = self.from_yaml File.read cfg_path - config.fill_defaults - return config - end - puts "The config file #{cfg_path} does not exist." \ - " Do you want mango to dump the default config there? [Y/n]" - input = gets - if input && input.downcase == "n" - abort "Aborting..." - end - default = self.allocate - default.fill_defaults - cfg_dir = File.dirname cfg_path - unless Dir.exists? cfg_dir - Dir.mkdir_p cfg_dir - end - File.write cfg_path, default.to_yaml - puts "The config file has been created at #{cfg_path}." - default - end + def self.load(path : String?) + path = "~/.config/mango/config.yml" if path.nil? + cfg_path = File.expand_path path, home: true + if File.exists? cfg_path + config = self.from_yaml File.read cfg_path + config.fill_defaults + return config + end + puts "The config file #{cfg_path} does not exist." \ + " Do you want mango to dump the default config there? [Y/n]" + input = gets + if input && input.downcase == "n" + abort "Aborting..." + end + default = self.allocate + default.fill_defaults + cfg_dir = File.dirname cfg_path + unless Dir.exists? cfg_dir + Dir.mkdir_p cfg_dir + end + File.write cfg_path, default.to_yaml + puts "The config file has been created at #{cfg_path}." + default + end - def fill_defaults - {% for hash_name in ["mangadex"] %} - @{{hash_name.id}}_defaults.map do |k, v| - if @{{hash_name.id}}[k]?.nil? - @{{hash_name.id}}[k] = v - end - end - {% end %} - end + def fill_defaults + {% for hash_name in ["mangadex"] %} + @{{hash_name.id}}_defaults.map do |k, v| + if @{{hash_name.id}}[k]?.nil? + @{{hash_name.id}}[k] = v + end + end + {% end %} + end end diff --git a/src/context.cr b/src/context.cr index edafd6e..a4e4d4b 100644 --- a/src/context.cr +++ b/src/context.cr @@ -4,18 +4,18 @@ require "./storage" require "./logger" class Context - property config : Config - property library : Library - property storage : Storage - property logger : Logger - property queue : MangaDex::Queue + property config : Config + property library : Library + property storage : Storage + property logger : Logger + property queue : MangaDex::Queue - def initialize(@config, @logger, @library, @storage, @queue) - end + def initialize(@config, @logger, @library, @storage, @queue) + end - {% for lvl in Logger::LEVELS %} - def {{lvl.id}}(msg) - @logger.{{lvl.id}} msg - end - {% end %} + {% for lvl in Logger::LEVELS %} + def {{lvl.id}}(msg) + @logger.{{lvl.id}} msg + end + {% end %} end diff --git a/src/library.cr b/src/library.cr index 59d2c2f..0e8368c 100644 --- a/src/library.cr +++ b/src/library.cr @@ -5,359 +5,364 @@ require "uri" require "./util" struct Image - property data : Bytes - property mime : String - property filename : String - property size : Int32 + property data : Bytes + property mime : String + property filename : String + property size : Int32 - def initialize(@data, @mime, @filename, @size) - end + def initialize(@data, @mime, @filename, @size) + end end class Entry - property zip_path : String, book : Title, title : String, - size : String, pages : Int32, cover_url : String, id : String, - title_id : String, encoded_path : String, encoded_title : String, - mtime : Time + property zip_path : String, book : Title, 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_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 initialize(path, @book, @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 to_json(json : JSON::Builder) - json.object do - {% for str in ["zip_path", "title", "size", "cover_url", "id", - "title_id", "encoded_path", "encoded_title"] %} - json.field {{str}}, @{{str.id}} - {% end %} - json.field "display_name", @book.display_name @title - json.field "pages" {json.number @pages} - json.field "mtime" {json.number @mtime.to_unix} - end - end + def to_json(json : JSON::Builder) + json.object do + {% for str in ["zip_path", "title", "size", "cover_url", "id", + "title_id", "encoded_path", "encoded_title"] %} + json.field {{str}}, @{{str.id}} + {% end %} + json.field "display_name", @book.display_name @title + json.field "pages" { json.number @pages } + json.field "mtime" { json.number @mtime.to_unix } + end + end - def display_name - @book.display_name @title - end + def display_name + @book.display_name @title + end - def encoded_display_name - URI.encode display_name - end + def encoded_display_name + URI.encode display_name + 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 a.filename, 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 + 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 a.filename, 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 - property dir : String, parent_id : String, title_ids : Array(String), - entries : Array(Entry), title : String, id : String, - encoded_title : String, mtime : Time + property dir : String, parent_id : String, title_ids : Array(String), + entries : Array(Entry), title : String, id : String, + encoded_title : String, mtime : Time - def initialize(@dir : String, @parent_id, storage, - @logger : Logger, @library : Library) - @id = storage.get_id @dir, true - @title = File.basename dir - @encoded_title = URI.encode @title - @title_ids = [] of String - @entries = [] of Entry - @mtime = File.info(dir).modification_time + def initialize(@dir : String, @parent_id, storage, + @logger : Logger, @library : Library) + @id = storage.get_id @dir, true + @title = File.basename dir + @encoded_title = URI.encode @title + @title_ids = [] of String + @entries = [] of Entry + @mtime = File.info(dir).modification_time - Dir.entries(dir).each do |fn| - next if fn.starts_with? "." - path = File.join dir, fn - if File.directory? path - title = Title.new path, @id, storage, @logger, library - next if title.entries.size == 0 && title.titles.size == 0 - @library.title_hash[title.id] = title - @title_ids << title.id - next - end - if [".zip", ".cbz"].includes? File.extname path - next if !valid_zip path - entry = Entry.new path, self, @id, storage - @entries << entry if entry.pages > 0 - end - end + Dir.entries(dir).each do |fn| + next if fn.starts_with? "." + path = File.join dir, fn + if File.directory? path + title = Title.new path, @id, storage, @logger, library + next if title.entries.size == 0 && title.titles.size == 0 + @library.title_hash[title.id] = title + @title_ids << title.id + next + end + if [".zip", ".cbz"].includes? File.extname path + next if !valid_zip path + entry = Entry.new path, self, @id, storage + @entries << entry if entry.pages > 0 + end + end - mtimes = [@mtime] - mtimes += @title_ids.map{|e| @library.title_hash[e].mtime} - mtimes += @entries.map{|e| e.mtime} - @mtime = mtimes.max + mtimes = [@mtime] + mtimes += @title_ids.map { |e| @library.title_hash[e].mtime } + mtimes += @entries.map { |e| e.mtime } + @mtime = mtimes.max - @title_ids.sort! do |a, b| - compare_alphanumerically @library.title_hash[a].title, - @library.title_hash[b].title - end - @entries.sort! do |a, b| - compare_alphanumerically a.title, b.title - end - end + @title_ids.sort! do |a, b| + compare_alphanumerically @library.title_hash[a].title, + @library.title_hash[b].title + end + @entries.sort! do |a, b| + compare_alphanumerically a.title, b.title + end + end - def to_json(json : JSON::Builder) - json.object do - {% for str in ["dir", "title", "id", "encoded_title"] %} - json.field {{str}}, @{{str.id}} - {% end %} - json.field "display_name", display_name - json.field "mtime" {json.number @mtime.to_unix} - json.field "titles" do - json.raw self.titles.to_json - end - json.field "entries" do - json.raw @entries.to_json - end - json.field "parents" do - json.array do - self.parents.each do |title| - json.object do - json.field "title", title.title - json.field "id", title.id - end - end - end - end - end - end + def to_json(json : JSON::Builder) + json.object do + {% for str in ["dir", "title", "id", "encoded_title"] %} + json.field {{str}}, @{{str.id}} + {% end %} + json.field "display_name", display_name + json.field "mtime" { json.number @mtime.to_unix } + json.field "titles" do + json.raw self.titles.to_json + end + json.field "entries" do + json.raw @entries.to_json + end + json.field "parents" do + json.array do + self.parents.each do |title| + json.object do + json.field "title", title.title + json.field "id", title.id + end + end + end + end + end + end - def titles - @title_ids.map {|tid| @library.get_title! tid} - end + def titles + @title_ids.map { |tid| @library.get_title! tid } + end - def parents - ary = [] of Title - tid = @parent_id - while !tid.empty? - title = @library.get_title! tid - ary << title - tid = title.parent_id - end - ary - end + def parents + ary = [] of Title + tid = @parent_id + while !tid.empty? + title = @library.get_title! tid + ary << title + tid = title.parent_id + end + ary + end - def size - @entries.size + @title_ids.size - end + def size + @entries.size + @title_ids.size + 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 + # 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 + def get_entry(eid) + @entries.find { |e| e.id == eid } + end - def display_name - info = TitleInfo.new @dir - dn = info.display_name - dn.empty? ? @title : dn - end + def display_name + info = TitleInfo.new @dir + dn = info.display_name + dn.empty? ? @title : dn + end - def encoded_display_name - URI.encode display_name - end + def encoded_display_name + URI.encode display_name + end - def display_name(entry_name) - info = TitleInfo.new @dir - dn = info.entry_display_name[entry_name]? - unless dn.nil? || dn.empty? - return dn - end - entry_name - end + def display_name(entry_name) + info = TitleInfo.new @dir + dn = info.entry_display_name[entry_name]? + unless dn.nil? || dn.empty? + return dn + end + entry_name + end - def set_display_name(dn) - info = TitleInfo.new @dir - info.display_name = dn - info.save - end + def set_display_name(dn) + info = TitleInfo.new @dir + info.display_name = dn + info.save + end - def set_display_name(entry_name : String, dn) - info = TitleInfo.new @dir - info.entry_display_name[entry_name] = dn - info.save - end + def set_display_name(entry_name : String, dn) + info = TitleInfo.new @dir + info.entry_display_name[entry_name] = dn + info.save + 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 - return - end - info.progress[username][entry] = page - info.save - 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 + return + end + info.progress[username][entry] = page + info.save + 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_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.0 if entry_obj.nil? - page / entry_obj.pages - 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.0 if entry_obj.nil? + page / entry_obj.pages + end - def load_percetage(username) - return 0.0 if @entries.empty? - 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 load_percetage(username) + return 0.0 if @entries.empty? + 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 + 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 - include JSON::Serializable + include JSON::Serializable - property comment = "Generated by Mango. DO NOT EDIT!" - # { user1: { entry1: 10, entry2: 0 } } - property progress = {} of String => Hash(String, Int32) - property display_name = "" - # { entry1 : "display name" } - property entry_display_name = {} of String => String + property comment = "Generated by Mango. DO NOT EDIT!" + # { user1: { entry1: 10, entry2: 0 } } + property progress = {} of String => Hash(String, Int32) + property display_name = "" + # { entry1 : "display name" } + property entry_display_name = {} of String => String - @[JSON::Field(ignore: true)] - property dir : String = "" + @[JSON::Field(ignore: true)] + property dir : String = "" - def initialize(@dir) - json_path = File.join @dir, "info.json" - if File.exists? json_path - info = TitleInfo.from_json File.read json_path - @progress = info.progress.clone - @display_name = info.display_name - @entry_display_name = info.entry_display_name.clone - end - end + def initialize(@dir) + json_path = File.join @dir, "info.json" + if File.exists? json_path + info = TitleInfo.from_json File.read json_path + @progress = info.progress.clone + @display_name = info.display_name + @entry_display_name = info.entry_display_name.clone + end + end - def save - json_path = File.join @dir, "info.json" - File.write json_path, self.to_pretty_json - end + def save + json_path = File.join @dir, "info.json" + File.write json_path, self.to_pretty_json + end end class Library - property dir : String, title_ids : Array(String), scan_interval : Int32, - logger : Logger, storage : Storage, title_hash : Hash(String, Title) + property dir : String, title_ids : Array(String), scan_interval : Int32, + logger : Logger, storage : Storage, title_hash : Hash(String, Title) - 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 - @title_ids = [] of String - @title_hash = {} of String => Title + 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 + @title_ids = [] of String + @title_hash = {} of String => Title - return scan if @scan_interval < 1 - 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 * 60 - end - end - end - def titles - @title_ids.map {|tid| self.get_title!(tid) } - 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 - @title_ids.clear - (Dir.entries @dir) - .select { |fn| !fn.starts_with? "." } - .map { |fn| File.join @dir, fn } - .select { |path| File.directory? path } - .map { |path| Title.new path, "", @storage, @logger, 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" - end + return scan if @scan_interval < 1 + 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 * 60 + end + end + end + + def titles + @title_ids.map { |tid| self.get_title!(tid) } + 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 + @title_ids.clear + (Dir.entries @dir) + .select { |fn| !fn.starts_with? "." } + .map { |fn| File.join @dir, fn } + .select { |path| File.directory? path } + .map { |path| Title.new path, "", @storage, @logger, 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" + end end diff --git a/src/log_handler.cr b/src/log_handler.cr index fb0bcca..e1a1f4f 100644 --- a/src/log_handler.cr +++ b/src/log_handler.cr @@ -2,25 +2,25 @@ require "kemal" require "./logger" class LogHandler < Kemal::BaseLogHandler - def initialize(@logger : Logger) - end + 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) - env - 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 + env + end - def write(msg) - @logger.debug(msg) - end + def write(msg) + @logger.debug msg + end - private def elapsed_text(elapsed) - millis = elapsed.total_milliseconds - return "#{millis.round(2)}ms" if millis >= 1 - "#{(millis * 1000).round(2)}µs" - end + private def elapsed_text(elapsed) + millis = elapsed.total_milliseconds + return "#{millis.round(2)}ms" if millis >= 1 + "#{(millis * 1000).round(2)}µs" + end end diff --git a/src/logger.cr b/src/logger.cr index f00c73b..6a7536a 100644 --- a/src/logger.cr +++ b/src/logger.cr @@ -2,57 +2,57 @@ require "log" require "colorize" class Logger - LEVELS = ["debug", "error", "fatal", "info", "warn"] - SEVERITY_IDS = [0, 4, 5, 2, 3] - COLORS = [:light_cyan, :light_red, :red, :light_yellow, :light_magenta] + LEVELS = ["debug", "error", "fatal", "info", "warn"] + SEVERITY_IDS = [0, 4, 5, 2, 3] + COLORS = [:light_cyan, :light_red, :red, :light_yellow, :light_magenta] - @@severity : Log::Severity = :info + @@severity : Log::Severity = :info - def initialize(level : String) - {% begin %} - case level.downcase - when "off" - @@severity = :none - {% for lvl, i in LEVELS %} - when {{lvl}} - @@severity = Log::Severity.new SEVERITY_IDS[{{i}}] - {% end %} - else - raise "Unknown log level #{level}" - end - {% end %} + def initialize(level : String) + {% begin %} + case level.downcase + when "off" + @@severity = :none + {% for lvl, i in LEVELS %} + when {{lvl}} + @@severity = Log::Severity.new SEVERITY_IDS[{{i}}] + {% end %} + else + raise "Unknown log level #{level}" + end + {% end %} - @log = Log.for("") + @log = Log.for("") - @backend = Log::IOBackend.new - @backend.formatter = ->(entry : Log::Entry, io : IO) do - color = :default - {% begin %} - case entry.severity.label.to_s().downcase - {% for lvl, i in LEVELS %} - when {{lvl}}, "#{{{lvl}}}ing" - color = COLORS[{{i}}] - {% end %} - else - end - {% end %} + @backend = Log::IOBackend.new + @backend.formatter = ->(entry : Log::Entry, io : IO) do + color = :default + {% begin %} + case entry.severity.label.to_s().downcase + {% for lvl, i in LEVELS %} + when {{lvl}}, "#{{{lvl}}}ing" + color = COLORS[{{i}}] + {% end %} + else + end + {% end %} - io << "[#{entry.severity.label}]".ljust(10).colorize(color) - io << entry.timestamp.to_s("%Y/%m/%d %H:%M:%S") << " | " - io << entry.message - end + io << "[#{entry.severity.label}]".ljust(10).colorize(color) + io << entry.timestamp.to_s("%Y/%m/%d %H:%M:%S") << " | " + io << entry.message + end - Log.builder.bind "*", @@severity, @backend - end + Log.builder.bind "*", @@severity, @backend + end - # Ignores @@severity and always log msg - def log(msg) - @backend.write Log::Entry.new "", Log::Severity::None, msg, nil - end + # Ignores @@severity and always log msg + def log(msg) + @backend.write Log::Entry.new "", Log::Severity::None, msg, nil + end - {% for lvl in LEVELS %} - def {{lvl.id}}(msg) - @log.{{lvl.id}} { msg } - end - {% end %} + {% for lvl in LEVELS %} + def {{lvl.id}}(msg) + @log.{{lvl.id}} { msg } + end + {% end %} end diff --git a/src/mangadex/api.cr b/src/mangadex/api.cr index 0cf181c..e79320b 100644 --- a/src/mangadex/api.cr +++ b/src/mangadex/api.cr @@ -2,202 +2,200 @@ require "http/client" require "json" require "csv" -macro string_properties (names) - {% for name in names %} - property {{name.id}} = "" - {% end %} +macro string_properties(names) + {% for name in names %} + property {{name.id}} = "" + {% end %} end -macro parse_strings_from_json (names) - {% for name in names %} - @{{name.id}} = obj[{{name}}].as_s - {% end %} +macro parse_strings_from_json(names) + {% for name in names %} + @{{name.id}} = obj[{{name}}].as_s + {% end %} end module MangaDex - class Chapter - string_properties ["lang_code", "title", "volume", "chapter"] - property manga : Manga - property time = Time.local - property id : String - property full_title = "" - property language = "" - property pages = [] of {String, String} # filename, url - property groups = [] of {Int32, String} # group_id, group_name + class Chapter + string_properties ["lang_code", "title", "volume", "chapter"] + property manga : Manga + property time = Time.local + property id : String + property full_title = "" + property language = "" + property pages = [] of {String, String} # filename, url + property groups = [] of {Int32, String} # group_id, group_name - def initialize(@id, json_obj : JSON::Any, @manga, lang : - Hash(String, String)) - self.parse_json json_obj, lang - end + def initialize(@id, json_obj : JSON::Any, @manga, + lang : Hash(String, String)) + self.parse_json json_obj, lang + end - def to_info_json - JSON.build do |json| - json.object do - {% for name in ["id", "title", "volume", "chapter", - "language", "full_title"] %} - json.field {{name}}, @{{name.id}} - {% end %} - json.field "time", @time.to_unix.to_s - json.field "manga_title", @manga.title - json.field "manga_id", @manga.id - json.field "groups" do - json.object do - @groups.each do |gid, gname| - json.field gname, gid - end - end - end - end - end - end + def to_info_json + JSON.build do |json| + json.object do + {% for name in ["id", "title", "volume", "chapter", + "language", "full_title"] %} + json.field {{name}}, @{{name.id}} + {% end %} + json.field "time", @time.to_unix.to_s + json.field "manga_title", @manga.title + json.field "manga_id", @manga.id + json.field "groups" do + json.object do + @groups.each do |gid, gname| + json.field gname, gid + end + end + end + end + end + end - def parse_json(obj, lang) - begin - parse_strings_from_json ["lang_code", "title", "volume", - "chapter"] - language = lang[@lang_code]? - @language = language if language - @time = Time.unix obj["timestamp"].as_i - suffixes = ["", "_2", "_3"] - suffixes.each do |s| - gid = obj["group_id#{s}"].as_i - next if gid == 0 - gname = obj["group_name#{s}"].as_s - @groups << {gid, gname} - end - @full_title = @title - unless @chapter.empty? - @full_title = "Ch.#{@chapter} " + @full_title - end - unless @volume.empty? - @full_title = "Vol.#{@volume} " + @full_title - end - rescue e - raise "failed to parse json: #{e}" - end - end - end - class Manga - string_properties ["cover_url", "description", "title", "author", - "artist"] - property chapters = [] of Chapter - property id : String + def parse_json(obj, lang) + begin + parse_strings_from_json ["lang_code", "title", "volume", + "chapter"] + language = lang[@lang_code]? + @language = language if language + @time = Time.unix obj["timestamp"].as_i + suffixes = ["", "_2", "_3"] + suffixes.each do |s| + gid = obj["group_id#{s}"].as_i + next if gid == 0 + gname = obj["group_name#{s}"].as_s + @groups << {gid, gname} + end + @full_title = @title + unless @chapter.empty? + @full_title = "Ch.#{@chapter} " + @full_title + end + unless @volume.empty? + @full_title = "Vol.#{@volume} " + @full_title + end + rescue e + raise "failed to parse json: #{e}" + end + end + end - def initialize(@id, json_obj : JSON::Any) - self.parse_json json_obj - end + class Manga + string_properties ["cover_url", "description", "title", "author", "artist"] + property chapters = [] of Chapter + property id : String - def to_info_json(with_chapters = true) - JSON.build do |json| - json.object do - {% for name in ["id", "title", "description", - "author", "artist", "cover_url"] %} - json.field {{name}}, @{{name.id}} - {% end %} - if with_chapters - json.field "chapters" do - json.array do - @chapters.each do |c| - json.raw c.to_info_json - end - end - end - end - end - end - end + def initialize(@id, json_obj : JSON::Any) + self.parse_json json_obj + end - def parse_json(obj) - begin - parse_strings_from_json ["cover_url", "description", "title", - "author", "artist"] - rescue e - raise "failed to parse json: #{e}" - end - end - end - class API - def initialize(@base_url = "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] - end - end + def to_info_json(with_chapters = true) + JSON.build do |json| + json.object do + {% for name in ["id", "title", "description", "author", "artist", + "cover_url"] %} + json.field {{name}}, @{{name.id}} + {% end %} + if with_chapters + json.field "chapters" do + json.array do + @chapters.each do |c| + json.raw c.to_info_json + end + end + end + end + end + end + end - def get(url) - headers = HTTP::Headers { - "User-agent" => "Mangadex.cr" - } - res = HTTP::Client.get url, headers - raise "Failed to get #{url}. [#{res.status_code}] "\ - "#{res.status_message}" if !res.success? - JSON.parse res.body - end + def parse_json(obj) + begin + parse_strings_from_json ["cover_url", "description", "title", "author", + "artist"] + rescue e + raise "failed to parse json: #{e}" + end + end + end - def get_manga(id) - obj = self.get File.join @base_url, "manga/#{id}" - if obj["status"]? != "OK" - raise "Expecting `OK` in the `status` field. " \ - "Got `#{obj["status"]?}`" - end - begin - manga = Manga.new id, obj["manga"] - obj["chapter"].as_h.map do |k, v| - chapter = Chapter.new k, v, manga, @lang - manga.chapters << chapter - end - return manga - rescue - raise "Failed to parse JSON" - end - end + class API + def initialize(@base_url = "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] + end + end - def get_chapter(chapter : Chapter) - obj = self.get File.join @base_url, "chapter/#{chapter.id}" - if obj["status"]? == "external" - raise "This chapter is hosted on an external site " \ - "#{obj["external"]?}, and Mango does not support " \ - "external chapters." - end - if obj["status"]? != "OK" - raise "Expecting `OK` in the `status` field. " \ - "Got `#{obj["status"]?}`" - end - begin - server = obj["server"].as_s - hash = obj["hash"].as_s - chapter.pages = obj["page_array"].as_a.map do |fn| - { - fn.as_s, - "#{server}#{hash}/#{fn.as_s}" - } - end - rescue - raise "Failed to parse JSON" - end - end + def get(url) + headers = HTTP::Headers{ + "User-agent" => "Mangadex.cr", + } + res = HTTP::Client.get url, headers + raise "Failed to get #{url}. [#{res.status_code}] " \ + "#{res.status_message}" if !res.success? + JSON.parse res.body + end - def get_chapter(id : String) - obj = self.get File.join @base_url, "chapter/#{id}" - if obj["status"]? == "external" - raise "This chapter is hosted on an external site " \ - "#{obj["external"]?}, and Mango does not support " \ - "external chapters." - end - if obj["status"]? != "OK" - raise "Expecting `OK` in the `status` field. " \ - "Got `#{obj["status"]?}`" - end - manga_id = "" - begin - manga_id = obj["manga_id"].as_i.to_s - rescue - raise "Failed to parse JSON" - end - manga = self.get_manga manga_id - chapter = manga.chapters.find {|c| c.id == id}.not_nil! - self.get_chapter chapter - return chapter - end - end + def get_manga(id) + obj = self.get File.join @base_url, "manga/#{id}" + if obj["status"]? != "OK" + raise "Expecting `OK` in the `status` field. Got `#{obj["status"]?}`" + end + begin + manga = Manga.new id, obj["manga"] + obj["chapter"].as_h.map do |k, v| + chapter = Chapter.new k, v, manga, @lang + manga.chapters << chapter + end + return manga + rescue + raise "Failed to parse JSON" + end + end + + def get_chapter(chapter : Chapter) + obj = self.get File.join @base_url, "chapter/#{chapter.id}" + if obj["status"]? == "external" + raise "This chapter is hosted on an external site " \ + "#{obj["external"]?}, and Mango does not support " \ + "external chapters." + end + if obj["status"]? != "OK" + raise "Expecting `OK` in the `status` field. Got `#{obj["status"]?}`" + end + begin + server = obj["server"].as_s + hash = obj["hash"].as_s + chapter.pages = obj["page_array"].as_a.map do |fn| + { + fn.as_s, + "#{server}#{hash}/#{fn.as_s}", + } + end + rescue + raise "Failed to parse JSON" + end + end + + def get_chapter(id : String) + obj = self.get File.join @base_url, "chapter/#{id}" + if obj["status"]? == "external" + raise "This chapter is hosted on an external site " \ + "#{obj["external"]?}, and Mango does not support " \ + "external chapters." + end + if obj["status"]? != "OK" + raise "Expecting `OK` in the `status` field. Got `#{obj["status"]?}`" + end + manga_id = "" + begin + manga_id = obj["manga_id"].as_i.to_s + rescue + raise "Failed to parse JSON" + end + manga = self.get_manga manga_id + chapter = manga.chapters.find { |c| c.id == id }.not_nil! + self.get_chapter chapter + return chapter + end + end end diff --git a/src/mangadex/downloader.cr b/src/mangadex/downloader.cr index 266396d2..231b790 100644 --- a/src/mangadex/downloader.cr +++ b/src/mangadex/downloader.cr @@ -2,373 +2,374 @@ require "./api" require "sqlite3" module MangaDex - class PageJob - property success = false - property url : String - property filename : String - property writer : Zip::Writer - property tries_remaning : Int32 - def initialize(@url, @filename, @writer, @tries_remaning) - end - end + class PageJob + property success = false + property url : String + property filename : String + property writer : Zip::Writer + property tries_remaning : Int32 - enum JobStatus - Pending # 0 - Downloading # 1 - Error # 2 - Completed # 3 - MissingPages # 4 - end + def initialize(@url, @filename, @writer, @tries_remaning) + end + end - struct Job - property id : String - property manga_id : String - property title : String - property manga_title : String - property status : JobStatus - property status_message : String = "" - property pages : Int32 = 0 - property success_count : Int32 = 0 - property fail_count : Int32 = 0 - property time : Time + enum JobStatus + Pending # 0 + Downloading # 1 + Error # 2 + Completed # 3 + MissingPages # 4 + end - def parse_query_result(res : DB::ResultSet) - @id = res.read String - @manga_id = res.read String - @title = res.read String - @manga_title = res.read String - status = res.read Int32 - @status_message = res.read String - @pages = res.read Int32 - @success_count = res.read Int32 - @fail_count = res.read Int32 - time = res.read Int64 - @status = JobStatus.new status - @time = Time.unix_ms time - end + struct Job + property id : String + property manga_id : String + property title : String + property manga_title : String + property status : JobStatus + property status_message : String = "" + property pages : Int32 = 0 + property success_count : Int32 = 0 + property fail_count : Int32 = 0 + property time : Time - # Raises if the result set does not contain the correct set of columns - def self.from_query_result(res : DB::ResultSet) - job = Job.allocate - job.parse_query_result res - return job - end + def parse_query_result(res : DB::ResultSet) + @id = res.read String + @manga_id = res.read String + @title = res.read String + @manga_title = res.read String + status = res.read Int32 + @status_message = res.read String + @pages = res.read Int32 + @success_count = res.read Int32 + @fail_count = res.read Int32 + time = res.read Int64 + @status = JobStatus.new status + @time = Time.unix_ms time + end - def initialize(@id, @manga_id, @title, @manga_title, @status, @time) - end + # Raises if the result set does not contain the correct set of columns + def self.from_query_result(res : DB::ResultSet) + job = Job.allocate + job.parse_query_result res + return job + end - def to_json(json) - json.object do - {% for name in ["id", "manga_id", "title", "manga_title", - "status_message"] %} - json.field {{name}}, @{{name.id}} - {% end %} - {% for name in ["pages", "success_count", "fail_count"] %} - json.field {{name}} do - json.number @{{name.id}} - end - {% end %} - json.field "status", @status.to_s - json.field "time" do - json.number @time.to_unix_ms - end - end - end - end + def initialize(@id, @manga_id, @title, @manga_title, @status, @time) + end - class Queue - property downloader : Downloader? + def to_json(json) + json.object do + {% for name in ["id", "manga_id", "title", "manga_title", + "status_message"] %} + json.field {{name}}, @{{name.id}} + {% end %} + {% for name in ["pages", "success_count", "fail_count"] %} + json.field {{name}} do + json.number @{{name.id}} + end + {% end %} + json.field "status", @status.to_s + json.field "time" do + json.number @time.to_unix_ms + end + end + end + end - def initialize(@path : String, @logger : Logger) - dir = File.dirname path - unless Dir.exists? dir - @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| - begin - db.exec "create table if not exists queue " \ - "(id text, manga_id text, title text, manga_title " \ - "text, status integer, status_message text, " \ - "pages integer, success_count integer, " \ - "fail_count integer, time integer)" - db.exec "create unique index if not exists id_idx " \ - "on queue (id)" - db.exec "create index if not exists manga_id_idx " \ - "on queue (manga_id)" - db.exec "create index if not exists status_idx " \ - "on queue (status)" - rescue e - @logger.error "Error when checking tables in DB: #{e}" - raise e - end - end - end + class Queue + property downloader : Downloader? - # Returns the earliest job in queue or nil if the job cannot be parsed. - # Returns nil if queue is empty - def pop - job = nil - DB.open "sqlite3://#{@path}" do |db| - begin - db.query_one "select * from queue where status = 0 "\ - "or status = 1 order by time limit 1" do |res| - job = Job.from_query_result res - end - rescue - end - end - return job - end + def initialize(@path : String, @logger : Logger) + dir = File.dirname path + unless Dir.exists? dir + @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| + begin + db.exec "create table if not exists queue " \ + "(id text, manga_id text, title text, manga_title " \ + "text, status integer, status_message text, " \ + "pages integer, success_count integer, " \ + "fail_count integer, time integer)" + db.exec "create unique index if not exists id_idx " \ + "on queue (id)" + db.exec "create index if not exists manga_id_idx " \ + "on queue (manga_id)" + db.exec "create index if not exists status_idx " \ + "on queue (status)" + rescue e + @logger.error "Error when checking tables in DB: #{e}" + raise e + end + end + end - # Push an array of jobs into the queue, and return the number of jobs - # inserted. Any job already exists in the queue will be ignored. - def push(jobs : Array(Job)) - start_count = self.count - DB.open "sqlite3://#{@path}" do |db| - jobs.each do |job| - db.exec "insert or ignore into queue values "\ - "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - job.id, job.manga_id, job.title, job.manga_title, - job.status.to_i, job.status_message, job.pages, - job.success_count, job.fail_count, job.time.to_unix_ms - end - end - self.count - start_count - end + # Returns the earliest job in queue or nil if the job cannot be parsed. + # Returns nil if queue is empty + def pop + job = nil + DB.open "sqlite3://#{@path}" do |db| + begin + db.query_one "select * from queue where status = 0 " \ + "or status = 1 order by time limit 1" do |res| + job = Job.from_query_result res + end + rescue + end + end + return job + end - def reset(id : String) - DB.open "sqlite3://#{@path}" do |db| - db.exec "update queue set status = 0, status_message = '', " \ - "pages = 0, success_count = 0, fail_count = 0 " \ - "where id = (?)", id - end - end + # Push an array of jobs into the queue, and return the number of jobs + # inserted. Any job already exists in the queue will be ignored. + def push(jobs : Array(Job)) + start_count = self.count + DB.open "sqlite3://#{@path}" do |db| + jobs.each do |job| + db.exec "insert or ignore into queue values " \ + "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + job.id, job.manga_id, job.title, job.manga_title, + job.status.to_i, job.status_message, job.pages, + job.success_count, job.fail_count, job.time.to_unix_ms + end + end + self.count - start_count + end - def reset (job : Job) - self.reset job.id - end + def reset(id : String) + DB.open "sqlite3://#{@path}" do |db| + db.exec "update queue set status = 0, status_message = '', " \ + "pages = 0, success_count = 0, fail_count = 0 " \ + "where id = (?)", id + end + end - # Reset all failed tasks (missing pages and error) - def reset - DB.open "sqlite3://#{@path}" do |db| - db.exec "update queue set status = 0, status_message = '', " \ - "pages = 0, success_count = 0, fail_count = 0 " \ - "where status = 2 or status = 4" - end - end + def reset(job : Job) + self.reset job.id + end - def delete(id : String) - DB.open "sqlite3://#{@path}" do |db| - db.exec "delete from queue where id = (?)", id - end - end + # Reset all failed tasks (missing pages and error) + def reset + DB.open "sqlite3://#{@path}" do |db| + db.exec "update queue set status = 0, status_message = '', " \ + "pages = 0, success_count = 0, fail_count = 0 " \ + "where status = 2 or status = 4" + end + end - def delete(job : Job) - self.delete job.id - end + def delete(id : String) + DB.open "sqlite3://#{@path}" do |db| + db.exec "delete from queue where id = (?)", id + end + end - def delete_status(status : JobStatus) - DB.open "sqlite3://#{@path}" do |db| - db.exec "delete from queue where status = (?)", status.to_i - end - end + def delete(job : Job) + self.delete job.id + end - def count_status(status : JobStatus) - DB.open "sqlite3://#{@path}" do |db| - return db.query_one "select count(*) from queue where "\ - "status = (?)", status.to_i, as: Int32 - end - end + def delete_status(status : JobStatus) + DB.open "sqlite3://#{@path}" do |db| + db.exec "delete from queue where status = (?)", status.to_i + end + end - def count - DB.open "sqlite3://#{@path}" do |db| - return db.query_one "select count(*) from queue", as: Int32 - end - end + def count_status(status : JobStatus) + DB.open "sqlite3://#{@path}" do |db| + return db.query_one "select count(*) from queue where " \ + "status = (?)", status.to_i, as: Int32 + end + end - def set_status(status : JobStatus, job : Job) - DB.open "sqlite3://#{@path}" do |db| - db.exec "update queue set status = (?) where id = (?)", - status.to_i, job.id - end - end + def count + DB.open "sqlite3://#{@path}" do |db| + return db.query_one "select count(*) from queue", as: Int32 + end + end - def get_all - jobs = [] of Job - DB.open "sqlite3://#{@path}" do |db| - jobs = db.query_all "select * from queue order by time", do |rs| - Job.from_query_result rs - end - end - return jobs - end + def set_status(status : JobStatus, job : Job) + DB.open "sqlite3://#{@path}" do |db| + db.exec "update queue set status = (?) where id = (?)", + status.to_i, job.id + end + end - def add_success(job : Job) - DB.open "sqlite3://#{@path}" do |db| - db.exec "update queue set success_count = success_count + 1 " \ - "where id = (?)", job.id - end - end + def get_all + jobs = [] of Job + DB.open "sqlite3://#{@path}" do |db| + jobs = db.query_all "select * from queue order by time" do |rs| + Job.from_query_result rs + end + end + return jobs + end - def add_fail(job : Job) - DB.open "sqlite3://#{@path}" do |db| - db.exec "update queue set fail_count = fail_count + 1 " \ - "where id = (?)", job.id - end - end + def add_success(job : Job) + DB.open "sqlite3://#{@path}" do |db| + db.exec "update queue set success_count = success_count + 1 " \ + "where id = (?)", job.id + end + end - def set_pages(pages : Int32, job : Job) - DB.open "sqlite3://#{@path}" do |db| - db.exec "update queue set pages = (?), success_count = 0, " \ - "fail_count = 0 where id = (?)", pages, job.id - end - end + def add_fail(job : Job) + DB.open "sqlite3://#{@path}" do |db| + db.exec "update queue set fail_count = fail_count + 1 " \ + "where id = (?)", job.id + end + end - def add_message(msg : String, job : Job) - DB.open "sqlite3://#{@path}" do |db| - db.exec "update queue set status_message = " \ - "status_message || (?) || (?) where id = (?)", - "\n", msg, job.id - end - end + def set_pages(pages : Int32, job : Job) + DB.open "sqlite3://#{@path}" do |db| + db.exec "update queue set pages = (?), success_count = 0, " \ + "fail_count = 0 where id = (?)", pages, job.id + end + end - def pause - @downloader.not_nil!.stopped = true - end + def add_message(msg : String, job : Job) + DB.open "sqlite3://#{@path}" do |db| + db.exec "update queue set status_message = " \ + "status_message || (?) || (?) where id = (?)", + "\n", msg, job.id + end + end - def resume - @downloader.not_nil!.stopped = false - end + def pause + @downloader.not_nil!.stopped = true + end - def paused? - @downloader.not_nil!.stopped - end - end + def resume + @downloader.not_nil!.stopped = false + end - class Downloader - property stopped = false - @downloading = false + def paused? + @downloader.not_nil!.stopped + end + end - def initialize(@queue : Queue, @api : API, @library_path : String, - @wait_seconds : Int32, @retries : Int32, - @logger : Logger) - @queue.downloader = self + class Downloader + property stopped = false + @downloading = false - spawn do - loop do - sleep 1.second - next if @stopped || @downloading - begin - job = @queue.pop - next if job.nil? - download job - rescue e - @logger.error e - end - end - end - end + def initialize(@queue : Queue, @api : API, @library_path : String, + @wait_seconds : Int32, @retries : Int32, + @logger : Logger) + @queue.downloader = self - private def download(job : Job) - @downloading = true - @queue.set_status JobStatus::Downloading, job - begin - chapter = @api.get_chapter(job.id) - rescue e - @logger.error e - @queue.set_status JobStatus::Error, job - unless e.message.nil? - @queue.add_message e.message.not_nil!, job - end - @downloading = false - return - end - @queue.set_pages chapter.pages.size, job - lib_dir = @library_path - manga_dir = File.join lib_dir, chapter.manga.title - unless File.exists? manga_dir - Dir.mkdir_p manga_dir - end - zip_path = File.join manga_dir, "#{job.title}.cbz" + spawn do + loop do + sleep 1.second + next if @stopped || @downloading + begin + job = @queue.pop + next if job.nil? + download job + rescue e + @logger.error e + end + end + end + end - # Find the number of digits needed to store the number of pages - len = Math.log10(chapter.pages.size).to_i + 1 + private def download(job : Job) + @downloading = true + @queue.set_status JobStatus::Downloading, job + begin + chapter = @api.get_chapter(job.id) + rescue e + @logger.error e + @queue.set_status JobStatus::Error, job + unless e.message.nil? + @queue.add_message e.message.not_nil!, job + end + @downloading = false + return + end + @queue.set_pages chapter.pages.size, job + lib_dir = @library_path + manga_dir = File.join lib_dir, chapter.manga.title + unless File.exists? manga_dir + Dir.mkdir_p manga_dir + end + zip_path = File.join manga_dir, "#{job.title}.cbz" - writer = Zip::Writer.new zip_path - # Create a buffered channel. It works as an FIFO queue - channel = Channel(PageJob).new chapter.pages.size - spawn do - chapter.pages.each_with_index do |tuple, i| - fn, url = tuple - ext = File.extname fn - fn = "#{i.to_s.rjust len, '0'}#{ext}" - page_job = PageJob.new url, fn, writer, @retries - @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}" - end + # Find the number of digits needed to store the number of pages + len = Math.log10(chapter.pages.size).to_i + 1 - channel.send page_job - end - end + writer = Zip::Writer.new zip_path + # Create a buffered channel. It works as an FIFO queue + channel = Channel(PageJob).new chapter.pages.size + spawn do + chapter.pages.each_with_index do |tuple, i| + fn, url = tuple + ext = File.extname fn + fn = "#{i.to_s.rjust len, '0'}#{ext}" + page_job = PageJob.new url, fn, writer, @retries + @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}" + end - spawn do - page_jobs = [] of PageJob - chapter.pages.size.times do - page_job = channel.receive - @logger.debug "[#{page_job.success ? "success" : "failed"}] " \ - "#{page_job.url}" - page_jobs << page_job - if page_job.success - @queue.add_success job - else - @queue.add_fail job - msg = "Failed to download page #{page_job.url}" - @queue.add_message msg, job - @logger.error msg - end - end - fail_count = page_jobs.select{|j| !j.success}.size - @logger.debug "Download completed. "\ - "#{fail_count}/#{page_jobs.size} failed" - writer.close - @logger.debug "cbz File created at #{zip_path}" - if fail_count == 0 - @queue.set_status JobStatus::Completed, job - else - @queue.set_status JobStatus::MissingPages, job - end - @downloading = false - end - end + channel.send page_job + end + end - private def download_page(job : PageJob) - @logger.debug "downloading #{job.url}" - headers = HTTP::Headers { - "User-agent" => "Mangadex.cr" - } - begin - HTTP::Client.get job.url, headers do |res| - unless res.success? - raise "Failed to download page #{job.url}. " \ - "[#{res.status_code}] #{res.status_message}" - end - job.writer.add job.filename, res.body_io - end - job.success = true - rescue e - @logger.error e - job.success = false - end - end - end + spawn do + page_jobs = [] of PageJob + chapter.pages.size.times do + page_job = channel.receive + @logger.debug "[#{page_job.success ? "success" : "failed"}] " \ + "#{page_job.url}" + page_jobs << page_job + if page_job.success + @queue.add_success job + else + @queue.add_fail job + msg = "Failed to download page #{page_job.url}" + @queue.add_message msg, job + @logger.error msg + end + end + fail_count = page_jobs.select { |j| !j.success }.size + @logger.debug "Download completed. " \ + "#{fail_count}/#{page_jobs.size} failed" + writer.close + @logger.debug "cbz File created at #{zip_path}" + if fail_count == 0 + @queue.set_status JobStatus::Completed, job + else + @queue.set_status JobStatus::MissingPages, job + end + @downloading = false + end + end + + private def download_page(job : PageJob) + @logger.debug "downloading #{job.url}" + headers = HTTP::Headers{ + "User-agent" => "Mangadex.cr", + } + begin + HTTP::Client.get job.url, headers do |res| + unless res.success? + raise "Failed to download page #{job.url}. " \ + "[#{res.status_code}] #{res.status_message}" + end + job.writer.add job.filename, res.body_io + end + job.success = true + rescue e + @logger.error e + job.success = false + end + end + end end diff --git a/src/mango.cr b/src/mango.cr index 29da3d0..c11dcc7 100644 --- a/src/mango.cr +++ b/src/mango.cr @@ -8,20 +8,20 @@ VERSION = "0.2.5" config_path = nil parser = OptionParser.parse do |parser| - parser.banner = "Mango e-manga server/reader. Version #{VERSION}\n" + parser.banner = "Mango e-manga server/reader. Version #{VERSION}\n" - parser.on "-v", "--version", "Show version" do - puts "Version #{VERSION}" - exit - end - parser.on "-h", "--help", "Show help" do - puts parser - exit - end - parser.on "-c PATH", "--config=PATH", "Path to the config file. " \ - "Default is `~/.config/mango/config.yml`" do |path| - config_path = path - end + parser.on "-v", "--version", "Show version" do + puts "Version #{VERSION}" + exit + end + parser.on "-h", "--help", "Show help" do + puts parser + exit + end + parser.on "-c PATH", "--config=PATH", + "Path to the config file. Default is `~/.config/mango/config.yml`" do |path| + config_path = path + end end config = Config.load config_path @@ -29,11 +29,11 @@ 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 + logger +api = MangaDex::API.new config.mangadex["api_url"].to_s downloader = MangaDex::Downloader.new queue, api, config.library_path, - config.mangadex["download_wait_seconds"].to_i, - config.mangadex["download_retries"].to_i, logger + config.mangadex["download_wait_seconds"].to_i, + config.mangadex["download_retries"].to_i, logger context = Context.new config, logger, library, storage, queue diff --git a/src/routes/admin.cr b/src/routes/admin.cr index 02ca4c1..944941e 100644 --- a/src/routes/admin.cr +++ b/src/routes/admin.cr @@ -1,108 +1,107 @@ require "./router" class AdminRouter < Router - def setup - get "/admin" do |env| - layout "admin" - end + def setup + get "/admin" do |env| + layout "admin" + end - get "/admin/user" do |env| - users = @context.storage.list_users - username = get_username env - layout "user" - end + get "/admin/user" do |env| + users = @context.storage.list_users + username = get_username env + layout "user" + end - get "/admin/user/edit" do |env| - username = env.params.query["username"]? - admin = env.params.query["admin"]? - if admin - admin = admin == "true" - end - error = env.params.query["error"]? - current_user = get_username env - new_user = username.nil? && admin.nil? - layout "user-edit" - end + get "/admin/user/edit" do |env| + username = env.params.query["username"]? + admin = env.params.query["admin"]? + if admin + admin = admin == "true" + end + error = env.params.query["error"]? + current_user = get_username env + new_user = username.nil? && admin.nil? + layout "user-edit" + end - post "/admin/user/edit" do |env| - # creating new user - begin - username = env.params.body["username"] - password = env.params.body["password"] - # if `admin` is unchecked, the body hash - # would not contain `admin` - admin = !env.params.body["admin"]?.nil? + post "/admin/user/edit" do |env| + # creating new user + begin + username = env.params.body["username"] + password = env.params.body["password"] + # if `admin` is unchecked, the body hash + # would not contain `admin` + admin = !env.params.body["admin"]?.nil? - if username.size < 3 - raise "Username should contain at least 3 characters" - end - if (username =~ /^[A-Za-z0-9_]+$/).nil? - raise "Username should contain alphanumeric characters "\ - "and underscores only" - end - if password.size < 6 - raise "Password should contain at least 6 characters" - end - if (password =~ /^[[:ascii:]]+$/).nil? - raise "password should contain ASCII characters only" - end + if username.size < 3 + raise "Username should contain at least 3 characters" + end + if (username =~ /^[A-Za-z0-9_]+$/).nil? + raise "Username should contain alphanumeric characters " \ + "and underscores only" + end + if password.size < 6 + raise "Password should contain at least 6 characters" + end + if (password =~ /^[[:ascii:]]+$/).nil? + raise "password should contain ASCII characters only" + end - @context.storage.new_user username, password, admin + @context.storage.new_user username, password, admin - env.redirect "/admin/user" - rescue e - @context.error e - redirect_url = URI.new \ - path: "/admin/user/edit",\ - query: hash_to_query({"error" => e.message}) - env.redirect redirect_url.to_s - end - end + env.redirect "/admin/user" + rescue e + @context.error e + redirect_url = URI.new \ + path: "/admin/user/edit", + query: hash_to_query({"error" => e.message}) + env.redirect redirect_url.to_s + end + end - post "/admin/user/edit/:original_username" do |env| - # editing existing user - begin - username = env.params.body["username"] - password = env.params.body["password"] - # if `admin` is unchecked, the body - # hash would not contain `admin` - admin = !env.params.body["admin"]?.nil? - original_username = env.params.url["original_username"] + post "/admin/user/edit/:original_username" do |env| + # editing existing user + begin + username = env.params.body["username"] + password = env.params.body["password"] + # if `admin` is unchecked, the body hash would not contain `admin` + admin = !env.params.body["admin"]?.nil? + original_username = env.params.url["original_username"] - if username.size < 3 - raise "Username should contain at least 3 characters" - end - if (username =~ /^[A-Za-z0-9_]+$/).nil? - raise "Username should contain alphanumeric characters "\ - "and underscores only" - end + if username.size < 3 + raise "Username should contain at least 3 characters" + end + if (username =~ /^[A-Za-z0-9_]+$/).nil? + raise "Username should contain alphanumeric characters " \ + "and underscores only" + end - if password.size != 0 - if password.size < 6 - raise "Password should contain at least 6 characters" - end - if (password =~ /^[[:ascii:]]+$/).nil? - raise "password should contain ASCII characters only" - end - end + if password.size != 0 + if password.size < 6 + raise "Password should contain at least 6 characters" + end + if (password =~ /^[[:ascii:]]+$/).nil? + raise "password should contain ASCII characters only" + end + end - @context.storage.update_user \ - original_username, username, password, admin + @context.storage.update_user \ + original_username, username, password, admin - env.redirect "/admin/user" - rescue e - @context.error e - redirect_url = URI.new \ - path: "/admin/user/edit",\ - query: hash_to_query({"username" => original_username, \ - "admin" => admin, "error" => e.message}) - env.redirect redirect_url.to_s - end - end + env.redirect "/admin/user" + rescue e + @context.error e + redirect_url = URI.new \ + path: "/admin/user/edit", + query: hash_to_query({"username" => original_username, \ + "admin" => admin, "error" => e.message}) + env.redirect redirect_url.to_s + end + end - get "/admin/downloads" do |env| - base_url = @context.config.mangadex["base_url"]; - layout "download-manager" - end - end + get "/admin/downloads" do |env| + base_url = @context.config.mangadex["base_url"] + layout "download-manager" + end + end end diff --git a/src/routes/api.cr b/src/routes/api.cr index c0423cb..71da618 100644 --- a/src/routes/api.cr +++ b/src/routes/api.cr @@ -2,202 +2,200 @@ require "./router" require "../mangadex/*" class APIRouter < Router - def setup - get "/api/page/:tid/:eid/:page" do |env| - begin - tid = env.params.url["tid"] - eid = env.params.url["eid"] - page = env.params.url["page"].to_i + def setup + get "/api/page/:tid/:eid/:page" do |env| + begin + tid = env.params.url["tid"] + eid = env.params.url["eid"] + page = env.params.url["page"].to_i - title = @context.library.get_title tid - raise "Title ID `#{tid}` not found" if title.nil? - entry = title.get_entry eid - raise "Entry ID `#{eid}` of `#{title.title}` not found" if \ - entry.nil? - img = entry.read_page page - raise "Failed to load page #{page} of " \ - "`#{title.title}/#{entry.title}`" if img.nil? + title = @context.library.get_title tid + raise "Title ID `#{tid}` not found" if title.nil? + entry = title.get_entry eid + raise "Entry ID `#{eid}` of `#{title.title}` not found" if entry.nil? + img = entry.read_page page + raise "Failed to load page #{page} of " \ + "`#{title.title}/#{entry.title}`" if img.nil? - send_img env, img - rescue e - @context.error e - env.response.status_code = 500 - e.message - end - end + send_img env, img + rescue e + @context.error e + env.response.status_code = 500 + e.message + end + end - get "/api/book/:tid" do |env| - begin - tid = env.params.url["tid"] - title = @context.library.get_title tid - raise "Title ID `#{tid}` not found" if title.nil? + get "/api/book/:tid" do |env| + begin + tid = env.params.url["tid"] + title = @context.library.get_title tid + raise "Title ID `#{tid}` not found" if title.nil? - send_json env, title.to_json - rescue e - @context.error e - env.response.status_code = 500 - e.message - end - end + send_json env, title.to_json + rescue e + @context.error e + env.response.status_code = 500 + e.message + end + end - get "/api/book" do |env| - send_json env, @context.library.to_json - end + get "/api/book" do |env| + send_json env, @context.library.to_json + end - post "/api/admin/scan" do |env| - start = Time.utc - @context.library.scan - ms = (Time.utc - start).total_milliseconds - send_json env, { - "milliseconds" => ms, - "titles" => @context.library.titles.size - }.to_json - end + post "/api/admin/scan" do |env| + start = Time.utc + @context.library.scan + ms = (Time.utc - start).total_milliseconds + send_json env, { + "milliseconds" => ms, + "titles" => @context.library.titles.size, + }.to_json + end - post "/api/admin/user/delete/:username" do |env| - begin - username = env.params.url["username"] - @context.storage.delete_user username - rescue e - @context.error e - send_json env, { - "success" => false, - "error" => e.message - }.to_json - else - send_json env, {"success" => true}.to_json - end - end + post "/api/admin/user/delete/:username" do |env| + begin + username = env.params.url["username"] + @context.storage.delete_user username + rescue e + @context.error e + send_json env, { + "success" => false, + "error" => e.message, + }.to_json + else + send_json env, {"success" => true}.to_json + end + end - post "/api/progress/:title/:entry/:page" do |env| - begin - username = get_username env - title = (@context.library.get_title env.params.url["title"]) - .not_nil! - entry = (title.get_entry env.params.url["entry"]).not_nil! - page = env.params.url["page"].to_i + post "/api/progress/:title/:entry/:page" do |env| + begin + username = get_username env + title = (@context.library.get_title env.params.url["title"]) + .not_nil! + entry = (title.get_entry env.params.url["entry"]).not_nil! + page = env.params.url["page"].to_i - raise "incorrect page value" if page < 0 || page > entry.pages - title.save_progress username, entry.title, page - rescue e - @context.error e - send_json env, { - "success" => false, - "error" => e.message - }.to_json - else - send_json env, {"success" => true}.to_json - end - end + raise "incorrect page value" if page < 0 || page > entry.pages + title.save_progress username, entry.title, page + rescue e + @context.error e + send_json env, { + "success" => false, + "error" => e.message, + }.to_json + else + send_json env, {"success" => true}.to_json + end + end - post "/api/admin/display_name/:title/:name" do |env| - begin - title = (@context.library.get_title env.params.url["title"]) - .not_nil! - name = env.params.url["name"] - entry = env.params.query["entry"]? - if entry.nil? - title.set_display_name name - else - eobj = title.get_entry entry - title.set_display_name eobj.not_nil!.title, name - end - rescue e - @context.error e - send_json env, { - "success" => false, - "error" => e.message - }.to_json - else - send_json env, {"success" => true}.to_json - end - end + post "/api/admin/display_name/:title/:name" do |env| + begin + title = (@context.library.get_title env.params.url["title"]) + .not_nil! + name = env.params.url["name"] + entry = env.params.query["entry"]? + if entry.nil? + title.set_display_name name + else + eobj = title.get_entry entry + title.set_display_name eobj.not_nil!.title, name + end + rescue e + @context.error e + send_json env, { + "success" => false, + "error" => e.message, + }.to_json + else + send_json env, {"success" => true}.to_json + end + end - 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 - manga = api.get_manga id - send_json env, manga.to_info_json - rescue e - @context.error e - send_json env, {"error" => e.message}.to_json - end - end + 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 + manga = api.get_manga id + send_json env, manga.to_info_json + rescue e + @context.error e + send_json env, {"error" => e.message}.to_json + end + end - post "/api/admin/mangadex/download" do |env| - begin - chapters = env.params.json["chapters"].as(Array).map{|c| c.as_h} - jobs = chapters.map {|chapter| - MangaDex::Job.new( - chapter["id"].as_s, - chapter["manga_id"].as_s, - chapter["full_title"].as_s, - chapter["manga_title"].as_s, - MangaDex::JobStatus::Pending, - Time.unix chapter["time"].as_s.to_i - ) - } - inserted_count = @context.queue.push jobs - send_json env, { - "success": inserted_count, - "fail": jobs.size - inserted_count - }.to_json - rescue e - @context.error e - send_json env, {"error" => e.message}.to_json - end - end + post "/api/admin/mangadex/download" do |env| + begin + chapters = env.params.json["chapters"].as(Array).map { |c| c.as_h } + jobs = chapters.map { |chapter| + MangaDex::Job.new( + chapter["id"].as_s, + chapter["manga_id"].as_s, + chapter["full_title"].as_s, + chapter["manga_title"].as_s, + MangaDex::JobStatus::Pending, + Time.unix chapter["time"].as_s.to_i + ) + } + inserted_count = @context.queue.push jobs + send_json env, { + "success": inserted_count, + "fail": jobs.size - inserted_count, + }.to_json + rescue e + @context.error e + send_json env, {"error" => e.message}.to_json + end + end - get "/api/admin/mangadex/queue" do |env| - begin - jobs = @context.queue.get_all - send_json env, { - "jobs" => jobs, - "paused" => @context.queue.paused?, - "success" => true - }.to_json - rescue e - send_json env, { - "success" => false, - "error" => e.message - }.to_json - end - end + get "/api/admin/mangadex/queue" do |env| + begin + jobs = @context.queue.get_all + send_json env, { + "jobs" => jobs, + "paused" => @context.queue.paused?, + "success" => true, + }.to_json + rescue e + send_json env, { + "success" => false, + "error" => e.message, + }.to_json + end + end - post "/api/admin/mangadex/queue/:action" do |env| - begin - action = env.params.url["action"] - id = env.params.query["id"]? - case action - when "delete" - if id.nil? - @context.queue.delete_status MangaDex::JobStatus::Completed - else - @context.queue.delete id - end - when "retry" - if id.nil? - @context.queue.reset - else - @context.queue.reset id - end - when "pause" - @context.queue.pause - when "resume" - @context.queue.resume - else - raise "Unknown queue action #{action}" - end + post "/api/admin/mangadex/queue/:action" do |env| + begin + action = env.params.url["action"] + id = env.params.query["id"]? + case action + when "delete" + if id.nil? + @context.queue.delete_status MangaDex::JobStatus::Completed + else + @context.queue.delete id + end + when "retry" + if id.nil? + @context.queue.reset + else + @context.queue.reset id + end + when "pause" + @context.queue.pause + when "resume" + @context.queue.resume + else + raise "Unknown queue action #{action}" + end - send_json env, {"success" => true}.to_json - rescue e - send_json env, { - "success" => false, - "error" => e.message - }.to_json - end - end - end + send_json env, {"success" => true}.to_json + rescue e + send_json env, { + "success" => false, + "error" => e.message, + }.to_json + end + end + end end diff --git a/src/routes/main.cr b/src/routes/main.cr index ae3d586..b48bc8b 100644 --- a/src/routes/main.cr +++ b/src/routes/main.cr @@ -1,63 +1,61 @@ require "./router" class MainRouter < Router - def setup - get "/login" do |env| - render "src/views/login.ecr" - end + def setup + get "/login" do |env| + render "src/views/login.ecr" + end - get "/logout" do |env| - begin - cookie = env.request.cookies - .find { |c| c.name == "token" }.not_nil! - @context.storage.logout cookie.value - rescue e - @context.error "Error when attempting to log out: #{e}" - ensure - env.redirect "/login" - end - end + get "/logout" do |env| + begin + cookie = env.request.cookies.find { |c| c.name == "token" }.not_nil! + @context.storage.logout cookie.value + rescue e + @context.error "Error when attempting to log out: #{e}" + ensure + env.redirect "/login" + end + end - post "/login" do |env| - begin - username = env.params.body["username"] - password = env.params.body["password"] - token = @context.storage.verify_user(username, password) - .not_nil! + post "/login" do |env| + begin + username = env.params.body["username"] + password = env.params.body["password"] + token = @context.storage.verify_user(username, password).not_nil! - cookie = HTTP::Cookie.new "token", token - cookie.expires = Time.local.shift years: 1 - env.response.cookies << cookie - env.redirect "/" - rescue - env.redirect "/login" - end - end + cookie = HTTP::Cookie.new "token", token + cookie.expires = Time.local.shift years: 1 + env.response.cookies << cookie + env.redirect "/" + rescue + env.redirect "/login" + end + end - get "/" do |env| - titles = @context.library.titles - username = get_username env - percentage = titles.map &.load_percetage username - layout "index" - end + get "/" do |env| + titles = @context.library.titles + username = get_username env + percentage = titles.map &.load_percetage username + layout "index" + end - get "/book/:title" do |env| - begin - title = (@context.library.get_title env.params.url["title"]) - .not_nil! - username = get_username env - percentage = title.entries.map { |e| - title.load_percetage username, e.title } - layout "title" - rescue e - @context.error e - env.response.status_code = 404 - end - end + get "/book/:title" do |env| + begin + title = (@context.library.get_title env.params.url["title"]).not_nil! + username = get_username env + percentage = title.entries.map { |e| + title.load_percetage username, e.title + } + layout "title" + rescue e + @context.error e + env.response.status_code = 404 + end + end - get "/download" do |env| - base_url = @context.config.mangadex["base_url"]; - layout "download" - end - end + get "/download" do |env| + base_url = @context.config.mangadex["base_url"] + layout "download" + end + end end diff --git a/src/routes/reader.cr b/src/routes/reader.cr index 266396da..49d9a27 100644 --- a/src/routes/reader.cr +++ b/src/routes/reader.cr @@ -1,58 +1,61 @@ require "./router" class ReaderRouter < Router - def setup - get "/reader/:title/:entry" do |env| - begin - title = (@context.library.get_title env.params.url["title"]) - .not_nil! - entry = (title.get_entry env.params.url["entry"]).not_nil! + def setup + get "/reader/:title/:entry" do |env| + begin + title = (@context.library.get_title env.params.url["title"]).not_nil! + entry = (title.get_entry env.params.url["entry"]).not_nil! - # load progress - username = get_username env - page = title.load_progress username, entry.title - # we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll - # library perloads a few pages in advance, and the user - # might not have actually read them - page = [page - 2 * IMGS_PER_PAGE, 1].max + # load progress + username = get_username env + page = title.load_progress username, entry.title + # we go back 2 * `IMGS_PER_PAGE` pages. the infinite scroll + # library perloads a few pages in advance, and the user + # might not have actually read them + page = [page - 2 * IMGS_PER_PAGE, 1].max - env.redirect "/reader/#{title.id}/#{entry.id}/#{page}" - rescue e - @context.error e - env.response.status_code = 404 - end - end + env.redirect "/reader/#{title.id}/#{entry.id}/#{page}" + rescue e + @context.error e + env.response.status_code = 404 + end + end - get "/reader/:title/:entry/:page" do |env| - begin - title = (@context.library.get_title env.params.url["title"]) - .not_nil! - entry = (title.get_entry env.params.url["entry"]).not_nil! - page = env.params.url["page"].to_i - raise "" if page > entry.pages || page <= 0 + get "/reader/:title/:entry/:page" do |env| + begin + title = (@context.library.get_title env.params.url["title"]).not_nil! + entry = (title.get_entry env.params.url["entry"]).not_nil! + page = env.params.url["page"].to_i + raise "" if page > entry.pages || page <= 0 - # save progress - username = get_username env - title.save_progress username, entry.title, page + # save progress + username = get_username env + title.save_progress username, entry.title, page - pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min) - urls = pages.map { |idx| - "/api/page/#{title.id}/#{entry.id}/#{idx}" } - reader_urls = pages.map { |idx| - "/reader/#{title.id}/#{entry.id}/#{idx}" } - next_page = page + IMGS_PER_PAGE - next_url = next_page > entry.pages ? nil : - "/reader/#{title.id}/#{entry.id}/#{next_page}" - exit_url = "/book/#{title.id}" - next_entry = title.next_entry entry - next_entry_url = next_entry.nil? ? nil : \ - "/reader/#{title.id}/#{next_entry.id}" + pages = (page...[entry.pages + 1, page + IMGS_PER_PAGE].min) + urls = pages.map { |idx| + "/api/page/#{title.id}/#{entry.id}/#{idx}" + } + reader_urls = pages.map { |idx| + "/reader/#{title.id}/#{entry.id}/#{idx}" + } + next_page = page + IMGS_PER_PAGE + next_url = next_entry_url = nil + exit_url = "/book/#{title.id}" + next_entry = title.next_entry entry + unless next_page > entry.pages + next_url = "/reader/#{title.id}/#{entry.id}/#{next_page}" + end + unless next_entry.nil? + next_entry_url = "/reader/#{title.id}/#{next_entry.id}" + end - render "src/views/reader.ecr" - rescue e - @context.error e - env.response.status_code = 404 - end - end - end + render "src/views/reader.ecr" + rescue e + @context.error e + env.response.status_code = 404 + end + end + end end diff --git a/src/routes/router.cr b/src/routes/router.cr index 16389fb..97fb698 100644 --- a/src/routes/router.cr +++ b/src/routes/router.cr @@ -1,6 +1,6 @@ require "../context" class Router - def initialize(@context : Context) - end + def initialize(@context : Context) + end end diff --git a/src/server.cr b/src/server.cr index e31e10a..510542c 100644 --- a/src/server.cr +++ b/src/server.cr @@ -7,44 +7,42 @@ require "./util" require "./routes/*" class Server - def initialize(@context : Context) + def initialize(@context : Context) + error 403 do |env| + message = "HTTP 403: You are not authorized to visit #{env.request.path}" + layout "message" + end + error 404 do |env| + message = "HTTP 404: Mango cannot find the page #{env.request.path}" + layout "message" + end + error 500 do |env| + message = "HTTP 500: Internal server error. Please try again later." + layout "message" + end - error 403 do |env| - message = "HTTP 403: You are not authorized to visit " \ - "#{env.request.path}" - layout "message" - end - error 404 do |env| - message = "HTTP 404: Mango cannot find the page #{env.request.path}" - layout "message" - end - error 500 do |env| - message = "HTTP 500: Internal server error. Please try again later." - layout "message" - end + MainRouter.new(@context).setup + AdminRouter.new(@context).setup + ReaderRouter.new(@context).setup + APIRouter.new(@context).setup - MainRouter.new(@context).setup - AdminRouter.new(@context).setup - ReaderRouter.new(@context).setup - APIRouter.new(@context).setup + Kemal.config.logging = false + add_handler LogHandler.new @context.logger + add_handler AuthHandler.new @context.storage + {% if flag?(:release) %} + # when building for relase, embed the static files in binary + @context.debug "We are in release mode. Using embedded static files." + serve_static false + add_handler StaticHandler.new + {% end %} + end - Kemal.config.logging = false - add_handler LogHandler.new @context.logger - add_handler AuthHandler.new @context.storage - {% if flag?(:release) %} - # when building for relase, embed the static files in binary - @context.debug "We are in release mode. Using embedded static files." - serve_static false - add_handler StaticHandler.new - {% end %} - end - - def start - @context.debug "Starting Kemal server" - {% if flag?(:release) %} - Kemal.config.env = "production" - {% end %} - Kemal.config.port = @context.config.port - Kemal.run - end + def start + @context.debug "Starting Kemal server" + {% if flag?(:release) %} + Kemal.config.env = "production" + {% end %} + Kemal.config.port = @context.config.port + Kemal.run + end end diff --git a/src/static_handler.cr b/src/static_handler.cr index f39cd46..f03eda4 100644 --- a/src/static_handler.cr +++ b/src/static_handler.cr @@ -3,30 +3,30 @@ require "kemal" require "./util" class FS - extend BakedFileSystem - {% if flag?(:release) %} - {% if read_file? "#{__DIR__}/../dist/favicon.ico" %} - {% puts "baking ../dist" %} - bake_folder "../dist" - {% else %} - {% puts "baking ../public" %} - bake_folder "../public" - {% end %} - {% end %} + extend BakedFileSystem + {% if flag?(:release) %} + {% if read_file? "#{__DIR__}/../dist/favicon.ico" %} + {% puts "baking ../dist" %} + bake_folder "../dist" + {% else %} + {% puts "baking ../public" %} + bake_folder "../public" + {% end %} + {% end %} end class StaticHandler < Kemal::Handler - @dirs = ["/css", "/js", "/img", "/favicon.ico"] + @dirs = ["/css", "/js", "/img", "/favicon.ico"] - def call(env) - if request_path_startswith env, @dirs - file = FS.get? env.request.path - return call_next env if file.nil? + def call(env) + if request_path_startswith env, @dirs + file = FS.get? env.request.path + return call_next env if file.nil? - slice = Bytes.new file.size - file.read slice - return send_file env, slice, file.mime_type - end - call_next env - end + slice = Bytes.new file.size + file.read slice + return send_file env, slice, file.mime_type + end + call_next env + end end diff --git a/src/storage.cr b/src/storage.cr index 8dc7cd3..c618cd3 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -4,174 +4,172 @@ require "uuid" require "base64" def hash_password(pw) - Crypto::Bcrypt::Password.create(pw).to_s + Crypto::Bcrypt::Password.create(pw).to_s end def verify_password(hash, pw) - (Crypto::Bcrypt::Password.new hash).verify pw + (Crypto::Bcrypt::Password.new hash).verify pw end def random_str - UUID.random.to_s.gsub "-", "" + UUID.random.to_s.gsub "-", "" end class Storage - def initialize(@path : String, @logger : Logger) - dir = File.dirname path - unless Dir.exists? dir - @logger.info "The DB directory #{dir} does not exist. " \ - "Attepmting to create it" - Dir.mkdir_p dir - end - 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, - # we will still be able to create `ids` - db.exec "create table ids" \ - "(path text, id text, is_title integer)" - db.exec "create unique index path_idx on ids (path)" - db.exec "create unique index id_idx on ids (id)" + def initialize(@path : String, @logger : Logger) + dir = File.dirname path + unless Dir.exists? dir + @logger.info "The DB directory #{dir} does not exist. " \ + "Attepmting to create it" + Dir.mkdir_p dir + end + 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, + # we will still be able to create `ids` + db.exec "create table ids" \ + "(path text, id text, is_title integer)" + db.exec "create unique index path_idx on ids (path)" + db.exec "create unique index id_idx on ids (id)" - db.exec "create table users" \ - "(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}" - raise e - end - else - @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}}" - end - end - end + db.exec "create table users" \ + "(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}" + raise e + end + else + @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}}" + end + end + end - def verify_user(username, password) - DB.open "sqlite3://#{@path}" do |db| - begin - hash, token = db.query_one "select password, token from "\ - "users where username = (?)", \ - username, as: {String, String?} - unless verify_password hash, password - @logger.debug "Password does not match the hash" - return nil - end - @logger.debug "User #{username} verified" - return token if token - token = random_str - @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}" - return nil - end - end - end + def verify_user(username, password) + DB.open "sqlite3://#{@path}" do |db| + begin + hash, token = db.query_one "select password, token from " \ + "users where username = (?)", + username, as: {String, String?} + unless verify_password hash, password + @logger.debug "Password does not match the hash" + return nil + end + @logger.debug "User #{username} verified" + return token if token + token = random_str + @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}" + return nil + end + end + end - def verify_token(token) - DB.open "sqlite3://#{@path}" do |db| - begin - username = db.query_one "select username from users where " \ - "token = (?)", token, as: String - return username - rescue e - @logger.debug "Unable to verify token" - return nil - end - end - end + def verify_token(token) + DB.open "sqlite3://#{@path}" do |db| + begin + username = db.query_one "select username from users where " \ + "token = (?)", token, as: String + return username + rescue e + @logger.debug "Unable to verify token" + return nil + end + end + end - def verify_admin(token) - DB.open "sqlite3://#{@path}" do |db| - begin - return db.query_one "select admin from users where " \ - "token = (?)", token, as: Bool - rescue e - @logger.debug "Unable to verify user as admin" - return false - end - end - end + def verify_admin(token) + DB.open "sqlite3://#{@path}" do |db| + begin + return db.query_one "select admin from users where " \ + "token = (?)", token, as: Bool + rescue e + @logger.debug "Unable to verify user as admin" + return false + end + end + end - def list_users - results = Array(Tuple(String, Bool)).new - DB.open "sqlite3://#{@path}" do |db| - db.query "select username, admin from users" do |rs| - rs.each do - results << {rs.read(String), rs.read(Bool)} - end - end - end - results - end + def list_users + results = Array(Tuple(String, Bool)).new + DB.open "sqlite3://#{@path}" do |db| + db.query "select username, admin from users" do |rs| + rs.each do + results << {rs.read(String), rs.read(Bool)} + end + end + end + results + end - def new_user(username, password, admin) - admin = (admin ? 1 : 0) - DB.open "sqlite3://#{@path}" do |db| - hash = hash_password password - db.exec "insert into users values (?, ?, ?, ?)", - username, hash, nil, admin - end - end + def new_user(username, password, admin) + admin = (admin ? 1 : 0) + DB.open "sqlite3://#{@path}" do |db| + hash = hash_password password + db.exec "insert into users values (?, ?, ?, ?)", + username, hash, nil, admin + end + end - def update_user(original_username, username, password, admin) - admin = (admin ? 1 : 0) - DB.open "sqlite3://#{@path}" do |db| - if password.size == 0 - db.exec "update users set username = (?), admin = (?) "\ - "where username = (?)",\ - username, admin, original_username - else - hash = hash_password password - db.exec "update users set username = (?), admin = (?),"\ - "password = (?) where username = (?)",\ - username, admin, hash, original_username - end - end - end + def update_user(original_username, username, password, admin) + admin = (admin ? 1 : 0) + DB.open "sqlite3://#{@path}" do |db| + if password.size == 0 + db.exec "update users set username = (?), admin = (?) " \ + "where username = (?)", + username, admin, original_username + else + hash = hash_password password + db.exec "update users set username = (?), admin = (?)," \ + "password = (?) where username = (?)", + username, admin, hash, original_username + end + end + end - def delete_user(username) - DB.open "sqlite3://#{@path}" do |db| - db.exec "delete from users where username = (?)", username - end - end + def delete_user(username) + DB.open "sqlite3://#{@path}" do |db| + db.exec "delete from users where username = (?)", username + end + end - def logout(token) - DB.open "sqlite3://#{@path}" do |db| - begin - db.exec "update users set token = (?) where token = (?)", \ - nil, token - rescue - end - end - end + def logout(token) + DB.open "sqlite3://#{@path}" do |db| + begin + db.exec "update users set token = (?) where token = (?)", nil, token + rescue + end + end + end - def get_id(path, is_title) - DB.open "sqlite3://#{@path}" do |db| - begin - id = db.query_one "select id from ids where path = (?)", - path, as: {String} - return id - rescue - id = random_str - db.exec "insert into ids values (?, ?, ?)", path, id, - is_title ? 1 : 0 - return id - end - end - end + def get_id(path, is_title) + DB.open "sqlite3://#{@path}" do |db| + begin + id = db.query_one "select id from ids where path = (?)", path, + as: {String} + return id + rescue + id = random_str + db.exec "insert into ids values (?, ?, ?)", path, id, is_title ? 1 : 0 + return id + end + end + end - def to_json(json : JSON::Builder) - json.string self - end + def to_json(json : JSON::Builder) + json.string self + end end diff --git a/src/util.cr b/src/util.cr index 8fb67c9..66fb5be 100644 --- a/src/util.cr +++ b/src/util.cr @@ -3,81 +3,81 @@ require "big" IMGS_PER_PAGE = 5 macro layout(name) - begin - cookie = env.request.cookies.find { |c| c.name == "token" } - is_admin = false - unless cookie.nil? - is_admin = @context.storage.verify_admin cookie.value - end - render "src/views/#{{{name}}}.ecr", "src/views/layout.ecr" - rescue e - message = e.to_s - render "message" - end + begin + cookie = env.request.cookies.find { |c| c.name == "token" } + is_admin = false + unless cookie.nil? + is_admin = @context.storage.verify_admin cookie.value + end + render "src/views/#{{{name}}}.ecr", "src/views/layout.ecr" + rescue e + message = e.to_s + render "message" + end end macro send_img(env, img) - send_file {{env}}, {{img}}.data, {{img}}.mime + send_file {{env}}, {{img}}.data, {{img}}.mime end macro get_username(env) - # if the request gets here, it has gone through the auth handler, and - # we can be sure that a valid token exists, so we can use not_nil! here - cookie = {{env}}.request.cookies.find { |c| c.name == "token" }.not_nil! - (@context.storage.verify_token cookie.value).not_nil! + # if the request gets here, it has gone through the auth handler, and + # we can be sure that a valid token exists, so we can use not_nil! here + cookie = {{env}}.request.cookies.find { |c| c.name == "token" }.not_nil! + (@context.storage.verify_token cookie.value).not_nil! end macro send_json(env, json) - {{env}}.response.content_type = "application/json" - {{json}} + {{env}}.response.content_type = "application/json" + {{json}} end def hash_to_query(hash) - hash.map { |k, v| "#{k}=#{v}" }.join("&") + hash.map { |k, v| "#{k}=#{v}" }.join("&") end def request_path_startswith(env, ary) - ary.each do |prefix| - if env.request.path.starts_with? prefix - return true - end - end - return false + ary.each do |prefix| + if env.request.path.starts_with? prefix + return true + end + end + return false end def is_numeric(str) - /^\d+/.match(str) != nil + /^\d+/.match(str) != nil end def split_by_alphanumeric(str) - arr = [] of String - str.scan(/([^\d\n\r]*)(\d*)([^\d\n\r]*)/) do |match| - arr += match.captures.select{|s| s != ""} - end - arr + arr = [] of String + str.scan(/([^\d\n\r]*)(\d*)([^\d\n\r]*)/) do |match| + arr += match.captures.select { |s| s != "" } + end + arr end def compare_alphanumerically(c, d) - is_c_bigger = c.size <=> d.size - if c.size > d.size - d += [nil] * (c.size - d.size) - elsif c.size < d.size - c += [nil] * (d.size - c.size) - end - c.zip(d) do |a, b| - return -1 if a.nil? - return 1 if b.nil? - if is_numeric(a) && is_numeric(b) - compare = a.to_big_i <=> b.to_big_i - return compare if compare != 0 - else - compare = a <=> b - return compare if compare != 0 - end - end - is_c_bigger + is_c_bigger = c.size <=> d.size + if c.size > d.size + d += [nil] * (c.size - d.size) + elsif c.size < d.size + c += [nil] * (d.size - c.size) + end + c.zip(d) do |a, b| + return -1 if a.nil? + return 1 if b.nil? + if is_numeric(a) && is_numeric(b) + compare = a.to_big_i <=> b.to_big_i + return compare if compare != 0 + else + compare = a <=> b + return compare if compare != 0 + end + end + is_c_bigger end def compare_alphanumerically(a : String, b : String) - compare_alphanumerically split_by_alphanumeric(a), split_by_alphanumeric(b) + compare_alphanumerically split_by_alphanumeric(a), split_by_alphanumeric(b) end