Работа над opds
This commit is contained in:
@@ -85,7 +85,7 @@ class AuthorPage extends BasePage {
|
|||||||
if (query.series) {
|
if (query.series) {
|
||||||
//книги по серии
|
//книги по серии
|
||||||
const bookList = await this.webWorker.getSeriesBookList(query.series);
|
const bookList = await this.webWorker.getSeriesBookList(query.series);
|
||||||
|
|
||||||
if (bookList.books) {
|
if (bookList.books) {
|
||||||
let books = JSON.parse(bookList.books);
|
let books = JSON.parse(bookList.books);
|
||||||
const filtered = (query.all ? books : this.filterBooks(books, query));
|
const filtered = (query.all ? books : this.filterBooks(books, query));
|
||||||
@@ -96,6 +96,7 @@ class AuthorPage extends BasePage {
|
|||||||
if (query.all) {
|
if (query.all) {
|
||||||
title = `${this.bookAuthor(book.author)} "${title}"`;
|
title = `${this.bookAuthor(book.author)} "${title}"`;
|
||||||
}
|
}
|
||||||
|
title += ` (${book.ext})`;
|
||||||
|
|
||||||
entry.push(
|
entry.push(
|
||||||
this.makeEntry({
|
this.makeEntry({
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class BasePage {
|
|||||||
|
|
||||||
acqLink(attrs) {
|
acqLink(attrs) {
|
||||||
return this.makeLink({
|
return this.makeLink({
|
||||||
href: this.opdsRoot + (attrs.href || ''),
|
href: (attrs.hrefAsIs ? attrs.href : `${this.opdsRoot}${attrs.href || ''}`),
|
||||||
rel: attrs.rel || 'subsection',
|
rel: attrs.rel || 'subsection',
|
||||||
type: 'application/atom+xml;profile=opds-catalog;kind=acquisition',
|
type: 'application/atom+xml;profile=opds-catalog;kind=acquisition',
|
||||||
});
|
});
|
||||||
@@ -143,41 +143,43 @@ class BasePage {
|
|||||||
for (const row of queryRes.found)
|
for (const row of queryRes.found)
|
||||||
count += row.count;
|
count += row.count;
|
||||||
|
|
||||||
if (count <= query.limit)
|
|
||||||
return await this.search(from, query);
|
|
||||||
|
|
||||||
const result = [];
|
|
||||||
const others = [];
|
const others = [];
|
||||||
const names = new Set();
|
let result = [];
|
||||||
for (const row of queryRes.found) {
|
if (count <= query.limit) {
|
||||||
const name = row.name.toUpperCase();
|
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)) {
|
if (!names.has(name)) {
|
||||||
const rec = {
|
const rec = {
|
||||||
id: row.id,
|
id: row.id,
|
||||||
title: name.replace(/ /g, spaceChar),
|
title: name.replace(/ /g, spaceChar),
|
||||||
q: encodeURIComponent(row.name.toLowerCase()),
|
q: encodeURIComponent(row.name.toLowerCase()),
|
||||||
count: row.count,
|
count: row.count,
|
||||||
};
|
};
|
||||||
if (query.depth > 1 || enru.has(row.name[0].toLowerCase())) {
|
if (query.depth > 1 || enru.has(row.name[0].toLowerCase())) {
|
||||||
result.push(rec);
|
result.push(rec);
|
||||||
} else {
|
} else {
|
||||||
others.push(rec);
|
others.push(rec);
|
||||||
|
}
|
||||||
|
names.add(name);
|
||||||
}
|
}
|
||||||
names.add(name);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.depth > 1 && result.length == 1 && query[from]) {
|
if (query.depth > 1 && result.length == 1 && query[from]) {
|
||||||
const newQuery = _.cloneDeep(query);
|
const newQuery = _.cloneDeep(query);
|
||||||
newQuery[from] = decodeURIComponent(result[0].q);
|
newQuery[from] = decodeURIComponent(result[0].q);
|
||||||
|
|
||||||
if (newQuery[from].length >= query.depth) {
|
if (newQuery[from].length >= query.depth) {
|
||||||
newQuery.depth = newQuery[from].length + 1;
|
newQuery.depth = newQuery[from].length + 1;
|
||||||
return await this.opdsQuery(from, newQuery);
|
return await this.opdsQuery(from, newQuery);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!query.others && query.depth == 1)
|
if (!query.others && others.length)
|
||||||
result.push({id: 'other', title: 'Все остальные', q: '___others'});
|
result.push({id: 'other', title: 'Все остальные', q: '___others'});
|
||||||
|
|
||||||
return (!query.others ? result : 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;
|
module.exports = BasePage;
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const _ = require('lodash');
|
||||||
|
const he = require('he');
|
||||||
|
const dayjs = require('dayjs');
|
||||||
|
|
||||||
const BasePage = require('./BasePage');
|
const BasePage = require('./BasePage');
|
||||||
|
const Fb2Parser = require('../fb2/Fb2Parser');
|
||||||
|
|
||||||
class BookPage extends BasePage {
|
class BookPage extends BasePage {
|
||||||
constructor(config) {
|
constructor(config) {
|
||||||
@@ -7,24 +12,181 @@ class BookPage extends BasePage {
|
|||||||
|
|
||||||
this.id = 'book';
|
this.id = 'book';
|
||||||
this.title = 'Книга';
|
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 += `<h3>${part.label}</h3>`;
|
||||||
|
for (const rec of part.value)
|
||||||
|
info += `<p>${rec.label}: ${rec.value}</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info)
|
||||||
|
info = `<h2>${title}</h2>${info}`;
|
||||||
|
|
||||||
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
async body(req) {
|
async body(req) {
|
||||||
const result = {};
|
const result = {};
|
||||||
|
|
||||||
|
result.link = [
|
||||||
|
this.navLink({rel: 'start'}),
|
||||||
|
this.acqLink({rel: 'self', href: req.originalUrl, hrefAsIs: true}),
|
||||||
|
];
|
||||||
|
|
||||||
const bookUid = req.query.uid;
|
const bookUid = req.query.uid;
|
||||||
const entry = [];
|
const entry = [];
|
||||||
if (bookUid) {
|
if (bookUid) {
|
||||||
const {bookInfo} = await this.webWorker.getBookInfo(bookUid);
|
const {bookInfo} = await this.webWorker.getBookInfo(bookUid);
|
||||||
|
|
||||||
if (bookInfo) {
|
if (bookInfo) {
|
||||||
|
const {genreMap} = await this.getGenres();
|
||||||
|
const fileFormat = `${bookInfo.book.ext}+zip`;
|
||||||
|
|
||||||
|
//entry
|
||||||
const e = this.makeEntry({
|
const e = this.makeEntry({
|
||||||
id: bookUid,
|
id: bookUid,
|
||||||
title: bookInfo.book.title || 'Без названия',
|
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) {
|
if (bookInfo.cover) {
|
||||||
let coverType = 'image/jpeg';
|
let coverType = 'image/jpeg';
|
||||||
if (path.extname(bookInfo.cover) == '.png')
|
if (path.extname(bookInfo.cover) == '.png')
|
||||||
@@ -39,6 +201,7 @@ class BookPage extends BasePage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.entry = entry;
|
result.entry = entry;
|
||||||
|
|
||||||
return this.makeBody(result, req);
|
return this.makeBody(result, req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user