diff --git a/client/package.json b/client/package.json index 73f21f9d..5f95ec3a 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "1.0.8", + "version": "1.1.0", "description": "Audiobook manager and player", "main": "index.js", "scripts": { diff --git a/images/ss_audiobook.png b/images/ss_audiobook.png index 5794c7a4..b2030166 100644 Binary files a/images/ss_audiobook.png and b/images/ss_audiobook.png differ diff --git a/images/ss_bookshelf.png b/images/ss_bookshelf.png deleted file mode 100644 index 21eb8d6b..00000000 Binary files a/images/ss_bookshelf.png and /dev/null differ diff --git a/images/ss_streaming.png b/images/ss_streaming.png index 35b874b4..49dbc0f0 100644 Binary files a/images/ss_streaming.png and b/images/ss_streaming.png differ diff --git a/package.json b/package.json index 6b1c0176..82275be9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "1.0.8", + "version": "1.1.0", "description": "Self-hosted audiobook server for managing and playing audiobooks.", "main": "index.js", "scripts": { diff --git a/readme.md b/readme.md index 8cb853e7..d9a66698 100644 --- a/readme.md +++ b/readme.md @@ -28,8 +28,6 @@ will store "With a Subtitle" as the subtitle #### Features coming soon: -* Auto add and update audiobooks (currently you need to press scan) -* User permissions & editing users * Support different views to see more details of each audiobook * Option to download all files in a zip file * iOS App (Android is in beta [here](https://play.google.com/store/apps/details?id=com.audiobookshelf.app)) diff --git a/server/Scanner.js b/server/Scanner.js index 276db4e7..3df852bc 100644 --- a/server/Scanner.js +++ b/server/Scanner.js @@ -1,10 +1,13 @@ +const fs = require('fs-extra') const Logger = require('./Logger') const BookFinder = require('./BookFinder') const Audiobook = require('./objects/Audiobook') const audioFileScanner = require('./utils/audioFileScanner') -const { getAllAudiobookFiles } = require('./utils/scandir') +const { getAllAudiobookFileData, getAudiobookFileData } = require('./utils/scandir') const { comparePaths, getIno } = require('./utils/index') const { secondsToTimestamp } = require('./utils/fileUtils') +const { ScanResult } = require('./utils/constants') + class Scanner { constructor(AUDIOBOOK_PATH, METADATA_PATH, db, emitter) { @@ -60,6 +63,110 @@ class Scanner { return audiobookDataAudioFiles.filter(abdFile => !!abdFile.ino) } + async scanAudiobookData(audiobookData) { + var existingAudiobook = this.audiobooks.find(a => a.ino === audiobookData.ino) + Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`) + + if (existingAudiobook) { + + // REMOVE: No valid audio files + if (!audiobookData.audioFiles.length) { + Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`) + + await this.db.removeEntity('audiobook', existingAudiobook.id) + this.emitter('audiobook_removed', existingAudiobook.toJSONMinified()) + + return ScanResult.REMOVED + } + + audiobookData.audioFiles = await this.setAudioFileInos(audiobookData.audioFiles, existingAudiobook.audioFiles) + + // Check for audio files that were removed + var abdAudioFileInos = audiobookData.audioFiles.map(af => af.ino) + var removedAudioFiles = existingAudiobook.audioFiles.filter(file => !abdAudioFileInos.includes(file.ino)) + if (removedAudioFiles.length) { + Logger.info(`[Scanner] ${removedAudioFiles.length} audio files removed for audiobook "${existingAudiobook.title}"`) + removedAudioFiles.forEach((af) => existingAudiobook.removeAudioFile(af)) + } + + // Check for new audio files and sync existing audio files + var newAudioFiles = [] + var hasUpdatedAudioFiles = false + audiobookData.audioFiles.forEach((file) => { + var existingAudioFile = existingAudiobook.getAudioFileByIno(file.ino) + if (existingAudioFile) { // Audio file exists, sync paths + if (existingAudiobook.syncAudioFile(existingAudioFile, file)) { + hasUpdatedAudioFiles = true + } + } else { + newAudioFiles.push(file) + } + }) + if (newAudioFiles.length) { + Logger.info(`[Scanner] ${newAudioFiles.length} new audio files were found for audiobook "${existingAudiobook.title}"`) + // Scan new audio files found - sets tracks + await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles) + } + + + // REMOVE: No valid audio tracks + if (!existingAudiobook.tracks.length) { + Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`) + + await this.db.removeEntity('audiobook', existingAudiobook.id) + this.emitter('audiobook_removed', existingAudiobook.toJSONMinified()) + return ScanResult.REMOVED + } + + var hasUpdates = removedAudioFiles.length || newAudioFiles.length || hasUpdatedAudioFiles + + if (existingAudiobook.checkUpdateMissingParts()) { + Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`) + hasUpdates = true + } + + if (existingAudiobook.syncOtherFiles(audiobookData.otherFiles)) { + hasUpdates = true + } + + // Syncs path and fullPath + if (existingAudiobook.syncPaths(audiobookData)) { + hasUpdates = true + } + + if (hasUpdates) { + Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`) + existingAudiobook.lastUpdate = Date.now() + await this.db.updateAudiobook(existingAudiobook) + this.emitter('audiobook_updated', existingAudiobook.toJSONMinified()) + + return ScanResult.UPDATED + } + + return ScanResult.UPTODATE + } + + // NEW: Check new audiobook + if (!audiobookData.audioFiles.length) { + Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path) + return ScanResult.NOTHING + } + + var audiobook = new Audiobook() + audiobook.setData(audiobookData) + await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles) + if (!audiobook.tracks.length) { + Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title) + return ScanResult.NOTHING + } + + audiobook.checkUpdateMissingParts() + Logger.info(`[Scanner] Audiobook "${audiobook.title}" Scanned (${audiobook.sizePretty}) [${audiobook.durationPretty}]`) + await this.db.insertAudiobook(audiobook) + this.emitter('audiobook_added', audiobook.toJSONMinified()) + return ScanResult.ADDED + } + async scan() { // TEMP - fix relative file paths // TEMP - update ino for each audiobook @@ -80,7 +187,7 @@ class Scanner { } const scanStart = Date.now() - var audiobookDataFound = await getAllAudiobookFiles(this.AudiobookPath, this.db.serverSettings) + var audiobookDataFound = await getAllAudiobookFileData(this.AudiobookPath, this.db.serverSettings) // Set ino for each ab data as a string audiobookDataFound = await this.setAudiobookDataInos(audiobookDataFound) @@ -112,97 +219,14 @@ class Scanner { } } + // Check for new and updated audiobooks for (let i = 0; i < audiobookDataFound.length; i++) { var audiobookData = audiobookDataFound[i] - var existingAudiobook = this.audiobooks.find(a => a.ino === audiobookData.ino) - Logger.debug(`[Scanner] Scanning "${audiobookData.title}" (${audiobookData.ino}) - ${!!existingAudiobook ? 'Exists' : 'New'}`) + var result = await this.scanAudiobookData(audiobookData) + if (result === ScanResult.ADDED) scanResults.added++ + if (result === ScanResult.REMOVED) scanResults.removed++ + if (result === ScanResult.UPDATED) scanResults.updated++ - if (existingAudiobook) { - if (!audiobookData.audioFiles.length) { - Logger.error(`[Scanner] "${existingAudiobook.title}" no valid audio files found - removing audiobook`) - - await this.db.removeEntity('audiobook', existingAudiobook.id) - this.emitter('audiobook_removed', existingAudiobook.toJSONMinified()) - scanResults.removed++ - } else { - audiobookData.audioFiles = await this.setAudioFileInos(audiobookData.audioFiles, existingAudiobook.audioFiles) - var abdAudioFileInos = audiobookData.audioFiles.map(af => af.ino) - - // Check for audio files that were removed - var removedAudioFiles = existingAudiobook.audioFiles.filter(file => !abdAudioFileInos.includes(file.ino)) - if (removedAudioFiles.length) { - Logger.info(`[Scanner] ${removedAudioFiles.length} audio files removed for audiobook "${existingAudiobook.title}"`) - removedAudioFiles.forEach((af) => existingAudiobook.removeAudioFile(af)) - } - - // Check for new audio files and sync existing audio files - var newAudioFiles = [] - var hasUpdatedAudioFiles = false - audiobookData.audioFiles.forEach((file) => { - var existingAudioFile = existingAudiobook.getAudioFileByIno(file.ino) - if (existingAudioFile) { // Audio file exists, sync paths - if (existingAudiobook.syncAudioFile(existingAudioFile, file)) { - hasUpdatedAudioFiles = true - } - } else { - newAudioFiles.push(file) - } - }) - if (newAudioFiles.length) { - Logger.info(`[Scanner] ${newAudioFiles.length} new audio files were found for audiobook "${existingAudiobook.title}"`) - // Scan new audio files found - await audioFileScanner.scanAudioFiles(existingAudiobook, newAudioFiles) - } - - if (!existingAudiobook.tracks.length) { - Logger.error(`[Scanner] "${existingAudiobook.title}" has no valid tracks after update - removing audiobook`) - - await this.db.removeEntity('audiobook', existingAudiobook.id) - this.emitter('audiobook_removed', existingAudiobook.toJSONMinified()) - } else { - var hasUpdates = removedAudioFiles.length || newAudioFiles.length || hasUpdatedAudioFiles - - if (existingAudiobook.checkUpdateMissingParts()) { - Logger.info(`[Scanner] "${existingAudiobook.title}" missing parts updated`) - hasUpdates = true - } - - if (existingAudiobook.syncOtherFiles(audiobookData.otherFiles)) { - hasUpdates = true - } - - // Syncs path and fullPath - if (existingAudiobook.syncPaths(audiobookData)) { - hasUpdates = true - } - - if (hasUpdates) { - Logger.info(`[Scanner] "${existingAudiobook.title}" was updated - saving`) - existingAudiobook.lastUpdate = Date.now() - await this.db.updateAudiobook(existingAudiobook) - this.emitter('audiobook_updated', existingAudiobook.toJSONMinified()) - scanResults.updated++ - } - } - } // end if update existing - } else { - if (!audiobookData.audioFiles.length) { - Logger.error('[Scanner] No valid audio tracks for Audiobook', audiobookData.path) - } else { - var audiobook = new Audiobook() - audiobook.setData(audiobookData) - await audioFileScanner.scanAudioFiles(audiobook, audiobookData.audioFiles) - if (!audiobook.tracks.length) { - Logger.warn('[Scanner] Invalid audiobook, no valid tracks', audiobook.title) - } else { - audiobook.checkUpdateMissingParts() - Logger.info(`[Scanner] Audiobook "${audiobook.title}" Scanned (${audiobook.sizePretty}) [${audiobook.durationPretty}]`) - await this.db.insertAudiobook(audiobook) - this.emitter('audiobook_added', audiobook.toJSONMinified()) - scanResults.added++ - } - } // end if add new - } var progress = Math.round(100 * (i + 1) / audiobookDataFound.length) this.emitter('scan_progress', { scanType: 'files', @@ -222,6 +246,29 @@ class Scanner { return scanResults } + async scanAudiobook(audiobookPath) { + var exists = await fs.pathExists(audiobookPath) + if (!exists) { + // Audiobook was deleted, TODO: Should confirm this better + var audiobook = this.db.audiobooks.find(ab => ab.fullPath === audiobookPath) + if (audiobook) { + var audiobookJSON = audiobook.toJSONMinified() + await this.db.removeEntity('audiobook', audiobook.id) + this.emitter('audiobook_removed', audiobookJSON) + return ScanResult.REMOVED + } + Logger.warn('Path was deleted but no audiobook found', audiobookPath) + return ScanResult.NOTHING + } + + var audiobookData = await getAudiobookFileData(this.AudiobookPath, audiobookPath, this.db.serverSettings) + if (!audiobookData) { + return ScanResult.NOTHING + } + audiobookData.ino = await getIno(audiobookData.fullPath) + return this.scanAudiobookData(audiobookData) + } + async fetchMetadata(id, trackIndex = 0) { var audiobook = this.audiobooks.find(a => a.id === id) if (!audiobook) { diff --git a/server/Server.js b/server/Server.js index d36250cc..2489b5ad 100644 --- a/server/Server.js +++ b/server/Server.js @@ -14,6 +14,7 @@ const StreamManager = require('./StreamManager') const RssFeeds = require('./RssFeeds') const DownloadManager = require('./DownloadManager') const Logger = require('./Logger') +const { ScanResult } = require('./utils/constants') class Server { constructor(PORT, CONFIG_PATH, METADATA_PATH, AUDIOBOOK_PATH) { @@ -75,8 +76,21 @@ class Server { }) } - async fileAddedUpdated({ path, fullPath }) { } - async fileRemoved({ path, fullPath }) { } + async newFilesAdded({ dir, files }) { + Logger.info(files.length, 'New Files Added in dir', dir) + var result = await this.scanner.scanAudiobook(dir) + Logger.info('New Files Added result', result) + } + async filesRemoved({ dir, files }) { + Logger.info(files.length, 'Files Removed in dir', dir) + var result = await this.scanner.scanAudiobook(dir) + Logger.info('Files Removed result', result) + } + async filesRenamed({ dir, files }) { + Logger.info(files.length, 'Files Renamed in dir', dir) + var result = await this.scanner.scanAudiobook(dir) + Logger.info('Files Renamed result', result) + } async scan() { Logger.info('[Server] Starting Scan') @@ -112,9 +126,9 @@ class Server { this.auth.init() this.watcher.initWatcher() - this.watcher.on('file_added', this.fileAddedUpdated.bind(this)) - this.watcher.on('file_removed', this.fileRemoved.bind(this)) - this.watcher.on('file_updated', this.fileAddedUpdated.bind(this)) + this.watcher.on('new_files', this.newFilesAdded.bind(this)) + this.watcher.on('removed_files', this.filesRemoved.bind(this)) + this.watcher.on('renamed_files', this.filesRenamed.bind(this)) } authMiddleware(req, res, next) { diff --git a/server/Watcher.js b/server/Watcher.js index 12835e39..3ee7e223 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -1,6 +1,8 @@ -var EventEmitter = require('events') -var Logger = require('./Logger') -var Watcher = require('watcher') +const Path = require('path') +const EventEmitter = require('events') +const Watcher = require('watcher') +const Logger = require('./Logger') +const { getIno } = require('./utils/index') class FolderWatcher extends EventEmitter { constructor(audiobookPath) { @@ -8,6 +10,11 @@ class FolderWatcher extends EventEmitter { this.AudiobookPath = audiobookPath this.folderMap = {} this.watcher = null + + this.pendingBatchDelay = 4000 + + // Audiobook paths with changes + this.pendingBatch = {} } initWatcher() { @@ -46,32 +53,69 @@ class FolderWatcher extends EventEmitter { return this.watcher.close() } - onNewFile(path) { + // After [pendingBatchDelay] seconds emit batch + async onNewFile(path) { Logger.debug('FolderWatcher: New File', path) - this.emit('file_added', { - path: path.replace(this.AudiobookPath, ''), - fullPath: path - }) + + var dir = Path.dirname(path) + if (this.pendingBatch[dir]) { + this.pendingBatch[dir].files.push(path) + clearTimeout(this.pendingBatch[dir].timeout) + } else { + this.pendingBatch[dir] = { + dir, + files: [path] + } + } + + this.pendingBatch[dir].timeout = setTimeout(() => { + this.emit('new_files', this.pendingBatch[dir]) + delete this.pendingBatch[dir] + }, this.pendingBatchDelay) } onFileRemoved(path) { Logger.debug('[FolderWatcher] File Removed', path) - this.emit('file_removed', { - path: path.replace(this.AudiobookPath, ''), - fullPath: path - }) + + var dir = Path.dirname(path) + if (this.pendingBatch[dir]) { + this.pendingBatch[dir].files.push(path) + clearTimeout(this.pendingBatch[dir].timeout) + } else { + this.pendingBatch[dir] = { + dir, + files: [path] + } + } + + this.pendingBatch[dir].timeout = setTimeout(() => { + this.emit('removed_files', this.pendingBatch[dir]) + delete this.pendingBatch[dir] + }, this.pendingBatchDelay) } onFileUpdated(path) { Logger.debug('[FolderWatcher] Updated File', path) - this.emit('file_updated', { - path: path.replace(this.AudiobookPath, ''), - fullPath: path - }) } onRename(pathFrom, pathTo) { Logger.debug(`[FolderWatcher] Rename ${pathFrom} => ${pathTo}`) + + var dir = Path.dirname(pathTo) + if (this.pendingBatch[dir]) { + this.pendingBatch[dir].files.push(pathTo) + clearTimeout(this.pendingBatch[dir].timeout) + } else { + this.pendingBatch[dir] = { + dir, + files: [pathTo] + } + } + + this.pendingBatch[dir].timeout = setTimeout(() => { + this.emit('renamed_files', this.pendingBatch[dir]) + delete this.pendingBatch[dir] + }, this.pendingBatchDelay) } } module.exports = FolderWatcher \ No newline at end of file diff --git a/server/utils/constants.js b/server/utils/constants.js new file mode 100644 index 00000000..cdac37a5 --- /dev/null +++ b/server/utils/constants.js @@ -0,0 +1,7 @@ +module.exports.ScanResult = { + NOTHING: 0, + ADDED: 1, + UPDATED: 2, + REMOVED: 3, + UPTODATE: 4 +} \ No newline at end of file diff --git a/server/utils/index.js b/server/utils/index.js index 8b14a6e5..5ea10706 100644 --- a/server/utils/index.js +++ b/server/utils/index.js @@ -32,17 +32,20 @@ module.exports.levenshteinDistance = levenshteinDistance const cleanString = (str) => { if (!str) return '' + // Now supporting all utf-8 characters, can remove this method in future + // replace accented characters: https://stackoverflow.com/a/49901740/7431543 - str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, "") + // str = str.normalize('NFD').replace(/[\u0300-\u036f]/g, "") - const availableChars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" - const cleanChar = (char) => availableChars.indexOf(char) < 0 ? '?' : char + // const availableChars = " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~" + // const cleanChar = (char) => availableChars.indexOf(char) < 0 ? '?' : char - var cleaned = '' - for (let i = 0; i < str.length; i++) { - cleaned += cleanChar(str[i]) - } - return cleaned + // var cleaned = '' + // for (let i = 0; i < str.length; i++) { + // cleaned += cleanChar(str[i]) + // } + + return cleaned.trim() } module.exports.cleanString = cleanString diff --git a/server/utils/scandir.js b/server/utils/scandir.js index aa697f03..bae3aafb 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -30,7 +30,54 @@ function getFileType(ext) { return 'unknown' } -async function getAllAudiobookFiles(abRootPath, serverSettings = {}) { +// Input relative filepath, output all details that can be parsed +function getAudiobookDataFromFilepath(abRootPath, relpath, parseSubtitle = false) { + var pathformat = Path.parse(relpath) + var path = pathformat.dir + + if (!path) { + Logger.error('Ignoring file in root dir', relpath) + return null + } + + // If relative file directory has 3 folders, then the middle folder will be series + var splitDir = path.split(Path.sep) + var author = null + if (splitDir.length > 1) author = splitDir.shift() + var series = null + if (splitDir.length > 1) series = splitDir.shift() + var title = splitDir.shift() + + var publishYear = null + var subtitle = null + + // If Title is of format 1999 - Title, then use 1999 as publish year + var publishYearMatch = title.match(/^([0-9]{4}) - (.+)/) + if (publishYearMatch && publishYearMatch.length > 2) { + if (!isNaN(publishYearMatch[1])) { + publishYear = publishYearMatch[1] + title = publishYearMatch[2] + } + } + + if (parseSubtitle && title.includes(' - ')) { + var splitOnSubtitle = title.split(' - ') + title = splitOnSubtitle.shift() + subtitle = splitOnSubtitle.join(' - ') + } + + return { + author, + title, + subtitle, + series, + publishYear, + path, // relative audiobook path i.e. /Author Name/Book Name/.. + fullPath: Path.join(abRootPath, path) // i.e. /audiobook/Author Name/Book Name/.. + } +} + +async function getAllAudiobookFileData(abRootPath, serverSettings = {}) { var parseSubtitle = !!serverSettings.scannerParseSubtitle var paths = await getPaths(abRootPath) @@ -38,59 +85,26 @@ async function getAllAudiobookFiles(abRootPath, serverSettings = {}) { paths.files.forEach((filepath) => { var relpath = Path.normalize(filepath).replace(abRootPath, '').slice(1) - var pathformat = Path.parse(relpath) - var path = pathformat.dir - - if (!path) { - Logger.error('Ignoring file in root dir', filepath) - return - } - - // If relative file directory has 3 folders, then the middle folder will be series - var splitDir = pathformat.dir.split(Path.sep) - var author = null - if (splitDir.length > 1) author = splitDir.shift() - var series = null - if (splitDir.length > 1) series = splitDir.shift() - var title = splitDir.shift() - - var publishYear = null - var subtitle = null - - // If Title is of format 1999 - Title, then use 1999 as publish year - var publishYearMatch = title.match(/^([0-9]{4}) - (.+)/) - if (publishYearMatch && publishYearMatch.length > 2) { - if (!isNaN(publishYearMatch[1])) { - publishYear = publishYearMatch[1] - title = publishYearMatch[2] - } - } - - if (parseSubtitle && title.includes(' - ')) { - var splitOnSubtitle = title.split(' - ') - title = splitOnSubtitle.shift() - subtitle = splitOnSubtitle.join(' - ') - } + var parsed = Path.parse(relpath) + var path = parsed.dir if (!audiobooks[path]) { + var audiobookData = getAudiobookDataFromFilepath(abRootPath, relpath, parseSubtitle) + if (!audiobookData) return + audiobooks[path] = { - author, - title, - subtitle, - series: cleanString(series), - publishYear: publishYear, - path: path, - fullPath: Path.join(abRootPath, path), + ...audiobookData, audioFiles: [], otherFiles: [] } } + var fileObj = { - filetype: getFileType(pathformat.ext), - filename: pathformat.base, + filetype: getFileType(parsed.ext), + filename: parsed.base, path: relpath, fullPath: filepath, - ext: pathformat.ext + ext: parsed.ext } if (fileObj.filetype === 'audio') { audiobooks[path].audioFiles.push(fileObj) @@ -100,4 +114,44 @@ async function getAllAudiobookFiles(abRootPath, serverSettings = {}) { }) return Object.values(audiobooks) } -module.exports.getAllAudiobookFiles = getAllAudiobookFiles \ No newline at end of file +module.exports.getAllAudiobookFileData = getAllAudiobookFileData + + +async function getAudiobookFileData(abRootPath, audiobookPath, serverSettings = {}) { + var parseSubtitle = !!serverSettings.scannerParseSubtitle + + var paths = await getPaths(audiobookPath) + var audiobook = null + + paths.files.forEach((filepath) => { + var relpath = Path.normalize(filepath).replace(abRootPath, '').slice(1) + + if (!audiobook) { + var audiobookData = getAudiobookDataFromFilepath(abRootPath, relpath, parseSubtitle) + if (!audiobookData) return + + audiobook = { + ...audiobookData, + audioFiles: [], + otherFiles: [] + } + } + + var extname = Path.extname(filepath) + var basename = Path.basename(filepath) + var fileObj = { + filetype: getFileType(extname), + filename: basename, + path: relpath, + fullPath: filepath, + ext: extname + } + if (fileObj.filetype === 'audio') { + audiobook.audioFiles.push(fileObj) + } else { + audiobook.otherFiles.push(fileObj) + } + }) + return audiobook +} +module.exports.getAudiobookFileData = getAudiobookFileData \ No newline at end of file