mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Fix:Server crash when deleting library item #2031
This commit is contained in:
		
							parent
							
								
									a3899b68e1
								
							
						
					
					
						commit
						a38e43213d
					
				| @ -34,9 +34,12 @@ class SocketAuthority { | |||||||
|     return Object.values(this.clients).filter(c => c.user && c.user.id === userId) |     return Object.values(this.clients).filter(c => c.user && c.user.id === userId) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Emits event to all authorized clients
 |   /** | ||||||
|   //  optional filter function to only send event to specific users
 |    * Emits event to all authorized clients | ||||||
|   //  TODO: validate that filter is actually a function
 |    * @param {string} evt  | ||||||
|  |    * @param {any} data  | ||||||
|  |    * @param {Function} [filter] optional filter function to only send event to specific users | ||||||
|  |    */ | ||||||
|   emitter(evt, data, filter = null) { |   emitter(evt, data, filter = null) { | ||||||
|     for (const socketId in this.clients) { |     for (const socketId in this.clients) { | ||||||
|       if (this.clients[socketId].user) { |       if (this.clients[socketId].user) { | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								server/scanner/AudioFileScanner.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								server/scanner/AudioFileScanner.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | class AudioFileScanner { | ||||||
|  |   constructor() { } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | module.exports = new AudioFileScanner() | ||||||
							
								
								
									
										147
									
								
								server/scanner/LibraryItemScanData.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								server/scanner/LibraryItemScanData.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,147 @@ | |||||||
|  | const packageJson = require('../../package.json') | ||||||
|  | const { LogLevel } = require('../utils/constants') | ||||||
|  | const LibraryItem = require('../models/LibraryItem') | ||||||
|  | 
 | ||||||
|  | class LibraryItemScanData { | ||||||
|  |   constructor(data) { | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.libraryFolderId = data.libraryFolderId | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.libraryId = data.libraryId | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.ino = data.ino | ||||||
|  |     /** @type {number} */ | ||||||
|  |     this.mtimeMs = data.mtimeMs | ||||||
|  |     /** @type {number} */ | ||||||
|  |     this.ctimeMs = data.ctimeMs | ||||||
|  |     /** @type {number} */ | ||||||
|  |     this.birthtimeMs = data.birthtimeMs | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.path = data.path | ||||||
|  |     /** @type {string} */ | ||||||
|  |     this.relPath = data.relPath | ||||||
|  |     /** @type {boolean} */ | ||||||
|  |     this.isFile = data.isFile | ||||||
|  |     /** @type {{title:string, subtitle:string, series:string, sequence:string, publishedYear:string, narrators:string}} */ | ||||||
|  |     this.mediaMetadata = data.mediaMetadata | ||||||
|  |     /** @type {import('../objects/files/LibraryFile')[]} */ | ||||||
|  |     this.libraryFiles = data.libraryFiles | ||||||
|  | 
 | ||||||
|  |     // Set after check
 | ||||||
|  |     /** @type {boolean} */ | ||||||
|  |     this.hasChanges | ||||||
|  |     /** @type {boolean} */ | ||||||
|  |     this.hasPathChange | ||||||
|  |     /** @type {LibraryItem.LibraryFileObject[]} */ | ||||||
|  |     this.libraryFilesRemoved | ||||||
|  |     /** @type {LibraryItem.LibraryFileObject[]} */ | ||||||
|  |     this.libraryFilesAdded | ||||||
|  |     /** @type {LibraryItem.LibraryFileObject[]} */ | ||||||
|  |     this.libraryFilesModified | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    *  | ||||||
|  |    * @param {LibraryItem} existingLibraryItem  | ||||||
|  |    * @param {import('./LibraryScan')} libraryScan | ||||||
|  |    */ | ||||||
|  |   async checkLibraryItemData(existingLibraryItem, libraryScan) { | ||||||
|  |     const keysToCompare = ['libraryFolderId', 'ino', 'mtimeMs', 'ctimeMs', 'birthtimeMs', 'path', 'relPath', 'isFile'] | ||||||
|  |     this.hasChanges = false | ||||||
|  |     this.hasPathChange = false | ||||||
|  |     for (const key of keysToCompare) { | ||||||
|  |       if (existingLibraryItem[key] !== this[key]) { | ||||||
|  |         libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.relPath}" key "${key}" changed from "${existingLibraryItem[key]}" to "${this[key]}"`) | ||||||
|  |         existingLibraryItem[key] = this[key] | ||||||
|  |         this.hasChanges = true | ||||||
|  | 
 | ||||||
|  |         if (key === 'relPath') { | ||||||
|  |           this.hasPathChange = true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.libraryFilesRemoved = [] | ||||||
|  |     this.libraryFilesModified = [] | ||||||
|  |     let libraryFilesAdded = this.libraryFiles.map(lf => lf) | ||||||
|  | 
 | ||||||
|  |     for (const existingLibraryFile of existingLibraryItem.libraryFiles) { | ||||||
|  |       // Find matching library file using path first and fallback to using inode value
 | ||||||
|  |       let matchingLibraryFile = this.libraryFiles.find(lf => lf.metadata.path === existingLibraryFile.metadata.path) | ||||||
|  |       if (!matchingLibraryFile) { | ||||||
|  |         matchingLibraryFile = this.libraryFiles.find(lf => lf.ino === existingLibraryFile.ino) | ||||||
|  |         if (matchingLibraryFile) { | ||||||
|  |           libraryScan.addLog(LogLevel.INFO, `Library file with path "${existingLibraryFile.metadata.path}" not found, but found file with matching inode value "${existingLibraryFile.ino}" at path "${matchingLibraryFile.metadata.path}"`) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (!matchingLibraryFile) { // Library file removed
 | ||||||
|  |         libraryScan.addLog(LogLevel.INFO, `Library file "${existingLibraryFile.metadata.path}" was removed from library item "${existingLibraryItem.path}"`) | ||||||
|  |         this.libraryFilesRemoved.push(existingLibraryFile) | ||||||
|  |         existingLibraryItem.libraryFiles = existingLibraryItem.libraryFiles.filter(lf => lf !== existingLibraryFile) | ||||||
|  |         this.hasChanges = true | ||||||
|  |       } else { | ||||||
|  |         libraryFilesAdded = libraryFilesAdded.filter(lf => lf !== matchingLibraryFile) | ||||||
|  |         if (this.compareUpdateLibraryFile(existingLibraryItem.path, existingLibraryFile, matchingLibraryFile, libraryScan)) { | ||||||
|  |           this.libraryFilesModified.push(existingLibraryFile) | ||||||
|  |           this.hasChanges = true | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Log new library files found
 | ||||||
|  |     if (libraryFilesAdded.length) { | ||||||
|  |       this.hasChanges = true | ||||||
|  |       for (const libraryFile of libraryFilesAdded) { | ||||||
|  |         libraryScan.addLog(LogLevel.INFO, `New library file found with path "${libraryFile.metadata.path}" for library item "${existingLibraryItem.path}"`) | ||||||
|  |         existingLibraryItem.libraryFiles.push(libraryFile.toJSON()) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (this.hasChanges) { | ||||||
|  |       existingLibraryItem.lastScan = Date.now() | ||||||
|  |       existingLibraryItem.lastScanVersion = packageJson.version | ||||||
|  |       await existingLibraryItem.save() | ||||||
|  |     } else { | ||||||
|  |       libraryScan.addLog(LogLevel.DEBUG, `Library item "${existingLibraryItem.path}" is up-to-date`) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.libraryFilesAdded = libraryFilesAdded | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Update existing library file with scanned in library file data | ||||||
|  |    * @param {string} libraryItemPath | ||||||
|  |    * @param {LibraryItem.LibraryFileObject} existingLibraryFile  | ||||||
|  |    * @param {import('../objects/files/LibraryFile')} scannedLibraryFile  | ||||||
|  |    * @param {import('./LibraryScan')} libraryScan | ||||||
|  |    * @returns {boolean} false if no changes | ||||||
|  |    */ | ||||||
|  |   compareUpdateLibraryFile(libraryItemPath, existingLibraryFile, scannedLibraryFile, libraryScan) { | ||||||
|  |     let hasChanges = false | ||||||
|  | 
 | ||||||
|  |     if (existingLibraryFile.ino !== scannedLibraryFile.ino) { | ||||||
|  |       existingLibraryFile.ino = scannedLibraryFile.ino | ||||||
|  |       hasChanges = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (const key in existingLibraryFile.metadata) { | ||||||
|  |       if (existingLibraryFile.metadata[key] !== scannedLibraryFile.metadata[key]) { | ||||||
|  |         if (key !== 'path' && key !== 'relPath') { | ||||||
|  |           libraryScan.addLog(LogLevel.DEBUG, `Library file "${existingLibraryFile.metadata.path}" for library item "${libraryItemPath}" key "${key}" changed from "${existingLibraryFile.metadata[key]}" to "${scannedLibraryFile.metadata[key]}"`) | ||||||
|  |         } else { | ||||||
|  |           libraryScan.addLog(LogLevel.DEBUG, `Library file for library item "${libraryItemPath}" key "${key}" changed from "${existingLibraryFile.metadata[key]}" to "${scannedLibraryFile.metadata[key]}"`) | ||||||
|  |         } | ||||||
|  |         existingLibraryFile.metadata[key] = scannedLibraryFile.metadata[key] | ||||||
|  |         hasChanges = true | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (hasChanges) { | ||||||
|  |       existingLibraryFile.updatedAt = Date.now() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return hasChanges | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports = LibraryItemScanData | ||||||
							
								
								
									
										230
									
								
								server/scanner/LibraryScanner.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										230
									
								
								server/scanner/LibraryScanner.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,230 @@ | |||||||
|  | const Path = require('path') | ||||||
|  | const packageJson = require('../../package.json') | ||||||
|  | const Logger = require('../Logger') | ||||||
|  | const SocketAuthority = require('../SocketAuthority') | ||||||
|  | const Database = require('../Database') | ||||||
|  | const fs = require('../libs/fsExtra') | ||||||
|  | const fileUtils = require('../utils/fileUtils') | ||||||
|  | const scanUtils = require('../utils/scandir') | ||||||
|  | const { ScanResult, LogLevel } = require('../utils/constants') | ||||||
|  | const LibraryItemScanData = require('./LibraryItemScanData') | ||||||
|  | 
 | ||||||
|  | class LibraryScanner { | ||||||
|  |   constructor(coverManager, taskManager) { | ||||||
|  |     this.coverManager = coverManager | ||||||
|  |     this.taskManager = taskManager | ||||||
|  | 
 | ||||||
|  |     this.cancelLibraryScan = {} | ||||||
|  |     this.librariesScanning = [] | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * @param {string} libraryId  | ||||||
|  |    * @returns {boolean} | ||||||
|  |    */ | ||||||
|  |   isLibraryScanning(libraryId) { | ||||||
|  |     return this.librariesScanning.some(ls => ls.id === libraryId) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    *  | ||||||
|  |    * @param {import('../objects/Library')} library  | ||||||
|  |    * @param {*} options  | ||||||
|  |    */ | ||||||
|  |   async scan(library, options = {}) { | ||||||
|  |     if (this.isLibraryScanning(library.id)) { | ||||||
|  |       Logger.error(`[Scanner] Already scanning ${library.id}`) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!library.folders.length) { | ||||||
|  |       Logger.warn(`[Scanner] Library has no folders to scan "${library.name}"`) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const scanOptions = new ScanOptions() | ||||||
|  |     scanOptions.setData(options, Database.serverSettings) | ||||||
|  | 
 | ||||||
|  |     const libraryScan = new LibraryScan() | ||||||
|  |     libraryScan.setData(library, scanOptions) | ||||||
|  |     libraryScan.verbose = true | ||||||
|  |     this.librariesScanning.push(libraryScan.getScanEmitData) | ||||||
|  | 
 | ||||||
|  |     SocketAuthority.emitter('scan_start', libraryScan.getScanEmitData) | ||||||
|  | 
 | ||||||
|  |     Logger.info(`[Scanner] Starting library scan ${libraryScan.id} for ${libraryScan.libraryName}`) | ||||||
|  | 
 | ||||||
|  |     const canceled = await this.scanLibrary(libraryScan) | ||||||
|  | 
 | ||||||
|  |     if (canceled) { | ||||||
|  |       Logger.info(`[Scanner] Library scan canceled for "${libraryScan.libraryName}"`) | ||||||
|  |       delete this.cancelLibraryScan[libraryScan.libraryId] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     libraryScan.setComplete() | ||||||
|  | 
 | ||||||
|  |     Logger.info(`[Scanner] Library scan ${libraryScan.id} completed in ${libraryScan.elapsedTimestamp} | ${libraryScan.resultStats}`) | ||||||
|  |     this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id) | ||||||
|  | 
 | ||||||
|  |     if (canceled && !libraryScan.totalResults) { | ||||||
|  |       const emitData = libraryScan.getScanEmitData | ||||||
|  |       emitData.results = null | ||||||
|  |       SocketAuthority.emitter('scan_complete', emitData) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     SocketAuthority.emitter('scan_complete', libraryScan.getScanEmitData) | ||||||
|  | 
 | ||||||
|  |     if (libraryScan.totalResults) { | ||||||
|  |       libraryScan.saveLog() | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    *  | ||||||
|  |    * @param {import('./LibraryScan')} libraryScan  | ||||||
|  |    */ | ||||||
|  |   async scanLibrary(libraryScan) { | ||||||
|  |     /** @type {LibraryItemScanData[]} */ | ||||||
|  |     let libraryItemDataFound = [] | ||||||
|  | 
 | ||||||
|  |     // Scan each library folder
 | ||||||
|  |     for (let i = 0; i < libraryScan.folders.length; i++) { | ||||||
|  |       const folder = libraryScan.folders[i] | ||||||
|  |       const itemDataFoundInFolder = await this.scanFolder(libraryScan.library, folder) | ||||||
|  |       libraryScan.addLog(LogLevel.INFO, `${itemDataFoundInFolder.length} item data found in folder "${folder.fullPath}"`) | ||||||
|  |       libraryItemDataFound = libraryItemDataFound.concat(itemDataFoundInFolder) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (this.cancelLibraryScan[libraryScan.libraryId]) return true | ||||||
|  | 
 | ||||||
|  |     const existingLibraryItems = await Database.libraryItemModel.findAll({ | ||||||
|  |       where: { | ||||||
|  |         libraryId: libraryScan.libraryId | ||||||
|  |       }, | ||||||
|  |       attributes: ['id', 'mediaId', 'mediaType', 'path', 'relPath', 'ino', 'isMissing', 'mtime', 'ctime', 'birthtime', 'libraryFiles', 'libraryFolderId'] | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const libraryItemIdsMissing = [] | ||||||
|  |     for (const existingLibraryItem of existingLibraryItems) { | ||||||
|  |       // First try to find matching library item with exact file path
 | ||||||
|  |       let libraryItemData = libraryItemDataFound.find(lid => lid.path === existingLibraryItem.path) | ||||||
|  |       if (!libraryItemData) { | ||||||
|  |         // Fallback to finding matching library item with matching inode value
 | ||||||
|  |         libraryItemData = libraryItemDataFound.find(lid => lid.ino === existingLibraryItem.ino) | ||||||
|  |         if (libraryItemData) { | ||||||
|  |           libraryScan.addLog(LogLevel.INFO, `Library item with path "${existingLibraryItem.path}" was not found, but library item inode "${existingLibraryItem.ino}" was found at path "${libraryItemData.path}"`) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       if (!libraryItemData) { | ||||||
|  |         // Podcast folder can have no episodes and still be valid
 | ||||||
|  |         if (libraryScan.libraryMediaType === 'podcast' && await fs.pathExists(existingLibraryItem.path)) { | ||||||
|  |           libraryScan.addLog(LogLevel.INFO, `Library item "${existingLibraryItem.relPath}" folder exists but has no episodes`) | ||||||
|  |         } else { | ||||||
|  |           libraryScan.addLog(LogLevel.WARN, `Library Item "${existingLibraryItem.path}" (inode: ${existingLibraryItem.ino}) is missing`) | ||||||
|  |           if (!existingLibraryItem.isMissing) { | ||||||
|  |             libraryItemIdsMissing.push(existingLibraryItem.id) | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } else { | ||||||
|  |         await libraryItemData.checkLibraryItemData(existingLibraryItem, libraryScan) | ||||||
|  |         if (libraryItemData.hasChanges) { | ||||||
|  |           await this.rescanLibraryItem(existingLibraryItem, libraryItemData) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Update missing library items
 | ||||||
|  |     if (libraryItemIdsMissing.length) { | ||||||
|  |       libraryScan.addLog(LogLevel.INFO, `Updating ${libraryItemIdsMissing.length} library items missing`) | ||||||
|  |       await Database.libraryItemModel.update({ | ||||||
|  |         isMissing: true, | ||||||
|  |         lastScan: Date.now(), | ||||||
|  |         lastScanVersion: packageJson.version | ||||||
|  |       }, { | ||||||
|  |         where: { | ||||||
|  |           id: libraryItemIdsMissing | ||||||
|  |         } | ||||||
|  |       }) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    * Get scan data for library folder | ||||||
|  |    * @param {import('../objects/Library')} library  | ||||||
|  |    * @param {import('../objects/Folder')} folder  | ||||||
|  |    * @returns {LibraryItemScanData[]} | ||||||
|  |    */ | ||||||
|  |   async scanFolder(library, folder) { | ||||||
|  |     const folderPath = fileUtils.filePathToPOSIX(folder.fullPath) | ||||||
|  | 
 | ||||||
|  |     const pathExists = await fs.pathExists(folderPath) | ||||||
|  |     if (!pathExists) { | ||||||
|  |       Logger.error(`[scandir] Invalid folder path does not exist "${folderPath}"`) | ||||||
|  |       return [] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const fileItems = await fileUtils.recurseFiles(folderPath) | ||||||
|  |     const libraryItemGrouping = scanUtils.groupFileItemsIntoLibraryItemDirs(library.mediaType, fileItems, library.settings.audiobooksOnly) | ||||||
|  | 
 | ||||||
|  |     if (!Object.keys(libraryItemGrouping).length) { | ||||||
|  |       Logger.error(`Root path has no media folders: ${folderPath}`) | ||||||
|  |       return [] | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const items = [] | ||||||
|  |     for (const libraryItemPath in libraryItemGrouping) { | ||||||
|  |       let isFile = false // item is not in a folder
 | ||||||
|  |       let libraryItemData = null | ||||||
|  |       let 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 scanUtils.buildLibraryFile(folderPath, [libraryItemPath]) | ||||||
|  |         isFile = true | ||||||
|  |       } else { | ||||||
|  |         libraryItemData = scanUtils.getDataFromMediaDir(library.mediaType, folderPath, libraryItemPath) | ||||||
|  |         fileObjs = await scanUtils.buildLibraryFile(libraryItemData.path, libraryItemGrouping[libraryItemPath]) | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       const libraryItemFolderStats = await fileUtils.getFileTimestampsWithIno(libraryItemData.path) | ||||||
|  | 
 | ||||||
|  |       if (!libraryItemFolderStats.ino) { | ||||||
|  |         Logger.warn(`[LibraryScanner] Library item folder "${libraryItemData.path}" has no inode value`) | ||||||
|  |         continue | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       items.push(new LibraryItemScanData({ | ||||||
|  |         libraryFolderId: 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, | ||||||
|  |         mediaMetadata: libraryItemData.mediaMetadata || null, | ||||||
|  |         libraryFiles: fileObjs | ||||||
|  |       })) | ||||||
|  |     } | ||||||
|  |     return items | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   /** | ||||||
|  |    *  | ||||||
|  |    * @param {import('../models/LibraryItem')} existingLibraryItem  | ||||||
|  |    * @param {LibraryItemScanData} libraryItemData  | ||||||
|  |    */ | ||||||
|  |   async rescanLibraryItem(existingLibraryItem, libraryItemData) { | ||||||
|  | 
 | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports = LibraryScanner | ||||||
| @ -4,13 +4,18 @@ const AudioFile = require('../objects/files/AudioFile') | |||||||
| const VideoFile = require('../objects/files/VideoFile') | const VideoFile = require('../objects/files/VideoFile') | ||||||
| 
 | 
 | ||||||
| const prober = require('../utils/prober') | const prober = require('../utils/prober') | ||||||
| const toneProber = require('../utils/toneProber') |  | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| const { LogLevel } = require('../utils/constants') | const { LogLevel } = require('../utils/constants') | ||||||
| 
 | 
 | ||||||
| class MediaFileScanner { | class MediaFileScanner { | ||||||
|   constructor() { } |   constructor() { } | ||||||
| 
 | 
 | ||||||
|  |   /** | ||||||
|  |    * Get track and disc number from audio filename | ||||||
|  |    * @param {{title:string, subtitle:string, series:string, sequence:string, publishedYear:string, narrators:string}} mediaMetadataFromScan  | ||||||
|  |    * @param {import('../objects/files/LibraryFile')} audioLibraryFile  | ||||||
|  |    * @returns {{trackNumber:number, discNumber:number}} | ||||||
|  |    */ | ||||||
|   getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) { |   getTrackAndDiscNumberFromFilename(mediaMetadataFromScan, audioLibraryFile) { | ||||||
|     const { title, author, series, publishedYear } = mediaMetadataFromScan |     const { title, author, series, publishedYear } = mediaMetadataFromScan | ||||||
|     const { filename, path } = audioLibraryFile.metadata |     const { filename, path } = audioLibraryFile.metadata | ||||||
| @ -102,7 +107,12 @@ class MediaFileScanner { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   // Returns array of { MediaFile, elapsed, averageScanDuration } from audio file scan objects
 |   /** | ||||||
|  |    * Returns array of { MediaFile, elapsed, averageScanDuration } from audio file scan objects | ||||||
|  |    * @param {import('../objects/LibraryItem')} libraryItem  | ||||||
|  |    * @param {import('../objects/files/LibraryFile')[]} mediaLibraryFiles  | ||||||
|  |    * @returns {Promise<object>} | ||||||
|  |    */ | ||||||
|   async executeMediaFileScans(libraryItem, mediaLibraryFiles) { |   async executeMediaFileScans(libraryItem, mediaLibraryFiles) { | ||||||
|     const mediaType = libraryItem.mediaType |     const mediaType = libraryItem.mediaType | ||||||
| 
 | 
 | ||||||
| @ -206,11 +216,10 @@ class MediaFileScanner { | |||||||
| 
 | 
 | ||||||
|   /** |   /** | ||||||
|   * Scans media files for a library item and adds them as audio tracks and sets library item metadata |   * Scans media files for a library item and adds them as audio tracks and sets library item metadata | ||||||
|   * @async |   * @param {import('../objects/files/LibraryFile')[]} mediaLibraryFiles | ||||||
|   * @param {Array<LibraryFile>} mediaLibraryFiles - Media files for this library item |   * @param {import('../objects/LibraryItem')} libraryItem | ||||||
|   * @param {LibraryItem} libraryItem |   * @param {import('./LibraryScan')} [libraryScan=null] - Optional when doing a library scan to use LibraryScan config/logs | ||||||
|   * @param {LibraryScan} [libraryScan=null] - Optional when doing a library scan to use LibraryScan config/logs |   * @return {Promise<boolean>} True if any updates were made | ||||||
|   * @return {Promise<Boolean>} True if any updates were made |  | ||||||
|   */ |   */ | ||||||
|   async scanMediaFiles(mediaLibraryFiles, libraryItem, libraryScan = null) { |   async scanMediaFiles(mediaLibraryFiles, libraryItem, libraryScan = null) { | ||||||
|     const preferAudioMetadata = libraryScan ? !!libraryScan.preferAudioMetadata : !!global.ServerSettings.scannerPreferAudioMetadata |     const preferAudioMetadata = libraryScan ? !!libraryScan.preferAudioMetadata : !!global.ServerSettings.scannerPreferAudioMetadata | ||||||
|  | |||||||
| @ -695,7 +695,7 @@ class Scanner { | |||||||
|         Logger.debug(`[Scanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" - scan for updates`) |         Logger.debug(`[Scanner] Folder update for relative path "${itemDir}" is in library item "${existingLibraryItem.media.metadata.title}" - scan for updates`) | ||||||
|         itemGroupingResults[itemDir] = await this.scanLibraryItem(library, folder, existingLibraryItem) |         itemGroupingResults[itemDir] = await this.scanLibraryItem(library, folder, existingLibraryItem) | ||||||
|         continue |         continue | ||||||
|       } else if (library.settings.audiobooksOnly && !fileUpdateGroup[itemDir].some(checkFilepathIsAudioFile)) { |       } else if (library.settings.audiobooksOnly && !fileUpdateGroup[itemDir].some?.(checkFilepathIsAudioFile)) { | ||||||
|         Logger.debug(`[Scanner] Folder update for relative path "${itemDir}" has no audio files`) |         Logger.debug(`[Scanner] Folder update for relative path "${itemDir}" has no audio files`) | ||||||
|         continue |         continue | ||||||
|       } |       } | ||||||
|  | |||||||
| @ -92,6 +92,12 @@ function bytesPretty(bytes, decimals = 0) { | |||||||
| } | } | ||||||
| module.exports.bytesPretty = bytesPretty | module.exports.bytesPretty = bytesPretty | ||||||
| 
 | 
 | ||||||
|  | /** | ||||||
|  |  * 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}[]} | ||||||
|  |  */ | ||||||
| async function recurseFiles(path, relPathToReplace = null) { | async function recurseFiles(path, relPathToReplace = null) { | ||||||
|   path = filePathToPOSIX(path) |   path = filePathToPOSIX(path) | ||||||
|   if (!path.endsWith('/')) path = path + '/' |   if (!path.endsWith('/')) path = path + '/' | ||||||
|  | |||||||
| @ -22,9 +22,12 @@ function checkFilepathIsAudioFile(filepath) { | |||||||
| } | } | ||||||
| module.exports.checkFilepathIsAudioFile = checkFilepathIsAudioFile | module.exports.checkFilepathIsAudioFile = checkFilepathIsAudioFile | ||||||
| 
 | 
 | ||||||
| // TODO: Function needs to be re-done
 | /** | ||||||
| // Input: array of relative file paths
 |  * TODO: Function needs to be re-done | ||||||
| // Output: map of files grouped into potential item dirs
 |  * @param {string} mediaType  | ||||||
|  |  * @param {string[]} paths array of relative file paths | ||||||
|  |  * @returns {Record<string,string[]>} map of files grouped into potential libarary item dirs | ||||||
|  |  */ | ||||||
| function groupFilesIntoLibraryItemPaths(mediaType, paths) { | function groupFilesIntoLibraryItemPaths(mediaType, paths) { | ||||||
|   // Step 1: Clean path, Remove leading "/", Filter out non-media files in root dir
 |   // Step 1: Clean path, Remove leading "/", Filter out non-media files in root dir
 | ||||||
|   var nonMediaFilePaths = [] |   var nonMediaFilePaths = [] | ||||||
| @ -97,8 +100,12 @@ function groupFilesIntoLibraryItemPaths(mediaType, paths) { | |||||||
| } | } | ||||||
| module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths | module.exports.groupFilesIntoLibraryItemPaths = groupFilesIntoLibraryItemPaths | ||||||
| 
 | 
 | ||||||
| // Input: array of relative file items (see recurseFiles)
 | /** | ||||||
| // Output: map of files grouped into potential libarary item dirs
 |  * @param {string} mediaType  | ||||||
|  |  * @param {{name:string, path:string, dirpath:string, reldirpath:string, fullpath:string, extension:string, deep:number}[]} fileItems (see recurseFiles) | ||||||
|  |  * @param {boolean} [audiobooksOnly=false]  | ||||||
|  |  * @returns {Record<string,string[]>} map of files grouped into potential libarary item dirs | ||||||
|  |  */ | ||||||
| function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly = false) { | function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly = false) { | ||||||
|   // Handle music where every audio file is a library item
 |   // Handle music where every audio file is a library item
 | ||||||
|   if (mediaType === 'music') { |   if (mediaType === 'music') { | ||||||
| @ -173,8 +180,15 @@ function groupFileItemsIntoLibraryItemDirs(mediaType, fileItems, audiobooksOnly | |||||||
|   }) |   }) | ||||||
|   return libraryItemGroup |   return libraryItemGroup | ||||||
| } | } | ||||||
|  | module.exports.groupFileItemsIntoLibraryItemDirs = groupFileItemsIntoLibraryItemDirs | ||||||
| 
 | 
 | ||||||
| function cleanFileObjects(libraryItemPath, files) { | /** | ||||||
|  |  * Get LibraryFile from filepath | ||||||
|  |  * @param {string} libraryItemPath  | ||||||
|  |  * @param {string[]} files  | ||||||
|  |  * @returns {import('../objects/files/LibraryFile')} | ||||||
|  |  */ | ||||||
|  | function buildLibraryFile(libraryItemPath, files) { | ||||||
|   return Promise.all(files.map(async (file) => { |   return Promise.all(files.map(async (file) => { | ||||||
|     const filePath = Path.posix.join(libraryItemPath, file) |     const filePath = Path.posix.join(libraryItemPath, file) | ||||||
|     const newLibraryFile = new LibraryFile() |     const newLibraryFile = new LibraryFile() | ||||||
| @ -182,6 +196,7 @@ function cleanFileObjects(libraryItemPath, files) { | |||||||
|     return newLibraryFile |     return newLibraryFile | ||||||
|   })) |   })) | ||||||
| } | } | ||||||
|  | module.exports.buildLibraryFile = buildLibraryFile | ||||||
| 
 | 
 | ||||||
| // Scan folder
 | // Scan folder
 | ||||||
| async function scanFolder(library, folder) { | async function scanFolder(library, folder) { | ||||||
| @ -211,7 +226,7 @@ async function scanFolder(library, folder) { | |||||||
|         path: Path.posix.join(folderPath, libraryItemPath), |         path: Path.posix.join(folderPath, libraryItemPath), | ||||||
|         relPath: libraryItemPath |         relPath: libraryItemPath | ||||||
|       } |       } | ||||||
|       fileObjs = await cleanFileObjects(folderPath, [libraryItemPath]) |       fileObjs = await buildLibraryFile(folderPath, [libraryItemPath]) | ||||||
|       isFile = true |       isFile = true | ||||||
|     } else if (libraryItemPath === libraryItemGrouping[libraryItemPath]) { |     } else if (libraryItemPath === libraryItemGrouping[libraryItemPath]) { | ||||||
|       // Media file in root only get title
 |       // Media file in root only get title
 | ||||||
| @ -222,11 +237,11 @@ async function scanFolder(library, folder) { | |||||||
|         path: Path.posix.join(folderPath, libraryItemPath), |         path: Path.posix.join(folderPath, libraryItemPath), | ||||||
|         relPath: libraryItemPath |         relPath: libraryItemPath | ||||||
|       } |       } | ||||||
|       fileObjs = await cleanFileObjects(folderPath, [libraryItemPath]) |       fileObjs = await buildLibraryFile(folderPath, [libraryItemPath]) | ||||||
|       isFile = true |       isFile = true | ||||||
|     } else { |     } else { | ||||||
|       libraryItemData = getDataFromMediaDir(library.mediaType, folderPath, libraryItemPath) |       libraryItemData = getDataFromMediaDir(library.mediaType, folderPath, libraryItemPath) | ||||||
|       fileObjs = await cleanFileObjects(libraryItemData.path, libraryItemGrouping[libraryItemPath]) |       fileObjs = await buildLibraryFile(libraryItemData.path, libraryItemGrouping[libraryItemPath]) | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path) |     const libraryItemFolderStats = await getFileTimestampsWithIno(libraryItemData.path) | ||||||
| @ -365,6 +380,7 @@ function getDataFromMediaDir(libraryMediaType, folderPath, relPath) { | |||||||
|     return getPodcastDataFromDir(folderPath, relPath) |     return getPodcastDataFromDir(folderPath, relPath) | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | module.exports.getDataFromMediaDir = getDataFromMediaDir | ||||||
| 
 | 
 | ||||||
| // Called from Scanner.js
 | // Called from Scanner.js
 | ||||||
| async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, isSingleMediaItem) { | async function getLibraryItemFileData(libraryMediaType, folder, libraryItemPath, isSingleMediaItem) { | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user