diff --git a/server/core/opds/AuthorPage.js b/server/core/opds/AuthorPage.js index 2490ff2..42f732e 100644 --- a/server/core/opds/AuthorPage.js +++ b/server/core/opds/AuthorPage.js @@ -17,51 +17,129 @@ class AuthorPage extends BasePage { return ''; } + sortBooks(bookList) { + //схлопывание серий + const books = []; + const seriesSet = new Set(); + for (const book of bookList) { + if (book.series) { + if (!seriesSet.has(book.series)) { + books.push({ + type: 'series', + book + }); + + seriesSet.add(book.series); + } + } else { + books.push({ + type: 'book', + book + }); + } + } + + //сортировка + books.sort((a, b) => { + if (a.type == 'series') { + return (b.type == 'series' ? a.book.series.localeCompare(b.book.series) : -1); + } else { + return (b.type == 'book' ? a.book.title.localeCompare(b.book.title) : 1); + } + }); + + return books; + } + + sortSeriesBooks(seriesBooks) { + seriesBooks.sort((a, b) => { + const dserno = (a.serno || Number.MAX_VALUE) - (b.serno || Number.MAX_VALUE); + const dtitle = a.title.localeCompare(b.title); + const dext = a.ext.localeCompare(b.ext); + return (dserno ? dserno : (dtitle ? dtitle : dext)); + }); + + return seriesBooks; + } + async body(req) { const result = {}; - const query = {author: '', depth: 1, del: 0, limit: 100}; - if (req.query.author) { - query.author = req.query.author; - query.depth = query.author.length + 1; - } + const query = { + author: req.query.author || '', + series: req.query.series || '', + depth: 0, + del: 0, + limit: 100 + }; + query.depth = query.author.length + 1; - if (req.query.author == '___others') { + if (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 (query.series) { + //книги по серии + const bookList = await this.webWorker.getSeriesBookList(query.series); if (bookList.books) { - const books = JSON.parse(bookList.books); + let books = JSON.parse(bookList.books); + books = this.sortSeriesBooks(this.filterBooks(books, query)); for (const book of books) { - const title = book.title || 'Без названия'; + const title = `${book.serno ? `${book.serno}. `: ''}${book.title || 'Без названия'}`; entry.push( this.makeEntry({ id: book._uid, title, - link: this.navLink({rel: 'subsection', href: `/${this.id}?book=${book._uid}`}), + link: this.navLink({href: `/book?uid=${encodeURIComponent(book._uid)}`}), }) ); } } + } else if (query.author && query.author[0] == '=') { + //книги по автору + const bookList = await this.webWorker.getAuthorBookList(0, query.author.substring(1)); + + if (bookList.books) { + let books = JSON.parse(bookList.books); + books = this.sortBooks(this.filterBooks(books, query)); + + for (const b of books) { + if (b.type == 'series') { + entry.push( + this.makeEntry({ + id: b.book._uid, + title: `Серия: ${b.book.series}`, + link: this.navLink({ + href: `/${this.id}?author=${encodeURIComponent(query.author)}` + + `&series=${encodeURIComponent(b.book.series)}`}), + }) + ); + } else { + const title = b.book.title || 'Без названия'; + entry.push( + this.makeEntry({ + id: b.book._uid, + title, + link: this.navLink({href: `/book?uid=${encodeURIComponent(b.book._uid)}`}), + }) + ); + } + } + } } else { //поиск по каталогу const queryRes = await this.opdsQuery('author', query); - for (const rec of queryRes) { -console.log(rec); + for (const rec of queryRes) { 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}`}), + link: this.navLink({href: `/${this.id}?author=${rec.q}`}), }) ); } diff --git a/server/core/opds/BasePage.js b/server/core/opds/BasePage.js index 374ff01..7f8d299 100644 --- a/server/core/opds/BasePage.js +++ b/server/core/opds/BasePage.js @@ -1,9 +1,12 @@ +const _ = require('lodash'); const he = require('he'); const WebWorker = require('../WebWorker');//singleton const XmlParser = require('../xml/XmlParser'); const spaceChar = String.fromCodePoint(0x00B7); +const emptyFieldValue = '?'; +const maxUtf8Char = String.fromCodePoint(0xFFFFF); const ruAlphabet = 'абвгдеёжзийклмнопрстуфхцчшщъыьэюя'; const enAlphabet = 'abcdefghijklmnopqrstuvwxyz'; const enruArr = (ruAlphabet + enAlphabet).split(''); @@ -37,7 +40,7 @@ class BasePage { return this.makeEntry({ id: this.id, title: this.title, - link: this.navLink({rel: 'subsection', href: `/${this.id}`}), + link: this.navLink({href: `/${this.id}`}), }); } @@ -48,11 +51,35 @@ class BasePage { navLink(attrs) { return this.makeLink({ href: this.opdsRoot + (attrs.href || ''), - rel: attrs.rel || '', + rel: attrs.rel || 'subsection', type: 'application/atom+xml; profile=opds-catalog; kind=navigation', }); } + acqLink(attrs) { + if (!attrs.href) + throw new Error('acqLink: no href'); + if (!attrs.type) + throw new Error('acqLink: no type'); + + return this.makeLink({ + href: attrs.href, + rel: 'http://opds-spec.org/acquisition/open-access', + type: attrs.type, + }); + } + + imgLink(attrs) { + if (!attrs.href) + throw new Error('acqLink: no href'); + + return this.makeLink({ + href: attrs.href, + rel: `http://opds-spec.org/image${attrs.thumb ? '/thumbnail' : ''}`, + type: attrs.type || 'image/jpeg', + }); + } + baseLinks() { return [ this.navLink({rel: 'start'}), @@ -92,7 +119,7 @@ class BasePage { for (const row of queryRes.found) { const rec = { id: row.id, - title: '=' + (row[from] || 'Без имени'), + title: (row[from] || 'Без автора'), q: `=${encodeURIComponent(row[from])}`, }; @@ -103,8 +130,6 @@ class BasePage { } async opdsQuery(from, query) { - const result = []; - const queryRes = await this.webWorker.opdsQuery(from, query); let count = 0; for (const row of queryRes.found) @@ -113,8 +138,9 @@ class BasePage { if (count <= query.limit) return await this.search(from, query); - const names = new Set(); + const result = []; const others = []; + const names = new Set(); for (const row of queryRes.found) { const name = row.name.toUpperCase(); @@ -134,11 +160,129 @@ class BasePage { } } + if (query.depth > 1 && result.length == 1 && query[from]) { + const newQuery = _.cloneDeep(query); + newQuery[from] = decodeURIComponent(result[0].q); + if (newQuery[from].length >= query.depth) { + newQuery.depth = newQuery[from].length + 1; + return await this.opdsQuery(from, newQuery); + } + } + if (!query.others && query.depth == 1) result.push({id: 'other', title: 'Все остальные', q: '___others'}); return (!query.others ? result : others); } + + //скопировано из BaseList.js, часть функционала не используется + filterBooks(books, query) { + const s = query; + + const splitAuthor = (author) => { + if (!author) { + author = emptyFieldValue; + } + + const result = author.split(','); + if (result.length > 1) + result.push(author); + + return result; + }; + + const filterBySearch = (bookValue, searchValue) => { + if (!searchValue) + return true; + + if (!bookValue) + bookValue = emptyFieldValue; + + bookValue = bookValue.toLowerCase(); + searchValue = searchValue.toLowerCase(); + + //особая обработка префиксов + if (searchValue[0] == '=') { + + searchValue = searchValue.substring(1); + return bookValue.localeCompare(searchValue) == 0; + } else if (searchValue[0] == '*') { + + searchValue = searchValue.substring(1); + return bookValue !== emptyFieldValue && bookValue.indexOf(searchValue) >= 0; + } else if (searchValue[0] == '#') { + + searchValue = searchValue.substring(1); + return !bookValue || (bookValue !== emptyFieldValue && !enru.has(bookValue[0]) && bookValue.indexOf(searchValue) >= 0); + } else { + //where = `@dirtyIndexLR('value', ${db.esc(a)}, ${db.esc(a + maxUtf8Char)})`; + return bookValue.localeCompare(searchValue) >= 0 && bookValue.localeCompare(searchValue + maxUtf8Char) <= 0; + } + }; + + return books.filter((book) => { + //author + let authorFound = false; + const authors = splitAuthor(book.author); + for (const a of authors) { + if (filterBySearch(a, s.author)) { + authorFound = true; + break; + } + } + + //genre + let genreFound = !s.genre; + if (!genreFound) { + const searchGenres = new Set(s.genre.split(',')); + const bookGenres = book.genre.split(','); + + for (let g of bookGenres) { + if (!g) + g = emptyFieldValue; + + if (searchGenres.has(g)) { + genreFound = true; + break; + } + } + } + + //lang + let langFound = !s.lang; + if (!langFound) { + const searchLang = new Set(s.lang.split(',')); + langFound = searchLang.has(book.lang || emptyFieldValue); + } + + //date + let dateFound = !s.date; + if (!dateFound) { + const date = this.queryDate(s.date).split(','); + let [from = '0000-00-00', to = '9999-99-99'] = date; + + dateFound = (book.date >= from && book.date <= to); + } + + //librate + let librateFound = !s.librate; + if (!librateFound) { + const searchLibrate = new Set(s.librate.split(',').map(n => parseInt(n, 10)).filter(n => !isNaN(n))); + librateFound = searchLibrate.has(book.librate); + } + + return (this.showDeleted || !book.del) + && authorFound + && filterBySearch(book.series, s.series) + && filterBySearch(book.title, s.title) + && genreFound + && langFound + && dateFound + && librateFound + ; + }); + } + } module.exports = BasePage; \ No newline at end of file diff --git a/server/core/opds/BookPage.js b/server/core/opds/BookPage.js new file mode 100644 index 0000000..469e5db --- /dev/null +++ b/server/core/opds/BookPage.js @@ -0,0 +1,37 @@ +const BasePage = require('./BasePage'); + +class BookPage extends BasePage { + constructor(config) { + super(config); + + this.id = 'book'; + this.title = 'Книга'; + } + + async body(req) { + const result = {}; + + const bookUid = req.query.uid; + const entry = []; + if (bookUid) { + const {bookInfo} = await this.webWorker.getBookInfo(bookUid); + if (bookInfo) { + entry.push( + this.makeEntry({ + id: bookUid, + title: bookInfo.book.title || 'Без названия', + link: [ + //this.imgLink({href: bookInfo.cover, type: coverType}), + this.acqLink({href: bookInfo.link, type: `application/${bookInfo.book.ext}+gzip`}), + ], + }) + ); + } + } + + result.entry = entry; + return this.makeBody(result); + } +} + +module.exports = BookPage; \ No newline at end of file diff --git a/server/core/opds/index.js b/server/core/opds/index.js index 6f80b74..95c1ede 100644 --- a/server/core/opds/index.js +++ b/server/core/opds/index.js @@ -1,5 +1,6 @@ const RootPage = require('./RootPage'); const AuthorPage = require('./AuthorPage'); +const BookPage = require('./BookPage'); module.exports = function(app, config) { const opdsRoot = '/opds'; @@ -7,11 +8,13 @@ module.exports = function(app, config) { const root = new RootPage(config); const author = new AuthorPage(config); + const book = new BookPage(config); const routes = [ ['', root], ['/root', root], ['/author', author], + ['/book', book], ]; const pages = new Map(); @@ -35,6 +38,9 @@ module.exports = function(app, config) { } } catch (e) { res.status(500).send({error: e.message}); + if (config.branch == 'development') { + console.error({error: e.message, url: req.originalUrl}); + } } };