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 @@
-
{{ title }}
@@ -49,8 +44,8 @@ export default {
episode: {
type: Object,
default: () => {}
- },
- isDragging: Boolean
+ }
+ // isDragging: Boolean
},
data() {
return {
@@ -59,15 +54,15 @@ export default {
isHovering: false
}
},
- watch: {
- isDragging: {
- handler(newVal) {
- if (newVal) {
- this.isHovering = false
- }
- }
- }
- },
+ // watch: {
+ // isDragging: {
+ // handler(newVal) {
+ // if (newVal) {
+ // this.isHovering = false
+ // }
+ // }
+ // }
+ // },
computed: {
userCanUpdate() {
return this.$store.getters['user/getUserCanUpdate']
@@ -117,7 +112,7 @@ export default {
},
methods: {
mouseover() {
- if (this.isDragging) return
+ // if (this.isDragging) return
this.isHovering = true
},
mouseleave() {
diff --git a/client/components/tables/podcast/EpisodesTable.vue b/client/components/tables/podcast/EpisodesTable.vue
index 3eac0c09..56546f69 100644
--- a/client/components/tables/podcast/EpisodesTable.vue
+++ b/client/components/tables/podcast/EpisodesTable.vue
@@ -9,23 +9,14 @@
No Episodes
-
-
-
-
-
-
-
+
+
+
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
+
+
+
+
updateSettingsKey('enableEReader', val)" />
+
+
+ Enable e-reader for all users
+ 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 = /^(?.*)\{(?.*)\} *$/
- let match = folder.match(pattern)
- return match ? [match.groups.title.trimEnd(), match.groups.narrators] : [folder, null]
+ let pattern = /^(?.*)\{(?.*)\} *$/
+ 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