2021-11-23 02:58:20 +01:00
const fs = require ( 'fs-extra' )
const Path = require ( 'path' )
// Utils
const Logger = require ( '../Logger' )
2022-03-13 00:45:32 +01:00
const { groupFilesIntoLibraryItemPaths , getLibraryItemFileData , scanFolder } = require ( '../utils/scandir' )
2022-03-18 15:16:10 +01:00
const { comparePaths } = require ( '../utils/index' )
2022-02-27 19:47:56 +01:00
const { ScanResult , LogLevel } = require ( '../utils/constants' )
2021-11-23 02:58:20 +01:00
const AudioFileScanner = require ( './AudioFileScanner' )
2022-03-06 23:32:04 +01:00
const BookFinder = require ( '../finders/BookFinder' )
2022-03-13 00:45:32 +01:00
const LibraryItem = require ( '../objects/LibraryItem' )
2021-11-23 02:58:20 +01:00
const LibraryScan = require ( './LibraryScan' )
const ScanOptions = require ( './ScanOptions' )
2022-03-13 00:45:32 +01:00
const Author = require ( '../objects/entities/Author' )
const Series = require ( '../objects/entities/Series' )
2021-11-23 02:58:20 +01:00
class Scanner {
2022-03-20 22:41:06 +01:00
constructor ( db , coverManager , emitter ) {
2022-02-27 20:47:52 +01:00
this . ScanLogPath = Path . posix . join ( global . MetadataPath , 'logs' , 'scans' )
2021-11-23 02:58:20 +01:00
this . db = db
2022-03-20 22:41:06 +01:00
this . coverManager = coverManager
2021-11-23 02:58:20 +01:00
this . emitter = emitter
this . cancelLibraryScan = { }
this . librariesScanning = [ ]
this . bookFinder = new BookFinder ( )
}
2021-11-26 01:39:02 +01:00
isLibraryScanning ( libraryId ) {
return this . librariesScanning . find ( ls => ls . id === libraryId )
}
2021-11-26 03:25:44 +01:00
setCancelLibraryScan ( libraryId ) {
var libraryScanning = this . librariesScanning . find ( ls => ls . id === libraryId )
if ( ! libraryScanning ) return
this . cancelLibraryScan [ libraryId ] = true
}
2022-03-13 00:45:32 +01:00
async scanLibraryItemById ( libraryItemId ) {
var libraryItem = this . db . libraryItems . find ( li => li . id === libraryItemId )
if ( ! libraryItem ) {
Logger . error ( ` [Scanner] Scan libraryItem by id not found ${ libraryItemId } ` )
2021-11-26 01:39:02 +01:00
return ScanResult . NOTHING
}
2022-03-13 00:45:32 +01:00
const library = this . db . libraries . find ( lib => lib . id === libraryItem . libraryId )
2021-11-26 01:39:02 +01:00
if ( ! library ) {
2022-03-13 00:45:32 +01:00
Logger . error ( ` [Scanner] Scan libraryItem by id library not found " ${ libraryItem . libraryId } " ` )
2021-11-26 01:39:02 +01:00
return ScanResult . NOTHING
}
2022-03-13 00:45:32 +01:00
const folder = library . folders . find ( f => f . id === libraryItem . folderId )
2021-11-26 01:39:02 +01:00
if ( ! folder ) {
2022-03-13 00:45:32 +01:00
Logger . error ( ` [Scanner] Scan libraryItem by id folder not found " ${ libraryItem . folderId } " in library " ${ library . name } " ` )
2021-11-26 01:39:02 +01:00
return ScanResult . NOTHING
}
2022-03-13 00:45:32 +01:00
Logger . info ( ` [Scanner] Scanning Library Item " ${ libraryItem . media . metadata . title } " ` )
return this . scanLibraryItem ( library . mediaType , folder , libraryItem )
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
async scanLibraryItem ( libraryMediaType , folder , libraryItem ) {
var libraryItemData = await getLibraryItemFileData ( libraryMediaType , folder , libraryItem . path , this . db . serverSettings )
if ( ! libraryItemData ) {
2021-11-26 01:39:02 +01:00
return ScanResult . NOTHING
}
var hasUpdated = false
2022-03-13 00:45:32 +01:00
var checkRes = libraryItem . checkScanData ( libraryItemData )
2021-11-26 01:39:02 +01:00
if ( checkRes . updated ) hasUpdated = true
// Sync other files first so that local images are used as cover art
2022-03-13 00:45:32 +01:00
if ( await libraryItem . syncFiles ( this . db . serverSettings . scannerPreferOpfMetadata ) ) {
2021-11-26 01:39:02 +01:00
hasUpdated = true
}
// Scan all audio files
2022-03-13 00:45:32 +01:00
if ( libraryItem . hasAudioFiles ) {
var libraryAudioFiles = libraryItem . libraryFiles . filter ( lf => lf . fileType === 'audio' )
if ( await AudioFileScanner . scanAudioFiles ( libraryAudioFiles , libraryItemData , libraryItem , this . db . serverSettings . scannerPreferAudioMetadata ) ) {
2021-11-26 01:39:02 +01:00
hasUpdated = true
}
// Extract embedded cover art if cover is not already in directory
2022-03-13 00:45:32 +01:00
if ( libraryItem . media . hasEmbeddedCoverArt && ! libraryItem . media . coverPath ) {
2022-03-20 22:41:06 +01:00
var coverPath = await this . coverManager . saveEmbeddedCoverArt ( libraryItem )
2022-03-13 00:45:32 +01:00
if ( coverPath ) {
Logger . debug ( ` [Scanner] Saved embedded cover art " ${ coverPath } " ` )
2021-11-26 01:39:02 +01:00
hasUpdated = true
}
}
}
2022-03-14 01:34:31 +01:00
2022-03-18 20:08:57 +01:00
await this . createNewAuthorsAndSeries ( libraryItem )
2022-03-17 01:15:25 +01:00
if ( ! libraryItem . hasMediaEntities ) { // Library Item is invalid
2022-03-13 00:45:32 +01:00
libraryItem . setInvalid ( )
2021-11-26 01:39:02 +01:00
hasUpdated = true
2022-03-13 00:45:32 +01:00
} else if ( libraryItem . isInvalid ) {
libraryItem . isInvalid = false
2021-11-26 01:39:02 +01:00
hasUpdated = true
}
if ( hasUpdated ) {
2022-03-13 00:45:32 +01:00
this . emitter ( 'item_updated' , libraryItem . toJSONExpanded ( ) )
await this . db . updateLibraryItem ( libraryItem )
2021-11-26 01:39:02 +01:00
return ScanResult . UPDATED
}
return ScanResult . UPTODATE
}
2022-03-18 17:51:55 +01:00
async scan ( library , options = { } ) {
if ( this . isLibraryScanning ( library . id ) ) {
Logger . error ( ` [Scanner] Already scanning ${ library . id } ` )
2021-11-23 02:58:20 +01:00
return
}
2022-03-18 17:51:55 +01:00
if ( ! library . folders . length ) {
2021-11-23 02:58:20 +01:00
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-26 01:39:02 +01:00
libraryScan . verbose = false
this . librariesScanning . push ( libraryScan . getScanEmitData )
2021-11-25 03:15:50 +01:00
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-26 01:39:02 +01:00
var canceled = await this . scanLibrary ( libraryScan )
if ( canceled ) {
Logger . info ( ` [Scanner] Library scan canceled for " ${ libraryScan . libraryName } " ` )
delete this . cancelLibraryScan [ libraryScan . libraryId ]
}
2021-11-23 02:58:20 +01:00
2021-11-25 03:15:50 +01:00
libraryScan . setComplete ( )
2021-11-23 02:58:20 +01:00
2021-11-26 01:39:02 +01:00
Logger . info ( ` [Scanner] Library scan ${ libraryScan . id } completed in ${ libraryScan . elapsedTimestamp } | ${ libraryScan . resultStats } ` )
2021-11-25 03:15:50 +01:00
this . librariesScanning = this . librariesScanning . filter ( ls => ls . id !== library . id )
2021-11-26 01:39:02 +01:00
if ( canceled && ! libraryScan . totalResults ) {
var emitData = libraryScan . getScanEmitData
emitData . results = null
this . emitter ( 'scan_complete' , emitData )
return
}
2021-11-25 03:15:50 +01:00
this . emitter ( 'scan_complete' , libraryScan . getScanEmitData )
2021-11-26 01:39:02 +01:00
if ( libraryScan . totalResults ) {
libraryScan . saveLog ( this . ScanLogPath )
}
2021-11-23 02:58:20 +01:00
}
async scanLibrary ( libraryScan ) {
2022-03-13 00:45:32 +01:00
var libraryItemDataFound = [ ]
2021-11-26 01:39:02 +01:00
// Scan each library
2021-11-23 02:58:20 +01:00
for ( let i = 0 ; i < libraryScan . folders . length ; i ++ ) {
var folder = libraryScan . folders [ i ]
2022-03-13 00:45:32 +01:00
var itemDataFoundInFolder = await scanFolder ( libraryScan . libraryMediaType , folder , this . db . serverSettings )
libraryScan . addLog ( LogLevel . INFO , ` ${ itemDataFoundInFolder . length } item data found in folder " ${ folder . fullPath } " ` )
libraryItemDataFound = libraryItemDataFound . concat ( itemDataFoundInFolder )
2021-11-23 02:58:20 +01:00
}
2021-11-26 01:39:02 +01:00
if ( this . cancelLibraryScan [ libraryScan . libraryId ] ) return true
2022-03-26 20:29:49 +01:00
// Remove items with no inode
2022-03-13 00:45:32 +01:00
libraryItemDataFound = libraryItemDataFound . filter ( lid => lid . ino )
var libraryItemsInLibrary = this . db . libraryItems . filter ( li => li . libraryId === libraryScan . libraryId )
2021-11-23 02:58:20 +01:00
2022-03-18 15:16:10 +01:00
const MaxSizePerChunk = 2.5 e9
2022-03-13 00:45:32 +01:00
const itemDataToRescanChunks = [ ]
const newItemDataToScanChunks = [ ]
var itemsToUpdate = [ ]
var itemDataToRescan = [ ]
2022-03-18 15:16:10 +01:00
var itemDataToRescanSize = 0
2022-03-13 00:45:32 +01:00
var newItemDataToScan = [ ]
2022-03-18 15:16:10 +01:00
var newItemDataToScanSize = 0
2022-03-13 00:45:32 +01:00
var itemsToFindCovers = [ ]
// Check for existing & removed library items
for ( let i = 0 ; i < libraryItemsInLibrary . length ; i ++ ) {
var libraryItem = libraryItemsInLibrary [ i ]
// Find library item folder with matching inode or matching path
var dataFound = libraryItemDataFound . find ( lid => lid . ino === libraryItem . ino || comparePaths ( lid . relPath , libraryItem . relPath ) )
2021-11-23 02:58:20 +01:00
if ( ! dataFound ) {
2022-03-13 00:45:32 +01:00
libraryScan . addLog ( LogLevel . WARN , ` Library Item " ${ libraryItem . media . metadata . title } " is missing ` )
2021-11-26 01:39:02 +01:00
libraryScan . resultsMissing ++
2022-03-13 00:45:32 +01:00
libraryItem . setMissing ( )
itemsToUpdate . push ( libraryItem )
2021-11-23 02:58:20 +01:00
} else {
2022-03-13 00:45:32 +01:00
var checkRes = libraryItem . checkScanData ( dataFound )
if ( checkRes . newLibraryFiles . length || libraryScan . scanOptions . forceRescan ) { // Item has new files
checkRes . libraryItem = libraryItem
checkRes . scanData = dataFound
2022-03-18 15:16:10 +01:00
// If this item will go over max size then push current chunk
if ( libraryItem . audioFileTotalSize + itemDataToRescanSize > MaxSizePerChunk && itemDataToRescan . length > 0 ) {
itemDataToRescanChunks . push ( itemDataToRescan )
itemDataToRescanSize = 0
itemDataToRescan = [ ]
}
2022-03-13 00:45:32 +01:00
itemDataToRescan . push ( checkRes )
2022-03-18 15:16:10 +01:00
itemDataToRescanSize += libraryItem . audioFileTotalSize
if ( itemDataToRescanSize >= MaxSizePerChunk ) {
2022-03-13 00:45:32 +01:00
itemDataToRescanChunks . push ( itemDataToRescan )
2022-03-18 15:16:10 +01:00
itemDataToRescanSize = 0
2022-03-13 00:45:32 +01:00
itemDataToRescan = [ ]
2021-11-26 01:39:02 +01:00
}
2022-03-18 15:16:10 +01:00
} else if ( libraryScan . findCovers && libraryItem . media . shouldSearchForCover ) { // Search cover
2021-11-26 01:39:02 +01:00
libraryScan . resultsUpdated ++
2022-03-13 00:45:32 +01:00
itemsToFindCovers . push ( libraryItem )
itemsToUpdate . push ( libraryItem )
2021-11-26 01:39:02 +01:00
} else if ( checkRes . updated ) { // Updated but no scan required
2021-11-25 03:15:50 +01:00
libraryScan . resultsUpdated ++
2022-03-13 00:45:32 +01:00
itemsToUpdate . push ( libraryItem )
2021-11-23 02:58:20 +01:00
}
2022-03-13 00:45:32 +01:00
libraryItemDataFound = libraryItemDataFound . filter ( lid => lid . ino !== dataFound . ino )
2021-11-23 02:58:20 +01:00
}
}
2022-03-13 00:45:32 +01:00
if ( itemDataToRescan . length ) itemDataToRescanChunks . push ( itemDataToRescan )
// Potential NEW Library Items
for ( let i = 0 ; i < libraryItemDataFound . length ; i ++ ) {
var dataFound = libraryItemDataFound [ i ]
2021-11-23 02:58:20 +01:00
2022-03-13 00:45:32 +01:00
var hasMediaFile = dataFound . libraryFiles . some ( lf => lf . isMediaFile )
if ( ! hasMediaFile ) {
libraryScan . addLog ( LogLevel . WARN , ` Directory found " ${ libraryItemDataFound . path } " has no media files ` )
2021-11-23 02:58:20 +01:00
} else {
2022-03-18 15:16:10 +01:00
var audioFileSize = 0
dataFound . libraryFiles . filter ( lf => lf . fileType == 'audio' ) . forEach ( lf => audioFileSize += lf . metadata . size )
// If this item will go over max size then push current chunk
if ( audioFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan . length > 0 ) {
newItemDataToScanChunks . push ( newItemDataToScan )
newItemDataToScanSize = 0
newItemDataToScan = [ ]
}
2022-03-13 00:45:32 +01:00
newItemDataToScan . push ( dataFound )
2022-03-18 15:16:10 +01:00
newItemDataToScanSize += audioFileSize
if ( newItemDataToScanSize >= MaxSizePerChunk ) {
2022-03-13 00:45:32 +01:00
newItemDataToScanChunks . push ( newItemDataToScan )
2022-03-18 15:16:10 +01:00
newItemDataToScanSize = 0
2022-03-13 00:45:32 +01:00
newItemDataToScan = [ ]
2021-11-26 01:39:02 +01:00
}
2021-11-23 02:58:20 +01:00
}
}
2022-03-13 00:45:32 +01:00
if ( newItemDataToScan . length ) newItemDataToScanChunks . push ( newItemDataToScan )
2021-11-23 02:58:20 +01:00
2022-03-13 00:45:32 +01:00
// Library Items not requiring a scan but require a search for cover
for ( let i = 0 ; i < itemsToFindCovers . length ; i ++ ) {
var libraryItem = itemsToFindCovers [ i ]
var updatedCover = await this . searchForCover ( libraryItem , libraryScan )
libraryItem . media . updateLastCoverSearch ( updatedCover )
2021-11-23 02:58:20 +01:00
}
2022-03-18 15:16:10 +01:00
if ( itemsToUpdate . length ) {
await this . updateLibraryItemChunk ( itemsToUpdate )
2021-11-26 01:39:02 +01:00
if ( this . cancelLibraryScan [ libraryScan . libraryId ] ) return true
}
2022-03-13 00:45:32 +01:00
for ( let i = 0 ; i < itemDataToRescanChunks . length ; i ++ ) {
await this . rescanLibraryItemDataChunk ( itemDataToRescanChunks [ i ] , libraryScan )
2021-11-26 01:39:02 +01:00
if ( this . cancelLibraryScan [ libraryScan . libraryId ] ) return true
2022-03-13 00:45:32 +01:00
// console.log('Rescan chunk done', i, 'of', itemDataToRescanChunks.length)
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
for ( let i = 0 ; i < newItemDataToScanChunks . length ; i ++ ) {
await this . scanNewLibraryItemDataChunk ( newItemDataToScanChunks [ i ] , libraryScan )
// console.log('New scan chunk done', i, 'of', newItemDataToScanChunks.length)
2021-11-26 01:39:02 +01:00
if ( this . cancelLibraryScan [ libraryScan . libraryId ] ) return true
2021-11-23 02:58:20 +01:00
}
}
2022-03-13 00:45:32 +01:00
async updateLibraryItemChunk ( itemsToUpdate ) {
await this . db . updateLibraryItems ( itemsToUpdate )
this . emitter ( 'items_updated' , itemsToUpdate . map ( li => li . toJSONExpanded ( ) ) )
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
async rescanLibraryItemDataChunk ( itemDataToRescan , libraryScan ) {
var itemsUpdated = await Promise . all ( itemDataToRescan . map ( ( lid ) => {
return this . rescanLibraryItem ( lid , libraryScan )
2021-11-26 01:39:02 +01:00
} ) )
2022-03-18 20:08:57 +01:00
for ( const libraryItem of itemsUpdated ) {
// Temp authors & series are inserted - create them if found
await this . createNewAuthorsAndSeries ( libraryItem )
}
2022-03-13 00:45:32 +01:00
itemsUpdated = itemsUpdated . filter ( li => li ) // Filter out nulls
if ( itemsUpdated . length ) {
libraryScan . resultsUpdated += itemsUpdated . length
await this . db . updateLibraryItems ( itemsUpdated )
this . emitter ( 'items_updated' , itemsUpdated . map ( li => li . toJSONExpanded ( ) ) )
2021-12-05 18:29:42 +01:00
}
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
async scanNewLibraryItemDataChunk ( newLibraryItemsData , libraryScan ) {
var newLibraryItems = await Promise . all ( newLibraryItemsData . map ( ( lid ) => {
return this . scanNewLibraryItem ( lid , libraryScan . libraryMediaType , libraryScan . preferAudioMetadata , libraryScan . preferOpfMetadata , libraryScan . findCovers , libraryScan )
2021-11-26 01:39:02 +01:00
} ) )
2022-03-13 00:45:32 +01:00
newLibraryItems = newLibraryItems . filter ( li => li ) // Filter out nulls
2022-03-18 20:08:57 +01:00
for ( const libraryItem of newLibraryItems ) {
// Temp authors & series are inserted - create them if found
await this . createNewAuthorsAndSeries ( libraryItem )
}
2022-03-13 00:45:32 +01:00
libraryScan . resultsAdded += newLibraryItems . length
await this . db . insertLibraryItems ( newLibraryItems )
this . emitter ( 'items_added' , newLibraryItems . map ( li => li . toJSONExpanded ( ) ) )
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
async rescanLibraryItem ( libraryItemCheckData , libraryScan ) {
const { newLibraryFiles , filesRemoved , existingLibraryFiles , libraryItem , scanData , updated } = libraryItemCheckData
libraryScan . addLog ( LogLevel . DEBUG , ` Library " ${ libraryScan . libraryName } " Re-scanning " ${ libraryItem . path } " ` )
2021-12-05 18:29:42 +01:00
var hasUpdated = updated
2021-11-26 01:39:02 +01:00
// Sync other files first to use local images as cover before extracting audio file cover
2022-03-13 00:45:32 +01:00
if ( await libraryItem . syncFiles ( libraryScan . preferOpfMetadata ) ) {
hasUpdated = true
2021-11-26 01:39:02 +01:00
}
2021-11-25 03:15:50 +01:00
2021-12-05 18:29:42 +01:00
// forceRescan all existing audio files - will probe and update ID3 tag metadata
2022-03-13 00:45:32 +01:00
var existingAudioFiles = existingLibraryFiles . filter ( lf => lf . fileType === 'audio' )
if ( libraryScan . scanOptions . forceRescan && existingAudioFiles . length ) {
if ( await AudioFileScanner . scanAudioFiles ( existingAudioFiles , scanData , libraryItem , libraryScan . preferAudioMetadata , libraryScan ) ) {
2021-12-05 18:29:42 +01:00
hasUpdated = true
}
}
// Scan new audio files
2022-03-13 00:45:32 +01:00
var newAudioFiles = newLibraryFiles . filter ( lf => lf . fileType === 'audio' )
var removedAudioFiles = filesRemoved . filter ( lf => lf . fileType === 'audio' )
if ( newAudioFiles . length || removedAudioFiles . length ) {
if ( await AudioFileScanner . scanAudioFiles ( newAudioFiles , scanData , libraryItem , libraryScan . preferAudioMetadata , libraryScan ) ) {
2021-12-05 18:29:42 +01:00
hasUpdated = true
}
}
// If an audio file has embedded cover art and no cover is set yet, extract & use it
2022-03-13 00:45:32 +01:00
if ( newAudioFiles . length || libraryScan . scanOptions . forceRescan ) {
if ( libraryItem . media . hasEmbeddedCoverArt && ! libraryItem . media . coverPath ) {
2022-03-20 22:41:06 +01:00
var savedCoverPath = await this . coverManager . saveEmbeddedCoverArt ( libraryItem )
2022-03-13 00:45:32 +01:00
if ( savedCoverPath ) {
2021-12-05 18:29:42 +01:00
hasUpdated = true
2022-03-13 00:45:32 +01:00
libraryScan . addLog ( LogLevel . DEBUG , ` Saved embedded cover art " ${ savedCoverPath } " ` )
2021-11-25 03:15:50 +01:00
}
}
2021-11-23 02:58:20 +01:00
}
2021-11-26 01:39:02 +01:00
2022-03-17 01:15:25 +01:00
if ( ! libraryItem . hasMediaEntities ) { // Library item is invalid
2022-03-13 00:45:32 +01:00
libraryItem . setInvalid ( )
2021-12-05 18:29:42 +01:00
hasUpdated = true
2022-03-13 00:45:32 +01:00
} else if ( libraryItem . isInvalid ) {
libraryItem . isInvalid = false
2021-12-05 18:29:42 +01:00
hasUpdated = true
2021-11-26 01:39:02 +01:00
}
2021-12-05 18:29:42 +01:00
// Scan for cover if enabled and has no cover (and author or title has changed OR has been 7 days since last lookup)
2022-03-13 00:45:32 +01:00
if ( libraryScan . findCovers && ! libraryItem . media . coverPath && libraryItem . media . shouldSearchForCover ) {
var updatedCover = await this . searchForCover ( libraryItem , libraryScan )
libraryItem . media . updateLastCoverSearch ( updatedCover )
2021-12-05 18:29:42 +01:00
hasUpdated = true
2021-11-23 02:58:20 +01:00
}
2021-11-26 01:39:02 +01:00
2022-03-13 00:45:32 +01:00
return hasUpdated ? libraryItem : null
2021-11-25 03:15:50 +01:00
}
2021-11-23 02:58:20 +01:00
2022-03-13 00:45:32 +01:00
async scanNewLibraryItem ( libraryItemData , libraryMediaType , preferAudioMetadata , preferOpfMetadata , findCovers , libraryScan = null ) {
if ( libraryScan ) libraryScan . addLog ( LogLevel . DEBUG , ` Scanning new library item " ${ libraryItemData . path } " ` )
else Logger . debug ( ` [Scanner] Scanning new item " ${ libraryItemData . path } " ` )
2021-11-26 01:39:02 +01:00
2022-03-13 00:45:32 +01:00
var libraryItem = new LibraryItem ( )
libraryItem . setData ( libraryMediaType , libraryItemData )
2021-11-25 03:15:50 +01:00
2022-03-13 00:45:32 +01:00
var audioFiles = libraryItemData . libraryFiles . filter ( lf => lf . fileType === 'audio' )
if ( audioFiles . length ) {
await AudioFileScanner . scanAudioFiles ( audioFiles , libraryItemData , libraryItem , preferAudioMetadata , libraryScan )
2021-11-26 01:39:02 +01:00
}
2022-03-17 01:15:25 +01:00
if ( ! libraryItem . hasMediaEntities ) {
2022-03-13 00:45:32 +01:00
Logger . warn ( ` [Scanner] Library item has no media files " ${ libraryItemData . path } " ` )
2021-11-26 01:39:02 +01:00
return null
2021-11-23 02:58:20 +01:00
}
2022-03-13 00:45:32 +01:00
await libraryItem . syncFiles ( preferOpfMetadata )
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
2022-03-13 00:45:32 +01:00
if ( libraryItem . media . hasEmbeddedCoverArt && ! libraryItem . media . coverPath ) {
2022-03-20 22:41:06 +01:00
var coverPath = await this . coverManager . saveEmbeddedCoverArt ( libraryItem )
2022-03-13 00:45:32 +01:00
if ( coverPath ) {
if ( libraryScan ) libraryScan . addLog ( LogLevel . DEBUG , ` Saved embedded cover art " ${ coverPath } " ` )
else Logger . debug ( ` [Scanner] Saved embedded cover art " ${ coverPath } " ` )
2021-11-25 03:15:50 +01:00
}
2021-11-23 02:58:20 +01:00
}
2021-11-25 03:15:50 +01:00
2021-11-26 01:39:02 +01:00
// Scan for cover if enabled and has no cover
2022-03-13 00:45:32 +01:00
if ( libraryMediaType !== 'podcast' ) {
if ( libraryItem && findCovers && ! libraryItem . media . coverPath && libraryItem . media . shouldSearchForCover ) {
var updatedCover = await this . searchForCover ( libraryItem , libraryScan )
libraryItem . media . updateLastCoverSearch ( updatedCover )
}
2022-03-13 19:47:36 +01:00
}
return libraryItem
}
async createNewAuthorsAndSeries ( libraryItem ) {
2022-03-18 20:08:57 +01:00
if ( libraryItem . mediaType !== 'book' ) return
2022-03-13 19:47:36 +01:00
// Create or match all new authors and series
if ( libraryItem . media . metadata . authors . some ( au => au . id . startsWith ( 'new' ) ) ) {
var newAuthors = [ ]
libraryItem . media . metadata . authors = libraryItem . media . metadata . authors . map ( ( tempMinAuthor ) => {
var _author = this . db . authors . find ( au => au . checkNameEquals ( tempMinAuthor . name ) )
if ( ! _author ) _author = newAuthors . find ( au => au . checkNameEquals ( tempMinAuthor . name ) ) // Check new unsaved authors
if ( ! _author ) {
_author = new Author ( )
_author . setData ( tempMinAuthor )
newAuthors . push ( _author )
2022-03-13 00:45:32 +01:00
}
2022-03-13 19:47:36 +01:00
return {
id : _author . id ,
name : _author . name
}
} )
if ( newAuthors . length ) {
await this . db . insertEntities ( 'author' , newAuthors )
this . emitter ( 'authors_added' , newAuthors . map ( au => au . toJSON ( ) ) )
2022-03-13 00:45:32 +01:00
}
2022-03-13 19:47:36 +01:00
}
if ( libraryItem . media . metadata . series . some ( se => se . id . startsWith ( 'new' ) ) ) {
var newSeries = [ ]
libraryItem . media . metadata . series = libraryItem . media . metadata . series . map ( ( tempMinSeries ) => {
var _series = this . db . series . find ( se => se . checkNameEquals ( tempMinSeries . name ) )
if ( ! _series ) _series = newSeries . find ( se => se . checkNameEquals ( tempMinSeries . name ) ) // Check new unsaved series
if ( ! _series ) {
_series = new Series ( )
_series . setData ( tempMinSeries )
newSeries . push ( _series )
2022-03-13 00:45:32 +01:00
}
2022-03-13 19:47:36 +01:00
return {
id : _series . id ,
name : _series . name ,
sequence : tempMinSeries . sequence
}
} )
if ( newSeries . length ) {
await this . db . insertEntities ( 'series' , newSeries )
this . emitter ( 'series_added' , newSeries . map ( se => se . toJSON ( ) ) )
2022-03-13 00:45:32 +01:00
}
2021-11-26 01:39:02 +01:00
}
2021-11-23 02:58:20 +01:00
}
2021-11-26 01:39:02 +01:00
getFileUpdatesGrouped ( fileUpdates ) {
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 ]
}
}
} )
return folderGroups
}
async scanFilesChanged ( fileUpdates ) {
if ( ! fileUpdates . length ) return
// files grouped by folder
var folderGroups = this . getFileUpdatesGrouped ( fileUpdates )
for ( const folderId in folderGroups ) {
var libraryId = folderGroups [ folderId ] . libraryId
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 ;
}
var relFilePaths = folderGroups [ folderId ] . fileUpdates . map ( fileUpdate => fileUpdate . relPath )
2022-03-13 00:45:32 +01:00
var fileUpdateGroup = groupFilesIntoLibraryItemPaths ( relFilePaths , true )
var folderScanResults = await this . scanFolderUpdates ( library , folder , fileUpdateGroup )
2021-11-26 01:39:02 +01:00
Logger . debug ( ` [Scanner] Folder scan results ` , folderScanResults )
}
}
2022-03-13 00:45:32 +01:00
async scanFolderUpdates ( library , folder , fileUpdateGroup ) {
2021-11-26 01:39:02 +01:00
Logger . debug ( ` [Scanner] Scanning file update groups in folder " ${ folder . id } " of library " ${ library . name } " ` )
2022-03-22 01:24:38 +01:00
Logger . debug ( ` [Scanner] scanFolderUpdates fileUpdateGroup ` , fileUpdateGroup )
2021-11-26 01:39:02 +01:00
2022-03-13 00:45:32 +01:00
// First pass - Remove files in parent dirs of items and remap the fileupdate group
// Test Case: Moving audio files from library item folder to author folder should trigger a re-scan of the item
var updateGroup = { ... fileUpdateGroup }
for ( const itemDir in updateGroup ) {
var itemDirNestedFiles = fileUpdateGroup [ itemDir ] . filter ( b => b . includes ( '/' ) )
if ( ! itemDirNestedFiles . length ) continue ;
2022-01-10 18:12:47 +01:00
2022-03-13 00:45:32 +01:00
var firstNest = itemDirNestedFiles [ 0 ] . split ( '/' ) . shift ( )
var altDir = ` ${ itemDir } / ${ firstNest } `
2022-01-10 18:12:47 +01:00
2022-03-13 00:45:32 +01:00
var fullPath = Path . posix . join ( folder . fullPath . replace ( /\\/g , '/' ) , itemDir )
var childLibraryItem = this . db . libraryItems . find ( li => li . path !== fullPath && li . fullPath . startsWith ( fullPath ) )
if ( ! childLibraryItem ) {
2022-01-10 18:12:47 +01:00
continue ;
}
var altFullPath = Path . posix . join ( folder . fullPath . replace ( /\\/g , '/' ) , altDir )
2022-03-13 00:45:32 +01:00
var altChildLibraryItem = this . db . libraryItems . find ( li => li . path !== altFullPath && li . path . startsWith ( altFullPath ) )
if ( altChildLibraryItem ) {
2022-01-10 18:12:47 +01:00
continue ;
}
2022-03-13 00:45:32 +01:00
delete fileUpdateGroup [ itemDir ]
fileUpdateGroup [ altDir ] = itemDirNestedFiles . map ( ( f ) => f . split ( '/' ) . slice ( 1 ) . join ( '/' ) )
Logger . warn ( ` [Scanner] Some files were modified in a parent directory of a library item " ${ childLibraryItem . title } " - ignoring ` )
2022-01-10 18:12:47 +01:00
}
2022-03-13 00:45:32 +01:00
// Second pass: Check for new/updated/removed items
var itemGroupingResults = { }
for ( const itemDir in fileUpdateGroup ) {
var fullPath = Path . posix . join ( folder . fullPath . replace ( /\\/g , '/' ) , itemDir )
2021-11-26 01:39:02 +01:00
2022-03-13 00:45:32 +01:00
// Check if book dir group is already an item
var existingLibraryItem = this . db . libraryItems . find ( li => fullPath . startsWith ( li . path ) )
if ( existingLibraryItem ) {
// Is the item exactly - check if was deleted
if ( existingLibraryItem . path === fullPath ) {
2021-11-26 01:39:02 +01:00
var exists = await fs . pathExists ( fullPath )
if ( ! exists ) {
2022-03-13 00:45:32 +01:00
Logger . info ( ` [Scanner] Scanning file update group and library item was deleted " ${ existingLibraryItem . media . metadata . title } " - marking as missing ` )
existingLibraryItem . setMissing ( )
await this . db . updateLibraryItem ( existingLibraryItem )
this . emitter ( 'item_updated' , existingLibraryItem . toJSONExpanded ( ) )
2021-11-26 01:39:02 +01:00
2022-03-13 00:45:32 +01:00
itemGroupingResults [ itemDir ] = ScanResult . REMOVED
2021-11-26 01:39:02 +01:00
continue ;
}
}
2022-03-13 00:45:32 +01:00
// Scan library item for updates
Logger . debug ( ` [Scanner] Folder update for relative path " ${ itemDir } " is in library item " ${ existingLibraryItem . media . metadata . title } " - scan for updates ` )
itemGroupingResults [ itemDir ] = await this . scanLibraryItem ( library . mediaType , folder , existingLibraryItem )
2021-11-26 01:39:02 +01:00
continue ;
}
2022-03-13 00:45:32 +01:00
// Check if a library item is a subdirectory of this dir
var childItem = this . db . libraryItems . find ( li => li . path . startsWith ( fullPath ) )
if ( childItem ) {
Logger . warn ( ` [Scanner] Files were modified in a parent directory of a library item " ${ childItem . media . metadata . title } " - ignoring ` )
itemGroupingResults [ itemDir ] = ScanResult . NOTHING
2021-11-26 01:39:02 +01:00
continue ;
}
2022-03-13 00:45:32 +01:00
Logger . debug ( ` [Scanner] Folder update group must be a new item " ${ itemDir } " in library " ${ library . name } " ` )
var newLibraryItem = await this . scanPotentialNewLibraryItem ( library . mediaType , folder , fullPath )
if ( newLibraryItem ) {
2022-03-18 20:08:57 +01:00
await this . createNewAuthorsAndSeries ( newLibraryItem )
2022-03-13 00:45:32 +01:00
await this . db . insertLibraryItem ( newLibraryItem )
this . emitter ( 'item_added' , newLibraryItem . toJSONExpanded ( ) )
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
itemGroupingResults [ itemDir ] = newLibraryItem ? ScanResult . ADDED : ScanResult . NOTHING
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
return itemGroupingResults
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
async scanPotentialNewLibraryItem ( libraryMediaType , folder , fullPath ) {
var libraryItemData = await getLibraryItemFileData ( libraryMediaType , folder , fullPath , this . db . serverSettings )
if ( ! libraryItemData ) return null
2021-11-26 01:39:02 +01:00
var serverSettings = this . db . serverSettings
2022-03-13 00:45:32 +01:00
return this . scanNewLibraryItem ( libraryItemData , libraryMediaType , serverSettings . scannerPreferAudioMetadata , serverSettings . scannerPreferOpfMetadata , serverSettings . scannerFindCovers )
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
async searchForCover ( libraryItem , libraryScan = null ) {
2021-11-26 01:39:02 +01:00
var options = {
titleDistance : 2 ,
authorDistance : 2
}
2022-01-09 00:03:33 +01:00
var scannerCoverProvider = this . db . serverSettings . scannerCoverProvider
2022-03-13 00:45:32 +01:00
var results = await this . bookFinder . findCovers ( scannerCoverProvider , libraryItem . media . metadata . title , libraryItem . media . metadata . authorName , options )
2021-11-26 01:39:02 +01:00
if ( results . length ) {
2022-03-13 00:45:32 +01:00
if ( libraryScan ) libraryScan . addLog ( LogLevel . DEBUG , ` Found best cover for " ${ libraryItem . media . metadata . title } " ` )
else Logger . debug ( ` [Scanner] Found best cover for " ${ libraryItem . media . metadata . title } " ` )
2021-11-26 01:39:02 +01:00
// 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
2022-03-20 22:41:06 +01:00
var result = await this . coverManager . downloadCoverFromUrl ( libraryItem , results [ i ] )
2021-11-26 01:39:02 +01:00
if ( result . error ) {
Logger . error ( ` [Scanner] Failed to download cover from url " ${ results [ i ] } " | Attempt ${ i + 1 } ` , result . error )
} else {
return true
}
}
}
return false
}
2021-12-25 01:06:17 +01:00
2022-03-26 17:59:34 +01:00
// TODO: Redo metadata
2021-12-25 01:06:17 +01:00
async saveMetadata ( audiobookId ) {
2022-03-26 17:59:34 +01:00
// 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-12-25 01:06:17 +01:00
}
2022-03-14 01:34:31 +01:00
async quickMatchBook ( libraryItem , options = { } ) {
2022-02-16 01:33:33 +01:00
var provider = options . provider || 'google'
2022-03-14 01:34:31 +01:00
var searchTitle = options . title || libraryItem . media . metadata . title
var searchAuthor = options . author || libraryItem . media . metadata . authorName
2022-02-16 01:33:33 +01:00
var results = await this . bookFinder . search ( provider , searchTitle , searchAuthor )
if ( ! results . length ) {
return {
warning : ` No ${ provider } match found `
}
}
var matchData = results [ 0 ]
// Update cover if not set OR overrideCover flag
var hasUpdated = false
2022-03-14 01:34:31 +01:00
if ( matchData . cover && ( ! libraryItem . media . coverPath || options . overrideCover ) ) {
Logger . debug ( ` [Scanner] Updating cover " ${ matchData . cover } " ` )
2022-03-20 22:41:06 +01:00
var coverResult = await this . coverManager . downloadCoverFromUrl ( libraryItem , matchData . cover )
2022-02-16 01:33:33 +01:00
if ( ! coverResult || coverResult . error || ! coverResult . cover ) {
2022-03-14 01:34:31 +01:00
Logger . warn ( ` [Scanner] Match cover " ${ matchData . cover } " failed to use: ${ coverResult ? coverResult . error : 'Unknown Error' } ` )
2022-02-16 01:33:33 +01:00
} else {
hasUpdated = true
}
}
2022-03-14 01:34:31 +01:00
// Update media metadata if not set OR overrideDetails flag
const detailKeysToUpdate = [ 'title' , 'subtitle' , 'narrator' , 'publisher' , 'publishedYear' , 'asin' , 'isbn' ]
2022-02-16 01:33:33 +01:00
const updatePayload = { }
for ( const key in matchData ) {
2022-03-14 01:34:31 +01:00
if ( matchData [ key ] && detailKeysToUpdate . includes ( key ) ) {
if ( key === 'narrator' ) {
if ( ( ! libraryItem . media . metadata . narratorName || options . overrideDetails ) ) {
updatePayload . narrators = [ matchData [ key ] ]
}
} else if ( ( ! libraryItem . media . metadata [ key ] || options . overrideDetails ) ) {
updatePayload [ key ] = matchData [ key ]
}
}
}
// Add or set author if not set
if ( matchData . author && ! libraryItem . media . metadata . authorName ) {
var author = this . db . authors . find ( au => au . checkNameEquals ( matchData . author ) )
if ( ! author ) {
author = new Author ( )
author . setData ( { name : matchData . author } )
await this . db . insertEntity ( 'author' , author )
this . emitter ( 'author_added' , author )
2022-02-16 01:33:33 +01:00
}
2022-03-14 01:34:31 +01:00
updatePayload . authors = [ author . toJSONMinimal ( ) ]
}
// Add or set series if not set
if ( matchData . series && ! libraryItem . media . metadata . seriesName ) {
var seriesItem = this . db . series . find ( au => au . checkNameEquals ( matchData . series ) )
if ( ! seriesItem ) {
seriesItem = new Series ( )
seriesItem . setData ( { name : matchData . series } )
await this . db . insertEntity ( 'series' , seriesItem )
this . emitter ( 'series_added' , seriesItem )
}
updatePayload . series = [ seriesItem . toJSONMinimal ( matchData . volumeNumber ) ]
2022-02-16 01:33:33 +01:00
}
if ( Object . keys ( updatePayload ) . length ) {
2022-03-14 01:34:31 +01:00
Logger . debug ( '[Scanner] Updating details' , updatePayload )
if ( libraryItem . media . update ( { metadata : updatePayload } ) ) {
2022-02-16 01:33:33 +01:00
hasUpdated = true
}
}
if ( hasUpdated ) {
2022-03-14 01:34:31 +01:00
await this . db . updateLibraryItem ( libraryItem )
this . emitter ( 'item_updated' , libraryItem . toJSONExpanded ( ) )
2022-02-16 01:33:33 +01:00
}
return {
updated : hasUpdated ,
2022-03-14 01:34:31 +01:00
libraryItem : libraryItem . toJSONExpanded ( )
2022-02-16 01:33:33 +01:00
}
}
2022-03-26 17:59:34 +01:00
// TODO: Redo quick match full library
2022-02-16 01:33:33 +01:00
async matchLibraryBooks ( library ) {
2022-03-26 17:59:34 +01:00
// if (this.isLibraryScanning(library.id)) {
// Logger.error(`[Scanner] Already scanning ${library.id}`)
// return
// }
// const provider = library.provider || 'google'
// var audiobooksInLibrary = this.db.audiobooks.filter(ab => ab.libraryId === library.id)
// if (!audiobooksInLibrary.length) {
// return
// }
// var libraryScan = new LibraryScan()
// libraryScan.setData(library, null, 'match')
// this.librariesScanning.push(libraryScan.getScanEmitData)
// this.emitter('scan_start', libraryScan.getScanEmitData)
// Logger.info(`[Scanner] Starting library match books scan ${libraryScan.id} for ${libraryScan.libraryName}`)
// for (let i = 0; i < audiobooksInLibrary.length; i++) {
// var audiobook = audiobooksInLibrary[i]
// Logger.debug(`[Scanner] Quick matching "${audiobook.title}" (${i + 1} of ${audiobooksInLibrary.length})`)
// var result = await this.quickMatchBook(audiobook, { provider })
// if (result.warning) {
// Logger.warn(`[Scanner] Match warning ${result.warning} for audiobook "${audiobook.title}"`)
// } else if (result.updated) {
// libraryScan.resultsUpdated++
// }
// if (this.cancelLibraryScan[libraryScan.libraryId]) {
// Logger.info(`[Scanner] Library match scan canceled for "${libraryScan.libraryName}"`)
// delete this.cancelLibraryScan[libraryScan.libraryId]
// var scanData = libraryScan.getScanEmitData
// scanData.results = false
// this.emitter('scan_complete', scanData)
// this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
// return
// }
// }
// this.librariesScanning = this.librariesScanning.filter(ls => ls.id !== library.id)
// this.emitter('scan_complete', libraryScan.getScanEmitData)
2022-02-16 01:33:33 +01:00
}
2021-11-23 02:58:20 +01:00
}
module . exports = Scanner