mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-27 11:18:14 +01:00 
			
		
		
		
	Fix: Scanner check path and inode value for removed books, scanner v5 outlined
This commit is contained in:
		
							parent
							
								
									ea366c00ca
								
							
						
					
					
						commit
						3fa0fe4b64
					
				| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "audiobookshelf-client", |   "name": "audiobookshelf-client", | ||||||
|   "version": "1.6.25", |   "version": "1.6.26", | ||||||
|   "description": "Audiobook manager and player", |   "description": "Audiobook manager and player", | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| { | { | ||||||
|   "name": "audiobookshelf", |   "name": "audiobookshelf", | ||||||
|   "version": "1.6.25", |   "version": "1.6.26", | ||||||
|   "description": "Self-hosted audiobook server for managing and playing audiobooks", |   "description": "Self-hosted audiobook server for managing and playing audiobooks", | ||||||
|   "main": "index.js", |   "main": "index.js", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|  | |||||||
| @ -454,7 +454,6 @@ class Scanner { | |||||||
|     var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId) |     var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryId) | ||||||
| 
 | 
 | ||||||
|     // TODO: This temporary fix from pre-release should be removed soon, "checkUpdateInos"
 |     // TODO: This temporary fix from pre-release should be removed soon, "checkUpdateInos"
 | ||||||
|     // TEMP - update ino for each audiobook
 |  | ||||||
|     if (audiobooksInLibrary.length) { |     if (audiobooksInLibrary.length) { | ||||||
|       for (let i = 0; i < audiobooksInLibrary.length; i++) { |       for (let i = 0; i < audiobooksInLibrary.length; i++) { | ||||||
|         var ab = audiobooksInLibrary[i] |         var ab = audiobooksInLibrary[i] | ||||||
| @ -463,7 +462,7 @@ class Scanner { | |||||||
|         if (shouldUpdateIno) { |         if (shouldUpdateIno) { | ||||||
|           var filesWithMissingIno = ab.getFilesWithMissingIno() |           var filesWithMissingIno = ab.getFilesWithMissingIno() | ||||||
| 
 | 
 | ||||||
|           Logger.debug(`\n\Updating inos for "${ab.title}"`) |           Logger.debug(`\nUpdating inos for "${ab.title}"`) | ||||||
|           Logger.debug(`In Scan, Files with missing inode`, filesWithMissingIno) |           Logger.debug(`In Scan, Files with missing inode`, filesWithMissingIno) | ||||||
| 
 | 
 | ||||||
|           var hasUpdates = await ab.checkUpdateInos() |           var hasUpdates = await ab.checkUpdateInos() | ||||||
| @ -504,7 +503,7 @@ class Scanner { | |||||||
|     // Check for removed audiobooks
 |     // Check for removed audiobooks
 | ||||||
|     for (let i = 0; i < audiobooksInLibrary.length; i++) { |     for (let i = 0; i < audiobooksInLibrary.length; i++) { | ||||||
|       var audiobook = audiobooksInLibrary[i] |       var audiobook = audiobooksInLibrary[i] | ||||||
|       var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino) |       var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path)) | ||||||
|       if (!dataFound) { |       if (!dataFound) { | ||||||
|         Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`) |         Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`) | ||||||
|         audiobook.isMissing = true |         audiobook.isMissing = true | ||||||
|  | |||||||
| @ -153,6 +153,17 @@ class AudioFile { | |||||||
|     this.metadata.setData(data) |     this.metadata.setData(data) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  |   // New scanner creates AudioFile from AudioFileScanner
 | ||||||
|  |   setData2(fileData, probeData) { | ||||||
|  |     this.index = fileData.index || null | ||||||
|  |     this.ino = fileData.ino || null | ||||||
|  |     this.filename = fileData.filename | ||||||
|  |     this.ext = fileData.ext | ||||||
|  |     this.path = fileData.path | ||||||
|  |     this.fullPath = fileData.fullPath | ||||||
|  |     this.addedAt = Date.now() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|   syncChapters(updatedChapters) { |   syncChapters(updatedChapters) { | ||||||
|     if (this.chapters.length !== updatedChapters.length) { |     if (this.chapters.length !== updatedChapters.length) { | ||||||
|       this.chapters = updatedChapters.map(ch => ({ ...ch })) |       this.chapters = updatedChapters.map(ch => ({ ...ch })) | ||||||
|  | |||||||
| @ -823,5 +823,141 @@ class Audiobook { | |||||||
|     var audioFile = this.audioFiles[0] |     var audioFile = this.audioFiles[0] | ||||||
|     return this.book.setDetailsFromFileMetadata(audioFile.metadata) |     return this.book.setDetailsFromFileMetadata(audioFile.metadata) | ||||||
|   } |   } | ||||||
|  | 
 | ||||||
|  |   // Returns null if file not found, true if file was updated, false if up to date
 | ||||||
|  |   checkFileFound(fileFound, isAudioFile) { | ||||||
|  |     var hasUpdated = false | ||||||
|  | 
 | ||||||
|  |     const arrayToCheck = isAudioFile ? this.audioFiles : this.otherFiles | ||||||
|  | 
 | ||||||
|  |     var existingFile = arrayToCheck.find(_af => _af.ino === fileFound.ino) | ||||||
|  |     if (!existingFile) { | ||||||
|  |       existingFile = arrayToCheck.find(_af => _af.path === fileFound.path) | ||||||
|  |       if (existingFile) { | ||||||
|  |         // file inode was updated
 | ||||||
|  |         existingFile.ino = fileFound.ino | ||||||
|  |         hasUpdated = true | ||||||
|  |       } else { | ||||||
|  |         // file not found
 | ||||||
|  |         return null | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (existingFile.filename !== fileFound.filename) { | ||||||
|  |       existingFile.filename = fileFound.filename | ||||||
|  |       existingFile.ext = fileFound.ext | ||||||
|  |       hasUpdated = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (existingFile.path !== fileFound.path) { | ||||||
|  |       existingFile.path = fileFound.path | ||||||
|  |       existingFile.fullPath = fileFound.fullPath | ||||||
|  |       hasUpdated = true | ||||||
|  |     } else if (existingFile.fullPath !== fileFound.fullPath) { | ||||||
|  |       existingFile.fullPath = fileFound.fullPath | ||||||
|  |       hasUpdated = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (!isAudioFile && existingFile.filetype !== fileFound.filetype) { | ||||||
|  |       existingFile.filetype = fileFound.filetype | ||||||
|  |       hasUpdated = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return hasUpdated | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   checkShouldScan(dataFound) { | ||||||
|  |     var hasUpdated = false | ||||||
|  | 
 | ||||||
|  |     if (dataFound.ino !== this.ino) { | ||||||
|  |       this.ino = dataFound.ino | ||||||
|  |       hasUpdated = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (dataFound.folderId !== this.folderId) { | ||||||
|  |       Logger.warn(`[Audiobook] Check scan audiobook changed folder ${this.folderId} -> ${dataFound.folderId}`) | ||||||
|  |       this.folderId = dataFound.folderId | ||||||
|  |       hasUpdated = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (dataFound.path !== this.path) { | ||||||
|  |       Logger.warn(`[Audiobook] Check scan audiobook changed path "${this.path}" -> "${dataFound.path}"`) | ||||||
|  |       this.path = dataFound.path | ||||||
|  |       this.fullPath = dataFound.fullPath | ||||||
|  |       hasUpdated = true | ||||||
|  |     } else if (dataFound.fullPath !== this.fullPath) { | ||||||
|  |       Logger.warn(`[Audiobook] Check scan audiobook changed fullpath "${this.fullPath}" -> "${dataFound.fullPath}"`) | ||||||
|  |       this.fullPath = dataFound.fullPath | ||||||
|  |       hasUpdated = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var newAudioFileData = [] | ||||||
|  |     var newOtherFileData = [] | ||||||
|  | 
 | ||||||
|  |     dataFound.audioFiles.forEach((af) => { | ||||||
|  |       var audioFileFoundCheck = this.checkFileFound(af, true) | ||||||
|  |       if (audioFileFoundCheck === null) { | ||||||
|  |         newAudioFileData.push(af) | ||||||
|  |       } else if (audioFileFoundCheck === true) { | ||||||
|  |         hasUpdated = true | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     dataFound.otherFiles.forEach((otherFileData) => { | ||||||
|  |       var fileFoundCheck = this.checkFileFound(otherFileData, false) | ||||||
|  |       if (fileFoundCheck === null) { | ||||||
|  |         newOtherFileData.push(otherFileData) | ||||||
|  |       } else if (fileFoundCheck === true) { | ||||||
|  |         hasUpdated = true | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     const audioFilesRemoved = [] | ||||||
|  |     const otherFilesRemoved = [] | ||||||
|  | 
 | ||||||
|  |     // inodes will all be up to date at this point
 | ||||||
|  |     this.audioFiles = this.audioFiles.filter(af => { | ||||||
|  |       if (!dataFound.audioFiles.find(_af => _af.ino === af.ino)) { | ||||||
|  |         audioFilesRemoved.push(af.toJSON()) | ||||||
|  |         return false | ||||||
|  |       } | ||||||
|  |       return true | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     // Remove all tracks that were associated with removed audio files
 | ||||||
|  |     if (audioFilesRemoved.length) { | ||||||
|  |       const audioFilesRemovedInodes = audioFilesRemoved.map(afr => afr.ino) | ||||||
|  |       this.tracks = this.tracks.filter(t => !audioFilesRemovedInodes.includes(t.ino)) | ||||||
|  |       this.checkUpdateMissingParts() | ||||||
|  |       hasUpdated = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.otherFiles = this.otherFiles.filter(otherFile => { | ||||||
|  |       if (!dataFound.otherFiles.find(_otherFile => _otherFile.ino === otherFile.ino)) { | ||||||
|  |         otherFilesRemoved.push(otherFile.toJSON()) | ||||||
|  | 
 | ||||||
|  |         // Check remove cover
 | ||||||
|  |         if (otherFile.fullPath === this.book.coverFullPath) { | ||||||
|  |           Logger.debug(`[Audiobook] "${this.title}" Check scan book cover removed`) | ||||||
|  |           this.book.removeCover() | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return false | ||||||
|  |       } | ||||||
|  |       return true | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     if (otherFilesRemoved.length) { | ||||||
|  |       hasUpdated = true | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       updated: hasUpdated, | ||||||
|  |       newAudioFileData, | ||||||
|  |       newOtherFileData, | ||||||
|  |       audioFilesRemoved, | ||||||
|  |       otherFilesRemoved | ||||||
|  |     } | ||||||
|  |   } | ||||||
| } | } | ||||||
| module.exports = Audiobook | module.exports = Audiobook | ||||||
							
								
								
									
										22
									
								
								server/scanner/AudioFileScanner.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								server/scanner/AudioFileScanner.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | const AudioFile = require('../objects/AudioFile') | ||||||
|  | const AudioProbeData = require('./AudioProbeData') | ||||||
|  | 
 | ||||||
|  | const prober = require('../utils/prober') | ||||||
|  | const Logger = require('../Logger') | ||||||
|  | 
 | ||||||
|  | class AudioFileScanner { | ||||||
|  |   constructor() { } | ||||||
|  | 
 | ||||||
|  |   async scan(audioFileData, verbose = false) { | ||||||
|  |     var probeData = await prober.probe2(audioFileData.fullPath, verbose) | ||||||
|  |     if (probeData.error) { | ||||||
|  |       Logger.error(`[AudioFileScanner] ${probeData.error} : "${audioFileData.fullPath}"`) | ||||||
|  |       return null | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var audioFile = new AudioFile() | ||||||
|  |     // TODO: Build audio file
 | ||||||
|  |     return audioFile | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports = new AudioFileScanner() | ||||||
							
								
								
									
										74
									
								
								server/scanner/AudioProbeData.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								server/scanner/AudioProbeData.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,74 @@ | |||||||
|  | const AudioFileMetadata = require('../objects/AudioFileMetadata') | ||||||
|  | 
 | ||||||
|  | class AudioProbeData { | ||||||
|  |   constructor() { | ||||||
|  |     this.embeddedCoverArt = null | ||||||
|  |     this.format = null | ||||||
|  |     this.duration = null | ||||||
|  |     this.size = null | ||||||
|  |     this.bitRate = null | ||||||
|  |     this.codec = null | ||||||
|  |     this.timeBase = null | ||||||
|  |     this.language = null | ||||||
|  |     this.channelLayout = null | ||||||
|  |     this.channels = null | ||||||
|  |     this.sampleRate = null | ||||||
|  |     this.chapters = [] | ||||||
|  | 
 | ||||||
|  |     this.audioFileMetadata = null | ||||||
|  | 
 | ||||||
|  |     this.trackNumber = null | ||||||
|  |     this.trackTotal = null | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getDefaultAudioStream(audioStreams) { | ||||||
|  |     if (audioStreams.length === 1) return audioStreams[0] | ||||||
|  |     var defaultStream = audioStreams.find(a => a.is_default) | ||||||
|  |     if (!defaultStream) return audioStreams[0] | ||||||
|  |     return defaultStream | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getEmbeddedCoverArt(videoStream) { | ||||||
|  |     const ImageCodecs = ['mjpeg', 'jpeg', 'png'] | ||||||
|  |     return ImageCodecs.includes(videoStream.codec) ? videoStream.codec : null | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setData(data) { | ||||||
|  |     var audioStream = getDefaultAudioStream(data.audio_streams) | ||||||
|  | 
 | ||||||
|  |     this.embeddedCoverArt = data.video_stream ? this.getEmbeddedCoverArt(data.video_stream) : false | ||||||
|  |     this.format = data.format | ||||||
|  |     this.duration = data.duration | ||||||
|  |     this.size = data.size | ||||||
|  |     this.bitRate = audioStream.bit_rate || data.bit_rate | ||||||
|  |     this.codec = audioStream.codec | ||||||
|  |     this.timeBase = audioStream.time_base | ||||||
|  |     this.language = audioStream.language | ||||||
|  |     this.channelLayout = audioStream.channel_layout | ||||||
|  |     this.channels = audioStream.channels | ||||||
|  |     this.sampleRate = audioStream.sample_rate | ||||||
|  |     this.chapters = data.chapters || [] | ||||||
|  | 
 | ||||||
|  |     var metatags = {} | ||||||
|  |     for (const key in data) { | ||||||
|  |       if (data[key] && key.startsWith('file_tag')) { | ||||||
|  |         metatags[key] = data[key] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     this.audioFileMetadata = new AudioFileMetadata() | ||||||
|  |     this.audioFileMetadata.setData(metatags) | ||||||
|  | 
 | ||||||
|  |     // Track ID3 tag might be "3/10" or just "3"
 | ||||||
|  |     if (this.audioFileMetadata.tagTrack) { | ||||||
|  |       var trackParts = this.audioFileMetadata.tagTrack.split('/').map(part => Number(part)) | ||||||
|  |       if (trackParts.length > 0) { | ||||||
|  |         this.trackNumber = trackParts[0] | ||||||
|  |       } | ||||||
|  |       if (trackParts.length > 1) { | ||||||
|  |         this.trackTotal = trackParts[1] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports = AudioProbeData | ||||||
							
								
								
									
										34
									
								
								server/scanner/LibraryScan.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								server/scanner/LibraryScan.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,34 @@ | |||||||
|  | const Folder = require('../objects/Folder') | ||||||
|  | 
 | ||||||
|  | const { getId } = require('../utils/index') | ||||||
|  | 
 | ||||||
|  | class LibraryScan { | ||||||
|  |   constructor() { | ||||||
|  |     this.id = null | ||||||
|  |     this.libraryId = null | ||||||
|  |     this.libraryName = null | ||||||
|  |     this.folders = null | ||||||
|  | 
 | ||||||
|  |     this.scanOptions = null | ||||||
|  | 
 | ||||||
|  |     this.startedAt = null | ||||||
|  |     this.finishedAt = null | ||||||
|  | 
 | ||||||
|  |     this.folderScans = [] | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   get _scanOptions() { return this.scanOptions || {} } | ||||||
|  |   get forceRescan() { return !!this._scanOptions.forceRescan } | ||||||
|  | 
 | ||||||
|  |   setData(library, scanOptions) { | ||||||
|  |     this.id = getId('lscan') | ||||||
|  |     this.libraryId = library.id | ||||||
|  |     this.libraryName = library.name | ||||||
|  |     this.folders = library.folders.map(folder => Folder(folder.toJSON())) | ||||||
|  | 
 | ||||||
|  |     this.scanOptions = scanOptions | ||||||
|  | 
 | ||||||
|  |     this.startedAt = Date.now() | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports = LibraryScan | ||||||
							
								
								
									
										68
									
								
								server/scanner/ScanOptions.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								server/scanner/ScanOptions.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,68 @@ | |||||||
|  | const { CoverDestination } = require('../utils/constants') | ||||||
|  | 
 | ||||||
|  | class ScanOptions { | ||||||
|  |   constructor(options) { | ||||||
|  |     this.forceRescan = false | ||||||
|  | 
 | ||||||
|  |     this.metadataPrecedence = [ | ||||||
|  |       { | ||||||
|  |         id: 'directory', | ||||||
|  |         include: true | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'reader-desc-txt', | ||||||
|  |         include: true | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'audio-file-metadata', | ||||||
|  |         include: true | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'metadata-opf', | ||||||
|  |         include: true | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'external-source', | ||||||
|  |         include: false | ||||||
|  |       } | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     // Server settings
 | ||||||
|  |     this.parseSubtitles = false | ||||||
|  |     this.findCovers = false | ||||||
|  |     this.coverDestination = CoverDestination.METADATA | ||||||
|  | 
 | ||||||
|  |     if (options) { | ||||||
|  |       this.construct(options) | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   construct(options) { | ||||||
|  |     for (const key in options) { | ||||||
|  |       if (key === 'metadataPrecedence' && options[key].length) { | ||||||
|  |         this.metadataPrecedence = [...options[key]] | ||||||
|  |       } else if (this[key] !== undefined) { | ||||||
|  |         this[key] = options[key] | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   toJSON() { | ||||||
|  |     return { | ||||||
|  |       forceRescan: this.forceRescan, | ||||||
|  |       metadataPrecedence: this.metadataPrecedence, | ||||||
|  |       parseSubtitles: this.parseSubtitles, | ||||||
|  |       findCovers: this.findCovers, | ||||||
|  |       coverDestination: this.coverDestination | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setData(options, serverSettings) { | ||||||
|  |     this.forceRescan = !!options.forceRescan | ||||||
|  | 
 | ||||||
|  |     this.parseSubtitles = !!serverSettings.scannerParseSubtitle | ||||||
|  |     this.findCovers = !!serverSettings.scannerFindCovers | ||||||
|  |     this.coverDestination = serverSettings.coverDestination | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports = ScanOptions | ||||||
							
								
								
									
										172
									
								
								server/scanner/Scanner.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								server/scanner/Scanner.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,172 @@ | |||||||
|  | const fs = require('fs-extra') | ||||||
|  | const Path = require('path') | ||||||
|  | 
 | ||||||
|  | // Utils
 | ||||||
|  | const Logger = require('../Logger') | ||||||
|  | const { version } = require('../../package.json') | ||||||
|  | const audioFileScanner = require('../utils/audioFileScanner') | ||||||
|  | const { groupFilesIntoAudiobookPaths, getAudiobookFileData, scanRootDir } = require('../utils/scandir') | ||||||
|  | const { comparePaths, getIno, getId } = require('../utils/index') | ||||||
|  | const { secondsToTimestamp } = require('../utils/fileUtils') | ||||||
|  | const { ScanResult, CoverDestination } = require('../utils/constants') | ||||||
|  | 
 | ||||||
|  | const AudioFileScanner = require('./AudioFileScanner') | ||||||
|  | const BookFinder = require('../BookFinder') | ||||||
|  | const Audiobook = require('../objects/Audiobook') | ||||||
|  | const LibraryScan = require('./LibraryScan') | ||||||
|  | const ScanOptions = require('./ScanOptions') | ||||||
|  | 
 | ||||||
|  | class Scanner { | ||||||
|  |   constructor(AUDIOBOOK_PATH, METADATA_PATH, db, coverController, emitter) { | ||||||
|  |     this.AudiobookPath = AUDIOBOOK_PATH | ||||||
|  |     this.MetadataPath = METADATA_PATH | ||||||
|  |     this.BookMetadataPath = Path.posix.join(this.MetadataPath.replace(/\\/g, '/'), 'books') | ||||||
|  | 
 | ||||||
|  |     this.db = db | ||||||
|  |     this.coverController = coverController | ||||||
|  |     this.emitter = emitter | ||||||
|  | 
 | ||||||
|  |     this.cancelScan = false | ||||||
|  |     this.cancelLibraryScan = {} | ||||||
|  |     this.librariesScanning = [] | ||||||
|  | 
 | ||||||
|  |     this.bookFinder = new BookFinder() | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async scan(libraryId, options = {}) { | ||||||
|  |     if (this.librariesScanning.includes(libraryId)) { | ||||||
|  |       Logger.error(`[Scanner] Already scanning ${libraryId}`) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var library = this.db.libraries.find(lib => lib.id === libraryId) | ||||||
|  |     if (!library) { | ||||||
|  |       Logger.error(`[Scanner] Library not found for scan ${libraryId}`) | ||||||
|  |       return | ||||||
|  |     } else if (!library.folders.length) { | ||||||
|  |       Logger.warn(`[Scanner] Library has no folders to scan "${library.name}"`) | ||||||
|  |       return | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var scanOptions = new ScanOptions() | ||||||
|  |     scanOptions.setData(options, this.db.serverSettings) | ||||||
|  | 
 | ||||||
|  |     var libraryScan = new LibraryScan() | ||||||
|  |     libraryScan.setData(library, scanOptions) | ||||||
|  | 
 | ||||||
|  |     Logger.info(`[Scanner] Starting library scan ${libraryScan.id} for ${libraryScan.libraryName}`) | ||||||
|  | 
 | ||||||
|  |     var results = await this.scanLibrary(libraryScan) | ||||||
|  | 
 | ||||||
|  |     Logger.info(`[Scanner] Library scan ${libraryScan.id} complete`) | ||||||
|  | 
 | ||||||
|  |     return results | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async scanLibrary(libraryScan) { | ||||||
|  |     var audiobookDataFound = [] | ||||||
|  |     for (let i = 0; i < libraryScan.folders.length; i++) { | ||||||
|  |       var folder = libraryScan.folders[i] | ||||||
|  |       var abDataFoundInFolder = await scanRootDir(folder, this.db.serverSettings) | ||||||
|  |       Logger.debug(`[Scanner] ${abDataFoundInFolder.length} ab data found in folder "${folder.fullPath}"`) | ||||||
|  |       audiobookDataFound = audiobookDataFound.concat(abDataFoundInFolder) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Remove audiobooks with no inode
 | ||||||
|  |     audiobookDataFound = audiobookDataFound.filter(abd => abd.ino) | ||||||
|  | 
 | ||||||
|  |     var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === libraryScan.libraryId) | ||||||
|  | 
 | ||||||
|  |     const audiobooksToUpdate = [] | ||||||
|  |     const audiobooksToRescan = [] | ||||||
|  |     const newAudiobookData = [] | ||||||
|  | 
 | ||||||
|  |     // Check for existing & removed audiobooks
 | ||||||
|  |     for (let i = 0; i < audiobooksInLibrary.length; i++) { | ||||||
|  |       var audiobook = audiobooksInLibrary[i] | ||||||
|  |       var dataFound = audiobookDataFound.find(abd => abd.ino === audiobook.ino || comparePaths(abd.path, audiobook.path)) | ||||||
|  |       if (!dataFound) { | ||||||
|  |         Logger.info(`[Scanner] Audiobook "${audiobook.title}" is missing`) | ||||||
|  |         audiobook.isMissing = true | ||||||
|  |         audiobook.lastUpdate = Date.now() | ||||||
|  |         scanResults.missing++ | ||||||
|  |         audiobooksToUpdate.push(audiobook) | ||||||
|  |       } else { | ||||||
|  |         var checkRes = audiobook.checkShouldRescan(dataFound) | ||||||
|  |         if (checkRes.newAudioFileData.length || checkRes.newOtherFileData.length) { | ||||||
|  |           // existing audiobook has new files
 | ||||||
|  |           checkRes.audiobook = audiobook | ||||||
|  |           audiobooksToRescan.push(checkRes) | ||||||
|  |         } else if (checkRes.updated) { | ||||||
|  |           audiobooksToUpdate.push(audiobook) | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Remove this abf
 | ||||||
|  |         audiobookDataFound = audiobookDataFound.filter(abf => abf.ino !== dataFound.ino) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     // Potential NEW Audiobooks
 | ||||||
|  |     for (let i = 0; i < audiobookDataFound.length; i++) { | ||||||
|  |       var dataFound = audiobookDataFound[i] | ||||||
|  |       var hasEbook = dataFound.otherFiles.find(otherFile => otherFile.filetype === 'ebook') | ||||||
|  |       if (!hasEbook && !dataFound.audioFiles.length) { | ||||||
|  |         Logger.info(`[Scanner] Directory found "${audiobookDataFound.path}" has no ebook or audio files`) | ||||||
|  |       } else { | ||||||
|  |         newAudiobookData.push(dataFound) | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var rescans = [] | ||||||
|  |     for (let i = 0; i < audiobooksToRescan.length; i++) { | ||||||
|  |       var rescan = this.rescanAudiobook(audiobooksToRescan[i]) | ||||||
|  |       rescans.push(rescan) | ||||||
|  |     } | ||||||
|  |     var newscans = [] | ||||||
|  |     for (let i = 0; i < newAudiobookData.length; i++) { | ||||||
|  |       var newscan = this.scanNewAudiobook(newAudiobookData[i]) | ||||||
|  |       newscans.push(newscan) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     var rescanResults = await Promise.all(rescans) | ||||||
|  | 
 | ||||||
|  |     var newscanResults = await Promise.all(newscans) | ||||||
|  | 
 | ||||||
|  |     // TODO: Return report
 | ||||||
|  |     return { | ||||||
|  |       updates: 0, | ||||||
|  |       additions: 0 | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // Return scan result payload
 | ||||||
|  |   async rescanAudiobook(audiobookCheckData) { | ||||||
|  |     const { newAudioFileData, newOtherFileData, audiobook } = audiobookCheckData | ||||||
|  |     if (newAudioFileData.length) { | ||||||
|  |       var newAudioFiles = await this.scanAudioFiles(newAudioFileData) | ||||||
|  |       // TODO: Update audiobook tracks
 | ||||||
|  |     } | ||||||
|  |     if (newOtherFileData.length) { | ||||||
|  |       // TODO: Check other files
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |       updated: true | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async scanNewAudiobook(audiobookData) { | ||||||
|  |     // TODO: Return new audiobook
 | ||||||
|  |     return null | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async scanAudioFiles(audioFileData) { | ||||||
|  |     var proms = [] | ||||||
|  |     for (let i = 0; i < audioFileData.length; i++) { | ||||||
|  |       var prom = AudioFileScanner.scan(audioFileData[i]) | ||||||
|  |       proms.push(prom) | ||||||
|  |     } | ||||||
|  |     return Promise.all(proms) | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | module.exports = Scanner | ||||||
| @ -13,7 +13,7 @@ function getDefaultAudioStream(audioStreams) { | |||||||
| 
 | 
 | ||||||
| async function scan(path, verbose = false) { | async function scan(path, verbose = false) { | ||||||
|   Logger.debug(`Scanning path "${path}"`) |   Logger.debug(`Scanning path "${path}"`) | ||||||
|   var probeData = await prober(path, verbose) |   var probeData = await prober.probe(path, verbose) | ||||||
|   if (!probeData || !probeData.audio_streams || !probeData.audio_streams.length) { |   if (!probeData || !probeData.audio_streams || !probeData.audio_streams.length) { | ||||||
|     return { |     return { | ||||||
|       error: 'Invalid audio file' |       error: 'Invalid audio file' | ||||||
|  | |||||||
| @ -1,5 +1,8 @@ | |||||||
| var Ffmpeg = require('fluent-ffmpeg') | var Ffmpeg = require('fluent-ffmpeg') | ||||||
| const Path = require('path') | const Path = require('path') | ||||||
|  | 
 | ||||||
|  | const AudioProbeData = require('../scanner/AudioProbeData') | ||||||
|  | 
 | ||||||
| const Logger = require('../Logger') | const Logger = require('../Logger') | ||||||
| 
 | 
 | ||||||
| function tryGrabBitRate(stream, all_streams, total_bit_rate) { | function tryGrabBitRate(stream, all_streams, total_bit_rate) { | ||||||
| @ -241,4 +244,31 @@ function probe(filepath, verbose = false) { | |||||||
|     }) |     }) | ||||||
|   }) |   }) | ||||||
| } | } | ||||||
| module.exports = probe | module.exports.probe = probe | ||||||
|  | 
 | ||||||
|  | // Updated probe returns AudioProbeData object
 | ||||||
|  | function probe2(filepath, verbose = false) { | ||||||
|  |   return new Promise((resolve) => { | ||||||
|  |     Ffmpeg.ffprobe(filepath, ['-show_chapters'], (err, raw) => { | ||||||
|  |       if (err) { | ||||||
|  |         console.error(err) | ||||||
|  |         var errorMsg = err ? err.message : null | ||||||
|  |         resolve({ | ||||||
|  |           error: errorMsg || 'Probe Error' | ||||||
|  |         }) | ||||||
|  |       } else { | ||||||
|  |         var rawProbeData = parseProbeData(raw, verbose) | ||||||
|  |         if (!rawProbeData || !rawProbeData.audio_streams.length) { | ||||||
|  |           resolve({ | ||||||
|  |             error: rawProbeData ? 'Invalid audio file: no audio streams found' : 'Probe Failed' | ||||||
|  |           }) | ||||||
|  |         } else { | ||||||
|  |           var probeData = new AudioProbeData() | ||||||
|  |           probeData.setData(rawProbeData) | ||||||
|  |           resolve(probeData) | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }) | ||||||
|  |   }) | ||||||
|  | } | ||||||
|  | module.exports.probe2 = probe2 | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user