From 35925dbc6e0254e80e5c2581142bd05ad4a89e06 Mon Sep 17 00:00:00 2001 From: Book Pauk Date: Tue, 22 Nov 2022 20:09:00 +0700 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20=D0=BD?= =?UTF-8?q?=D0=B0=D0=B4=20opds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/core/DbSearcher.js | 91 +++++++++++++++++++++++++++++++--- server/core/WebWorker.js | 10 +++- server/core/opds/AuthorPage.js | 60 ++++++++++++++++++++-- server/core/opds/BasePage.js | 66 ++++++++++++++++++++++++ server/core/opds/RootPage.js | 3 +- 5 files changed, 216 insertions(+), 14 deletions(-) diff --git a/server/core/DbSearcher.js b/server/core/DbSearcher.js index 04d0698..659d7b7 100644 --- a/server/core/DbSearcher.js +++ b/server/core/DbSearcher.js @@ -534,28 +534,105 @@ class DbSearcher { } } - async getAuthorBookList(authorId) { + async opdsQuery(from, query) { if (this.closed) throw new Error('DbSearcher closed'); - if (!authorId) + if (!['author', 'series', 'title'].includes(from)) + throw new Error(`Unknown value for param 'from'`); + + this.searchFlag++; + + try { + const db = this.db; + + const queryKey = this.queryKey(query); + const opdsKey = `${from}-opds-${queryKey}`; + let result = await this.getCached(opdsKey); + + if (result === null) { + const ids = await this.selectTableIds(from, query); + + const totalFound = ids.length; + const depth = query.depth || 1; + + //группировка по name длиной depth + const found = await db.select({ + table: from, + rawResult: true, + where: ` + const depth = ${db.esc(depth)}; + const group = new Map(); + + const ids = ${db.esc(Array.from(ids))}; + for (const id of ids) { + const row = @unsafeRow(id); + const s = row.name.substring(0, depth); + let g = group.get(s); + if (!g) { + g = {id: row.id, name: s, count: 0}; + group.set(s, g); + } + g.count++; + } + + const result = Array.from(group.values()); + result.sort((a, b) => a.name.localeCompare(b.name)); + + return result; + ` + }); + + result = {found: found[0].rawResult, totalFound}; + + await this.putCached(opdsKey, result); + } + + return result; + } finally { + this.searchFlag--; + } + } + + async getAuthorBookList(authorId, author) { + if (this.closed) + throw new Error('DbSearcher closed'); + + if (!authorId && !author) return {author: '', books: ''}; this.searchFlag++; try { - //выборка книг автора по authorId - const rows = await this.restoreBooks('author', [authorId]) + const db = this.db; - let author = ''; + if (!authorId) { + //восстановим authorId + authorId = 0; + author = author.toLowerCase(); + + const rows = await db.select({ + table: 'author', + rawResult: true, + where: `return Array.from(@dirtyIndexLR('value', ${db.esc(author)}, ${db.esc(author)}))` + }); + + if (rows.length && rows[0].rawResult.length) + authorId = rows[0].rawResult[0]; + } + + //выборка книг автора по authorId + const rows = await this.restoreBooks('author', [authorId]); + + let authorName = ''; let books = ''; if (rows.length) { - author = rows[0].name; + authorName = rows[0].name; books = rows[0].books; } - return {author, books: (books && books.length ? JSON.stringify(books) : '')}; + return {author: authorName, books: (books && books.length ? JSON.stringify(books) : '')}; } finally { this.searchFlag--; } diff --git a/server/core/WebWorker.js b/server/core/WebWorker.js index 6f02b64..8c4bac7 100644 --- a/server/core/WebWorker.js +++ b/server/core/WebWorker.js @@ -267,10 +267,16 @@ class WebWorker { return result; } - async getAuthorBookList(authorId) { + async opdsQuery(from, query) { this.checkMyState(); - return await this.dbSearcher.getAuthorBookList(authorId); + return await this.dbSearcher.opdsQuery(from, query); + } + + async getAuthorBookList(authorId, author) { + this.checkMyState(); + + return await this.dbSearcher.getAuthorBookList(authorId, author); } async getSeriesBookList(series) { diff --git a/server/core/opds/AuthorPage.js b/server/core/opds/AuthorPage.js index 491552b..2490ff2 100644 --- a/server/core/opds/AuthorPage.js +++ b/server/core/opds/AuthorPage.js @@ -8,12 +8,66 @@ class AuthorPage extends BasePage { this.title = 'Авторы'; } - async body() { + bookAuthor(author) { + if (author) { + let a = author.split(','); + return a.slice(0, 3).join(', ') + (a.length > 3 ? ' и др.' : ''); + } + + return ''; + } + + async body(req) { const result = {}; - result.entry = [ - ]; + const query = {author: '', depth: 1, del: 0, limit: 100}; + if (req.query.author) { + query.author = req.query.author; + query.depth = query.author.length + 1; + } + if (req.query.author == '___others') { + query.author = ''; + query.depth = 1; + query.others = true; + } + + const entry = []; + if (query.author && query.author[0] == '=') { + //книги по автору + const bookList = await this.webWorker.getAuthorBookList(0, query.author.substring(1)); + + if (bookList.books) { + const books = JSON.parse(bookList.books); + + for (const book of books) { + const title = book.title || 'Без названия'; + entry.push( + this.makeEntry({ + id: book._uid, + title, + link: this.navLink({rel: 'subsection', href: `/${this.id}?book=${book._uid}`}), + }) + ); + } + } + } else { + //поиск по каталогу + const queryRes = await this.opdsQuery('author', query); + + for (const rec of queryRes) { +console.log(rec); + entry.push( + this.makeEntry({ + id: rec.id, + title: this.bookAuthor(rec.title),//${(query.depth > 1 && rec.count ? ` (${rec.count})` : '')} + link: this.navLink({rel: 'subsection', href: `/${this.id}?author=${rec.q}`}), + }) + ); + } + } + + result.entry = entry; return this.makeBody(result); } } diff --git a/server/core/opds/BasePage.js b/server/core/opds/BasePage.js index 0b99bd6..374ff01 100644 --- a/server/core/opds/BasePage.js +++ b/server/core/opds/BasePage.js @@ -1,6 +1,14 @@ +const he = require('he'); + const WebWorker = require('../WebWorker');//singleton const XmlParser = require('../xml/XmlParser'); +const spaceChar = String.fromCodePoint(0x00B7); +const ruAlphabet = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'; +const enAlphabet = 'abcdefghijklmnopqrstuvwxyz'; +const enruArr = (ruAlphabet + enAlphabet).split(''); +const enru = new Set(enruArr); + class BasePage { constructor(config) { this.config = config; @@ -16,6 +24,8 @@ class BasePage { if (!entry.title) throw new Error('makeEntry: no title'); + entry.title = he.escape(entry.title); + const result = { updated: (new Date()).toISOString().substring(0, 19) + 'Z', }; @@ -73,6 +83,62 @@ class BasePage { async body() { throw new Error('Body not implemented'); } + + // -- stuff ------------------------------------------- + async search(from, query) { + const result = []; + const queryRes = await this.webWorker.search(from, query); + + for (const row of queryRes.found) { + const rec = { + id: row.id, + title: '=' + (row[from] || 'Без имени'), + q: `=${encodeURIComponent(row[from])}`, + }; + + result.push(rec); + } + + return result; + } + + async opdsQuery(from, query) { + const result = []; + + const queryRes = await this.webWorker.opdsQuery(from, query); + let count = 0; + for (const row of queryRes.found) + count += row.count; + + if (count <= query.limit) + return await this.search(from, query); + + const names = new Set(); + const others = []; + for (const row of queryRes.found) { + const name = row.name.toUpperCase(); + + if (!names.has(name)) { + const rec = { + id: row.id, + title: name.replace(/ /g, spaceChar), + q: encodeURIComponent(row.name.toLowerCase()), + count: row.count, + }; + if (query.depth > 1 || enru.has(row.name[0].toLowerCase())) { + result.push(rec); + } else { + others.push(rec); + } + names.add(name); + } + } + + if (!query.others && query.depth == 1) + result.push({id: 'other', title: 'Все остальные', q: '___others'}); + + return (!query.others ? result : others); + } } module.exports = BasePage; \ No newline at end of file diff --git a/server/core/opds/RootPage.js b/server/core/opds/RootPage.js index ad8e8c5..ffbe16c 100644 --- a/server/core/opds/RootPage.js +++ b/server/core/opds/RootPage.js @@ -13,10 +13,9 @@ class RootPage extends BasePage { async body() { const result = {}; - const ww = this.webWorker; if (!this.title) { - const dbConfig = await ww.dbConfig(); + const dbConfig = await this.webWorker.dbConfig(); const collection = dbConfig.inpxInfo.collection.split('\n'); this.title = collection[0].trim(); if (!this.title)