From a8ed8b29e581ddeb329a05841cc99a0ec71eab1e Mon Sep 17 00:00:00 2001 From: Book Pauk Date: Wed, 23 Nov 2022 17:03:33 +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/opds/AuthorPage.js | 3 +- server/core/opds/BasePage.js | 66 +++++++++---- server/core/opds/BookPage.js | 171 ++++++++++++++++++++++++++++++++- 3 files changed, 214 insertions(+), 26 deletions(-) diff --git a/server/core/opds/AuthorPage.js b/server/core/opds/AuthorPage.js index 2ad07e9..fb31546 100644 --- a/server/core/opds/AuthorPage.js +++ b/server/core/opds/AuthorPage.js @@ -85,7 +85,7 @@ class AuthorPage extends BasePage { if (query.series) { //книги по серии const bookList = await this.webWorker.getSeriesBookList(query.series); - + if (bookList.books) { let books = JSON.parse(bookList.books); const filtered = (query.all ? books : this.filterBooks(books, query)); @@ -96,6 +96,7 @@ class AuthorPage extends BasePage { if (query.all) { title = `${this.bookAuthor(book.author)} "${title}"`; } + title += ` (${book.ext})`; entry.push( this.makeEntry({ diff --git a/server/core/opds/BasePage.js b/server/core/opds/BasePage.js index 08a1614..645e279 100644 --- a/server/core/opds/BasePage.js +++ b/server/core/opds/BasePage.js @@ -58,7 +58,7 @@ class BasePage { acqLink(attrs) { return this.makeLink({ - href: this.opdsRoot + (attrs.href || ''), + href: (attrs.hrefAsIs ? attrs.href : `${this.opdsRoot}${attrs.href || ''}`), rel: attrs.rel || 'subsection', type: 'application/atom+xml;profile=opds-catalog;kind=acquisition', }); @@ -143,41 +143,43 @@ class BasePage { for (const row of queryRes.found) count += row.count; - if (count <= query.limit) - return await this.search(from, query); - - const result = []; const others = []; - const names = new Set(); - for (const row of queryRes.found) { - const name = row.name.toUpperCase(); + let result = []; + if (count <= query.limit) { + result = await this.search(from, query); + } else { + const names = new Set(); + 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); + 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); } - names.add(name); } } 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) + if (!query.others && others.length) result.push({id: 'other', title: 'Все остальные', q: '___others'}); return (!query.others ? result : others); @@ -291,6 +293,28 @@ class BasePage { }); } + async getGenres() { + let result; + if (!this.genres) { + const res = await this.webWorker.getGenreTree(); + + result = { + genreTree: res.genreTree, + genreMap: new Map(), + }; + + for (const section of result.genreTree) { + for (const g of section.value) + result.genreMap.set(g.value, g.name); + } + + this.genres = result; + } else { + result = this.genres; + } + + return result; + } } module.exports = BasePage; \ No newline at end of file diff --git a/server/core/opds/BookPage.js b/server/core/opds/BookPage.js index 88db7d9..c765d1d 100644 --- a/server/core/opds/BookPage.js +++ b/server/core/opds/BookPage.js @@ -1,5 +1,10 @@ const path = require('path'); +const _ = require('lodash'); +const he = require('he'); +const dayjs = require('dayjs'); + const BasePage = require('./BasePage'); +const Fb2Parser = require('../fb2/Fb2Parser'); class BookPage extends BasePage { constructor(config) { @@ -7,24 +12,181 @@ class BookPage extends BasePage { this.id = 'book'; this.title = 'Книга'; + + } + + formatSize(size) { + size = size/1024; + let unit = 'KB'; + if (size > 1024) { + size = size/1024; + unit = 'MB'; + } + return `${size.toFixed(1)} ${unit}`; + } + + inpxInfo(bookRec) { + const mapping = [ + {name: 'fileInfo', label: 'Информация о файле', value: [ + {name: 'folder', label: 'Папка'}, + {name: 'file', label: 'Файл'}, + {name: 'size', label: 'Размер'}, + {name: 'date', label: 'Добавлен'}, + {name: 'del', label: 'Удален'}, + {name: 'libid', label: 'LibId'}, + {name: 'insno', label: 'InsideNo'}, + ]}, + + {name: 'titleInfo', label: 'Общая информация', value: [ + {name: 'author', label: 'Автор(ы)'}, + {name: 'title', label: 'Название'}, + {name: 'series', label: 'Серия'}, + {name: 'genre', label: 'Жанр'}, + {name: 'librate', label: 'Оценка'}, + {name: 'lang', label: 'Язык книги'}, + {name: 'keywords', label: 'Ключевые слова'}, + ]}, + ]; + + const valueToString = (value, nodePath, b) => {//eslint-disable-line no-unused-vars + if (nodePath == 'fileInfo/file') + return `${value}.${b.ext}`; + + if (nodePath == 'fileInfo/size') + return `${this.formatSize(value)} (${value.toString().replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 ')} Bytes)`; + + if (nodePath == 'fileInfo/date') + return dayjs(value, 'YYYY-MM-DD').format('DD.MM.YYYY'); + + if (nodePath == 'fileInfo/del') + return (value ? 'Да' : null); + + if (nodePath == 'fileInfo/insno') + return (value ? value : null); + + if (nodePath == 'titleInfo/author') + return value.split(',').join(', '); + + if (nodePath == 'titleInfo/librate' && !value) + return null; + + if (typeof(value) === 'string') { + return value; + } + + return (value.toString ? value.toString() : ''); + }; + + let result = []; + const book = _.cloneDeep(bookRec); + book.series = [book.series, book.serno].filter(v => v).join(' #'); + + for (const item of mapping) { + const itemOut = {name: item.name, label: item.label, value: []}; + + for (const subItem of item.value) { + const subItemOut = { + name: subItem.name, + label: subItem.label, + value: valueToString(book[subItem.name], `${item.name}/${subItem.name}`, book) + }; + if (subItemOut.value) + itemOut.value.push(subItemOut); + } + + if (itemOut.value.length) + result.push(itemOut); + } + + return result; + } + + htmlInfo(title, infoList) { + let info = ''; + for (const part of infoList) { + if (part.value.length) + info += `

${part.label}

`; + for (const rec of part.value) + info += `

${rec.label}: ${rec.value}

`; + } + + if (info) + info = `

${title}

${info}`; + + return info; } async body(req) { const result = {}; + result.link = [ + this.navLink({rel: 'start'}), + this.acqLink({rel: 'self', href: req.originalUrl, hrefAsIs: true}), + ]; + const bookUid = req.query.uid; const entry = []; - if (bookUid) { + if (bookUid) { const {bookInfo} = await this.webWorker.getBookInfo(bookUid); + if (bookInfo) { + const {genreMap} = await this.getGenres(); + const fileFormat = `${bookInfo.book.ext}+zip`; + + //entry const e = this.makeEntry({ id: bookUid, title: bookInfo.book.title || 'Без названия', - link: [ - this.downLink({href: bookInfo.link, type: `application/${bookInfo.book.ext}+zip`}), - ], }); + e['dc:language'] = bookInfo.book.lang; + e['dc:format'] = fileFormat; + + //genre + const genre = bookInfo.book.genre.split(','); + for (const g of genre) { + const genreName = genreMap.get(g); + if (genreName) { + if (!e.category) + e.category = []; + e.category.push({ + '*ATTRS': {term: genreName, label: genreName}, + }); + } + } + + let content = ''; + let ann = ''; + let info = ''; + //fb2 info + if (bookInfo.fb2) { + const parser = new Fb2Parser(bookInfo.fb2); + const infoObj = parser.bookInfo(); + + if (infoObj.titleInfo) { + if (infoObj.titleInfo.author.length) { + e.author = infoObj.titleInfo.author.map(a => ({name: a})); + } + + ann = infoObj.titleInfo.annotationHtml || ''; + const infoList = parser.bookInfoList(infoObj); + info += this.htmlInfo('Fb2 инфо', infoList); + } + } + + //content + info += this.htmlInfo('Inpx инфо', this.inpxInfo(bookInfo.book)); + + content = `${ann}${info}`; + if (content) { + e.content = { + '*ATTRS': {type: 'text/html'}, + '*TEXT': he.escape(content), + }; + } + + //links + e.link = [ this.downLink({href: bookInfo.link, type: `application/${fileFormat}`}) ]; if (bookInfo.cover) { let coverType = 'image/jpeg'; if (path.extname(bookInfo.cover) == '.png') @@ -39,6 +201,7 @@ class BookPage extends BasePage { } result.entry = entry; + return this.makeBody(result, req); } }