diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..54a761e
--- /dev/null
+++ b/CHANGELOG.md
@@ -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
+------------------
+
+- Добавлены разделы "Серии" и "Книги"
+- Расширена форма поиска: добавлен поиск по датам поступления и оценкам
+
diff --git a/client/components/Api/Api.vue b/client/components/Api/Api.vue
index b8d8854..7b34ed3 100644
--- a/client/components/Api/Api.vue
+++ b/client/components/Api/Api.vue
@@ -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'});
}
}
diff --git a/client/components/Search/AuthorList/AuthorList.vue b/client/components/Search/AuthorList/AuthorList.vue
index bc7718f..2f5892a 100644
--- a/client/components/Search/AuthorList/AuthorList.vue
+++ b/client/components/Search/AuthorList/AuthorList.vue
@@ -56,7 +56,7 @@
- {{ getSeriesBookCount(book) }}
+ {{ getSeriesBookCount(item, book) }}
@@ -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,
diff --git a/client/components/Search/BaseList.js b/client/components/Search/BaseList.js
index 8db041d..0082d58 100644
--- a/client/components/Search/BaseList.js
+++ b/client/components/Search/BaseList.js
@@ -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, 'Ошибка');
}
diff --git a/client/components/Search/authorBooksStorage.js b/client/components/Search/authorBooksStorage.js
index 6e6be34..535b641 100644
--- a/client/components/Search/authorBooksStorage.js
+++ b/client/components/Search/authorBooksStorage.js
@@ -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;
}
diff --git a/package-lock.json b/package-lock.json
index 1609bee..e019db9 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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": {
diff --git a/package.json b/package.json
index c5015fc..055a81a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "inpx-web",
- "version": "1.3.2",
+ "version": "1.3.3",
"author": "Book Pauk ",
"license": "CC0-1.0",
"repository": "bookpauk/inpx-web",
diff --git a/server/controllers/WebSocketController.js b/server/controllers/WebSocketController.js
index eea3c01..73344b4 100644
--- a/server/controllers/WebSocketController.js
+++ b/server/controllers/WebSocketController.js
@@ -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);
diff --git a/server/core/DbSearcher.js b/server/core/DbSearcher.js
index 4199208..fe3012f 100644
--- a/server/core/DbSearcher.js
+++ b/server/core/DbSearcher.js
@@ -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--;
}
diff --git a/server/core/RemoteLib.js b/server/core/RemoteLib.js
index 0ab0c8e..2a8d2a4 100644
--- a/server/core/RemoteLib.js
+++ b/server/core/RemoteLib.js
@@ -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);
diff --git a/server/core/WebAccess.js b/server/core/WebAccess.js
index a272cb7..6da3548 100644
--- a/server/core/WebAccess.js
+++ b/server/core/WebAccess.js
@@ -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 {
diff --git a/server/core/WebWorker.js b/server/core/WebWorker.js
index 8c4bac7..50a89e4 100644
--- a/server/core/WebWorker.js
+++ b/server/core/WebWorker.js
@@ -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);
}
}
diff --git a/server/core/opds/AuthorPage.js b/server/core/opds/AuthorPage.js
index f8cb8f1..e58996a 100644
--- a/server/core/opds/AuthorPage.js
+++ b/server/core/opds/AuthorPage.js
@@ -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) {
diff --git a/server/core/opds/SeriesPage.js b/server/core/opds/SeriesPage.js
index 260592a..15b3d8b 100644
--- a/server/core/opds/SeriesPage.js
+++ b/server/core/opds/SeriesPage.js
@@ -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);
diff --git a/server/index.js b/server/index.js
index 33ea96e..4d968fa 100644
--- a/server/index.js
+++ b/server/index.js
@@ -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');