2021-11-25 03:15:50 +01:00
const Path = require ( 'path' )
2021-11-23 02:58:20 +01:00
const AudioFile = require ( '../objects/AudioFile' )
const prober = require ( '../utils/prober' )
const Logger = require ( '../Logger' )
2021-11-26 01:39:02 +01:00
const { LogLevel } = require ( '../utils/constants' )
2021-11-23 02:58:20 +01:00
class AudioFileScanner {
constructor ( ) { }
2022-01-10 00:36:25 +01:00
getTrackAndDiscNumberFromFilename ( bookScanData , filename ) {
2021-11-25 03:15:50 +01:00
const { title , author , series , publishYear } = bookScanData
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 )
2022-01-10 00:36:25 +01:00
// Look for disc number
var discNumber = null
var discMatch = partbasename . match ( /\b(disc|cd) ?(\d\d?)\b/i )
if ( discMatch && discMatch . length > 2 && discMatch [ 2 ] ) {
if ( ! isNaN ( discMatch [ 2 ] ) ) {
discNumber = Number ( discMatch [ 2 ] )
}
2021-11-25 03:15:50 +01:00
2022-01-10 00:36:25 +01:00
// Remove disc number from filename
partbasename = partbasename . replace ( /\b(disc|cd) ?(\d\d?)\b/i , '' )
}
2021-11-25 03:15:50 +01:00
var numbersinpath = partbasename . match ( /\d{1,4}/g )
2022-01-10 00:36:25 +01:00
var trackNumber = numbersinpath && numbersinpath . length ? parseInt ( numbersinpath [ 0 ] ) : null
return {
trackNumber ,
discNumber
2021-11-25 03:15:50 +01:00
}
}
getAverageScanDurationMs ( results ) {
if ( ! results . length ) return 0
var total = 0
for ( let i = 0 ; i < results . length ; i ++ ) total += results [ i ] . elapsed
return Math . floor ( total / results . length )
}
async scan ( audioFileData , bookScanData , verbose = false ) {
var probeStart = Date . now ( )
// Logger.debug(`[AudioFileScanner] Start Probe ${audioFileData.fullPath}`)
2021-12-25 01:06:17 +01:00
var probeData = await prober . probe ( audioFileData . fullPath , verbose )
2021-11-23 02:58:20 +01:00
if ( probeData . error ) {
Logger . error ( ` [AudioFileScanner] ${ probeData . error } : " ${ audioFileData . fullPath } " ` )
return null
}
2021-11-25 03:15:50 +01:00
// Logger.debug(`[AudioFileScanner] Finished Probe ${audioFileData.fullPath} elapsed ${msToTimestamp(Date.now() - probeStart, true)}`)
2021-11-23 02:58:20 +01:00
var audioFile = new AudioFile ( )
2022-01-10 00:36:25 +01:00
audioFileData . trackNumFromMeta = probeData . trackNumber
audioFileData . discNumFromMeta = probeData . discNumber
const { trackNumber , discNumber } = this . getTrackAndDiscNumberFromFilename ( bookScanData , audioFileData . filename )
audioFileData . trackNumFromFilename = trackNumber
audioFileData . discNumFromFilename = discNumber
2021-11-25 03:15:50 +01:00
audioFile . setDataFromProbe ( audioFileData , probeData )
2021-11-26 01:39:02 +01:00
2021-11-25 03:15:50 +01:00
return {
audioFile ,
elapsed : Date . now ( ) - probeStart
}
}
2021-11-26 01:39:02 +01:00
// Returns array of { AudioFile, elapsed, averageScanDuration } from audio file scan objects
async executeAudioFileScans ( audioFileDataArray , bookScanData ) {
2021-11-25 03:15:50 +01:00
var proms = [ ]
for ( let i = 0 ; i < audioFileDataArray . length ; i ++ ) {
2021-11-26 01:39:02 +01:00
proms . push ( this . scan ( audioFileDataArray [ i ] , bookScanData ) )
2021-11-25 03:15:50 +01:00
}
var scanStart = Date . now ( )
var results = await Promise . all ( proms ) . then ( ( scanResults ) => scanResults . filter ( sr => sr ) )
return {
audioFiles : results . map ( r => r . audioFile ) ,
elapsed : Date . now ( ) - scanStart ,
averageScanDuration : this . getAverageScanDurationMs ( results )
}
2021-11-23 02:58:20 +01:00
}
2021-11-26 01:39:02 +01:00
2022-01-10 00:36:25 +01:00
isSequential ( nums ) {
if ( ! nums || ! nums . length ) return false
if ( nums . length === 1 ) return true
var prev = nums [ 0 ]
for ( let i = 1 ; i < nums . length ; i ++ ) {
if ( nums [ i ] - prev > 1 ) return false
prev = nums [ i ]
}
return true
}
removeDupes ( nums ) {
if ( ! nums || ! nums . length ) return [ ]
if ( nums . length === 1 ) return nums
var nodupes = [ nums [ 0 ] ]
nums . forEach ( ( num ) => {
if ( num > nodupes [ nodupes . length - 1 ] ) nodupes . push ( num )
} )
return nodupes
}
// Must be all audiofiles in audiobook
runSmartTrackOrder ( audiobook , audioFiles ) {
var discsFromFilename = [ ]
var tracksFromFilename = [ ]
var discsFromMeta = [ ]
var tracksFromMeta = [ ]
audioFiles . forEach ( ( af ) => {
if ( af . discNumFromFilename !== null ) discsFromFilename . push ( af . discNumFromFilename )
if ( af . discNumFromMeta !== null ) discsFromMeta . push ( af . discNumFromMeta )
if ( af . trackNumFromFilename !== null ) tracksFromFilename . push ( af . trackNumFromFilename )
if ( af . trackNumFromMeta !== null ) tracksFromMeta . push ( af . trackNumFromMeta )
af . validateTrackIndex ( ) // Sets error if no valid track number
} )
discsFromFilename . sort ( ( a , b ) => a - b )
discsFromMeta . sort ( ( a , b ) => a - b )
tracksFromFilename . sort ( ( a , b ) => a - b )
tracksFromMeta . sort ( ( a , b ) => a - b )
console . log ( 'AB DISCS' , audiobook . title , discsFromFilename , discsFromMeta )
console . log ( 'AB TRACKS' , audiobook . title , tracksFromFilename , tracksFromMeta )
var discKey = null
if ( discsFromMeta . length === audioFiles . length && this . isSequential ( discsFromMeta ) ) {
discKey = 'discNumFromMeta'
} else if ( discsFromFilename . length === audioFiles . length && this . isSequential ( discsFromFilename ) ) {
discKey = 'discNumFromFilename'
}
var trackKey = null
tracksFromFilename = this . removeDupes ( tracksFromFilename )
tracksFromMeta = this . removeDupes ( tracksFromMeta )
if ( tracksFromFilename . length > tracksFromMeta . length ) {
trackKey = 'trackNumFromFilename'
} else {
trackKey = 'trackNumFromMeta'
}
if ( discKey !== null ) {
Logger . debug ( ` [AudioFileScanner] Smart track order for " ${ audiobook . title } " using disc key ${ discKey } and track key ${ trackKey } ` )
audioFiles . sort ( ( a , b ) => {
let Dx = a [ discKey ] - b [ discKey ]
if ( Dx === 0 ) Dx = a [ trackKey ] - b [ trackKey ]
return Dx
} )
} else {
Logger . debug ( ` [AudioFileScanner] Smart track order for " ${ audiobook . title } " using track key ${ trackKey } ` )
audioFiles . sort ( ( a , b ) => a [ trackKey ] - b [ trackKey ] )
}
for ( let i = 0 ; i < audioFiles . length ; i ++ ) {
audioFiles [ i ] . index = i + 1
audiobook . addAudioFile ( audioFiles [ i ] )
}
}
2021-11-26 01:39:02 +01:00
async scanAudioFiles ( audioFileDataArray , bookScanData , audiobook , preferAudioMetadata , libraryScan = null ) {
var hasUpdated = false
var audioScanResult = await this . executeAudioFileScans ( audioFileDataArray , bookScanData )
if ( audioScanResult . audioFiles . length ) {
if ( libraryScan ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Book " ${ bookScanData . path } " Audio file scan took ${ audioScanResult . elapsed } ms for ${ audioScanResult . audioFiles . length } with average time of ${ audioScanResult . averageScanDuration } ms ` )
}
2022-01-10 00:36:25 +01:00
var numExistingAudioFilesToInclude = audiobook . audioFilesToInclude . filter ( af => ! audioScanResult . audioFiles . find ( _af => _af . ino === af . ino ) ) . length
var totalAudioFilesToInclude = numExistingAudioFilesToInclude + audioScanResult . audioFiles . length
2021-11-26 01:39:02 +01:00
2022-01-10 00:36:25 +01:00
if ( numExistingAudioFilesToInclude <= 0 ) { // SMART TRACK ORDER for New or empty audiobooks
this . runSmartTrackOrder ( audiobook , audioScanResult . audioFiles )
hasUpdated = true
} else {
// validate & add/update audio files to existing audiobook
for ( let i = 0 ; i < audioScanResult . audioFiles . length ; i ++ ) {
var newAF = audioScanResult . audioFiles [ i ]
var existingAF = audiobook . getAudioFileByIno ( newAF . ino )
var trackIndex = null
if ( totalAudioFilesToInclude === 1 ) { // Single track audiobooks
trackIndex = 1
} else if ( existingAF && existingAF . manuallyVerified ) { // manually verified audio files use existing index
trackIndex = existingAF . index
2021-11-26 01:39:02 +01:00
} else {
2022-01-10 00:36:25 +01:00
trackIndex = newAF . validateTrackIndex ( )
2021-11-26 01:39:02 +01:00
}
2022-01-10 00:36:25 +01:00
if ( trackIndex !== null ) {
if ( audiobook . checkHasTrackNum ( trackIndex , newAF . ino ) ) {
newAF . setDuplicateTrackNumber ( trackIndex )
} else {
newAF . index = trackIndex
}
}
if ( existingAF ) {
if ( audiobook . updateAudioFile ( newAF ) ) {
hasUpdated = true
}
} else {
audiobook . addAudioFile ( newAF )
2021-11-26 01:39:02 +01:00
hasUpdated = true
}
}
}
// Set book details from audio file ID3 tags, optional prefer
if ( audiobook . setDetailsFromFileMetadata ( preferAudioMetadata ) ) {
hasUpdated = true
}
2021-11-27 23:01:53 +01:00
if ( hasUpdated ) {
audiobook . rebuildTracks ( )
}
2021-11-26 01:39:02 +01:00
}
return hasUpdated
}
2021-11-23 02:58:20 +01:00
}
module . exports = new AudioFileScanner ( )