mirror of
https://github.com/hkalexling/Mango.git
synced 2026-03-20 00:00:48 -04:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a17ca07d8 | ||
|
|
46e6e41bfe | ||
|
|
c9f55e7a8e | ||
|
|
741c3a4e20 | ||
|
|
f6da20321d | ||
|
|
2764e955b2 | ||
|
|
00c15014a1 | ||
|
|
c6fdbfd9fd | ||
|
|
e03bf32358 | ||
|
|
bbf1520c73 | ||
|
|
8950c3a1ed | ||
|
|
17837d8a29 | ||
|
|
b4a69425c8 | ||
|
|
a612500b0f | ||
|
|
9bb7144479 | ||
|
|
ee52c52f46 | ||
|
|
daec2bdac6 | ||
|
|
e9a490676b | ||
|
|
757f7c8214 | ||
|
|
eed1a9717e | ||
|
|
8829d2e237 | ||
|
|
eec6ec60bf | ||
|
|
3a82effa40 | ||
|
|
0b3e78bcb7 | ||
|
|
cb4e4437a6 | ||
|
|
6a275286ea | ||
|
|
2743868438 | ||
|
|
d3f26ecbc9 | ||
|
|
f62344806a | ||
|
|
b7b7e6f718 | ||
|
|
05b4e77fa9 | ||
|
|
8aab113aab | ||
|
|
371c8056e7 | ||
|
|
a9a2c9faa8 | ||
|
|
011768ed1f | ||
|
|
c36d2608e8 | ||
|
|
1b25a1fa47 | ||
|
|
df7e2270a4 |
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -12,12 +12,12 @@ jobs:
|
|||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: crystallang/crystal:0.35.1-alpine
|
image: crystallang/crystal:0.36.1-alpine
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static libjpeg-turbo-dev libpng-dev tiff-dev
|
run: apk add --no-cache yarn yaml-static sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static libjpeg-turbo-dev libpng-dev tiff-dev
|
||||||
- name: Build
|
- name: Build
|
||||||
run: make static || make static
|
run: make static || make static
|
||||||
- name: Linter
|
- name: Linter
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
FROM crystallang/crystal:0.35.1-alpine AS builder
|
FROM crystallang/crystal:0.36.1-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /Mango
|
WORKDIR /Mango
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN apk add --no-cache yarn yaml sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static libjpeg-turbo-dev libpng-dev tiff-dev
|
RUN apk add --no-cache yarn yaml-static sqlite-static libarchive-dev libarchive-static acl-static expat-static zstd-static lz4-static bzip2-static libjpeg-turbo-dev libpng-dev tiff-dev
|
||||||
RUN make static || make static
|
RUN make static || make static
|
||||||
|
|
||||||
FROM library/alpine
|
FROM library/alpine
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ FROM arm32v7/ubuntu:18.04
|
|||||||
|
|
||||||
RUN apt-get update && apt-get install -y wget git make llvm-8 llvm-8-dev g++ libsqlite3-dev libyaml-dev libgc-dev libssl-dev libcrypto++-dev libevent-dev libgmp-dev zlib1g-dev libpcre++-dev pkg-config libarchive-dev libxml2-dev libacl1-dev nettle-dev liblzo2-dev liblzma-dev libbz2-dev libjpeg-turbo8-dev libpng-dev libtiff-dev
|
RUN apt-get update && apt-get install -y wget git make llvm-8 llvm-8-dev g++ libsqlite3-dev libyaml-dev libgc-dev libssl-dev libcrypto++-dev libevent-dev libgmp-dev zlib1g-dev libpcre++-dev pkg-config libarchive-dev libxml2-dev libacl1-dev nettle-dev liblzo2-dev liblzma-dev libbz2-dev libjpeg-turbo8-dev libpng-dev libtiff-dev
|
||||||
|
|
||||||
RUN git clone https://github.com/crystal-lang/crystal && cd crystal && git checkout 0.35.1 && make deps && cd ..
|
RUN git clone https://github.com/crystal-lang/crystal && cd crystal && git checkout 0.36.1 && make deps && cd ..
|
||||||
RUN git clone https://github.com/kostya/myhtml && cd myhtml/src/ext && git checkout v1.5.0 && make && cd ..
|
RUN git clone https://github.com/kostya/myhtml && cd myhtml/src/ext && git checkout v1.5.0 && make && cd ..
|
||||||
RUN git clone https://github.com/jessedoyle/duktape.cr && cd duktape.cr/ext && git checkout v0.20.0 && make && cd ..
|
RUN git clone https://github.com/jessedoyle/duktape.cr && cd duktape.cr/ext && git checkout v0.20.0 && make && cd ..
|
||||||
RUN git clone https://github.com/hkalexling/image_size.cr && cd image_size.cr && git checkout v0.2.0 && make && cd ..
|
RUN git clone https://github.com/hkalexling/image_size.cr && cd image_size.cr && git checkout v0.2.0 && make && cd ..
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ FROM arm64v8/ubuntu:18.04
|
|||||||
|
|
||||||
RUN apt-get update && apt-get install -y wget git make llvm-8 llvm-8-dev g++ libsqlite3-dev libyaml-dev libgc-dev libssl-dev libcrypto++-dev libevent-dev libgmp-dev zlib1g-dev libpcre++-dev pkg-config libarchive-dev libxml2-dev libacl1-dev nettle-dev liblzo2-dev liblzma-dev libbz2-dev libjpeg-turbo8-dev libpng-dev libtiff-dev
|
RUN apt-get update && apt-get install -y wget git make llvm-8 llvm-8-dev g++ libsqlite3-dev libyaml-dev libgc-dev libssl-dev libcrypto++-dev libevent-dev libgmp-dev zlib1g-dev libpcre++-dev pkg-config libarchive-dev libxml2-dev libacl1-dev nettle-dev liblzo2-dev liblzma-dev libbz2-dev libjpeg-turbo8-dev libpng-dev libtiff-dev
|
||||||
|
|
||||||
RUN git clone https://github.com/crystal-lang/crystal && cd crystal && git checkout 0.35.1 && make deps && cd ..
|
RUN git clone https://github.com/crystal-lang/crystal && cd crystal && git checkout 0.36.1 && make deps && cd ..
|
||||||
RUN git clone https://github.com/kostya/myhtml && cd myhtml/src/ext && git checkout v1.5.0 && make && cd ..
|
RUN git clone https://github.com/kostya/myhtml && cd myhtml/src/ext && git checkout v1.5.0 && make && cd ..
|
||||||
RUN git clone https://github.com/jessedoyle/duktape.cr && cd duktape.cr/ext && git checkout v0.20.0 && make && cd ..
|
RUN git clone https://github.com/jessedoyle/duktape.cr && cd duktape.cr/ext && git checkout v0.20.0 && make && cd ..
|
||||||
RUN git clone https://github.com/hkalexling/image_size.cr && cd image_size.cr && git checkout v0.2.0 && make && cd ..
|
RUN git clone https://github.com/hkalexling/image_size.cr && cd image_size.cr && git checkout v0.2.0 && make && cd ..
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ The official docker images are available on [Dockerhub](https://hub.docker.com/r
|
|||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
```
|
```
|
||||||
Mango - Manga Server and Web Reader. Version 0.20.2
|
Mango - Manga Server and Web Reader. Version 0.22.0
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
@@ -93,12 +93,13 @@ default_username: ""
|
|||||||
auth_proxy_header_name: ""
|
auth_proxy_header_name: ""
|
||||||
mangadex:
|
mangadex:
|
||||||
base_url: https://mangadex.org
|
base_url: https://mangadex.org
|
||||||
api_url: https://mangadex.org/api/v2
|
api_url: https://api.mangadex.org/v2
|
||||||
download_wait_seconds: 5
|
download_wait_seconds: 5
|
||||||
download_retries: 4
|
download_retries: 4
|
||||||
download_queue_db_path: ~/mango/queue.db
|
download_queue_db_path: ~/mango/queue.db
|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
||||||
- `scan_interval_minutes`, `thumbnail_generation_interval_hours` and `db_optimization_interval_hours` can be any non-negative integer. Setting them to `0` disables the periodic tasks
|
- `scan_interval_minutes`, `thumbnail_generation_interval_hours` and `db_optimization_interval_hours` can be any non-negative integer. Setting them to `0` disables the periodic tasks
|
||||||
|
|||||||
20
migration/md_account.11.cr
Normal file
20
migration/md_account.11.cr
Normal file
@@ -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
|
||||||
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
|
||||||
@@ -34,9 +34,11 @@
|
|||||||
.uk-card-body {
|
.uk-card-body {
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
.uk-card-title {
|
.uk-card-title {
|
||||||
max-height: 3em;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
.uk-card-title:not(.free-height) {
|
||||||
|
max-height: 3em;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,3 +43,22 @@
|
|||||||
@internal-list-bullet-image: "../img/list-bullet.svg";
|
@internal-list-bullet-image: "../img/list-bullet.svg";
|
||||||
@internal-accordion-open-image: "../img/accordion-open.svg";
|
@internal-accordion-open-image: "../img/accordion-open.svg";
|
||||||
@internal-accordion-close-image: "../img/accordion-close.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -117,14 +117,10 @@ const setTheme = (theme) => {
|
|||||||
if (theme === 'dark') {
|
if (theme === 'dark') {
|
||||||
$('html').css('background', 'rgb(20, 20, 20)');
|
$('html').css('background', 'rgb(20, 20, 20)');
|
||||||
$('body').addClass('uk-light');
|
$('body').addClass('uk-light');
|
||||||
$('.uk-card').addClass('uk-card-secondary');
|
|
||||||
$('.uk-card').removeClass('uk-card-default');
|
|
||||||
$('.ui-widget-content').addClass('dark');
|
$('.ui-widget-content').addClass('dark');
|
||||||
} else {
|
} else {
|
||||||
$('html').css('background', '');
|
$('html').css('background', '');
|
||||||
$('body').removeClass('uk-light');
|
$('body').removeClass('uk-light');
|
||||||
$('.uk-card').removeClass('uk-card-secondary');
|
|
||||||
$('.uk-card').addClass('uk-card-default');
|
|
||||||
$('.ui-widget-content').removeClass('dark');
|
$('.ui-widget-content').removeClass('dark');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ const downloadComponent = () => {
|
|||||||
chaptersLimit: 1000,
|
chaptersLimit: 1000,
|
||||||
loading: false,
|
loading: false,
|
||||||
addingToDownload: false,
|
addingToDownload: false,
|
||||||
|
searchAvailable: false,
|
||||||
searchInput: '',
|
searchInput: '',
|
||||||
data: {},
|
data: {},
|
||||||
chapters: [],
|
chapters: [],
|
||||||
|
mangaAry: undefined, // undefined: not searching; []: searched but no result
|
||||||
|
candidateManga: {},
|
||||||
langChoice: 'All',
|
langChoice: 'All',
|
||||||
groupChoice: 'All',
|
groupChoice: 'All',
|
||||||
chapterRange: '',
|
chapterRange: '',
|
||||||
@@ -48,7 +51,21 @@ const downloadComponent = () => {
|
|||||||
childList: true,
|
childList: true,
|
||||||
subtree: 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() {
|
filtersUpdated() {
|
||||||
if (!this.data.chapters)
|
if (!this.data.chapters)
|
||||||
this.chapters = [];
|
this.chapters = [];
|
||||||
@@ -90,10 +107,11 @@ const downloadComponent = () => {
|
|||||||
console.log('filtered chapters:', _chapters);
|
console.log('filtered chapters:', _chapters);
|
||||||
this.chapters = _chapters;
|
this.chapters = _chapters;
|
||||||
},
|
},
|
||||||
|
|
||||||
search() {
|
search() {
|
||||||
if (this.loading || this.searchInput === '') return;
|
if (this.loading || this.searchInput === '') return;
|
||||||
this.loading = true;
|
|
||||||
this.data = {};
|
this.data = {};
|
||||||
|
this.mangaAry = undefined;
|
||||||
|
|
||||||
var int_id = -1;
|
var int_id = -1;
|
||||||
try {
|
try {
|
||||||
@@ -103,12 +121,10 @@ const downloadComponent = () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
int_id = parseInt(this.searchInput);
|
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}`)
|
$.getJSON(`${base_url}api/admin/mangadex/manga/${int_id}`)
|
||||||
.done((data) => {
|
.done((data) => {
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
@@ -118,6 +134,7 @@ const downloadComponent = () => {
|
|||||||
|
|
||||||
this.data = data;
|
this.data = data;
|
||||||
this.chapters = data.chapters;
|
this.chapters = data.chapters;
|
||||||
|
this.mangaAry = undefined;
|
||||||
})
|
})
|
||||||
.fail((jqXHR, status) => {
|
.fail((jqXHR, status) => {
|
||||||
alert('danger', `Failed to get manga info. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
alert('danger', `Failed to get manga info. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
@@ -125,7 +142,33 @@ const downloadComponent = () => {
|
|||||||
.always(() => {
|
.always(() => {
|
||||||
this.loading = false;
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseRange(str) {
|
parseRange(str) {
|
||||||
@@ -217,9 +260,7 @@ const downloadComponent = () => {
|
|||||||
}
|
}
|
||||||
const successCount = parseInt(data.success);
|
const successCount = parseInt(data.success);
|
||||||
const failCount = parseInt(data.fail);
|
const failCount = parseInt(data.fail);
|
||||||
UIkit.modal.confirm(`${successCount} of ${successCount + failCount} chapters added to the download queue. Proceed to the download manager?`).then(() => {
|
alert('success', `${successCount} of ${successCount + failCount} chapters added to the download queue. You can view and manage your download queue on the <a href="${base_url}admin/downloads">download manager page</a>.`);
|
||||||
window.location.href = base_url + 'admin/downloads';
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.fail((jqXHR, status) => {
|
.fail((jqXHR, status) => {
|
||||||
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
@@ -228,6 +269,111 @@ const downloadComponent = () => {
|
|||||||
this.addingToDownload = false;
|
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();
|
||||||
|
},
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
alert('success', `You've successfully subscribed to this manga! You can view and manage your subscriptions on the <a href="${base_url}download/subscription">subscription manager page</a>.`);
|
||||||
|
})
|
||||||
|
.fail((jqXHR, status) => {
|
||||||
|
alert('danger', `Failed to subscribe. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
61
public/js/mangadex.js
Normal file
61
public/js/mangadex.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -126,9 +126,7 @@ const download = () => {
|
|||||||
}
|
}
|
||||||
const successCount = parseInt(data.success);
|
const successCount = parseInt(data.success);
|
||||||
const failCount = parseInt(data.fail);
|
const failCount = parseInt(data.fail);
|
||||||
UIkit.modal.confirm(`${successCount} of ${successCount + failCount} chapters added to the download queue. Proceed to the download manager?`).then(() => {
|
alert('success', `${successCount} of ${successCount + failCount} chapters added to the download queue. You can view and manage your download queue on the <a href="${base_url}admin/downloads">download manager page</a>.`);
|
||||||
window.location.href = base_url + 'admin/downloads';
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.fail((jqXHR, status) => {
|
.fail((jqXHR, status) => {
|
||||||
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
alert('danger', `Failed to add chapters to the download queue. Error: [${jqXHR.status}] ${jqXHR.statusText}`);
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ const readerComponent = () => {
|
|||||||
flipAnimation: null,
|
flipAnimation: null,
|
||||||
longPages: false,
|
longPages: false,
|
||||||
lastSavedPage: page,
|
lastSavedPage: page,
|
||||||
|
selectedIndex: 0, // 0: not selected; 1: the first page
|
||||||
|
margin: 30,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the component by fetching the page dimensions
|
* Initialize the component by fetching the page dimensions
|
||||||
@@ -26,7 +28,6 @@ const readerComponent = () => {
|
|||||||
url: `${base_url}api/page/${tid}/${eid}/${i+1}`,
|
url: `${base_url}api/page/${tid}/${eid}/${i+1}`,
|
||||||
width: d.width,
|
width: d.width,
|
||||||
height: d.height,
|
height: d.height,
|
||||||
style: `margin-top: ${data.margin}px; margin-bottom: ${data.margin}px;`
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,6 +47,11 @@ const readerComponent = () => {
|
|||||||
const mode = this.mode;
|
const mode = this.mode;
|
||||||
this.updateMode(this.mode, page, nextTick);
|
this.updateMode(this.mode, page, nextTick);
|
||||||
$('#mode-select').val(mode);
|
$('#mode-select').val(mode);
|
||||||
|
|
||||||
|
const savedMargin = localStorage.getItem('margin');
|
||||||
|
if (savedMargin) {
|
||||||
|
this.margin = savedMargin;
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch(e => {
|
.catch(e => {
|
||||||
const errMsg = `Failed to get the page dimensions. ${e}`;
|
const errMsg = `Failed to get the page dimensions. ${e}`;
|
||||||
@@ -221,10 +227,7 @@ const readerComponent = () => {
|
|||||||
*/
|
*/
|
||||||
showControl(event) {
|
showControl(event) {
|
||||||
const idx = event.currentTarget.id;
|
const idx = event.currentTarget.id;
|
||||||
const pageCount = this.items.length;
|
this.selectedIndex = idx;
|
||||||
const progressText = `Progress: ${idx}/${pageCount} (${(idx/pageCount * 100).toFixed(1)}%)`;
|
|
||||||
$('#progress-label').text(progressText);
|
|
||||||
$('#page-select').val(idx);
|
|
||||||
UIkit.modal($('#modal-sections')).show();
|
UIkit.modal($('#modal-sections')).show();
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -263,19 +266,27 @@ 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 {string} exitUrl - The Exit URL
|
||||||
* @param {boolean} [markCompleted] - Whether we should mark the
|
|
||||||
* reading progress to 100%
|
|
||||||
*/
|
*/
|
||||||
exitReader(exitUrl, markCompleted = false) {
|
exitReader(exitUrl) {
|
||||||
if (!markCompleted) {
|
|
||||||
return this.redirect(exitUrl);
|
|
||||||
}
|
|
||||||
this.saveProgress(this.items.length, () => {
|
this.saveProgress(this.items.length, () => {
|
||||||
this.redirect(exitUrl);
|
this.redirect(exitUrl);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the `change` event for the entry selector
|
||||||
|
*/
|
||||||
|
entryChanged() {
|
||||||
|
const id = $('#entry-select').val();
|
||||||
|
this.redirect(`${base_url}reader/${tid}/${id}`);
|
||||||
|
},
|
||||||
|
|
||||||
|
marginChanged() {
|
||||||
|
localStorage.setItem('margin', this.margin);
|
||||||
|
this.toPage(this.selectedIndex);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
82
public/js/subscription.js
Normal file
82
public/js/subscription.js
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
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}`);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
formatRange(min, max) {
|
||||||
|
if (!isNaN(min) && isNaN(max)) return `≥ ${min}`;
|
||||||
|
if (isNaN(min) && !isNaN(max)) return `≤ ${max}`;
|
||||||
|
if (isNaN(min) && isNaN(max)) return 'All';
|
||||||
|
|
||||||
|
if (min === max) return `= ${min}`;
|
||||||
|
return `${min} - ${max}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
10
shard.lock
10
shard.lock
@@ -2,7 +2,7 @@ version: 2.0
|
|||||||
shards:
|
shards:
|
||||||
ameba:
|
ameba:
|
||||||
git: https://github.com/crystal-ameba/ameba.git
|
git: https://github.com/crystal-ameba/ameba.git
|
||||||
version: 0.12.1
|
version: 0.14.0
|
||||||
|
|
||||||
archive:
|
archive:
|
||||||
git: https://github.com/hkalexling/archive.cr.git
|
git: https://github.com/hkalexling/archive.cr.git
|
||||||
@@ -30,7 +30,7 @@ shards:
|
|||||||
|
|
||||||
http_proxy:
|
http_proxy:
|
||||||
git: https://github.com/mamantoha/http_proxy.git
|
git: https://github.com/mamantoha/http_proxy.git
|
||||||
version: 0.7.1
|
version: 0.8.0
|
||||||
|
|
||||||
image_size:
|
image_size:
|
||||||
git: https://github.com/hkalexling/image_size.cr.git
|
git: https://github.com/hkalexling/image_size.cr.git
|
||||||
@@ -42,7 +42,7 @@ shards:
|
|||||||
|
|
||||||
kemal-session:
|
kemal-session:
|
||||||
git: https://github.com/kemalcr/kemal-session.git
|
git: https://github.com/kemalcr/kemal-session.git
|
||||||
version: 0.12.1
|
version: 0.13.0
|
||||||
|
|
||||||
kilt:
|
kilt:
|
||||||
git: https://github.com/jeromegn/kilt.git
|
git: https://github.com/jeromegn/kilt.git
|
||||||
@@ -50,11 +50,11 @@ shards:
|
|||||||
|
|
||||||
koa:
|
koa:
|
||||||
git: https://github.com/hkalexling/koa.git
|
git: https://github.com/hkalexling/koa.git
|
||||||
version: 0.5.0
|
version: 0.7.0
|
||||||
|
|
||||||
mangadex:
|
mangadex:
|
||||||
git: https://github.com/hkalexling/mangadex.git
|
git: https://github.com/hkalexling/mangadex.git
|
||||||
version: 0.5.0+git.commit.323110c56c2d5134ce4162b27a9b24ec34137fcb
|
version: 0.11.0+git.commit.f5b0d64fbb138879fb9228b6e9ff34ec97c3e824
|
||||||
|
|
||||||
mg:
|
mg:
|
||||||
git: https://github.com/hkalexling/mg.git
|
git: https://github.com/hkalexling/mg.git
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
name: mango
|
name: mango
|
||||||
version: 0.20.2
|
version: 0.22.0
|
||||||
|
|
||||||
authors:
|
authors:
|
||||||
- Alex Ling <hkalexling@gmail.com>
|
- Alex Ling <hkalexling@gmail.com>
|
||||||
@@ -8,7 +8,7 @@ targets:
|
|||||||
mango:
|
mango:
|
||||||
main: src/mango.cr
|
main: src/mango.cr
|
||||||
|
|
||||||
crystal: 0.35.1
|
crystal: 0.36.1
|
||||||
|
|
||||||
license: MIT
|
license: MIT
|
||||||
|
|
||||||
|
|||||||
@@ -8,9 +8,7 @@ describe Storage do
|
|||||||
end
|
end
|
||||||
|
|
||||||
it "deletes user" do
|
it "deletes user" do
|
||||||
with_storage do |storage|
|
with_storage &.delete_user "admin"
|
||||||
storage.delete_user "admin"
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it "creates new user" do
|
it "creates new user" do
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ describe "compare_numerically" do
|
|||||||
it "sorts like the stack exchange post" do
|
it "sorts like the stack exchange post" do
|
||||||
ary = ["2", "12", "200000", "1000000", "a", "a12", "b2", "text2",
|
ary = ["2", "12", "200000", "1000000", "a", "a12", "b2", "text2",
|
||||||
"text2a", "text2a2", "text2a12", "text2ab", "text12", "text12a"]
|
"text2a", "text2a2", "text2a12", "text2ab", "text12", "text12a"]
|
||||||
ary.reverse.sort { |a, b|
|
ary.reverse.sort! { |a, b|
|
||||||
compare_numerically a, b
|
compare_numerically a, b
|
||||||
}.should eq ary
|
}.should eq ary
|
||||||
end
|
end
|
||||||
@@ -29,7 +29,7 @@ describe "compare_numerically" do
|
|||||||
# https://github.com/hkalexling/Mango/issues/22
|
# https://github.com/hkalexling/Mango/issues/22
|
||||||
it "handles numbers larger than Int32" do
|
it "handles numbers larger than Int32" do
|
||||||
ary = ["14410155591588.jpg", "21410155591588.png", "104410155591588.jpg"]
|
ary = ["14410155591588.jpg", "21410155591588.png", "104410155591588.jpg"]
|
||||||
ary.reverse.sort { |a, b|
|
ary.reverse.sort! { |a, b|
|
||||||
compare_numerically a, b
|
compare_numerically a, b
|
||||||
}.should eq ary
|
}.should eq ary
|
||||||
end
|
end
|
||||||
@@ -56,7 +56,7 @@ describe "chapter_sort" do
|
|||||||
it "sorts correctly" do
|
it "sorts correctly" do
|
||||||
ary = ["Vol.1 Ch.01", "Vol.1 Ch.02", "Vol.2 Ch. 2.5", "Ch. 3", "Ch.04"]
|
ary = ["Vol.1 Ch.01", "Vol.1 Ch.02", "Vol.2 Ch. 2.5", "Ch. 3", "Ch.04"]
|
||||||
sorter = ChapterSorter.new ary
|
sorter = ChapterSorter.new ary
|
||||||
ary.reverse.sort do |a, b|
|
ary.reverse.sort! do |a, b|
|
||||||
sorter.compare a, b
|
sorter.compare a, b
|
||||||
end.should eq ary
|
end.should eq ary
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class Config
|
|||||||
property plugin_path : String = File.expand_path "~/mango/plugins",
|
property plugin_path : String = File.expand_path "~/mango/plugins",
|
||||||
home: true
|
home: true
|
||||||
property download_timeout_seconds : Int32 = 30
|
property download_timeout_seconds : Int32 = 30
|
||||||
property page_margin : Int32 = 30
|
|
||||||
property disable_login = false
|
property disable_login = false
|
||||||
property default_username = ""
|
property default_username = ""
|
||||||
property auth_proxy_header_name = ""
|
property auth_proxy_header_name = ""
|
||||||
@@ -29,13 +28,15 @@ class Config
|
|||||||
@[YAML::Field(ignore: true)]
|
@[YAML::Field(ignore: true)]
|
||||||
@mangadex_defaults = {
|
@mangadex_defaults = {
|
||||||
"base_url" => "https://mangadex.org",
|
"base_url" => "https://mangadex.org",
|
||||||
"api_url" => "https://mangadex.org/api/v2",
|
"api_url" => "https://api.mangadex.org/v2",
|
||||||
"download_wait_seconds" => 5,
|
"download_wait_seconds" => 5,
|
||||||
"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?
|
||||||
@@ -93,15 +94,23 @@ class Config
|
|||||||
raise "Login is disabled, but default username is not set. " \
|
raise "Login is disabled, but default username is not set. " \
|
||||||
"Please set a default username"
|
"Please set a default username"
|
||||||
end
|
end
|
||||||
unless mangadex["api_url"] =~ /\/v2/
|
|
||||||
# `Logger.default` is not available yet
|
# `Logger.default` is not available yet
|
||||||
Log.setup :debug
|
Log.setup :debug
|
||||||
|
unless mangadex["api_url"] =~ /\/v2/
|
||||||
Log.warn { "It looks like you are using the deprecated MangaDex API " \
|
Log.warn { "It looks like you are using the deprecated MangaDex API " \
|
||||||
"v1 in your config file. Please update it to either " \
|
"v1 in your config file. Please update it to " \
|
||||||
"https://mangadex.org/api/v2 or " \
|
|
||||||
"https://api.mangadex.org/v2 to suppress this warning." }
|
"https://api.mangadex.org/v2 to suppress this warning." }
|
||||||
mangadex["api_url"] = "https://mangadex.org/api/v2"
|
mangadex["api_url"] = "https://api.mangadex.org/v2"
|
||||||
end
|
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["api_url"] = mangadex["api_url"].to_s.rstrip "/"
|
||||||
mangadex["base_url"] = mangadex["base_url"].to_s.rstrip "/"
|
mangadex["base_url"] = mangadex["base_url"].to_s.rstrip "/"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class Entry
|
|||||||
SUPPORTED_IMG_TYPES.includes? \
|
SUPPORTED_IMG_TYPES.includes? \
|
||||||
MIME.from_filename? e.filename
|
MIME.from_filename? e.filename
|
||||||
}
|
}
|
||||||
.sort { |a, b|
|
.sort! { |a, b|
|
||||||
compare_numerically a.filename, b.filename
|
compare_numerically a.filename, b.filename
|
||||||
}
|
}
|
||||||
yield file, entries
|
yield file, entries
|
||||||
@@ -134,10 +134,11 @@ class Entry
|
|||||||
entries[idx + 1]
|
entries[idx + 1]
|
||||||
end
|
end
|
||||||
|
|
||||||
def previous_entry
|
def previous_entry(username)
|
||||||
idx = @book.entries.index self
|
entries = @book.sorted_entries username
|
||||||
|
idx = entries.index self
|
||||||
return nil if idx.nil? || idx == 0
|
return nil if idx.nil? || idx == 0
|
||||||
@book.entries[idx - 1]
|
entries[idx - 1]
|
||||||
end
|
end
|
||||||
|
|
||||||
def date_added
|
def date_added
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -63,7 +82,7 @@ class Library
|
|||||||
end
|
end
|
||||||
|
|
||||||
def deep_titles
|
def deep_titles
|
||||||
titles + titles.map { |t| t.deep_titles }.flatten
|
titles + titles.flat_map &.deep_titles
|
||||||
end
|
end
|
||||||
|
|
||||||
def to_json(json : JSON::Builder)
|
def to_json(json : JSON::Builder)
|
||||||
@@ -98,7 +117,7 @@ class Library
|
|||||||
.select { |path| File.directory? path }
|
.select { |path| File.directory? path }
|
||||||
.map { |path| Title.new path, "" }
|
.map { |path| Title.new path, "" }
|
||||||
.select { |title| !(title.entries.empty? && title.titles.empty?) }
|
.select { |title| !(title.entries.empty? && title.titles.empty?) }
|
||||||
.sort { |a, b| a.title <=> b.title }
|
.sort! { |a, b| a.title <=> b.title }
|
||||||
.tap { |_| @title_ids.clear }
|
.tap { |_| @title_ids.clear }
|
||||||
.each do |title|
|
.each do |title|
|
||||||
@title_hash[title.id] = title
|
@title_hash[title.id] = title
|
||||||
@@ -114,14 +133,14 @@ class Library
|
|||||||
|
|
||||||
def get_continue_reading_entries(username)
|
def get_continue_reading_entries(username)
|
||||||
cr_entries = deep_titles
|
cr_entries = deep_titles
|
||||||
.map { |t| t.get_last_read_entry username }
|
.map(&.get_last_read_entry username)
|
||||||
# Select elements with type `Entry` from the array and ignore all `Nil`s
|
# Select elements with type `Entry` from the array and ignore all `Nil`s
|
||||||
.select(Entry)[0...ENTRIES_IN_HOME_SECTIONS]
|
.select(Entry)[0...ENTRIES_IN_HOME_SECTIONS]
|
||||||
.map { |e|
|
.map { |e|
|
||||||
# Get the last read time of the entry. If it hasn't been started, get
|
# Get the last read time of the entry. If it hasn't been started, get
|
||||||
# the last read time of the previous entry
|
# the last read time of the previous entry
|
||||||
last_read = e.load_last_read username
|
last_read = e.load_last_read username
|
||||||
pe = e.previous_entry
|
pe = e.previous_entry username
|
||||||
if last_read.nil? && pe
|
if last_read.nil? && pe
|
||||||
last_read = pe.load_last_read username
|
last_read = pe.load_last_read username
|
||||||
end
|
end
|
||||||
@@ -150,14 +169,14 @@ class Library
|
|||||||
recently_added = [] of RA
|
recently_added = [] of RA
|
||||||
last_date_added = nil
|
last_date_added = nil
|
||||||
|
|
||||||
titles.map { |t| t.deep_entries_with_date_added }.flatten
|
titles.flat_map(&.deep_entries_with_date_added)
|
||||||
.select { |e| e[:date_added] > 1.month.ago }
|
.select(&.[:date_added].> 1.month.ago)
|
||||||
.sort { |a, b| b[:date_added] <=> a[:date_added] }
|
.sort! { |a, b| b[:date_added] <=> a[:date_added] }
|
||||||
.each do |e|
|
.each do |e|
|
||||||
break if recently_added.size > 12
|
break if recently_added.size > 12
|
||||||
last = recently_added.last?
|
last = recently_added.last?
|
||||||
if last && e[:entry].book.id == last[:entry].book.id &&
|
if last && e[:entry].book.id == last[:entry].book.id &&
|
||||||
(e[:date_added] - last_date_added.not_nil!).duration < 1.day
|
(e[:date_added] - last_date_added.not_nil!).abs < 1.day
|
||||||
# A NamedTuple is immutable, so we have to cast it to a Hash first
|
# A NamedTuple is immutable, so we have to cast it to a Hash first
|
||||||
last_hash = last.to_h
|
last_hash = last.to_h
|
||||||
count = last_hash[:grouped_count].as(Int32)
|
count = last_hash[:grouped_count].as(Int32)
|
||||||
@@ -188,9 +207,9 @@ class Library
|
|||||||
# If we use `deep_titles`, the start reading section might include `Vol. 2`
|
# If we use `deep_titles`, the start reading section might include `Vol. 2`
|
||||||
# when the user hasn't started `Vol. 1` yet
|
# when the user hasn't started `Vol. 1` yet
|
||||||
titles
|
titles
|
||||||
.select { |t| t.load_percentage(username) == 0 }
|
.select(&.load_percentage(username).== 0)
|
||||||
.sample(ENTRIES_IN_HOME_SECTIONS)
|
.sample(ENTRIES_IN_HOME_SECTIONS)
|
||||||
.shuffle
|
.shuffle!
|
||||||
end
|
end
|
||||||
|
|
||||||
def thumbnail_generation_progress
|
def thumbnail_generation_progress
|
||||||
@@ -205,7 +224,7 @@ class Library
|
|||||||
end
|
end
|
||||||
|
|
||||||
Logger.info "Starting thumbnail generation"
|
Logger.info "Starting thumbnail generation"
|
||||||
entries = deep_titles.map(&.deep_entries).flatten.reject &.err_msg
|
entries = deep_titles.flat_map(&.deep_entries).reject &.err_msg
|
||||||
@entries_count = entries.size
|
@entries_count = entries.size
|
||||||
@thumbnails_count = 0
|
@thumbnails_count = 0
|
||||||
|
|
||||||
|
|||||||
@@ -44,14 +44,14 @@ class Title
|
|||||||
|
|
||||||
mtimes = [@mtime]
|
mtimes = [@mtime]
|
||||||
mtimes += @title_ids.map { |e| Library.default.title_hash[e].mtime }
|
mtimes += @title_ids.map { |e| Library.default.title_hash[e].mtime }
|
||||||
mtimes += @entries.map { |e| e.mtime }
|
mtimes += @entries.map &.mtime
|
||||||
@mtime = mtimes.max
|
@mtime = mtimes.max
|
||||||
|
|
||||||
@title_ids.sort! do |a, b|
|
@title_ids.sort! do |a, b|
|
||||||
compare_numerically Library.default.title_hash[a].title,
|
compare_numerically Library.default.title_hash[a].title,
|
||||||
Library.default.title_hash[b].title
|
Library.default.title_hash[b].title
|
||||||
end
|
end
|
||||||
sorter = ChapterSorter.new @entries.map { |e| e.title }
|
sorter = ChapterSorter.new @entries.map &.title
|
||||||
@entries.sort! do |a, b|
|
@entries.sort! do |a, b|
|
||||||
sorter.compare a.title, b.title
|
sorter.compare a.title, b.title
|
||||||
end
|
end
|
||||||
@@ -92,12 +92,12 @@ class Title
|
|||||||
# Get all entries, including entries in nested titles
|
# Get all entries, including entries in nested titles
|
||||||
def deep_entries
|
def deep_entries
|
||||||
return @entries if title_ids.empty?
|
return @entries if title_ids.empty?
|
||||||
@entries + titles.map { |t| t.deep_entries }.flatten
|
@entries + titles.flat_map &.deep_entries
|
||||||
end
|
end
|
||||||
|
|
||||||
def deep_titles
|
def deep_titles
|
||||||
return [] of Title if titles.empty?
|
return [] of Title if titles.empty?
|
||||||
titles + titles.map { |t| t.deep_titles }.flatten
|
titles + titles.flat_map &.deep_titles
|
||||||
end
|
end
|
||||||
|
|
||||||
def parents
|
def parents
|
||||||
@@ -138,7 +138,7 @@ class Title
|
|||||||
end
|
end
|
||||||
|
|
||||||
def get_entry(eid)
|
def get_entry(eid)
|
||||||
@entries.find { |e| e.id == eid }
|
@entries.find &.id.== eid
|
||||||
end
|
end
|
||||||
|
|
||||||
def display_name
|
def display_name
|
||||||
@@ -217,29 +217,23 @@ class Title
|
|||||||
@entries.each do |e|
|
@entries.each do |e|
|
||||||
e.save_progress username, e.pages
|
e.save_progress username, e.pages
|
||||||
end
|
end
|
||||||
titles.each do |t|
|
titles.each &.read_all username
|
||||||
t.read_all username
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set the reading progress of all entries and nested libraries to 0%
|
# Set the reading progress of all entries and nested libraries to 0%
|
||||||
def unread_all(username)
|
def unread_all(username)
|
||||||
@entries.each do |e|
|
@entries.each &.save_progress(username, 0)
|
||||||
e.save_progress username, 0
|
titles.each &.unread_all username
|
||||||
end
|
|
||||||
titles.each do |t|
|
|
||||||
t.unread_all username
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def deep_read_page_count(username) : Int32
|
def deep_read_page_count(username) : Int32
|
||||||
load_progress_for_all_entries(username).sum +
|
load_progress_for_all_entries(username).sum +
|
||||||
titles.map { |t| t.deep_read_page_count username }.flatten.sum
|
titles.flat_map(&.deep_read_page_count username).sum
|
||||||
end
|
end
|
||||||
|
|
||||||
def deep_total_page_count : Int32
|
def deep_total_page_count : Int32
|
||||||
entries.map { |e| e.pages }.sum +
|
entries.sum(&.pages) +
|
||||||
titles.map { |t| t.deep_total_page_count }.flatten.sum
|
titles.flat_map(&.deep_total_page_count).sum
|
||||||
end
|
end
|
||||||
|
|
||||||
def load_percentage(username)
|
def load_percentage(username)
|
||||||
@@ -311,13 +305,13 @@ class Title
|
|||||||
ary = @entries.zip(percentage_ary)
|
ary = @entries.zip(percentage_ary)
|
||||||
.sort { |a_tp, b_tp| (a_tp[1] <=> b_tp[1]).or \
|
.sort { |a_tp, b_tp| (a_tp[1] <=> b_tp[1]).or \
|
||||||
compare_numerically a_tp[0].title, b_tp[0].title }
|
compare_numerically a_tp[0].title, b_tp[0].title }
|
||||||
.map { |tp| tp[0] }
|
.map &.[0]
|
||||||
else
|
else
|
||||||
unless opt.method.auto?
|
unless opt.method.auto?
|
||||||
Logger.warn "Unknown sorting method #{opt.not_nil!.method}. Using " \
|
Logger.warn "Unknown sorting method #{opt.not_nil!.method}. Using " \
|
||||||
"Auto instead"
|
"Auto instead"
|
||||||
end
|
end
|
||||||
sorter = ChapterSorter.new @entries.map { |e| e.title }
|
sorter = ChapterSorter.new @entries.map &.title
|
||||||
ary = @entries.sort do |a, b|
|
ary = @entries.sort do |a, b|
|
||||||
sorter.compare(a.title, b.title).or \
|
sorter.compare(a.title, b.title).or \
|
||||||
compare_numerically a.title, b.title
|
compare_numerically a.title, b.title
|
||||||
@@ -383,13 +377,13 @@ class Title
|
|||||||
{entry: e, date_added: da_ary[i]}
|
{entry: e, date_added: da_ary[i]}
|
||||||
end
|
end
|
||||||
return zip if title_ids.empty?
|
return zip if title_ids.empty?
|
||||||
zip + titles.map { |t| t.deep_entries_with_date_added }.flatten
|
zip + titles.flat_map &.deep_entries_with_date_added
|
||||||
end
|
end
|
||||||
|
|
||||||
def bulk_progress(action, ids : Array(String), username)
|
def bulk_progress(action, ids : Array(String), username)
|
||||||
selected_entries = ids
|
selected_entries = ids
|
||||||
.map { |id|
|
.map { |id|
|
||||||
@entries.find { |e| e.id == id }
|
@entries.find &.id.==(id)
|
||||||
}
|
}
|
||||||
.select(Entry)
|
.select(Entry)
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,9 @@ module MangaDex
|
|||||||
@queue.set_status Queue::JobStatus::Downloading, job
|
@queue.set_status Queue::JobStatus::Downloading, job
|
||||||
begin
|
begin
|
||||||
chapter = @client.chapter job.id
|
chapter = @client.chapter job.id
|
||||||
|
# We must put the `.pages` call in a rescue block to handle external
|
||||||
|
# chapters.
|
||||||
|
pages = chapter.pages
|
||||||
rescue e
|
rescue e
|
||||||
Logger.error e
|
Logger.error e
|
||||||
@queue.set_status Queue::JobStatus::Error, job
|
@queue.set_status Queue::JobStatus::Error, job
|
||||||
@@ -58,7 +61,7 @@ module MangaDex
|
|||||||
@downloading = false
|
@downloading = false
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
@queue.set_pages chapter.pages.size, job
|
@queue.set_pages pages.size, job
|
||||||
lib_dir = @library_path
|
lib_dir = @library_path
|
||||||
rename_rule = Rename::Rule.new \
|
rename_rule = Rename::Rule.new \
|
||||||
Config.current.mangadex["manga_rename_rule"].to_s
|
Config.current.mangadex["manga_rename_rule"].to_s
|
||||||
@@ -69,13 +72,13 @@ module MangaDex
|
|||||||
zip_path = File.join manga_dir, "#{job.title}.cbz.part"
|
zip_path = File.join manga_dir, "#{job.title}.cbz.part"
|
||||||
|
|
||||||
# Find the number of digits needed to store the number of pages
|
# Find the number of digits needed to store the number of pages
|
||||||
len = Math.log10(chapter.pages.size).to_i + 1
|
len = Math.log10(pages.size).to_i + 1
|
||||||
|
|
||||||
writer = Compress::Zip::Writer.new zip_path
|
writer = Compress::Zip::Writer.new zip_path
|
||||||
# Create a buffered channel. It works as an FIFO queue
|
# Create a buffered channel. It works as an FIFO queue
|
||||||
channel = Channel(PageJob).new chapter.pages.size
|
channel = Channel(PageJob).new pages.size
|
||||||
spawn do
|
spawn do
|
||||||
chapter.pages.each_with_index do |url, i|
|
pages.each_with_index do |url, i|
|
||||||
fn = Path.new(URI.parse(url).path).basename
|
fn = Path.new(URI.parse(url).path).basename
|
||||||
ext = File.extname fn
|
ext = File.extname fn
|
||||||
fn = "#{i.to_s.rjust len, '0'}#{ext}"
|
fn = "#{i.to_s.rjust len, '0'}#{ext}"
|
||||||
@@ -99,7 +102,7 @@ module MangaDex
|
|||||||
|
|
||||||
spawn do
|
spawn do
|
||||||
page_jobs = [] of PageJob
|
page_jobs = [] of PageJob
|
||||||
chapter.pages.size.times do
|
pages.size.times do
|
||||||
page_job = channel.receive
|
page_job = channel.receive
|
||||||
|
|
||||||
break unless @queue.exists? job
|
break unless @queue.exists? job
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ module MangaDex
|
|||||||
struct Chapter
|
struct Chapter
|
||||||
def rename(rule : Rename::Rule)
|
def rename(rule : Rename::Rule)
|
||||||
hash = properties_to_hash %w(id title volume chapter lang_code language)
|
hash = properties_to_hash %w(id title volume chapter lang_code language)
|
||||||
hash["groups"] = groups.map(&.name).join ","
|
hash["groups"] = groups.join(",", &.name)
|
||||||
rule.render hash
|
rule.render hash
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ require "option_parser"
|
|||||||
require "clim"
|
require "clim"
|
||||||
require "tallboy"
|
require "tallboy"
|
||||||
|
|
||||||
MANGO_VERSION = "0.20.2"
|
MANGO_VERSION = "0.22.0"
|
||||||
|
|
||||||
# From http://www.network-science.de/ascii/
|
# From http://www.network-science.de/ascii/
|
||||||
BANNER = %{
|
BANNER = %{
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ class Plugin
|
|||||||
def initialize(id : String)
|
def initialize(id : String)
|
||||||
Plugin.build_info_ary
|
Plugin.build_info_ary
|
||||||
|
|
||||||
@info = @@info_ary.find { |i| i.id == id }
|
@info = @@info_ary.find &.id.== id
|
||||||
if @info.nil?
|
if @info.nil?
|
||||||
raise Error.new "Plugin with ID #{id} not found"
|
raise Error.new "Plugin with ID #{id} not found"
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -303,12 +303,12 @@ class Queue
|
|||||||
end
|
end
|
||||||
|
|
||||||
def pause
|
def pause
|
||||||
@downloaders.each { |d| d.stopped = true }
|
@downloaders.each &.stopped=(true)
|
||||||
@paused = true
|
@paused = true
|
||||||
end
|
end
|
||||||
|
|
||||||
def resume
|
def resume
|
||||||
@downloaders.each { |d| d.stopped = false }
|
@downloaders.each &.stopped=(false)
|
||||||
@paused = false
|
@paused = false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -35,15 +35,15 @@ module Rename
|
|||||||
|
|
||||||
class Group < Base(Pattern | String)
|
class Group < Base(Pattern | String)
|
||||||
def render(hash : VHash)
|
def render(hash : VHash)
|
||||||
return "" if @ary.select(&.is_a? Pattern)
|
return "" if @ary.select(Pattern)
|
||||||
.any? &.as(Pattern).render(hash).empty?
|
.any? &.as(Pattern).render(hash).empty?
|
||||||
@ary.map do |e|
|
@ary.join do |e|
|
||||||
if e.is_a? Pattern
|
if e.is_a? Pattern
|
||||||
e.render hash
|
e.render hash
|
||||||
else
|
else
|
||||||
e
|
e
|
||||||
end
|
end
|
||||||
end.join
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -129,13 +129,13 @@ module Rename
|
|||||||
end
|
end
|
||||||
|
|
||||||
def render(hash : VHash)
|
def render(hash : VHash)
|
||||||
str = @ary.map do |e|
|
str = @ary.join do |e|
|
||||||
if e.is_a? String
|
if e.is_a? String
|
||||||
e
|
e
|
||||||
else
|
else
|
||||||
e.render hash
|
e.render hash
|
||||||
end
|
end
|
||||||
end.join.strip
|
end.strip
|
||||||
post_process str
|
post_process str
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -73,5 +73,9 @@ struct AdminRouter
|
|||||||
get "/admin/missing" do |env|
|
get "/admin/missing" do |env|
|
||||||
layout "missing-items"
|
layout "missing-items"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
get "/admin/mangadex" do |env|
|
||||||
|
layout "mangadex"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ struct APIRouter
|
|||||||
macro s(fields)
|
macro s(fields)
|
||||||
{
|
{
|
||||||
{% for field in fields %}
|
{% for field in fields %}
|
||||||
{{field}} => "string",
|
{{field}} => String,
|
||||||
{% end %}
|
{% end %}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -33,160 +33,49 @@ struct APIRouter
|
|||||||
MD
|
MD
|
||||||
|
|
||||||
Koa.cookie_auth "cookie", "mango-sessid-#{Config.current.port}"
|
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.
|
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
|
MD
|
||||||
|
|
||||||
Koa.binary "binary", desc: "A binary file"
|
Koa.schema "entry", {
|
||||||
Koa.array "entryAry", "$entry", desc: "An array of entries"
|
"pages" => Int32,
|
||||||
Koa.array "titleAry", "$title", desc: "An array of titles"
|
"mtime" => Int64,
|
||||||
Koa.array "strAry", "string", desc: "An array of strings"
|
}.merge(s %w(zip_path title size id title_id display_name cover_url)),
|
||||||
|
desc: "An entry in a book"
|
||||||
|
|
||||||
entry_schema = {
|
Koa.schema "title", {
|
||||||
"pages" => "integer",
|
"mtime" => Int64,
|
||||||
"mtime" => "integer",
|
"entries" => ["entry"],
|
||||||
}.merge s %w(zip_path title size id title_id display_name cover_url)
|
"titles" => ["title"],
|
||||||
Koa.object "entry", entry_schema, desc: "An entry in a book"
|
"parents" => [String],
|
||||||
|
}.merge(s %w(dir title id display_name cover_url)),
|
||||||
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,
|
|
||||||
desc: "A manga title (a collection of entries and sub-titles)"
|
desc: "A manga title (a collection of entries and sub-titles)"
|
||||||
|
|
||||||
Koa.object "library", {
|
Koa.schema "result", {
|
||||||
"dir" => "string",
|
"success" => Bool,
|
||||||
"titles" => "$titleAry",
|
"error" => String?,
|
||||||
}, desc: "A library containing a list of top-level titles"
|
|
||||||
|
|
||||||
Koa.object "scanResult", {
|
|
||||||
"milliseconds" => "integer",
|
|
||||||
"titles" => "integer",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Koa.object "progressResult", {
|
Koa.schema("mdChapter", {
|
||||||
"progress" => "number",
|
"id" => Int64,
|
||||||
}
|
"group" => {} of String => String,
|
||||||
|
}.merge(s %w(title volume chapter language full_title time
|
||||||
|
manga_title manga_id)),
|
||||||
|
desc: "A MangaDex chapter")
|
||||||
|
|
||||||
Koa.object "result", {
|
Koa.schema "mdManga", {
|
||||||
"success" => "boolean",
|
"id" => Int64,
|
||||||
"error" => "string?",
|
"chapters" => ["mdChapter"],
|
||||||
}
|
}.merge(s %w(title description author artist cover_url)),
|
||||||
|
desc: "A MangaDex manga"
|
||||||
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.describe "Returns a page in a manga entry"
|
Koa.describe "Returns a page in a manga entry"
|
||||||
Koa.path "tid", desc: "Title ID"
|
Koa.path "tid", desc: "Title ID"
|
||||||
Koa.path "eid", desc: "Entry ID"
|
Koa.path "eid", desc: "Entry ID"
|
||||||
Koa.path "page", type: "integer", desc: "The page number to return (starts from 1)"
|
Koa.path "page", schema: Int32, desc: "The page number to return (starts from 1)"
|
||||||
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.response 500, "Page not found or not readable"
|
||||||
|
Koa.tag "reader"
|
||||||
get "/api/page/:tid/:eid/:page" do |env|
|
get "/api/page/:tid/:eid/:page" do |env|
|
||||||
begin
|
begin
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
@@ -212,8 +101,9 @@ struct APIRouter
|
|||||||
Koa.describe "Returns the cover image of a manga entry"
|
Koa.describe "Returns the cover image of a manga entry"
|
||||||
Koa.path "tid", desc: "Title ID"
|
Koa.path "tid", desc: "Title ID"
|
||||||
Koa.path "eid", desc: "Entry 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.response 500, "Page not found or not readable"
|
||||||
|
Koa.tag "library"
|
||||||
get "/api/cover/:tid/:eid" do |env|
|
get "/api/cover/:tid/:eid" do |env|
|
||||||
begin
|
begin
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
@@ -238,8 +128,9 @@ struct APIRouter
|
|||||||
|
|
||||||
Koa.describe "Returns the book with title `tid`"
|
Koa.describe "Returns the book with title `tid`"
|
||||||
Koa.path "tid", desc: "Title ID"
|
Koa.path "tid", desc: "Title ID"
|
||||||
Koa.response 200, ref: "$title"
|
Koa.response 200, schema: "title"
|
||||||
Koa.response 404, "Title not found"
|
Koa.response 404, "Title not found"
|
||||||
|
Koa.tag "library"
|
||||||
get "/api/book/:tid" do |env|
|
get "/api/book/:tid" do |env|
|
||||||
begin
|
begin
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
@@ -255,14 +146,21 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Returns the entire library with all titles and entries"
|
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|
|
get "/api/library" do |env|
|
||||||
send_json env, Library.default.to_json
|
send_json env, Library.default.to_json
|
||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Triggers a library scan"
|
Koa.describe "Triggers a library scan"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "library"]
|
||||||
Koa.response 200, ref: "$scanResult"
|
Koa.response 200, schema: {
|
||||||
|
"milliseconds" => Float64,
|
||||||
|
"titles" => Int32,
|
||||||
|
}
|
||||||
post "/api/admin/scan" do |env|
|
post "/api/admin/scan" do |env|
|
||||||
start = Time.utc
|
start = Time.utc
|
||||||
Library.default.scan
|
Library.default.scan
|
||||||
@@ -274,8 +172,10 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Returns the thumbnail generation progress between 0 and 1"
|
Koa.describe "Returns the thumbnail generation progress between 0 and 1"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "library"]
|
||||||
Koa.response 200, ref: "$progressResult"
|
Koa.response 200, schema: {
|
||||||
|
"progress" => Float64,
|
||||||
|
}
|
||||||
get "/api/admin/thumbnail_progress" do |env|
|
get "/api/admin/thumbnail_progress" do |env|
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"progress" => Library.default.thumbnail_generation_progress,
|
"progress" => Library.default.thumbnail_generation_progress,
|
||||||
@@ -283,7 +183,7 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Triggers a thumbnail generation"
|
Koa.describe "Triggers a thumbnail generation"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "library"]
|
||||||
post "/api/admin/generate_thumbnails" do |env|
|
post "/api/admin/generate_thumbnails" do |env|
|
||||||
spawn do
|
spawn do
|
||||||
Library.default.generate_thumbnails
|
Library.default.generate_thumbnails
|
||||||
@@ -291,8 +191,8 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Deletes a user with `username`"
|
Koa.describe "Deletes a user with `username`"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "users"]
|
||||||
Koa.response 200, ref: "$result"
|
Koa.response 200, schema: "result"
|
||||||
delete "/api/admin/user/delete/:username" do |env|
|
delete "/api/admin/user/delete/:username" do |env|
|
||||||
begin
|
begin
|
||||||
username = env.params.url["username"]
|
username = env.params.url["username"]
|
||||||
@@ -319,7 +219,8 @@ struct APIRouter
|
|||||||
Koa.path "tid", desc: "Title ID"
|
Koa.path "tid", desc: "Title ID"
|
||||||
Koa.query "eid", desc: "Entry ID", required: false
|
Koa.query "eid", desc: "Entry ID", required: false
|
||||||
Koa.path "page", desc: "The new page number indicating the progress"
|
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|
|
put "/api/progress/:tid/:page" do |env|
|
||||||
begin
|
begin
|
||||||
username = get_username env
|
username = get_username env
|
||||||
@@ -350,8 +251,11 @@ struct APIRouter
|
|||||||
Koa.describe "Updates the reading progress of multiple entries in a title"
|
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 "action", desc: "The action to perform. Can be either `read` or `unread`"
|
||||||
Koa.path "tid", desc: "Title ID"
|
Koa.path "tid", desc: "Title ID"
|
||||||
Koa.body ref: "$ids", desc: "An array of entry IDs"
|
Koa.body schema: {
|
||||||
Koa.response 200, ref: "$result"
|
"ids" => [String],
|
||||||
|
}, desc: "An array of entry IDs"
|
||||||
|
Koa.response 200, schema: "result"
|
||||||
|
Koa.tag "progress"
|
||||||
put "/api/bulk_progress/:action/:tid" do |env|
|
put "/api/bulk_progress/:action/:tid" do |env|
|
||||||
begin
|
begin
|
||||||
username = get_username env
|
username = get_username env
|
||||||
@@ -377,11 +281,11 @@ struct APIRouter
|
|||||||
Koa.describe "Sets the display name of a title or an entry", <<-MD
|
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`.
|
When `eid` is provided, apply the display name to the entry. Otherwise, apply the display name to the title identified by `tid`.
|
||||||
MD
|
MD
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "library"]
|
||||||
Koa.path "tid", desc: "Title ID"
|
Koa.path "tid", desc: "Title ID"
|
||||||
Koa.query "eid", desc: "Entry ID", required: false
|
Koa.query "eid", desc: "Entry ID", required: false
|
||||||
Koa.path "name", desc: "The new display name"
|
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|
|
put "/api/admin/display_name/:tid/:name" do |env|
|
||||||
begin
|
begin
|
||||||
title = (Library.default.get_title env.params.url["tid"])
|
title = (Library.default.get_title env.params.url["tid"])
|
||||||
@@ -408,9 +312,9 @@ struct APIRouter
|
|||||||
Koa.describe "Returns a MangaDex manga identified by `id`", <<-MD
|
Koa.describe "Returns a MangaDex manga identified by `id`", <<-MD
|
||||||
On error, returns a JSON that contains the error message in the `error` field.
|
On error, returns a JSON that contains the error message in the `error` field.
|
||||||
MD
|
MD
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "mangadex"]
|
||||||
Koa.path "id", desc: "A MangaDex manga ID"
|
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|
|
get "/api/admin/mangadex/manga/:id" do |env|
|
||||||
begin
|
begin
|
||||||
id = env.params.url["id"]
|
id = env.params.url["id"]
|
||||||
@@ -425,12 +329,17 @@ struct APIRouter
|
|||||||
Koa.describe "Adds a list of MangaDex chapters to the download queue", <<-MD
|
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.
|
On error, returns a JSON that contains the error message in the `error` field.
|
||||||
MD
|
MD
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "mangadex", "downloader"]
|
||||||
Koa.body ref: "$chaptersObj"
|
Koa.body schema: {
|
||||||
Koa.response 200, ref: "$successFailCount"
|
"chapters" => ["mdChapter"],
|
||||||
|
}
|
||||||
|
Koa.response 200, schema: {
|
||||||
|
"success" => Int32,
|
||||||
|
"fail" => Int32,
|
||||||
|
}
|
||||||
post "/api/admin/mangadex/download" do |env|
|
post "/api/admin/mangadex/download" do |env|
|
||||||
begin
|
begin
|
||||||
chapters = env.params.json["chapters"].as(Array).map { |c| c.as_h }
|
chapters = env.params.json["chapters"].as(Array).map &.as_h
|
||||||
jobs = chapters.map { |chapter|
|
jobs = chapters.map { |chapter|
|
||||||
Queue::Job.new(
|
Queue::Job.new(
|
||||||
chapter["id"].as_i64.to_s,
|
chapter["id"].as_i64.to_s,
|
||||||
@@ -457,7 +366,7 @@ struct APIRouter
|
|||||||
interval = (interval_raw.to_i? if interval_raw) || 5
|
interval = (interval_raw.to_i? if interval_raw) || 5
|
||||||
loop do
|
loop do
|
||||||
socket.send({
|
socket.send({
|
||||||
"jobs" => Queue.default.get_all,
|
"jobs" => Queue.default.get_all.reverse,
|
||||||
"paused" => Queue.default.paused?,
|
"paused" => Queue.default.paused?,
|
||||||
}.to_json)
|
}.to_json)
|
||||||
sleep interval.seconds
|
sleep interval.seconds
|
||||||
@@ -467,17 +376,27 @@ struct APIRouter
|
|||||||
Koa.describe "Returns the current download queue", <<-MD
|
Koa.describe "Returns the current download queue", <<-MD
|
||||||
On error, returns a JSON that contains the error message in the `error` field.
|
On error, returns a JSON that contains the error message in the `error` field.
|
||||||
MD
|
MD
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "downloader"]
|
||||||
Koa.response 200, ref: "$jobs"
|
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|
|
get "/api/admin/mangadex/queue" do |env|
|
||||||
begin
|
begin
|
||||||
jobs = Queue.default.get_all
|
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"jobs" => jobs,
|
"jobs" => Queue.default.get_all.reverse,
|
||||||
"paused" => Queue.default.paused?,
|
"paused" => Queue.default.paused?,
|
||||||
"success" => true,
|
"success" => true,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -494,10 +413,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.
|
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
|
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.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.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|
|
post "/api/admin/mangadex/queue/:action" do |env|
|
||||||
begin
|
begin
|
||||||
action = env.params.url["action"]
|
action = env.params.url["action"]
|
||||||
@@ -525,6 +444,7 @@ struct APIRouter
|
|||||||
|
|
||||||
send_json env, {"success" => true}.to_json
|
send_json env, {"success" => true}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -546,8 +466,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.
|
When `eid` is omitted, the new cover image will be applied to the title. Otherwise, applies the image to the specified entry.
|
||||||
MD
|
MD
|
||||||
Koa.tag "admin"
|
Koa.tag "admin"
|
||||||
Koa.body type: "multipart/form-data", ref: "$binaryUpload"
|
Koa.body media_type: "multipart/form-data", schema: {
|
||||||
Koa.response 200, ref: "$result"
|
"file" => Bytes,
|
||||||
|
}
|
||||||
|
Koa.response 200, schema: "result"
|
||||||
post "/api/admin/upload/:target" do |env|
|
post "/api/admin/upload/:target" do |env|
|
||||||
begin
|
begin
|
||||||
target = env.params.url["target"]
|
target = env.params.url["target"]
|
||||||
@@ -595,6 +517,7 @@ struct APIRouter
|
|||||||
|
|
||||||
raise "No part with name `file` found"
|
raise "No part with name `file` found"
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -603,9 +526,18 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Lists the chapters in a title from a plugin"
|
Koa.describe "Lists the chapters in a title from a plugin"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "downloader"]
|
||||||
Koa.body ref: "$pluginListBody"
|
Koa.query "plugin", schema: String
|
||||||
Koa.response 200, ref: "$pluginList"
|
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|
|
get "/api/admin/plugin/list" do |env|
|
||||||
begin
|
begin
|
||||||
query = env.params.query["query"].as String
|
query = env.params.query["query"].as String
|
||||||
@@ -621,6 +553,7 @@ struct APIRouter
|
|||||||
"title" => title,
|
"title" => title,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -629,9 +562,19 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Adds a list of chapters from a plugin to the download queue"
|
Koa.describe "Adds a list of chapters from a plugin to the download queue"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "downloader"]
|
||||||
Koa.body ref: "$pluginDownload"
|
Koa.body schema: {
|
||||||
Koa.response 200, ref: "$successFailCount"
|
"plugin" => String,
|
||||||
|
"title" => String,
|
||||||
|
"chapters" => [{
|
||||||
|
"id" => String,
|
||||||
|
"title" => String,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
Koa.response 200, schema: {
|
||||||
|
"success" => Int32,
|
||||||
|
"fail" => Int32,
|
||||||
|
}
|
||||||
post "/api/admin/plugin/download" do |env|
|
post "/api/admin/plugin/download" do |env|
|
||||||
begin
|
begin
|
||||||
plugin = Plugin.new env.params.json["plugin"].as String
|
plugin = Plugin.new env.params.json["plugin"].as String
|
||||||
@@ -654,6 +597,7 @@ struct APIRouter
|
|||||||
"fail": jobs.size - inserted_count,
|
"fail": jobs.size - inserted_count,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -664,7 +608,15 @@ struct APIRouter
|
|||||||
Koa.describe "Returns the image dimensions of all pages in an entry"
|
Koa.describe "Returns the image dimensions of all pages in an entry"
|
||||||
Koa.path "tid", desc: "A title ID"
|
Koa.path "tid", desc: "A title ID"
|
||||||
Koa.path "eid", desc: "An entry 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,
|
||||||
|
}],
|
||||||
|
}
|
||||||
get "/api/dimensions/:tid/:eid" do |env|
|
get "/api/dimensions/:tid/:eid" do |env|
|
||||||
begin
|
begin
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
@@ -679,9 +631,9 @@ struct APIRouter
|
|||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => true,
|
"success" => true,
|
||||||
"dimensions" => sizes,
|
"dimensions" => sizes,
|
||||||
"margin" => Config.current.page_margin,
|
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -692,8 +644,9 @@ struct APIRouter
|
|||||||
Koa.describe "Downloads an entry"
|
Koa.describe "Downloads an entry"
|
||||||
Koa.path "tid", desc: "A title ID"
|
Koa.path "tid", desc: "A title ID"
|
||||||
Koa.path "eid", desc: "An entry 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.response 404, "Entry not found"
|
||||||
|
Koa.tags ["library", "reader"]
|
||||||
get "/api/download/:tid/:eid" do |env|
|
get "/api/download/:tid/:eid" do |env|
|
||||||
begin
|
begin
|
||||||
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
||||||
@@ -708,7 +661,12 @@ struct APIRouter
|
|||||||
|
|
||||||
Koa.describe "Gets the tags of a title"
|
Koa.describe "Gets the tags of a title"
|
||||||
Koa.path "tid", desc: "A title ID"
|
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|
|
get "/api/tags/:tid" do |env|
|
||||||
begin
|
begin
|
||||||
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
||||||
@@ -728,7 +686,12 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Returns all tags"
|
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|
|
get "/api/tags" do |env|
|
||||||
begin
|
begin
|
||||||
tags = Storage.default.list_tags
|
tags = Storage.default.list_tags
|
||||||
@@ -747,8 +710,8 @@ struct APIRouter
|
|||||||
|
|
||||||
Koa.describe "Adds a new tag to a title"
|
Koa.describe "Adds a new tag to a title"
|
||||||
Koa.path "tid", desc: "A title ID"
|
Koa.path "tid", desc: "A title ID"
|
||||||
Koa.response 200, ref: "$result"
|
Koa.response 200, schema: "result"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "library", "tags"]
|
||||||
put "/api/admin/tags/:tid/:tag" do |env|
|
put "/api/admin/tags/:tid/:tag" do |env|
|
||||||
begin
|
begin
|
||||||
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
||||||
@@ -770,8 +733,8 @@ struct APIRouter
|
|||||||
|
|
||||||
Koa.describe "Deletes a tag from a title"
|
Koa.describe "Deletes a tag from a title"
|
||||||
Koa.path "tid", desc: "A title ID"
|
Koa.path "tid", desc: "A title ID"
|
||||||
Koa.response 200, ref: "$result"
|
Koa.response 200, schema: "result"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "library", "tags"]
|
||||||
delete "/api/admin/tags/:tid/:tag" do |env|
|
delete "/api/admin/tags/:tid/:tag" do |env|
|
||||||
begin
|
begin
|
||||||
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
title = (Library.default.get_title env.params.url["tid"]).not_nil!
|
||||||
@@ -792,8 +755,16 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Lists all missing titles"
|
Koa.describe "Lists all missing titles"
|
||||||
Koa.response 200, ref: "$missingResult"
|
Koa.response 200, schema: {
|
||||||
Koa.tag "admin"
|
"success" => Bool,
|
||||||
|
"error" => String?,
|
||||||
|
"titles?" => [{
|
||||||
|
"path" => String,
|
||||||
|
"id" => String,
|
||||||
|
"signature" => String,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
Koa.tags ["admin", "library"]
|
||||||
get "/api/admin/titles/missing" do |env|
|
get "/api/admin/titles/missing" do |env|
|
||||||
begin
|
begin
|
||||||
send_json env, {
|
send_json env, {
|
||||||
@@ -802,6 +773,7 @@ struct APIRouter
|
|||||||
"titles" => Storage.default.missing_titles,
|
"titles" => Storage.default.missing_titles,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -810,8 +782,16 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Lists all missing entries"
|
Koa.describe "Lists all missing entries"
|
||||||
Koa.response 200, ref: "$missingResult"
|
Koa.response 200, schema: {
|
||||||
Koa.tag "admin"
|
"success" => Bool,
|
||||||
|
"error" => String?,
|
||||||
|
"entries?" => [{
|
||||||
|
"path" => String,
|
||||||
|
"id" => String,
|
||||||
|
"signature" => String,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
Koa.tags ["admin", "library"]
|
||||||
get "/api/admin/entries/missing" do |env|
|
get "/api/admin/entries/missing" do |env|
|
||||||
begin
|
begin
|
||||||
send_json env, {
|
send_json env, {
|
||||||
@@ -820,6 +800,7 @@ struct APIRouter
|
|||||||
"entries" => Storage.default.missing_entries,
|
"entries" => Storage.default.missing_entries,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -828,8 +809,8 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Deletes all missing titles"
|
Koa.describe "Deletes all missing titles"
|
||||||
Koa.response 200, ref: "$result"
|
Koa.response 200, schema: "result"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "library"]
|
||||||
delete "/api/admin/titles/missing" do |env|
|
delete "/api/admin/titles/missing" do |env|
|
||||||
begin
|
begin
|
||||||
Storage.default.delete_missing_title
|
Storage.default.delete_missing_title
|
||||||
@@ -838,6 +819,7 @@ struct APIRouter
|
|||||||
"error" => nil,
|
"error" => nil,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -846,8 +828,8 @@ struct APIRouter
|
|||||||
end
|
end
|
||||||
|
|
||||||
Koa.describe "Deletes all missing entries"
|
Koa.describe "Deletes all missing entries"
|
||||||
Koa.response 200, ref: "$result"
|
Koa.response 200, schema: "result"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "library"]
|
||||||
delete "/api/admin/entries/missing" do |env|
|
delete "/api/admin/entries/missing" do |env|
|
||||||
begin
|
begin
|
||||||
Storage.default.delete_missing_entry
|
Storage.default.delete_missing_entry
|
||||||
@@ -856,6 +838,7 @@ struct APIRouter
|
|||||||
"error" => nil,
|
"error" => nil,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -866,8 +849,8 @@ struct APIRouter
|
|||||||
Koa.describe "Deletes a missing title identified by `tid`", <<-MD
|
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.
|
Does nothing if the given `tid` is not found or if the title is not missing.
|
||||||
MD
|
MD
|
||||||
Koa.response 200, ref: "$result"
|
Koa.response 200, schema: "result"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "library"]
|
||||||
delete "/api/admin/titles/missing/:tid" do |env|
|
delete "/api/admin/titles/missing/:tid" do |env|
|
||||||
begin
|
begin
|
||||||
tid = env.params.url["tid"]
|
tid = env.params.url["tid"]
|
||||||
@@ -877,6 +860,7 @@ struct APIRouter
|
|||||||
"error" => nil,
|
"error" => nil,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
@@ -887,8 +871,8 @@ struct APIRouter
|
|||||||
Koa.describe "Deletes a missing entry identified by `eid`", <<-MD
|
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.
|
Does nothing if the given `eid` is not found or if the entry is not missing.
|
||||||
MD
|
MD
|
||||||
Koa.response 200, ref: "$result"
|
Koa.response 200, schema: "result"
|
||||||
Koa.tag "admin"
|
Koa.tags ["admin", "library"]
|
||||||
delete "/api/admin/entries/missing/:eid" do |env|
|
delete "/api/admin/entries/missing/:eid" do |env|
|
||||||
begin
|
begin
|
||||||
eid = env.params.url["eid"]
|
eid = env.params.url["eid"]
|
||||||
@@ -898,6 +882,240 @@ struct APIRouter
|
|||||||
"error" => nil,
|
"error" => nil,
|
||||||
}.to_json
|
}.to_json
|
||||||
rescue e
|
rescue e
|
||||||
|
Logger.error e
|
||||||
|
send_json env, {
|
||||||
|
"success" => false,
|
||||||
|
"error" => e.message,
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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
|
||||||
|
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 (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
|
||||||
|
_, 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
|
||||||
|
|
||||||
|
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
|
||||||
|
query = env.params.query["query"]
|
||||||
|
|
||||||
|
send_json env, {
|
||||||
|
"success" => true,
|
||||||
|
"error" => nil,
|
||||||
|
"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
|
||||||
|
|
||||||
|
Koa.describe "Lists all MangaDex subscriptions"
|
||||||
|
Koa.response 200, schema: {
|
||||||
|
"success" => Bool,
|
||||||
|
"error" => String?,
|
||||||
|
"subscriptions?" => [{
|
||||||
|
"id" => Int64,
|
||||||
|
"username" => String,
|
||||||
|
"manga_id" => Int64,
|
||||||
|
"language" => String?,
|
||||||
|
"group_id" => Int64?,
|
||||||
|
"min_volume" => Int64?,
|
||||||
|
"max_volume" => Int64?,
|
||||||
|
"min_chapter" => Int64?,
|
||||||
|
"max_chapter" => Int64?,
|
||||||
|
"last_checked" => Int64,
|
||||||
|
"created_at" => Int64,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
Koa.tags ["admin", "mangadex", "subscriptions"]
|
||||||
|
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
|
||||||
|
|
||||||
|
Koa.describe "Creates a new MangaDex subscription"
|
||||||
|
Koa.body schema: {
|
||||||
|
"subscription" => {
|
||||||
|
"manga" => Int64,
|
||||||
|
"language" => String?,
|
||||||
|
"groupId" => Int64?,
|
||||||
|
"volumeMin" => Int64?,
|
||||||
|
"volumeMax" => Int64?,
|
||||||
|
"chapterMin" => Int64?,
|
||||||
|
"chapterMax" => Int64?,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Koa.response 200, schema: {
|
||||||
|
"success" => Bool,
|
||||||
|
"error" => String?,
|
||||||
|
}
|
||||||
|
Koa.tags ["admin", "mangadex", "subscriptions"]
|
||||||
|
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
|
||||||
|
|
||||||
|
Koa.describe "Deletes a MangaDex subscription identified by `id`", <<-MD
|
||||||
|
Does nothing if the subscription was not created by the current user.
|
||||||
|
MD
|
||||||
|
Koa.response 200, schema: {
|
||||||
|
"success" => Bool,
|
||||||
|
"error" => String?,
|
||||||
|
}
|
||||||
|
Koa.tags ["admin", "mangadex", "subscriptions"]
|
||||||
|
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
|
||||||
|
|
||||||
|
Koa.describe "Triggers an update for a MangaDex subscription identified by `id`", <<-MD
|
||||||
|
Does nothing if the subscription was not created by the current user.
|
||||||
|
MD
|
||||||
|
Koa.response 200, schema: {
|
||||||
|
"success" => Bool,
|
||||||
|
"error" => String?,
|
||||||
|
}
|
||||||
|
Koa.tags ["admin", "mangadex", "subscriptions"]
|
||||||
|
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
|
||||||
|
rescue e
|
||||||
|
Logger.error e
|
||||||
send_json env, {
|
send_json env, {
|
||||||
"success" => false,
|
"success" => false,
|
||||||
"error" => e.message,
|
"error" => e.message,
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -103,7 +109,7 @@ struct MainRouter
|
|||||||
recently_added = Library.default.get_recently_added_entries username
|
recently_added = Library.default.get_recently_added_entries username
|
||||||
start_reading = Library.default.get_start_reading_titles username
|
start_reading = Library.default.get_start_reading_titles username
|
||||||
titles = Library.default.titles
|
titles = Library.default.titles
|
||||||
new_user = !titles.any? { |t| t.load_percentage(username) > 0 }
|
new_user = !titles.any? &.load_percentage(username).> 0
|
||||||
empty_library = titles.size == 0
|
empty_library = titles.size == 0
|
||||||
layout "home"
|
layout "home"
|
||||||
rescue e
|
rescue e
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ struct ReaderRouter
|
|||||||
|
|
||||||
title = (Library.default.get_title env.params.url["title"]).not_nil!
|
title = (Library.default.get_title env.params.url["title"]).not_nil!
|
||||||
entry = (title.get_entry env.params.url["entry"]).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
|
page_idx = env.params.url["page"].to_i
|
||||||
if page_idx > entry.pages || page_idx <= 0
|
if page_idx > entry.pages || page_idx <= 0
|
||||||
raise "Page #{page_idx} not found."
|
raise "Page #{page_idx} not found."
|
||||||
@@ -37,10 +42,12 @@ struct ReaderRouter
|
|||||||
|
|
||||||
exit_url = "#{base_url}book/#{title.id}"
|
exit_url = "#{base_url}book/#{title.id}"
|
||||||
|
|
||||||
next_entry_url = nil
|
next_entry_url = entry.next_entry(username).try do |e|
|
||||||
next_entry = entry.next_entry username
|
"#{base_url}reader/#{title.id}/#{e.id}"
|
||||||
unless next_entry.nil?
|
end
|
||||||
next_entry_url = "#{base_url}reader/#{title.id}/#{next_entry.id}"
|
|
||||||
|
previous_entry_url = entry.previous_entry(username).try do |e|
|
||||||
|
"#{base_url}reader/#{title.id}/#{e.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
render "src/views/reader.html.ecr"
|
render "src/views/reader.html.ecr"
|
||||||
|
|||||||
105
src/storage.cr
105
src/storage.cr
@@ -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
|
||||||
@@ -34,7 +38,7 @@ class Storage
|
|||||||
dir = File.dirname @path
|
dir = File.dirname @path
|
||||||
unless Dir.exists? dir
|
unless Dir.exists? dir
|
||||||
Logger.info "The DB directory #{dir} does not exist. " \
|
Logger.info "The DB directory #{dir} does not exist. " \
|
||||||
"Attepmting to create it"
|
"Attempting to create it"
|
||||||
Dir.mkdir_p dir
|
Dir.mkdir_p dir
|
||||||
end
|
end
|
||||||
MainFiber.run do
|
MainFiber.run do
|
||||||
@@ -445,7 +449,7 @@ class Storage
|
|||||||
Logger.debug "Marking #{trash_ids.size} entries as unavailable"
|
Logger.debug "Marking #{trash_ids.size} entries as unavailable"
|
||||||
end
|
end
|
||||||
db.exec "update ids set unavailable = 1 where id in " \
|
db.exec "update ids set unavailable = 1 where id in " \
|
||||||
"(#{trash_ids.map { |i| "'#{i}'" }.join ","})"
|
"(#{trash_ids.join "," { |i| "'#{i}'" }})"
|
||||||
|
|
||||||
# Detect dangling title IDs
|
# Detect dangling title IDs
|
||||||
trash_titles = [] of String
|
trash_titles = [] of String
|
||||||
@@ -461,7 +465,7 @@ class Storage
|
|||||||
Logger.debug "Marking #{trash_titles.size} titles as unavailable"
|
Logger.debug "Marking #{trash_titles.size} titles as unavailable"
|
||||||
end
|
end
|
||||||
db.exec "update titles set unavailable = 1 where id in " \
|
db.exec "update titles set unavailable = 1 where id in " \
|
||||||
"(#{trash_titles.map { |i| "'#{i}'" }.join ","})"
|
"(#{trash_titles.join "," { |i| "'#{i}'" }})"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -514,6 +518,101 @@ class Storage
|
|||||||
delete_missing "titles", id
|
delete_missing "titles", id
|
||||||
end
|
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 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
|
||||||
|
|
||||||
|
private 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
|
||||||
@@ -73,7 +73,7 @@ class ChapterSorter
|
|||||||
.select do |key|
|
.select do |key|
|
||||||
keys[key].count >= str_ary.size / 2
|
keys[key].count >= str_ary.size / 2
|
||||||
end
|
end
|
||||||
.sort do |a_key, b_key|
|
.sort! do |a_key, b_key|
|
||||||
a = keys[a_key]
|
a = keys[a_key]
|
||||||
b = keys[b_key]
|
b = keys[b_key]
|
||||||
# Sort keys by the number of times they appear
|
# Sort keys by the number of times they appear
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ end
|
|||||||
def split_by_alphanumeric(str)
|
def split_by_alphanumeric(str)
|
||||||
arr = [] of String
|
arr = [] of String
|
||||||
str.scan(/([^\d\n\r]*)(\d*)([^\d\n\r]*)/) do |match|
|
str.scan(/([^\d\n\r]*)(\d*)([^\d\n\r]*)/) do |match|
|
||||||
arr += match.captures.select { |s| s != "" }
|
arr += match.captures.select &.!= ""
|
||||||
end
|
end
|
||||||
arr
|
arr
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ class String
|
|||||||
def components_similarity(other : String) : Float64
|
def components_similarity(other : String) : Float64
|
||||||
s, l = [self, other]
|
s, l = [self, other]
|
||||||
.map { |str| Path.new(str).parts }
|
.map { |str| Path.new(str).parts }
|
||||||
.sort_by &.size
|
.sort_by! &.size
|
||||||
|
|
||||||
match = s.reverse.zip(l.reverse).count { |a, b| a == b }
|
match = s.reverse.zip(l.reverse).count { |a, b| a == b }
|
||||||
match / s.size
|
match / s.size
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ def redirect(env, path)
|
|||||||
end
|
end
|
||||||
|
|
||||||
def hash_to_query(hash)
|
def hash_to_query(hash)
|
||||||
hash.map { |k, v| "#{k}=#{v}" }.join("&")
|
hash.join "&" { |k, v| "#{k}=#{v}" }
|
||||||
end
|
end
|
||||||
|
|
||||||
def request_path_startswith(env, ary)
|
def request_path_startswith(env, ary)
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -33,6 +33,7 @@
|
|||||||
<option>System</option>
|
<option>System</option>
|
||||||
</select>
|
</select>
|
||||||
</li>
|
</li>
|
||||||
|
<li><a class="uk-link-reset" href="<%= base_url %>admin/mangadex">Connect to MangaDex</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<hr class="uk-divider-icon">
|
<hr class="uk-divider-icon">
|
||||||
|
|||||||
1
src/views/components/jquery-ui.html.ecr
Normal file
1
src/views/components/jquery-ui.html.ecr
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
||||||
1
src/views/components/moment.html.ecr
Normal file
1
src/views/components/moment.html.ecr
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
||||||
@@ -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>
|
||||||
@@ -51,9 +52,9 @@
|
|||||||
<td x-text="`${job.plugin_id || ''}`"></td>
|
<td x-text="`${job.plugin_id || ''}`"></td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<a @click="jobAction('delete', $event)" uk-icon="trash"></a>
|
<a @click="jobAction('delete', $event)" uk-icon="trash" uk-tooltip="Delete"></a>
|
||||||
<template x-if="job.status_message.length > 0">
|
<template x-if="job.status_message.length > 0">
|
||||||
<a @click="jobAction('retry', $event)" uk-icon="refresh"></a>
|
<a @click="jobAction('retry', $event)" uk-icon="refresh" uk-tooltip="Retry"></a>
|
||||||
</template>
|
</template>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -61,9 +62,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
<%= render_component "moment" %>
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/download-manager.js"></script>
|
<script src="<%= base_url %>js/download-manager.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -1,17 +1,39 @@
|
|||||||
<h2 class=uk-title>Download from MangaDex</h2>
|
<h2 class=uk-title>Download from MangaDex</h2>
|
||||||
<div x-data="downloadComponent()" x-init="init()">
|
<div x-data="downloadComponent()" x-init="init()">
|
||||||
<div class="uk-grid-small" uk-grid>
|
<div class="uk-grid-small" uk-grid style="margin-bottom:40px;">
|
||||||
<div class="uk-width-3-4">
|
<div class="uk-width-expand">
|
||||||
<input class="uk-input" type="text" placeholder="MangaDex manga ID or URL" x-model="searchInput" @keydown.enter.debounce="search()">
|
<input class="uk-input" type="text" :placeholder="searchAvailable ? 'Search MangaDex or enter a manga ID/URL' : 'MangaDex manga ID or URL'" x-model="searchInput" @keydown.enter.debounce="search()">
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-width-1-4">
|
<div class="uk-width-auto">
|
||||||
<div uk-spinner class="uk-align-center" x-show="loading" x-cloak></div>
|
<div uk-spinner class="uk-align-center" x-show="loading" x-cloak></div>
|
||||||
<button class="uk-button uk-button-default" x-show="!loading" @click="search()">Search</button>
|
<button class="uk-button uk-button-default" x-show="!loading" @click="search()">Search</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<template x-if="mangaAry">
|
||||||
|
<div>
|
||||||
|
<p x-show="mangaAry.length === 0">No matching manga found.</p>
|
||||||
|
|
||||||
|
<div class="uk-child-width-1-4@m uk-child-width-1-2" uk-grid>
|
||||||
|
<template x-for="manga in mangaAry" :key="manga.id">
|
||||||
|
<div class="item" :data-id="manga.id" @click="chooseManga(manga)">
|
||||||
|
<div class="uk-card uk-card-default">
|
||||||
|
<div class="uk-card-media-top uk-inline">
|
||||||
|
<img uk-img :data-src="manga.mainCover">
|
||||||
|
</div>
|
||||||
|
<div class="uk-card-body">
|
||||||
|
<h3 class="uk-card-title break-word uk-margin-remove-bottom free-height" x-text="manga.title"></h3>
|
||||||
|
<p class="uk-text-meta" x-text="`ID: ${manga.id}`"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<div x-show="data && data.chapters" x-cloak>
|
<div x-show="data && data.chapters" x-cloak>
|
||||||
<div class"uk-grid-small" uk-grid style="margin-top:40px">
|
<div class"uk-grid-small" uk-grid>
|
||||||
<div class="uk-width-1-4@s">
|
<div class="uk-width-1-4@s">
|
||||||
<img :src="data.mainCover">
|
<img :src="data.mainCover">
|
||||||
</div>
|
</div>
|
||||||
@@ -21,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>
|
||||||
@@ -71,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>
|
||||||
@@ -109,9 +135,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="modal" class="uk-flex-top" uk-modal="container: false">
|
||||||
|
<div class="uk-modal-dialog uk-margin-auto-vertical">
|
||||||
|
<button class="uk-modal-close-default" type="button" uk-close></button>
|
||||||
|
<div class="uk-modal-header">
|
||||||
|
<h3 class="uk-modal-title break-word" x-text="candidateManga.title"></h3>
|
||||||
|
</div>
|
||||||
|
<div class="uk-modal-body">
|
||||||
|
<div class="uk-grid">
|
||||||
|
<div class="uk-width-1-3@s">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="uk-width-2-3@s">
|
||||||
|
<p x-text="candidateManga.description"></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-modal-footer">
|
||||||
|
<button class="uk-button uk-button-primary" type="button" @click="confirmManga(candidateManga.id)">Choose</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js"></script>
|
<%= render_component "moment" %>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
<%= 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 %>
|
||||||
|
|||||||
@@ -77,7 +77,7 @@
|
|||||||
<%- end -%>
|
<%- end -%>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<%= render_component "dots-scripts" %>
|
<%= render_component "dots" %>
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/title.js"></script>
|
<script src="<%= base_url %>js/title.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>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<%= render_component "dots-scripts" %>
|
<%= render_component "dots" %>
|
||||||
<script src="<%= base_url %>js/search.js"></script>
|
<script src="<%= base_url %>js/search.js"></script>
|
||||||
<script src="<%= base_url %>js/sort-items.js"></script>
|
<script src="<%= base_url %>js/sort-items.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
39
src/views/mangadex.html.ecr
Normal file
39
src/views/mangadex.html.ecr
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<div x-data="component()" x-init="init()">
|
||||||
|
<h2 class="uk-title">Connect to MangaDex</h2>
|
||||||
|
<div class"uk-grid-small" uk-grid x-show="!loading" x-cloak>
|
||||||
|
<div class="uk-width-1-2@s" x-show="!expires">
|
||||||
|
<p>This step is optional but highly recommended if you are using the MangaDex downloader. Connecting to MangaDex allows you to:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Search MangaDex by search terms in addition to manga IDs</li>
|
||||||
|
<li>Automatically download new chapters when they are available (coming soon)</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uk-width-1-2@s" x-show="expires">
|
||||||
|
<p>
|
||||||
|
<span x-show="!expired">You have logged in to MangaDex!</span>
|
||||||
|
<span x-show="expired">You have logged in to MangaDex but the token has expired.</span>
|
||||||
|
The expiration date of your token is <code x-text="moment.unix(expires).format('MMMM Do YYYY, HH:mm:ss')"></code>.
|
||||||
|
<span x-show="!expired">If the integration is not working, you</span>
|
||||||
|
<span x-show="expired">You</span>
|
||||||
|
can log in again and the token will be updated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uk-width-1-2@s">
|
||||||
|
<div class="uk-margin">
|
||||||
|
<div class="uk-inline uk-width-1-1"><span class="uk-form-icon" uk-icon="icon:user"></span><input class="uk-input uk-form-large" type="text" x-model="username" @keydown.enter.debounce="login()"></div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-margin">
|
||||||
|
<div class="uk-inline uk-width-1-1"><span class="uk-form-icon" uk-icon="icon:lock"></span><input class="uk-input uk-form-large" type="password" x-model="password" @keydown.enter.debounce="login()"></div>
|
||||||
|
</div>
|
||||||
|
<div class="uk-margin"><button class="uk-button uk-button-primary uk-button-large uk-width-1-1" @click="login()" :disabled="loggingIn">Login to MangaDex</button></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<% content_for "script" do %>
|
||||||
|
<%= render_component "moment" %>
|
||||||
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
|
<script src="<%= base_url %>js/mangadex.js"></script>
|
||||||
|
<% end %>
|
||||||
@@ -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 %>
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +70,7 @@
|
|||||||
var pid = "<%= plugin.not_nil!.info.id %>";
|
var pid = "<%= plugin.not_nil!.info.id %>";
|
||||||
</script>
|
</script>
|
||||||
<% end %>
|
<% end %>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js"></script>
|
<%= render_component "jquery-ui" %>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/jquery.tablesorter.combined.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.tablesorter/2.31.3/js/jquery.tablesorter.combined.min.js"></script>
|
||||||
<script src="<%= base_url %>js/alert.js"></script>
|
<script src="<%= base_url %>js/alert.js"></script>
|
||||||
<script src="<%= base_url %>js/plugin-download.js"></script>
|
<script src="<%= base_url %>js/plugin-download.js"></script>
|
||||||
|
|||||||
@@ -25,18 +25,18 @@
|
|||||||
<img
|
<img
|
||||||
uk-img
|
uk-img
|
||||||
:class="{'uk-align-center': true, 'spine': item.width < 50}"
|
:class="{'uk-align-center': true, 'spine': item.width < 50}"
|
||||||
:style="item.style"
|
|
||||||
:data-src="item.url"
|
:data-src="item.url"
|
||||||
:width="item.width"
|
:width="item.width"
|
||||||
:height="item.height"
|
:height="item.height"
|
||||||
:id="item.id"
|
:id="item.id"
|
||||||
|
:style="`margin-top:${margin}px; margin-bottom:${margin}px`"
|
||||||
@click="showControl($event)"
|
@click="showControl($event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<%- if next_entry_url -%>
|
<%- if next_entry_url -%>
|
||||||
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="nextEntry('<%= next_entry_url %>')">Next Entry</button>
|
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="nextEntry('<%= next_entry_url %>')">Next Entry</button>
|
||||||
<%- else -%>
|
<%- else -%>
|
||||||
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="exitReader('<%= exit_url %>', true)">Exit Reader</button>
|
<button id="next-btn" class="uk-align-center uk-button uk-button-primary" @click="exitReader('<%= exit_url %>')">Exit Reader</button>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -68,18 +68,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="uk-modal-body">
|
<div class="uk-modal-body">
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<p id="progress-label"></p>
|
<p x-text="`Progress: ${selectedIndex}/${items.length} (${(selectedIndex/items.length * 100).toFixed(1)}%)`"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="page-select">Jump to page</label>
|
<label class="uk-form-label" for="page-select">Jump to Page</label>
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
<select id="page-select" class="uk-select" @change="pageChanged()">
|
<select id="page-select" class="uk-select" @change="pageChanged()" x-model="selectedIndex">
|
||||||
<%- (1..entry.pages).each do |p| -%>
|
<%- (1..entry.pages).each do |p| -%>
|
||||||
<option value="<%= p %>"><%= p %></option>
|
<option value="<%= p %>"><%= p %></option>
|
||||||
<%- end -%>
|
<%- end -%>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="uk-margin">
|
<div class="uk-margin">
|
||||||
<label class="uk-form-label" for="mode-select">Mode</label>
|
<label class="uk-form-label" for="mode-select">Mode</label>
|
||||||
<div class="uk-form-controls">
|
<div class="uk-form-controls">
|
||||||
@@ -89,9 +90,40 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="uk-margin" x-show="mode === 'continuous'">
|
||||||
|
<label class="uk-form-label" for="margin-range" x-text="`Page Margin: ${margin}px`"></label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<input id="margin-range" class="uk-range" type="range" min="0" max="50" step="5" x-model="margin" @change="marginChanged()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="uk-divider-icon">
|
||||||
|
|
||||||
|
<div class="uk-margin">
|
||||||
|
<label class="uk-form-label" for="entry-select">Jump to Entry</label>
|
||||||
|
<div class="uk-form-controls">
|
||||||
|
<select id="entry-select" class="uk-select" @change="entryChanged()">
|
||||||
|
<% entries.each do |e| %>
|
||||||
|
<option value="<%= e.id %>"
|
||||||
|
<% if e.id == entry.id %>
|
||||||
|
selected
|
||||||
|
<% end %>>
|
||||||
|
<%= e.title %>
|
||||||
|
</option>
|
||||||
|
<% end %>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="uk-modal-footer uk-text-right">
|
<div class="uk-modal-footer uk-text-right">
|
||||||
<button class="uk-button uk-button-danger" type="button" @click="exitReader('<%= exit_url %>')">Exit Reader</button>
|
<% if previous_entry_url %>
|
||||||
|
<a class="uk-button uk-button-default uk-margin-small-bottom uk-margin-small-right" href="<%= previous_entry_url %>">Previous Entry</a>
|
||||||
|
<% end %>
|
||||||
|
<% if next_entry_url %>
|
||||||
|
<a class="uk-button uk-button-default uk-margin-small-bottom uk-margin-small-right" href="<%= next_entry_url %>">Next Entry</a>
|
||||||
|
<% end %>
|
||||||
|
<a class="uk-button uk-button-danger uk-margin-small-bottom uk-margin-small-right" href="<%= exit_url %>">Exit Reader</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
54
src/views/subscription.html.ecr
Normal file
54
src/views/subscription.html.ecr
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<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>Volume Range</th>
|
||||||
|
<th>Chapter Range</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="formatRange(sub.min_volume, sub.max_volume)"></td>
|
||||||
|
<td x-text="formatRange(sub.min_chapter, 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 %>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<%= render_component "dots-scripts" %>
|
<%= render_component "dots" %>
|
||||||
<script src="<%= base_url %>js/search.js"></script>
|
<script src="<%= base_url %>js/search.js"></script>
|
||||||
<script src="<%= base_url %>js/sort-items.js"></script>
|
<script src="<%= base_url %>js/sort-items.js"></script>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<% content_for "script" do %>
|
<% content_for "script" do %>
|
||||||
<%= render_component "dots-scripts" %>
|
<%= render_component "dots" %>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet" />
|
<link href="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/css/select2.min.css" rel="stylesheet" />
|
||||||
<link href="<%= base_url %>css/tags.css" rel="stylesheet" />
|
<link href="<%= base_url %>css/tags.css" rel="stylesheet" />
|
||||||
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/select2@4.1.0-beta.1/dist/js/select2.min.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user