mirror of
https://github.com/hkalexling/Mango.git
synced 2025-08-02 02:45:29 -04:00
Subscription manager
This commit is contained in:
parent
9bb7144479
commit
a612500b0f
31
migration/subscription.12.cr
Normal file
31
migration/subscription.12.cr
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
class CreateSubscription < MG::Base
|
||||||
|
def up : String
|
||||||
|
# We allow multiple subscriptions for the same manga.
|
||||||
|
# This can be useful for example when you want to download from multiple
|
||||||
|
# groups.
|
||||||
|
<<-SQL
|
||||||
|
CREATE TABLE subscription (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
language TEXT,
|
||||||
|
group_id INTEGER,
|
||||||
|
min_volume INTEGER,
|
||||||
|
max_volume INTEGER,
|
||||||
|
min_chapter INTEGER,
|
||||||
|
max_chapter INTEGER,
|
||||||
|
last_checked INTEGER NOT NULL,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
username TEXT NOT NULL,
|
||||||
|
FOREIGN KEY (username) REFERENCES users (username)
|
||||||
|
ON UPDATE CASCADE
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
|
||||||
|
def down : String
|
||||||
|
<<-SQL
|
||||||
|
DROP TABLE subscription;
|
||||||
|
SQL
|
||||||
|
end
|
||||||
|
end
|
@ -282,6 +282,99 @@ const downloadComponent = () => {
|
|||||||
UIkit.modal($('#modal').get(0)).hide();
|
UIkit.modal($('#modal').get(0)).hide();
|
||||||
this.searchInput = id;
|
this.searchInput = id;
|
||||||
this.search();
|
this.search();
|
||||||
|
},
|
||||||
|
|
||||||
|
subscribe(langConfirmed = false, groupConfirmed = false) {
|
||||||
|
const filters = {
|
||||||
|
manga: this.data.id,
|
||||||
|
language: this.langChoice === 'All' ? null : this.langChoice,
|
||||||
|
group: this.groupChoice === 'All' ? null : this.groupChoice,
|
||||||
|
volume: this.volumeRange === '' ? null : this.volumeRange,
|
||||||
|
chapter: this.chapterRange === '' ? null : this.chapterRange
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get group ID
|
||||||
|
if (filters.group) {
|
||||||
|
this.data.chapters.forEach(chp => {
|
||||||
|
const gid = chp.groups[filters.group];
|
||||||
|
if (gid) {
|
||||||
|
filters.groupId = gid;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse range values
|
||||||
|
if (filters.volume) {
|
||||||
|
[filters.volumeMin, filters.volumeMax] = this.parseRange(filters.volume);
|
||||||
|
}
|
||||||
|
if (filters.chapter) {
|
||||||
|
[filters.chapterMin, filters.chapterMax] = this.parseRange(filters.chapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filters.language && !langConfirmed) {
|
||||||
|
UIkit.modal.confirm('You didn\'t specify a language in the filtering rules. This might cause Mango to download chapters that are not in your preferred language. Are you sure you want to continue?', {
|
||||||
|
labels: {
|
||||||
|
ok: 'Yes',
|
||||||
|
cancel: 'Cancel'
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
this.subscribe(true, groupConfirmed);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filters.group && !groupConfirmed) {
|
||||||
|
UIkit.modal.confirm('You didn\'t specify a group in the filtering rules. This might cause Mango to download multiple versions of the same chapter. Are you sure you want to continue?', {
|
||||||
|
labels: {
|
||||||
|
ok: 'Yes',
|
||||||
|
cancel: 'Cancel'
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
this.subscribe(langConfirmed, true);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mangaURL = `${mangadex_base_url}/manga/${filters.manga}`;
|
||||||
|
|
||||||
|
console.log(filters);
|
||||||
|
UIkit.modal.confirm(`All <strong>FUTURE</strong> chapters matching the following filters will be downloaded:<br>
|
||||||
|
<ul>
|
||||||
|
<li>Manga ID: ${filters.manga}</li>
|
||||||
|
<li>Language: ${filters.language || 'all'}</li>
|
||||||
|
<li>Group: ${filters.group || 'all'}</li>
|
||||||
|
<li>Volume: ${filters.volume || 'all'}</li>
|
||||||
|
<li>Chapter: ${filters.chapter || 'all'}</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<strong>IMPORTANT:</strong> Please make sure you are following the manga on MangaDex, otherwise Mango won't be able to receive any updates. To follow it, visit <a href="${mangaURL}">${mangaURL}</a> and click "Follow".
|
||||||
|
`, {
|
||||||
|
labels: {
|
||||||
|
ok: 'Confirm',
|
||||||
|
cancel: 'Cancel'
|
||||||
|
}
|
||||||
|
}).then(() => {
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: `${base_url}api/admin/mangadex/subscriptions`,
|
||||||
|
data: JSON.stringify({
|
||||||
|
subscription: filters
|
||||||
|
}),
|
||||||
|
contentType: "application/json",
|
||||||
|
dataType: 'json'
|
||||||
|
})
|
||||||
|
.done(data => {
|
||||||
|
console.log(data);
|
||||||
|
if (data.error) {
|
||||||
|
alert('danger', `Failed to subscribe. Error: ${data.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Failed to subscribe. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
73
public/js/subscription.js
Normal file
73
public/js/subscription.js
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
const component = () => {
|
||||||
|
return {
|
||||||
|
available: undefined,
|
||||||
|
subscriptions: [],
|
||||||
|
|
||||||
|
init() {
|
||||||
|
$.getJSON(`${base_url}api/admin/mangadex/expires`)
|
||||||
|
.done((data) => {
|
||||||
|
if (data.error) {
|
||||||
|
alert('danger', 'Failed to check MangaDex integration status. Error: ' + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.available = Boolean(data.expires && data.expires > Math.floor(Date.now() / 1000));
|
||||||
|
|
||||||
|
if (this.available) this.getSubscriptions();
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Failed to check MangaDex integration status. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
getSubscriptions() {
|
||||||
|
$.getJSON(`${base_url}api/admin/mangadex/subscriptions`)
|
||||||
|
.done(data => {
|
||||||
|
if (data.error) {
|
||||||
|
alert('danger', 'Failed to get subscriptions. Error: ' + data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.subscriptions = data.subscriptions;
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Failed to get subscriptions. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
rm(event) {
|
||||||
|
const id = event.currentTarget.parentNode.getAttribute('data-id');
|
||||||
|
$.ajax({
|
||||||
|
type: 'DELETE',
|
||||||
|
url: `${base_url}api/admin/mangadex/subscriptions/${id}`,
|
||||||
|
contentType: 'application/json'
|
||||||
|
})
|
||||||
|
.done(data => {
|
||||||
|
if (data.error) {
|
||||||
|
alert('danger', `Failed to delete subscription. Error: ${data.error}`);
|
||||||
|
}
|
||||||
|
this.getSubscriptions();
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Failed to delete subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
check(event) {
|
||||||
|
const id = event.currentTarget.parentNode.getAttribute('data-id');
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: `${base_url}api/admin/mangadex/subscriptions/check/${id}`,
|
||||||
|
contentType: 'application/json'
|
||||||
|
})
|
||||||
|
.done(data => {
|
||||||
|
if (data.error) {
|
||||||
|
alert('danger', `Failed to check subscription. Error: ${data.error}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
alert('success', 'Mango is now checking the subscription for updates. This might take a while, but you can safely leave the page.');
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Failed to check subscription. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
@ -54,7 +54,7 @@ shards:
|
|||||||
|
|
||||||
mangadex:
|
mangadex:
|
||||||
git: https://github.com/hkalexling/mangadex.git
|
git: https://github.com/hkalexling/mangadex.git
|
||||||
version: 0.9.0+git.commit.a8e5deb3e6f882f5bc0f4de66e0f6c20aa98a8a6
|
version: 0.11.0+git.commit.f5b0d64fbb138879fb9228b6e9ff34ec97c3e824
|
||||||
|
|
||||||
mg:
|
mg:
|
||||||
git: https://github.com/hkalexling/mg.git
|
git: https://github.com/hkalexling/mg.git
|
||||||
|
@ -33,8 +33,10 @@ class Config
|
|||||||
"download_retries" => 4,
|
"download_retries" => 4,
|
||||||
"download_queue_db_path" => File.expand_path("~/mango/queue.db",
|
"download_queue_db_path" => File.expand_path("~/mango/queue.db",
|
||||||
home: true),
|
home: true),
|
||||||
"chapter_rename_rule" => "[Vol.{volume} ][Ch.{chapter} ]{title|id}",
|
"chapter_rename_rule" => "[Vol.{volume} ]" \
|
||||||
|
"[Ch.{chapter} ]{title|id}",
|
||||||
"manga_rename_rule" => "{title}",
|
"manga_rename_rule" => "{title}",
|
||||||
|
"subscription_update_interval_hours" => 24,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@singlet : Config?
|
@@singlet : Config?
|
||||||
|
@ -42,6 +42,25 @@ class Library
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
subscription_interval = Config.current
|
||||||
|
.mangadex["subscription_update_interval_hours"].as Int32
|
||||||
|
unless subscription_interval < 1
|
||||||
|
spawn do
|
||||||
|
loop do
|
||||||
|
subscriptions = Storage.default.subscriptions
|
||||||
|
Logger.info "Checking MangaDex for updates on " \
|
||||||
|
"#{subscriptions.size} subscriptions"
|
||||||
|
added_count = 0
|
||||||
|
subscriptions.each do |sub|
|
||||||
|
added_count += sub.check_for_updates
|
||||||
|
end
|
||||||
|
Logger.info "Subscription update completed. Added #{added_count} " \
|
||||||
|
"chapters to the download queue"
|
||||||
|
sleep subscription_interval.hours
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def titles
|
def titles
|
||||||
|
@ -56,5 +56,39 @@ module MangaDex
|
|||||||
hash["full_title"] = JSON::Any.new full_title
|
hash["full_title"] = JSON::Any.new full_title
|
||||||
hash.to_json
|
hash.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# We don't need to rename the manga title here. It will be renamed in
|
||||||
|
# src/mangadex/downloader.cr
|
||||||
|
def to_job : Queue::Job
|
||||||
|
Queue::Job.new(
|
||||||
|
id.to_s,
|
||||||
|
manga_id.to_s,
|
||||||
|
full_title,
|
||||||
|
manga_title,
|
||||||
|
Queue::JobStatus::Pending,
|
||||||
|
Time.unix timestamp
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
struct User
|
||||||
|
def updates_after(time : Time, &block : Chapter ->)
|
||||||
|
page = 1
|
||||||
|
stopped = false
|
||||||
|
until stopped
|
||||||
|
chapters = followed_updates(page: page).chapters
|
||||||
|
return if chapters.empty?
|
||||||
|
chapters.each do |c|
|
||||||
|
if time > Time.unix c.timestamp
|
||||||
|
stopped = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
yield c
|
||||||
|
end
|
||||||
|
page += 1
|
||||||
|
# Let's not DDOS MangaDex :)
|
||||||
|
sleep 5.seconds
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -961,23 +961,95 @@ struct APIRouter
|
|||||||
Koa.tags ["admin", "mangadex"]
|
Koa.tags ["admin", "mangadex"]
|
||||||
get "/api/admin/mangadex/search" do |env|
|
get "/api/admin/mangadex/search" do |env|
|
||||||
begin
|
begin
|
||||||
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
|
|
||||||
|
|
||||||
query = env.params.query["query"]
|
query = env.params.query["query"]
|
||||||
|
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => true,
|
"success" => true,
|
||||||
"error" => nil,
|
"error" => nil,
|
||||||
"manga" => client.partial_search query,
|
"manga" => get_client(env).partial_search query,
|
||||||
|
}.to_json
|
||||||
|
rescue e
|
||||||
|
Logger.error e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
get "/api/admin/mangadex/subscriptions" do |env|
|
||||||
|
begin
|
||||||
|
send_json env, {
|
||||||
|
"success" => true,
|
||||||
|
"error" => nil,
|
||||||
|
"subscriptions" => Storage.default.subscriptions,
|
||||||
|
}.to_json
|
||||||
|
rescue e
|
||||||
|
Logger.error e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/api/admin/mangadex/subscriptions" do |env|
|
||||||
|
begin
|
||||||
|
json = env.params.json["subscription"].as Hash(String, JSON::Any)
|
||||||
|
sub = Subscription.new json["manga"].as_i64, get_username env
|
||||||
|
sub.language = json["language"]?.try &.as_s?
|
||||||
|
sub.group_id = json["groupId"]?.try &.as_i64?
|
||||||
|
sub.min_volume = json["volumeMin"]?.try &.as_i64?
|
||||||
|
sub.max_volume = json["volumeMax"]?.try &.as_i64?
|
||||||
|
sub.min_chapter = json["chapterMin"]?.try &.as_i64?
|
||||||
|
sub.max_chapter = json["chapterMax"]?.try &.as_i64?
|
||||||
|
|
||||||
|
Storage.default.save_subscription sub
|
||||||
|
|
||||||
|
send_json env, {
|
||||||
|
"success" => true,
|
||||||
|
"error" => nil,
|
||||||
|
}.to_json
|
||||||
|
rescue e
|
||||||
|
Logger.error e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
delete "/api/admin/mangadex/subscriptions/:id" do |env|
|
||||||
|
begin
|
||||||
|
id = env.params.url["id"].to_i64
|
||||||
|
Storage.default.delete_subscription id, get_username env
|
||||||
|
send_json env, {
|
||||||
|
"success" => true,
|
||||||
|
"error" => nil,
|
||||||
|
}.to_json
|
||||||
|
rescue e
|
||||||
|
Logger.error e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
post "/api/admin/mangadex/subscriptions/check/:id" do |env|
|
||||||
|
begin
|
||||||
|
id = env.params.url["id"].to_i64
|
||||||
|
username = get_username env
|
||||||
|
sub = Storage.default.get_subscription id, username
|
||||||
|
unless sub
|
||||||
|
raise "Subscription with id #{id} not found under user #{username}"
|
||||||
|
end
|
||||||
|
spawn do
|
||||||
|
sub.check_for_updates
|
||||||
|
end
|
||||||
|
send_json env, {
|
||||||
|
"success" => true,
|
||||||
|
"error" => nil,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
|
@ -95,6 +95,12 @@ struct MainRouter
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get "/download/subscription" do |env|
|
||||||
|
mangadex_base_url = Config.current.mangadex["base_url"]
|
||||||
|
username = get_username env
|
||||||
|
layout "subscription"
|
||||||
|
end
|
||||||
|
|
||||||
get "/" do |env|
|
get "/" do |env|
|
||||||
begin
|
begin
|
||||||
username = get_username env
|
username = get_username env
|
||||||
|
@ -5,6 +5,7 @@ require "base64"
|
|||||||
require "./util/*"
|
require "./util/*"
|
||||||
require "mg"
|
require "mg"
|
||||||
require "../migration/*"
|
require "../migration/*"
|
||||||
|
require "./subscription"
|
||||||
|
|
||||||
def hash_password(pw)
|
def hash_password(pw)
|
||||||
Crypto::Bcrypt::Password.create(pw).to_s
|
Crypto::Bcrypt::Password.create(pw).to_s
|
||||||
@ -14,6 +15,9 @@ def verify_password(hash, pw)
|
|||||||
(Crypto::Bcrypt::Password.new hash).verify pw
|
(Crypto::Bcrypt::Password.new hash).verify pw
|
||||||
end
|
end
|
||||||
|
|
||||||
|
SUB_ATTR = %w(manga_id language group_id min_volume max_volume min_chapter
|
||||||
|
max_chapter username)
|
||||||
|
|
||||||
class Storage
|
class Storage
|
||||||
@@insert_entry_ids = [] of IDTuple
|
@@insert_entry_ids = [] of IDTuple
|
||||||
@@insert_title_ids = [] of IDTuple
|
@@insert_title_ids = [] of IDTuple
|
||||||
@ -545,6 +549,70 @@ class Storage
|
|||||||
{token, expires}
|
{token, expires}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def save_subscription(sub : Subscription)
|
||||||
|
MainFiber.run do
|
||||||
|
get_db do |db|
|
||||||
|
{% begin %}
|
||||||
|
db.exec "insert into subscription (#{SUB_ATTR.join ","}, " \
|
||||||
|
"last_checked, created_at) values " \
|
||||||
|
"(#{Array.new(SUB_ATTR.size + 2, "?").join ","})",
|
||||||
|
{% for type in SUB_ATTR %}
|
||||||
|
sub.{{type.id}},
|
||||||
|
{% end %}
|
||||||
|
sub.last_checked.to_unix, sub.created_at.to_unix
|
||||||
|
{% end %}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def subscriptions : Array(Subscription)
|
||||||
|
subs = [] of Subscription
|
||||||
|
MainFiber.run do
|
||||||
|
get_db do |db|
|
||||||
|
db.query "select * from subscription" do |rs|
|
||||||
|
subs += Subscription.from_rs rs
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
subs
|
||||||
|
end
|
||||||
|
|
||||||
|
def delete_subscription(id : Int64, username : String)
|
||||||
|
MainFiber.run do
|
||||||
|
get_db do |db|
|
||||||
|
db.exec "delete from subscription where id = (?) and username = (?)",
|
||||||
|
id, username
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_subscription(id : Int64, username : String) : Subscription?
|
||||||
|
sub = nil
|
||||||
|
MainFiber.run do
|
||||||
|
get_db do |db|
|
||||||
|
db.query "select * from subscription where id = (?) and " \
|
||||||
|
"username = (?) limit 1", id, username do |rs|
|
||||||
|
sub = Subscription.from_rs(rs).first?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
sub
|
||||||
|
end
|
||||||
|
|
||||||
|
def update_subscription_last_checked(id : Int64? = nil)
|
||||||
|
MainFiber.run do
|
||||||
|
get_db do |db|
|
||||||
|
if id
|
||||||
|
db.exec "update subscription set last_checked = (?) where id = (?)",
|
||||||
|
Time.utc.to_unix, id
|
||||||
|
else
|
||||||
|
db.exec "update subscription set last_checked = (?)",
|
||||||
|
Time.utc.to_unix
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def close
|
def close
|
||||||
MainFiber.run do
|
MainFiber.run do
|
||||||
unless @db.nil?
|
unless @db.nil?
|
||||||
|
83
src/subscription.cr
Normal file
83
src/subscription.cr
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
require "db"
|
||||||
|
require "json"
|
||||||
|
|
||||||
|
struct Subscription
|
||||||
|
include DB::Serializable
|
||||||
|
include JSON::Serializable
|
||||||
|
|
||||||
|
getter id : Int64 = 0
|
||||||
|
getter username : String
|
||||||
|
getter manga_id : Int64
|
||||||
|
property language : String?
|
||||||
|
property group_id : Int64?
|
||||||
|
property min_volume : Int64?
|
||||||
|
property max_volume : Int64?
|
||||||
|
property min_chapter : Int64?
|
||||||
|
property max_chapter : Int64?
|
||||||
|
@[DB::Field(key: "last_checked")]
|
||||||
|
@[JSON::Field(key: "last_checked")]
|
||||||
|
@raw_last_checked : Int64
|
||||||
|
@[DB::Field(key: "created_at")]
|
||||||
|
@[JSON::Field(key: "created_at")]
|
||||||
|
@raw_created_at : Int64
|
||||||
|
|
||||||
|
def last_checked : Time
|
||||||
|
Time.unix @raw_last_checked
|
||||||
|
end
|
||||||
|
|
||||||
|
def created_at : Time
|
||||||
|
Time.unix @raw_created_at
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(@manga_id, @username)
|
||||||
|
@raw_created_at = Time.utc.to_unix
|
||||||
|
@raw_last_checked = Time.utc.to_unix
|
||||||
|
end
|
||||||
|
|
||||||
|
def in_range?(value : String, lowerbound : Int64?,
|
||||||
|
upperbound : Int64?) : Bool
|
||||||
|
lb = lowerbound.try &.to_f64
|
||||||
|
ub = upperbound.try &.to_f64
|
||||||
|
|
||||||
|
return true if lb.nil? && ub.nil?
|
||||||
|
|
||||||
|
v = value.to_f64?
|
||||||
|
return false unless v
|
||||||
|
|
||||||
|
if lb.nil?
|
||||||
|
v <= ub.not_nil!
|
||||||
|
elsif ub.nil?
|
||||||
|
v >= lb.not_nil!
|
||||||
|
else
|
||||||
|
v >= lb.not_nil! && v <= ub.not_nil!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def match?(chapter : MangaDex::Chapter) : Bool
|
||||||
|
if chapter.manga_id != manga_id ||
|
||||||
|
(language && chapter.language != language) ||
|
||||||
|
(group_id && !chapter.groups.map(&.id).includes? group_id)
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
in_range?(chapter.volume, min_volume, max_volume) &&
|
||||||
|
in_range?(chapter.chapter, min_chapter, max_chapter)
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_for_updates : Int32
|
||||||
|
Logger.debug "Checking updates for subscription with ID #{id}"
|
||||||
|
jobs = [] of Queue::Job
|
||||||
|
get_client(username).user.updates_after last_checked do |chapter|
|
||||||
|
next unless match? chapter
|
||||||
|
jobs << chapter.to_job
|
||||||
|
end
|
||||||
|
Storage.default.update_subscription_last_checked id
|
||||||
|
count = Queue.default.push jobs
|
||||||
|
Logger.debug "#{count}/#{jobs.size} of updates added to queue"
|
||||||
|
count
|
||||||
|
rescue e
|
||||||
|
Logger.error "Error occurred when checking updates for " \
|
||||||
|
"subscription with ID #{id}. #{e}"
|
||||||
|
0
|
||||||
|
end
|
||||||
|
end
|
@ -107,6 +107,25 @@ macro get_sort_opt
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Returns an authorized client
|
||||||
|
def get_client(username : String) : MangaDex::Client
|
||||||
|
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
|
||||||
|
|
||||||
|
client
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_client(env) : MangaDex::Client
|
||||||
|
get_client get_username env
|
||||||
|
end
|
||||||
|
|
||||||
module HTTP
|
module HTTP
|
||||||
class Client
|
class Client
|
||||||
private def self.exec(uri : URI, tls : TLSContext = nil)
|
private def self.exec(uri : URI, tls : TLSContext = nil)
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
<button class="uk-button uk-button-default" @click="load()" :disabled="loading">Refresh Queue</button>
|
<button class="uk-button uk-button-default" @click="load()" :disabled="loading">Refresh Queue</button>
|
||||||
<button class="uk-button uk-button-default" x-show="paused !== undefined" x-text="paused ? 'Resume Download' : 'Pause Download'" @click="toggle()" :disabled="toggling"></button>
|
<button class="uk-button uk-button-default" x-show="paused !== undefined" x-text="paused ? 'Resume Download' : 'Pause Download'" @click="toggle()" :disabled="toggling"></button>
|
||||||
</div>
|
</div>
|
||||||
<table class="uk-table uk-table-striped uk-overflow-auto">
|
<div class="uk-overflow-auto">
|
||||||
|
<table class="uk-table uk-table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Chapter</th>
|
<th>Chapter</th>
|
||||||
@ -61,6 +62,7 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<%= render_component "moment" %>
|
<%= render_component "moment" %>
|
||||||
|
@ -43,7 +43,10 @@
|
|||||||
<p x-text="`Author: ${data.author}`"></p>
|
<p x-text="`Author: ${data.author}`"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-form-stacked uk-width-1-2@s" id="filters">
|
<div class="uk-form-stacked uk-width-1-2@s" id="filters">
|
||||||
<p class="uk-text-lead uk-margin-remove-bottom">Filter Chapters</p>
|
<p class="uk-text-lead uk-margin-remove-bottom">
|
||||||
|
<span>Filter Chapters</span>
|
||||||
|
<button class="uk-icon-button uk-align-right" uk-icon="rss" uk-tooltip="Subscribe" x-show="searchAvailable" @click="subscribe()"></button>
|
||||||
|
</p>
|
||||||
<p class="uk-text-meta uk-margin-remove-top" x-text="`${chapters.length} chapters found`"></p>
|
<p class="uk-text-meta uk-margin-remove-top" x-text="`${chapters.length} chapters found`"></p>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label">Language</label>
|
<label class="uk-form-label">Language</label>
|
||||||
@ -93,7 +96,8 @@
|
|||||||
<p class="uk-text-meta">Click on a table row to select the chapter. Drag your mouse over multiple rows to select them all. Hold Ctrl to make multiple non-adjacent selections.</p>
|
<p class="uk-text-meta">Click on a table row to select the chapter. Drag your mouse over multiple rows to select them all. Hold Ctrl to make multiple non-adjacent selections.</p>
|
||||||
</div>
|
</div>
|
||||||
<p x-text="`Mango can only list ${chaptersLimit} chapters, but we found ${chapters.length} chapters. Please use the filter options above to narrow down your search.`" x-show="chapters.length > chaptersLimit"></p>
|
<p x-text="`Mango can only list ${chaptersLimit} chapters, but we found ${chapters.length} chapters. Please use the filter options above to narrow down your search.`" x-show="chapters.length > chaptersLimit"></p>
|
||||||
<table class="uk-table uk-table-striped uk-overflow-auto" x-show="chapters.length <= chaptersLimit">
|
<div class="uk-overflow-auto">
|
||||||
|
<table class="uk-table uk-table-striped" x-show="chapters.length <= chaptersLimit">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>ID</th>
|
||||||
@ -129,6 +133,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="modal" class="uk-flex-top" uk-modal="container: false">
|
<div id="modal" class="uk-flex-top" uk-modal="container: false">
|
||||||
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
||||||
@ -142,7 +147,7 @@
|
|||||||
<img uk-img data-width data-height :src="candidateManga.mainCover" style="width:100%;margin-bottom:10px;">
|
<img uk-img data-width data-height :src="candidateManga.mainCover" style="width:100%;margin-bottom:10px;">
|
||||||
<a :href="`<%= mangadex_base_url %>/manga/${candidateManga.id}`" x-text="`ID: ${candidateManga.id}`" class="uk-link-muted"></a>
|
<a :href="`<%= mangadex_base_url %>/manga/${candidateManga.id}`" x-text="`ID: ${candidateManga.id}`" class="uk-link-muted"></a>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-width-2-3@s" uk-overflow-auto>
|
<div class="uk-width-2-3@s">
|
||||||
<p x-text="candidateManga.description"></p>
|
<p x-text="candidateManga.description"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -157,6 +162,9 @@
|
|||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<%= render_component "moment" %>
|
<%= render_component "moment" %>
|
||||||
<%= render_component "jquery-ui" %>
|
<%= render_component "jquery-ui" %>
|
||||||
|
<script>
|
||||||
|
const mangadex_base_url = "<%= mangadex_base_url %>";
|
||||||
|
</script>
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/download.js"></script>
|
<script src="<%= base_url %>js/download.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
<li><a href="<%= base_url %>download">MangaDex</a></li>
|
<li><a href="<%= base_url %>download">MangaDex</a></li>
|
||||||
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
|
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
|
||||||
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
|
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
|
||||||
|
<li><a href="<%= base_url %>download/subscription">Subscription Manager</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
<% end %>
|
<% end %>
|
||||||
@ -53,6 +54,7 @@
|
|||||||
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
|
<li><a href="<%= base_url %>download/plugins">Plugins</a></li>
|
||||||
<li class="uk-nav-divider"></li>
|
<li class="uk-nav-divider"></li>
|
||||||
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
|
<li><a href="<%= base_url %>admin/downloads">Download Manager</a></li>
|
||||||
|
<li><a href="<%= base_url %>download/subscription">Subscription Manager</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
<div x-show="!empty">
|
<div x-show="!empty">
|
||||||
<p>The following items were present in your library, but now we can't find them anymore. If you deleted them mistakenly, try to recover the files or folders, put them back to where they were, and rescan the library. Otherwise, you can safely delete them and the associated metadata using the buttons below to free up database space.</p>
|
<p>The following items were present in your library, but now we can't find them anymore. If you deleted them mistakenly, try to recover the files or folders, put them back to where they were, and rescan the library. Otherwise, you can safely delete them and the associated metadata using the buttons below to free up database space.</p>
|
||||||
<button class="uk-button uk-button-danger" @click="rmAll()">Delete All</button>
|
<button class="uk-button uk-button-danger" @click="rmAll()">Delete All</button>
|
||||||
<table class="uk-table uk-table-striped uk-overflow-auto">
|
<div class="uk-overflow-auto">
|
||||||
|
<table class="uk-table uk-table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Type</th>
|
<th>Type</th>
|
||||||
@ -33,6 +34,7 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
|
@ -56,9 +56,11 @@
|
|||||||
<div id="download-spinner" uk-spinner class="uk-margin-left" hidden></div>
|
<div id="download-spinner" uk-spinner class="uk-margin-left" hidden></div>
|
||||||
</div>
|
</div>
|
||||||
<p class="uk-text-meta">Click on a table row to select the chapter. Drag your mouse over multiple rows to select them all. Hold Ctrl to make multiple non-adjacent selections.</p>
|
<p class="uk-text-meta">Click on a table row to select the chapter. Drag your mouse over multiple rows to select them all. Hold Ctrl to make multiple non-adjacent selections.</p>
|
||||||
<table class="uk-table uk-table-striped uk-overflow-auto tablesorter">
|
<div class="uk-overflow-auto">
|
||||||
|
<table class="uk-table uk-table-striped tablesorter">
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|
||||||
|
|
||||||
|
58
src/views/subscription.html.ecr
Normal file
58
src/views/subscription.html.ecr
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<h2 class="uk-title">MangaDex Subscription Manager</h2>
|
||||||
|
|
||||||
|
<div x-data="component()" x-init="init()">
|
||||||
|
<p x-show="available === false">The subscription manager uses a MangaDex API that requires authentication. Please <a href="<%= base_url %>admin/mangadex">connect to MangaDex</a> before using this feature.</p>
|
||||||
|
|
||||||
|
<p x-show="available && subscriptions.length === 0">No subscription found. Go to the <a href="<%= base_url %>download">MangaDex download page</a> and start subscribing.</p>
|
||||||
|
|
||||||
|
<template x-if="subscriptions.length > 0">
|
||||||
|
<div class="uk-overflow-auto">
|
||||||
|
<table class="uk-table uk-table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Manga ID</th>
|
||||||
|
<th>Language</th>
|
||||||
|
<th>Group ID</th>
|
||||||
|
<th>Min Volume</th>
|
||||||
|
<th>Max Volume</th>
|
||||||
|
<th>Min Chapter</th>
|
||||||
|
<th>Max Chapter</th>
|
||||||
|
<th>Creator</th>
|
||||||
|
<th>Last Checked</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<template x-for="sub in subscriptions" :key="sub">
|
||||||
|
<tr>
|
||||||
|
<td><a :href="`<%= mangadex_base_url %>/manga/${sub.manga_id}`" x-text="sub.manga_id"></a></td>
|
||||||
|
<td x-text="sub.language || 'All'"></td>
|
||||||
|
<td>
|
||||||
|
<a x-show="sub.group_id" :href="`<%= mangadex_base_url %>/group/${sub.group_id}`" x-text="sub.group_id"></a>
|
||||||
|
<span x-show="!sub.group_id">All</span>
|
||||||
|
</td>
|
||||||
|
<td x-text="sub.min_volume || '0'"></td>
|
||||||
|
<td x-text="sub.max_volume || '+∞'"></td>
|
||||||
|
<td x-text="sub.min_chapter || '0'"></td>
|
||||||
|
<td x-text="sub.max_chapter || '+∞'"></td>
|
||||||
|
<td x-text="sub.username"></td>
|
||||||
|
<td x-text="`${moment.unix(sub.last_checked).fromNow()}`"></td>
|
||||||
|
<td x-text="`${moment.unix(sub.created_at).fromNow()}`"></td>
|
||||||
|
<td :data-id="sub.id">
|
||||||
|
<a @click="check($event)" x-show="sub.username === '<%= username %>'" uk-icon="refresh" uk-tooltip="Check for updates"></a>
|
||||||
|
<a @click="rm($event)" x-show="sub.username === '<%= username %>'" uk-icon="trash" uk-tooltip="Delete"></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% content_for "script" do %>
|
||||||
|
<%= render_component "moment" %>
|
||||||
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
|
<script src="<%= base_url %>js/subscription.js"></script>
|
||||||
|
<% end %>
|
Loading…
x
Reference in New Issue
Block a user