Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90c3faadcf | ||
|
|
e594206759 | ||
|
|
80b21371a4 | ||
|
|
af575a87a2 | ||
|
|
a6aa8d8e95 | ||
|
|
aa436feae7 | ||
|
|
abce84999f | ||
|
|
74bb3f2362 | ||
|
|
abcbbc86bc | ||
|
|
d859202fb1 |
48
CHANGELOG.md
Normal file
48
CHANGELOG.md
Normal file
@@ -0,0 +1,48 @@
|
||||
1.3.3 / 2022-11-28
|
||||
------------------
|
||||
|
||||
- Исправление выявленных недочетов
|
||||
|
||||
1.3.2 / 2022-11-27
|
||||
------------------
|
||||
|
||||
- Изменения механизма ограничения доступа по паролю:
|
||||
- появилась возможность выхода из сессии
|
||||
- в конфиг добавлена настройка таймаута для автозавершения сессии
|
||||
- Добавлено отображение количества книг в серии в разделе "Авторы"
|
||||
|
||||
1.3.1 / 2022-11-25
|
||||
------------------
|
||||
|
||||
- Улучшена кроссплатформенность приложения
|
||||
|
||||
1.3.0 / 2022-11-24
|
||||
------------------
|
||||
|
||||
- Добавлен OPDS-сервер для inpx-коллекции
|
||||
- Произведена небольшая оптимизация поисковой БД
|
||||
- Добавлен релиз для macos, без тестирования
|
||||
|
||||
1.2.4 / 2022-11-14
|
||||
------------------
|
||||
|
||||
- Добавлена возможность посмотреть обложку в увеличении
|
||||
- Исправление выявленных недочетов
|
||||
|
||||
1.2.3 / 2022-11-12
|
||||
------------------
|
||||
|
||||
- Добавлено диалоговое окно "Информация о книге"
|
||||
- Небольшие изменения интерфейса, добавлена кнопка "Клонировать поиск"
|
||||
|
||||
1.1.4 / 2022-11-03
|
||||
------------------
|
||||
|
||||
- Исправлен баг "Не качает книги #1"
|
||||
|
||||
1.1.2 / 2022-10-31
|
||||
------------------
|
||||
|
||||
- Добавлены разделы "Серии" и "Книги"
|
||||
- Расширена форма поиска: добавлен поиск по датам поступления и оценкам
|
||||
|
||||
@@ -239,6 +239,10 @@ class Api {
|
||||
return await this.request({action: 'get-author-book-list', authorId});
|
||||
}
|
||||
|
||||
async getAuthorSeriesList(authorId) {
|
||||
return await this.request({action: 'get-author-series-list', authorId});
|
||||
}
|
||||
|
||||
async getSeriesBookList(series) {
|
||||
return await this.request({action: 'get-series-book-list', series});
|
||||
}
|
||||
@@ -261,6 +265,7 @@ class Api {
|
||||
|
||||
async logout() {
|
||||
await this.request({action: 'logout'});
|
||||
this.accessGranted = false;
|
||||
await this.request({action: 'test'});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
</div>
|
||||
|
||||
<div class="q-ml-sm text-bold" style="color: #555">
|
||||
{{ getSeriesBookCount(book) }}
|
||||
{{ getSeriesBookCount(item, book) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -188,15 +188,17 @@ class AuthorList extends BaseList {
|
||||
return `(${result})`;
|
||||
}
|
||||
|
||||
getSeriesBookCount(book) {
|
||||
getSeriesBookCount(item, book) {
|
||||
let result = '';
|
||||
if (!this.showCounts || book.type != 'series')
|
||||
return result;
|
||||
|
||||
let count = book.seriesBooks.length;
|
||||
result = `${count}`;
|
||||
if (book.allBooksLoaded) {
|
||||
result += `/${book.allBooksLoaded.length}`;
|
||||
if (item.seriesLoaded) {
|
||||
const rec = item.seriesLoaded[book.series];
|
||||
const totalCount = (this.showDeleted ? rec.bookCount + rec.bookDelCount : rec.bookCount);
|
||||
result += `/${totalCount}`;
|
||||
}
|
||||
|
||||
return `(${result})`;
|
||||
@@ -227,6 +229,19 @@ class AuthorList extends BaseList {
|
||||
}
|
||||
}
|
||||
|
||||
async getAuthorSeries(item) {
|
||||
if (item.seriesLoaded)
|
||||
return;
|
||||
|
||||
const series = await this.loadAuthorSeries(item.key);
|
||||
const loaded = {};
|
||||
for (const s of series) {
|
||||
loaded[s.series] = {bookCount: s.bookCount, bookDelCount: s.bookDelCount};
|
||||
}
|
||||
|
||||
item.seriesLoaded = loaded;
|
||||
}
|
||||
|
||||
async getAuthorBooks(item) {
|
||||
if (item.books) {
|
||||
if (item.count > this.maxItemCount) {
|
||||
@@ -328,6 +343,7 @@ class AuthorList extends BaseList {
|
||||
}
|
||||
|
||||
item.booksLoaded = books;
|
||||
this.getAuthorSeries(item);//no await
|
||||
this.showMore(item);
|
||||
|
||||
await this.$nextTick();
|
||||
@@ -360,6 +376,7 @@ class AuthorList extends BaseList {
|
||||
name: rec.author.replace(/,/g, ', '),
|
||||
count,
|
||||
booksLoaded: false,
|
||||
seriesLoaded: false,
|
||||
books: false,
|
||||
bookLoading: false,
|
||||
showMore: false,
|
||||
|
||||
@@ -253,7 +253,30 @@ export default class BaseList {
|
||||
result = await this.api.getAuthorBookList(authorId);
|
||||
}
|
||||
|
||||
return (result.books ? JSON.parse(result.books) : []);
|
||||
return result.books;
|
||||
} catch (e) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка');
|
||||
}
|
||||
}
|
||||
|
||||
async loadAuthorSeries(authorId) {
|
||||
try {
|
||||
let result;
|
||||
|
||||
if (this.abCacheEnabled) {
|
||||
const key = `author-${authorId}-series-${this.list.inpxHash}`;
|
||||
const data = await authorBooksStorage.getData(key);
|
||||
if (data) {
|
||||
result = JSON.parse(data);
|
||||
} else {
|
||||
result = await this.api.getAuthorSeriesList(authorId);
|
||||
await authorBooksStorage.setData(key, JSON.stringify(result));
|
||||
}
|
||||
} else {
|
||||
result = await this.api.getAuthorSeriesList(authorId);
|
||||
}
|
||||
|
||||
return result.series;
|
||||
} catch (e) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка');
|
||||
}
|
||||
@@ -276,7 +299,7 @@ export default class BaseList {
|
||||
result = await this.api.getSeriesBookList(series);
|
||||
}
|
||||
|
||||
return (result.books ? JSON.parse(result.books) : []);
|
||||
return result.books;
|
||||
} catch (e) {
|
||||
this.$root.stdDialog.alert(e.message, 'Ошибка');
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ const abStore = localForage.createInstance({
|
||||
name: 'authorBooksStorage'
|
||||
});
|
||||
|
||||
const storageVersion = '1';
|
||||
|
||||
class AuthorBooksStorage {
|
||||
constructor() {
|
||||
}
|
||||
@@ -17,6 +19,8 @@ class AuthorBooksStorage {
|
||||
}
|
||||
|
||||
async setData(key, data) {
|
||||
key += storageVersion;
|
||||
|
||||
if (typeof data !== 'string')
|
||||
throw new Error('AuthorBooksStorage: data must be a string');
|
||||
|
||||
@@ -25,6 +29,8 @@ class AuthorBooksStorage {
|
||||
}
|
||||
|
||||
async getData(key) {
|
||||
key += storageVersion;
|
||||
|
||||
const item = await abStore.getItem(key);
|
||||
|
||||
//обновим addTime
|
||||
@@ -34,9 +40,9 @@ class AuthorBooksStorage {
|
||||
return item;
|
||||
}
|
||||
|
||||
async removeData(key) {
|
||||
await abStore.removeItem(key);
|
||||
await abStore.removeItem(`addTime-${key}`);
|
||||
async _removeData(fullKey) {
|
||||
await abStore.removeItem(fullKey);
|
||||
await abStore.removeItem(`addTime-${fullKey}`);
|
||||
}
|
||||
|
||||
async cleanStorage() {
|
||||
@@ -62,7 +68,7 @@ class AuthorBooksStorage {
|
||||
}
|
||||
|
||||
if (size > maxDataSize && toDel) {
|
||||
await this.removeData(toDel);
|
||||
await this._removeData(toDel);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "inpx-web",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "inpx-web",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"hasInstallScript": true,
|
||||
"license": "CC0-1.0",
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "inpx-web",
|
||||
"version": "1.3.2",
|
||||
"version": "1.3.3",
|
||||
"author": "Book Pauk <bookpauk@gmail.com>",
|
||||
"license": "CC0-1.0",
|
||||
"repository": "bookpauk/inpx-web",
|
||||
|
||||
@@ -89,6 +89,8 @@ class WebSocketController {
|
||||
await this.search(req, ws); break;
|
||||
case 'get-author-book-list':
|
||||
await this.getAuthorBookList(req, ws); break;
|
||||
case 'get-author-series-list':
|
||||
await this.getAuthorSeriesList(req, ws); break;
|
||||
case 'get-series-book-list':
|
||||
await this.getSeriesBookList(req, ws); break;
|
||||
case 'get-genre-tree':
|
||||
@@ -169,6 +171,12 @@ class WebSocketController {
|
||||
this.send(result, req, ws);
|
||||
}
|
||||
|
||||
async getAuthorSeriesList(req, ws) {
|
||||
const result = await this.webWorker.getAuthorSeriesList(req.authorId);
|
||||
|
||||
this.send(result, req, ws);
|
||||
}
|
||||
|
||||
async getSeriesBookList(req, ws) {
|
||||
const result = await this.webWorker.getSeriesBookList(req.series);
|
||||
|
||||
|
||||
@@ -599,7 +599,7 @@ class DbSearcher {
|
||||
throw new Error('DbSearcher closed');
|
||||
|
||||
if (!authorId && !author)
|
||||
return {author: '', books: ''};
|
||||
return {author: '', books: []};
|
||||
|
||||
this.searchFlag++;
|
||||
|
||||
@@ -625,14 +625,60 @@ class DbSearcher {
|
||||
const rows = await this.restoreBooks('author', [authorId]);
|
||||
|
||||
let authorName = '';
|
||||
let books = '';
|
||||
let books = [];
|
||||
|
||||
if (rows.length) {
|
||||
authorName = rows[0].name;
|
||||
books = rows[0].books;
|
||||
}
|
||||
|
||||
return {author: authorName, books: (books && books.length ? JSON.stringify(books) : '')};
|
||||
return {author: authorName, books};
|
||||
} finally {
|
||||
this.searchFlag--;
|
||||
}
|
||||
}
|
||||
|
||||
async getAuthorSeriesList(authorId) {
|
||||
if (this.closed)
|
||||
throw new Error('DbSearcher closed');
|
||||
|
||||
if (!authorId)
|
||||
return {author: '', series: []};
|
||||
|
||||
this.searchFlag++;
|
||||
|
||||
try {
|
||||
const db = this.db;
|
||||
|
||||
//выборка книг автора по authorId
|
||||
const bookList = await this.getAuthorBookList(authorId);
|
||||
const books = bookList.books;
|
||||
const seriesSet = new Set();
|
||||
for (const book of books) {
|
||||
if (book.series)
|
||||
seriesSet.add(book.series.toLowerCase());
|
||||
}
|
||||
|
||||
let series = [];
|
||||
if (seriesSet.size) {
|
||||
//выборка серий по названиям
|
||||
series = await db.select({
|
||||
table: 'series',
|
||||
map: `(r) => ({id: r.id, series: r.name, bookCount: r.bookCount, bookDelCount: r.bookDelCount})`,
|
||||
where: `
|
||||
const seriesArr = ${db.esc(Array.from(seriesSet))};
|
||||
const ids = new Set();
|
||||
for (const value of seriesArr) {
|
||||
for (const id of @dirtyIndexLR('value', value, value))
|
||||
ids.add(id);
|
||||
}
|
||||
|
||||
return ids;
|
||||
`
|
||||
});
|
||||
}
|
||||
|
||||
return {author: bookList.author, series};
|
||||
} finally {
|
||||
this.searchFlag--;
|
||||
}
|
||||
@@ -643,7 +689,7 @@ class DbSearcher {
|
||||
throw new Error('DbSearcher closed');
|
||||
|
||||
if (!series)
|
||||
return {books: ''};
|
||||
return {books: []};
|
||||
|
||||
this.searchFlag++;
|
||||
|
||||
@@ -659,7 +705,7 @@ class DbSearcher {
|
||||
where: `return Array.from(@dirtyIndexLR('value', ${db.esc(series)}, ${db.esc(series)}))`
|
||||
});
|
||||
|
||||
let books;
|
||||
let books = [];
|
||||
if (rows.length && rows[0].rawResult.length) {
|
||||
//выборка книг серии
|
||||
const bookRows = await this.restoreBooks('series', [rows[0].rawResult[0]])
|
||||
@@ -668,7 +714,7 @@ class DbSearcher {
|
||||
books = bookRows[0].books;
|
||||
}
|
||||
|
||||
return {books: (books && books.length ? JSON.stringify(books) : '')};
|
||||
return {books};
|
||||
} finally {
|
||||
this.searchFlag--;
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ class RemoteLib {
|
||||
this.config = config;
|
||||
|
||||
this.wsc = new WebSocketConnection(config.remoteLib.url, 10, 30, {rejectUnauthorized: false});
|
||||
if (config.remoteLib.accessPassword)
|
||||
this.accessToken = utils.getBufHash(config.remoteLib.accessPassword, 'sha256', 'hex');
|
||||
|
||||
this.remoteHost = config.remoteLib.url.replace(/^ws:\/\//, 'http://').replace(/^wss:\/\//, 'https://');
|
||||
|
||||
@@ -31,7 +29,7 @@ class RemoteLib {
|
||||
return instance;
|
||||
}
|
||||
|
||||
async wsRequest(query) {
|
||||
async wsRequest(query, recurse = false) {
|
||||
if (this.accessToken)
|
||||
query.accessToken = this.accessToken;
|
||||
|
||||
@@ -40,6 +38,11 @@ class RemoteLib {
|
||||
120
|
||||
);
|
||||
|
||||
if (!recurse && response && response.error == 'need_access_token' && this.config.remoteLib.accessPassword) {
|
||||
this.accessToken = utils.getBufHash(this.config.remoteLib.accessPassword + response.salt, 'sha256', 'hex');
|
||||
return await this.wsRequest(query, true);
|
||||
}
|
||||
|
||||
if (response.error)
|
||||
throw new Error(response.error);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
const { JembaDbThread } = require('jembadb');
|
||||
const utils = require('../core/utils');
|
||||
const log = new (require('../core/AppLogger'))().log;//singleton
|
||||
const asyncExit = new (require('./AsyncExit'))();
|
||||
|
||||
const cleanPeriod = 1*60*1000;//1 минута
|
||||
const cleanUnusedTokenTimeout = 5*60*1000;//5 минут
|
||||
@@ -13,6 +14,8 @@ class WebAccess {
|
||||
this.accessTimeout = config.accessTimeout*60*1000;
|
||||
this.accessMap = new Map();
|
||||
|
||||
asyncExit.add(this.closeDb.bind(this));
|
||||
|
||||
setTimeout(() => { this.periodicClean(); }, cleanPeriod);
|
||||
}
|
||||
|
||||
@@ -67,6 +70,13 @@ class WebAccess {
|
||||
this.db = db;
|
||||
}
|
||||
|
||||
async closeDb() {
|
||||
if (this.db) {
|
||||
await this.db.unlock();
|
||||
this.db = null;
|
||||
}
|
||||
}
|
||||
|
||||
async periodicClean() {
|
||||
while (1) {//eslint-disable-line no-constant-condition
|
||||
try {
|
||||
|
||||
@@ -11,7 +11,7 @@ const DbSearcher = require('./DbSearcher');
|
||||
const InpxHashCreator = require('./InpxHashCreator');
|
||||
const RemoteLib = require('./RemoteLib');//singleton
|
||||
|
||||
const ayncExit = new (require('./AsyncExit'))();
|
||||
const asyncExit = new (require('./AsyncExit'))();
|
||||
const log = new (require('./AppLogger'))().log;//singleton
|
||||
const utils = require('./utils');
|
||||
const genreTree = require('./genres');
|
||||
@@ -53,7 +53,7 @@ class WebWorker {
|
||||
this.db = null;
|
||||
this.dbSearcher = null;
|
||||
|
||||
ayncExit.add(this.closeDb.bind(this));
|
||||
asyncExit.add(this.closeDb.bind(this));
|
||||
|
||||
this.loadOrCreateDb();//no await
|
||||
this.periodicLogServerStats();//no await
|
||||
@@ -221,7 +221,7 @@ class WebWorker {
|
||||
this.logServerStats();
|
||||
} catch (e) {
|
||||
log(LM_FATAL, e.message);
|
||||
ayncExit.exit(1);
|
||||
asyncExit.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,6 +279,12 @@ class WebWorker {
|
||||
return await this.dbSearcher.getAuthorBookList(authorId, author);
|
||||
}
|
||||
|
||||
async getAuthorSeriesList(authorId) {
|
||||
this.checkMyState();
|
||||
|
||||
return await this.dbSearcher.getAuthorSeriesList(authorId);
|
||||
}
|
||||
|
||||
async getSeriesBookList(series) {
|
||||
this.checkMyState();
|
||||
|
||||
@@ -628,7 +634,7 @@ class WebWorker {
|
||||
}
|
||||
} catch (e) {
|
||||
log(LM_FATAL, e.message);
|
||||
ayncExit.exit(1);
|
||||
asyncExit.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ class AuthorPage extends BasePage {
|
||||
const bookList = await this.webWorker.getSeriesBookList(query.series);
|
||||
|
||||
if (bookList.books) {
|
||||
let books = JSON.parse(bookList.books);
|
||||
let books = bookList.books;
|
||||
const booksAll = this.filterBooks(books, {del: 0});
|
||||
const filtered = (query.all ? booksAll : this.filterBooks(books, query));
|
||||
const sorted = this.sortSeriesBooks(filtered);
|
||||
@@ -122,7 +122,7 @@ class AuthorPage extends BasePage {
|
||||
const bookList = await this.webWorker.getAuthorBookList(0, query.author.substring(1));
|
||||
|
||||
if (bookList.books) {
|
||||
let books = JSON.parse(bookList.books);
|
||||
let books = bookList.books;
|
||||
books = this.sortBooks(this.filterBooks(books, query));
|
||||
|
||||
for (const b of books) {
|
||||
|
||||
@@ -44,7 +44,7 @@ class SeriesPage extends BasePage {
|
||||
const bookList = await this.webWorker.getSeriesBookList(query.series.substring(1));
|
||||
|
||||
if (bookList.books) {
|
||||
let books = JSON.parse(bookList.books);
|
||||
let books = bookList.books;
|
||||
const booksAll = this.filterBooks(books, {del: 0});
|
||||
const filtered = (query.all ? booksAll : this.filterBooks(books, query));
|
||||
const sorted = this.sortSeriesBooks(filtered);
|
||||
|
||||
@@ -158,8 +158,7 @@ async function main() {
|
||||
opds(app, config);
|
||||
initStatic(app, config);
|
||||
|
||||
const WebAccess = require('./core/WebAccess');
|
||||
const webAccess = new WebAccess(config);
|
||||
const webAccess = new (require('./core/WebAccess'))(config);
|
||||
await webAccess.init();
|
||||
|
||||
const { WebSocketController } = require('./controllers');
|
||||
|
||||
Reference in New Issue
Block a user