Ver código fonte

Merge branch 'release/1.5.3'

Book Pauk 2 anos atrás
pai
commit
636e34bdc1

+ 7 - 0
CHANGELOG.md

@@ -1,3 +1,10 @@
+1.5.3 / 2023-03-02
+
+- OPDS: исправление проблемы поиска для koreader
+- OPDS: улучшено скачивание для не-fb2 форматов файлов (djvu, pdf и пр.)
+- Добавлена полоска уведомления о выходе новой версии (отключается в настройках веб-интерфейса).
+  Проверка новой версии настраивается параметром checkReleaseLink в конфиге сервера (#15)
+
 1.5.2 / 2023-02-05
 
 - Исправление проблемы чтения каталога opds для koreader

+ 9 - 1
README.md

@@ -174,6 +174,13 @@ Options:
         "root": "/opds"
     },
 
+    // страница для скачивания свежего релиза
+    "latestReleaseLink": "https://github.com/bookpauk/inpx-web/releases/latest",
+
+    // api для проверки новой версии, 
+    // пустая строка - отключить проверку выхода новых версий
+    "checkReleaseLink": "https://api.github.com/repos/bookpauk/inpx-web/releases/latest",
+
     // настройки по умолчанию для веб-интерфейса
     // устанавливаются при первой загрузке страницы в браузере
     // дальнейшие изменения настроек с помощью веб-интерфейса уже сохраняются в самом браузере
@@ -188,7 +195,8 @@ Options:
         "showDeleted": false, // показывать удаленные
         "abCacheEnabled": true, // кешировать запросы
         "langDefault": "", // язык по умолчанию (например "ru,en")
-        "showJson": false // показывать JSON (в расширенном поиске)
+        "showJson": false, // показывать JSON (в расширенном поиске)
+        "showNewReleaseAvailable": true // уведомлять о выходе новой версии
     }
 }
 ```

+ 27 - 2
client/components/Search/Search.vue

@@ -3,6 +3,19 @@
         <div ref="scroller" class="col fit column no-wrap" style="overflow: auto; position: relative" @scroll="onScroll">
             <!-- Tool Panel begin -->
             <div ref="toolPanel" class="tool-panel column bg-cyan-2" style="position: sticky; top: 0; z-index: 10;">
+                <!-- Обновление -->
+                <div v-show="showNewReleaseAvailable && newReleaseAvailable" class="row q-py-sm bg-green-4 items-center">
+                    <div class="q-ml-sm" style="font-size: 120%">
+                        Доступна новая версия <b>{{ config.name }} v{{ config.latestVersion }}</b>
+                    </div>
+                    <DivBtn class="q-ml-sm q-px-sm bg-white" :size="20" @click.stop.prevent="openReleasePage">
+                        Скачать
+                    </DivBtn>
+                    <DivBtn class="q-ml-sm q-px-sm bg-white" :size="20" @click.stop.prevent="settingsDialogVisible = true">
+                        Отключить уведомление
+                    </DivBtn>
+                </div>
+
                 <!-- 1 -->
                 <div class="row">
                     <!-- 1-1 -->
@@ -504,6 +517,7 @@ class Search {
     limit = 20;
     extendedParams = false;
     showJson = false;
+    showNewReleaseAvailable = true;
 
     //stuff
     prevList = {};
@@ -555,6 +569,7 @@ class Search {
 
     mounted() {
         (async() => {
+            //для срабатывания watch.config
             await this.api.updateConfig();
 
             //устанавливаем uiDefaults от сервера, если это необходимо
@@ -604,6 +619,7 @@ class Search {
         this.abCacheEnabled = settings.abCacheEnabled;
         this.langDefault = settings.langDefault;
         this.showJson = settings.showJson;
+        this.showNewReleaseAvailable = settings.showNewReleaseAvailable;
     }
 
     recvMessage(d) {
@@ -631,6 +647,10 @@ class Search {
         return this.$store.state.config;
     }
 
+    get newReleaseAvailable() {
+        return (this.config.latestVersion && this.config.version != this.config.latestVersion);
+    }
+
     get recStruct() {
         if (this.config.dbConfig && this.config.dbConfig.inpxInfo.recStruct)
             return this.config.dbConfig.inpxInfo.recStruct;
@@ -728,14 +748,19 @@ class Search {
     }
 
     openReleasePage() {
-        window.open('https://github.com/bookpauk/inpx-web/releases', '_blank');
+        if (this.config.latestReleaseLink)
+            window.open(this.config.latestReleaseLink, '_blank');
     }
 
     makeProjectName() {
         const collection = this.config.dbConfig.inpxInfo.collection.split('\n');
         this.collection = collection[0].trim();
 
-        this.projectName = `${this.config.name} v${this.config.webAppVersion}`;
+        let projectName = `${this.config.name} v${this.config.webAppVersion}`;
+        if (this.newReleaseAvailable)
+            projectName += `, доступно обновление: v${this.config.latestVersion}`;
+
+        this.projectName = projectName;
         this.makeTitle();
     }
 

+ 10 - 0
client/components/Search/SettingsDialog/SettingsDialog.vue

@@ -19,6 +19,7 @@
                 />
             </div>
 
+            <q-checkbox v-show="config.latestVersion" v-model="showNewReleaseAvailable" size="36px" label="Уведомлять о выходе новой версии" />
             <q-checkbox v-model="downloadAsZip" size="36px" label="Скачивать книги в виде zip-архива" />
             <q-checkbox v-model="showCounts" size="36px" label="Показывать количество" />
             <q-checkbox v-model="showRates" size="36px" label="Показывать оценки" />
@@ -85,6 +86,9 @@ const componentOptions = {
         abCacheEnabled(newValue) {
             this.commit('setSettings', {'abCacheEnabled': newValue});
         },
+        showNewReleaseAvailable(newValue) {
+            this.commit('setSettings', {'showNewReleaseAvailable': newValue});
+        },
     }
 };
 class SettingsDialog {
@@ -105,6 +109,7 @@ class SettingsDialog {
     showDates = true;
     showDeleted = false;
     abCacheEnabled = true;
+    showNewReleaseAvailable = true;
 
     limitOptions = [
         {label: '10', value: 10},
@@ -125,6 +130,10 @@ class SettingsDialog {
     mounted() {
     }
 
+    get config() {
+        return this.$store.state.config;
+    }
+
     get settings() {
         return this.$store.state.settings;
     }
@@ -142,6 +151,7 @@ class SettingsDialog {
         this.showDates = settings.showDates;
         this.showDeleted = settings.showDeleted;
         this.abCacheEnabled = settings.abCacheEnabled;
+        this.showNewReleaseAvailable = settings.showNewReleaseAvailable;
     }
 
     okClick() {

+ 4 - 2
client/components/share/DivBtn.vue

@@ -89,8 +89,10 @@ export default vueComponent(DivBtn);
 }
 
 .button-pressed {
-    margin-left: 2px;
-    margin-top: 2px;
+    margin-left: 1px;
+    margin-top: 1px;
+    margin-right: -1px;
+    margin-bottom: -1px;
 }
 
 .clickable {

+ 1 - 0
client/store/root.js

@@ -21,6 +21,7 @@ const state = {
         abCacheEnabled: true,
         langDefault: '',
         showJson: false,
+        showNewReleaseAvailable: true,
     },    
 };
 

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "inpx-web",
-  "version": "1.5.2",
+  "version": "1.5.3",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "inpx-web",
-      "version": "1.5.2",
+      "version": "1.5.3",
       "hasInstallScript": true,
       "license": "CC0-1.0",
       "dependencies": {

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "inpx-web",
-  "version": "1.5.2",
+  "version": "1.5.3",
   "author": "Book Pauk <bookpauk@gmail.com>",
   "license": "CC0-1.0",
   "repository": "bookpauk/inpx-web",

+ 8 - 2
server/config/base.js

@@ -6,6 +6,7 @@ const execDir = path.resolve(__dirname, '..');
 module.exports = {
     branch: 'unknown',
     version: pckg.version,
+    latestVersion: '',
     name: pckg.name,
 
     execDir,
@@ -19,7 +20,7 @@ module.exports = {
     loggingEnabled: true,
 
     //поправить в случае, если были критические изменения в DbCreator или InpxParser
-    //иначе будет рассинхронизация между сервером и клиентом на уровне БД
+    //иначе будет рассинхронизация по кешу между сервером и клиентом на уровне БД
     dbVersion: '11',
     dbCacheSize: 5,
 
@@ -33,7 +34,7 @@ module.exports = {
     lowMemoryMode: false,
     fullOptimization: false,
 
-    webConfigParams: ['name', 'version', 'branch', 'bookReadLink', 'dbVersion', 'extendedSearch', 'uiDefaults'],
+    webConfigParams: ['name', 'version', 'latestVersion', 'branch', 'bookReadLink', 'dbVersion', 'extendedSearch', 'latestReleaseLink', 'uiDefaults'],
 
     allowRemoteLib: false,
     remoteLib: false,
@@ -57,6 +58,10 @@ module.exports = {
         password: '',
         root: '/opds',
     },
+
+    latestReleaseLink: 'https://github.com/bookpauk/inpx-web/releases/latest',
+    checkReleaseLink: 'https://api.github.com/repos/bookpauk/inpx-web/releases/latest',
+
     uiDefaults: {
         limit: 20,
         downloadAsZip: false,
@@ -69,6 +74,7 @@ module.exports = {
         abCacheEnabled: true,
         langDefault: '',
         showJson: false,
+        showNewReleaseAvailable: true,
     },
 };
 

+ 2 - 0
server/config/index.js

@@ -25,6 +25,8 @@ const propsToSave = [
     'remoteLib',
     'server',
     'opds',
+    'latestReleaseLink',
+    'checkReleaseLink',
     'uiDefaults',
 ];
 

+ 26 - 2
server/core/WebWorker.js

@@ -10,6 +10,7 @@ const DbCreator = require('./DbCreator');
 const DbSearcher = require('./DbSearcher');
 const InpxHashCreator = require('./InpxHashCreator');
 const RemoteLib = require('./RemoteLib');//singleton
+const FileDownloader = require('./FileDownloader');
 
 const asyncExit = new (require('./AsyncExit'))();
 const log = new (require('./AppLogger'))().log;//singleton
@@ -28,7 +29,8 @@ const stateToText = {
     [ssDbCreating]: 'Создание поисковой базы',
 };
 
-const cleanDirPeriod = 60*60*1000;//каждый час
+const cleanDirInterval = 60*60*1000;//каждый час
+const checkReleaseInterval = 2*60*60*1000;//каждые 2 часа
 
 //singleton
 let instance = null;
@@ -67,6 +69,7 @@ class WebWorker {
 
             this.periodicCleanDir(dirConfig);//no await
             this.periodicCheckInpx();//no await
+            this.periodicCheckNewRelease();//no await
 
             instance = this;
         }
@@ -638,7 +641,7 @@ class WebWorker {
             let lastCleanDirTime = 0;
             while (1) {// eslint-disable-line no-constant-condition
                 //чистка папок
-                if (Date.now() - lastCleanDirTime >= cleanDirPeriod) {
+                if (Date.now() - lastCleanDirTime >= cleanDirInterval) {
                     for (const config of dirConfig) {
                         try {
                             await this.cleanDir(config);
@@ -690,6 +693,27 @@ class WebWorker {
             await utils.sleep(inpxCheckInterval*60*1000);
         }
     }
+
+    async periodicCheckNewRelease() {
+        const checkReleaseLink = this.config.checkReleaseLink;
+        if (!checkReleaseLink)
+            return;
+        const down = new FileDownloader(1024*1024);
+
+        while (1) {// eslint-disable-line no-constant-condition
+            try {
+                let release = await down.load(checkReleaseLink);
+                release = JSON.parse(release.toString());
+
+                if (release.tag_name)
+                    this.config.latestVersion = release.tag_name;
+            } catch(e) {
+                log(LM_ERR, `periodicCheckNewRelease: ${e.message}`);
+            }
+
+            await utils.sleep(checkReleaseInterval);
+        }
+    }
 }
 
 module.exports = WebWorker;

+ 18 - 9
server/core/opds/BookPage.js

@@ -130,15 +130,15 @@ class BookPage extends BasePage {
 
                 //format
                 const ext = bookInfo.book.ext;
-                let fileFormat = `${ext}+zip`;
-                let downHref = bookInfo.link;
+                const formats = {
+                    [`${ext}+zip`]: `${bookInfo.link}/zip`,
+                    [ext]: bookInfo.link,
+                };
 
                 if (ext === 'mobi') {
-                    fileFormat = 'x-mobipocket-ebook';
+                    formats['x-mobipocket-ebook'] = bookInfo.link;
                 } else if (ext == 'epub') {
-                    //
-                } else {
-                    downHref = `${bookInfo.link}/zip`;
+                    formats[`${ext}+zip`] = bookInfo.link;
                 }
 
                 //entry
@@ -147,8 +147,13 @@ class BookPage extends BasePage {
                     title: bookInfo.book.title || 'Без названия',
                 });
 
+                //author bookInfo
+                if (bookInfo.book.author) {
+                    e.author = bookInfo.book.author.split(',').map(a => ({name: a}));
+                }
+
                 e['dc:language'] = bookInfo.book.lang;
-                e['dc:format'] = fileFormat;
+                e['dc:format'] = ext;
 
                 //genre
                 const genre = bookInfo.book.genre.split(',');
@@ -172,7 +177,8 @@ class BookPage extends BasePage {
                     const infoObj = parser.bookInfo();
 
                     if (infoObj.titleInfo) {
-                        if (infoObj.titleInfo.author.length) {
+                        //author fb2Info
+                        if (!e.author && infoObj.titleInfo.author.length) {
                             e.author = infoObj.titleInfo.author.map(a => ({name: a}));
                         }
 
@@ -194,7 +200,10 @@ class BookPage extends BasePage {
                 }
 
                 //links
-                e.link = [ this.downLink({href: downHref, type: `application/${fileFormat}`}) ];
+                e.link = [];
+                for (const [fileFormat, downHref] of Object.entries(formats))
+                    e.link.push(this.downLink({href: downHref, type: `application/${fileFormat}`}));
+
                 if (bookInfo.cover) {
                     let coverType = 'image/jpeg';
                     if (path.extname(bookInfo.cover) == '.png')

+ 9 - 1
server/core/opds/SearchPage.js

@@ -1,5 +1,6 @@
 const BasePage = require('./BasePage');
 const utils = require('../utils');
+const iconv = require('iconv-lite');
 
 class SearchPage extends BasePage {
     constructor(config) {
@@ -28,7 +29,14 @@ class SearchPage extends BasePage {
 
                     const limit = 100;
                     const offset = (page - 1)*limit;
-                    const queryRes = await this.webWorker.search(from, {[from]: query.term, genre: query.genre, del: '0', offset, limit});
+
+                    const searchQuery = {[from]: query.term, genre: query.genre, del: '0', offset, limit};
+                    let queryRes = await this.webWorker.search(from, searchQuery);
+                    
+                    if (queryRes.totalFound === 0) { // не нашли ничего, проверим, может term в кодировке ISO-8859-1 (баг koreader)
+                        searchQuery[from] = iconv.encode(query.term, 'ISO-8859-1').toString();
+                        queryRes = await this.webWorker.search(from, searchQuery);
+                    }
 
                     const found = queryRes.found;