2021-09-07 03:14:04 +02:00
const fs = require ( 'fs-extra' )
2021-09-11 02:55:02 +02:00
const Path = require ( 'path' )
2021-10-05 05:11:42 +02:00
// Utils
2021-08-18 00:01:11 +02:00
const Logger = require ( './Logger' )
2021-10-05 05:11:42 +02:00
const { version } = require ( '../package.json' )
2021-08-18 00:01:11 +02:00
const audioFileScanner = require ( './utils/audioFileScanner' )
2021-09-11 02:55:02 +02:00
const { groupFilesIntoAudiobookPaths , getAudiobookFileData , scanRootDir } = require ( './utils/scandir' )
2021-11-25 03:15:50 +01:00
const { comparePaths , getIno , getId , secondsToTimestamp } = require ( './utils/index' )
2021-09-30 03:43:36 +02:00
const { ScanResult , CoverDestination } = require ( './utils/constants' )
2021-09-07 03:14:04 +02:00
2021-10-05 05:11:42 +02:00
const BookFinder = require ( './BookFinder' )
const Audiobook = require ( './objects/Audiobook' )
2021-08-18 00:01:11 +02:00
class Scanner {
2021-10-02 03:29:00 +02:00
constructor ( AUDIOBOOK _PATH , METADATA _PATH , db , coverController , emitter ) {
2021-08-18 00:01:11 +02:00
this . AudiobookPath = AUDIOBOOK _PATH
this . MetadataPath = METADATA _PATH
2021-11-06 23:26:44 +01:00
this . BookMetadataPath = Path . posix . join ( this . MetadataPath . replace ( /\\/g , '/' ) , 'books' )
2021-09-29 17:16:38 +02:00
2021-08-18 00:01:11 +02:00
this . db = db
2021-10-02 03:29:00 +02:00
this . coverController = coverController
2021-08-18 00:01:11 +02:00
this . emitter = emitter
2021-08-25 03:24:40 +02:00
this . cancelScan = false
2021-10-05 05:11:42 +02:00
this . cancelLibraryScan = { }
this . librariesScanning = [ ]
2021-08-25 03:24:40 +02:00
2021-08-18 00:01:11 +02:00
this . bookFinder = new BookFinder ( )
}
get audiobooks ( ) {
return this . db . audiobooks
}
2021-09-30 03:43:36 +02:00
getCoverDirectory ( audiobook ) {
if ( this . db . serverSettings . coverDestination === CoverDestination . AUDIOBOOK ) {
return {
fullPath : audiobook . fullPath ,
2021-10-05 05:11:42 +02:00
relPath : '/s/book/' + audiobook . id
2021-09-30 03:43:36 +02:00
}
} else {
return {
2021-11-06 23:26:44 +01:00
fullPath : Path . posix . join ( this . BookMetadataPath , audiobook . id ) ,
relPath : Path . posix . join ( '/metadata' , 'books' , audiobook . id )
2021-09-30 03:43:36 +02:00
}
}
}
2021-08-26 00:36:54 +02:00
async setAudioFileInos ( audiobookDataAudioFiles , audiobookAudioFiles ) {
for ( let i = 0 ; i < audiobookDataAudioFiles . length ; i ++ ) {
var abdFile = audiobookDataAudioFiles [ i ]
var matchingFile = audiobookAudioFiles . find ( af => comparePaths ( af . path , abdFile . path ) )
if ( matchingFile ) {
if ( ! matchingFile . ino ) {
matchingFile . ino = await getIno ( matchingFile . fullPath )
}
abdFile . ino = matchingFile . ino
} else {
abdFile . ino = await getIno ( abdFile . fullPath )
if ( ! abdFile . ino ) {
Logger . error ( '[Scanner] Invalid abdFile ino - ignoring abd audio file' , abdFile . path )
}
}
}
return audiobookDataAudioFiles . filter ( abdFile => ! ! abdFile . ino )
}
2021-10-01 21:52:10 +02:00
// Only updates audio files with matching paths
syncAudiobookInodeValues ( audiobook , { audioFiles , otherFiles } ) {
var filesUpdated = 0
// Sync audio files & audio tracks with updated inodes
audiobook . _audioFiles . forEach ( ( audioFile ) => {
var matchingAudioFile = audioFiles . find ( af => af . ino !== audioFile . ino && af . path === audioFile . path )
if ( matchingAudioFile ) {
// Audio Track should always have the same ino as the equivalent audio file (not all audio files have a track)
var audioTrack = audiobook . tracks . find ( t => t . ino === audioFile . ino )
if ( audioTrack ) {
Logger . debug ( ` [Scanner] Found audio file & track with mismatched inode " ${ audioFile . filename } " - updating it ` )
audioTrack . ino = matchingAudioFile . ino
filesUpdated ++
} else {
Logger . debug ( ` [Scanner] Found audio file with mismatched inode " ${ audioFile . filename } " - updating it ` )
}
audioFile . ino = matchingAudioFile . ino
filesUpdated ++
}
} )
// Sync other files with updated inodes
audiobook . _otherFiles . forEach ( ( otherFile ) => {
var matchingOtherFile = otherFiles . find ( of => of . ino !== otherFile . ino && of . path === otherFile . path )
if ( matchingOtherFile ) {
Logger . debug ( ` [Scanner] Found other file with mismatched inode " ${ otherFile . filename } " - updating it ` )
otherFile . ino = matchingOtherFile . ino
filesUpdated ++
}
} )
return filesUpdated
}
2021-10-06 04:10:49 +02:00
async searchForCover ( audiobook ) {
var options = {
titleDistance : 2 ,
authorDistance : 2
}
2021-10-24 03:31:48 +02:00
var results = await this . bookFinder . findCovers ( 'openlibrary' , audiobook . title , audiobook . authorFL , options )
2021-10-06 04:10:49 +02:00
if ( results . length ) {
Logger . debug ( ` [Scanner] Found best cover for " ${ audiobook . title } " ` )
// If the first cover result fails, attempt to download the second
for ( let i = 0 ; i < results . length && i < 2 ; i ++ ) {
// Downloads and updates the book cover
var result = await this . coverController . downloadCoverFromUrl ( audiobook , results [ i ] )
if ( result . error ) {
Logger . error ( ` [Scanner] Failed to download cover from url " ${ results [ i ] } " | Attempt ${ i + 1 } ` , result . error )
} else {
return true
}
}
}
return false
}
async scanExistingAudiobook ( existingAudiobook , audiobookData , hasUpdatedIno , hasUpdatedLibraryOrFolder , forceAudioFileScan ) {
2021-10-05 05:11:42 +02:00
// Always sync files and inode values
var filesInodeUpdated = this . syncAudiobookInodeValues ( existingAudiobook , audiobookData )
if ( hasUpdatedIno || filesInodeUpdated > 0 ) {
Logger . info ( ` [Scanner] Updating inode value for " ${ existingAudiobook . title } " - ${ filesInodeUpdated } files updated ` )
hasUpdatedIno = true
2021-10-01 21:52:10 +02:00
}
2021-10-05 05:11:42 +02:00
// TEMP: Check if is older audiobook and needs force rescan
if ( ! forceAudioFileScan && ( ! existingAudiobook . scanVersion || existingAudiobook . checkHasOldCoverPath ( ) ) ) {
Logger . info ( ` [Scanner] Force rescan for " ${ existingAudiobook . title } " | Last scan v ${ existingAudiobook . scanVersion } | Old Cover Path ${ ! ! existingAudiobook . checkHasOldCoverPath ( ) } ` )
forceAudioFileScan = true
}
2021-09-07 03:14:04 +02:00
2021-10-10 23:36:21 +02:00
// inode is required
2021-10-05 05:11:42 +02:00
audiobookData . audioFiles = audiobookData . audioFiles . filter ( af => af . ino )
2021-09-07 03:14:04 +02:00
2021-10-10 23:36:21 +02:00
// No valid ebook and audio files found, mark as incomplete
var ebookFiles = audiobookData . otherFiles . filter ( f => f . filetype === 'ebook' )
if ( ! audiobookData . audioFiles . length && ! ebookFiles . length ) {
Logger . error ( ` [Scanner] " ${ existingAudiobook . title } " no valid book files found - marking as incomplete ` )
existingAudiobook . setLastScan ( version )
2021-12-02 02:07:03 +01:00
existingAudiobook . isInvalid = true
2021-10-10 23:36:21 +02:00
await this . db . updateAudiobook ( existingAudiobook )
this . emitter ( 'audiobook_updated' , existingAudiobook . toJSONMinified ( ) )
return ScanResult . UPDATED
2021-12-02 02:07:03 +01:00
} else if ( existingAudiobook . isInvalid ) { // Was incomplete but now is not
2021-10-10 23:36:21 +02:00
Logger . info ( ` [Scanner] " ${ existingAudiobook . title } " was incomplete but now has book files ` )
2021-12-02 02:07:03 +01:00
existingAudiobook . isInvalid = false
2021-10-05 05:11:42 +02:00
}
2021-09-07 03:14:04 +02:00
2021-10-05 05:11:42 +02:00
// Check for audio files that were removed
var abdAudioFileInos = audiobookData . audioFiles . map ( af => af . ino )
var removedAudioFiles = existingAudiobook . audioFiles . filter ( file => ! abdAudioFileInos . includes ( file . ino ) )
if ( removedAudioFiles . length ) {
Logger . info ( ` [Scanner] ${ removedAudioFiles . length } audio files removed for audiobook " ${ existingAudiobook . title } " ` )
removedAudioFiles . forEach ( ( af ) => existingAudiobook . removeAudioFile ( af ) )
}
2021-09-27 13:52:21 +02:00
2021-10-05 05:11:42 +02:00
// Check for mismatched audio tracks - tracks with no matching audio file
var removedAudioTracks = existingAudiobook . tracks . filter ( track => ! abdAudioFileInos . includes ( track . ino ) )
if ( removedAudioTracks . length ) {
Logger . error ( ` [Scanner] ${ removedAudioTracks . length } tracks removed no matching audio file for audiobook " ${ existingAudiobook . title } " ` )
removedAudioTracks . forEach ( ( at ) => existingAudiobook . removeAudioTrack ( at ) )
}
2021-10-01 01:52:32 +02:00
2021-10-05 05:11:42 +02:00
// Check for new audio files and sync existing audio files
var newAudioFiles = [ ]
var hasUpdatedAudioFiles = false
audiobookData . audioFiles . forEach ( ( file ) => {
var existingAudioFile = existingAudiobook . getAudioFileByIno ( file . ino )
if ( existingAudioFile ) { // Audio file exists, sync path (path may have been renamed)
if ( existingAudiobook . syncAudioFile ( existingAudioFile , file ) ) {
2021-10-01 01:52:32 +02:00
hasUpdatedAudioFiles = true
2021-10-05 05:11:42 +02:00
}
} else {
// New audio file, triple check for matching file path
var audioFileWithMatchingPath = existingAudiobook . getAudioFileByPath ( file . fullPath )
if ( audioFileWithMatchingPath ) {
Logger . warn ( ` [Scanner] Audio file with path already exists with different inode, New: " ${ file . filename } " ( ${ file . ino } ) | Existing: ${ audioFileWithMatchingPath . filename } ( ${ audioFileWithMatchingPath . ino } ) ` )
2021-10-01 01:52:32 +02:00
} else {
2021-10-05 05:11:42 +02:00
newAudioFiles . push ( file )
2021-10-01 01:52:32 +02:00
}
}
2021-10-05 05:11:42 +02:00
} )
2021-10-01 01:52:32 +02:00
2021-11-10 00:54:28 +01:00
// Sync other files (all files that are not audio files) - Updates cover path
var hasOtherFileUpdates = false
2021-11-26 01:39:02 +01:00
var otherFilesUpdated = await existingAudiobook . syncOtherFiles ( audiobookData . otherFiles , this . MetadataPath , false , forceAudioFileScan )
2021-11-10 00:54:28 +01:00
if ( otherFilesUpdated ) {
hasOtherFileUpdates = true
}
2021-10-05 05:11:42 +02:00
// Rescan audio file metadata
if ( forceAudioFileScan ) {
Logger . info ( ` [Scanner] Rescanning ${ existingAudiobook . audioFiles . length } audio files for " ${ existingAudiobook . title } " ` )
var numAudioFilesUpdated = await audioFileScanner . rescanAudioFiles ( existingAudiobook )
2021-10-23 13:50:13 +02:00
// Set book details from metadata pulled from audio files
var bookMetadataUpdated = existingAudiobook . setDetailsFromFileMetadata ( )
if ( bookMetadataUpdated ) {
Logger . debug ( ` [Scanner] Book Metadata Updated for " ${ existingAudiobook . title } " ` )
hasUpdatedAudioFiles = true
}
2021-10-05 05:11:42 +02:00
if ( numAudioFilesUpdated > 0 ) {
Logger . info ( ` [Scanner] Rescan complete, ${ numAudioFilesUpdated } audio files were updated for " ${ existingAudiobook . title } " ` )
hasUpdatedAudioFiles = true
// Use embedded cover art if audiobook has no cover
if ( existingAudiobook . hasEmbeddedCoverArt && ! existingAudiobook . cover ) {
var outputCoverDirs = this . getCoverDirectory ( existingAudiobook )
var relativeDir = await existingAudiobook . saveEmbeddedCoverArt ( outputCoverDirs . fullPath , outputCoverDirs . relPath )
if ( relativeDir ) {
Logger . debug ( ` [Scanner] Saved embedded cover art " ${ relativeDir } " ` )
}
}
} else {
Logger . info ( ` [Scanner] Rescan complete, audio files were up to date for " ${ existingAudiobook . title } " ` )
2021-09-07 03:14:04 +02:00
}
2021-10-05 05:11:42 +02:00
}
2021-09-07 03:14:04 +02:00
2021-10-05 05:11:42 +02:00
// Scan and add new audio files found and set tracks
if ( newAudioFiles . length ) {
Logger . info ( ` [Scanner] ${ newAudioFiles . length } new audio files were found for audiobook " ${ existingAudiobook . title } " ` )
await audioFileScanner . scanAudioFiles ( existingAudiobook , newAudioFiles )
}
2021-09-07 03:14:04 +02:00
2021-10-10 23:36:21 +02:00
// After scanning audio files, some may no longer be valid
// so make sure the directory still has valid book files
if ( ! existingAudiobook . tracks . length && ! ebookFiles . length ) {
Logger . error ( ` [Scanner] " ${ existingAudiobook . title } " no valid book files found after update - marking as incomplete ` )
existingAudiobook . setLastScan ( version )
2021-12-02 02:07:03 +01:00
existingAudiobook . isInvalid = true
2021-10-10 23:36:21 +02:00
await this . db . updateAudiobook ( existingAudiobook )
this . emitter ( 'audiobook_updated' , existingAudiobook . toJSONMinified ( ) )
return ScanResult . UPDATED
2021-10-05 05:11:42 +02:00
}
2021-09-07 03:14:04 +02:00
2021-11-10 00:54:28 +01:00
var hasUpdates = hasOtherFileUpdates || hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles . length || removedAudioTracks . length || newAudioFiles . length || hasUpdatedAudioFiles
2021-09-07 03:14:04 +02:00
2021-10-05 05:11:42 +02:00
// Check that audio tracks are in sequential order with no gaps
2021-11-26 01:39:02 +01:00
if ( existingAudiobook . checkUpdateMissingTracks ( ) ) {
2021-10-05 05:11:42 +02:00
Logger . info ( ` [Scanner] " ${ existingAudiobook . title } " missing parts updated ` )
hasUpdates = true
}
2021-09-07 03:14:04 +02:00
2021-10-05 05:11:42 +02:00
// Syncs path and fullPath
if ( existingAudiobook . syncPaths ( audiobookData ) ) {
hasUpdates = true
}
2021-09-08 16:15:54 +02:00
2021-10-05 05:11:42 +02:00
// If audiobook was missing before, it is now found
if ( existingAudiobook . isMissing ) {
existingAudiobook . isMissing = false
hasUpdates = true
Logger . info ( ` [Scanner] " ${ existingAudiobook . title } " was missing but now it is found ` )
}
2021-09-07 03:14:04 +02:00
2021-10-06 04:10:49 +02:00
if ( hasUpdates || version !== existingAudiobook . scanVersion ) {
2021-10-05 05:11:42 +02:00
existingAudiobook . setChapters ( )
existingAudiobook . setLastScan ( version )
await this . db . updateAudiobook ( existingAudiobook )
2021-09-07 03:14:04 +02:00
2021-10-06 04:10:49 +02:00
Logger . info ( ` [Scanner] " ${ existingAudiobook . title } " was updated ` )
this . emitter ( 'audiobook_updated' , existingAudiobook . toJSONMinified ( ) )
2021-10-05 05:11:42 +02:00
return ScanResult . UPDATED
2021-09-07 03:14:04 +02:00
}
2021-10-05 05:11:42 +02:00
return ScanResult . UPTODATE
}
async scanNewAudiobook ( audiobookData ) {
2021-10-10 23:36:21 +02:00
var ebookFiles = audiobookData . otherFiles . map ( f => f . filetype === 'ebook' )
if ( ! audiobookData . audioFiles . length && ! ebookFiles . length ) {
Logger . error ( '[Scanner] No valid audio files and ebooks for Audiobook' , audiobookData . path )
2021-10-06 04:10:49 +02:00
return null
2021-09-07 03:14:04 +02:00
}
var audiobook = new Audiobook ( )
audiobook . setData ( audiobookData )
2021-10-05 05:11:42 +02:00
// Scan audio files and set tracks, pulls metadata
2021-09-07 03:14:04 +02:00
await audioFileScanner . scanAudioFiles ( audiobook , audiobookData . audioFiles )
2021-10-10 23:36:21 +02:00
if ( ! audiobook . tracks . length && ! audiobook . ebooks . length ) {
Logger . warn ( '[Scanner] Invalid audiobook, no valid audio tracks and ebook files' , audiobook . title )
2021-10-06 04:10:49 +02:00
return null
2021-09-07 03:14:04 +02:00
}
2021-10-05 05:11:42 +02:00
// Look for desc.txt and reader.txt and update
2021-11-26 01:39:02 +01:00
await audiobook . saveDataFromTextFiles ( false )
2021-09-30 03:43:36 +02:00
2021-10-06 04:10:49 +02:00
// Extract embedded cover art if cover is not already in directory
if ( audiobook . hasEmbeddedCoverArt && ! audiobook . cover ) {
2021-09-30 03:43:36 +02:00
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-10-05 05:11:42 +02:00
// Set book details from metadata pulled from audio files
2021-09-30 03:43:36 +02:00
audiobook . setDetailsFromFileMetadata ( )
2021-10-05 05:11:42 +02:00
// Check for gaps in track numbers
2021-11-26 01:39:02 +01:00
audiobook . checkUpdateMissingTracks ( )
2021-10-05 05:11:42 +02:00
// Set chapters from audio files
2021-09-08 16:15:54 +02:00
audiobook . setChapters ( )
2021-10-05 05:11:42 +02:00
audiobook . setLastScan ( version )
2021-09-07 03:14:04 +02:00
Logger . info ( ` [Scanner] Audiobook " ${ audiobook . title } " Scanned ( ${ audiobook . sizePretty } ) [ ${ audiobook . durationPretty } ] ` )
2021-10-05 05:11:42 +02:00
await this . db . insertEntity ( 'audiobook' , audiobook )
2021-09-07 03:14:04 +02:00
this . emitter ( 'audiobook_added' , audiobook . toJSONMinified ( ) )
2021-10-06 04:10:49 +02:00
return audiobook
2021-09-07 03:14:04 +02:00
}
2021-10-05 05:11:42 +02:00
async scanAudiobookData ( audiobookData , forceAudioFileScan = false ) {
var scannerFindCovers = this . db . serverSettings . scannerFindCovers
var libraryId = audiobookData . libraryId
2021-10-06 04:10:49 +02:00
var folderId = audiobookData . folderId
var hasUpdatedLibraryOrFolder = false
var existingAudiobook = this . audiobooks . find ( ab => ab . ino === audiobookData . ino )
// Make sure existing audiobook has the same library & folder id
if ( existingAudiobook && ( existingAudiobook . libraryId !== libraryId || existingAudiobook . folderId !== folderId ) ) {
var existingAudiobookLibrary = this . db . libraries . find ( lib => lib . id === existingAudiobook . libraryId )
if ( ! existingAudiobookLibrary ) {
Logger . error ( ` [Scanner] Audiobook " ${ existingAudiobook . title } " found in different library that no longer exists ${ existingAudiobook . libraryId } ` )
} else if ( existingAudiobook . libraryId !== libraryId ) {
Logger . warn ( ` [Scanner] Audiobook " ${ existingAudiobook . title } " found in different library " ${ existingAudiobookLibrary . name } " ` )
} else {
Logger . warn ( ` [Scanner] Audiobook " ${ existingAudiobook . title } " found in different folder " ${ existingAudiobook . folderId } " of library " ${ existingAudiobookLibrary . name } " ` )
}
existingAudiobook . libraryId = libraryId
existingAudiobook . folderId = folderId
hasUpdatedLibraryOrFolder = true
Logger . info ( ` [Scanner] Updated Audiobook " ${ existingAudiobook . title } " library and folder to " ${ libraryId } " " ${ folderId } " ` )
}
2021-10-05 05:11:42 +02:00
// inode value may change when using shared drives, update inode if matching path is found
// Note: inode will not change on rename
var hasUpdatedIno = false
if ( ! existingAudiobook ) {
// check an audiobook exists with matching path, then update inodes
2021-10-06 04:10:49 +02:00
existingAudiobook = this . audiobooks . find ( a => a . path === audiobookData . path )
2021-10-05 05:11:42 +02:00
if ( existingAudiobook ) {
2021-10-06 04:10:49 +02:00
var oldIno = existingAudiobook . ino
2021-10-05 05:11:42 +02:00
existingAudiobook . ino = audiobookData . ino
2021-10-06 04:10:49 +02:00
Logger . debug ( ` [Scanner] Scan Audiobook Data: Updated inode from " ${ oldIno } " to " ${ existingAudiobook . ino } " ` )
2021-10-05 05:11:42 +02:00
hasUpdatedIno = true
2021-10-06 04:10:49 +02:00
if ( existingAudiobook . libraryId !== libraryId || existingAudiobook . folderId !== folderId ) {
Logger . warn ( ` [Scanner] Audiobook found by path is in a different library or folder, ${ existingAudiobook . libraryId } / ${ existingAudiobook . folderId } should be ${ libraryId } / ${ folderId } ` )
existingAudiobook . libraryId = libraryId
existingAudiobook . folderId = folderId
hasUpdatedLibraryOrFolder = true
Logger . info ( ` [Scanner] Updated Audiobook " ${ existingAudiobook . title } " library and folder to " ${ libraryId } " " ${ folderId } " ` )
}
2021-10-05 05:11:42 +02:00
}
}
2021-10-06 04:10:49 +02:00
var scanResult = null
var finalAudiobook = null
2021-10-05 05:11:42 +02:00
if ( existingAudiobook ) {
2021-10-06 04:10:49 +02:00
finalAudiobook = existingAudiobook
scanResult = await this . scanExistingAudiobook ( existingAudiobook , audiobookData , hasUpdatedIno , hasUpdatedLibraryOrFolder , forceAudioFileScan )
if ( scanResult === ScanResult . REMOVED || scanResult === ScanResult . NOTHING ) {
finalAudiobook = null
}
} else {
finalAudiobook = await this . scanNewAudiobook ( audiobookData )
scanResult = finalAudiobook ? ScanResult . ADDED : ScanResult . NOTHING
if ( finalAudiobook === ScanResult . NOTHING ) {
finalAudiobook = null
scanResult = ScanResult . NOTHING
} else {
scanResult = ScanResult . ADDED
}
}
// Scan for cover if enabled and has no cover
if ( finalAudiobook && scannerFindCovers && ! finalAudiobook . cover ) {
if ( finalAudiobook . book . shouldSearchForCover ) {
var updatedCover = await this . searchForCover ( finalAudiobook )
finalAudiobook . book . updateLastCoverSearch ( updatedCover )
if ( updatedCover && scanResult === ScanResult . UPTODATE ) {
scanResult = ScanResult . UPDATED
}
await this . db . updateAudiobook ( finalAudiobook )
this . emitter ( 'audiobook_updated' , finalAudiobook . toJSONMinified ( ) )
} else {
Logger . debug ( ` [Scanner] Audiobook " ${ finalAudiobook . title } " cover already scanned - not re-scanning ` )
}
2021-10-05 05:11:42 +02:00
}
2021-10-06 04:10:49 +02:00
return scanResult
2021-10-05 05:11:42 +02:00
}
async scan ( libraryId , forceAudioFileScan = false ) {
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
}
2021-10-06 04:10:49 +02:00
var scanPayload = {
2021-10-05 05:11:42 +02:00
id : libraryId ,
name : library . name ,
folders : library . folders . length
2021-10-06 04:10:49 +02:00
}
this . emitter ( 'scan_start' , scanPayload )
2021-10-05 05:11:42 +02:00
Logger . info ( ` [Scanner] Starting scan of library " ${ library . name } " with ${ library . folders . length } folders ` )
2021-10-06 04:10:49 +02:00
library . lastScan = Date . now ( )
await this . db . updateEntity ( 'library' , library )
this . librariesScanning . push ( scanPayload )
2021-10-05 05:11:42 +02:00
var audiobooksInLibrary = this . db . audiobooks . filter ( ab => ab . libraryId === libraryId )
2021-09-30 03:43:36 +02:00
// TODO: This temporary fix from pre-release should be removed soon, "checkUpdateInos"
2021-10-05 05:11:42 +02:00
if ( audiobooksInLibrary . length ) {
for ( let i = 0 ; i < audiobooksInLibrary . length ; i ++ ) {
var ab = audiobooksInLibrary [ i ]
2021-09-27 13:52:21 +02:00
// Update ino if inos are not set
var shouldUpdateIno = ab . hasMissingIno
if ( shouldUpdateIno ) {
2021-10-06 04:10:49 +02:00
var filesWithMissingIno = ab . getFilesWithMissingIno ( )
2021-11-23 02:58:20 +01:00
Logger . debug ( ` \n Updating inos for " ${ ab . title } " ` )
2021-10-06 04:10:49 +02:00
Logger . debug ( ` In Scan, Files with missing inode ` , filesWithMissingIno )
2021-09-27 13:52:21 +02:00
var hasUpdates = await ab . checkUpdateInos ( )
if ( hasUpdates ) {
await this . db . updateAudiobook ( ab )
}
}
}
}
2021-08-24 14:50:36 +02:00
2021-08-18 00:01:11 +02:00
const scanStart = Date . now ( )
2021-10-05 05:11:42 +02:00
var audiobookDataFound = [ ]
for ( let i = 0 ; i < library . folders . length ; i ++ ) {
var folder = library . 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 )
}
2021-08-24 14:15:56 +02:00
2021-09-27 13:52:21 +02:00
// Remove audiobooks with no inode
audiobookDataFound = audiobookDataFound . filter ( abd => abd . ino )
2021-08-26 00:36:54 +02:00
2021-10-05 05:11:42 +02:00
if ( this . cancelLibraryScan [ libraryId ] ) {
Logger . info ( ` [Scanner] Canceling scan ${ libraryId } ` )
delete this . cancelLibraryScan [ libraryId ]
2021-10-06 04:10:49 +02:00
this . librariesScanning = this . librariesScanning . filter ( l => l . id !== libraryId )
2021-11-25 03:15:50 +01:00
this . emitter ( 'scan_complete' , { id : libraryId , name : library . name , results : null } )
2021-08-25 03:24:40 +02:00
return null
}
2021-08-24 14:15:56 +02:00
var scanResults = {
removed : 0 ,
updated : 0 ,
2021-09-18 01:40:30 +02:00
added : 0 ,
missing : 0
2021-08-24 14:15:56 +02:00
}
// Check for removed audiobooks
2021-10-05 05:11:42 +02:00
for ( let i = 0 ; i < audiobooksInLibrary . length ; i ++ ) {
var audiobook = audiobooksInLibrary [ i ]
2021-11-23 02:58:20 +01:00
var dataFound = audiobookDataFound . find ( abd => abd . ino === audiobook . ino || comparePaths ( abd . path , audiobook . path ) )
2021-08-24 14:15:56 +02:00
if ( ! dataFound ) {
2021-09-18 01:40:30 +02:00
Logger . info ( ` [Scanner] Audiobook " ${ audiobook . title } " is missing ` )
audiobook . isMissing = true
audiobook . lastUpdate = Date . now ( )
scanResults . missing ++
await this . db . updateAudiobook ( audiobook )
this . emitter ( 'audiobook_updated' , audiobook . toJSONMinified ( ) )
2021-08-24 14:15:56 +02:00
}
2021-10-05 05:11:42 +02:00
if ( this . cancelLibraryScan [ libraryId ] ) {
Logger . info ( ` [Scanner] Canceling scan ${ libraryId } ` )
delete this . cancelLibraryScan [ libraryId ]
2021-10-06 04:10:49 +02:00
this . librariesScanning = this . librariesScanning . filter ( l => l . id !== libraryId )
2021-11-25 03:15:50 +01:00
this . emitter ( 'scan_complete' , { id : libraryId , name : library . name , results : scanResults } )
2021-10-05 05:11:42 +02:00
return
2021-08-25 03:24:40 +02:00
}
2021-08-24 14:15:56 +02:00
}
2021-09-07 03:14:04 +02:00
// Check for new and updated audiobooks
2021-08-18 00:01:11 +02:00
for ( let i = 0 ; i < audiobookDataFound . length ; i ++ ) {
2021-10-01 01:52:32 +02:00
var result = await this . scanAudiobookData ( audiobookDataFound [ i ] , forceAudioFileScan )
2021-09-07 03:14:04 +02:00
if ( result === ScanResult . ADDED ) scanResults . added ++
if ( result === ScanResult . REMOVED ) scanResults . removed ++
if ( result === ScanResult . UPDATED ) scanResults . updated ++
2021-08-24 14:15:56 +02:00
var progress = Math . round ( 100 * ( i + 1 ) / audiobookDataFound . length )
this . emitter ( 'scan_progress' , {
2021-10-05 05:11:42 +02:00
id : libraryId ,
name : library . name ,
2021-08-25 03:24:40 +02:00
progress : {
total : audiobookDataFound . length ,
done : i + 1 ,
progress
}
2021-08-24 14:15:56 +02:00
} )
2021-10-05 05:11:42 +02:00
if ( this . cancelLibraryScan [ libraryId ] ) {
Logger . info ( ` [Scanner] Canceling scan ${ libraryId } ` )
delete this . cancelLibraryScan [ libraryId ]
2021-08-25 03:24:40 +02:00
break
}
2021-08-18 00:01:11 +02:00
}
const scanElapsed = Math . floor ( ( Date . now ( ) - scanStart ) / 1000 )
2021-09-18 01:40:30 +02:00
Logger . info ( ` [Scanned] Finished | ${ scanResults . added } added | ${ scanResults . updated } updated | ${ scanResults . removed } removed | ${ scanResults . missing } missing | elapsed: ${ secondsToTimestamp ( scanElapsed ) } ` )
2021-10-06 04:10:49 +02:00
this . librariesScanning = this . librariesScanning . filter ( l => l . id !== libraryId )
2021-11-25 03:15:50 +01:00
this . emitter ( 'scan_complete' , { id : libraryId , name : library . name , results : scanResults } )
2021-08-18 00:01:11 +02:00
}
2021-10-01 01:52:32 +02:00
async scanAudiobookById ( audiobookId ) {
const audiobook = this . db . audiobooks . find ( ab => ab . id === audiobookId )
if ( ! audiobook ) {
Logger . error ( ` [Scanner] Scan audiobook by id not found ${ audiobookId } ` )
return ScanResult . NOTHING
}
2021-10-05 05:11:42 +02:00
const library = this . db . libraries . find ( lib => lib . id === audiobook . libraryId )
if ( ! library ) {
Logger . error ( ` [Scanner] Scan audiobook by id library not found " ${ audiobook . libraryId } " ` )
return ScanResult . NOTHING
}
const folder = library . folders . find ( f => f . id === audiobook . folderId )
if ( ! folder ) {
Logger . error ( ` [Scanner] Scan audiobook by id folder not found " ${ audiobook . folderId } " in library " ${ library . name } " ` )
return ScanResult . NOTHING
}
2021-10-06 04:10:49 +02:00
if ( ! folder . libraryId ) {
Logger . fatal ( ` [Scanner] Folder does not have a library id set... ` , folder )
return ScanResult . NOTHING
}
2021-10-05 05:11:42 +02:00
2021-10-01 01:52:32 +02:00
Logger . info ( ` [Scanner] Scanning Audiobook " ${ audiobook . title } " ` )
2021-10-05 05:11:42 +02:00
return this . scanAudiobook ( folder , audiobook . fullPath , true )
2021-10-01 01:52:32 +02:00
}
2021-10-05 05:11:42 +02:00
async scanAudiobook ( folder , audiobookFullPath , forceAudioFileScan = false ) {
Logger . debug ( '[Scanner] scanAudiobook' , audiobookFullPath )
var audiobookData = await getAudiobookFileData ( folder , audiobookFullPath , this . db . serverSettings )
2021-09-11 02:55:02 +02:00
if ( ! audiobookData ) {
return ScanResult . NOTHING
}
2021-10-01 01:52:32 +02:00
return this . scanAudiobookData ( audiobookData , forceAudioFileScan )
2021-09-11 02:55:02 +02:00
}
2021-10-05 05:11:42 +02:00
async scanFolderUpdates ( libraryId , folderId , fileUpdateBookGroup ) {
var library = this . db . libraries . find ( lib => lib . id === libraryId )
if ( ! library ) {
Logger . error ( ` [Scanner] Library " ${ libraryId } " not found for scan library updates ` )
return null
}
var folder = library . folders . find ( f => f . id === folderId )
if ( ! folder ) {
Logger . error ( ` [Scanner] Folder " ${ folderId } " not found in library " ${ library . name } " for scan library updates ` )
return null
}
2021-10-06 04:10:49 +02:00
2021-10-05 05:11:42 +02:00
Logger . debug ( ` [Scanner] Scanning file update groups in folder " ${ folder . id } " of library " ${ library . name } " ` )
var bookGroupingResults = { }
for ( const bookDir in fileUpdateBookGroup ) {
2021-11-06 23:26:44 +01:00
var fullPath = Path . posix . join ( folder . fullPath . replace ( /\\/g , '/' ) , bookDir )
2021-10-05 05:11:42 +02:00
// Check if book dir group is already an audiobook or in a subdir of an audiobook
var existingAudiobook = this . db . audiobooks . find ( ab => fullPath . startsWith ( ab . fullPath ) )
if ( existingAudiobook ) {
// Is the audiobook exactly - check if was deleted
if ( existingAudiobook . fullPath === fullPath ) {
var exists = await fs . pathExists ( fullPath )
if ( ! exists ) {
Logger . info ( ` [Scanner] Scanning file update group and audiobook was deleted " ${ existingAudiobook . title } " - marking as missing ` )
existingAudiobook . isMissing = true
existingAudiobook . lastUpdate = Date . now ( )
await this . db . updateAudiobook ( existingAudiobook )
this . emitter ( 'audiobook_updated' , existingAudiobook . toJSONMinified ( ) )
bookGroupingResults [ bookDir ] = ScanResult . REMOVED
continue ;
}
}
// Scan audiobook for updates
Logger . debug ( ` [Scanner] Folder update for relative path " ${ bookDir } " is in audiobook " ${ existingAudiobook . title } " - scan for updates ` )
bookGroupingResults [ bookDir ] = await this . scanAudiobook ( folder , existingAudiobook . fullPath )
continue ;
2021-09-07 03:14:04 +02:00
}
2021-09-11 02:55:02 +02:00
2021-10-05 05:11:42 +02:00
// Check if an audiobook is a subdirectory of this dir
var childAudiobook = this . db . audiobooks . find ( ab => ab . fullPath . startsWith ( fullPath ) )
if ( childAudiobook ) {
Logger . warn ( ` [Scanner] Files were modified in a parent directory of an audiobook " ${ childAudiobook . title } " - ignoring ` )
bookGroupingResults [ bookDir ] = ScanResult . NOTHING
continue ;
2021-09-11 02:55:02 +02:00
}
2021-10-05 05:11:42 +02:00
Logger . debug ( ` [Scanner] Folder update group must be a new book " ${ bookDir } " in library " ${ library . name } " ` )
bookGroupingResults [ bookDir ] = await this . scanAudiobook ( folder , fullPath )
2021-09-07 03:14:04 +02:00
}
2021-10-05 05:11:42 +02:00
return bookGroupingResults
}
2021-09-11 02:55:02 +02:00
2021-10-05 05:11:42 +02:00
// Array of file update objects that may have been renamed, removed or added
async filesChanged ( fileUpdates ) {
if ( ! fileUpdates . length ) return null
2021-09-11 02:55:02 +02:00
2021-10-05 05:11:42 +02:00
// Group files by folder
var folderGroups = { }
fileUpdates . forEach ( ( file ) => {
if ( folderGroups [ file . folderId ] ) {
folderGroups [ file . folderId ] . fileUpdates . push ( file )
} else {
folderGroups [ file . folderId ] = {
libraryId : file . libraryId ,
folderId : file . folderId ,
fileUpdates : [ file ]
}
}
} )
const libraryScanResults = { }
// Group files by book
for ( const folderId in folderGroups ) {
var libraryId = folderGroups [ folderId ] . libraryId
2021-10-06 04:10:49 +02:00
var library = this . db . libraries . find ( lib => lib . id === libraryId )
if ( ! library ) {
Logger . error ( ` [Scanner] Library not found in files changed ${ libraryId } ` )
continue ;
}
var folder = library . getFolderById ( folderId )
if ( ! folder ) {
Logger . error ( ` [Scanner] Folder is not in library in files changed " ${ folderId } ", Library " ${ library . name } " ` )
continue ;
}
2021-10-05 05:11:42 +02:00
var relFilePaths = folderGroups [ folderId ] . fileUpdates . map ( fileUpdate => fileUpdate . relPath )
var fileUpdateBookGroup = groupFilesIntoAudiobookPaths ( relFilePaths , true )
var folderScanResults = await this . scanFolderUpdates ( libraryId , folderId , fileUpdateBookGroup )
libraryScanResults [ libraryId ] = folderScanResults
}
2021-09-11 02:55:02 +02:00
2021-10-05 05:11:42 +02:00
Logger . debug ( ` [Scanner] Finished scanning file changes, results: ` , libraryScanResults )
return libraryScanResults
2021-09-07 03:14:04 +02:00
}
2021-09-29 17:16:38 +02:00
async saveMetadata ( audiobookId ) {
if ( audiobookId ) {
var audiobook = this . db . audiobooks . find ( ab => ab . id === audiobookId )
if ( ! audiobook ) {
return {
error : 'Audiobook not found'
}
}
var savedPath = await audiobook . writeNfoFile ( )
return {
audiobookId ,
audiobookTitle : audiobook . title ,
savedPath
}
} else {
var response = {
success : 0 ,
failed : 0
}
for ( let i = 0 ; i < this . db . audiobooks . length ; i ++ ) {
var audiobook = this . db . audiobooks [ i ]
var savedPath = await audiobook . writeNfoFile ( )
if ( savedPath ) {
Logger . info ( ` [Scanner] Saved metadata nfo ${ savedPath } ` )
response . success ++
} else {
response . failed ++
}
}
return response
}
}
2021-08-18 00:01:11 +02:00
async find ( req , res ) {
var method = req . params . method
var query = req . query
var result = null
if ( method === 'isbn' ) {
result = await this . bookFinder . findByISBN ( query )
} else if ( method === 'search' ) {
2021-08-21 16:15:44 +02:00
result = await this . bookFinder . search ( query . provider , query . title , query . author || null )
2021-08-18 00:01:11 +02:00
}
res . json ( result )
}
2021-08-21 16:15:44 +02:00
async findCovers ( req , res ) {
var query = req . query
2021-08-26 02:15:00 +02:00
var options = {
fallbackTitleOnly : ! ! query . fallbackTitleOnly
}
var result = await this . bookFinder . findCovers ( query . provider , query . title , query . author || null , options )
2021-08-21 16:15:44 +02:00
res . json ( result )
}
2021-11-16 03:09:42 +01:00
async fixDuplicateIds ( ) {
var ids = { }
var audiobooksUpdated = 0
for ( let i = 0 ; i < this . db . audiobooks . length ; i ++ ) {
var ab = this . db . audiobooks [ i ]
if ( ids [ ab . id ] ) {
var abCopy = new Audiobook ( ab . toJSON ( ) )
abCopy . id = getId ( 'ab' )
if ( abCopy . book . cover ) {
abCopy . book . cover = abCopy . book . cover . replace ( ab . id , abCopy . id )
}
Logger . warn ( 'Found duplicate ID - updating from' , ab . id , 'to' , abCopy . id )
await this . db . removeEntity ( 'audiobook' , ab . id )
await this . db . insertEntity ( 'audiobook' , abCopy )
audiobooksUpdated ++
} else {
ids [ ab . id ] = true
}
}
if ( audiobooksUpdated ) {
Logger . info ( ` [Scanner] Updated ${ audiobooksUpdated } audiobook IDs ` )
}
}
2021-08-18 00:01:11 +02:00
}
module . exports = Scanner