Book Pauk 2 жил өмнө
parent
commit
35925dbc6e

+ 83 - 6
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 {
+            const db = this.db;
+
+            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])
+            const rows = await this.restoreBooks('author', [authorId]);
 
-            let author = '';
+            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--;
         }

+ 8 - 2
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) {

+ 57 - 3
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);
     }
 }

+ 66 - 0
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;

+ 1 - 2
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)