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' )
2022-03-13 00:45:32 +01:00
const { groupFilesIntoLibraryItemPaths , getLibraryItemFileData , scanFolder } = require ( '../utils/scandir' )
2022-02-27 19:47:56 +01:00
const { comparePaths , getId } = require ( '../utils/index' )
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-11 01:45:02 +01:00
const Audiobook = require ( '../objects/legacy/Audiobook' )
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-02-27 20:47:52 +01:00
constructor ( db , coverController , emitter ) {
this . BookMetadataPath = Path . posix . join ( global . MetadataPath , 'books' )
this . ScanLogPath = Path . posix . join ( global . MetadataPath , 'logs' , 'scans' )
2021-11-23 02:58:20 +01:00
this . db = db
this . coverController = coverController
this . emitter = emitter
this . cancelLibraryScan = { }
this . librariesScanning = [ ]
this . bookFinder = new BookFinder ( )
}
2021-11-25 03:15:50 +01:00
getCoverDirectory ( audiobook ) {
2022-02-27 19:47:56 +01:00
if ( this . db . serverSettings . storeCoverWithBook ) {
2021-11-25 03:15:50 +01:00
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-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 ) {
var coverPath = await this . coverController . saveEmbeddedCoverArt ( libraryItem )
if ( coverPath ) {
Logger . debug ( ` [Scanner] Saved embedded cover art " ${ coverPath } " ` )
2021-11-26 01:39:02 +01:00
hasUpdated = true
}
}
}
2022-03-13 00:45:32 +01:00
console . log ( 'Finished library item scan' , libraryItem . hasMediaFiles , hasUpdated )
if ( ! libraryItem . hasMediaFiles ) { // Library Item is invalid
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
}
2021-11-23 02:58:20 +01:00
async scan ( libraryId , options = { } ) {
2021-11-26 01:39:02 +01:00
if ( this . isLibraryScanning ( libraryId ) ) {
2021-11-23 02:58:20 +01:00
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-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
2021-11-23 02:58:20 +01:00
// Remove audiobooks 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
2021-11-26 01:39:02 +01:00
const NumScansPerChunk = 25
2022-03-13 00:45:32 +01:00
const itemsToUpdateChunks = [ ]
const itemDataToRescanChunks = [ ]
const newItemDataToScanChunks = [ ]
var itemsToUpdate = [ ]
var itemDataToRescan = [ ]
var newItemDataToScan = [ ]
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 )
if ( itemsToUpdate . length === NumScansPerChunk ) {
itemsToUpdateChunks . push ( itemsToUpdate )
itemsToUpdate = [ ]
2021-11-26 01:39:02 +01:00
}
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
itemDataToRescan . push ( checkRes )
if ( itemDataToRescan . length === NumScansPerChunk ) {
itemDataToRescanChunks . push ( itemDataToRescan )
itemDataToRescan = [ ]
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
} else if ( libraryScan . findCovers && libraryItem . media . shouldSearchForCover ) {
2021-11-26 01:39:02 +01:00
libraryScan . resultsUpdated ++
2022-03-13 00:45:32 +01:00
itemsToFindCovers . push ( libraryItem )
itemsToUpdate . push ( libraryItem )
if ( itemsToUpdate . length === NumScansPerChunk ) {
itemsToUpdateChunks . push ( itemsToUpdate )
itemsToUpdate = [ ]
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 )
if ( itemsToUpdate . length === NumScansPerChunk ) {
itemsToUpdateChunks . push ( itemsToUpdate )
itemsToUpdate = [ ]
2021-11-26 01:39:02 +01:00
}
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 ( itemsToUpdate . length ) itemsToUpdateChunks . push ( itemsToUpdate )
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-13 00:45:32 +01:00
newItemDataToScan . push ( dataFound )
if ( newItemDataToScan . length === NumScansPerChunk ) {
newItemDataToScanChunks . push ( newItemDataToScan )
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-13 00:45:32 +01:00
for ( let i = 0 ; i < itemsToUpdateChunks . length ; i ++ ) {
await this . updateLibraryItemChunk ( itemsToUpdateChunks [ i ] )
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('Update chunk done', i, 'of', itemsToUpdateChunks.length)
2021-11-26 01:39:02 +01:00
}
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-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
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 ) {
var savedCoverPath = await this . coverController . saveEmbeddedCoverArt ( libraryItem )
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-13 00:45:32 +01:00
if ( ! libraryItem . media . hasMediaFiles ) { // Library item is invalid
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-13 00:45:32 +01:00
if ( ! libraryItem . media . hasMediaFiles ) {
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 ) {
var coverPath = await this . coverController . saveEmbeddedCoverArt ( libraryItem )
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 )
}
// 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 = new Author ( )
_author . setData ( tempMinAuthor )
newAuthors . push ( _author )
}
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 ( ) ) )
}
}
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 = new Series ( )
_series . setData ( tempMinSeries )
newSeries . push ( _series )
}
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 ( ) ) )
}
}
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
return libraryItem
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-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 ) {
2021-11-26 01:39:02 +01:00
2022-03-13 00:45:32 +01:00
// 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 ) {
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-13 00:45:32 +01:00
var result = await this . coverController . 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
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
}
}
// TEMP: Old version created ids that had a chance of repeating
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 )
2022-02-27 23:14:57 +01:00
await this . db . insertAudiobook ( abCopy )
2021-12-25 01:06:17 +01:00
audiobooksUpdated ++
} else {
ids [ ab . id ] = true
}
}
if ( audiobooksUpdated ) {
Logger . info ( ` [Scanner] Updated ${ audiobooksUpdated } audiobook IDs ` )
}
}
2022-02-16 01:33:33 +01:00
async quickMatchBook ( audiobook , options = { } ) {
var provider = options . provider || 'google'
var searchTitle = options . title || audiobook . book . _title
var searchAuthor = options . author || audiobook . book . _author
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
if ( matchData . cover && ( ! audiobook . book . cover || options . overrideCover ) ) {
Logger . debug ( ` [BookController] Updating cover " ${ matchData . cover } " ` )
var coverResult = await this . coverController . downloadCoverFromUrl ( audiobook , matchData . cover )
if ( ! coverResult || coverResult . error || ! coverResult . cover ) {
Logger . warn ( ` [BookController] Match cover " ${ matchData . cover } " failed to use: ${ coverResult ? coverResult . error : 'Unknown Error' } ` )
} else {
hasUpdated = true
}
}
// Update book details if not set OR overrideDetails flag
const detailKeysToUpdate = [ 'title' , 'subtitle' , 'author' , 'narrator' , 'publisher' , 'publishYear' , 'series' , 'volumeNumber' , 'asin' , 'isbn' ]
const updatePayload = { }
for ( const key in matchData ) {
if ( matchData [ key ] && detailKeysToUpdate . includes ( key ) && ( ! audiobook . book [ key ] || options . overrideDetails ) ) {
updatePayload [ key ] = matchData [ key ]
}
}
if ( Object . keys ( updatePayload ) . length ) {
Logger . debug ( '[BookController] Updating details' , updatePayload )
if ( audiobook . update ( { book : updatePayload } ) ) {
hasUpdated = true
}
}
if ( hasUpdated ) {
2022-02-27 23:14:57 +01:00
await this . db . updateAudiobook ( audiobook )
2022-02-16 01:33:33 +01:00
this . emitter ( 'audiobook_updated' , audiobook . toJSONExpanded ( ) )
}
return {
updated : hasUpdated ,
audiobook : audiobook . toJSONExpanded ( )
}
}
async matchLibraryBooks ( library ) {
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 )
}
2021-11-23 02:58:20 +01:00
}
module . exports = Scanner