diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index 533ed14a..90c5c3b3 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -10,7 +10,7 @@
{{ title }}
- +Report bugs, request features, provide feedback, and contribute on github.
@@ -93,6 +94,7 @@ export default { storeCoversInAudiobookDir: false, updatingServerSettings: false, useSquareBookCovers: false, + isPurgingCache: false, newServerSettings: {} } }, @@ -209,6 +211,19 @@ export default { this.$toast.error('Failed to reset audiobooks - manually remove the /config/audiobooks folder') }) } + }, + async purgeCache() { + this.isPurgingCache = true + await this.$axios + .$post('/api/purgecache') + .then(() => { + this.$toast.success('Cache Purged!') + }) + .catch((error) => { + console.error('Failed to purge cache', error) + this.$toast.error('Failed to purge cache') + }) + this.isPurgingCache = false } }, mounted() { diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index 61d04852..8978e44a 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -6,8 +6,6 @@ Vue.directive('click-outside', vClickOutside.directive) Vue.prototype.$eventBus = new Vue() -Vue.prototype.$isDev = process.env.NODE_ENV !== 'production' - Vue.prototype.$dateDistanceFromNow = (unixms) => { if (!unixms) return '' return formatDistance(unixms, Date.now(), { addSuffix: true }) @@ -145,4 +143,5 @@ export { export default ({ app }, inject) => { app.$decode = decode app.$encode = encode + app.$isDev = process.env.NODE_ENV !== 'production' } \ No newline at end of file diff --git a/client/store/audiobooks.js b/client/store/audiobooks.js index 95e9f7a9..c84d8210 100644 --- a/client/store/audiobooks.js +++ b/client/store/audiobooks.js @@ -16,36 +16,17 @@ export const getters = { if (!bookItem) return placeholder var book = bookItem.book if (!book || !book.cover || book.cover === placeholder) return placeholder - var cover = book.cover // Absolute URL covers (should no longer be used) - if (cover.startsWith('http:') || cover.startsWith('https:')) return cover + if (book.cover.startsWith('http:') || book.cover.startsWith('https:')) return book.cover - // Server hosted covers - try { - // Ensure cover is refreshed if cached - var bookLastUpdate = book.lastUpdate || Date.now() - var userToken = rootGetters['user/getToken'] + var userToken = rootGetters['user/getToken'] + var bookLastUpdate = book.lastUpdate || Date.now() - cover = cover.replace(/\\/g, '/') - - // Map old covers to new format /s/book/{bookid}/* - if (cover.startsWith('/local')) { - cover = cover.replace('local', `s/book/${bookItem.id}`) - if (cover.includes(bookItem.path + '/')) { // Remove book path - cover = cover.replace(bookItem.path + '/', '') - } - } - - // Easier to replace these special characters then to encodeUriComponent of the filename - var encodedCover = cover.replace(/%/g, '%25').replace(/#/g, '%23') - - var url = new URL(encodedCover, document.baseURI) - return url.href + `?token=${userToken}&ts=${bookLastUpdate}` - } catch (err) { - console.error(err) - return placeholder + if (process.env.NODE_ENV !== 'production') { // Testing + return `http://localhost:3333/api/books/${bookItem.id}/cover?token=${userToken}&ts=${bookLastUpdate}` } + return `/api/books/${bookItem.id}/cover?token=${userToken}` } } diff --git a/package-lock.json b/package-lock.json index 781a1988..68de2efa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "1.6.30", + "version": "1.6.39", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -105,14 +105,12 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "optional": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "optional": true + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "archiver": { "version": "5.3.0", @@ -173,7 +171,6 @@ "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" @@ -183,7 +180,6 @@ "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", @@ -346,8 +342,7 @@ "chownr": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "optional": true + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" }, "clone-response": { "version": "1.0.2", @@ -360,8 +355,38 @@ "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 + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" + }, + "color": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/color/-/color-4.1.0.tgz", + "integrity": "sha512-o2rkkxyLGgYoeUy1OodXpbPAQNmlNBrirQ8ODO8QutzDiDMNdezSOZLNnusQ6pUpCQJUsaJIo9DZJKqa2HgH7A==", + "requires": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-string": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.0.tgz", + "integrity": "sha512-9Mrz2AQLefkH1UvASKj6v6hj/7eWgjnT/cVsR8CumieLoT+g900exWeNogqtweI8dxloXN9BDQTYro1oWu/5CQ==", + "requires": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } }, "command-line-args": { "version": "5.2.0", @@ -398,8 +423,7 @@ "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 + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "content-disposition": { "version": "0.5.3", @@ -492,8 +516,7 @@ "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 + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "defer-to-connect": { "version": "2.0.1", @@ -503,8 +526,7 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "optional": true + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "depd": { "version": "1.1.2", @@ -519,8 +541,7 @@ "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "optional": true + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, "dicer": { "version": "0.3.0", @@ -623,6 +644,11 @@ "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" }, + "expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" + }, "express": { "version": "4.17.1", "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", @@ -762,7 +788,6 @@ "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", @@ -782,6 +807,11 @@ "pump": "^3.0.0" } }, + "github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha1-l/tdlr/eiXMxPyDoKI75oWf6ZM4=" + }, "glob": { "version": "7.1.7", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", @@ -822,8 +852,7 @@ "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 + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "html-entities": { "version": "2.3.2", @@ -903,8 +932,7 @@ "ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "optional": true + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, "ip": { "version": "1.1.5", @@ -916,11 +944,15 @@ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" }, + "is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" + }, "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" } @@ -1117,6 +1149,21 @@ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==" }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + }, + "dependencies": { + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -1166,8 +1213,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 + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minipass": { "version": "2.9.0", @@ -1197,6 +1243,11 @@ "minimist": "^1.2.5" } }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "moment": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", @@ -1221,6 +1272,11 @@ "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", "optional": true }, + "napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "needle": { "version": "2.9.1", "resolved": "https://registry.npmjs.org/needle/-/needle-2.9.1.tgz", @@ -1262,6 +1318,29 @@ "proper-lockfile": "^4.1.2" } }, + "node-abi": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.5.0.tgz", + "integrity": "sha512-LtHvNIBgOy5mO8mPEUtkCW/YCRWYEKshIvqhe1GHHyXEHEB5mgICyYnAcl4qan3uFeRROErKGzatFHPf6kDxWw==", + "requires": { + "semver": "^7.3.5" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "node-addon-api": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.2.0.tgz", + "integrity": "sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==" + }, "node-cron": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.0.tgz", @@ -1343,7 +1422,6 @@ "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", @@ -1354,8 +1432,7 @@ "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 + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", @@ -1443,6 +1520,26 @@ "rss": "^1.2.2" } }, + "prebuild-install": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.0.0.tgz", + "integrity": "sha512-IvSenf33K7JcgddNz2D5w521EgO+4aMMjFt73Uk9FRzQ7P+QZPKrp7qPsDydsSwjGt3T5xRNnM1bj1zMTD5fTA==", + "requires": { + "detect-libc": "^1.0.3", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "npmlog": "^4.0.1", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + } + }, "printj": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", @@ -1554,7 +1651,6 @@ "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", @@ -1719,19 +1815,66 @@ "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 + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" }, + "sharp": { + "version": "0.29.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.29.3.tgz", + "integrity": "sha512-fKWUuOw77E4nhpyzCCJR1ayrttHoFHBT2U/kR/qEMRhvPEcluG4BKj324+SCO1e84+knXHwhJ1HHJGnUt4ElGA==", + "requires": { + "color": "^4.0.1", + "detect-libc": "^1.0.3", + "node-addon-api": "^4.2.0", + "prebuild-install": "^7.0.0", + "semver": "^7.3.5", + "simple-get": "^4.0.0", + "tar-fs": "^2.1.1", + "tunnel-agent": "^0.6.0" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==" }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.0.tgz", + "integrity": "sha512-ZalZGexYr3TA0SwySsr5HlgOOinS4Jsa8YB2GJ6lUNAazyAu4KG/VmzMTwAt2YVXzzVj8QmefmAonZIK2BSGcQ==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "requires": { + "is-arrayish": "^0.3.1" + } + }, "socket.io": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.1.3.tgz", @@ -1853,7 +1996,6 @@ "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", @@ -1872,7 +2014,6 @@ "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" } @@ -1880,8 +2021,7 @@ "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 + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "tar": { "version": "4.4.19", @@ -1906,6 +2046,17 @@ } } }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, "tar-stream": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", @@ -1931,6 +2082,14 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "requires": { + "safe-buffer": "^5.0.1" + } + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -1995,7 +2154,6 @@ "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" } diff --git a/package.json b/package.json index acfeb316..586be54c 100644 --- a/package.json +++ b/package.json @@ -52,4 +52,4 @@ "xml2js": "^0.4.23" }, "devDependencies": {} -} \ No newline at end of file +} diff --git a/server/ApiController.js b/server/ApiController.js index 7f423bf9..f1da5357 100644 --- a/server/ApiController.js +++ b/server/ApiController.js @@ -5,7 +5,6 @@ const date = require('date-and-time') const Logger = require('./Logger') const { isObject } = require('./utils/index') -const resize = require('./utils/resizeImage') const audioFileScanner = require('./utils/audioFileScanner') const BookController = require('./controllers/BookController') @@ -19,7 +18,7 @@ const BookFinder = require('./BookFinder') const AuthorFinder = require('./AuthorFinder') class ApiController { - constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, emitter, clientEmitter) { + constructor(MetadataPath, db, scanner, auth, streamManager, rssFeeds, downloadManager, coverController, backupManager, watcher, cacheManager, emitter, clientEmitter) { this.db = db this.scanner = scanner this.auth = auth @@ -29,6 +28,7 @@ class ApiController { this.backupManager = backupManager this.coverController = coverController this.watcher = watcher + this.cacheManager = cacheManager this.emitter = emitter this.clientEmitter = clientEmitter this.MetadataPath = MetadataPath @@ -62,12 +62,10 @@ class ApiController { this.router.get('/libraries/:id/authors', LibraryController.middleware.bind(this), LibraryController.getAuthors.bind(this)) this.router.post('/libraries/order', LibraryController.reorder.bind(this)) - // TEMP: Support old syntax for mobile app this.router.get('/library/:id/audiobooks', LibraryController.middleware.bind(this), LibraryController.getBooksForLibrary.bind(this)) this.router.get('/library/:id/search', LibraryController.middleware.bind(this), LibraryController.search.bind(this)) - // // Book Routes // @@ -83,7 +81,7 @@ class ApiController { this.router.patch('/books/:id/tracks', BookController.updateTracks.bind(this)) this.router.get('/books/:id/stream', BookController.openStream.bind(this)) this.router.post('/books/:id/cover', BookController.uploadCover.bind(this)) - this.router.get('/books/:id/cover', this.resizeCover.bind(this)) + this.router.get('/books/:id/cover', BookController.getCover.bind(this)) this.router.patch('/books/:id/coverfile', BookController.updateCoverFromFile.bind(this)) // TEMP: Support old syntax for mobile app @@ -91,7 +89,6 @@ class ApiController { this.router.get('/audiobook/:id', BookController.findOne.bind(this)) this.router.get('/audiobook/:id/stream', BookController.openStream.bind(this)) - // // User Routes // @@ -104,7 +101,6 @@ class ApiController { this.router.get('/users/:id/listening-sessions', UserController.getListeningStats.bind(this)) this.router.get('/users/:id/listening-stats', UserController.getListeningStats.bind(this)) - // // Collection Routes // @@ -123,7 +119,6 @@ class ApiController { this.router.get('/collection/:id', CollectionController.findOne.bind(this)) this.router.delete('/collection/:id/book/:bookId', CollectionController.removeBook.bind(this)) - // // Current User Routes (Me) // @@ -140,7 +135,6 @@ class ApiController { this.router.patch('/user/audiobook/:id', MeController.updateAudiobookData.bind(this)) this.router.patch('/user/settings', MeController.updateSettings.bind(this)) - // // Backup Routes // @@ -176,31 +170,10 @@ class ApiController { this.router.get('/scantracks/:id', this.scanAudioTrackNums.bind(this)) this.router.post('/syncUserAudiobookData', this.syncUserAudiobookData.bind(this)) + + this.router.post('/purgecache', this.purgeCache.bind(this)) } - async resizeCover(req, res) { - let { query: { width, height }, params: { id }, user } = req; - if (!user) { - return res.sendStatus(403) - } - var audiobook = this.db.audiobooks.find(a => a.id === id) - if (!audiobook) return res.sendStatus(404) - - // Check user can access this audiobooks library - if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) { - return res.sendStatus(403) - } - - - res.type('image/jpeg'); - - if (width) width = parseInt(width) - if (height) height = parseInt(height) - - return resize(audiobook.book.coverFullPath, width, height).pipe(res) - } - - async findBooks(req, res) { var provider = req.query.provider || 'google' var title = req.query.title || '' @@ -485,6 +458,11 @@ class ApiController { this.clientEmitter(collection.userId, 'collection_updated', collection.toJSONExpanded(this.db.audiobooks)) } + // purge cover cache + if (audiobook.cover) { + await this.cacheManager.purgeCoverCache(audiobook.id) + } + var audiobookJSON = audiobook.toJSONMinified() await this.db.removeEntity('audiobook', audiobook.id) this.emitter('audiobook_removed', audiobookJSON) @@ -527,5 +505,14 @@ class ApiController { }) return listeningStats } + + async purgeCache(req, res) { + if (!req.user.isRoot) { + return res.sendStatus(403) + } + Logger.info(`[ApiController] Purging all cache`) + await this.cacheManager.purgeAll() + res.sendStatus(200) + } } module.exports = ApiController \ No newline at end of file diff --git a/server/CacheManager.js b/server/CacheManager.js new file mode 100644 index 00000000..c31a25f9 --- /dev/null +++ b/server/CacheManager.js @@ -0,0 +1,75 @@ +const Path = require('path') +const fs = require('fs-extra') +const stream = require('stream') +const resize = require('./utils/resizeImage') +const Logger = require('./Logger') + +class CacheManager { + constructor(MetadataPath) { + this.MetadataPath = MetadataPath + this.CachePath = Path.join(this.MetadataPath, 'cache') + this.CoverCachePath = Path.join(this.CachePath, 'covers') + } + + async handleCoverCache(res, audiobook, options = {}) { + const format = options.format || 'webp' + const width = options.width || 400 + const height = options.height || null + + res.type(`image/${format}`) + + var path = Path.join(this.CoverCachePath, audiobook.id) + '.' + format + + // Cache exists + if (await fs.pathExists(path)) { + const r = fs.createReadStream(path) + const ps = new stream.PassThrough() + stream.pipeline(r, ps, (err) => { + if (err) { + console.log(err) + return res.sendStatus(400) + } + }) + return ps.pipe(res) + } + + // Write cache + await fs.ensureDir(this.CoverCachePath) + var readStream = resize(audiobook.book.coverFullPath, width, height, format) + var writeStream = fs.createWriteStream(path) + writeStream.on('error', (e) => { + Logger.error(`[CacheManager] Cache write error ${e.message}`) + }) + readStream.pipe(writeStream) + + readStream.pipe(res) + } + + purgeCoverCache(audiobookId) { + var basepath = Path.join(this.CoverCachePath, audiobookId) + // Remove both webp and jpg caches if exist + var webpPath = basepath + '.webp' + var jpgPath = basepath + '.jpg' + return Promise.all([this.removeCache(webpPath), this.removeCache(jpgPath)]) + } + + removeCache(path) { + if (!path) return false + return fs.pathExists(path).then((exists) => { + if (!exists) return false + return fs.unlink(path).then(() => true).catch((err) => { + Logger.error(`[CacheManager] Failed to remove cache "${path}"`, err) + return false + }) + }) + } + + async purgeAll() { + if (await fs.pathExists(this.CachePath)) { + await fs.remove(this.CachePath).catch((error) => { + Logger.error(`[CacheManager] Failed to remove cache dir "${this.CachePath}"`, error) + }) + } + } +} +module.exports = CacheManager \ No newline at end of file diff --git a/server/CoverController.js b/server/CoverController.js index 1b02de2e..18d81a8f 100644 --- a/server/CoverController.js +++ b/server/CoverController.js @@ -10,8 +10,10 @@ const { CoverDestination } = require('./utils/constants') const { downloadFile } = require('./utils/fileUtils') class CoverController { - constructor(db, MetadataPath, AudiobookPath) { + constructor(db, cacheManager, MetadataPath, AudiobookPath) { this.db = db + this.cacheManager = cacheManager + this.MetadataPath = MetadataPath.replace(/\\/g, '/') this.BookMetadataPath = Path.posix.join(this.MetadataPath, 'books') this.AudiobookPath = AudiobookPath @@ -115,6 +117,7 @@ class CoverController { } await this.removeOldCovers(fullPath, extname) + await this.cacheManager.purgeCoverCache(audiobook.id) Logger.info(`[CoverController] Uploaded audiobook cover "${coverPath}" for "${audiobook.title}"`) @@ -152,6 +155,7 @@ class CoverController { await fs.rename(temppath, coverFullPath) await this.removeOldCovers(fullPath, '.' + imgtype.ext) + await this.cacheManager.purgeCoverCache(audiobook.id) Logger.info(`[CoverController] Downloaded audiobook cover "${coverPath}" from url "${url}" for "${audiobook.title}"`) diff --git a/server/Server.js b/server/Server.js index 09dd4b63..6f1e125a 100644 --- a/server/Server.js +++ b/server/Server.js @@ -28,6 +28,7 @@ const StreamManager = require('./StreamManager') const RssFeeds = require('./RssFeeds') const DownloadManager = require('./DownloadManager') const CoverController = require('./CoverController') +const CacheManager = require('./CacheManager') class Server { constructor(PORT, UID, GID, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) { @@ -47,15 +48,16 @@ class Server { this.auth = new Auth(this.db) this.backupManager = new BackupManager(this.MetadataPath, this.Uid, this.Gid, this.db) this.logManager = new LogManager(this.MetadataPath, this.db) + this.cacheManager = new CacheManager(this.MetadataPath) this.watcher = new Watcher(this.AudiobookPath) - this.coverController = new CoverController(this.db, this.MetadataPath, this.AudiobookPath) + this.coverController = new CoverController(this.db, this.cacheManager, this.MetadataPath, this.AudiobookPath) this.scanner = new Scanner(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this)) this.scanner2 = new Scanner2(this.AudiobookPath, this.MetadataPath, this.db, this.coverController, this.emitter.bind(this)) this.streamManager = new StreamManager(this.db, this.MetadataPath, this.emitter.bind(this), this.clientEmitter.bind(this)) this.rssFeeds = new RssFeeds(this.Port, this.db) this.downloadManager = new DownloadManager(this.db, this.MetadataPath, this.AudiobookPath, this.emitter.bind(this)) - this.apiController = new ApiController(this.MetadataPath, this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.emitter.bind(this), this.clientEmitter.bind(this)) + this.apiController = new ApiController(this.MetadataPath, this.db, this.scanner, this.auth, this.streamManager, this.rssFeeds, this.downloadManager, this.coverController, this.backupManager, this.watcher, this.cacheManager, this.emitter.bind(this), this.clientEmitter.bind(this)) this.hlsController = new HlsController(this.db, this.scanner, this.auth, this.streamManager, this.emitter.bind(this), this.streamManager.StreamsPath) Logger.logManager = this.logManager diff --git a/server/StreamManager.js b/server/StreamManager.js index 29537fbe..236eed34 100644 --- a/server/StreamManager.js +++ b/server/StreamManager.js @@ -72,7 +72,7 @@ class StreamManager { if (!dirs || !dirs.length) return true await Promise.all(dirs.map(async (dirname) => { - if (dirname !== 'streams' && dirname !== 'books' && dirname !== 'downloads' && dirname !== 'backups' && dirname !== 'logs') { + if (dirname !== 'streams' && dirname !== 'books' && dirname !== 'downloads' && dirname !== 'backups' && dirname !== 'logs' && dirname !== 'cache') { var fullPath = Path.join(this.MetadataPath, dirname) Logger.warn(`Removing OLD Orphan Stream ${dirname}`) return fs.remove(fullPath) diff --git a/server/controllers/BookController.js b/server/controllers/BookController.js index 01e264c9..3fa22136 100644 --- a/server/controllers/BookController.js +++ b/server/controllers/BookController.js @@ -1,4 +1,5 @@ const Logger = require('../Logger') +const { reqSupportsWebp } = require('../utils/index') class BookController { constructor() { } @@ -38,6 +39,12 @@ class BookController { } var audiobook = this.db.audiobooks.find(a => a.id === req.params.id) if (!audiobook) return res.sendStatus(404) + + // Book has cover and update is removing cover then purge cache + if (audiobook.cover && req.body.book && (req.body.book.cover === '' || req.body.book.cover === null)) { + await this.cacheManager.purgeCoverCache(audiobook.id) + } + var hasUpdates = audiobook.update(req.body) if (hasUpdates) { await this.db.updateAudiobook(audiobook) @@ -222,5 +229,24 @@ class BookController { if (updated) res.status(200).send('Cover updated successfully') else res.status(200).send('No update was made to cover') } + + // GET api/books/:id/cover + async getCover(req, res) { + let { query: { width, height, format }, params: { id } } = req + var audiobook = this.db.audiobooks.find(a => a.id === id) + if (!audiobook || !audiobook.book.coverFullPath) return res.sendStatus(404) + + // Check user can access this audiobooks library + if (!req.user.checkCanAccessLibrary(audiobook.libraryId)) { + return res.sendStatus(403) + } + + const options = { + format: format || (reqSupportsWebp(req) ? 'webp' : 'jpg'), + height: height ? parseInt(height) : null, + width: width ? parseInt(width) : null + } + return this.cacheManager.handleCoverCache(res, audiobook, options) + } } module.exports = new BookController() \ No newline at end of file diff --git a/server/utils/index.js b/server/utils/index.js index 79b6a93a..43fbefa1 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -101,4 +101,9 @@ function secondsToTimestamp(seconds, includeMs = false) { } module.exports.secondsToTimestamp = secondsToTimestamp -module.exports.msToTimestamp = (ms, includeMs) => secondsToTimestamp(ms / 1000, includeMs) \ No newline at end of file +module.exports.msToTimestamp = (ms, includeMs) => secondsToTimestamp(ms / 1000, includeMs) + +module.exports.reqSupportsWebp = (req) => { + if (!req || !req.headers || !req.headers.accept) return false + return req.headers.accept.includes('image/webp') || req.headers.accept === '*/*' +} \ No newline at end of file diff --git a/server/utils/resizeImage.js b/server/utils/resizeImage.js index ec359725..aea5adff 100644 --- a/server/utils/resizeImage.js +++ b/server/utils/resizeImage.js @@ -1,13 +1,13 @@ const sharp = require('sharp') const fs = require('fs') -function resize(filePath, width, height) { +function resize(filePath, width, height, format = 'webp') { const readStream = fs.createReadStream(filePath); let sharpie = sharp() - sharpie.toFormat('jpeg') + sharpie.toFormat(format) if (width || height) { - sharpie.resize(width, height) + sharpie.resize(width, height, { withoutEnlargement: true }) } return readStream.pipe(sharpie)