Jelajahi Sumber

Работа над opds

Book Pauk 2 tahun lalu
induk
melakukan
a8ed8b29e5
3 mengubah file dengan 215 tambahan dan 27 penghapusan
  1. 2 1
      server/core/opds/AuthorPage.js
  2. 46 22
      server/core/opds/BasePage.js
  3. 167 4
      server/core/opds/BookPage.js

+ 2 - 1
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({

+ 46 - 22
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();
-
-            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);
+        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);
+                    }
+                    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;

+ 167 - 4
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 += `<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) {
         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);
     }
 }