diff --git a/client/components/modals/edit-tabs/Cover.vue b/client/components/modals/edit-tabs/Cover.vue index 22dc340f..5cc2d3d3 100644 --- a/client/components/modals/edit-tabs/Cover.vue +++ b/client/components/modals/edit-tabs/Cover.vue @@ -143,7 +143,7 @@ export default { .map((file) => { var _file = { ...file } var imgRelPath = _file.path.replace(this.audiobookPath, '') - _file.localPath = `/s/book/${this.audiobookId}${imgRelPath}` + _file.localPath = `/s/book/${this.audiobookId}/${imgRelPath}` return _file }) } diff --git a/client/package.json b/client/package.json index 4d2510f0..2570e765 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf-client", - "version": "1.6.6", + "version": "1.6.7", "description": "Audiobook manager and player", "main": "index.js", "scripts": { diff --git a/package-lock.json b/package-lock.json index 9dca5d5f..1473fb1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "1.6.2", + "version": "1.6.6", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -1222,9 +1222,9 @@ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "njodb": { - "version": "0.4.22", - "resolved": "https://registry.npmjs.org/njodb/-/njodb-0.4.22.tgz", - "integrity": "sha512-/paIiYKICyV/z540d27dF54y3Tv/DgY7MY/jQxcMLALyFpdYY/xSu+o78nko/3FpGBt34KPPlNOwnzLsy+WuxA==", + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/njodb/-/njodb-0.4.24.tgz", + "integrity": "sha512-d7S5mJJlEwWMhKblOE5BVKLnCubYuwZTLeoVq054GawnrxK3ATwauX/mkOKiZdBjbzrWoHg+dgatUkxoQIUvCA==", "requires": { "proper-lockfile": "^4.1.2" } @@ -1237,14 +1237,6 @@ "moment-timezone": "^0.5.31" } }, - "node-dir": { - "version": "0.1.17", - "resolved": "https://registry.npmjs.org/node-dir/-/node-dir-0.1.17.tgz", - "integrity": "sha1-X1Zl2TNRM1yqvvjxxVRRbPXx5OU=", - "requires": { - "minimatch": "^3.0.2" - } - }, "node-pre-gyp": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz", @@ -1526,6 +1518,11 @@ "minimatch": "^3.0.4" } }, + "recursive-readdir-async": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/recursive-readdir-async/-/recursive-readdir-async-1.1.8.tgz", + "integrity": "sha512-Iqosi7g1iTx2MkOYKQo5UmncQWBcKUPdbwBLizeZ9MXiCLKeaTLc/Mt2vUdBpmcEMIQGR8bOUT5sS/o19ufwrA==" + }, "resolve-alpn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.0.tgz", diff --git a/package.json b/package.json index 453257f3..a7bf7da4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "audiobookshelf", - "version": "1.6.6", + "version": "1.6.7", "description": "Self-hosted audiobook server for managing and playing audiobooks", "main": "index.js", "scripts": { @@ -38,12 +38,12 @@ "ip": "^1.1.5", "jsonwebtoken": "^8.5.1", "libgen": "^2.1.0", - "njodb": "^0.4.22", + "njodb": "^0.4.24", "node-cron": "^3.0.0", - "node-dir": "^0.1.17", "node-stream-zip": "^1.15.0", "podcast": "^1.3.0", "read-chunk": "^3.1.0", + "recursive-readdir-async": "^1.1.8", "socket.io": "^4.1.3", "watcher": "^1.2.0" }, diff --git a/server/CoverController.js b/server/CoverController.js index b22761ef..1532546f 100644 --- a/server/CoverController.js +++ b/server/CoverController.js @@ -11,8 +11,8 @@ const { CoverDestination } = require('./utils/constants') class CoverController { constructor(db, MetadataPath, AudiobookPath) { this.db = db - this.MetadataPath = MetadataPath - this.BookMetadataPath = Path.join(this.MetadataPath, 'books') + this.MetadataPath = MetadataPath.replace(/\\/g, '/') + this.BookMetadataPath = Path.posix.join(this.MetadataPath, 'books') this.AudiobookPath = AudiobookPath } @@ -24,8 +24,8 @@ class CoverController { } } else { return { - fullPath: Path.join(this.BookMetadataPath, audiobook.id), - relPath: Path.join('/metadata', 'books', audiobook.id) + fullPath: Path.posix.join(this.BookMetadataPath, audiobook.id), + relPath: Path.posix.join('/metadata', 'books', audiobook.id) } } } @@ -98,8 +98,8 @@ class CoverController { await fs.ensureDir(fullPath) var coverFilename = `cover${extname}` - var coverFullPath = Path.join(fullPath, coverFilename) - var coverPath = Path.join(relPath, coverFilename) + var coverFullPath = Path.posix.join(fullPath, coverFilename) + var coverPath = Path.posix.join(relPath, coverFilename) // Move cover from temp upload dir to destination var success = await coverFile.mv(coverFullPath).then(() => true).catch((error) => { @@ -143,7 +143,7 @@ class CoverController { var { fullPath, relPath } = this.getCoverDirectory(audiobook) await fs.ensureDir(fullPath) - var temppath = Path.join(fullPath, 'cover') + var temppath = Path.posix.join(fullPath, 'cover') var success = await this.downloadFile(url, temppath).then(() => true).catch((err) => { Logger.error(`[CoverController] Download image file failed for "${url}"`, err) return false @@ -161,8 +161,8 @@ class CoverController { } var coverFilename = `cover.${imgtype.ext}` - var coverPath = Path.join(relPath, coverFilename) - var coverFullPath = Path.join(fullPath, coverFilename) + var coverPath = Path.posix.join(relPath, coverFilename) + var coverFullPath = Path.posix.join(fullPath, coverFilename) await fs.rename(temppath, coverFullPath) await this.removeOldCovers(fullPath, '.' + imgtype.ext) diff --git a/server/Db.js b/server/Db.js index dc86c1c0..a7651c73 100644 --- a/server/Db.js +++ b/server/Db.js @@ -201,11 +201,6 @@ class Db { return true }).catch((error) => { Logger.error(`[DB] Update entity ${entityName} Failed: ${error}`) - - if (error && error.code === 'ENOENT') { - this.attemptDataRecovery(entityName) - } - return false }) } @@ -223,71 +218,6 @@ class Db { }) } - async attemptDataRecovery(entityName) { - var dbDirName = this.getEntityArrayKey(entityName) - var dbdir = Path.join(this.ConfigPath, dbDirName) - console.log('Attempting data recovery for:', dbdir) - - var exists = await fs.pathExists(dbdir) - if (!exists) { - console.error('Db dir does not exist', dbdir) - return - } - - try { - var dbdatadir = Path.join(dbdir, 'data') - var dbtmpdir = Path.join(dbdir, 'tmp') - - var datafiles = await fs.readdir(dbdatadir) - var tempfiles = await fs.readdir(dbtmpdir) - - var orphanOld = datafiles.find(df => df.endsWith('.old')) - if (orphanOld) { - // Get data file num - var dbnum = orphanOld.split('.')[1] - console.log('Found orphan json.old', orphanOld, `Num: ${dbnum}`) - - var dbDataFilename = `data.${dbnum}.json` - - // make sure data.#.json does not already exist - if (datafiles.includes(dbDataFilename)) { - console.warn(`${dbDataFilename} already exists, not recovering`) - return - } - - // find temp file that was supposed to be renamed - var matchingTmp = tempfiles.find(tmp => tmp.startsWith(`data.${dbnum}`)) - if (matchingTmp) { - console.log('found matching tmp file', matchingTmp) - - var tmpfileFullPath = Path.join(dbtmpdir, matchingTmp) - var renameToPath = Path.join(dbdatadir, dbDataFilename) - - console.log(`Renamining "${tmpfileFullPath}" => "${renameToPath}"`) - await fs.rename(tmpfileFullPath, renameToPath) - - console.log('Data recovery successful -- unlinking old') - - var orphanOldPath = Path.join(dbdatadir, orphanOld) - await fs.unlink(orphanOldPath) - console.log('Removed .old file') - - // Removing lock dir throws error in proper-lockfile - // var lockdirpath = Path.join(dbdatadir, `data.${dbnum}.json.lock`) - // var lockdirexists = await fs.pathExists(lockdirpath) - // if (lockdirexists) { - // await fs.rmdir(lockdirpath) - // console.log('Removed lock dir') - // } else { - // console.log('No lock dir found', lockdirpath) - // } - } - } - } catch (error) { - console.error('Data recovery failed', error) - } - } - recreateAudiobookDb() { return this.audiobooksDb.drop().then((results) => { Logger.info(`[DB] Dropped audiobook db`, results) diff --git a/server/DownloadManager.js b/server/DownloadManager.js index e87789c2..9f925976 100644 --- a/server/DownloadManager.js +++ b/server/DownloadManager.js @@ -240,11 +240,12 @@ class DownloadManager { } if (shouldIncludeCover) { - var _cover = audiobook.book.coverFullPath + var _cover = audiobook.book.coverFullPath.replace(/\\/g, '/') // Supporting old local file prefix - if (!_cover && audiobook.book.cover && audiobook.book.cover.startsWith(Path.sep + 'local')) { - _cover = Path.join(this.AudiobookPath, _cover.replace(Path.sep + 'local', '')) + var bookCoverPath = audiobook.book.cover ? audiobook.book.cover.replace(/\\/g, '/') : null + if (!_cover && bookCoverPath && bookCoverPath.startsWith('/local')) { + _cover = Path.posix.join(this.AudiobookPath.replace(/\\/g, '/'), _cover.replace('/local', '')) Logger.debug('Local cover url', _cover) } diff --git a/server/Scanner.js b/server/Scanner.js index 130db58c..5739e9de 100644 --- a/server/Scanner.js +++ b/server/Scanner.js @@ -17,7 +17,7 @@ class Scanner { constructor(AUDIOBOOK_PATH, METADATA_PATH, db, coverController, emitter) { this.AudiobookPath = AUDIOBOOK_PATH this.MetadataPath = METADATA_PATH - this.BookMetadataPath = Path.join(this.MetadataPath, 'books') + this.BookMetadataPath = Path.posix.join(this.MetadataPath.replace(/\\/g, '/'), 'books') this.db = db this.coverController = coverController @@ -42,8 +42,8 @@ class Scanner { } } else { return { - fullPath: Path.join(this.BookMetadataPath, audiobook.id), - relPath: Path.join('/metadata', 'books', audiobook.id) + fullPath: Path.posix.join(this.BookMetadataPath, audiobook.id), + relPath: Path.posix.join('/metadata', 'books', audiobook.id) } } } @@ -603,7 +603,7 @@ class Scanner { var bookGroupingResults = {} for (const bookDir in fileUpdateBookGroup) { - var fullPath = Path.join(folder.fullPath, bookDir) + var fullPath = Path.posix.join(folder.fullPath.replace(/\\/g, '/'), bookDir) // Check if book dir group is already an audiobook or in a subdir of an audiobook var existingAudiobook = this.db.audiobooks.find(ab => fullPath.startsWith(ab.fullPath)) diff --git a/server/Watcher.js b/server/Watcher.js index 3755dd0c..0de3bc29 100644 --- a/server/Watcher.js +++ b/server/Watcher.js @@ -119,6 +119,7 @@ class FolderWatcher extends EventEmitter { } addFileUpdate(libraryId, path, type) { + path = path.replace(/\\/g, '/') if (this.pendingFilePaths.includes(path)) return // Get file library @@ -129,20 +130,28 @@ class FolderWatcher extends EventEmitter { } // Get file folder - var folder = libwatcher.folders.find(fold => path.startsWith(fold.fullPath)) + var folder = libwatcher.folders.find(fold => path.startsWith(fold.fullPath.replace(/\\/g, '/'))) if (!folder) { Logger.error(`[Watcher] New file folder not found in library "${libwatcher.name}" with path "${path}"`) return } + var folderFullPath = folder.fullPath.replace(/\\/g, '/') // Check if file was added to root directory var dir = Path.dirname(path) - if (dir === folder.fullPath) { + if (dir === folderFullPath) { Logger.warn(`[Watcher] New file "${Path.basename(path)}" added to folder root - ignoring it`) return } - var relPath = path.replace(folder.fullPath, '') + var relPath = path.replace(folderFullPath, '') + + var hasDotPath = relPath.split('/').find(p => p.startsWith('.')) + if (hasDotPath) { + Logger.debug(`[Watcher] Ignoring dot path "${relPath}" | Piece "${hasDotPath}"`) + return + } + Logger.debug(`[Watcher] Modified file in library "${libwatcher.name}" and folder "${folder.id}" with relPath "${relPath}"`) this.pendingFileUpdates.push({ diff --git a/server/objects/Audiobook.js b/server/objects/Audiobook.js index be4528c2..69111c04 100644 --- a/server/objects/Audiobook.js +++ b/server/objects/Audiobook.js @@ -350,9 +350,9 @@ class Audiobook { if (this.otherFiles && this.otherFiles.length) { var imageFile = this.otherFiles.find(f => f.filetype === 'image') if (imageFile) { - data.coverFullPath = Path.normalize(imageFile.fullPath) + data.coverFullPath = imageFile.fullPath var relImagePath = imageFile.path.replace(this.path, '') - data.cover = Path.normalize(Path.join(`/s/book/${this.id}`, relImagePath)) + data.cover = Path.posix.join(`/s/book/${this.id}`, relImagePath) } } @@ -366,10 +366,10 @@ class Audiobook { return false } var updateBookPayload = {} - updateBookPayload.coverFullPath = Path.normalize(file.fullPath) + updateBookPayload.coverFullPath = file.fullPath // Set ab local static path from file relative path var relImagePath = file.path.replace(this.path, '') - updateBookPayload.cover = Path.normalize(Path.join(`/s/book/${this.id}`, relImagePath)) + updateBookPayload.cover = Path.posix.join(`/s/book/${this.id}`, relImagePath) return this.book.update(updateBookPayload) } @@ -556,23 +556,24 @@ class Audiobook { var oldFormat = this.book.cover // Update book cover path to new format - this.book.coverFullPath = Path.normalize(Path.join(this.fullPath, this.book.cover.substr(7))) - this.book.cover = Path.normalize(coverStripped.replace(this.path, `/s/book/${this.id}`)) + this.book.coverFullPath = Path.join(this.fullPath, this.book.cover.substr(7)).replace(/\\/g, '/') + this.book.cover = coverStripped.replace(this.path, `/s/book/${this.id}`) Logger.debug(`[Audiobook] updated book cover to new format "${oldFormat}" => "${this.book.cover}"`) } hasUpdates = true } // Check if book was removed from book dir - if (this.book.cover && this.book.cover.substr(1).startsWith('s\\book\\')) { + var bookCoverPath = this.book.cover ? this.book.cover.replace(/\\/g, '/') : null + if (bookCoverPath && bookCoverPath.startsWith('/s/book/')) { // Fixing old cover paths if (!this.book.coverFullPath) { - this.book.coverFullPath = Path.normalize(Path.join(this.fullPath, this.book.cover.substr(`/s/book/${this.id}`.length))) + this.book.coverFullPath = Path.join(this.fullPath, this.book.cover.substr(`/s/book/${this.id}`.length)).replace(/\\/g, '/').replace(/\/\//g, '/') Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`) hasUpdates = true } - var coverStillExists = imageFiles.find(f => f.fullPath === this.book.coverFullPath) + var coverStillExists = imageFiles.find(f => comparePaths(f.fullPath, this.book.coverFullPath)) if (!coverStillExists) { Logger.info(`[Audiobook] Local cover "${this.book.cover}" was removed | "${this.title}"`) this.book.removeCover() @@ -580,14 +581,14 @@ class Audiobook { } } - if (this.book.cover && this.book.cover.substr(1).startsWith('metadata')) { + if (bookCoverPath && bookCoverPath.startsWith('/metadata')) { // Fixing old cover paths if (!this.book.coverFullPath) { - this.book.coverFullPath = Path.normalize(Path.join(metadataPath, this.book.cover.substr('/metadata/'.length))) + this.book.coverFullPath = Path.join(metadataPath, this.book.cover.substr('/metadata/'.length)).replace(/\\/g, '/').replace(/\/\//g, '/') Logger.debug(`[Audiobook] Metadata cover full path set "${this.book.coverFullPath}" for "${this.title}"`) hasUpdates = true } - var coverStillExists = imageFiles.find(f => f.fullPath === this.book.coverFullPath) + var coverStillExists = imageFiles.find(f => comparePaths(f.fullPath, this.book.coverFullPath)) if (!coverStillExists) { Logger.info(`[Audiobook] Metadata cover "${this.book.cover}" was removed | "${this.title}"`) this.book.removeCover() @@ -608,7 +609,7 @@ class Audiobook { // If no cover set and image file exists then use it if (!this.book.cover && imageFiles.length) { var imagePathRelativeToBook = imageFiles[0].path.replace(this.path, '') - this.book.cover = Path.normalize(Path.join(`/s/book/${this.id}`, imagePathRelativeToBook)) + this.book.cover = Path.posix.join(`/s/book/${this.id}`, imagePathRelativeToBook) this.book.coverFullPath = imageFiles[0].fullPath Logger.info(`[Audiobook] Local cover was set to "${this.book.cover}" | "${this.title}"`) hasUpdates = true @@ -733,7 +734,7 @@ class Audiobook { var success = await extractCoverArt(audioFileWithCover.fullPath, coverFilePath) if (success) { - var coverRelPath = Path.join(coverDirRelPath, coverFilename) + var coverRelPath = Path.join(coverDirRelPath, coverFilename).replace(/\\/g, '/').replace(/\/\//g, '/') this.update({ book: { cover: coverRelPath } }) return coverRelPath } diff --git a/server/objects/Book.js b/server/objects/Book.js index af28c7b3..d89b0ead 100644 --- a/server/objects/Book.js +++ b/server/objects/Book.js @@ -136,18 +136,18 @@ class Book { update(payload) { var hasUpdates = false - // Normalize cover paths if passed + // Clean cover paths if passed if (payload.cover) { if (!payload.cover.startsWith('http:') && !payload.cover.startsWith('https:')) { - payload.cover = Path.normalize(payload.cover) - if (payload.coverFullPath) payload.coverFullPath = Path.normalize(payload.coverFullPath) + payload.cover = payload.cover.replace(/\\/g, '/') + if (payload.coverFullPath) payload.coverFullPath = payload.coverFullPath.replace(/\\/g, '/') else { Logger.warn(`[Book] "${this.title}" updating book cover to "${payload.cover}" but no full path was passed`) } } } else if (payload.coverFullPath) { Logger.warn(`[Book] "${this.title}" updating book full cover path to "${payload.coverFullPath}" but no relative path was passed`) - payload.coverFullPath = Path.normalize(payload.coverFullPath) + payload.coverFullPath = payload.coverFullPath.replace(/\\/g, '/') } for (const key in payload) { @@ -191,8 +191,8 @@ class Book { updateCover(cover, coverFullPath) { if (!cover) return false if (!cover.startsWith('http:') && !cover.startsWith('https:')) { - cover = Path.normalize(cover) - this.coverFullPath = Path.normalize(coverFullPath) + cover = cover.replace(/\\/g, '/') + this.coverFullPath = coverFullPath.replace(/\\/g, '/') } else { this.coverFullPath = cover } diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index 098c4992..fa8a4927 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -1,4 +1,5 @@ const fs = require('fs-extra') +const rra = require('recursive-readdir-async') const Logger = require('../Logger') async function getFileStat(path) { @@ -86,3 +87,58 @@ function setFileOwner(path, uid, gid) { } } module.exports.setFileOwner = setFileOwner + +async function recurseFiles(path) { + path = path.replace(/\\/g, '/') + if (!path.endsWith('/')) path = path + '/' + + const options = { + mode: rra.LIST, + recursive: true, + stats: false, + ignoreFolders: true, + extensions: true, + deep: true, + realPath: true, + normalizePath: true + } + var list = await rra.list(path, options) + if (list.error) { + Logger.error('[fileUtils] Recurse files error', list.error) + return [] + } + + list = list.filter((item) => { + if (item.error) { + Logger.error(`[fileUtils] Recurse files file "${item.fullName}" has error`, item.error) + return false + } + + // Ignore any file if a directory or the filename starts with "." + var relpath = item.fullname.replace(path, '') + var pathStartsWithPeriod = relpath.split('/').find(p => p.startsWith('.')) + if (pathStartsWithPeriod) { + Logger.debug(`[fileUtils] Ignoring path has . "${relpath}"`) + return false + } + + return true + }).map((item) => ({ + name: item.name, + path: item.fullname.replace(path, ''), + dirpath: item.path, + reldirpath: item.path.replace(path, ''), + fullpath: item.fullname, + extension: item.extension, + deep: item.deep + })) + + // Sort from least deep to most + list.sort((a, b) => a.deep - b.deep) + + // list.forEach((l) => { + // console.log(`${l.deep}: ${l.path}`) + // }) + return list +} +module.exports.recurseFiles = recurseFiles \ No newline at end of file diff --git a/server/utils/scandir.js b/server/utils/scandir.js index c7f28e60..b70e27cc 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -1,22 +1,10 @@ const Path = require('path') const fs = require('fs-extra') -const dir = require('node-dir') const Logger = require('../Logger') const { getIno } = require('./index') +const { recurseFiles } = require('./fileUtils') const globals = require('./globals') -function getPaths(path) { - return new Promise((resolve) => { - dir.paths(path, function (err, res) { - if (err) { - Logger.error(err) - resolve(false) - } - resolve(res) - }) - }) -} - function isBookFile(path) { if (!path) return false var ext = Path.extname(path) @@ -25,43 +13,36 @@ function isBookFile(path) { 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 audiobook dirs function groupFilesIntoAudiobookPaths(paths, useAllFileTypes = false) { - // Step 1: Normalize path, Remove leading "/", Filter out files in root dir - var pathsFiltered = paths.map(path => Path.normalize(path.slice(1))).filter(path => Path.parse(path).dir) + // 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) // Step 2: Sort by least number of directories pathsFiltered.sort((a, b) => { - var pathsA = Path.dirname(a).split(Path.sep).length - var pathsB = Path.dirname(b).split(Path.sep).length + var pathsA = Path.dirname(a).split('/').length + var pathsB = Path.dirname(b).split('/').length return pathsA - pathsB }) - // Step 2.5: Seperate audio/ebook files and other files (optional) - // - Directories without an audio or ebook file will not be included - var bookFilePaths = [] - var otherFilePaths = [] - pathsFiltered.forEach(path => { - if (isBookFile(path) || useAllFileTypes) bookFilePaths.push(path) - else otherFilePaths.push(path) - }) - - // Step 3: Group audio files in audiobooks + // Step 3: Group files in dirs var audiobookGroup = {} - bookFilePaths.forEach((path) => { - var dirparts = Path.dirname(path).split(Path.sep) + pathsFiltered.forEach((path) => { + var dirparts = Path.dirname(path).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.join(_path, dirpart) - + _path = Path.posix.join(_path, dirpart) if (audiobookGroup[_path]) { // Directory already has files, add file - var relpath = Path.join(dirparts.join(Path.sep), Path.basename(path)) + var relpath = Path.posix.join(dirparts.join('/'), Path.basename(path)) audiobookGroup[_path].push(relpath) return } else if (!dirparts.length) { // This is the last directory, create group @@ -70,19 +51,60 @@ function groupFilesIntoAudiobookPaths(paths, useAllFileTypes = false) { } } }) + return audiobookGroup +} +module.exports.groupFilesIntoAudiobookPaths = groupFilesIntoAudiobookPaths - // Step 4: Add other files into audiobook groups - otherFilePaths.forEach((path) => { - var dirparts = Path.dirname(path).split(Path.sep) +// Input: array of relative file items (see recurseFiles) +// Output: map of files grouped into potential audiobook dirs +function groupFileItemsIntoBooks(fileItems) { + // Step 1: Filter out files in root dir (with depth of 0) + var itemsFiltered = fileItems.filter(i => i.deep > 0) + + // Step 2: Seperate audio/ebook files and other files + // - Directories without an audio or ebook file will not be included + var bookFileItems = [] + var otherFileItems = [] + itemsFiltered.forEach(item => { + if (isBookFile(item.fullpath)) bookFileItems.push(item) + else otherFileItems.push(item) + }) + + // Step 3: Group audio files in audiobooks + var audiobookGroup = {} + bookFileItems.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.join(_path, dirpart) + _path = Path.posix.join(_path, dirpart) + + if (audiobookGroup[_path]) { // Directory already has files, add file + var relpath = Path.posix.join(dirparts.join('/'), item.name) + audiobookGroup[_path].push(relpath) + return + } else if (!dirparts.length) { // This is the last directory, create group + audiobookGroup[_path] = [item.name] + return + } + } + }) + + // Step 4: Add other files into audiobook 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 (audiobookGroup[_path]) { // Directory is audiobook group - var relpath = Path.join(dirparts.join(Path.sep), Path.basename(path)) + var relpath = Path.posix.join(dirparts.join('/'), item.name) audiobookGroup[_path].push(relpath) return } @@ -90,7 +112,6 @@ function groupFilesIntoAudiobookPaths(paths, useAllFileTypes = false) { }) return audiobookGroup } -module.exports.groupFilesIntoAudiobookPaths = groupFilesIntoAudiobookPaths function cleanFileObjects(basepath, abrelpath, files) { return files.map((file) => { @@ -98,8 +119,8 @@ function cleanFileObjects(basepath, abrelpath, files) { return { filetype: getFileType(ext), filename: Path.basename(file), - path: Path.join(abrelpath, file), // /AUDIOBOOK/PATH/filename.mp3 - fullPath: Path.join(basepath, file), // /audiobooks/AUDIOBOOK/PATH/filename.mp3 + path: Path.posix.join(abrelpath, file), // /AUDIOBOOK/PATH/filename.mp3 + fullPath: Path.posix.join(basepath, file), // /audiobooks/AUDIOBOOK/PATH/filename.mp3 ext: ext } }) @@ -118,7 +139,7 @@ function getFileType(ext) { // Scan folder async function scanRootDir(folder, serverSettings = {}) { - var folderPath = folder.fullPath + var folderPath = folder.fullPath.replace(/\\/g, '/') var parseSubtitle = !!serverSettings.scannerParseSubtitle var pathExists = await fs.pathExists(folderPath) @@ -127,15 +148,12 @@ async function scanRootDir(folder, serverSettings = {}) { return [] } - var pathdata = await getPaths(folderPath) - var filepaths = pathdata.files.map(filepath => { - return Path.normalize(filepath).replace(folderPath, '') - }) + var fileItems = await recurseFiles(folderPath) - var audiobookGrouping = groupFilesIntoAudiobookPaths(filepaths) + var audiobookGrouping = groupFileItemsIntoBooks(fileItems) if (!Object.keys(audiobookGrouping).length) { - Logger.error('Root path has no audiobooks', filepaths) + Logger.error('Root path has no books', fileItems.length) return [] } @@ -163,7 +181,8 @@ module.exports.scanRootDir = scanRootDir // Input relative filepath, output all details that can be parsed function getAudiobookDataFromDir(folderPath, dir, parseSubtitle = false) { - var splitDir = dir.split(Path.sep) + dir = dir.replace(/\\/g, '/') + var splitDir = dir.split('/') // Audio files will always be in the directory named for the title var title = splitDir.pop() @@ -240,25 +259,20 @@ function getAudiobookDataFromDir(folderPath, dir, parseSubtitle = false) { volumeNumber, publishYear, path: dir, // relative audiobook path i.e. /Author Name/Book Name/.. - fullPath: Path.join(folderPath, dir) // i.e. /audiobook/Author Name/Book Name/.. + fullPath: Path.posix.join(folderPath, dir) // i.e. /audiobook/Author Name/Book Name/.. } } async function getAudiobookFileData(folder, audiobookPath, serverSettings = {}) { var parseSubtitle = !!serverSettings.scannerParseSubtitle - var paths = await getPaths(audiobookPath) - var filepaths = paths.files + var fileItems = await recurseFiles(audiobookPath) - // Sort by least number of directories - filepaths.sort((a, b) => { - var pathsA = Path.dirname(a).split(Path.sep).length - var pathsB = Path.dirname(b).split(Path.sep).length - return pathsA - pathsB - }) + audiobookPath = audiobookPath.replace(/\\/g, '/') + var folderFullPath = folder.fullPath.replace(/\\/g, '/') - var audiobookDir = Path.normalize(audiobookPath).replace(folder.fullPath, '').slice(1) - var audiobookData = getAudiobookDataFromDir(folder.fullPath, audiobookDir, parseSubtitle) + var audiobookDir = audiobookPath.replace(folderFullPath, '').slice(1) + var audiobookData = getAudiobookDataFromDir(folderFullPath, audiobookDir, parseSubtitle) var audiobook = { ino: await getIno(audiobookData.fullPath), folderId: folder.id, @@ -268,20 +282,17 @@ async function getAudiobookFileData(folder, audiobookPath, serverSettings = {}) otherFiles: [] } - for (let i = 0; i < filepaths.length; i++) { - var filepath = filepaths[i] + for (let i = 0; i < fileItems.length; i++) { + var fileItem = fileItems[i] - var relpath = Path.normalize(filepath).replace(folder.fullPath, '').slice(1) - var extname = Path.extname(filepath) - var basename = Path.basename(filepath) - var ino = await getIno(filepath) + var ino = await getIno(fileItem.fullpath) var fileObj = { ino, - filetype: getFileType(extname), - filename: basename, - path: relpath, - fullPath: filepath, - ext: extname + filetype: getFileType(fileItem.extension), + filename: fileItem.name, + path: fileItem.path, + fullPath: fileItem.fullpath, + ext: fileItem.extension } if (fileObj.filetype === 'audio') { audiobook.audioFiles.push(fileObj)