2021-11-23 02:58:20 +01:00
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' )
2021-11-25 03:15:50 +01:00
const { comparePaths , getIno , getId , msToTimestamp } = require ( '../utils/index' )
2021-11-23 02:58:20 +01:00
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 ( )
}
2021-11-25 03:15:50 +01:00
getCoverDirectory ( audiobook ) {
if ( this . db . serverSettings . coverDestination === CoverDestination . AUDIOBOOK ) {
return {
fullPath : audiobook . fullPath ,
relPath : '/s/book/' + audiobook . id
}
} else {
return {
fullPath : Path . posix . join ( this . BookMetadataPath , audiobook . id ) ,
relPath : Path . posix . join ( '/metadata' , 'books' , audiobook . id )
}
}
}
2021-11-23 02:58:20 +01:00
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 )
2021-11-25 03:15:50 +01:00
this . librariesScanning . push ( libraryScan )
this . emitter ( 'scan_start' , libraryScan . getScanEmitData )
2021-11-23 02:58:20 +01:00
Logger . info ( ` [Scanner] Starting library scan ${ libraryScan . id } for ${ libraryScan . libraryName } ` )
2021-11-25 03:15:50 +01:00
await this . scanLibrary ( libraryScan )
2021-11-23 02:58:20 +01:00
2021-11-25 03:15:50 +01:00
libraryScan . setComplete ( )
Logger . info ( ` [Scanner] Library scan ${ libraryScan . id } completed in ${ libraryScan . elapsedTimestamp } . ${ libraryScan . resultStats } ` )
2021-11-23 02:58:20 +01:00
2021-11-25 03:15:50 +01:00
this . librariesScanning = this . librariesScanning . filter ( ls => ls . id !== library . id )
this . emitter ( 'scan_complete' , libraryScan . getScanEmitData )
2021-11-23 02:58:20 +01:00
}
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 )
2021-11-25 03:15:50 +01:00
var audiobooksToUpdate = [ ]
var audiobookRescans = [ ]
var newAudiobookScans = [ ]
2021-11-23 02:58:20 +01:00
// 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 ` )
2021-11-25 03:15:50 +01:00
audiobook . setMissing ( )
2021-11-23 02:58:20 +01:00
audiobooksToUpdate . push ( audiobook )
} else {
2021-11-25 03:15:50 +01:00
var checkRes = audiobook . checkScanData ( dataFound )
2021-11-23 02:58:20 +01:00
if ( checkRes . newAudioFileData . length || checkRes . newOtherFileData . length ) {
// existing audiobook has new files
checkRes . audiobook = audiobook
2021-11-25 03:15:50 +01:00
checkRes . bookScanData = dataFound
audiobookRescans . push ( this . rescanAudiobook ( checkRes , libraryScan ) )
libraryScan . resultsMissing ++
2021-11-23 02:58:20 +01:00
} else if ( checkRes . updated ) {
audiobooksToUpdate . push ( audiobook )
2021-11-25 03:15:50 +01:00
libraryScan . resultsUpdated ++
2021-11-23 02:58:20 +01:00
}
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 {
2021-11-25 03:15:50 +01:00
newAudiobookScans . push ( this . scanNewAudiobook ( dataFound , libraryScan ) )
2021-11-23 02:58:20 +01:00
}
}
2021-11-25 03:15:50 +01:00
if ( audiobookRescans . length ) {
var updatedAudiobooks = ( await Promise . all ( audiobookRescans ) ) . filter ( ab => ! ! ab )
if ( updatedAudiobooks . length ) {
audiobooksToUpdate = audiobooksToUpdate . concat ( updatedAudiobooks )
libraryScan . resultsUpdated += updatedAudiobooks . length
}
2021-11-23 02:58:20 +01:00
}
2021-11-25 03:15:50 +01:00
if ( audiobooksToUpdate . length ) {
Logger . debug ( ` [Scanner] Library " ${ libraryScan . libraryName } " updating ${ audiobooksToUpdate . length } books ` )
await this . db . updateEntities ( 'audiobook' , audiobooksToUpdate )
2021-11-23 02:58:20 +01:00
}
2021-11-25 03:15:50 +01:00
if ( newAudiobookScans . length ) {
var newAudiobooks = ( await Promise . all ( newAudiobookScans ) ) . filter ( ab => ! ! ab )
if ( newAudiobooks . length ) {
Logger . debug ( ` [Scanner] Library " ${ libraryScan . libraryName } " inserting ${ newAudiobooks . length } books ` )
await this . db . insertEntities ( 'audiobook' , newAudiobooks )
libraryScan . resultsAdded = newAudiobooks . length
}
2021-11-23 02:58:20 +01:00
}
}
2021-11-25 03:15:50 +01:00
async rescanAudiobook ( audiobookCheckData , libraryScan ) {
const { newAudioFileData , newOtherFileData , audiobook , bookScanData } = audiobookCheckData
Logger . debug ( ` [Scanner] Library " ${ libraryScan . libraryName } " Re-scanning " ${ audiobook . path } " ` )
2021-11-23 02:58:20 +01:00
if ( newAudioFileData . length ) {
2021-11-25 03:15:50 +01:00
var audioScanResult = await AudioFileScanner . scanAudioFiles ( newAudioFileData , bookScanData )
Logger . debug ( ` [Scanner] Library " ${ libraryScan . libraryName } " Book " ${ audiobook . path } " Audio file scan took ${ msToTimestamp ( audioScanResult . elapsed , true ) } for ${ audioScanResult . audioFiles . length } with average time of ${ msToTimestamp ( audioScanResult . averageScanDuration , true ) } ` )
if ( audioScanResult . audioFiles . length ) {
var totalAudioFilesToInclude = audiobook . audioFilesToInclude . length + audioScanResult . audioFiles . length
// validate & add audio files to audiobook
for ( let i = 0 ; i < audioScanResult . audioFiles . length ; i ++ ) {
var newAF = audioScanResult . audioFiles [ i ]
var trackIndex = newAF . validateTrackIndex ( totalAudioFilesToInclude === 1 )
if ( trackIndex !== null ) {
if ( audiobook . checkHasTrackNum ( trackIndex ) ) {
newAF . setDuplicateTrackNumber ( trackIndex )
} else {
newAF . index = trackIndex
}
}
audiobook . addAudioFile ( newAF )
}
audiobook . rebuildTracks ( )
}
2021-11-23 02:58:20 +01:00
}
if ( newOtherFileData . length ) {
2021-11-25 03:15:50 +01:00
await audiobook . syncOtherFiles ( newOtherFileData , this . MetadataPath )
2021-11-23 02:58:20 +01:00
}
2021-11-25 03:15:50 +01:00
return audiobook
}
2021-11-23 02:58:20 +01:00
2021-11-25 03:15:50 +01:00
async scanNewAudiobook ( audiobookData , libraryScan ) {
Logger . debug ( ` [Scanner] Library " ${ libraryScan . libraryName } " Scanning new " ${ audiobookData . path } " ` )
var audiobook = new Audiobook ( )
audiobook . setData ( audiobookData )
if ( audiobookData . audioFiles . length ) {
var audioScanResult = await AudioFileScanner . scanAudioFiles ( audiobookData . audioFiles , audiobookData )
Logger . debug ( ` [Scanner] Library " ${ libraryScan . libraryName } " Book " ${ audiobookData . path } " Audio file scan took ${ msToTimestamp ( audioScanResult . elapsed , true ) } for ${ audioScanResult . audioFiles . length } with average time of ${ msToTimestamp ( audioScanResult . averageScanDuration , true ) } ` )
if ( audioScanResult . audioFiles . length ) {
// validate & add audio files to audiobook
for ( let i = 0 ; i < audioScanResult . audioFiles . length ; i ++ ) {
var newAF = audioScanResult . audioFiles [ i ]
var trackIndex = newAF . validateTrackIndex ( audioScanResult . audioFiles . length === 1 )
if ( trackIndex !== null ) {
if ( audiobook . checkHasTrackNum ( trackIndex ) ) {
newAF . setDuplicateTrackNumber ( trackIndex )
} else {
newAF . index = trackIndex
}
}
audiobook . addAudioFile ( newAF )
}
audiobook . rebuildTracks ( )
} else if ( ! audiobook . ebooks . length ) {
// Audiobook has no ebooks and no valid audio tracks do not continue
Logger . warn ( ` [Scanner] Audiobook has no ebooks and no valid audio tracks " ${ audiobook . path } " ` )
return null
}
2021-11-23 02:58:20 +01:00
}
2021-11-25 03:15:50 +01:00
// Look for desc.txt and reader.txt and update
await audiobook . saveDataFromTextFiles ( )
2021-11-23 02:58:20 +01:00
2021-11-25 03:15:50 +01:00
// Extract embedded cover art if cover is not already in directory
if ( audiobook . hasEmbeddedCoverArt && ! audiobook . cover ) {
var outputCoverDirs = this . getCoverDirectory ( audiobook )
var relativeDir = await audiobook . saveEmbeddedCoverArt ( outputCoverDirs . fullPath , outputCoverDirs . relPath )
if ( relativeDir ) {
Logger . debug ( ` [Scanner] Saved embedded cover art " ${ relativeDir } " ` )
}
2021-11-23 02:58:20 +01:00
}
2021-11-25 03:15:50 +01:00
return audiobook
2021-11-23 02:58:20 +01:00
}
}
module . exports = Scanner