From df7e2270a4869814c2066174d64b85e4b071916c Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 12 Feb 2021 15:16:05 +0000 Subject: [PATCH 01/14] Add MangaDex login page --- migration/md_account.11.cr | 20 ++++++++ public/js/mangadex.js | 61 +++++++++++++++++++++++++ shard.lock | 2 +- src/routes/admin.cr | 4 ++ src/routes/api.cr | 51 +++++++++++++++++++++ src/storage.cr | 31 +++++++++++++ src/views/admin.html.ecr | 1 + src/views/components/jquery-ui.html.ecr | 1 + src/views/components/moment.html.ecr | 1 + src/views/download-manager.html.ecr | 2 +- src/views/download.html.ecr | 4 +- src/views/mangadex.html.ecr | 39 ++++++++++++++++ src/views/plugin-download.html.ecr | 2 +- 13 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 migration/md_account.11.cr create mode 100644 public/js/mangadex.js create mode 100644 src/views/components/jquery-ui.html.ecr create mode 100644 src/views/components/moment.html.ecr create mode 100644 src/views/mangadex.html.ecr diff --git a/migration/md_account.11.cr b/migration/md_account.11.cr new file mode 100644 index 0000000..3aece3e --- /dev/null +++ b/migration/md_account.11.cr @@ -0,0 +1,20 @@ +class CreateMangaDexAccount < MG::Base + def up : String + <<-SQL + CREATE TABLE md_account ( + username TEXT NOT NULL PRIMARY KEY, + token TEXT NOT NULL, + expire INTEGER NOT NULL, + FOREIGN KEY (username) REFERENCES users (username) + ON UPDATE CASCADE + ON DELETE CASCADE + ); + SQL + end + + def down : String + <<-SQL + DROP TABLE md_account; + SQL + end +end diff --git a/public/js/mangadex.js b/public/js/mangadex.js new file mode 100644 index 0000000..3271c4b --- /dev/null +++ b/public/js/mangadex.js @@ -0,0 +1,61 @@ +const component = () => { + return { + username: '', + password: '', + expires: undefined, + loading: true, + loggingIn: false, + + init() { + this.loading = true; + $.ajax({ + type: 'GET', + url: `${base_url}api/admin/mangadex/expires`, + contentType: "application/json", + }) + .done(data => { + console.log(data); + if (data.error) { + alert('danger', `Failed to retrieve MangaDex token status. Error: ${data.error}`); + return; + } + this.expires = data.expires; + this.loading = false; + }) + .fail((jqXHR, status) => { + alert('danger', `Failed to retrieve MangaDex token status. Error: [${jqXHR.status}] ${jqXHR.statusText}`); + }); + }, + login() { + if (!(this.username && this.password)) return; + this.loggingIn = true; + $.ajax({ + type: 'POST', + url: `${base_url}api/admin/mangadex/login`, + contentType: "application/json", + dataType: 'json', + data: JSON.stringify({ + username: this.username, + password: this.password + }) + }) + .done(data => { + console.log(data); + if (data.error) { + alert('danger', `Failed to log in. Error: ${data.error}`); + return; + } + this.expires = data.expires; + }) + .fail((jqXHR, status) => { + alert('danger', `Failed to log in. Error: [${jqXHR.status}] ${jqXHR.statusText}`); + }) + .always(() => { + this.loggingIn = false; + }); + }, + get expired() { + return this.expires && moment().diff(moment.unix(this.expires)) > 0; + } + }; +}; diff --git a/shard.lock b/shard.lock index 024ee1a..662715b 100644 --- a/shard.lock +++ b/shard.lock @@ -54,7 +54,7 @@ shards: mangadex: git: https://github.com/hkalexling/mangadex.git - version: 0.5.0+git.commit.323110c56c2d5134ce4162b27a9b24ec34137fcb + version: 0.6.0+git.commit.d87a1d7a6e84d122814b38618954dcc73fc5553b mg: git: https://github.com/hkalexling/mg.git diff --git a/src/routes/admin.cr b/src/routes/admin.cr index fd63ec8..616d452 100644 --- a/src/routes/admin.cr +++ b/src/routes/admin.cr @@ -73,5 +73,9 @@ struct AdminRouter get "/admin/missing" do |env| layout "missing-items" end + + get "/admin/mangadex" do |env| + layout "mangadex" + end end end diff --git a/src/routes/api.cr b/src/routes/api.cr index ff6a087..211f557 100644 --- a/src/routes/api.cr +++ b/src/routes/api.cr @@ -905,6 +905,57 @@ struct APIRouter end end + Koa.describe "Logs the current user into their MangaDex account, and " \ + "saves the token." + Koa.tag "admin" + post "/api/admin/mangadex/login" do |env| + begin + username = env.params.json["username"].as String + password = env.params.json["password"].as String + mango_username = get_username env + + client = MangaDex::Client.from_config + client.auth username, password + + Storage.default.save_md_token mango_username, client.token.not_nil!, + client.token_expires + + send_json env, { + "success" => true, + "error" => nil, + "expires" => client.token_expires.to_unix, + }.to_json + rescue e + Logger.error e + send_json env, { + "success" => false, + "error" => e.message, + }.to_json + end + end + + Koa.describe "Returns the expiration date of the mangadex token if the " \ + "current user has logged in to MangaDex" + Koa.tag "admin" + get "/api/admin/mangadex/expires" do |env| + begin + username = get_username env + _, expires = Storage.default.get_md_token username + + send_json env, { + "success" => true, + "error" => nil, + "expires" => expires.try &.to_unix, + }.to_json + rescue e + Logger.error e + send_json env, { + "success" => false, + "error" => e.message, + }.to_json + end + end + doc = Koa.generate @@api_json = doc.to_json if doc diff --git a/src/storage.cr b/src/storage.cr index 937200f..88a5142 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -514,6 +514,37 @@ class Storage delete_missing "titles", id end + def save_md_token(username : String, token : String, expire : Time) + MainFiber.run do + get_db do |db| + count = db.query_one "select count(*) from md_account where " \ + "username = (?)", username, as: Int64 + if count == 0 + db.exec "insert into md_account values (?, ?, ?)", username, token, + expire.to_unix + else + db.exec "update md_account set token = (?), expire = (?) " \ + "where username = (?)", token, expire.to_unix, username + end + end + end + end + + def get_md_token(username) : Tuple(String?, Time?) + token = nil + expires = nil + MainFiber.run do + get_db do |db| + db.query_one? "select token, expire from md_account where " \ + "username = (?)", username do |res| + token = res.read String + expires = Time.unix res.read Int64 + end + end + end + {token, expires} + end + def close MainFiber.run do unless @db.nil? diff --git a/src/views/admin.html.ecr b/src/views/admin.html.ecr index fb64d3e..a6e9b31 100644 --- a/src/views/admin.html.ecr +++ b/src/views/admin.html.ecr @@ -33,6 +33,7 @@ +
  • Connect to MangaDex

  • diff --git a/src/views/components/jquery-ui.html.ecr b/src/views/components/jquery-ui.html.ecr new file mode 100644 index 0000000..2141e25 --- /dev/null +++ b/src/views/components/jquery-ui.html.ecr @@ -0,0 +1 @@ + diff --git a/src/views/components/moment.html.ecr b/src/views/components/moment.html.ecr new file mode 100644 index 0000000..fdec5cf --- /dev/null +++ b/src/views/components/moment.html.ecr @@ -0,0 +1 @@ + diff --git a/src/views/download-manager.html.ecr b/src/views/download-manager.html.ecr index 552405a..2b6e434 100644 --- a/src/views/download-manager.html.ecr +++ b/src/views/download-manager.html.ecr @@ -63,7 +63,7 @@ <% content_for "script" do %> - + <%= render_component "moment" %> <% end %> diff --git a/src/views/download.html.ecr b/src/views/download.html.ecr index 7a96858..a0489c0 100644 --- a/src/views/download.html.ecr +++ b/src/views/download.html.ecr @@ -110,8 +110,8 @@ <% content_for "script" do %> - - + <%= render_component "moment" %> + <%= render_component "jquery-ui" %> <% end %> diff --git a/src/views/mangadex.html.ecr b/src/views/mangadex.html.ecr new file mode 100644 index 0000000..dc7a378 --- /dev/null +++ b/src/views/mangadex.html.ecr @@ -0,0 +1,39 @@ +
    +

    Connect to MangaDex

    +
    +
    +

    This step is optional but highly recommended if you are using the MangaDex downloader. Connecting to MangaDex allows you to:

    +
      +
    • Search MangaDex by search terms in addition to manga IDs
    • +
    • Automatically download new chapters when they are available
    • +
    +
    + +
    +

    + You have logged in to MangaDex! + You have logged in to MangaDex but the token has expired. + The expiration date of your token is . + If the integration is not working, you + You + can log in again and the token will be updated. +

    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +<% content_for "script" do %> + <%= render_component "moment" %> + + +<% end %> diff --git a/src/views/plugin-download.html.ecr b/src/views/plugin-download.html.ecr index 68eb156..692e22f 100644 --- a/src/views/plugin-download.html.ecr +++ b/src/views/plugin-download.html.ecr @@ -68,7 +68,7 @@ var pid = "<%= plugin.not_nil!.info.id %>"; <% end %> - + <%= render_component "jquery-ui" %> From 1b25a1fa4718102d6b79f5d2c6cbb6b0cba69167 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Mon, 15 Feb 2021 16:13:54 +0000 Subject: [PATCH 02/14] Update Koa --- shard.lock | 2 +- src/routes/api.cr | 386 ++++++++++++++++++++++------------------------ 2 files changed, 188 insertions(+), 200 deletions(-) diff --git a/shard.lock b/shard.lock index 662715b..b6974f7 100644 --- a/shard.lock +++ b/shard.lock @@ -50,7 +50,7 @@ shards: koa: git: https://github.com/hkalexling/koa.git - version: 0.5.0 + version: 0.7.0 mangadex: git: https://github.com/hkalexling/mangadex.git diff --git a/src/routes/api.cr b/src/routes/api.cr index 211f557..7599d1e 100644 --- a/src/routes/api.cr +++ b/src/routes/api.cr @@ -10,7 +10,7 @@ struct APIRouter macro s(fields) { {% for field in fields %} - {{field}} => "string", + {{field}} => String, {% end %} } end @@ -33,160 +33,47 @@ struct APIRouter MD Koa.cookie_auth "cookie", "mango-sessid-#{Config.current.port}" - Koa.global_tag "admin", desc: <<-MD + Koa.define_tag "admin", desc: <<-MD These are the admin endpoints only accessible for users with admin access. A non-admin user will get HTTP 403 when calling the endpoints. MD - Koa.binary "binary", desc: "A binary file" - Koa.array "entryAry", "$entry", desc: "An array of entries" - Koa.array "titleAry", "$title", desc: "An array of titles" - Koa.array "strAry", "string", desc: "An array of strings" + Koa.schema "entry", { + "pages" => Int32, + "mtime" => Int64, + }.merge(s %w(zip_path title size id title_id display_name cover_url)), + desc: "An entry in a book" - entry_schema = { - "pages" => "integer", - "mtime" => "integer", - }.merge s %w(zip_path title size id title_id display_name cover_url) - Koa.object "entry", entry_schema, desc: "An entry in a book" - - title_schema = { - "mtime" => "integer", - "entries" => "$entryAry", - "titles" => "$titleAry", - "parents" => "$strAry", - }.merge s %w(dir title id display_name cover_url) - Koa.object "title", title_schema, + Koa.schema "title", { + "mtime" => Int64, + "entries" => ["entry"], + "titles" => ["title"], + "parents" => [String], + }.merge(s %w(dir title id display_name cover_url)), desc: "A manga title (a collection of entries and sub-titles)" - Koa.object "library", { - "dir" => "string", - "titles" => "$titleAry", - }, desc: "A library containing a list of top-level titles" - - Koa.object "scanResult", { - "milliseconds" => "integer", - "titles" => "integer", + Koa.schema "result", { + "success" => Bool, + "error" => String?, } - Koa.object "progressResult", { - "progress" => "number", - } + Koa.schema("mdChapter", { + "group" => {} of String => String, + }.merge(s %w(id title volume chapter language full_title time + manga_title manga_id)), + desc: "A MangaDex chapter") - Koa.object "result", { - "success" => "boolean", - "error" => "string?", - } - - mc_schema = { - "groups" => "object", - }.merge s %w(id title volume chapter language full_title time manga_title manga_id) - Koa.object "mangadexChapter", mc_schema, desc: "A MangaDex chapter" - - Koa.array "chapterAry", "$mangadexChapter" - - mm_schema = { - "chapers" => "$chapterAry", - }.merge s %w(id title description author artist cover_url) - Koa.object "mangadexManga", mm_schema, desc: "A MangaDex manga" - - Koa.object "chaptersObj", { - "chapters" => "$chapterAry", - } - - Koa.object "successFailCount", { - "success" => "integer", - "fail" => "integer", - } - - job_schema = { - "pages" => "integer", - "success_count" => "integer", - "fail_count" => "integer", - "time" => "integer", - }.merge s %w(id manga_id title manga_title status_message status) - Koa.object "job", job_schema, desc: "A download job in the queue" - - Koa.array "jobAry", "$job" - - Koa.object "jobs", { - "success" => "boolean", - "paused" => "boolean", - "jobs" => "$jobAry", - } - - Koa.object "binaryUpload", { - "file" => "$binary", - } - - Koa.object "pluginListBody", { - "plugin" => "string", - "query" => "string", - } - - Koa.object "pluginChapter", { - "id" => "string", - "title" => "string", - } - - Koa.array "pluginChapterAry", "$pluginChapter" - - Koa.object "pluginList", { - "success" => "boolean", - "chapters" => "$pluginChapterAry?", - "title" => "string?", - "error" => "string?", - } - - Koa.object "pluginDownload", { - "plugin" => "string", - "title" => "string", - "chapters" => "$pluginChapterAry", - } - - Koa.object "dimension", { - "width" => "integer", - "height" => "integer", - } - - Koa.array "dimensionAry", "$dimension" - - Koa.object "dimensionResult", { - "success" => "boolean", - "dimensions" => "$dimensionAry?", - "margin" => "number", - "error" => "string?", - } - - Koa.object "ids", { - "ids" => "$strAry", - } - - Koa.object "tagsResult", { - "success" => "boolean", - "tags" => "$strAry?", - "error" => "string?", - } - - Koa.object "missing", { - "path" => "string", - "id" => "string", - "signature" => "string", - } - - Koa.array "missingAry", "$missing" - - Koa.object "missingResult", { - "success" => "boolean", - "error" => "string?", - "entries" => "$missingAry?", - "titles" => "$missingAry?", - } + Koa.schema "mdManga", { + "chapters" => ["mdChapter"], + }.merge(s %w(id title description author artist cover_url)), + desc: "A MangaDex manga" Koa.describe "Returns a page in a manga entry" Koa.path "tid", desc: "Title ID" Koa.path "eid", desc: "Entry ID" - Koa.path "page", type: "integer", desc: "The page number to return (starts from 1)" - Koa.response 200, ref: "$binary", media_type: "image/*" + Koa.path "page", schema: Int32, desc: "The page number to return (starts from 1)" + Koa.response 200, schema: Bytes, media_type: "image/*" Koa.response 500, "Page not found or not readable" + Koa.tag "reader" get "/api/page/:tid/:eid/:page" do |env| begin tid = env.params.url["tid"] @@ -212,8 +99,9 @@ struct APIRouter Koa.describe "Returns the cover image of a manga entry" Koa.path "tid", desc: "Title ID" Koa.path "eid", desc: "Entry ID" - Koa.response 200, ref: "$binary", media_type: "image/*" + Koa.response 200, schema: Bytes, media_type: "image/*" Koa.response 500, "Page not found or not readable" + Koa.tag "library" get "/api/cover/:tid/:eid" do |env| begin tid = env.params.url["tid"] @@ -238,8 +126,9 @@ struct APIRouter Koa.describe "Returns the book with title `tid`" Koa.path "tid", desc: "Title ID" - Koa.response 200, ref: "$title" + Koa.response 200, schema: "title" Koa.response 404, "Title not found" + Koa.tag "library" get "/api/book/:tid" do |env| begin tid = env.params.url["tid"] @@ -255,14 +144,21 @@ struct APIRouter end Koa.describe "Returns the entire library with all titles and entries" - Koa.response 200, ref: "$library" + Koa.response 200, schema: { + "dir" => String, + "titles" => ["title"], + } + Koa.tag "library" get "/api/library" do |env| send_json env, Library.default.to_json end Koa.describe "Triggers a library scan" - Koa.tag "admin" - Koa.response 200, ref: "$scanResult" + Koa.tags ["admin", "library"] + Koa.response 200, schema: { + "milliseconds" => Float64, + "titles" => Int32, + } post "/api/admin/scan" do |env| start = Time.utc Library.default.scan @@ -274,8 +170,10 @@ struct APIRouter end Koa.describe "Returns the thumbnail generation progress between 0 and 1" - Koa.tag "admin" - Koa.response 200, ref: "$progressResult" + Koa.tags ["admin", "library"] + Koa.response 200, schema: { + "progress" => Float64, + } get "/api/admin/thumbnail_progress" do |env| send_json env, { "progress" => Library.default.thumbnail_generation_progress, @@ -283,7 +181,7 @@ struct APIRouter end Koa.describe "Triggers a thumbnail generation" - Koa.tag "admin" + Koa.tags ["admin", "library"] post "/api/admin/generate_thumbnails" do |env| spawn do Library.default.generate_thumbnails @@ -291,8 +189,8 @@ struct APIRouter end Koa.describe "Deletes a user with `username`" - Koa.tag "admin" - Koa.response 200, ref: "$result" + Koa.tags ["admin", "users"] + Koa.response 200, schema: "result" delete "/api/admin/user/delete/:username" do |env| begin username = env.params.url["username"] @@ -319,7 +217,8 @@ struct APIRouter Koa.path "tid", desc: "Title ID" Koa.query "eid", desc: "Entry ID", required: false Koa.path "page", desc: "The new page number indicating the progress" - Koa.response 200, ref: "$result" + Koa.response 200, schema: "result" + Koa.tag "progress" put "/api/progress/:tid/:page" do |env| begin username = get_username env @@ -350,8 +249,11 @@ struct APIRouter Koa.describe "Updates the reading progress of multiple entries in a title" Koa.path "action", desc: "The action to perform. Can be either `read` or `unread`" Koa.path "tid", desc: "Title ID" - Koa.body ref: "$ids", desc: "An array of entry IDs" - Koa.response 200, ref: "$result" + Koa.body schema: { + "ids" => [String], + }, desc: "An array of entry IDs" + Koa.response 200, schema: "result" + Koa.tag "progress" put "/api/bulk_progress/:action/:tid" do |env| begin username = get_username env @@ -377,11 +279,11 @@ struct APIRouter Koa.describe "Sets the display name of a title or an entry", <<-MD When `eid` is provided, apply the display name to the entry. Otherwise, apply the display name to the title identified by `tid`. MD - Koa.tag "admin" + Koa.tags ["admin", "library"] Koa.path "tid", desc: "Title ID" Koa.query "eid", desc: "Entry ID", required: false Koa.path "name", desc: "The new display name" - Koa.response 200, ref: "$result" + Koa.response 200, schema: "result" put "/api/admin/display_name/:tid/:name" do |env| begin title = (Library.default.get_title env.params.url["tid"]) @@ -408,9 +310,9 @@ struct APIRouter Koa.describe "Returns a MangaDex manga identified by `id`", <<-MD On error, returns a JSON that contains the error message in the `error` field. MD - Koa.tag "admin" + Koa.tags ["admin", "mangadex"] Koa.path "id", desc: "A MangaDex manga ID" - Koa.response 200, ref: "$mangadexManga" + Koa.response 200, schema: "mdManga" get "/api/admin/mangadex/manga/:id" do |env| begin id = env.params.url["id"] @@ -425,9 +327,14 @@ struct APIRouter Koa.describe "Adds a list of MangaDex chapters to the download queue", <<-MD On error, returns a JSON that contains the error message in the `error` field. MD - Koa.tag "admin" - Koa.body ref: "$chaptersObj" - Koa.response 200, ref: "$successFailCount" + Koa.tags ["admin", "mangadex", "downloader"] + Koa.body schema: { + "chapters" => ["mdChapter"], + } + Koa.response 200, schema: { + "success" => Int32, + "fail" => Int32, + } post "/api/admin/mangadex/download" do |env| begin chapters = env.params.json["chapters"].as(Array).map { |c| c.as_h } @@ -467,8 +374,18 @@ struct APIRouter Koa.describe "Returns the current download queue", <<-MD On error, returns a JSON that contains the error message in the `error` field. MD - Koa.tag "admin" - Koa.response 200, ref: "$jobs" + Koa.tags ["admin", "downloader"] + Koa.response 200, schema: { + "success" => Bool, + "error" => String?, + "paused" => Bool?, + "jobs?" => [{ + "pages" => Int32, + "success_count" => Int32, + "fail_count" => Int32, + "time" => Int64, + }.merge(s %w(id manga_id title manga_title status_message status))], + } get "/api/admin/mangadex/queue" do |env| begin jobs = Queue.default.get_all @@ -494,10 +411,10 @@ struct APIRouter When `action` is set to `retry`, the behavior depends on `id`. If `id` is provided, restarts the job identified by the ID. Otherwise, retries all jobs in the `Error` or `MissingPages` status in the queue. MD - Koa.tag "admin" + Koa.tags ["admin", "downloader"] Koa.path "action", desc: "The action to perform. It should be one of the followins: `delete`, `retry`, `pause` and `resume`." Koa.query "id", required: false, desc: "A job ID" - Koa.response 200, ref: "$result" + Koa.response 200, schema: "result" post "/api/admin/mangadex/queue/:action" do |env| begin action = env.params.url["action"] @@ -546,8 +463,10 @@ struct APIRouter When `eid` is omitted, the new cover image will be applied to the title. Otherwise, applies the image to the specified entry. MD Koa.tag "admin" - Koa.body type: "multipart/form-data", ref: "$binaryUpload" - Koa.response 200, ref: "$result" + Koa.body media_type: "multipart/form-data", schema: { + "file" => Bytes, + } + Koa.response 200, schema: "result" post "/api/admin/upload/:target" do |env| begin target = env.params.url["target"] @@ -603,9 +522,18 @@ struct APIRouter end Koa.describe "Lists the chapters in a title from a plugin" - Koa.tag "admin" - Koa.body ref: "$pluginListBody" - Koa.response 200, ref: "$pluginList" + Koa.tags ["admin", "downloader"] + Koa.query "plugin", schema: String + Koa.query "query", schema: String + Koa.response 200, schema: { + "success" => Bool, + "error" => String?, + "chapters?" => [{ + "id" => String, + "title" => String, + }], + "title" => String?, + } get "/api/admin/plugin/list" do |env| begin query = env.params.query["query"].as String @@ -629,9 +557,19 @@ struct APIRouter end Koa.describe "Adds a list of chapters from a plugin to the download queue" - Koa.tag "admin" - Koa.body ref: "$pluginDownload" - Koa.response 200, ref: "$successFailCount" + Koa.tags ["admin", "downloader"] + Koa.body schema: { + "plugin" => String, + "title" => String, + "chapters" => [{ + "id" => String, + "title" => String, + }], + } + Koa.response 200, schema: { + "success" => Int32, + "fail" => Int32, + } post "/api/admin/plugin/download" do |env| begin plugin = Plugin.new env.params.json["plugin"].as String @@ -664,7 +602,16 @@ struct APIRouter Koa.describe "Returns the image dimensions of all pages in an entry" Koa.path "tid", desc: "A title ID" Koa.path "eid", desc: "An entry ID" - Koa.response 200, ref: "$dimensionResult" + Koa.tag "reader" + Koa.response 200, schema: { + "success" => Bool, + "error" => String?, + "dimensions?" => [{ + "width" => Int32, + "height" => Int32, + }], + "margin" => Int32?, + } get "/api/dimensions/:tid/:eid" do |env| begin tid = env.params.url["tid"] @@ -692,8 +639,9 @@ struct APIRouter Koa.describe "Downloads an entry" Koa.path "tid", desc: "A title ID" Koa.path "eid", desc: "An entry ID" - Koa.response 200, ref: "$binary" + Koa.response 200, schema: Bytes Koa.response 404, "Entry not found" + Koa.tags ["library", "reader"] get "/api/download/:tid/:eid" do |env| begin title = (Library.default.get_title env.params.url["tid"]).not_nil! @@ -708,7 +656,12 @@ struct APIRouter Koa.describe "Gets the tags of a title" Koa.path "tid", desc: "A title ID" - Koa.response 200, ref: "$tagsResult" + Koa.response 200, schema: { + "success" => Bool, + "error" => String?, + "tags" => [String?], + } + Koa.tags ["library", "tags"] get "/api/tags/:tid" do |env| begin title = (Library.default.get_title env.params.url["tid"]).not_nil! @@ -728,7 +681,12 @@ struct APIRouter end Koa.describe "Returns all tags" - Koa.response 200, ref: "$tagsResult" + Koa.response 200, schema: { + "success" => Bool, + "error" => String?, + "tags" => [String?], + } + Koa.tags ["library", "tags"] get "/api/tags" do |env| begin tags = Storage.default.list_tags @@ -747,8 +705,8 @@ struct APIRouter Koa.describe "Adds a new tag to a title" Koa.path "tid", desc: "A title ID" - Koa.response 200, ref: "$result" - Koa.tag "admin" + Koa.response 200, schema: "result" + Koa.tags ["admin", "library", "tags"] put "/api/admin/tags/:tid/:tag" do |env| begin title = (Library.default.get_title env.params.url["tid"]).not_nil! @@ -770,8 +728,8 @@ struct APIRouter Koa.describe "Deletes a tag from a title" Koa.path "tid", desc: "A title ID" - Koa.response 200, ref: "$result" - Koa.tag "admin" + Koa.response 200, schema: "result" + Koa.tags ["admin", "library", "tags"] delete "/api/admin/tags/:tid/:tag" do |env| begin title = (Library.default.get_title env.params.url["tid"]).not_nil! @@ -792,8 +750,16 @@ struct APIRouter end Koa.describe "Lists all missing titles" - Koa.response 200, ref: "$missingResult" - Koa.tag "admin" + Koa.response 200, schema: { + "success" => Bool, + "error" => String?, + "titles?" => [{ + "path" => String, + "id" => String, + "signature" => String, + }], + } + Koa.tags ["admin", "library"] get "/api/admin/titles/missing" do |env| begin send_json env, { @@ -810,8 +776,16 @@ struct APIRouter end Koa.describe "Lists all missing entries" - Koa.response 200, ref: "$missingResult" - Koa.tag "admin" + Koa.response 200, schema: { + "success" => Bool, + "error" => String?, + "entries?" => [{ + "path" => String, + "id" => String, + "signature" => String, + }], + } + Koa.tags ["admin", "library"] get "/api/admin/entries/missing" do |env| begin send_json env, { @@ -828,8 +802,8 @@ struct APIRouter end Koa.describe "Deletes all missing titles" - Koa.response 200, ref: "$result" - Koa.tag "admin" + Koa.response 200, schema: "result" + Koa.tags ["admin", "library"] delete "/api/admin/titles/missing" do |env| begin Storage.default.delete_missing_title @@ -846,8 +820,8 @@ struct APIRouter end Koa.describe "Deletes all missing entries" - Koa.response 200, ref: "$result" - Koa.tag "admin" + Koa.response 200, schema: "result" + Koa.tags ["admin", "library"] delete "/api/admin/entries/missing" do |env| begin Storage.default.delete_missing_entry @@ -866,8 +840,8 @@ struct APIRouter Koa.describe "Deletes a missing title identified by `tid`", <<-MD Does nothing if the given `tid` is not found or if the title is not missing. MD - Koa.response 200, ref: "$result" - Koa.tag "admin" + Koa.response 200, schema: "result" + Koa.tags ["admin", "library"] delete "/api/admin/titles/missing/:tid" do |env| begin tid = env.params.url["tid"] @@ -887,8 +861,8 @@ struct APIRouter Koa.describe "Deletes a missing entry identified by `eid`", <<-MD Does nothing if the given `eid` is not found or if the entry is not missing. MD - Koa.response 200, ref: "$result" - Koa.tag "admin" + Koa.response 200, schema: "result" + Koa.tags ["admin", "library"] delete "/api/admin/entries/missing/:eid" do |env| begin eid = env.params.url["eid"] @@ -905,9 +879,19 @@ struct APIRouter end end - Koa.describe "Logs the current user into their MangaDex account, and " \ - "saves the token." - Koa.tag "admin" + Koa.describe "Logs the current user into their MangaDex account", <<-MD + If successful, returns the expiration date (as a unix timestamp) of the newly created token. + MD + Koa.body schema: { + "username" => String, + "password" => String, + } + Koa.response 200, schema: { + "success" => Bool, + "error" => String?, + "expires" => Int64?, + } + Koa.tags ["admin", "mangadex", "users"] post "/api/admin/mangadex/login" do |env| begin username = env.params.json["username"].as String @@ -934,9 +918,13 @@ struct APIRouter end end - Koa.describe "Returns the expiration date of the mangadex token if the " \ - "current user has logged in to MangaDex" - Koa.tag "admin" + Koa.describe "Returns the expiration date (as a unix timestamp) of the mangadex token if it exists" + Koa.response 200, schema: { + "success" => Bool, + "error" => String?, + "expires" => Int64?, + } + Koa.tags ["admin", "mangadex", "users"] get "/api/admin/mangadex/expires" do |env| begin username = get_username env From c36d2608e8309123f0c304fb8ddd2db741440faf Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Wed, 17 Feb 2021 11:55:24 +0000 Subject: [PATCH 03/14] Make uk-card adaptive to dark/light mode --- public/css/uikit.less | 19 +++++++++++++++++++ public/js/common.js | 4 ---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/public/css/uikit.less b/public/css/uikit.less index 9621f3b..76aa4ac 100644 --- a/public/css/uikit.less +++ b/public/css/uikit.less @@ -43,3 +43,22 @@ @internal-list-bullet-image: "../img/list-bullet.svg"; @internal-accordion-open-image: "../img/accordion-open.svg"; @internal-accordion-close-image: "../img/accordion-close.svg"; + +.hook-card-default() { + .uk-light & { + background: @card-secondary-background; + color: @card-secondary-color; + } +} + +.hook-card-default-title() { + .uk-light & { + color: @card-secondary-title-color; + } +} + +.hook-card-default-hover() { + .uk-light & { + background-color: @card-secondary-hover-background; + } +} diff --git a/public/js/common.js b/public/js/common.js index 22fc7dc..d1fd829 100644 --- a/public/js/common.js +++ b/public/js/common.js @@ -117,14 +117,10 @@ const setTheme = (theme) => { if (theme === 'dark') { $('html').css('background', 'rgb(20, 20, 20)'); $('body').addClass('uk-light'); - $('.uk-card').addClass('uk-card-secondary'); - $('.uk-card').removeClass('uk-card-default'); $('.ui-widget-content').addClass('dark'); } else { $('html').css('background', ''); $('body').removeClass('uk-light'); - $('.uk-card').removeClass('uk-card-secondary'); - $('.uk-card').addClass('uk-card-default'); $('.ui-widget-content').removeClass('dark'); } }; From 011768ed1f9ef381ed292bf78cd1ad24a623b053 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Wed, 17 Feb 2021 11:57:04 +0000 Subject: [PATCH 04/14] Rename the `dots-scripts` component to `dots` --- src/views/components/{dots-scripts.html.ecr => dots.html.ecr} | 4 ++-- src/views/home.html.ecr | 2 +- src/views/library.html.ecr | 2 +- src/views/tag.html.ecr | 2 +- src/views/title.html.ecr | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) rename src/views/components/{dots-scripts.html.ecr => dots.html.ecr} (67%) diff --git a/src/views/components/dots-scripts.html.ecr b/src/views/components/dots.html.ecr similarity index 67% rename from src/views/components/dots-scripts.html.ecr rename to src/views/components/dots.html.ecr index 41bb7b0..6e98eb7 100644 --- a/src/views/components/dots-scripts.html.ecr +++ b/src/views/components/dots.html.ecr @@ -1,3 +1,3 @@ - - + + diff --git a/src/views/home.html.ecr b/src/views/home.html.ecr index 598709d..ceb9b69 100644 --- a/src/views/home.html.ecr +++ b/src/views/home.html.ecr @@ -77,7 +77,7 @@ <%- end -%> <% content_for "script" do %> - <%= render_component "dots-scripts" %> + <%= render_component "dots" %> <% end %> diff --git a/src/views/library.html.ecr b/src/views/library.html.ecr index 21ec280..39e9856 100644 --- a/src/views/library.html.ecr +++ b/src/views/library.html.ecr @@ -24,7 +24,7 @@ <% content_for "script" do %> - <%= render_component "dots-scripts" %> + <%= render_component "dots" %> <% end %> diff --git a/src/views/tag.html.ecr b/src/views/tag.html.ecr index c6f2332..8d6d257 100644 --- a/src/views/tag.html.ecr +++ b/src/views/tag.html.ecr @@ -24,7 +24,7 @@ <% content_for "script" do %> - <%= render_component "dots-scripts" %> + <%= render_component "dots" %> <% end %> diff --git a/src/views/title.html.ecr b/src/views/title.html.ecr index 11b4393..78edf98 100644 --- a/src/views/title.html.ecr +++ b/src/views/title.html.ecr @@ -123,7 +123,7 @@ <% content_for "script" do %> - <%= render_component "dots-scripts" %> + <%= render_component "dots" %> From a9a2c9faa866135f1403d9e6884fe2d83cfe603c Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 26 Feb 2021 10:32:50 +0000 Subject: [PATCH 05/14] Finish search for MD --- public/css/mango.less | 4 +- public/js/download.js | 100 +++++++++++++++++++++++++++--------- shard.lock | 2 +- src/routes/api.cr | 44 +++++++++++++++- src/storage.cr | 4 +- src/views/download.html.ecr | 55 ++++++++++++++++++-- 6 files changed, 175 insertions(+), 34 deletions(-) diff --git a/public/css/mango.less b/public/css/mango.less index 3309be4..cd69876 100644 --- a/public/css/mango.less +++ b/public/css/mango.less @@ -34,9 +34,11 @@ .uk-card-body { padding: 20px; .uk-card-title { - max-height: 3em; font-size: 1rem; } + .uk-card-title:not(.free-height) { + max-height: 3em; + } } } diff --git a/public/js/download.js b/public/js/download.js index 4041d6a..d6eed3a 100644 --- a/public/js/download.js +++ b/public/js/download.js @@ -3,9 +3,12 @@ const downloadComponent = () => { chaptersLimit: 1000, loading: false, addingToDownload: false, + searchAvailable: false, searchInput: '', data: {}, chapters: [], + mangaAry: undefined, // undefined: not searching; []: searched but no result + candidateManga: {}, langChoice: 'All', groupChoice: 'All', chapterRange: '', @@ -48,7 +51,21 @@ const downloadComponent = () => { childList: true, subtree: true }); + + $.getJSON(`${base_url}api/admin/mangadex/expires`) + .done((data) => { + if (data.error) { + alert('danger', 'Failed to check MangaDex integration status. Error: ' + data.error); + return; + } + if (data.expires && data.expires > Math.floor(Date.now() / 1000)) + this.searchAvailable = true; + }) + .fail((jqXHR, status) => { + alert('danger', `Failed to check MangaDex integration status. Error: [${jqXHR.status}] ${jqXHR.statusText}`); + }) }, + filtersUpdated() { if (!this.data.chapters) this.chapters = []; @@ -90,10 +107,11 @@ const downloadComponent = () => { console.log('filtered chapters:', _chapters); this.chapters = _chapters; }, + search() { if (this.loading || this.searchInput === '') return; - this.loading = true; this.data = {}; + this.mangaAry = undefined; var int_id = -1; try { @@ -103,29 +121,54 @@ const downloadComponent = () => { } catch (e) { int_id = parseInt(this.searchInput); } - if (int_id <= 0 || isNaN(int_id)) { - alert('danger', 'Please make sure you are using a valid manga ID or manga URL from Mangadex.'); - this.loading = false; - return; + + if (!isNaN(int_id) && int_id > 0) { + // The input is a positive integer. We treat it as an ID. + this.loading = true; + $.getJSON(`${base_url}api/admin/mangadex/manga/${int_id}`) + .done((data) => { + if (data.error) { + alert('danger', 'Failed to get manga info. Error: ' + data.error); + return; + } + + this.data = data; + this.chapters = data.chapters; + this.mangaAry = undefined; + }) + .fail((jqXHR, status) => { + alert('danger', `Failed to get manga info. Error: [${jqXHR.status}] ${jqXHR.statusText}`); + }) + .always(() => { + this.loading = false; + }); + } else { + if (!this.searchAvailable) { + alert('danger', 'Please make sure you are using a valid manga ID or manga URL from Mangadex. If you are trying to search MangaDex with a search term, please log in to MangaDex first by going to "Admin -> Connect to MangaDex"'); + return; + } + + // Search as a search term + this.loading = true; + $.getJSON(`${base_url}api/admin/mangadex/search?${$.param({ + query: this.searchInput + })}`) + .done((data) => { + if (data.error) { + alert('danger', `Failed to search MangaDex. Error: ${data.error}`); + return; + } + + this.mangaAry = data.manga; + this.data = {}; + }) + .fail((jqXHR, status) => { + alert('danger', `Failed to search MangaDex. Error: [${jqXHR.status}] ${jqXHR.statusText}`); + }) + .always(() => { + this.loading = false; + }); } - - $.getJSON(`${base_url}api/admin/mangadex/manga/${int_id}`) - .done((data) => { - if (data.error) { - alert('danger', 'Failed to get manga info. Error: ' + data.error); - return; - } - - this.data = data; - this.chapters = data.chapters; - }) - .fail((jqXHR, status) => { - alert('danger', `Failed to get manga info. Error: [${jqXHR.status}] ${jqXHR.statusText}`); - }) - .always(() => { - this.loading = false; - }); - }, parseRange(str) { @@ -228,6 +271,17 @@ const downloadComponent = () => { this.addingToDownload = false; }); }); + }, + + chooseManga(manga) { + this.candidateManga = manga; + UIkit.modal($('#modal').get(0)).show(); + }, + + confirmManga(id) { + UIkit.modal($('#modal').get(0)).hide(); + this.searchInput = id; + this.search(); } }; }; diff --git a/shard.lock b/shard.lock index b6974f7..254f3a5 100644 --- a/shard.lock +++ b/shard.lock @@ -54,7 +54,7 @@ shards: mangadex: git: https://github.com/hkalexling/mangadex.git - version: 0.6.0+git.commit.d87a1d7a6e84d122814b38618954dcc73fc5553b + version: 0.8.0+git.commit.24e6fb51afd043721139355854e305b43bf98c43 mg: git: https://github.com/hkalexling/mg.git diff --git a/src/routes/api.cr b/src/routes/api.cr index 7599d1e..1e15918 100644 --- a/src/routes/api.cr +++ b/src/routes/api.cr @@ -57,14 +57,16 @@ struct APIRouter } Koa.schema("mdChapter", { + "id" => Int64, "group" => {} of String => String, - }.merge(s %w(id title volume chapter language full_title time + }.merge(s %w(title volume chapter language full_title time manga_title manga_id)), desc: "A MangaDex chapter") Koa.schema "mdManga", { + "id" => Int64, "chapters" => ["mdChapter"], - }.merge(s %w(id title description author artist cover_url)), + }.merge(s %w(title description author artist cover_url)), desc: "A MangaDex manga" Koa.describe "Returns a page in a manga entry" @@ -944,6 +946,44 @@ struct APIRouter end end + Koa.describe "Searches MangaDex for manga matching `query`", <<-MD + Returns an empty list if the current user hasn't logged in to MangaDex. + MD + Koa.query "query" + Koa.response 200, schema: { + "success" => Bool, + "error" => String?, + "manga?" => [{ + "id" => Int64, + "title" => String, + "description" => String, + "mainCover" => String, + }], + } + Koa.tags ["admin", "mangadex"] + get "/api/admin/mangadex/search" do |env| + begin + token, expires = Storage.default.get_md_token get_username env + client = MangaDex::Client.from_config + client.token = token + client.token_expires = expires + + query = env.params.query["query"] + + send_json env, { + "success" => true, + "error" => nil, + "manga" => client.partial_search query, + }.to_json + rescue e + Logger.error e + send_json env, { + "success" => false, + "error" => e.message, + }.to_json + end + end + doc = Koa.generate @@api_json = doc.to_json if doc diff --git a/src/storage.cr b/src/storage.cr index 88a5142..1d47d3f 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -530,9 +530,9 @@ class Storage end end - def get_md_token(username) : Tuple(String?, Time?) + def get_md_token(username) : Tuple(String?, Time) token = nil - expires = nil + expires = Time.utc MainFiber.run do get_db do |db| db.query_one? "select token, expire from md_account where " \ diff --git a/src/views/download.html.ecr b/src/views/download.html.ecr index a0489c0..0ea8527 100644 --- a/src/views/download.html.ecr +++ b/src/views/download.html.ecr @@ -1,17 +1,39 @@

    Download from MangaDex

    -
    -
    - +
    +
    +
    -
    +
    + +
    -
    +
    @@ -107,6 +129,29 @@
    + +
    <% content_for "script" do %> From 371c8056e7ece172de413f5cf2d69ca417ab7acd Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 5 Mar 2021 10:57:23 +0000 Subject: [PATCH 06/14] Wording --- public/js/download.js | 2 +- src/views/mangadex.html.ecr | 66 ++++++++++++++++++------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/public/js/download.js b/public/js/download.js index d6eed3a..74fab76 100644 --- a/public/js/download.js +++ b/public/js/download.js @@ -144,7 +144,7 @@ const downloadComponent = () => { }); } else { if (!this.searchAvailable) { - alert('danger', 'Please make sure you are using a valid manga ID or manga URL from Mangadex. If you are trying to search MangaDex with a search term, please log in to MangaDex first by going to "Admin -> Connect to MangaDex"'); + alert('danger', 'Please make sure you are using a valid manga ID or manga URL from Mangadex. If you are trying to search MangaDex with a search term, please log in to MangaDex first by going to "Admin -> Connect to MangaDex".'); return; } diff --git a/src/views/mangadex.html.ecr b/src/views/mangadex.html.ecr index dc7a378..764c4f4 100644 --- a/src/views/mangadex.html.ecr +++ b/src/views/mangadex.html.ecr @@ -1,39 +1,39 @@
    -

    Connect to MangaDex

    -
    -
    -

    This step is optional but highly recommended if you are using the MangaDex downloader. Connecting to MangaDex allows you to:

    -
      -
    • Search MangaDex by search terms in addition to manga IDs
    • -
    • Automatically download new chapters when they are available
    • -
    -
    - -
    -

    - You have logged in to MangaDex! - You have logged in to MangaDex but the token has expired. - The expiration date of your token is . - If the integration is not working, you - You - can log in again and the token will be updated. -

    -
    - -
    -
    -
    -
    -
    -
    -
    -
    -
    +

    Connect to MangaDex

    +
    +
    +

    This step is optional but highly recommended if you are using the MangaDex downloader. Connecting to MangaDex allows you to:

    +
      +
    • Search MangaDex by search terms in addition to manga IDs
    • +
    • Automatically download new chapters when they are available (coming soon)
    • +
    + +
    +

    + You have logged in to MangaDex! + You have logged in to MangaDex but the token has expired. + The expiration date of your token is . + If the integration is not working, you + You + can log in again and the token will be updated. +

    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    <% content_for "script" do %> - <%= render_component "moment" %> - - + <%= render_component "moment" %> + + <% end %> From 8aab113aabc5d162f08fd4f980461b29dd0c3153 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 5 Mar 2021 11:01:00 +0000 Subject: [PATCH 07/14] Expiration date should be nil when theres no token --- src/routes/api.cr | 8 +++++++- src/storage.cr | 4 ++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/routes/api.cr b/src/routes/api.cr index 1e15918..7d2af12 100644 --- a/src/routes/api.cr +++ b/src/routes/api.cr @@ -963,7 +963,13 @@ struct APIRouter Koa.tags ["admin", "mangadex"] get "/api/admin/mangadex/search" do |env| begin - token, expires = Storage.default.get_md_token get_username env + username = get_username env + token, expires = Storage.default.get_md_token username + + unless expires && token + raise "No token found for user #{username}" + end + client = MangaDex::Client.from_config client.token = token client.token_expires = expires diff --git a/src/storage.cr b/src/storage.cr index 1d47d3f..88a5142 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -530,9 +530,9 @@ class Storage end end - def get_md_token(username) : Tuple(String?, Time) + def get_md_token(username) : Tuple(String?, Time?) token = nil - expires = Time.utc + expires = nil MainFiber.run do get_db do |db| db.query_one? "select token, expire from md_account where " \ From 05b4e77fa9839868c7f983bf76ac95efdd7a19ad Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 5 Mar 2021 17:02:45 +0000 Subject: [PATCH 08/14] Entry selector on reader page (closes #168) --- public/js/reader.js | 23 ++++++++++++----------- src/library/entry.cr | 7 ++++--- src/library/library.cr | 2 +- src/routes/reader.cr | 15 +++++++++++---- src/views/reader.html.ecr | 34 +++++++++++++++++++++++++++++----- 5 files changed, 57 insertions(+), 24 deletions(-) diff --git a/public/js/reader.js b/public/js/reader.js index c6a00b5..1aa1bd1 100644 --- a/public/js/reader.js +++ b/public/js/reader.js @@ -9,6 +9,7 @@ const readerComponent = () => { flipAnimation: null, longPages: false, lastSavedPage: page, + selectedIndex: 0, // 0: not selected; 1: the first page /** * Initialize the component by fetching the page dimensions @@ -221,10 +222,7 @@ const readerComponent = () => { */ showControl(event) { const idx = event.currentTarget.id; - const pageCount = this.items.length; - const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`; - $('#progress-label').text(progressText); - $('#page-select').val(idx); + this.selectedIndex = idx; UIkit.modal($('#modal-sections')).show(); }, /** @@ -263,19 +261,22 @@ const readerComponent = () => { }); }, /** - * Exits the reader, and optionally sets the reading progress tp 100% + * Exits the reader, and sets the reading progress tp 100% * * @param {string} exitUrl - The Exit URL - * @param {boolean} [markCompleted] - Whether we should mark the - * reading progress to 100% */ - exitReader(exitUrl, markCompleted = false) { - if (!markCompleted) { - return this.redirect(exitUrl); - } + exitReader(exitUrl) { this.saveProgress(this.items.length, () => { this.redirect(exitUrl); }); + }, + + /** + * Handles the `change` event for the entry selector + */ + entryChanged() { + const id = $('#entry-select').val(); + this.redirect(`${base_url}reader/${tid}/${id}`); } }; } diff --git a/src/library/entry.cr b/src/library/entry.cr index 7a4e753..cb45895 100644 --- a/src/library/entry.cr +++ b/src/library/entry.cr @@ -134,10 +134,11 @@ class Entry entries[idx + 1] end - def previous_entry - idx = @book.entries.index self + def previous_entry(username) + entries = @book.sorted_entries username + idx = entries.index self return nil if idx.nil? || idx == 0 - @book.entries[idx - 1] + entries[idx - 1] end def date_added diff --git a/src/library/library.cr b/src/library/library.cr index 83c0b0a..7e97e85 100644 --- a/src/library/library.cr +++ b/src/library/library.cr @@ -121,7 +121,7 @@ class Library # Get the last read time of the entry. If it hasn't been started, get # the last read time of the previous entry last_read = e.load_last_read username - pe = e.previous_entry + pe = e.previous_entry username if last_read.nil? && pe last_read = pe.load_last_read username end diff --git a/src/routes/reader.cr b/src/routes/reader.cr index e61b9a0..40b86aa 100644 --- a/src/routes/reader.cr +++ b/src/routes/reader.cr @@ -30,6 +30,11 @@ struct ReaderRouter title = (Library.default.get_title env.params.url["title"]).not_nil! entry = (title.get_entry env.params.url["entry"]).not_nil! + + sort_opt = SortOptions.from_info_json title.dir, username + get_sort_opt + entries = title.sorted_entries username, sort_opt + page_idx = env.params.url["page"].to_i if page_idx > entry.pages || page_idx <= 0 raise "Page #{page_idx} not found." @@ -37,10 +42,12 @@ struct ReaderRouter exit_url = "#{base_url}book/#{title.id}" - next_entry_url = nil - next_entry = entry.next_entry username - unless next_entry.nil? - next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}" + next_entry_url = entry.next_entry(username).try do |e| + "#{base_url}reader/#{title.id}/#{e.id}" + end + + previous_entry_url = entry.previous_entry(username).try do |e| + "#{base_url}reader/#{title.id}/#{e.id}" end render "src/views/reader.html.ecr" diff --git a/src/views/reader.html.ecr b/src/views/reader.html.ecr index 90cc78a..0b4ee4d 100644 --- a/src/views/reader.html.ecr +++ b/src/views/reader.html.ecr @@ -36,7 +36,7 @@ <%- if next_entry_url -%> <%- else -%> - + <%- end -%>
    @@ -68,12 +68,12 @@
    -

    +

    - +
    - <%- (1..entry.pages).each do |p| -%> <%- end -%> @@ -89,9 +89,33 @@
    + +
    + +
    + +
    + +
    +
    From b7b7e6f7189c150b12b31d20fff9bf34b3ae9c56 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Fri, 5 Mar 2021 17:04:23 +0000 Subject: [PATCH 09/14] Fix typo [skip ci] --- src/storage.cr | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/storage.cr b/src/storage.cr index 88a5142..971bba3 100644 --- a/src/storage.cr +++ b/src/storage.cr @@ -34,7 +34,7 @@ class Storage dir = File.dirname @path unless Dir.exists? dir Logger.info "The DB directory #{dir} does not exist. " \ - "Attepmting to create it" + "Attempting to create it" Dir.mkdir_p dir end MainFiber.run do From f62344806a35977393fe19c926c18c82bb823fea Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Sat, 6 Mar 2021 06:16:07 +0000 Subject: [PATCH 10/14] Bump version to 0.21.0 --- README.md | 2 +- shard.yml | 2 +- src/mango.cr | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6e04104..b5fe269 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r ### CLI ``` - Mango - Manga Server and Web Reader. Version 0.20.2 + Mango - Manga Server and Web Reader. Version 0.21.0 Usage: diff --git a/shard.yml b/shard.yml index 40fbe72..390ddc6 100644 --- a/shard.yml +++ b/shard.yml @@ -1,5 +1,5 @@ name: mango -version: 0.20.2 +version: 0.21.0 authors: - Alex Ling diff --git a/src/mango.cr b/src/mango.cr index 0591045..b5fd168 100644 --- a/src/mango.cr +++ b/src/mango.cr @@ -8,7 +8,7 @@ require "option_parser" require "clim" require "tallboy" -MANGO_VERSION = "0.20.2" +MANGO_VERSION = "0.21.0" # From http://www.network-science.de/ascii/ BANNER = %{ From 2743868438bb21d03553230baf372776efa20bc2 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Sat, 6 Mar 2021 16:52:24 +0000 Subject: [PATCH 11/14] Remove outdated MD API link in warning --- src/config.cr | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/config.cr b/src/config.cr index abacbb3..5e4d055 100644 --- a/src/config.cr +++ b/src/config.cr @@ -97,9 +97,8 @@ class Config # `Logger.default` is not available yet Log.setup :debug Log.warn { "It looks like you are using the deprecated MangaDex API " \ - "v1 in your config file. Please update it to either " \ - "https://mangadex.org/api/v2 or " \ - "https://api.mangadex.org/v2 to suppress this warning." } + "v1 in your config file. Please update it to " \ + "https://mangadex.org/api/v2 to suppress this warning." } mangadex["api_url"] = "https://mangadex.org/api/v2" end mangadex["api_url"] = mangadex["api_url"].to_s.rstrip "/" From cb4e4437a6f7d2349fca06f2f5de4395867528d5 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Tue, 9 Mar 2021 16:43:46 +0000 Subject: [PATCH 12/14] Update MD API URL (closes #174) --- src/config.cr | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/config.cr b/src/config.cr index 5e4d055..b9885a2 100644 --- a/src/config.cr +++ b/src/config.cr @@ -29,7 +29,7 @@ class Config @[YAML::Field(ignore: true)] @mangadex_defaults = { "base_url" => "https://mangadex.org", - "api_url" => "https://mangadex.org/api/v2", + "api_url" => "https://api.mangadex.org/v2", "download_wait_seconds" => 5, "download_retries" => 4, "download_queue_db_path" => File.expand_path("~/mango/queue.db", @@ -98,8 +98,8 @@ class Config Log.setup :debug Log.warn { "It looks like you are using the deprecated MangaDex API " \ "v1 in your config file. Please update it to " \ - "https://mangadex.org/api/v2 to suppress this warning." } - mangadex["api_url"] = "https://mangadex.org/api/v2" + "https://api.mangadex.org/v2 to suppress this warning." } + mangadex["api_url"] = "https://api.mangadex.org/v2" end mangadex["api_url"] = mangadex["api_url"].to_s.rstrip "/" mangadex["base_url"] = mangadex["base_url"].to_s.rstrip "/" From 3a82effa4060e3930067e4ba69886c7408ed35f7 Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Tue, 9 Mar 2021 18:01:03 +0000 Subject: [PATCH 13/14] Update config in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b5fe269..e1545d6 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ default_username: "" auth_proxy_header_name: "" mangadex: base_url: https://mangadex.org - api_url: https://mangadex.org/api/v2 + api_url: https://api.mangadex.org/v2 download_wait_seconds: 5 download_retries: 4 download_queue_db_path: ~/mango/queue.db From eec6ec60bff4e261adcf54c8635688bf4db9d22f Mon Sep 17 00:00:00 2001 From: Alex Ling Date: Wed, 10 Mar 2021 05:47:25 +0000 Subject: [PATCH 14/14] Warn about old API url (#174) --- src/config.cr | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/config.cr b/src/config.cr index b9885a2..a05c3a7 100644 --- a/src/config.cr +++ b/src/config.cr @@ -93,14 +93,23 @@ class Config raise "Login is disabled, but default username is not set. " \ "Please set a default username" end + + # `Logger.default` is not available yet + Log.setup :debug unless mangadex["api_url"] =~ /\/v2/ - # `Logger.default` is not available yet - Log.setup :debug Log.warn { "It looks like you are using the deprecated MangaDex API " \ "v1 in your config file. Please update it to " \ "https://api.mangadex.org/v2 to suppress this warning." } mangadex["api_url"] = "https://api.mangadex.org/v2" end + if mangadex["api_url"] =~ /\/api\/v2/ + Log.warn { "It looks like you are using the outdated MangaDex API " \ + "url (mangadex.org/api/v2) in your config file. Please " \ + "update it to https://api.mangadex.org/v2 to suppress this " \ + "warning." } + mangadex["api_url"] = "https://api.mangadex.org/v2" + end + mangadex["api_url"] = mangadex["api_url"].to_s.rstrip "/" mangadex["base_url"] = mangadex["base_url"].to_s.rstrip "/" end