From c82d888b5adab161897424f3581370064d541da3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Wed, 29 Sep 2021 10:16:38 -0500 Subject: [PATCH] Write metadata file option, rate limiting login attempts, generic failed login message --- .../components/modals/edit-tabs/Details.vue | 29 +- client/layouts/default.vue | 16 + client/package.json | 2 +- client/pages/config/index.vue | 7 +- client/pages/login.vue | 11 +- client/plugins/axios.js | 3 +- package-lock.json | 432 +----------------- package.json | 3 +- server/Auth.js | 32 +- server/Scanner.js | 35 ++ server/Server.js | 35 +- server/objects/Audiobook.js | 5 + server/objects/Book.js | 1 + server/objects/ServerSettings.js | 12 +- server/objects/User.js | 6 +- server/utils/fileUtils.js | 3 +- server/utils/nfoGenerator.js | 91 ++++ 17 files changed, 271 insertions(+), 452 deletions(-) create mode 100644 server/utils/nfoGenerator.js diff --git a/client/components/modals/edit-tabs/Details.vue b/client/components/modals/edit-tabs/Details.vue index e387becf..a2f03297 100644 --- a/client/components/modals/edit-tabs/Details.vue +++ b/client/components/modals/edit-tabs/Details.vue @@ -55,6 +55,11 @@
Remove + + + Save Metadata + +
Submit
@@ -87,7 +92,8 @@ export default { }, newTags: [], resettingProgress: false, - isScrollable: false + isScrollable: false, + savingMetadata: false } }, watch: { @@ -107,6 +113,9 @@ export default { this.$emit('update:processing', val) } }, + isRootUser() { + return this.$store.getters['user/getIsRoot'] + }, audiobookId() { return this.audiobook ? this.audiobook.id : null }, @@ -127,6 +136,24 @@ export default { } }, methods: { + saveMetadataComplete(result) { + this.savingMetadata = false + if (result.error) { + this.$toast.error(result.error) + } else if (result.audiobookId) { + var { savedPath } = result + if (!savedPath) { + this.$toast.error(`Failed to save metadata file (${result.audiobookId})`) + } else { + this.$toast.success(`Metadata file saved "${result.audiobookTitle}"`) + } + } + }, + saveMetadata() { + this.savingMetadata = true + this.$root.socket.once('save_metadata_complete', this.saveMetadataComplete) + this.$root.socket.emit('save_metadata', this.audiobookId) + }, async submitForm() { if (this.isProcessing) { return diff --git a/client/layouts/default.vue b/client/layouts/default.vue index b02037cf..92c6e9fb 100644 --- a/client/layouts/default.vue +++ b/client/layouts/default.vue @@ -127,6 +127,21 @@ export default { this.$store.commit('setScanProgress', progress) } }, + saveMetadataComplete(result) { + if (result.error) { + this.$toast.error(result.error) + } else if (result.audiobookId) { + var { savedPath } = result + if (!savedPath) { + this.$toast.error(`Failed to save metadata file (${result.audiobookId})`) + } else { + this.$toast.success(`Metadata file saved (${result.audiobookId})`) + } + } else { + var { success, failed } = result + this.$toast.success(`Metadata save complete\n${success} Succeeded\n${failed} Failed`) + } + }, userUpdated(user) { if (this.$store.state.user.user.id === user.id) { this.$store.commit('user/setUser', user) @@ -230,6 +245,7 @@ export default { this.socket.on('scan_start', this.scanStart) this.socket.on('scan_complete', this.scanComplete) this.socket.on('scan_progress', this.scanProgress) + // this.socket.on('save_metadata_complete', this.saveMetadataComplete) // Download Listeners this.socket.on('download_started', this.downloadStarted) diff --git a/client/package.json b/client/package.json index add3355a..d739811f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "1.2.7", + "version": "1.2.8", "description": "Audiobook manager and player", "main": "index.js", "scripts": { diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index cfd2a8db..9d2bbe30 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -50,11 +50,13 @@
Scan -
+
Scan for Covers
+ +
@@ -152,6 +154,9 @@ export default { scanCovers() { this.$root.socket.emit('scan_covers') }, + saveMetadataFiles() { + this.$root.socket.emit('save_metadata') + }, loadUsers() { this.$axios .$get('/api/users') diff --git a/client/pages/login.vue b/client/pages/login.vue index dc09e419..8df2b4b4 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -57,15 +57,14 @@ export default { password: this.password || '' } var authRes = await this.$axios.$post('/login', payload).catch((error) => { - console.error('Failed', error) + console.error('Failed', error.response) + if (error.response) this.error = error.response.data + else this.error = 'Unknown Error' return false }) - console.log('Auth res', authRes) - if (!authRes) { - this.error = 'Unknown Failure' - } else if (authRes.error) { + if (authRes && authRes.error) { this.error = authRes.error - } else { + } else if (authRes) { this.$store.commit('user/setUser', authRes.user) } this.processing = false diff --git a/client/plugins/axios.js b/client/plugins/axios.js index 492f678b..f6f7c548 100644 --- a/client/plugins/axios.js +++ b/client/plugins/axios.js @@ -16,6 +16,7 @@ export default function ({ $axios, store }) { $axios.onError(error => { const code = parseInt(error.response && error.response.status) - console.error('Axios error code', code) + const message = error.response ? error.response.data || 'Unknown Error' : 'Unknown Error' + console.error('Axios error', code, message) }) } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 5e7fd4e6..3cfeeb3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "1.2.4", + "version": "1.2.7", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -69,12 +69,6 @@ "@types/node": "*" } }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "optional": true - }, "aborter": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/aborter/-/aborter-1.1.0.tgz", @@ -89,23 +83,6 @@ "negotiator": "0.6.2" } }, - "adm-zip": { - "version": "0.4.16", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.4.16.tgz", - "integrity": "sha512-TFi4HBKSGfIKsK5YCkKaaFG2m4PEDyViZmEwof3MTIgzimHLto6muaHVpbrljdIvIrFZzEq/p4nafOeLcYegrg==" - }, - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "optional": true - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true - }, "archiver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.0.tgz", @@ -161,33 +138,6 @@ "is-primitive": "^3.0.1" } }, - "are-we-there-yet": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", - "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", - "optional": true, - "requires": { - "delegates": "^1.0.0", - "readable-stream": "^2.0.6" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "optional": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, "array-back": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", @@ -335,12 +285,6 @@ "responselike": "^2.0.0" } }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "optional": true - }, "clone-response": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", @@ -349,12 +293,6 @@ "mimic-response": "^1.0.0" } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "optional": true - }, "command-line-args": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.0.tgz", @@ -387,12 +325,6 @@ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, - "console-control-strings": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "optional": true - }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -485,23 +417,11 @@ } } }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "optional": true - }, "defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==" }, - "delegates": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "optional": true - }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -512,12 +432,6 @@ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" }, - "detect-libc": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "optional": true - }, "dicer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz", @@ -654,6 +568,11 @@ "busboy": "^0.3.1" } }, + "express-rate-limit": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.3.0.tgz", + "integrity": "sha512-qJhfEgCnmteSeZAeuOKQ2WEIFTX5ajrzE0xS6gCOBCoRQcU+xEzQmgYQQTpzCcqUAAzTEtu4YEih4pnLfvNtew==" + }, "finalhandler": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", @@ -715,36 +634,11 @@ "universalify": "^2.0.0" } }, - "fs-minipass": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", - "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "optional": true, - "requires": { - "minipass": "^2.6.0" - } - }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, - "gauge": { - "version": "2.7.4", - "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", - "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "optional": true, - "requires": { - "aproba": "^1.0.3", - "console-control-strings": "^1.0.0", - "has-unicode": "^2.0.0", - "object-assign": "^4.1.0", - "signal-exit": "^3.0.0", - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1", - "wide-align": "^1.1.0" - } - }, "get-stream": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", @@ -790,12 +684,6 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" }, - "has-unicode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "optional": true - }, "http-cache-semantics": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", @@ -835,15 +723,6 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, - "ignore-walk": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.4.tgz", - "integrity": "sha512-PY6Ii8o1jMRA1z4F2hRkH/xN59ox43DavKvD3oDpfurRlOJyAHpifIwpbdv1n4jt4ov0jSpw3kQ4GhJnpBL6WQ==", - "optional": true, - "requires": { - "minimatch": "^3.0.4" - } - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -858,12 +737,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "optional": true - }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -874,15 +747,6 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "optional": true, - "requires": { - "number-is-nan": "^1.0.0" - } - }, "is-primitive": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/is-primitive/-/is-primitive-3.0.1.tgz", @@ -1106,79 +970,11 @@ "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "optional": true - }, - "minipass": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", - "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "optional": true, - "requires": { - "safe-buffer": "^5.1.2", - "yallist": "^3.0.0" - } - }, - "minizlib": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", - "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "optional": true, - "requires": { - "minipass": "^2.9.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "optional": true, - "requires": { - "minimist": "^1.2.5" - } - }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, - "nan": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz", - "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", - "optional": true - }, - "needle": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", - "integrity": "sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==", - "optional": true, - "requires": { - "debug": "^3.2.6", - "iconv-lite": "^0.4.4", - "sax": "^1.2.4" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "optional": true, - "requires": { - "ms": "^2.1.1" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "optional": true - } - } - }, "negotiator": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", @@ -1200,34 +996,6 @@ "minimatch": "^3.0.2" } }, - "node-pre-gyp": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", - "integrity": "sha512-d1xFs+C/IPS8Id0qPTZ4bUT8wWryfR/OzzAFxweG+uLN85oPzyo2Iw6bVlLQ/JOdgNonXLCoRyqDzDWq4iw72A==", - "optional": true, - "requires": { - "detect-libc": "^1.0.2", - "mkdirp": "^0.5.1", - "needle": "^2.2.1", - "nopt": "^4.0.1", - "npm-packlist": "^1.1.6", - "npmlog": "^4.0.2", - "rc": "^1.2.7", - "rimraf": "^2.6.1", - "semver": "^5.3.0", - "tar": "^4" - } - }, - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "optional": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1238,50 +1006,6 @@ "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==" }, - "npm-bundled": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", - "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", - "optional": true, - "requires": { - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npm-normalize-package-bin": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "optional": true - }, - "npm-packlist": { - "version": "1.4.8", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz", - "integrity": "sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A==", - "optional": true, - "requires": { - "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1", - "npm-normalize-package-bin": "^1.0.1" - } - }, - "npmlog": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", - "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "optional": true, - "requires": { - "are-we-there-yet": "~1.1.2", - "console-control-strings": "~1.1.0", - "gauge": "~2.7.3", - "set-blocking": "~2.0.0" - } - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "optional": true - }, "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -1303,28 +1027,6 @@ "wrappy": "1" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "optional": true - }, - "os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "optional": true - }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "optional": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" - } - }, "p-cancelable": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", @@ -1422,18 +1124,6 @@ "unpipe": "1.0.0" } }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "optional": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -1470,15 +1160,6 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "optional": true, - "requires": { - "glob": "^7.1.3" - } - }, "ripstat": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ripstat/-/ripstat-1.1.1.tgz", @@ -1521,11 +1202,6 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" - }, "semver": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", @@ -1569,12 +1245,6 @@ "send": "0.17.1" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "optional": true - }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", @@ -1661,17 +1331,6 @@ "resolved": "https://registry.npmjs.org/string-indexes/-/string-indexes-1.0.0.tgz", "integrity": "sha512-RUlx+2YydZJNlRAvoh1siPYWj/Xfk6t1sQLkA5n1tMGRCKkRLzkRtJhHk4qRmKergEBh8R3pWhsUsDqia/bolw==" }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "optional": true, - "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" - } - }, "string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", @@ -1680,44 +1339,6 @@ "safe-buffer": "~5.1.0" } }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "optional": true - }, - "tar": { - "version": "4.4.19", - "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.19.tgz", - "integrity": "sha512-a20gEsvHnWe0ygBY8JbxoM4w3SJdhc7ZAuxkLqh+nvNQN2IOt0B5lLgM490X5Hl8FF0dl0tOf2ewFYAlIFgzVA==", - "optional": true, - "requires": { - "chownr": "^1.1.4", - "fs-minipass": "^1.2.7", - "minipass": "^2.9.0", - "minizlib": "^1.3.3", - "mkdirp": "^0.5.5", - "safe-buffer": "^5.2.1", - "yallist": "^3.1.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "optional": true - } - } - }, "tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -1803,15 +1424,6 @@ "isexe": "^2.0.0" } }, - "wide-align": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", - "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "optional": true, - "requires": { - "string-width": "^1.0.2 || 2" - } - }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -1827,26 +1439,6 @@ "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", "integrity": "sha1-eLpyAgApxbyHuKgaPPzXS0ovweU=" }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "optional": true - }, "zip-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.0.tgz", @@ -1856,16 +1448,6 @@ "compress-commons": "^4.1.0", "readable-stream": "^3.6.0" } - }, - "zipfile": { - "version": "0.5.12", - "resolved": "https://registry.npmjs.org/zipfile/-/zipfile-0.5.12.tgz", - "integrity": "sha512-zA60gW+XgQBu/Q4qV3BCXNIDRald6Xi5UOPj3jWGlnkjmBHaKDwIz7kyXWV3kq7VEsQN/2t/IWjdXdKeVNm6Eg==", - "optional": true, - "requires": { - "nan": "~2.10.0", - "node-pre-gyp": "~0.10.2" - } } } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 1b778307..daeccf60 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "1.2.7", + "version": "1.2.8", "description": "Self-hosted audiobook server for managing and playing audiobooks", "main": "index.js", "scripts": { @@ -29,6 +29,7 @@ "cookie-parser": "^1.4.5", "express": "^4.17.1", "express-fileupload": "^1.2.1", + "express-rate-limit": "^5.3.0", "fluent-ffmpeg": "^2.1.2", "fs-extra": "^10.0.0", "ip": "^1.1.5", diff --git a/server/Auth.js b/server/Auth.js index c3188f04..6cd175ec 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -103,18 +103,18 @@ class Auth { var user = this.users.find(u => u.username === username) - if (!user) { - return res.json({ error: 'User not found' }) - } - - if (!user.isActive) { - return res.json({ error: 'User unavailable' }) + if (!user || !user.isActive) { + Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`) + if (req.rateLimit.remaining <= 2) { + return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`) + } + return res.status(401).send('Invalid user or password') } // Check passwordless root user if (user.id === 'root' && (!user.pash || user.pash === '')) { if (password) { - return res.json({ error: 'Invalid root password (hint: there is none)' }) + return res.status(401).send('Invalid root password (hint: there is none)') } else { return res.json({ user: user.toJSONForBrowser() }) } @@ -127,12 +127,24 @@ class Auth { user: user.toJSONForBrowser() }) } else { - res.json({ - error: 'Invalid Password' - }) + Logger.debug(`[Auth] Failed login attempt ${req.rateLimit.current} of ${req.rateLimit.limit}`) + if (req.rateLimit.remaining <= 2) { + Logger.error(`[Auth] Failed login attempt for user ${user.username}. Attempts: ${req.rateLimit.current}`) + return res.status(401).send(`Invalid user or password (${req.rateLimit.remaining === 0 ? '1 attempt remaining' : `${req.rateLimit.remaining + 1} attempts remaining`})`) + } + return res.status(401).send('Invalid user or password') } } + // Not in use now + lockUser(user) { + user.isLocked = true + return this.db.updateEntity('user', user).catch((error) => { + Logger.error('[Auth] Failed to lock user', user.username, error) + return false + }) + } + comparePassword(password, user) { if (user.type === 'root' && !password && !user.pash) return true if (!password || !user.pash) return false diff --git a/server/Scanner.js b/server/Scanner.js index 5cb62dca..8e7fd411 100644 --- a/server/Scanner.js +++ b/server/Scanner.js @@ -13,6 +13,8 @@ class Scanner { constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) { this.AudiobookPath = AUDIOBOOK_PATH this.MetadataPath = METADATA_PATH + this.BookMetadataPath = Path.join(this.MetadataPath, 'books') + this.db = db this.emitter = emitter @@ -387,6 +389,39 @@ class Scanner { } } + async saveMetadata(audiobookId) { + if (audiobookId) { + var audiobook = this.db.audiobooks.find(ab => ab.id === audiobookId) + if (!audiobook) { + return { + error: 'Audiobook not found' + } + } + var savedPath = await audiobook.writeNfoFile() + return { + audiobookId, + audiobookTitle: audiobook.title, + savedPath + } + } else { + var response = { + success: 0, + failed: 0 + } + for (let i = 0; i < this.db.audiobooks.length; i++) { + var audiobook = this.db.audiobooks[i] + var savedPath = await audiobook.writeNfoFile() + if (savedPath) { + Logger.info(`[Scanner] Saved metadata nfo ${savedPath}`) + response.success++ + } else { + response.failed++ + } + } + return response + } + } + async find(req, res) { var method = req.params.method var query = req.query diff --git a/server/Server.js b/server/Server.js index ed9dafbb..245fa868 100644 --- a/server/Server.js +++ b/server/Server.js @@ -4,6 +4,7 @@ const http = require('http') const SocketIO = require('socket.io') const fs = require('fs-extra') const fileUpload = require('express-fileupload') +const rateLimit = require('express-rate-limit') const Auth = require('./Auth') const Watcher = require('./Watcher') @@ -110,6 +111,14 @@ class Server { this.scanner.cancelScan = true } + // Generates an NFO metadata file, if no audiobookId is passed then all audiobooks are done + async saveMetadata(socket, audiobookId = null) { + Logger.info('[Server] Starting save metadata files') + var response = await this.scanner.saveMetadata(audiobookId) + Logger.info(`[Server] Finished saving metadata files Successful: ${response.success}, Failed: ${response.failed}`) + socket.emit('save_metadata_complete', response) + } + async init() { Logger.info('[Server] Init') await this.streamManager.ensureStreamsDir() @@ -172,6 +181,21 @@ class Server { res.sendStatus(200) } + // First time login rate limit is hit + loginLimitReached(req, res, options) { + Logger.error(`[Server] Login rate limit (${options.max}) was hit for ip ${req.ip}`) + options.message = 'Too many attempts. Login temporarily locked.' + } + + getLoginRateLimiter() { + return rateLimit({ + windowMs: this.db.serverSettings.rateLimitLoginWindow, // 5 minutes + max: this.db.serverSettings.rateLimitLoginRequests, + skipSuccessfulRequests: true, + onLimitReached: this.loginLimitReached + }) + } + async start() { Logger.info('=== Starting Server ===') await this.init() @@ -206,13 +230,18 @@ class Server { app.use('/api', this.authMiddleware.bind(this), this.apiController.router) app.use('/hls', this.authMiddleware.bind(this), this.hlsController.router) + + // Incomplete work in progress // app.use('/ebook', this.ebookReader.router) - app.use('/feeds', this.rssFeeds.router) + // app.use('/feeds', this.rssFeeds.router) app.post('/upload', this.authMiddleware.bind(this), this.handleUpload.bind(this)) - app.post('/login', (req, res) => this.auth.login(req, res)) + var loginRateLimiter = this.getLoginRateLimiter() + app.post('/login', loginRateLimiter, (req, res) => this.auth.login(req, res)) + app.post('/logout', this.logout.bind(this)) + app.get('/ping', (req, res) => { Logger.info('Recieved ping') res.json({ success: true }) @@ -231,7 +260,6 @@ class Server { }) } - this.server.listen(this.Port, this.Host, () => { Logger.info(`Running on http://${this.Host}:${this.Port}`) }) @@ -259,6 +287,7 @@ class Server { socket.on('scan', this.scan.bind(this)) socket.on('scan_covers', this.scanCovers.bind(this)) socket.on('cancel_scan', this.cancelScan.bind(this)) + socket.on('save_metadata', (audiobookId) => this.saveMetadata(socket, audiobookId)) // Streaming socket.on('open_stream', (audiobookId) => this.streamManager.openStreamSocketRequest(socket, audiobookId)) diff --git a/server/objects/Audiobook.js b/server/objects/Audiobook.js index c2382251..e46a24d5 100644 --- a/server/objects/Audiobook.js +++ b/server/objects/Audiobook.js @@ -1,6 +1,7 @@ const Path = require('path') const { bytesPretty, elapsedPretty } = require('../utils/fileUtils') const { comparePaths, getIno } = require('../utils/index') +const nfoGenerator = require('../utils/nfoGenerator') const Logger = require('../Logger') const Book = require('./Book') const AudioTrack = require('./AudioTrack') @@ -530,5 +531,9 @@ class Audiobook { }) } } + + writeNfoFile(nfoFilename = 'metadata.nfo') { + return nfoGenerator(this, nfoFilename) + } } module.exports = Audiobook \ No newline at end of file diff --git a/server/objects/Book.js b/server/objects/Book.js index d83a906a..649d4dfd 100644 --- a/server/objects/Book.js +++ b/server/objects/Book.js @@ -1,3 +1,4 @@ +const fs = require('fs-extra') const Path = require('path') const Logger = require('../Logger') const parseAuthors = require('../utils/parseAuthors') diff --git a/server/objects/ServerSettings.js b/server/objects/ServerSettings.js index 73cd24f8..469c6156 100644 --- a/server/objects/ServerSettings.js +++ b/server/objects/ServerSettings.js @@ -3,10 +3,14 @@ const { CoverDestination } = require('../utils/constants') class ServerSettings { constructor(settings) { this.id = 'server-settings' + this.autoTagNew = false this.newTagExpireDays = 15 this.scannerParseSubtitle = false this.coverDestination = CoverDestination.METADATA + this.saveMetadataFile = false + this.rateLimitLoginRequests = 10 + this.rateLimitLoginWindow = 10 * 60 * 1000 // 10 Minutes if (settings) { this.construct(settings) @@ -18,6 +22,9 @@ class ServerSettings { this.newTagExpireDays = settings.newTagExpireDays this.scannerParseSubtitle = settings.scannerParseSubtitle this.coverDestination = settings.coverDestination || CoverDestination.METADATA + this.saveMetadataFile = !!settings.saveMetadataFile + this.rateLimitLoginRequests = !isNaN(settings.rateLimitLoginRequests) ? Number(settings.rateLimitLoginRequests) : 10 + this.rateLimitLoginWindow = !isNaN(settings.rateLimitLoginWindow) ? Number(settings.rateLimitLoginWindow) : 10 * 60 * 1000 // 10 Minutes } toJSON() { @@ -26,7 +33,10 @@ class ServerSettings { autoTagNew: this.autoTagNew, newTagExpireDays: this.newTagExpireDays, scannerParseSubtitle: this.scannerParseSubtitle, - coverDestination: this.coverDestination + coverDestination: this.coverDestination, + saveMetadataFile: !!this.saveMetadataFile, + rateLimitLoginRequests: this.rateLimitLoginRequests, + rateLimitLoginWindow: this.rateLimitLoginWindow } } diff --git a/server/objects/User.js b/server/objects/User.js index 9d6243f6..0a41d24e 100644 --- a/server/objects/User.js +++ b/server/objects/User.js @@ -9,6 +9,7 @@ class User { this.stream = null this.token = null this.isActive = true + this.isLocked = false this.createdAt = null this.audiobooks = null @@ -76,6 +77,7 @@ class User { token: this.token, audiobooks: this.audiobooksToJSON(), isActive: this.isActive, + isLocked: this.isLocked, createdAt: this.createdAt, settings: this.settings, permissions: this.permissions @@ -91,6 +93,7 @@ class User { token: this.token, audiobooks: this.audiobooksToJSON(), isActive: this.isActive, + isLocked: this.isLocked, createdAt: this.createdAt, settings: this.settings, permissions: this.permissions @@ -112,7 +115,8 @@ class User { } } } - this.isActive = (user.isActive === undefined || user.id === 'root') ? true : !!user.isActive + this.isActive = (user.isActive === undefined || user.type === 'root') ? true : !!user.isActive + this.isLocked = user.type === 'root' ? false : !!user.isLocked this.createdAt = user.createdAt || Date.now() this.settings = user.settings || this.getDefaultUserSettings() this.permissions = user.permissions || this.getDefaultUserPermissions() diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 37d9ecd9..3ba82257 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -29,9 +29,10 @@ function bytesPretty(bytes, decimals = 0) { return '0 Bytes' } const k = 1024 - const dm = decimals < 0 ? 0 : decimals + var dm = decimals < 0 ? 0 : decimals const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] const i = Math.floor(Math.log(bytes) / Math.log(k)) + if (i > 2 && dm === 0) dm = 1 return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] } module.exports.bytesPretty = bytesPretty diff --git a/server/utils/nfoGenerator.js b/server/utils/nfoGenerator.js new file mode 100644 index 00000000..9eafc56a --- /dev/null +++ b/server/utils/nfoGenerator.js @@ -0,0 +1,91 @@ +const fs = require('fs-extra') +const Path = require('path') +const { bytesPretty } = require('./fileUtils') +const Logger = require('../Logger') + +const LEFT_COL_LEN = 25 + +function sectionHeaderLines(title) { + return [title, ''.padEnd(10, '=')] +} + +function generateSection(sectionTitle, sectionData) { + var lines = sectionHeaderLines(sectionTitle) + for (const key in sectionData) { + var line = key.padEnd(LEFT_COL_LEN) + (sectionData[key] || '') + lines.push(line) + } + return lines +} + +async function generate(audiobook, nfoFilename = 'metadata.nfo') { + var jsonObj = audiobook.toJSON() + var book = jsonObj.book + + var generalSectionData = { + 'Title': book.title, + 'Subtitle': book.subtitle, + 'Author': book.author, + 'Narrator': book.narrarator, + 'Series': book.series, + 'Volume Number': book.volumeNumber, + 'Publish Year': book.publishYear, + 'Genre': book.genres ? book.genres.join(', ') : '', + 'Duration': audiobook.durationPretty, + 'Chapters': jsonObj.chapters.length + } + + if (!book.subtitle) { + delete generalSectionData['Subtitle'] + } + + if (!book.series) { + delete generalSectionData['Series'] + delete generalSectionData['Volume Number'] + } + + var tracks = audiobook.tracks + var audioTrack = tracks.length ? audiobook.tracks[0] : {} + + var totalBitrate = 0 + var numBitrates = 0 + for (let i = 0; i < tracks.length; i++) { + if (tracks[i].bitRate) { + totalBitrate += tracks[i].bitRate + numBitrates++ + } + } + var averageBitrate = numBitrates ? totalBitrate / numBitrates : 0 + + var mediaSectionData = { + 'Tracks': jsonObj.tracks.length, + 'Size': audiobook.sizePretty, + 'Codec': audioTrack.codec, + 'Ext': audioTrack.ext, + 'Channels': audioTrack.channels, + 'Channel Layout': audioTrack.channelLayout, + 'Average Bitrate': bytesPretty(averageBitrate) + } + + var bookSection = generateSection('Book Info', generalSectionData) + + var descriptionSection = null + if (book.description) { + descriptionSection = sectionHeaderLines('Book Description') + descriptionSection.push(book.description) + } + + var mediaSection = generateSection('Media Info', mediaSectionData) + + var fullFile = bookSection.join('\n') + '\n\n' + if (descriptionSection) fullFile += descriptionSection.join('\n') + '\n\n' + fullFile += mediaSection.join('\n') + + var nfoPath = Path.join(audiobook.fullPath, nfoFilename) + var relativePath = Path.join(audiobook.path, nfoFilename) + return fs.writeFile(nfoPath, fullFile).then(() => relativePath).catch((error) => { + Logger.error(`Failed to write nfo file ${error}`) + return false + }) +} +module.exports = generate \ No newline at end of file