diff --git a/CHANGELOG.md b/CHANGELOG.md index d4cb90e..e806d0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +1.5.4 / 2023-04-12 + +- Добавлена возможность поиска по типу файла +- Улучшена работа с inpx, теперь понимает файлы в каталогах (без zip-архива) +- В readme добавлен раздел "Запуск без сборки релиза" для запуска inpx-web на любых платформах +- Исправления мелких багов + 1.5.3 / 2023-03-02 - OPDS: исправление проблемы поиска для koreader diff --git a/README.md b/README.md index eb574cd..d0957c7 100644 --- a/README.md +++ b/README.md @@ -29,7 +29,8 @@ OPDS-сервер доступен по адресу [http://127.0.0.1:12380/opd * [Удаленная библиотека](#remotelib) * [Фильтр по авторам и книгам](#filter) * [Настройка https с помощью nginx](#https) -* [Сборка проекта](#build) +* [Сборка релизов](#build) +* [Запуск без сборки релиза](#native_run) * [Разработка](#development) @@ -321,7 +322,7 @@ sudo service nginx reload -### Сборка проекта +### Сборка релизов Сборка только в среде Linux. Необходима версия node.js не ниже 16. @@ -331,15 +332,36 @@ sudo service nginx reload git clone https://github.com/bookpauk/inpx-web cd inpx-web npm i -``` - -#### Релизы -```sh npm run release ``` Результат сборки будет доступен в каталоге `dist/release` + + +### Запуск без сборки релиза +Т.к. сборщик pkg поддерживает не все платформы, то не всегда удается собрать релиз. +Однако, можно скачать и запустить inpx-web нативным путем, с помощью nodejs. +Ниже пример для Ubuntu, для других линуксов различия не принципиальны: + +```sh +# установка nodejs v16 и выше: +curl -s https://deb.nodesource.com/setup_16.x | sudo bash +sudo apt install nodejs -y + +# подготовка +git clone https://github.com/bookpauk/inpx-web +cd inpx-web +npm i +npm run build:client && node build/prepkg.js linux + +# удалим файл development-среды, чтобы запускался в production-режиме +rm ./server/config/application_env + +# запуск inpx-web, тут же будет создан каталог .inpx-web +node server --app-dir=.inpx-web +``` + ### Разработка diff --git a/client/components/Search/AuthorList/AuthorList.vue b/client/components/Search/AuthorList/AuthorList.vue index bc22ac2..271b51d 100644 --- a/client/components/Search/AuthorList/AuthorList.vue +++ b/client/components/Search/AuthorList/AuthorList.vue @@ -126,9 +126,9 @@ -
+
- Поиск не дал результатов + {{ (error ? error : 'Поиск не дал результатов') }}
@@ -438,6 +438,7 @@ class AuthorList extends BaseList { if (this.refreshing) return; + this.error = ''; this.refreshing = true; (async() => { @@ -467,7 +468,12 @@ class AuthorList extends BaseList { this.highlightPageScroller(query); } } catch (e) { - this.$root.stdDialog.alert(e.message, 'Ошибка'); + this.list.queryFound = 0; + this.list.totalFound = 0; + this.searchResult = {found: []}; + await this.updateTableData(); + //this.$root.stdDialog.alert(e.message, 'Ошибка'); + this.error = `Ошибка: ${e.message}`; } } } finally { diff --git a/client/components/Search/BaseList.js b/client/components/Search/BaseList.js index ddc7b9d..d1d98af 100644 --- a/client/components/Search/BaseList.js +++ b/client/components/Search/BaseList.js @@ -48,6 +48,7 @@ export default class BaseList { genreMap: Object, }; + error = ''; loadingMessage = ''; loadingMessage2 = ''; @@ -371,7 +372,8 @@ export default class BaseList { bookValue = emptyFieldValue; bookValue = bookValue.toLowerCase(); - searchValue = searchValue.toLowerCase(); + if (searchValue[0] !== '~') + searchValue = searchValue.toLowerCase(); //особая обработка префиксов if (searchValue[0] == '=') { @@ -450,6 +452,13 @@ export default class BaseList { librateFound = searchLibrate.has(book.librate); } + //ext + let extFound = !s.ext; + if (!extFound) { + const searchExt = new Set(s.ext.split('|')); + extFound = searchExt.has(book.ext.toLowerCase() || emptyFieldValue); + } + return (this.showDeleted || !book.del) && authorFound && filterBySearch(book.series, s.series) @@ -458,6 +467,7 @@ export default class BaseList { && langFound && dateFound && librateFound + && extFound ; }); } diff --git a/client/components/Search/BookInfoDialog/BookInfoDialog.vue b/client/components/Search/BookInfoDialog/BookInfoDialog.vue index 2507700..1307331 100644 --- a/client/components/Search/BookInfoDialog/BookInfoDialog.vue +++ b/client/components/Search/BookInfoDialog/BookInfoDialog.vue @@ -20,7 +20,7 @@
-
+
{{ book.ext }}
diff --git a/client/components/Search/ExtendedList/ExtendedList.vue b/client/components/Search/ExtendedList/ExtendedList.vue index 19a19d5..b34035a 100644 --- a/client/components/Search/ExtendedList/ExtendedList.vue +++ b/client/components/Search/ExtendedList/ExtendedList.vue @@ -14,9 +14,9 @@
-
+
- Поиск не дал результатов + {{ (error ? error : 'Поиск не дал результатов') }}
@@ -74,6 +74,7 @@ class ExtendedList extends BaseList { if (this.refreshing) return; + this.error = ''; this.refreshing = true; (async() => { @@ -103,7 +104,12 @@ class ExtendedList extends BaseList { this.highlightPageScroller(query); } } catch (e) { - this.$root.stdDialog.alert(e.message, 'Ошибка'); + this.list.queryFound = 0; + this.list.totalFound = 0; + this.searchResult = {found: []}; + await this.updateTableData(); + //this.$root.stdDialog.alert(e.message, 'Ошибка'); + this.error = `Ошибка: ${e.message}`; } } } finally { diff --git a/client/components/Search/Search.vue b/client/components/Search/Search.vue index 90d71e9..585e79f 100644 --- a/client/components/Search/Search.vue +++ b/client/components/Search/Search.vue @@ -160,7 +160,7 @@
@@ -234,6 +234,7 @@ class SeriesList extends BaseList { if (this.refreshing) return; + this.error = ''; this.refreshing = true; (async() => { @@ -263,7 +264,12 @@ class SeriesList extends BaseList { this.highlightPageScroller(query); } } catch (e) { - this.$root.stdDialog.alert(e.message, 'Ошибка'); + this.list.queryFound = 0; + this.list.totalFound = 0; + this.searchResult = {found: []}; + await this.updateTableData(); + //this.$root.stdDialog.alert(e.message, 'Ошибка'); + this.error = `Ошибка: ${e.message}`; } } } finally { diff --git a/client/components/Search/TitleList/TitleList.vue b/client/components/Search/TitleList/TitleList.vue index 0c47009..2f92701 100644 --- a/client/components/Search/TitleList/TitleList.vue +++ b/client/components/Search/TitleList/TitleList.vue @@ -22,9 +22,9 @@
-
+
- Поиск не дал результатов + {{ (error ? error : 'Поиск не дал результатов') }}
@@ -95,6 +95,7 @@ class TitleList extends BaseList { if (this.refreshing) return; + this.error = ''; this.refreshing = true; (async() => { @@ -124,7 +125,12 @@ class TitleList extends BaseList { this.highlightPageScroller(query); } } catch (e) { - this.$root.stdDialog.alert(e.message, 'Ошибка'); + this.list.queryFound = 0; + this.list.totalFound = 0; + this.searchResult = {found: []}; + await this.updateTableData(); + //this.$root.stdDialog.alert(e.message, 'Ошибка'); + this.error = `Ошибка: ${e.message}`; } } } finally { diff --git a/client/share/utils.js b/client/share/utils.js index 2e7fe96..36793d5 100644 --- a/client/share/utils.js +++ b/client/share/utils.js @@ -87,35 +87,6 @@ export async function copyTextToClipboard(text) { return result; } -/* -export function formatDate(d, format = 'normal') { - switch (format) { - case 'normal': - return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()} ` + - `${d.getHours().toString().padStart(2, '0')}:${d.getMinutes().toString().padStart(2, '0')}`; - case 'coDate': - return `${d.getFullYear()}-${(d.getMonth() + 1).toString().padStart(2, '0')}-${d.getDate().toString().padStart(2, '0')}`; - case 'coMonth': - return `${(d.getMonth() + 1).toString().padStart(2, '0')}`; - case 'noDate': - return `${d.getDate().toString().padStart(2, '0')}.${(d.getMonth() + 1).toString().padStart(2, '0')}.${d.getFullYear()}`; - - default: - throw new Error('formatDate: unknown date format'); - } -} - -export function parseDate(sqlDate) { - const d = sqlDate.split('-'); - const result = new Date(); - result.setDate(parseInt(d[2], 10)); - result.setMonth(parseInt(d[1], 10) - 1); - result.setYear(parseInt(d[0], 10)); - - return result; -} -*/ - export function isDigit(c) { return !isNaN(parseInt(c, 10)); } diff --git a/package-lock.json b/package-lock.json index 409bd5d..6ba3544 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "inpx-web", - "version": "1.5.3", + "version": "1.5.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "inpx-web", - "version": "1.5.3", + "version": "1.5.4", "hasInstallScript": true, "license": "CC0-1.0", "dependencies": { diff --git a/package.json b/package.json index 6b085f2..6b3db84 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "inpx-web", - "version": "1.5.3", + "version": "1.5.4", "author": "Book Pauk ", "license": "CC0-1.0", "repository": "bookpauk/inpx-web", diff --git a/server/config/base.js b/server/config/base.js index 2cf732b..71eaa92 100644 --- a/server/config/base.js +++ b/server/config/base.js @@ -21,7 +21,7 @@ module.exports = { //поправить в случае, если были критические изменения в DbCreator или InpxParser //иначе будет рассинхронизация по кешу между сервером и клиентом на уровне БД - dbVersion: '11', + dbVersion: '12', dbCacheSize: 5, maxPayloadSize: 500,//in MB diff --git a/server/core/DbCreator.js b/server/core/DbCreator.js index 4066ff7..841eeee 100644 --- a/server/core/DbCreator.js +++ b/server/core/DbCreator.js @@ -64,6 +64,8 @@ class DbCreator { let dateArr = []; let librateMap = new Map();//оценка let librateArr = []; + let extMap = new Map();//тип файла + let extArr = []; let uidSet = new Set();//уникальные идентификаторы @@ -215,6 +217,9 @@ class DbCreator { //оценка parseField(rec.librate, librateMap, librateArr, rec.id); + + //тип файла + parseField(rec.ext, extMap, extArr, rec.id); }; //основная процедура парсинга @@ -272,6 +277,8 @@ class DbCreator { delMap = null; dateMap = null; librateMap = null; + extMap = null; + uidSet = null; await db.close({table: 'book'}); @@ -408,6 +415,9 @@ class DbCreator { //librate await saveTable('librate', librateArr, () => {librateArr = null}, 'number'); + //ext + await saveTable('ext', extArr, () => {extArr = null}); + //кэш-таблицы запросов await db.create({table: 'query_cache'}); await db.create({table: 'query_time'}); diff --git a/server/core/DbSearcher.js b/server/core/DbSearcher.js index 79cceb2..0b2d7a5 100644 --- a/server/core/DbSearcher.js +++ b/server/core/DbSearcher.js @@ -49,7 +49,8 @@ class DbSearcher { getWhere(a) { const db = this.db; - a = a.toLowerCase(); + if (a[0] !== '~') + a = a.toLowerCase(); let where; //особая обработка префиксов @@ -288,6 +289,42 @@ class DbSearcher { idsArr.push(ids); } + //тип файла + if (query.ext) { + const key = `book-ids-ext-${query.ext}`; + let ids = await this.getCached(key); + + if (ids === null) { + const extRows = await db.select({ + table: 'ext', + rawResult: true, + where: ` + const exts = ${db.esc(query.ext.split('|'))}; + + const ids = new Set(); + for (const l of exts) { + for (const id of @indexLR('value', l, l)) + ids.add(id); + } + + const result = new Set(); + for (const id of ids) { + const row = @unsafeRow(id); + for (const bookId of row.bookIds) + result.add(bookId); + } + + return new Uint32Array(result); + ` + }); + + ids = extRows[0].rawResult; + await this.putCached(key, ids); + } + + idsArr.push(ids); + } + if (idsArr.length > 1) { //ищем пересечение множеств let proc = 0; diff --git a/server/core/WebWorker.js b/server/core/WebWorker.js index 06c4bb8..2cc3b95 100644 --- a/server/core/WebWorker.js +++ b/server/core/WebWorker.js @@ -30,7 +30,7 @@ const stateToText = { }; const cleanDirInterval = 60*60*1000;//каждый час -const checkReleaseInterval = 2*60*60*1000;//каждые 2 часа +const checkReleaseInterval = 7*60*60*1000;//каждые 7 часов //singleton let instance = null; @@ -350,9 +350,14 @@ class WebWorker { rows = await db.select({table: 'lang', map: `(r) => ({value: r.value})`}); const langs = rows.map(r => r.value); + // exts + rows = await db.select({table: 'ext', map: `(r) => ({value: r.value})`}); + const exts = rows.map(r => r.value); + result = { genreTree: genres, langList: langs, + extList: exts, inpxHash: (config.inpxHash ? config.inpxHash : ''), }; @@ -367,17 +372,28 @@ class WebWorker { async extractBook(bookPath) { const outFile = `${this.config.tempDir}/${utils.randomHexString(30)}`; - const folder = `${this.config.libDir}/${path.dirname(bookPath)}`; - const file = path.basename(bookPath); + bookPath = bookPath.replace(/\\/g, '/').replace(/\/\//g, '/'); - const zipReader = new ZipReader(); - await zipReader.open(folder); + const i = bookPath.indexOf('/'); + const folder = `${this.config.libDir}/${(i >= 0 ? bookPath.substring(0, i) : bookPath )}`; + const file = (i >= 0 ? bookPath.substring(i + 1) : '' ); - try { - await zipReader.extractToFile(file, outFile); + const fullPath = `${folder}/${file}`; + + if (!file || await fs.pathExists(fullPath)) {// файл есть на диске + + await fs.copy(fullPath, outFile); return outFile; - } finally { - await zipReader.close(); + } else {// файл в zip-архиве + const zipReader = new ZipReader(); + await zipReader.open(folder); + + try { + await zipReader.extractToFile(file, outFile); + return outFile; + } finally { + await zipReader.close(); + } } } diff --git a/server/core/fb2/Fb2Helper.js b/server/core/fb2/Fb2Helper.js index e2b1de3..837affb 100644 --- a/server/core/fb2/Fb2Helper.js +++ b/server/core/fb2/Fb2Helper.js @@ -35,7 +35,7 @@ class Fb2Helper { if (m) { let enc = m[1].toLowerCase(); if (enc != 'utf-8') { - //enc может не соответсвовать реальной кодировке файла, поэтому: + //если кодировка не определена в getEncoding, используем enc if (encoding.indexOf('ISO-8859') >= 0) { encoding = enc; } diff --git a/server/core/fb2/textUtils.js b/server/core/fb2/textUtils.js index ef05606..c3b2f40 100644 --- a/server/core/fb2/textUtils.js +++ b/server/core/fb2/textUtils.js @@ -4,7 +4,7 @@ function getEncoding(buf) { let selected = getEncodingLite(buf); if (selected == 'ISO-8859-5' && buf.length > 10) { - const charsetAll = chardet.analyse(buf.slice(0, 20000)); + const charsetAll = chardet.analyse(buf.slice(0, 100000)); for (const charset of charsetAll) { if (charset.name.indexOf('ISO-8859') < 0) { selected = charset.name; @@ -39,9 +39,7 @@ function getEncodingLite(buf, returnAll) { 'u': 0, }; - const len = buf.length; - const blockSize = (len > 5*3000 ? 3000 : len); - let counter = 0; + const len = (buf.length > 100000 ? 100000 : buf.length); let i = 0; let totalChecked = 0; while (i < len) { @@ -76,13 +74,6 @@ function getEncodingLite(buf, returnAll) { if (char > 207 && char < 240) charsets['i'] += lowerCase; if (char > 175 && char < 208) charsets['i'] += upperCase; } - - counter++; - - if (counter > blockSize) { - counter = 0; - i += Math.round(len/2 - 2*blockSize); - } } let sorted = Object.keys(charsets).map(function(key) { diff --git a/server/core/opds/BasePage.js b/server/core/opds/BasePage.js index 38eeb9a..83695a9 100644 --- a/server/core/opds/BasePage.js +++ b/server/core/opds/BasePage.js @@ -243,7 +243,8 @@ class BasePage { bookValue = emptyFieldValue; bookValue = bookValue.toLowerCase(); - searchValue = searchValue.toLowerCase(); + if (searchValue[0] !== '~') + searchValue = searchValue.toLowerCase(); //особая обработка префиксов if (searchValue[0] == '=') {