diff --git a/server/scanner/LibraryScanner.js b/server/scanner/LibraryScanner.js index bd0bb310..a52350f6 100644 --- a/server/scanner/LibraryScanner.js +++ b/server/scanner/LibraryScanner.js @@ -424,8 +424,8 @@ class LibraryScanner { } const folder = library.libraryFolders[0] - const relFilePaths = folderGroups[folderId].fileUpdates.map((fileUpdate) => fileUpdate.relPath) - const fileUpdateGroup = scanUtils.groupFilesIntoLibraryItemPaths(library.mediaType, relFilePaths) + const filePathItems = folderGroups[folderId].fileUpdates.map((fileUpdate) => fileUtils.getFilePathItemFromFileUpdate(fileUpdate)) + const fileUpdateGroup = scanUtils.groupFileItemsIntoLibraryItemDirs(library.mediaType, filePathItems, !!library.settings?.audiobooksOnly) if (!Object.keys(fileUpdateGroup).length) { Logger.info(`[LibraryScanner] No important changes to scan for in folder "${folderId}"`) diff --git a/server/utils/fileUtils.js b/server/utils/fileUtils.js index b0c73d6c..8b87d3a0 100644 --- a/server/utils/fileUtils.js +++ b/server/utils/fileUtils.js @@ -131,11 +131,21 @@ async function readTextFile(path) { } module.exports.readTextFile = readTextFile +/** + * @typedef FilePathItem + * @property {string} name - file name e.g. "audiofile.m4b" + * @property {string} path - fullpath excluding folder e.g. "Author/Book/audiofile.m4b" + * @property {string} reldirpath - path excluding file name e.g. "Author/Book" + * @property {string} fullpath - full path e.g. "/audiobooks/Author/Book/audiofile.m4b" + * @property {string} extension - file extension e.g. ".m4b" + * @property {number} deep - depth of file in directory (0 is file in folder root) + */ + /** * Get array of files inside dir * @param {string} path * @param {string} [relPathToReplace] - * @returns {{name:string, path:string, dirpath:string, reldirpath:string, fullpath:string, extension:string, deep:number}[]} + * @returns {FilePathItem[]} */ async function recurseFiles(path, relPathToReplace = null) { path = filePathToPOSIX(path) @@ -213,7 +223,6 @@ async function recurseFiles(path, relPathToReplace = null) { return { name: item.name, path: item.fullname.replace(relPathToReplace, ''), - dirpath: item.path, reldirpath: isInRoot ? '' : item.path.replace(relPathToReplace, ''), fullpath: item.fullname, extension: item.extension, @@ -228,6 +237,26 @@ async function recurseFiles(path, relPathToReplace = null) { } module.exports.recurseFiles = recurseFiles +/** + * + * @param {import('../Watcher').PendingFileUpdate} fileUpdate + * @returns {FilePathItem} + */ +module.exports.getFilePathItemFromFileUpdate = (fileUpdate) => { + let relPath = fileUpdate.relPath + if (relPath.startsWith('/')) relPath = relPath.slice(1) + + const dirname = Path.dirname(relPath) + return { + name: Path.basename(relPath), + path: relPath, + reldirpath: dirname === '.' ? '' : dirname, + fullpath: fileUpdate.path, + extension: Path.extname(relPath), + deep: relPath.split('/').length - 1 + } +} + /** * Download file from web to local file system * Uses SSRF filter to prevent internal URLs diff --git a/server/utils/scandir.js b/server/utils/scandir.js index 028a1022..a70e09bb 100644 --- a/server/utils/scandir.js +++ b/server/utils/scandir.js @@ -32,107 +32,6 @@ function checkFilepathIsAudioFile(filepath) { } module.exports.checkFilepathIsAudioFile = checkFilepathIsAudioFile -/** - * TODO: Function needs to be re-done - * @param {string} mediaType - * @param {string[]} paths array of relative file paths - * @returns {Record} map of files grouped into potential libarary item dirs - */ -function groupFilesIntoLibraryItemPaths(mediaType, paths) { - // Step 1: Clean path, Remove leading "/", Filter out non-media files in root dir - var nonMediaFilePaths = [] - var pathsFiltered = paths - .map((path) => { - return path.startsWith('/') ? path.slice(1) : path - }) - .filter((path) => { - let parsedPath = Path.parse(path) - // Is not in root dir OR is a book media file - if (parsedPath.dir) { - if (!isMediaFile(mediaType, parsedPath.ext, false)) { - // Seperate out non-media files - nonMediaFilePaths.push(path) - return false - } - return true - } else if (mediaType === 'book' && isMediaFile(mediaType, parsedPath.ext, false)) { - // (book media type supports single file audiobooks/ebooks in root dir) - return true - } - return false - }) - - // 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('/') - .filter((p) => !!p && p !== '.') // dirname returns . if no directory - var numparts = dirparts.length - var _path = '' - - 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|dis[ck])\s*\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 - } - } - } - }) - - // Step 4: Add in non-media files if they fit into item group - if (nonMediaFilePaths.length) { - for (const nonMediaFilePath of nonMediaFilePaths) { - const pathDir = Path.dirname(nonMediaFilePath) - const filename = Path.basename(nonMediaFilePath) - const dirparts = pathDir.split('/') - const numparts = dirparts.length - let _path = '' - - // Iterate over directories in path - for (let i = 0; i < numparts; i++) { - const dirpart = dirparts.shift() - _path = Path.posix.join(_path, dirpart) - if (itemGroup[_path]) { - // Directory is a group - const relpath = Path.posix.join(dirparts.join('/'), filename) - itemGroup[_path].push(relpath) - } else if (!dirparts.length) { - itemGroup[_path] = [filename] - } - } - } - } - - return itemGroup -} -module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths - /** * @param {string} mediaType * @param {{name:string, path:string, dirpath:string, reldirpath:string, fullpath:string, extension:string, deep:number}[]} fileItems (see recurseFiles)