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');