mirror of
				https://github.com/advplyr/audiobookshelf.git
				synced 2025-10-31 02:17:01 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			233 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			233 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| const Path = require('path')
 | |
| const Logger = require('../Logger')
 | |
| const prober = require('./prober')
 | |
| 
 | |
| const ImageCodecs = ['mjpeg', 'jpeg', 'png']
 | |
| 
 | |
| function getDefaultAudioStream(audioStreams) {
 | |
|   if (audioStreams.length === 1) return audioStreams[0]
 | |
|   var defaultStream = audioStreams.find(a => a.is_default)
 | |
|   if (!defaultStream) return audioStreams[0]
 | |
|   return defaultStream
 | |
| }
 | |
| 
 | |
| async function scan(path) {
 | |
|   var probeData = await prober(path)
 | |
|   if (!probeData || !probeData.audio_streams || !probeData.audio_streams.length) {
 | |
|     return {
 | |
|       error: 'Invalid audio file'
 | |
|     }
 | |
|   }
 | |
|   if (!probeData.duration || !probeData.size) {
 | |
|     return {
 | |
|       error: 'Invalid duration or size'
 | |
|     }
 | |
|   }
 | |
|   var audioStream = getDefaultAudioStream(probeData.audio_streams)
 | |
| 
 | |
|   const finalData = {
 | |
|     format: probeData.format,
 | |
|     duration: probeData.duration,
 | |
|     size: probeData.size,
 | |
|     bit_rate: audioStream.bit_rate || probeData.bit_rate,
 | |
|     codec: audioStream.codec,
 | |
|     time_base: audioStream.time_base,
 | |
|     language: audioStream.language,
 | |
|     channel_layout: audioStream.channel_layout,
 | |
|     channels: audioStream.channels,
 | |
|     sample_rate: audioStream.sample_rate,
 | |
|     chapters: probeData.chapters || []
 | |
|   }
 | |
| 
 | |
|   var hasCoverArt = probeData.video_stream ? ImageCodecs.includes(probeData.video_stream.codec) : false
 | |
|   if (hasCoverArt) {
 | |
|     finalData.embedded_cover_art = probeData.video_stream.codec
 | |
|   }
 | |
| 
 | |
|   for (const key in probeData) {
 | |
|     if (probeData[key] && key.startsWith('file_tag')) {
 | |
|       finalData[key] = probeData[key]
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   if (finalData.file_tag_track) {
 | |
|     var track = finalData.file_tag_track
 | |
|     var trackParts = track.split('/').map(part => Number(part))
 | |
|     if (trackParts.length > 0) {
 | |
|       finalData.trackNumber = trackParts[0]
 | |
|     }
 | |
|     if (trackParts.length > 1) {
 | |
|       finalData.trackTotal = trackParts[1]
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return finalData
 | |
| }
 | |
| module.exports.scan = scan
 | |
| 
 | |
| 
 | |
| function isNumber(val) {
 | |
|   return !isNaN(val) && val !== null
 | |
| }
 | |
| 
 | |
| function getTrackNumberFromMeta(scanData) {
 | |
|   return !isNaN(scanData.trackNumber) && scanData.trackNumber !== null ? Math.trunc(Number(scanData.trackNumber)) : null
 | |
| }
 | |
| 
 | |
| function getTrackNumberFromFilename(title, author, series, publishYear, filename) {
 | |
|   var partbasename = Path.basename(filename, Path.extname(filename))
 | |
| 
 | |
|   // Remove title, author, series, and publishYear from filename if there
 | |
|   if (title) partbasename = partbasename.replace(title, '')
 | |
|   if (author) partbasename = partbasename.replace(author, '')
 | |
|   if (series) partbasename = partbasename.replace(series, '')
 | |
|   if (publishYear) partbasename = partbasename.replace(publishYear)
 | |
| 
 | |
|   // Remove eg. "disc 1" from path
 | |
|   partbasename = partbasename.replace(/ disc \d\d? /i, '')
 | |
| 
 | |
|   var numbersinpath = partbasename.match(/\d+/g)
 | |
|   if (!numbersinpath) return null
 | |
| 
 | |
|   var number = numbersinpath.length ? parseInt(numbersinpath[0]) : null
 | |
|   return number
 | |
| }
 | |
| 
 | |
| async function scanAudioFiles(audiobook, newAudioFiles) {
 | |
|   if (!newAudioFiles || !newAudioFiles.length) {
 | |
|     Logger.error('[AudioFileScanner] Scan Audio Files no new files', audiobook.title)
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   var tracks = []
 | |
|   var numDuplicateTracks = 0
 | |
|   var numInvalidTracks = 0
 | |
| 
 | |
|   for (let i = 0; i < newAudioFiles.length; i++) {
 | |
|     var audioFile = newAudioFiles[i]
 | |
|     var scanData = await scan(audioFile.fullPath)
 | |
|     if (!scanData || scanData.error) {
 | |
|       Logger.error('[AudioFileScanner] Scan failed for', audioFile.path)
 | |
|       // audiobook.invalidAudioFiles.push(parts[i])
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     var trackNumFromMeta = getTrackNumberFromMeta(scanData)
 | |
|     var book = audiobook.book || {}
 | |
| 
 | |
|     var trackNumFromFilename = getTrackNumberFromFilename(book.title, book.author, book.series, book.publishYear, audioFile.filename)
 | |
| 
 | |
|     var audioFileObj = {
 | |
|       ino: audioFile.ino,
 | |
|       filename: audioFile.filename,
 | |
|       path: audioFile.path,
 | |
|       fullPath: audioFile.fullPath,
 | |
|       ext: audioFile.ext,
 | |
|       ...scanData,
 | |
|       trackNumFromMeta,
 | |
|       trackNumFromFilename
 | |
|     }
 | |
|     var audioFile = audiobook.addAudioFile(audioFileObj)
 | |
| 
 | |
|     var trackNumber = 1
 | |
|     if (newAudioFiles.length > 1) {
 | |
|       trackNumber = isNumber(trackNumFromMeta) ? trackNumFromMeta : trackNumFromFilename
 | |
|       if (trackNumber === null) {
 | |
|         Logger.debug('[AudioFileScanner] Invalid track number for', audioFile.filename)
 | |
|         audioFile.invalid = true
 | |
|         audioFile.error = 'Failed to get track number'
 | |
|         numInvalidTracks++
 | |
|         continue;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (tracks.find(t => t.index === trackNumber)) {
 | |
|       // Logger.debug('[AudioFileScanner] Duplicate track number for', audioFile.filename)
 | |
|       audioFile.invalid = true
 | |
|       audioFile.error = 'Duplicate track number'
 | |
|       numDuplicateTracks++
 | |
|       continue;
 | |
|     }
 | |
| 
 | |
|     audioFile.index = trackNumber
 | |
|     tracks.push(audioFile)
 | |
|   }
 | |
| 
 | |
|   if (!tracks.length) {
 | |
|     Logger.warn('[AudioFileScanner] No Tracks for audiobook', audiobook.id)
 | |
|     return
 | |
|   }
 | |
| 
 | |
|   if (numDuplicateTracks > 0) {
 | |
|     Logger.warn(`[AudioFileScanner] ${numDuplicateTracks} Duplicate tracks for "${audiobook.title}"`)
 | |
|   }
 | |
|   if (numInvalidTracks > 0) {
 | |
|     Logger.error(`[AudioFileScanner] ${numDuplicateTracks} Invalid tracks for "${audiobook.title}"`)
 | |
|   }
 | |
| 
 | |
|   tracks.sort((a, b) => a.index - b.index)
 | |
|   audiobook.audioFiles.sort((a, b) => {
 | |
|     var aNum = isNumber(a.trackNumFromMeta) ? a.trackNumFromMeta : isNumber(a.trackNumFromFilename) ? a.trackNumFromFilename : 0
 | |
|     var bNum = isNumber(b.trackNumFromMeta) ? b.trackNumFromMeta : isNumber(b.trackNumFromFilename) ? b.trackNumFromFilename : 0
 | |
|     return aNum - bNum
 | |
|   })
 | |
| 
 | |
|   // If first index is 0, increment all by 1
 | |
|   if (tracks[0].index === 0) {
 | |
|     tracks = tracks.map(t => {
 | |
|       t.index += 1
 | |
|       return t
 | |
|     })
 | |
|   }
 | |
| 
 | |
|   var hasTracksAlready = audiobook.tracks.length
 | |
|   tracks.forEach((track) => {
 | |
|     audiobook.addTrack(track)
 | |
|   })
 | |
|   if (hasTracksAlready) {
 | |
|     audiobook.tracks.sort((a, b) => a.index - b.index)
 | |
|   }
 | |
| }
 | |
| module.exports.scanAudioFiles = scanAudioFiles
 | |
| 
 | |
| 
 | |
| async function rescanAudioFiles(audiobook) {
 | |
|   var audioFiles = audiobook.audioFiles
 | |
|   var updates = 0
 | |
| 
 | |
|   for (let i = 0; i < audioFiles.length; i++) {
 | |
|     var audioFile = audioFiles[i]
 | |
|     var scanData = await scan(audioFile.fullPath)
 | |
|     if (!scanData || scanData.error) {
 | |
|       Logger.error('[AudioFileScanner] Scan failed for', audioFile.path)
 | |
|       // audiobook.invalidAudioFiles.push(parts[i])
 | |
|       continue;
 | |
|     }
 | |
|     var hasUpdates = audioFile.updateMetadata(scanData)
 | |
|     if (hasUpdates) {
 | |
|       // Sync audio track with audio file
 | |
|       var matchingAudioTrack = audiobook.tracks.find(t => t.ino === audioFile.ino)
 | |
|       if (matchingAudioTrack) {
 | |
|         matchingAudioTrack.syncMetadata(audioFile)
 | |
|       } else if (!audioFile.exclude) { // If audio file is not excluded then it should have an audio track
 | |
| 
 | |
|         // Fallback to checking path
 | |
|         matchingAudioTrack = audiobook.tracks.find(t => t.path === audioFile.path)
 | |
|         if (matchingAudioTrack) {
 | |
|           Logger.error(`[AudioFileScanner] Audio File mismatch ino with audio track "${audioFile.filename}"`)
 | |
|           matchingAudioTrack.ino = audioFile.ino
 | |
|           matchingAudioTrack.syncMetadata(audioFile)
 | |
|         } else {
 | |
|           Logger.error(`[AudioFileScanner] Audio File has no matching Track ${audioFile.filename} for "${audiobook.title}"`)
 | |
| 
 | |
|           // Exclude audio file to prevent further errors
 | |
|           // audioFile.exclude = true
 | |
|         }
 | |
|       }
 | |
|       updates++
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   return updates
 | |
| }
 | |
| module.exports.rescanAudioFiles = rescanAudioFiles |