From e2d869bb1971a633fc374b49c8b5e002504f965d Mon Sep 17 00:00:00 2001 From: Cassie Esposito Date: Thu, 19 May 2022 19:10:51 -0700 Subject: [PATCH 01/14] Added support for npm run watch to automatically reset server during development. --- package.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index fce51228..cfa5bad4 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,11 @@ "version": "2.0.14", "description": "Self-hosted audiobook and podcast server", "main": "index.js", + "watch": { + "dev": "server/{*,*/*}/*.js" + }, "scripts": { + "watch": "npm-watch", "dev": "node index.js", "start": "node index.js", "client": "cd client && npm install && npm run generate", @@ -53,5 +57,7 @@ "watcher": "^1.2.0", "xml2js": "^0.4.23" }, - "devDependencies": {} -} \ No newline at end of file + "devDependencies": { + "npm-watch": "^0.11.0" + } +} From 302870a101fd906f4f7ffb21c643eef84a999133 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 20 May 2022 15:55:03 -0500 Subject: [PATCH 02/14] Fix:Continue series shelf show next book in series #608 --- server/utils/libraryHelpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/utils/libraryHelpers.js b/server/utils/libraryHelpers.js index 459e28e4..46ea9d1f 100644 --- a/server/utils/libraryHelpers.js +++ b/server/utils/libraryHelpers.js @@ -418,7 +418,7 @@ module.exports = { books: [libraryItemJson], inProgress: bookInProgress, bookInProgressLastUpdate: bookInProgress ? mediaProgress.lastUpdate : null, - firstBookUnread: bookInProgress ? libraryItemJson : null + firstBookUnread: bookInProgress ? null : libraryItemJson } seriesMap[librarySeries.id] = series From 796602d1b215bdcbd06f2b71d90c7c137fa5a472 Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 20 May 2022 16:34:51 -0500 Subject: [PATCH 03/14] Add:Enable e-reader server setting to allow all users to access experimental e-reader #614 --- client/components/AudioPlayer.vue | 3 --- client/components/app/SideRail.vue | 3 --- client/components/app/StreamContainer.vue | 3 --- client/components/cards/GroupCard.vue | 5 ----- client/components/cards/LazyBookCard.vue | 7 ++++-- client/components/covers/GroupCover.vue | 3 --- client/components/modals/item/EditModal.vue | 3 +-- client/pages/audiobook/_id/edit.vue | 3 --- client/pages/config/index.vue | 25 ++++++++++++++++----- client/pages/config/users/_id.vue | 3 --- client/pages/item/_id/index.vue | 16 ++++++++----- server/objects/settings/ServerSettings.js | 15 ++++++------- 12 files changed, 44 insertions(+), 45 deletions(-) diff --git a/client/components/AudioPlayer.vue b/client/components/AudioPlayer.vue index 4bb39c7d..54bb1ec1 100644 --- a/client/components/AudioPlayer.vue +++ b/client/components/AudioPlayer.vue @@ -153,9 +153,6 @@ export default { }, currentChapterName() { return this.currentChapter ? this.currentChapter.title : '' - }, - showExperimentalFeatures() { - return this.$store.state.showExperimentalFeatures } }, methods: { diff --git a/client/components/app/SideRail.vue b/client/components/app/SideRail.vue index 46e1c4cd..c6b54c5a 100644 --- a/client/components/app/SideRail.vue +++ b/client/components/app/SideRail.vue @@ -89,9 +89,6 @@ export default { offsetTop() { return 64 }, - showExperimentalFeatures() { - return this.$store.state.showExperimentalFeatures - }, userIsAdminOrUp() { return this.$store.getters['user/getIsAdminOrUp'] }, diff --git a/client/components/app/StreamContainer.vue b/client/components/app/StreamContainer.vue index ea140a78..88049003 100644 --- a/client/components/app/StreamContainer.vue +++ b/client/components/app/StreamContainer.vue @@ -74,9 +74,6 @@ export default { } }, computed: { - showExperimentalFeatures() { - return this.$store.state.showExperimentalFeatures - }, coverAspectRatio() { return this.$store.getters['getServerSetting']('coverAspectRatio') }, diff --git a/client/components/cards/GroupCard.vue b/client/components/cards/GroupCard.vue index 5b31ed0e..ff12c561 100644 --- a/client/components/cards/GroupCard.vue +++ b/client/components/cards/GroupCard.vue @@ -109,19 +109,14 @@ export default { hasValidCovers() { var validCovers = this.bookItems.map((bookItem) => bookItem.media.coverPath) return !!validCovers.length - }, - showExperimentalFeatures() { - return this.$store.state.showExperimentalFeatures } }, methods: { mouseoverCard() { this.isHovering = true - // if (this.$refs.groupcover) this.$refs.groupcover.setHover(true) }, mouseleaveCard() { this.isHovering = false - // if (this.$refs.groupcover) this.$refs.groupcover.setHover(false) }, clickCard() { this.$emit('click', this.group) diff --git a/client/components/cards/LazyBookCard.vue b/client/components/cards/LazyBookCard.vue index dda73474..34b0be60 100644 --- a/client/components/cards/LazyBookCard.vue +++ b/client/components/cards/LazyBookCard.vue @@ -147,6 +147,9 @@ export default { showExperimentalFeatures() { return this.store.state.showExperimentalFeatures }, + enableEReader() { + return this.store.getters['getServerSetting']('enableEReader') + }, _libraryItem() { return this.libraryItem || {} }, @@ -287,13 +290,13 @@ export default { return this.store.getters['getlibraryItemIdStreaming'] === this.libraryItemId }, showReadButton() { - return !this.isSelectionMode && this.showExperimentalFeatures && !this.showPlayButton && this.hasEbook + return !this.isSelectionMode && !this.showPlayButton && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader) }, showPlayButton() { return !this.isSelectionMode && !this.isMissing && !this.isInvalid && !this.isStreaming && (this.numTracks || this.recentEpisode) }, showSmallEBookIcon() { - return !this.isSelectionMode && this.showExperimentalFeatures && this.hasEbook + return !this.isSelectionMode && this.hasEbook && (this.showExperimentalFeatures || this.enableEReader) }, isMissing() { return this._libraryItem.isMissing diff --git a/client/components/covers/GroupCover.vue b/client/components/covers/GroupCover.vue index 015e539f..671616f4 100644 --- a/client/components/covers/GroupCover.vue +++ b/client/components/covers/GroupCover.vue @@ -59,9 +59,6 @@ export default { if (this.bookCoverAspectRatio === 1) return this.width / (120 * 1.6 * 2) return this.width / 240 }, - showExperimentalFeatures() { - return this.store.state.showExperimentalFeatures - }, store() { return this.$store || this.$nuxt.$store }, diff --git a/client/components/modals/item/EditModal.vue b/client/components/modals/item/EditModal.vue index f4b33b9f..548ddf4e 100644 --- a/client/components/modals/item/EditModal.vue +++ b/client/components/modals/item/EditModal.vue @@ -64,8 +64,7 @@ export default { { id: 'manage', title: 'Manage', - component: 'modals-item-tabs-manage', - experimental: true + component: 'modals-item-tabs-manage' } ] } diff --git a/client/pages/audiobook/_id/edit.vue b/client/pages/audiobook/_id/edit.vue index d63e4eb8..887391d8 100644 --- a/client/pages/audiobook/_id/edit.vue +++ b/client/pages/audiobook/_id/edit.vue @@ -126,9 +126,6 @@ export default { } }, computed: { - showExperimentalFeatures() { - return this.$store.state.showExperimentalFeatures - }, media() { return this.libraryItem.media || {} }, diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index b7e57ca6..e678c29b 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -122,6 +122,20 @@

+ +
+

Experimental Feature Settings

+
+ +
+ + +

+ Enable e-reader for all users + info_outlined +

+
+
@@ -172,7 +186,9 @@

Experimental Features - info_outlined + + info_outlined +

@@ -207,6 +223,7 @@ export default { isPurgingCache: false, newServerSettings: {}, tooltips: { + experimentalFeatures: 'Features in development that could use your feedback and help testing. Click to open github discussion.', scannerDisableWatcher: 'Disables the automatic adding/updating of items when file changes are detected. *Requires server restart', scannerPreferOpfMetadata: 'OPF file metadata will be used for book details over folder names', scannerPreferAudioMetadata: 'Audio file ID3 meta tags will be used for book details over folder names', @@ -216,7 +233,8 @@ export default { bookshelfView: 'Alternative view without wooden bookshelf', storeCoverWithItem: 'By default covers are stored in /metadata/items, enabling this setting will store covers in your library item folder. Only one file named "cover" will be kept', storeMetadataWithItem: 'By default metadata files are stored in /metadata/items, enabling this setting will store metadata files in your library item folders. Uses .abs file extension', - coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers' + coverAspectRatio: 'Prefer to use square covers over standard 1.6:1 book covers', + enableEReader: 'E-reader is still a work in progress, but use this setting to open it up to all your users (or use the "Experimental Features" toggle below just for you)' }, showConfirmPurgeCache: false } @@ -229,9 +247,6 @@ export default { } }, computed: { - experimentalFeaturesTooltip() { - return 'Features in development that could use your feedback and help testing.' - }, serverSettings() { return this.$store.state.serverSettings }, diff --git a/client/pages/config/users/_id.vue b/client/pages/config/users/_id.vue index 5b953ff0..b44bd900 100644 --- a/client/pages/config/users/_id.vue +++ b/client/pages/config/users/_id.vue @@ -104,9 +104,6 @@ export default { bookCoverAspectRatio() { return this.coverAspectRatio === this.$constants.BookCoverAspectRatio.SQUARE ? 1 : 1.6 }, - showExperimentalFeatures() { - return this.$store.state.showExperimentalFeatures - }, username() { return this.user.username }, diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 2b25d0a1..12b9d9ee 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -135,7 +135,7 @@ {{ isMissing ? 'Missing' : 'Incomplete' }} - + auto_stories Read @@ -223,6 +223,12 @@ export default { } }, computed: { + showExperimentalFeatures() { + return this.$store.state.showExperimentalFeatures + }, + enableEReader() { + return this.store.getters['getServerSetting']('enableEReader') + }, userIsAdminOrUp() { return this.$store.getters['user/getIsAdminOrUp'] }, @@ -241,9 +247,6 @@ export default { isDeveloperMode() { return this.$store.state.developerMode }, - showExperimentalFeatures() { - return this.$store.state.showExperimentalFeatures - }, isPodcast() { return this.libraryItem.mediaType === 'podcast' }, @@ -262,6 +265,9 @@ export default { if (this.isPodcast) return this.podcastEpisodes.length return this.tracks.length }, + showReadButton() { + return this.ebookFile && (this.showExperimentalFeatures || this.enableEReader) + }, libraryId() { return this.libraryItem.libraryId }, @@ -342,7 +348,7 @@ export default { return this.media.ebookFile }, showExperimentalReadAlert() { - return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures + return !this.tracks.length && this.ebookFile && !this.showExperimentalFeatures && !this.enableEReader }, description() { return this.mediaMetadata.description || '' diff --git a/server/objects/settings/ServerSettings.js b/server/objects/settings/ServerSettings.js index e1e20e12..bea7b6ed 100644 --- a/server/objects/settings/ServerSettings.js +++ b/server/objects/settings/ServerSettings.js @@ -5,10 +5,6 @@ class ServerSettings { constructor(settings) { this.id = 'server-settings' - // Misc/Unused - this.autoTagNew = false - this.newTagExpireDays = 15 - // Scanner this.scannerParseSubtitle = false this.scannerFindCovers = false @@ -43,11 +39,16 @@ class ServerSettings { // Podcasts this.podcastEpisodeSchedule = '0 * * * *' // Every hour + // Sorting this.sortingIgnorePrefix = false this.sortingPrefixes = ['the', 'a'] + // Misc Flags this.chromecastEnabled = false + this.enableEReader = false + this.logLevel = Logger.logLevel + this.version = null if (settings) { @@ -56,8 +57,6 @@ class ServerSettings { } construct(settings) { - this.autoTagNew = settings.autoTagNew - this.newTagExpireDays = settings.newTagExpireDays this.scannerFindCovers = !!settings.scannerFindCovers this.scannerCoverProvider = settings.scannerCoverProvider || 'google' this.scannerParseSubtitle = settings.scannerParseSubtitle @@ -91,6 +90,7 @@ class ServerSettings { this.sortingIgnorePrefix = !!settings.sortingIgnorePrefix this.sortingPrefixes = settings.sortingPrefixes || ['the', 'a'] this.chromecastEnabled = !!settings.chromecastEnabled + this.enableEReader = !!settings.enableEReader this.logLevel = settings.logLevel || Logger.logLevel this.version = settings.version || null @@ -102,8 +102,6 @@ class ServerSettings { toJSON() { return { id: this.id, - autoTagNew: this.autoTagNew, - newTagExpireDays: this.newTagExpireDays, scannerFindCovers: this.scannerFindCovers, scannerCoverProvider: this.scannerCoverProvider, scannerParseSubtitle: this.scannerParseSubtitle, @@ -125,6 +123,7 @@ class ServerSettings { sortingIgnorePrefix: this.sortingIgnorePrefix, sortingPrefixes: [...this.sortingPrefixes], chromecastEnabled: this.chromecastEnabled, + enableEReader: this.enableEReader, logLevel: this.logLevel, version: this.version } From dabcad5ebd94ef4ce76cb5f41d2d6c66feb9733c Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 20 May 2022 16:47:00 -0500 Subject: [PATCH 04/14] Update experimental e-reader alert --- client/components/modals/item/tabs/Manage.vue | 7 +++++-- client/pages/config/index.vue | 2 +- client/pages/item/_id/index.vue | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/client/components/modals/item/tabs/Manage.vue b/client/components/modals/item/tabs/Manage.vue index de97ec49..644d3f44 100644 --- a/client/components/modals/item/tabs/Manage.vue +++ b/client/components/modals/item/tabs/Manage.vue @@ -26,7 +26,7 @@ -
+

Split M4B to MP3's

@@ -51,7 +51,7 @@
-
+

Embed Metadata

@@ -113,6 +113,9 @@ export default { } }, computed: { + showExperimentalFeatures() { + return this.$store.state.showExperimentalFeatures + }, libraryItemId() { return this.libraryItem ? this.libraryItem.id : null }, diff --git a/client/pages/config/index.vue b/client/pages/config/index.vue index e678c29b..87a5d969 100644 --- a/client/pages/config/index.vue +++ b/client/pages/config/index.vue @@ -183,7 +183,7 @@
- +

Experimental Features diff --git a/client/pages/item/_id/index.vue b/client/pages/item/_id/index.vue index 12b9d9ee..c41087dc 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -92,7 +92,8 @@

@@ -227,7 +228,7 @@ export default { return this.$store.state.showExperimentalFeatures }, enableEReader() { - return this.store.getters['getServerSetting']('enableEReader') + return this.$store.getters['getServerSetting']('enableEReader') }, userIsAdminOrUp() { return this.$store.getters['user/getIsAdminOrUp'] From f083d4b5f6f9e602b3d9c188f8c93201d052735c Mon Sep 17 00:00:00 2001 From: advplyr Date: Fri, 20 May 2022 18:15:54 -0500 Subject: [PATCH 05/14] Update dockerfile failing with dev dependency --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 39111c19..42a7e1b1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,6 +14,6 @@ COPY index.js index.js COPY package-lock.json package-lock.json COPY package.json package.json COPY server server -RUN npm ci --production +RUN npm ci --only=production EXPOSE 80 CMD ["npm", "start"] From 169b637720b4b4a71061b75f8c4319f71ab36a32 Mon Sep 17 00:00:00 2001 From: Cassie Esposito Date: Sat, 21 May 2022 08:06:06 -0700 Subject: [PATCH 06/14] Removed dependency erroniously added by IDE --- server/utils/scandir.js | 1 - 1 file changed, 1 deletion(-) diff --git a/server/utils/scandir.js b/server/utils/scandir.js index faeab015..9d52227a 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -4,7 +4,6 @@ const Logger = require('../Logger') const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils') const globals = require('./globals') const LibraryFile = require('../objects/files/LibraryFile') -const { response } = require('express') function isMediaFile(mediaType, ext) { // if (!path) return false From 8beac53f5f367e12875c6baadf236d154c8e018d Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 21 May 2022 11:21:03 -0500 Subject: [PATCH 07/14] Update:Send source back with auth request --- client/components/app/ConfigSideNav.vue | 13 ++++++++++--- client/components/modals/libraries/EditLibrary.vue | 6 ++++-- client/pages/login.vue | 3 ++- client/store/index.js | 4 ++++ package.json | 8 ++++---- server/Auth.js | 3 ++- server/Server.js | 2 +- server/controllers/MiscController.js | 3 ++- 8 files changed, 29 insertions(+), 13 deletions(-) diff --git a/client/components/app/ConfigSideNav.vue b/client/components/app/ConfigSideNav.vue index 8de98961..57c9f66c 100644 --- a/client/components/app/ConfigSideNav.vue +++ b/client/components/app/ConfigSideNav.vue @@ -9,9 +9,13 @@
- @@ -25,6 +29,9 @@ export default { return {} }, computed: { + Source() { + return this.$store.state.Source + }, currentLibraryId() { return this.$store.state.libraries.currentLibraryId }, diff --git a/client/components/modals/libraries/EditLibrary.vue b/client/components/modals/libraries/EditLibrary.vue index 60d7875f..db87d1db 100644 --- a/client/components/modals/libraries/EditLibrary.vue +++ b/client/components/modals/libraries/EditLibrary.vue @@ -28,10 +28,9 @@
- Browse for Folder + Browse for Folder
-
@@ -77,6 +76,9 @@ export default { } }, methods: { + browseForFolder() { + this.showDirectoryPicker = true + }, getLibraryData() { return { name: this.name, diff --git a/client/pages/login.vue b/client/pages/login.vue index ea9e995c..cf604a57 100644 --- a/client/pages/login.vue +++ b/client/pages/login.vue @@ -124,8 +124,9 @@ export default { location.reload() }, - setUser({ user, userDefaultLibraryId, serverSettings }) { + setUser({ user, userDefaultLibraryId, serverSettings, Source }) { this.$store.commit('setServerSettings', serverSettings) + this.$store.commit('setSource', Source) if (serverSettings.chromecastEnabled) { console.log('Chromecast enabled import script') diff --git a/client/store/index.js b/client/store/index.js index 2ac4a312..2b9a70ea 100644 --- a/client/store/index.js +++ b/client/store/index.js @@ -2,6 +2,7 @@ import { checkForUpdate } from '@/plugins/version' import Vue from 'vue' export const state = () => ({ + Source: null, versionData: null, serverSettings: null, streamLibraryItem: null, @@ -81,6 +82,9 @@ export const actions = { } export const mutations = { + setSource(state, source) { + state.Source = source + }, setLastBookshelfScrollData(state, { scrollTop, path, name }) { state.lastBookshelfScrollData[name] = { scrollTop, path } }, diff --git a/package.json b/package.json index c1ddcffc..678f8cb8 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ "watch": "npm-watch", "dev": "node index.js", "start": "node index.js", - "client": "cd client && npm install && npm run generate", - "prod": "npm run client && npm install && node prod.js", - "build-win": "pkg -t node16-win-x64 -o ./dist/win/audiobookshelf -C GZip .", + "client": "cd client && npm ci && npm run generate", + "prod": "npm run client && npm ci && node prod.js", + "build-win": "npm run client && pkg -t node16-win-x64 -o ./dist/win/audiobookshelf -C GZip .", "build-linux": "build/linuxpackager", "docker": "docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 --push . -t advplyr/audiobookshelf", "deploy": "node dist/autodeploy" @@ -60,4 +60,4 @@ "devDependencies": { "npm-watch": "^0.11.0" } -} +} \ No newline at end of file diff --git a/server/Auth.js b/server/Auth.js index b30e060f..1548854b 100644 --- a/server/Auth.js +++ b/server/Auth.js @@ -96,7 +96,8 @@ class Auth { return { user: user.toJSONForBrowser(), userDefaultLibraryId: user.getDefaultLibraryId(this.db.libraries), - serverSettings: this.db.serverSettings.toJSON() + serverSettings: this.db.serverSettings.toJSON(), + Source: global.Source } } diff --git a/server/Server.js b/server/Server.js index 41496f53..c34fde26 100644 --- a/server/Server.js +++ b/server/Server.js @@ -35,9 +35,9 @@ const RssFeedManager = require('./managers/RssFeedManager') class Server { constructor(SOURCE, PORT, HOST, UID, GID, CONFIG_PATH, METADATA_PATH) { - this.Source = SOURCE this.Port = PORT this.Host = HOST + global.Source = SOURCE global.Uid = isNaN(UID) ? 0 : Number(UID) global.Gid = isNaN(GID) ? 0 : Number(GID) global.ConfigPath = Path.normalize(CONFIG_PATH) diff --git a/server/controllers/MiscController.js b/server/controllers/MiscController.js index 852af27d..d16c9c8f 100644 --- a/server/controllers/MiscController.js +++ b/server/controllers/MiscController.js @@ -242,7 +242,8 @@ class MiscController { const userResponse = { user: req.user, userDefaultLibraryId: req.user.getDefaultLibraryId(this.db.libraries), - serverSettings: this.db.serverSettings.toJSON() + serverSettings: this.db.serverSettings.toJSON(), + Source: global.Source } res.json(userResponse) } From d22e9e32ed63aaa139021bc0f86aa68d1528cbe1 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sat, 21 May 2022 11:36:08 -0500 Subject: [PATCH 08/14] Remove dev dependency from package.json --- package.json | 7 ------- 1 file changed, 7 deletions(-) diff --git a/package.json b/package.json index 678f8cb8..babba376 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,7 @@ "version": "2.0.15", "description": "Self-hosted audiobook and podcast server", "main": "index.js", - "watch": { - "dev": "server/{*,*/*}/*.js" - }, "scripts": { - "watch": "npm-watch", "dev": "node index.js", "start": "node index.js", "client": "cd client && npm ci && npm run generate", @@ -56,8 +52,5 @@ "string-strip-html": "^8.3.0", "watcher": "^1.2.0", "xml2js": "^0.4.23" - }, - "devDependencies": { - "npm-watch": "^0.11.0" } } \ No newline at end of file From 4d227cbade4d1cf61a3c82ab500c26fb1b5a6c92 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 22 May 2022 08:05:39 -0500 Subject: [PATCH 09/14] Add copy to clipboard fallback --- client/plugins/init.client.js | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/client/plugins/init.client.js b/client/plugins/init.client.js index 90dd7814..0fab85aa 100644 --- a/client/plugins/init.client.js +++ b/client/plugins/init.client.js @@ -163,17 +163,26 @@ Vue.prototype.$sanitizeSlug = (str) => { Vue.prototype.$copyToClipboard = (str, ctx) => { return new Promise((resolve) => { if (!navigator.clipboard) { - console.warn('Clipboard not supported') - return resolve(false) + navigator.clipboard.writeText(str).then(() => { + if (ctx) ctx.$toast.success('Copied to clipboard') + resolve(true) + }, (err) => { + console.error('Clipboard copy failed', str, err) + resolve(false) + }) + } else { + const el = document.createElement('textarea') + el.value = str + el.setAttribute('readonly', '') + el.style.position = 'absolute' + el.style.left = '-9999px' + document.body.appendChild(el) + el.select() + document.execCommand('copy') + document.body.removeChild(el) + + if (ctx) ctx.$toast.success('Copied to clipboard') } - navigator.clipboard.writeText(str).then(() => { - console.log('Clipboard copy success', str) - ctx.$toast.success('Copied to clipboard') - resolve(true) - }, (err) => { - console.error('Clipboard copy failed', str, err) - resolve(false) - }) }) } From a8d5b543d7c092d9505b93874508102b9ec21aa3 Mon Sep 17 00:00:00 2001 From: advplyr Date: Sun, 22 May 2022 19:17:21 -0500 Subject: [PATCH 10/14] Update:Parsing sequence from folder will strip leading zeros #562 --- client/components/app/LazyBookshelf.vue | 2 -- server/utils/scandir.js | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/components/app/LazyBookshelf.vue b/client/components/app/LazyBookshelf.vue index 112efef7..3f8f7d71 100644 --- a/client/components/app/LazyBookshelf.vue +++ b/client/components/app/LazyBookshelf.vue @@ -43,7 +43,6 @@ export default { mixins: [bookshelfCardsHelpers], data() { return { - routeName: null, routeFullPath: null, initialized: false, bookshelfHeight: 0, @@ -632,7 +631,6 @@ export default { mounted() { this.initListeners() - this.routeName = this.$route.name // beforeDestroy will have the new route name already, so need to store this this.routeFullPath = window.location.pathname + (window.location.search || '') }, updated() { diff --git a/server/utils/scandir.js b/server/utils/scandir.js index 9d52227a..699a47b9 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -260,6 +260,10 @@ function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) { title = title.replace(replaceChunk, '').trim() } } + + if (volumeNumber != null && !isNaN(volumeNumber)) { + volumeNumber = String(Number(volumeNumber)) // Strips leading zeros + } } var publishedYear = null From 0ad7a98fc7f8e1ce2475c2b3c68ce8c888bbc1f4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 23 May 2022 18:15:15 -0500 Subject: [PATCH 11/14] Add:Support for single book files to be detected by Watcher #610, Fix:Single media file in library folder root is only supported for books not podcasts --- server/Watcher.js | 7 --- server/scanner/Scanner.js | 18 +++++--- server/utils/scandir.js | 91 +++++++++++++++++++++++---------------- 3 files changed, 68 insertions(+), 48 deletions(-) diff --git a/server/Watcher.js b/server/Watcher.js index 555dce06..d2166b6e 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -162,13 +162,6 @@ class FolderWatcher extends EventEmitter { } var folderFullPath = folder.fullPath.replace(/\\/g, '/') - // Check if file was added to root directory - var dir = Path.dirname(path) - if (dir === folderFullPath) { - Logger.warn(`[Watcher] New file "${Path.basename(path)}" added to folder root - ignoring it`) - return - } - var relPath = path.replace(folderFullPath, '') var hasDotPath = relPath.split('/').find(p => p.startsWith('.')) diff --git a/server/scanner/Scanner.js b/server/scanner/Scanner.js index 7e9ffc2e..74e181eb 100644 --- a/server/scanner/Scanner.js +++ b/server/scanner/Scanner.js @@ -62,7 +62,8 @@ class Scanner { } async scanLibraryItem(libraryMediaType, folder, libraryItem) { - var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, libraryItem.path, this.db.serverSettings) + // TODO: Support for single media item + var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, libraryItem.path, false, this.db.serverSettings) if (!libraryItemData) { return ScanResult.NOTHING } @@ -499,7 +500,11 @@ class Scanner { continue; } var relFilePaths = folderGroups[folderId].fileUpdates.map(fileUpdate => fileUpdate.relPath) - var fileUpdateGroup = groupFilesIntoLibraryItemPaths(relFilePaths, true) + var fileUpdateGroup = groupFilesIntoLibraryItemPaths(library.mediaType, relFilePaths) + if (!Object.keys(fileUpdateGroup).length) { + Logger.info(`[Scanner] No important changes to scan for in folder "${folderId}"`) + continue; + } var folderScanResults = await this.scanFolderUpdates(library, folder, fileUpdateGroup) Logger.debug(`[Scanner] Folder scan results`, folderScanResults) } @@ -513,6 +518,8 @@ class Scanner { // Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item var updateGroup = { ...fileUpdateGroup } for (const itemDir in updateGroup) { + if (itemDir == fileUpdateGroup[itemDir]) continue; // Media in root path + var itemDirNestedFiles = fileUpdateGroup[itemDir].filter(b => b.includes('/')) if (!itemDirNestedFiles.length) continue; @@ -582,7 +589,8 @@ class Scanner { } Logger.debug(`[Scanner] Folder update group must be a new item "${itemDir}" in library "${library.name}"`) - var newLibraryItem = await this.scanPotentialNewLibraryItem(library.mediaType, folder, fullPath) + var isSingleMediaItem = itemDir === fileUpdateGroup[itemDir] + var newLibraryItem = await this.scanPotentialNewLibraryItem(library.mediaType, folder, fullPath, isSingleMediaItem) if (newLibraryItem) { await this.createNewAuthorsAndSeries(newLibraryItem) await this.db.insertLibraryItem(newLibraryItem) @@ -594,8 +602,8 @@ class Scanner { return itemGroupingResults } - async scanPotentialNewLibraryItem(libraryMediaType, folder, fullPath) { - var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, this.db.serverSettings) + async scanPotentialNewLibraryItem(libraryMediaType, folder, fullPath, isSingleMediaItem = false) { + var libraryItemData = await getLibraryItemFileData(libraryMediaType, folder, fullPath, isSingleMediaItem, this.db.serverSettings) if (!libraryItemData) return null var serverSettings = this.db.serverSettings return this.scanNewLibraryItem(libraryItemData, libraryMediaType, serverSettings.scannerPreferAudioMetadata, serverSettings.scannerPreferOpfMetadata, serverSettings.scannerFindCovers) diff --git a/server/utils/scandir.js b/server/utils/scandir.js index 699a47b9..0c68dacf 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -17,11 +17,14 @@ function isMediaFile(mediaType, ext) { // TODO: Function needs to be re-done // Input: array of relative file paths // Output: map of files grouped into potential item dirs -function groupFilesIntoLibraryItemPaths(paths) { - // Step 1: Clean path, Remove leading "/", Filter out files in root dir +function groupFilesIntoLibraryItemPaths(mediaType, paths) { + // Step 1: Clean path, Remove leading "/", Filter out non-media files in root dir var pathsFiltered = paths.map(path => { return path.startsWith('/') ? path.slice(1) : path - }).filter(path => Path.parse(path).dir) + }).filter(path => { + let parsedPath = Path.parse(path) + return parsedPath.dir || (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext)) + }) // Step 2: Sort by least number of directories pathsFiltered.sort((a, b) => { @@ -33,25 +36,30 @@ function groupFilesIntoLibraryItemPaths(paths) { // Step 3: Group files in dirs var itemGroup = {} pathsFiltered.forEach((path) => { - var dirparts = Path.dirname(path).split('/') + var dirparts = Path.dirname(path).split('/').filter(p => !!p && p !== '.') // dirname returns . if no directory var numparts = dirparts.length var _path = '' - // Iterate over directories in path - for (let i = 0; i < numparts; i++) { - var dirpart = dirparts.shift() - _path = Path.posix.join(_path, dirpart) + if (!numparts) { + // Media file in root + itemGroup[path] = path + } else { + // Iterate over directories in path + for (let i = 0; i < numparts; i++) { + var dirpart = dirparts.shift() + _path = Path.posix.join(_path, dirpart) - if (itemGroup[_path]) { // Directory already has files, add file - var relpath = Path.posix.join(dirparts.join('/'), Path.basename(path)) - itemGroup[_path].push(relpath) - return - } else if (!dirparts.length) { // This is the last directory, create group - itemGroup[_path] = [Path.basename(path)] - return - } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group - itemGroup[_path] = [Path.posix.join(dirparts[0], Path.basename(path))] - return + if (itemGroup[_path]) { // Directory already has files, add file + var relpath = Path.posix.join(dirparts.join('/'), Path.basename(path)) + itemGroup[_path].push(relpath) + return + } else if (!dirparts.length) { // This is the last directory, create group + itemGroup[_path] = [Path.basename(path)] + return + } else if (dirparts.length === 1 && /^cd\d{1,3}$/i.test(dirparts[0])) { // Next directory is the last and is a CD dir, create group + itemGroup[_path] = [Path.posix.join(dirparts[0], Path.basename(path))] + return + } } } }) @@ -62,9 +70,9 @@ module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths // Input: array of relative file items (see recurseFiles) // Output: map of files grouped into potential libarary item dirs function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems) { - // Step 1: Filter out non-media files in root dir (with depth of 0) + // Step 1: Filter out non-book-media files in root dir (with depth of 0) var itemsFiltered = fileItems.filter(i => { - return i.deep > 0 || isMediaFile(mediaType, i.extension) + return i.deep > 0 || (mediaType === 'book' && isMediaFile(mediaType, i.extension)) }) // Step 2: Seperate media files and other files @@ -147,16 +155,6 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) { } var fileItems = await recurseFiles(folderPath) - var basePath = folderPath - - const isOpenAudibleFolder = fileItems.find(fi => fi.deep === 0 && fi.name === 'books.json') - if (isOpenAudibleFolder) { - Logger.info(`[scandir] Detected Open Audible Folder, looking in books folder`) - basePath = Path.posix.join(folderPath, 'books') - fileItems = await recurseFiles(basePath) - Logger.debug(`[scandir] ${fileItems.length} files found in books folder`) - } - var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems) if (!Object.keys(libraryItemGrouping).length) { @@ -175,10 +173,10 @@ async function scanFolder(libraryMediaType, folder, serverSettings = {}) { mediaMetadata: { title: Path.basename(libraryItemPath, Path.extname(libraryItemPath)) }, - path: Path.posix.join(basePath, libraryItemPath), + path: Path.posix.join(folderPath, libraryItemPath), relPath: libraryItemPath } - fileObjs = await cleanFileObjects(basePath, [libraryItemPath]) + fileObjs = await cleanFileObjects(folderPath, [libraryItemPath]) isFile = true } else { libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings) @@ -335,14 +333,34 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettin } // Called from Scanner.js -async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, serverSettings = {}) { - var fileItems = await recurseFiles(libraryItemPath) - +async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, isSingleMediaItem, serverSettings = {}) { libraryItemPath = libraryItemPath.replace(/\\/g, '/') var folderFullPath = folder.fullPath.replace(/\\/g, '/') var libraryItemDir = libraryItemPath.replace(folderFullPath, '').slice(1) - var libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings) + var libraryItemData = {} + + var fileItems = [] + + if (isSingleMediaItem) { // Single media item in root of folder + fileItems = [ + { + fullpath: libraryItemPath, + path: libraryItemDir // actually the relPath (only filename here) + } + ] + libraryItemData = { + path: libraryItemPath, // full path + relPath: libraryItemDir, // only filename + mediaMetadata: { + title: Path.basename(libraryItemDir, Path.extname(libraryItemDir)) + } + } + } else { + fileItems = await recurseFiles(libraryItemPath) + libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings) + } + var libraryItemDirStats = await getFileTimestampsWithIno(libraryItemData.path) var libraryItem = { ino: libraryItemDirStats.ino, @@ -353,6 +371,7 @@ async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, libraryId: folder.libraryId, path: libraryItemData.path, relPath: libraryItemData.relPath, + isFile: isSingleMediaItem, media: { metadata: libraryItemData.mediaMetadata || null }, From 6cfe583535217a8f4aa31329e57fa01208e282b4 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 23 May 2022 18:31:11 -0500 Subject: [PATCH 12/14] Fix:Static router for downloading single file library items #627 --- server/routers/StaticRouter.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/routers/StaticRouter.js b/server/routers/StaticRouter.js index b571869f..24b6f6da 100644 --- a/server/routers/StaticRouter.js +++ b/server/routers/StaticRouter.js @@ -17,7 +17,9 @@ class StaticRouter { if (!item) return res.status(404).send('Item not found with id ' + req.params.id) var remainingPath = req.params['0'] - var fullPath = Path.join(item.path, remainingPath) + var fullPath = null + if (item.isFile) fullPath = item.path + else fullPath = Path.join(item.path, remainingPath) res.sendFile(fullPath) }) } From 3c465994fe094c11d0fb991fec074e3fa7542e85 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 23 May 2022 19:12:40 -0500 Subject: [PATCH 13/14] Fix:Hide remove icon from author images with no image --- client/components/modals/authors/EditModal.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/components/modals/authors/EditModal.vue b/client/components/modals/authors/EditModal.vue index 503f8b12..ef3eb03e 100644 --- a/client/components/modals/authors/EditModal.vue +++ b/client/components/modals/authors/EditModal.vue @@ -11,7 +11,7 @@
-
+
delete
From 3e98b6f7499c8cdf96ccaed29a3fe474e37d0566 Mon Sep 17 00:00:00 2001 From: advplyr Date: Mon, 23 May 2022 19:28:00 -0500 Subject: [PATCH 14/14] Update:Remove manual sorting of podcast episodes and default to sort by published at --- .../components/controls/EpisodeSortSelect.vue | 8 +- .../components/modals/authors/EditModal.vue | 2 +- .../tables/podcast/EpisodeTableRow.vue | 29 +++--- .../tables/podcast/EpisodesTable.vue | 93 ++----------------- server/controllers/LibraryItemController.js | 14 --- server/objects/mediaTypes/Podcast.js | 12 +-- server/routers/ApiRouter.js | 1 - 7 files changed, 23 insertions(+), 136 deletions(-) diff --git a/client/components/controls/EpisodeSortSelect.vue b/client/components/controls/EpisodeSortSelect.vue index 27aced3f..7819e4a3 100644 --- a/client/components/controls/EpisodeSortSelect.vue +++ b/client/components/controls/EpisodeSortSelect.vue @@ -33,8 +33,8 @@ export default { showMenu: false, items: [ { - text: 'Current', - value: 'index' + text: 'Pub Date', + value: 'publishedAt' }, { text: 'Title', @@ -47,10 +47,6 @@ export default { { text: 'Episode', value: 'episode' - }, - { - text: 'Pub Date', - value: 'publishedAt' } ] } diff --git a/client/components/modals/authors/EditModal.vue b/client/components/modals/authors/EditModal.vue index ef3eb03e..3657bc85 100644 --- a/client/components/modals/authors/EditModal.vue +++ b/client/components/modals/authors/EditModal.vue @@ -6,7 +6,7 @@
-
+
diff --git a/client/components/tables/podcast/EpisodeTableRow.vue b/client/components/tables/podcast/EpisodeTableRow.vue index bcb64ecc..b4adeb49 100644 --- a/client/components/tables/podcast/EpisodeTableRow.vue +++ b/client/components/tables/podcast/EpisodeTableRow.vue @@ -1,11 +1,6 @@ diff --git a/server/controllers/LibraryItemController.js b/server/controllers/LibraryItemController.js index 6765e5f1..b920de46 100644 --- a/server/controllers/LibraryItemController.js +++ b/server/controllers/LibraryItemController.js @@ -224,20 +224,6 @@ class LibraryItemController { res.json(libraryItem.toJSON()) } - // PATCH: api/items/:id/episodes - async updateEpisodes(req, res) { // For updating podcast episode order - var libraryItem = req.libraryItem - var orderedFileData = req.body.episodes - if (!libraryItem.media.setEpisodeOrder) { - Logger.error(`[LibraryItemController] updateEpisodes invalid media type ${libraryItem.id}`) - return res.sendStatus(500) - } - libraryItem.media.setEpisodeOrder(orderedFileData) - await this.db.updateLibraryItem(libraryItem) - this.emitter('item_updated', libraryItem.toJSONExpanded()) - res.json(libraryItem.toJSON()) - } - // DELETE: api/items/:id/episode/:episodeId async removeEpisode(req, res) { var episodeId = req.params.episodeId diff --git a/server/objects/mediaTypes/Podcast.js b/server/objects/mediaTypes/Podcast.js index 55ae0c15..a6e38c51 100644 --- a/server/objects/mediaTypes/Podcast.js +++ b/server/objects/mediaTypes/Podcast.js @@ -224,18 +224,10 @@ class Podcast { this.episodes.push(pe) } - setEpisodeOrder(episodeIds) { - episodeIds.reverse() // episode Ids will already be in descending order - this.episodes = this.episodes.map(ep => { - var indexOf = episodeIds.findIndex(id => id === ep.id) - ep.index = indexOf + 1 - return ep - }) - this.episodes.sort((a, b) => b.index - a.index) - } - reorderEpisodes() { var hasUpdates = false + + // TODO: Sort by published date this.episodes = naturalSort(this.episodes).asc((ep) => ep.bestFilename) for (let i = 0; i < this.episodes.length; i++) { if (this.episodes[i].index !== (i + 1)) { diff --git a/server/routers/ApiRouter.js b/server/routers/ApiRouter.js index d2b67114..8af4d9f6 100644 --- a/server/routers/ApiRouter.js +++ b/server/routers/ApiRouter.js @@ -90,7 +90,6 @@ class ApiRouter { this.router.post('/items/:id/play', LibraryItemController.middleware.bind(this), LibraryItemController.startPlaybackSession.bind(this)) this.router.post('/items/:id/play/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.startEpisodePlaybackSession.bind(this)) this.router.patch('/items/:id/tracks', LibraryItemController.middleware.bind(this), LibraryItemController.updateTracks.bind(this)) - this.router.patch('/items/:id/episodes', LibraryItemController.middleware.bind(this), LibraryItemController.updateEpisodes.bind(this)) this.router.delete('/items/:id/episode/:episodeId', LibraryItemController.middleware.bind(this), LibraryItemController.removeEpisode.bind(this)) this.router.get('/items/:id/scan', LibraryItemController.middleware.bind(this), LibraryItemController.scan.bind(this)) this.router.get('/items/:id/audio-metadata', LibraryItemController.middleware.bind(this), LibraryItemController.updateAudioFileMetadata.bind(this))