2021-11-25 03:15:50 +01:00
const Path = require ( 'path' )
2022-03-09 02:31:44 +01:00
const AudioFile = require ( '../objects/files/AudioFile' )
2021-11-23 02:58:20 +01:00
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-03-13 00:45:32 +01:00
getTrackAndDiscNumberFromFilename ( mediaMetadataFromScan , audioLibraryFile ) {
const { title , author , series , publishYear } = mediaMetadataFromScan
const { filename , path } = audioLibraryFile . metadata
2021-11-25 03:15:50 +01:00
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
2022-03-07 23:22:20 +01:00
// Look for disc number in folder path e.g. /Book Title/CD01/audiofile.mp3
var pathdir = Path . dirname ( path ) . split ( '/' ) . pop ( )
if ( pathdir && /^cd\d{1,3}$/i . test ( pathdir ) ) {
var discFromFolder = Number ( pathdir . replace ( /cd/i , '' ) )
if ( ! isNaN ( discFromFolder ) && discFromFolder !== null ) discNumber = discFromFolder
}
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 )
}
2022-03-13 00:45:32 +01:00
async scan ( audioLibraryFile , mediaMetadataFromScan , verbose = false ) {
2021-11-25 03:15:50 +01:00
var probeStart = Date . now ( )
2022-03-13 00:45:32 +01:00
var probeData = await prober . probe ( audioLibraryFile . metadata . path , verbose )
2021-11-23 02:58:20 +01:00
if ( probeData . error ) {
2022-03-13 00:45:32 +01:00
Logger . error ( ` [AudioFileScanner] ${ probeData . error } : " ${ audioLibraryFile . metadata . path } " ` )
2021-11-23 02:58:20 +01:00
return null
}
var audioFile = new AudioFile ( )
2022-03-13 00:45:32 +01:00
audioFile . trackNumFromMeta = probeData . trackNumber
audioFile . discNumFromMeta = probeData . discNumber
2022-01-10 00:36:25 +01:00
2022-03-13 00:45:32 +01:00
const { trackNumber , discNumber } = this . getTrackAndDiscNumberFromFilename ( mediaMetadataFromScan , audioLibraryFile )
audioFile . trackNumFromFilename = trackNumber
audioFile . discNumFromFilename = discNumber
2022-01-10 00:36:25 +01:00
2022-03-13 00:45:32 +01:00
audioFile . setDataFromProbe ( audioLibraryFile , 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
2022-03-13 00:45:32 +01:00
async executeAudioFileScans ( audioLibraryFiles , scanData ) {
var mediaMetadataFromScan = scanData . mediaMetadata || null
2021-11-25 03:15:50 +01:00
var proms = [ ]
2022-03-13 00:45:32 +01:00
for ( let i = 0 ; i < audioLibraryFiles . length ; i ++ ) {
proms . push ( this . scan ( audioLibraryFiles [ i ] , mediaMetadataFromScan ) )
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
}
2022-03-13 00:45:32 +01:00
runSmartTrackOrder ( libraryItem , audioFiles ) {
2022-01-10 00:36:25 +01:00
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 )
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 ) {
2022-03-13 00:45:32 +01:00
Logger . debug ( ` [AudioFileScanner] Smart track order for " ${ libraryItem . media . metadata . title } " using disc key ${ discKey } and track key ${ trackKey } ` )
2022-01-10 00:36:25 +01:00
audioFiles . sort ( ( a , b ) => {
let Dx = a [ discKey ] - b [ discKey ]
if ( Dx === 0 ) Dx = a [ trackKey ] - b [ trackKey ]
return Dx
} )
} else {
2022-03-13 00:45:32 +01:00
Logger . debug ( ` [AudioFileScanner] Smart track order for " ${ libraryItem . media . metadata . title } " using track key ${ trackKey } ` )
2022-01-10 00:36:25 +01:00
audioFiles . sort ( ( a , b ) => a [ trackKey ] - b [ trackKey ] )
}
for ( let i = 0 ; i < audioFiles . length ; i ++ ) {
audioFiles [ i ] . index = i + 1
2022-03-13 00:45:32 +01:00
var existingAF = libraryItem . media . findFileWithInode ( audioFiles [ i ] . ino )
2022-01-10 18:12:47 +01:00
if ( existingAF ) {
2022-03-13 00:45:32 +01:00
if ( existingAF . updateFromScan ) existingAF . updateFromScan ( audioFiles [ i ] )
2022-01-10 18:12:47 +01:00
} else {
2022-03-13 00:45:32 +01:00
libraryItem . media . audioFiles . push ( audioFiles [ i ] )
2022-01-10 18:12:47 +01:00
}
2022-01-10 00:36:25 +01:00
}
}
2022-03-13 00:45:32 +01:00
async scanAudioFiles ( audioLibraryFiles , scanData , libraryItem , preferAudioMetadata , libraryScan = null ) {
2021-11-26 01:39:02 +01:00
var hasUpdated = false
2022-03-13 00:45:32 +01:00
var audioScanResult = await this . executeAudioFileScans ( audioLibraryFiles , scanData )
2021-11-26 01:39:02 +01:00
if ( audioScanResult . audioFiles . length ) {
if ( libraryScan ) {
2022-03-13 00:45:32 +01:00
libraryScan . addLog ( LogLevel . DEBUG , ` Library Item " ${ scanData . path } " Audio file scan took ${ audioScanResult . elapsed } ms for ${ audioScanResult . audioFiles . length } with average time of ${ audioScanResult . averageScanDuration } ms ` )
2021-11-26 01:39:02 +01:00
}
2022-01-10 18:12:47 +01:00
var totalAudioFilesToInclude = audioScanResult . audioFiles . length
var newAudioFiles = audioScanResult . audioFiles . filter ( af => {
2022-03-13 00:45:32 +01:00
return ! libraryItem . libraryFiles . find ( lf => lf . ino === af . ino )
2022-01-10 18:12:47 +01:00
} )
2021-11-26 01:39:02 +01:00
2022-03-13 00:45:32 +01:00
// Adding audio files to book media
if ( libraryItem . mediaType === 'book' ) {
if ( newAudioFiles . length ) {
// Single Track Audiobooks
if ( totalAudioFilesToInclude === 1 ) {
var af = audioScanResult . audioFiles [ 0 ]
af . index = 1
libraryItem . media . audioFiles . push ( af )
hasUpdated = true
} else {
this . runSmartTrackOrder ( libraryItem , audioScanResult . audioFiles )
hasUpdated = true
}
2022-01-10 18:12:47 +01:00
} else {
2022-03-13 00:45:32 +01:00
Logger . debug ( ` [AudioFileScanner] No audio track re-order required ` )
// Only update metadata not index
audioScanResult . audioFiles . forEach ( ( af ) => {
var existingAF = libraryItem . media . findFileWithInode ( af . ino )
if ( existingAF ) {
af . index = existingAF . index
if ( existingAF . updateFromScan && existingAF . updateFromScan ( af ) ) {
hasUpdated = true
}
2022-01-10 00:36:25 +01:00
}
2022-03-13 00:45:32 +01:00
} )
}
2021-11-26 01:39:02 +01:00
2022-03-13 00:45:32 +01:00
// Set book details from audio file ID3 tags, optional prefer
if ( libraryItem . media . setMetadataFromAudioFile ( preferAudioMetadata ) ) {
hasUpdated = true
}
2021-11-26 01:39:02 +01:00
2022-03-13 00:45:32 +01:00
if ( hasUpdated ) {
libraryItem . media . rebuildTracks ( )
}
} // End Book media type
2021-11-26 01:39:02 +01:00
}
return hasUpdated
}
2021-11-23 02:58:20 +01:00
}
module . exports = new AudioFileScanner ( )