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"] 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/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 @@
-
-

v{{ $config.version }}

- Update available: {{ latestVersion }} +
+
+

v{{ $config.version }}

+ +

{{ Source }}

+
+ Latest: {{ latestVersion }}
@@ -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/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/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/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/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/authors/EditModal.vue b/client/components/modals/authors/EditModal.vue index 503f8b12..3657bc85 100644 --- a/client/components/modals/authors/EditModal.vue +++ b/client/components/modals/authors/EditModal.vue @@ -6,12 +6,12 @@
-
+
-
+
delete
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/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/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/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/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..87a5d969 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 +

+
+
@@ -169,10 +183,12 @@
- +

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..c41087dc 100644 --- a/client/pages/item/_id/index.vue +++ b/client/pages/item/_id/index.vue @@ -92,7 +92,8 @@
warning_amber -

Book has no audio tracks but has valid ebook files. The e-reader is experimental and can be turned on in config.

+

Book has no audio tracks but has an ebook. The experimental e-reader can be enabled in config.

+

Book has no audio tracks but has an ebook. The experimental e-reader must be enabled by a server admin.

@@ -135,7 +136,7 @@ {{ isMissing ? 'Missing' : 'Incomplete' }} - + auto_stories Read @@ -223,6 +224,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 +248,6 @@ export default { isDeveloperMode() { return this.$store.state.developerMode }, - showExperimentalFeatures() { - return this.$store.state.showExperimentalFeatures - }, isPodcast() { return this.libraryItem.mediaType === 'podcast' }, @@ -262,6 +266,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 +349,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/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/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) - }) }) } 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 acc1532a..babba376 100644 --- a/package.json +++ b/package.json @@ -6,9 +6,9 @@ "scripts": { "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" @@ -52,6 +52,5 @@ "string-strip-html": "^8.3.0", "watcher": "^1.2.0", "xml2js": "^0.4.23" - }, - "devDependencies": {} + } } \ 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/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/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/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) } 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/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 } 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)) 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) }) } 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/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 diff --git a/server/utils/scandir.js b/server/utils/scandir.js index bf1e53dc..ec25f6bc 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -4,352 +4,367 @@ const Logger = require('../Logger') const { recurseFiles, getFileTimestampsWithIno } = require('./fileUtils') const globals = require('./globals') const LibraryFile = require('../objects/files/LibraryFile') -const { response } = require('express') -const e = require('express') function isMediaFile(mediaType, ext) { - // if (!path) return false - // var ext = Path.extname(path) - if (!ext) return false - var extclean = ext.slice(1).toLowerCase() - if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean) - return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean) + // if (!path) return false + // var ext = Path.extname(path) + if (!ext) return false + var extclean = ext.slice(1).toLowerCase() + if (mediaType === 'podcast') return globals.SupportedAudioTypes.includes(extclean) + return globals.SupportedAudioTypes.includes(extclean) || globals.SupportedEbookTypes.includes(extclean) } // 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 - var pathsFiltered = paths.map(path => { - return path.startsWith('/') ? path.slice(1) : path - }).filter(path => Path.parse(path).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 => { + 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) => { - var pathsA = Path.dirname(a).split('/').length - var pathsB = Path.dirname(b).split('/').length - return pathsA - pathsB - }) + // Step 2: Sort by least number of directories + pathsFiltered.sort((a, b) => { + var pathsA = Path.dirname(a).split('/').length + var pathsB = Path.dirname(b).split('/').length + return pathsA - pathsB + }) - // Step 3: Group files in dirs - var itemGroup = {} - pathsFiltered.forEach((path) => { - var dirparts = Path.dirname(path).split('/') - var numparts = dirparts.length - var _path = '' + // Step 3: Group files in dirs + var itemGroup = {} + pathsFiltered.forEach((path) => { + 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 - } - } - }) - return itemGroup + 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 + } + } + } + }) + return itemGroup } 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) - var itemsFiltered = fileItems.filter(i => { - return i.deep > 0 || isMediaFile(mediaType, i.extension) - }) + // Step 1: Filter out non-book-media files in root dir (with depth of 0) + var itemsFiltered = fileItems.filter(i => { + return i.deep > 0 || (mediaType === 'book' && isMediaFile(mediaType, i.extension)) + }) - // Step 2: Seperate media files and other files - // - Directories without a media file will not be included - var mediaFileItems = [] - var otherFileItems = [] - itemsFiltered.forEach(item => { - if (isMediaFile(mediaType, item.extension)) mediaFileItems.push(item) - else otherFileItems.push(item) - }) + // Step 2: Seperate media files and other files + // - Directories without a media file will not be included + var mediaFileItems = [] + var otherFileItems = [] + itemsFiltered.forEach(item => { + if (isMediaFile(mediaType, item.extension)) mediaFileItems.push(item) + else otherFileItems.push(item) + }) - // Step 3: Group audio files in library items - var libraryItemGroup = {} - mediaFileItems.forEach((item) => { - var dirparts = item.reldirpath.split('/').filter(p => !!p) - var numparts = dirparts.length - var _path = '' + // Step 3: Group audio files in library items + var libraryItemGroup = {} + mediaFileItems.forEach((item) => { + var dirparts = item.reldirpath.split('/').filter(p => !!p) + var numparts = dirparts.length + var _path = '' - if (!dirparts.length) { - // Media file in root - libraryItemGroup[item.name] = item.name - } else { - // Iterate over directories in path - for (let i = 0; i < numparts; i++) { - var dirpart = dirparts.shift() - _path = Path.posix.join(_path, dirpart) + if (!dirparts.length) { + // Media file in root + libraryItemGroup[item.name] = item.name + } else { + // Iterate over directories in path + for (let i = 0; i < numparts; i++) { + var dirpart = dirparts.shift() + _path = Path.posix.join(_path, dirpart) - if (libraryItemGroup[_path]) { // Directory already has files, add file - var relpath = Path.posix.join(dirparts.join('/'), item.name) - libraryItemGroup[_path].push(relpath) - return - } else if (!dirparts.length) { // This is the last directory, create group - libraryItemGroup[_path] = [item.name] - 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 - libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)] - return + if (libraryItemGroup[_path]) { // Directory already has files, add file + var relpath = Path.posix.join(dirparts.join('/'), item.name) + libraryItemGroup[_path].push(relpath) + return + } else if (!dirparts.length) { // This is the last directory, create group + libraryItemGroup[_path] = [item.name] + 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 + libraryItemGroup[_path] = [Path.posix.join(dirparts[0], item.name)] + return + } + } } - } - } - }) + }) - // Step 4: Add other files into library item groups - otherFileItems.forEach((item) => { - var dirparts = item.reldirpath.split('/') - var numparts = dirparts.length - var _path = '' + // Step 4: Add other files into library item groups + otherFileItems.forEach((item) => { + var dirparts = item.reldirpath.split('/') + 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 (libraryItemGroup[_path]) { // Directory is audiobook group - var relpath = Path.posix.join(dirparts.join('/'), item.name) - libraryItemGroup[_path].push(relpath) - return - } - } - }) - return libraryItemGroup + // Iterate over directories in path + for (let i = 0; i < numparts; i++) { + var dirpart = dirparts.shift() + _path = Path.posix.join(_path, dirpart) + if (libraryItemGroup[_path]) { // Directory is audiobook group + var relpath = Path.posix.join(dirparts.join('/'), item.name) + libraryItemGroup[_path].push(relpath) + return + } + } + }) + return libraryItemGroup } function cleanFileObjects(libraryItemPath, files) { - return Promise.all(files.map(async (file) => { - var filePath = Path.posix.join(libraryItemPath, file) - var newLibraryFile = new LibraryFile() - await newLibraryFile.setDataFromPath(filePath, file) - return newLibraryFile - })) + return Promise.all(files.map(async(file) => { + var filePath = Path.posix.join(libraryItemPath, file) + var newLibraryFile = new LibraryFile() + await newLibraryFile.setDataFromPath(filePath, file) + return newLibraryFile + })) } // Scan folder async function scanFolder(libraryMediaType, folder, serverSettings = {}) { - var folderPath = folder.fullPath.replace(/\\/g, '/') + var folderPath = folder.fullPath.replace(/\\/g, '/') - var pathExists = await fs.pathExists(folderPath) - if (!pathExists) { - Logger.error(`[scandir] Invalid folder path does not exist "${folderPath}"`) - return [] - } - - 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) { - Logger.error(`Root path has no media folders: ${folderPath}`) - return [] - } - - var isFile = false // item is not in a folder - var items = [] - for (const libraryItemPath in libraryItemGrouping) { - var libraryItemData = null - var fileObjs = [] - if (libraryItemPath === libraryItemGrouping[libraryItemPath]) { - // Media file in root only get title - libraryItemData = { - mediaMetadata: { - title: Path.basename(libraryItemPath, Path.extname(libraryItemPath)) - }, - path: Path.posix.join(basePath, libraryItemPath), - relPath: libraryItemPath - } - fileObjs = await cleanFileObjects(basePath, [libraryItemPath]) - isFile = true - } else { - libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings) - fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath]) + var pathExists = await fs.pathExists(folderPath) + if (!pathExists) { + Logger.error(`[scandir] Invalid folder path does not exist "${folderPath}"`) + return [] } - var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path) - items.push({ - folderId: folder.id, - libraryId: folder.libraryId, - ino: libraryItemFolderStats.ino, - mtimeMs: libraryItemFolderStats.mtimeMs || 0, - ctimeMs: libraryItemFolderStats.ctimeMs || 0, - birthtimeMs: libraryItemFolderStats.birthtimeMs || 0, - path: libraryItemData.path, - relPath: libraryItemData.relPath, - isFile, - media: { - metadata: libraryItemData.mediaMetadata || null - }, - libraryFiles: fileObjs - }) - } - return items + var fileItems = await recurseFiles(folderPath) + var libraryItemGrouping = groupFileItemsIntoLibraryItemDirs(libraryMediaType, fileItems) + + if (!Object.keys(libraryItemGrouping).length) { + Logger.error(`Root path has no media folders: ${folderPath}`) + return [] + } + + var isFile = false // item is not in a folder + var items = [] + for (const libraryItemPath in libraryItemGrouping) { + var libraryItemData = null + var fileObjs = [] + if (libraryItemPath === libraryItemGrouping[libraryItemPath]) { + // Media file in root only get title + libraryItemData = { + mediaMetadata: { + title: Path.basename(libraryItemPath, Path.extname(libraryItemPath)) + }, + path: Path.posix.join(folderPath, libraryItemPath), + relPath: libraryItemPath + } + fileObjs = await cleanFileObjects(folderPath, [libraryItemPath]) + isFile = true + } else { + libraryItemData = getDataFromMediaDir(libraryMediaType, folderPath, libraryItemPath, serverSettings) + fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath]) + } + + var libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path) + items.push({ + folderId: folder.id, + libraryId: folder.libraryId, + ino: libraryItemFolderStats.ino, + mtimeMs: libraryItemFolderStats.mtimeMs || 0, + ctimeMs: libraryItemFolderStats.ctimeMs || 0, + birthtimeMs: libraryItemFolderStats.birthtimeMs || 0, + path: libraryItemData.path, + relPath: libraryItemData.relPath, + isFile, + media: { + metadata: libraryItemData.mediaMetadata || null + }, + libraryFiles: fileObjs + }) + } + return items } module.exports.scanFolder = scanFolder // Input relative filepath, output all details that can be parsed function getBookDataFromDir(folderPath, relPath, parseSubtitle = false) { - relPath = relPath.replace(/\\/g, '/') - var splitDir = relPath.split('/') + relPath = relPath.replace(/\\/g, '/') + var splitDir = relPath.split('/') - var title = splitDir.pop() // Audio files will always be in the directory named for the title - series = (splitDir.length > 1) ? splitDir.pop() : null // If there are at least 2 more directories, next furthest will be the series - author = (splitDir.length > 0) ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/ + var title = splitDir.pop() // Audio files will always be in the directory named for the title + series = (splitDir.length > 1) ? splitDir.pop() : null // If there are at least 2 more directories, next furthest will be the series + author = (splitDir.length > 0) ? splitDir.pop() : null // There could be many more directories, but only the top 3 are used for naming /author/series/title/ - // The title directory may contain various other pieces of metadata, these functions extract it. - var [title, narrators] = getNarrator(title) - if (series) { var [title, sequence] = getSequence(title) } - var [title, publishedYear] = getPublishedYear(title) - if (parseSubtitle) { var [title, subtitle] = getSubtitle(title) } // Subtitle can be parsed from the title if user enabled + // The title directory may contain various other pieces of metadata, these functions extract it. + var [title, narrators] = getNarrator(title) + if (series) { var [title, sequence] = getSequence(title) } + var [title, publishedYear] = getPublishedYear(title) + if (parseSubtitle) { var [title, subtitle] = getSubtitle(title) } // Subtitle can be parsed from the title if user enabled - return { - mediaMetadata: { - author, - title, - subtitle, - series, - sequence, - publishedYear, - narrators, - }, - relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/.. - path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/.. - } + return { + mediaMetadata: { + author, + title, + subtitle, + series, + sequence, + publishedYear, + narrators, + }, + relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/.. + path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/.. + } } function getNarrator(folder) { - let pattern = /^(?.*)\{(?<narrators>.*)\} *$/ - let match = folder.match(pattern) - return match ? [match.groups.title.trimEnd(), match.groups.narrators] : [folder, null] + let pattern = /^(?<title>.*)\{(?<narrators>.*)\} *$/ + let match = folder.match(pattern) + return match ? [match.groups.title.trimEnd(), match.groups.narrators] : [folder, null] } function getSequence(title) { - // Valid ways of including a volume number: - // Book 2 - Title Here - Subtitle Here - // Title Here - Subtitle Here - Vol 12 - // Title Here - volume 9 - Subtitle Here - // Vol. 3 Title Here - Subtitle Here - // 1980 - Book 2-Title Here - // Title Here-Volume 999-Subtitle Here - // 2 - Book Title - // 100 - Book Title - // 0.5 - Book Title + // Valid ways of including a volume number: + // Book 2 - Title Here - Subtitle Here + // Title Here - Subtitle Here - Vol 12 + // Title Here - volume 9 - Subtitle Here + // Vol. 3 Title Here - Subtitle Here + // 1980 - Book 2-Title Here + // Title Here-Volume 999-Subtitle Here + // 2 - Book Title + // 100 - Book Title + // 0.5 - Book Title - // Matches a valid volume string, capturing each section for later processing. - let pattern = /^(vol\.? |volume |book )?(\d{1,3}(?:\.\d{1,2})?)(.*)/i + // Matches a valid volume string, capturing each section for later processing. + let pattern = /^(vol\.? |volume |book )?(\d{1,3}(?:\.\d{1,2})?)(.*)/i - let volumeNumber = null - let parts = title.split('-') - for (let i = 0; i < parts.length; i++) { - let match = parts[i].trim().match(pattern) - if (match && !(match[3].trim() && !match[1])) { // "101 Dalmations" shouldn't match - volumeNumber = match[2] - parts[i] = match[3] - if (!parts[i].trim()) { parts.splice(i, 1) } - break + let volumeNumber = null + let parts = title.split('-') + for (let i = 0; i < parts.length; i++) { + let match = parts[i].trim().match(pattern) + if (match && !(match[3].trim() && !match[1])) { // "101 Dalmations" shouldn't match + volumeNumber = match[2] + parts[i] = match[3] + if (!parts[i].trim()) { parts.splice(i, 1) } + break + } } - } - title = parts.join(' - ') + title = parts.join(' - ') - return [title, volumeNumber] + return [title, volumeNumber] } function getPublishedYear(title) { - var publishedYear = null + var publishedYear = null - pattern = /^ *\(?([0-9]{4})\)? *- *(.+)/ //Matches #### - title or (####) - title - var match = title.match(pattern) - if (match) { - publishedYear = match[1] - title = match[2] - } + pattern = /^ *\(?([0-9]{4})\)? *- *(.+)/ //Matches #### - title or (####) - title + var match = title.match(pattern) + if (match) { + publishedYear = match[1] + title = match[2] + } - return [title, publishedYear] + return [title, publishedYear] } function getSubtitle(title) { - // Subtitle is everything after " - " - var splitTitle = title.split(' - ') - return [splitTitle.shift().trim(), splitTitle.join(' - ').trim()] + // Subtitle is everything after " - " + var splitTitle = title.split(' - ') + return [splitTitle.shift().trim(), splitTitle.join(' - ').trim()] } function getPodcastDataFromDir(folderPath, relPath) { - relPath = relPath.replace(/\\/g, '/') - var splitDir = relPath.split('/') + relPath = relPath.replace(/\\/g, '/') + var splitDir = relPath.split('/') - // Audio files will always be in the directory named for the title - var title = splitDir.pop() - return { - mediaMetadata: { - title - }, - relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/.. - path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/.. - } + // Audio files will always be in the directory named for the title + var title = splitDir.pop() + return { + mediaMetadata: { + title + }, + relPath: relPath, // relative audiobook path i.e. /Author Name/Book Name/.. + path: Path.posix.join(folderPath, relPath) // i.e. /audiobook/Author Name/Book Name/.. + } } function getDataFromMediaDir(libraryMediaType, folderPath, relPath, serverSettings) { - if (libraryMediaType === 'podcast') { - return getPodcastDataFromDir(folderPath, relPath) - } else { - var parseSubtitle = !!serverSettings.scannerParseSubtitle - return getBookDataFromDir(folderPath, relPath, parseSubtitle) - } + if (libraryMediaType === 'podcast') { + return getPodcastDataFromDir(folderPath, relPath) + } else { + var parseSubtitle = !!serverSettings.scannerParseSubtitle + return getBookDataFromDir(folderPath, relPath, parseSubtitle) + } } // 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, '/') - libraryItemPath = libraryItemPath.replace(/\\/g, '/') - var folderFullPath = folder.fullPath.replace(/\\/g, '/') + var libraryItemDir = libraryItemPath.replace(folderFullPath, '').slice(1) + var libraryItemData = {} - var libraryItemDir = libraryItemPath.replace(folderFullPath, '').slice(1) - var libraryItemData = getDataFromMediaDir(libraryMediaType, folderFullPath, libraryItemDir, serverSettings) - var libraryItemDirStats = await getFileTimestampsWithIno(libraryItemData.path) - var libraryItem = { - ino: libraryItemDirStats.ino, - mtimeMs: libraryItemDirStats.mtimeMs || 0, - ctimeMs: libraryItemDirStats.ctimeMs || 0, - birthtimeMs: libraryItemDirStats.birthtimeMs || 0, - folderId: folder.id, - libraryId: folder.libraryId, - path: libraryItemData.path, - relPath: libraryItemData.relPath, - media: { - metadata: libraryItemData.mediaMetadata || null - }, - libraryFiles: [] - } + var fileItems = [] - for (let i = 0; i < fileItems.length; i++) { - var fileItem = fileItems[i] - var newLibraryFile = new LibraryFile() - // fileItem.path is the relative path - await newLibraryFile.setDataFromPath(fileItem.fullpath, fileItem.path) - libraryItem.libraryFiles.push(newLibraryFile) - } - return libraryItem + 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, + mtimeMs: libraryItemDirStats.mtimeMs || 0, + ctimeMs: libraryItemDirStats.ctimeMs || 0, + birthtimeMs: libraryItemDirStats.birthtimeMs || 0, + folderId: folder.id, + libraryId: folder.libraryId, + path: libraryItemData.path, + relPath: libraryItemData.relPath, + isFile: isSingleMediaItem, + media: { + metadata: libraryItemData.mediaMetadata || null + }, + libraryFiles: [] + } + + for (let i = 0; i < fileItems.length; i++) { + var fileItem = fileItems[i] + var newLibraryFile = new LibraryFile() + // fileItem.path is the relative path + await newLibraryFile.setDataFromPath(fileItem.fullpath, fileItem.path) + libraryItem.libraryFiles.push(newLibraryFile) + } + return libraryItem } -module.exports.getLibraryItemFileData = getLibraryItemFileData +module.exports.getLibraryItemFileData = getLibraryItemFileData \ No newline at end of file