2022-07-06 02:53:01 +02:00
const fs = require ( '../libs/fsExtra' )
2021-11-23 02:58:20 +01:00
const Path = require ( 'path' )
2022-11-24 22:53:58 +01:00
const Logger = require ( '../Logger' )
const SocketAuthority = require ( '../SocketAuthority' )
2021-11-23 02:58:20 +01:00
// Utils
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' )
2023-01-06 00:45:27 +01:00
const { getIno , filePathToPOSIX } = require ( '../utils/fileUtils' )
2022-02-27 19:47:56 +01:00
const { ScanResult , LogLevel } = require ( '../utils/constants' )
2022-09-16 01:35:56 +02:00
const { findMatchingEpisodesInFeed , getPodcastFeed } = require ( '../utils/podcastUtils' )
2021-11-23 02:58:20 +01:00
2022-05-31 02:26:53 +02:00
const MediaFileScanner = require ( './MediaFileScanner' )
2022-03-06 23:32:04 +01:00
const BookFinder = require ( '../finders/BookFinder' )
2022-09-03 00:50:09 +02:00
const PodcastFinder = require ( '../finders/PodcastFinder' )
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' )
2023-05-27 21:51:03 +02:00
const Task = require ( '../objects/Task' )
2022-03-13 00:45:32 +01:00
2021-11-23 02:58:20 +01:00
class Scanner {
2023-05-27 21:51:03 +02:00
constructor ( db , coverManager , taskManager ) {
2021-11-23 02:58:20 +01:00
this . db = db
2022-03-20 22:41:06 +01:00
this . coverManager = coverManager
2023-05-27 21:51:03 +02:00
this . taskManager = taskManager
2021-11-23 02:58:20 +01:00
this . cancelLibraryScan = { }
this . librariesScanning = [ ]
2022-09-01 00:39:02 +02:00
// Watcher file update scan vars
this . pendingFileUpdatesToScan = [ ]
this . scanningFilesChanged = false
2021-11-23 02:58:20 +01:00
this . bookFinder = new BookFinder ( )
2022-09-03 00:50:09 +02:00
this . podcastFinder = new PodcastFinder ( )
2021-11-23 02:58:20 +01:00
}
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
}
2023-05-27 21:51:03 +02:00
getScanResultDescription ( result ) {
switch ( result ) {
case ScanResult . ADDED :
return 'Added to library'
case ScanResult . NOTHING :
return 'No updates necessary'
case ScanResult . REMOVED :
return 'Removed from library'
case ScanResult . UPDATED :
return 'Item was updated'
case ScanResult . UPTODATE :
return 'No updates necessary'
default :
return ''
2021-11-26 01:39:02 +01:00
}
2023-05-27 21:51:03 +02:00
}
async scanLibraryItemByRequest ( libraryItem ) {
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 } " ` )
2023-05-27 21:51:03 +02:00
const task = new Task ( )
task . setData ( 'scan-item' , ` Scan ${ libraryItem . media . metadata . title } ` , '' , true , {
libraryItemId : libraryItem . id ,
libraryId : library . id ,
mediaType : library . mediaType
} )
this . taskManager . addTask ( task )
const result = await this . scanLibraryItem ( library . mediaType , folder , libraryItem )
task . setFinished ( this . getScanResultDescription ( result ) )
this . taskManager . taskFinished ( task )
return result
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
async scanLibraryItem ( libraryMediaType , folder , libraryItem ) {
2022-05-24 01:15:15 +02:00
// TODO: Support for single media item
2023-04-12 23:45:52 +02:00
const libraryItemData = await getLibraryItemFileData ( libraryMediaType , folder , libraryItem . path , false )
2022-03-13 00:45:32 +01:00
if ( ! libraryItemData ) {
2021-11-26 01:39:02 +01:00
return ScanResult . NOTHING
}
2022-12-22 23:38:55 +01:00
let hasUpdated = false
2021-11-26 01:39:02 +01:00
2022-12-22 23:38:55 +01:00
const 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 ) {
2022-12-22 23:38:55 +01:00
const libraryAudioFiles = libraryItem . libraryFiles . filter ( lf => lf . fileType === 'audio' )
2023-01-02 19:05:07 +01:00
if ( await MediaFileScanner . scanMediaFiles ( libraryAudioFiles , libraryItem ) ) {
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-12-22 23:38:55 +01:00
const 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-05-29 14:25:30 +02:00
// Library Item is invalid - (a book has no audio files or ebook files)
if ( ! libraryItem . hasMediaEntities && libraryItem . mediaType !== 'podcast' ) {
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
await this . db . updateLibraryItem ( libraryItem )
2023-05-17 01:58:01 +02:00
SocketAuthority . emitter ( 'item_updated' , libraryItem . toJSONExpanded ( ) )
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
2022-11-24 22:53:58 +01:00
SocketAuthority . 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
2022-11-24 22:53:58 +01:00
SocketAuthority . emitter ( 'scan_complete' , emitData )
2021-11-26 01:39:02 +01:00
return
}
2022-11-24 22:53:58 +01:00
SocketAuthority . emitter ( 'scan_complete' , libraryScan . getScanEmitData )
2021-11-26 01:39:02 +01:00
if ( libraryScan . totalResults ) {
2022-12-16 00:30:45 +01:00
libraryScan . saveLog ( )
2021-11-26 01:39:02 +01:00
}
2021-11-23 02:58:20 +01:00
}
async scanLibrary ( libraryScan ) {
2023-01-02 19:05:07 +01:00
let 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 ++ ) {
2022-12-22 23:38:55 +01:00
const folder = libraryScan . folders [ i ]
2023-04-12 23:45:52 +02:00
const itemDataFoundInFolder = await scanFolder ( libraryScan . libraryMediaType , folder )
2022-03-13 00:45:32 +01:00
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 )
2023-01-02 19:05:07 +01:00
const 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 = [ ]
2023-01-02 19:05:07 +01:00
let itemsToUpdate = [ ]
let itemDataToRescan = [ ]
let itemDataToRescanSize = 0
let newItemDataToScan = [ ]
let newItemDataToScanSize = 0
const itemsToFindCovers = [ ]
2022-03-13 00:45:32 +01:00
// Check for existing & removed library items
for ( let i = 0 ; i < libraryItemsInLibrary . length ; i ++ ) {
2022-12-22 23:38:55 +01:00
const libraryItem = libraryItemsInLibrary [ i ]
2022-03-13 00:45:32 +01:00
// Find library item folder with matching inode or matching path
2022-12-22 23:38:55 +01:00
const dataFound = libraryItemDataFound . find ( lid => lid . ino === libraryItem . ino || comparePaths ( lid . relPath , libraryItem . relPath ) )
2021-11-23 02:58:20 +01:00
if ( ! dataFound ) {
2023-04-13 00:20:11 +02:00
// Podcast folder can have no episodes and still be valid
if ( libraryScan . libraryMediaType === 'podcast' && await fs . pathExists ( libraryItem . path ) ) {
Logger . info ( ` [Scanner] Library item " ${ libraryItem . media . metadata . title } " folder exists but has no episodes ` )
if ( libraryItem . isMissing ) {
libraryScan . resultsUpdated ++
libraryItem . isMissing = false
libraryItem . setLastScan ( )
itemsToUpdate . push ( libraryItem )
}
} else {
libraryScan . addLog ( LogLevel . WARN , ` Library Item " ${ libraryItem . media . metadata . title } " is missing ` )
Logger . warn ( ` [Scanner] Library item " ${ libraryItem . media . metadata . title } " is missing (inode " ${ libraryItem . ino } ") ` )
libraryScan . resultsMissing ++
libraryItem . setMissing ( )
itemsToUpdate . push ( libraryItem )
}
2021-11-23 02:58:20 +01:00
} else {
2022-12-22 23:38:55 +01:00
const checkRes = libraryItem . checkScanData ( dataFound )
2022-03-13 00:45:32 +01:00
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
2023-01-02 19:07:26 +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 = [ ]
}
itemDataToRescan . push ( checkRes )
itemDataToRescanSize += libraryItem . audioFileTotalSize
if ( itemDataToRescanSize >= MaxSizePerChunk ) {
itemDataToRescanChunks . push ( itemDataToRescan )
itemDataToRescanSize = 0
itemDataToRescan = [ ]
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 ++ ) {
2022-12-22 23:38:55 +01:00
const dataFound = libraryItemDataFound [ i ]
2022-07-19 01:17:50 +02:00
2022-12-22 23:38:55 +01:00
const hasMediaFile = dataFound . libraryFiles . some ( lf => lf . isMediaFile )
2022-03-13 00:45:32 +01:00
if ( ! hasMediaFile ) {
2022-04-28 02:42:34 +02:00
libraryScan . addLog ( LogLevel . WARN , ` Item found " ${ libraryItemDataFound . path } " has no media files ` )
2021-11-23 02:58:20 +01:00
} else {
2023-01-02 19:07:26 +01:00
// If this item will go over max size then push current chunk
let mediaFileSize = 0
dataFound . libraryFiles . filter ( lf => lf . fileType === 'audio' || lf . fileType === 'video' ) . forEach ( lf => mediaFileSize += lf . metadata . size )
if ( mediaFileSize + newItemDataToScanSize > MaxSizePerChunk && newItemDataToScan . length > 0 ) {
newItemDataToScanChunks . push ( newItemDataToScan )
newItemDataToScanSize = 0
newItemDataToScan = [ ]
}
2022-07-17 01:54:34 +02:00
2023-01-02 19:07:26 +01:00
newItemDataToScan . push ( dataFound )
newItemDataToScanSize += mediaFileSize
2022-03-18 15:16:10 +01:00
2023-01-02 19:07:26 +01:00
if ( newItemDataToScanSize >= MaxSizePerChunk ) {
newItemDataToScanChunks . push ( newItemDataToScan )
newItemDataToScanSize = 0
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 ++ ) {
2022-12-22 23:38:55 +01:00
const libraryItem = itemsToFindCovers [ i ]
const updatedCover = await this . searchForCover ( libraryItem , libraryScan )
2022-03-13 00:45:32 +01:00
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-07-17 01:54:34 +02:00
// Chunking will be removed when legacy single threaded scanner is removed
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
for ( let i = 0 ; i < newItemDataToScanChunks . length ; i ++ ) {
await this . scanNewLibraryItemDataChunk ( newItemDataToScanChunks [ i ] , libraryScan )
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 )
2022-11-24 22:53:58 +01:00
SocketAuthority . 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
2022-05-09 14:23:29 +02:00
itemsUpdated = itemsUpdated . filter ( li => li ) // Filter out nulls
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
if ( itemsUpdated . length ) {
libraryScan . resultsUpdated += itemsUpdated . length
await this . db . updateLibraryItems ( itemsUpdated )
2022-11-24 22:53:58 +01:00
SocketAuthority . 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 ) {
2023-01-02 19:05:07 +01:00
let newLibraryItems = await Promise . all ( newLibraryItemsData . map ( ( lid ) => {
return this . scanNewLibraryItem ( lid , libraryScan . libraryMediaType , 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 )
2022-11-24 22:53:58 +01:00
SocketAuthority . 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 } " ` )
2023-01-02 19:05:07 +01:00
let 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
2023-01-02 19:05:07 +01:00
const existingAudioFiles = existingLibraryFiles . filter ( lf => lf . fileType === 'audio' )
2022-03-13 00:45:32 +01:00
if ( libraryScan . scanOptions . forceRescan && existingAudioFiles . length ) {
2023-01-02 19:05:07 +01:00
if ( await MediaFileScanner . scanMediaFiles ( existingAudioFiles , libraryItem , libraryScan ) ) {
2021-12-05 18:29:42 +01:00
hasUpdated = true
}
}
// Scan new audio files
2023-01-02 19:05:07 +01:00
const newAudioFiles = newLibraryFiles . filter ( lf => lf . fileType === 'audio' )
const removedAudioFiles = filesRemoved . filter ( lf => lf . fileType === 'audio' )
2022-03-13 00:45:32 +01:00
if ( newAudioFiles . length || removedAudioFiles . length ) {
2023-01-02 19:05:07 +01:00
if ( await MediaFileScanner . scanMediaFiles ( newAudioFiles , libraryItem , 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 ) {
2023-01-02 19:05:07 +01:00
const 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-05-29 14:25:30 +02:00
// Library Item is invalid - (a book has no audio files or ebook files)
if ( ! libraryItem . hasMediaEntities && libraryItem . mediaType !== 'podcast' ) {
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 ) {
2023-01-02 19:05:07 +01:00
const updatedCover = await this . searchForCover ( libraryItem , libraryScan )
2022-03-13 00:45:32 +01:00
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
2023-01-02 19:05:07 +01:00
async scanNewLibraryItem ( libraryItemData , libraryMediaType , libraryScan = null ) {
2022-03-13 00:45:32 +01:00
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
2023-01-02 19:05:07 +01:00
const preferOpfMetadata = libraryScan ? ! ! libraryScan . preferOpfMetadata : ! ! global . ServerSettings . scannerPreferOpfMetadata
const findCovers = libraryScan ? ! ! libraryScan . findCovers : ! ! global . ServerSettings . scannerFindCovers
2022-12-22 23:38:55 +01:00
const libraryItem = new LibraryItem ( )
2022-03-13 00:45:32 +01:00
libraryItem . setData ( libraryMediaType , libraryItemData )
2021-11-25 03:15:50 +01:00
2022-12-22 23:38:55 +01:00
const mediaFiles = libraryItemData . libraryFiles . filter ( lf => lf . fileType === 'audio' || lf . fileType === 'video' )
2022-05-31 02:26:53 +02:00
if ( mediaFiles . length ) {
2023-01-02 19:05:07 +01:00
await MediaFileScanner . scanMediaFiles ( mediaFiles , libraryItem , libraryScan )
2021-11-26 01:39:02 +01:00
}
2022-04-20 04:10:24 +02:00
await libraryItem . syncFiles ( preferOpfMetadata )
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
}
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-12-22 23:38:55 +01:00
const 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-05-31 02:26:53 +02:00
if ( libraryMediaType === 'book' ) {
2022-03-13 00:45:32 +01:00
if ( libraryItem && findCovers && ! libraryItem . media . coverPath && libraryItem . media . shouldSearchForCover ) {
2022-12-22 23:38:55 +01:00
const updatedCover = await this . searchForCover ( libraryItem , libraryScan )
2022-03-13 00:45:32 +01:00
libraryItem . media . updateLastCoverSearch ( updatedCover )
}
2022-03-13 19:47:36 +01:00
}
return libraryItem
}
2022-04-12 23:05:16 +02:00
// Any series or author object on library item with an id starting with "new"
// will create a new author/series OR find a matching author/series
2022-03-13 19:47:36 +01:00
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
2022-04-12 23:05:16 +02:00
if ( ! _author ) { // Must create new author
2022-03-13 19:47:36 +01:00
_author = new Author ( )
_author . setData ( tempMinAuthor )
newAuthors . push ( _author )
2022-03-13 00:45:32 +01:00
}
2022-04-12 23:05:16 +02: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 )
2022-11-24 22:53:58 +01:00
SocketAuthority . 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
2022-04-12 23:05:16 +02:00
if ( ! _series ) { // Must create new series
2022-03-13 19:47:36 +01:00
_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 )
2022-12-22 23:26:11 +01:00
SocketAuthority . emitter ( 'multiple_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 ) {
2022-09-01 00:39:02 +02:00
if ( ! fileUpdates || ! fileUpdates . length ) return
// If already scanning files from watcher then add these updates to queue
if ( this . scanningFilesChanged ) {
this . pendingFileUpdatesToScan . push ( fileUpdates )
Logger . debug ( ` [Scanner] Already scanning files from watcher - file updates pushed to queue (size ${ this . pendingFileUpdatesToScan . length } ) ` )
return
}
this . scanningFilesChanged = true
2021-11-26 01:39:02 +01:00
// 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-05-24 01:15:15 +02:00
var fileUpdateGroup = groupFilesIntoLibraryItemPaths ( library . mediaType , relFilePaths )
2022-07-19 15:33:32 +02:00
2022-05-24 01:15:15 +02:00
if ( ! Object . keys ( fileUpdateGroup ) . length ) {
Logger . info ( ` [Scanner] No important changes to scan for in folder " ${ folderId } " ` )
continue ;
}
2022-03-13 00:45:32 +01:00
var folderScanResults = await this . scanFolderUpdates ( library , folder , fileUpdateGroup )
2021-11-26 01:39:02 +01:00
Logger . debug ( ` [Scanner] Folder scan results ` , folderScanResults )
}
2022-09-01 00:39:02 +02:00
this . scanningFilesChanged = false
if ( this . pendingFileUpdatesToScan . length ) {
Logger . debug ( ` [Scanner] File updates finished scanning with more updates in queue ( ${ this . pendingFileUpdatesToScan . length } ) ` )
this . scanFilesChanged ( this . pendingFileUpdatesToScan . shift ( ) )
}
2021-11-26 01:39:02 +01:00
}
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 ) {
2022-05-24 01:15:15 +02:00
if ( itemDir == fileUpdateGroup [ itemDir ] ) continue ; // Media in root path
2022-03-13 00:45:32 +01:00
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
2023-01-06 00:45:27 +01:00
var fullPath = Path . posix . join ( filePathToPOSIX ( folder . fullPath ) , itemDir )
2022-04-23 07:29:38 +02:00
var childLibraryItem = this . db . libraryItems . find ( li => li . path !== fullPath && li . path . startsWith ( fullPath ) )
2022-03-13 00:45:32 +01:00
if ( ! childLibraryItem ) {
2022-01-10 18:12:47 +01:00
continue ;
}
2023-01-06 00:45:27 +01:00
var altFullPath = Path . posix . join ( filePathToPOSIX ( folder . fullPath ) , 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
2023-01-06 00:45:27 +01:00
const itemGroupingResults = { }
2022-03-13 00:45:32 +01:00
for ( const itemDir in fileUpdateGroup ) {
2023-01-06 00:45:27 +01:00
const fullPath = Path . posix . join ( filePathToPOSIX ( folder . fullPath ) , itemDir )
2022-05-12 01:18:54 +02:00
const dirIno = await getIno ( fullPath )
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
2023-01-06 00:45:27 +01:00
let existingLibraryItem = this . db . libraryItems . find ( li => fullPath . startsWith ( li . path ) )
2022-05-12 01:18:54 +02:00
if ( ! existingLibraryItem ) {
existingLibraryItem = this . db . libraryItems . find ( li => li . ino === dirIno )
if ( existingLibraryItem ) {
2023-02-11 22:39:34 +01:00
Logger . debug ( ` [Scanner] scanFolderUpdates: Library item found by inode value= ${ dirIno } . " ${ existingLibraryItem . relPath } => ${ itemDir } " ` )
2022-05-12 01:18:54 +02:00
// Update library item paths for scan and all library item paths will get updated in LibraryItem.checkScanData
existingLibraryItem . path = fullPath
existingLibraryItem . relPath = itemDir
}
}
2022-03-13 00:45:32 +01:00
if ( existingLibraryItem ) {
// Is the item exactly - check if was deleted
if ( existingLibraryItem . path === fullPath ) {
2023-01-06 00:45:27 +01:00
const exists = await fs . pathExists ( fullPath )
2021-11-26 01:39:02 +01:00
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 )
2022-11-24 22:53:58 +01:00
SocketAuthority . 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
2022-12-18 22:23:50 +01:00
var childItem = this . db . libraryItems . find ( li => ( li . path + '/' ) . startsWith ( fullPath + '/' ) )
2022-03-13 00:45:32 +01:00
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 } " ` )
2022-05-24 01:15:15 +02:00
var isSingleMediaItem = itemDir === fileUpdateGroup [ itemDir ]
var newLibraryItem = await this . scanPotentialNewLibraryItem ( library . mediaType , folder , fullPath , isSingleMediaItem )
2022-03-13 00:45:32 +01:00
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 )
2022-11-24 22:53:58 +01:00
SocketAuthority . 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-05-24 01:15:15 +02:00
async scanPotentialNewLibraryItem ( libraryMediaType , folder , fullPath , isSingleMediaItem = false ) {
2023-04-12 23:45:52 +02:00
const libraryItemData = await getLibraryItemFileData ( libraryMediaType , folder , fullPath , isSingleMediaItem )
2022-03-13 00:45:32 +01:00
if ( ! libraryItemData ) return null
2023-01-02 19:05:07 +01:00
return this . scanNewLibraryItem ( libraryItemData , libraryMediaType )
2021-11-26 01:39:02 +01:00
}
2022-03-13 00:45:32 +01:00
async searchForCover ( libraryItem , libraryScan = null ) {
2022-12-22 23:38:55 +01:00
const options = {
2021-11-26 01:39:02 +01:00
titleDistance : 2 ,
authorDistance : 2
}
2022-12-22 23:38:55 +01:00
const scannerCoverProvider = this . db . serverSettings . scannerCoverProvider
const 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-12-22 23:38:55 +01:00
const 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-04-21 01:05:09 +02:00
async quickMatchLibraryItem ( 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-09-23 20:39:20 +02:00
var overrideDefaults = options . overrideDefaults || false
2022-02-16 01:33:33 +01:00
2022-09-23 19:53:30 +02:00
// Set to override existing metadata if scannerPreferMatchedMetadata setting is true and
2022-09-23 20:39:20 +02:00
// the overrideDefaults option is not set or set to false.
2022-09-23 19:53:30 +02:00
if ( ( overrideDefaults == false ) && ( this . db . serverSettings . scannerPreferMatchedMetadata ) ) {
2022-05-23 04:56:51 +02:00
options . overrideCover = true
options . overrideDetails = true
}
2022-09-03 00:50:09 +02:00
var updatePayload = { }
2022-02-16 01:33:33 +01:00
var hasUpdated = false
2022-09-03 00:50:09 +02:00
2022-09-16 01:35:56 +02:00
if ( libraryItem . isBook ) {
2022-09-03 00:50:09 +02:00
var searchISBN = options . isbn || libraryItem . media . metadata . isbn
var searchASIN = options . asin || libraryItem . media . metadata . asin
var results = await this . bookFinder . search ( provider , searchTitle , searchAuthor , searchISBN , searchASIN )
if ( ! results . length ) {
return {
warning : ` No ${ provider } match found `
}
}
var matchData = results [ 0 ]
// Update cover if not set OR overrideCover flag
if ( matchData . cover && ( ! libraryItem . media . coverPath || options . overrideCover ) ) {
Logger . debug ( ` [Scanner] Updating cover " ${ matchData . cover } " ` )
var coverResult = await this . coverManager . downloadCoverFromUrl ( libraryItem , matchData . cover )
if ( ! coverResult || coverResult . error || ! coverResult . cover ) {
Logger . warn ( ` [Scanner] Match cover " ${ matchData . cover } " failed to use: ${ coverResult ? coverResult . error : 'Unknown Error' } ` )
} else {
hasUpdated = true
}
}
updatePayload = await this . quickMatchBookBuildUpdatePayload ( libraryItem , matchData , options )
2022-09-16 01:35:56 +02:00
} else if ( libraryItem . isPodcast ) { // Podcast quick match
2022-09-03 00:50:09 +02:00
var results = await this . podcastFinder . search ( searchTitle )
if ( ! results . length ) {
return {
warning : ` No ${ provider } match found `
}
}
var matchData = results [ 0 ]
// Update cover if not set OR overrideCover flag
if ( matchData . cover && ( ! libraryItem . media . coverPath || options . overrideCover ) ) {
Logger . debug ( ` [Scanner] Updating cover " ${ matchData . cover } " ` )
var coverResult = await this . coverManager . downloadCoverFromUrl ( libraryItem , matchData . cover )
if ( ! coverResult || coverResult . error || ! coverResult . cover ) {
Logger . warn ( ` [Scanner] Match cover " ${ matchData . cover } " failed to use: ${ coverResult ? coverResult . error : 'Unknown Error' } ` )
} else {
hasUpdated = true
}
}
updatePayload = this . quickMatchPodcastBuildUpdatePayload ( libraryItem , matchData , options )
}
if ( Object . keys ( updatePayload ) . length ) {
Logger . debug ( '[Scanner] Updating details' , updatePayload )
if ( libraryItem . media . update ( updatePayload ) ) {
2022-02-16 01:33:33 +01:00
hasUpdated = true
}
}
2022-09-03 00:50:09 +02:00
if ( hasUpdated ) {
2022-09-16 01:35:56 +02:00
if ( libraryItem . isPodcast && libraryItem . media . metadata . feedUrl ) { // Quick match all unmatched podcast episodes
await this . quickMatchPodcastEpisodes ( libraryItem , options )
}
2022-09-03 00:50:09 +02:00
await this . db . updateLibraryItem ( libraryItem )
2022-11-24 22:53:58 +01:00
SocketAuthority . emitter ( 'item_updated' , libraryItem . toJSONExpanded ( ) )
2022-09-03 00:50:09 +02:00
}
return {
updated : hasUpdated ,
libraryItem : libraryItem . toJSONExpanded ( )
}
}
quickMatchPodcastBuildUpdatePayload ( libraryItem , matchData , options ) {
const updatePayload = { }
updatePayload . metadata = { }
const matchDataTransformed = {
title : matchData . title || null ,
author : matchData . artistName || null ,
genres : matchData . genres || [ ] ,
itunesId : matchData . id || null ,
itunesPageUrl : matchData . pageUrl || null ,
itunesArtistId : matchData . artistId || null ,
releaseDate : matchData . releaseDate || null ,
imageUrl : matchData . cover || null ,
2022-09-16 01:35:56 +02:00
feedUrl : matchData . feedUrl || null ,
2022-09-03 00:50:09 +02:00
description : matchData . descriptionPlain || null
}
for ( const key in matchDataTransformed ) {
if ( matchDataTransformed [ key ] ) {
if ( key === 'genres' ) {
2022-11-09 23:50:26 +01:00
if ( ( ! libraryItem . media . metadata . genres . length || options . overrideDetails ) ) {
var genresArray = [ ]
if ( Array . isArray ( matchDataTransformed [ key ] ) ) genresArray = [ ... matchDataTransformed [ key ] ]
else { // Genres should always be passed in as an array but just incase handle a string
Logger . warn ( ` [Scanner] quickMatch genres is not an array ${ matchDataTransformed [ key ] } ` )
genresArray = matchDataTransformed [ key ] . split ( ',' ) . map ( v => v . trim ( ) ) . filter ( v => ! ! v )
}
updatePayload . metadata [ key ] = genresArray
2022-09-03 00:50:09 +02:00
}
2022-09-16 01:35:56 +02:00
} else if ( libraryItem . media . metadata [ key ] !== matchDataTransformed [ key ] && ( ! libraryItem . media . metadata [ key ] || options . overrideDetails ) ) {
2022-09-03 00:50:09 +02:00
updatePayload . metadata [ key ] = matchDataTransformed [ key ]
}
}
}
if ( ! Object . keys ( updatePayload . metadata ) . length ) {
delete updatePayload . metadata
}
return updatePayload
}
async quickMatchBookBuildUpdatePayload ( libraryItem , matchData , options ) {
2022-03-14 01:34:31 +01:00
// Update media metadata if not set OR overrideDetails flag
2023-03-23 00:05:43 +01:00
const detailKeysToUpdate = [ 'title' , 'subtitle' , 'description' , 'narrator' , 'publisher' , 'publishedYear' , 'genres' , 'tags' , 'language' , 'explicit' , 'abridged' , 'asin' , 'isbn' ]
2022-02-16 01:33:33 +01:00
const updatePayload = { }
2022-05-23 04:56:51 +02:00
updatePayload . metadata = { }
2022-10-01 23:51:22 +02:00
2022-02-16 01:33:33 +01:00
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 ) ) {
2022-07-06 00:26:14 +02:00
updatePayload . metadata . narrators = matchData [ key ] . split ( ',' ) . map ( v => v . trim ( ) ) . filter ( v => ! ! v )
2022-05-23 04:56:51 +02:00
}
} else if ( key === 'genres' ) {
2022-10-01 23:51:22 +02:00
if ( ( ! libraryItem . media . metadata . genres . length || options . overrideDetails ) ) {
var genresArray = [ ]
if ( Array . isArray ( matchData [ key ] ) ) genresArray = [ ... matchData [ key ] ]
else { // Genres should always be passed in as an array but just incase handle a string
Logger . warn ( ` [Scanner] quickMatch genres is not an array ${ matchData [ key ] } ` )
genresArray = matchData [ key ] . split ( ',' ) . map ( v => v . trim ( ) ) . filter ( v => ! ! v )
}
updatePayload . metadata [ key ] = genresArray
2022-05-23 04:56:51 +02:00
}
} else if ( key === 'tags' ) {
2022-10-01 23:51:22 +02:00
if ( ( ! libraryItem . media . tags . length || options . overrideDetails ) ) {
var tagsArray = [ ]
if ( Array . isArray ( matchData [ key ] ) ) tagsArray = [ ... matchData [ key ] ]
2022-10-03 02:38:52 +02:00
else tagsArray = matchData [ key ] . split ( ',' ) . map ( v => v . trim ( ) ) . filter ( v => ! ! v )
2022-10-01 23:51:22 +02:00
updatePayload [ key ] = tagsArray
2022-03-14 01:34:31 +01:00
}
} else if ( ( ! libraryItem . media . metadata [ key ] || options . overrideDetails ) ) {
2022-05-23 04:56:51 +02:00
updatePayload . metadata [ key ] = matchData [ key ]
2022-03-14 01:34:31 +01:00
}
}
}
// Add or set author if not set
2022-06-04 17:52:37 +02:00
if ( matchData . author && ( ! libraryItem . media . metadata . authorName || options . overrideDetails ) ) {
2022-07-06 00:26:14 +02:00
if ( ! Array . isArray ( matchData . author ) ) {
matchData . author = matchData . author . split ( ',' ) . map ( au => au . trim ( ) ) . filter ( au => ! ! au )
}
2022-05-23 04:56:51 +02:00
const authorPayload = [ ]
for ( let index = 0 ; index < matchData . author . length ; index ++ ) {
const authorName = matchData . author [ index ]
var author = this . db . authors . find ( au => au . checkNameEquals ( authorName ) )
if ( ! author ) {
author = new Author ( )
author . setData ( { name : authorName } )
await this . db . insertEntity ( 'author' , author )
2022-12-22 23:26:11 +01:00
SocketAuthority . emitter ( 'author_added' , author . toJSON ( ) )
2022-05-23 04:56:51 +02:00
}
authorPayload . push ( author . toJSONMinimal ( ) )
2022-02-16 01:33:33 +01:00
}
2022-05-23 04:56:51 +02:00
updatePayload . metadata . authors = authorPayload
2022-03-14 01:34:31 +01:00
}
// Add or set series if not set
2022-06-04 17:52:37 +02:00
if ( matchData . series && ( ! libraryItem . media . metadata . seriesName || options . overrideDetails ) ) {
2022-10-02 00:01:22 +02:00
if ( ! Array . isArray ( matchData . series ) ) matchData . series = [ { series : matchData . series , sequence : matchData . sequence } ]
2022-05-23 04:56:51 +02:00
const seriesPayload = [ ]
for ( let index = 0 ; index < matchData . series . length ; index ++ ) {
const seriesMatchItem = matchData . series [ index ]
var seriesItem = this . db . series . find ( au => au . checkNameEquals ( seriesMatchItem . series ) )
if ( ! seriesItem ) {
seriesItem = new Series ( )
seriesItem . setData ( { name : seriesMatchItem . series } )
await this . db . insertEntity ( 'series' , seriesItem )
2022-12-22 23:26:11 +01:00
SocketAuthority . emitter ( 'series_added' , seriesItem . toJSON ( ) )
2022-05-23 04:56:51 +02:00
}
2022-10-02 00:01:22 +02:00
seriesPayload . push ( seriesItem . toJSONMinimal ( seriesMatchItem . sequence ) )
2022-03-14 01:34:31 +01:00
}
2022-05-23 04:56:51 +02:00
updatePayload . metadata . series = seriesPayload
2022-02-16 01:33:33 +01:00
}
2022-09-03 00:50:09 +02:00
if ( ! Object . keys ( updatePayload . metadata ) . length ) {
delete updatePayload . metadata
2022-02-16 01:33:33 +01:00
}
2022-09-03 00:50:09 +02:00
return updatePayload
2022-02-16 01:33:33 +01:00
}
2022-09-16 01:35:56 +02:00
async quickMatchPodcastEpisodes ( libraryItem , options = { } ) {
const episodesToQuickMatch = libraryItem . media . episodes . filter ( ep => ! ep . enclosureUrl ) // Only quick match episodes without enclosure
if ( ! episodesToQuickMatch . length ) return false
const feed = await getPodcastFeed ( libraryItem . media . metadata . feedUrl )
if ( ! feed ) {
Logger . error ( ` [Scanner] quickMatchPodcastEpisodes: Unable to quick match episodes feed not found for " ${ libraryItem . media . metadata . feedUrl } " ` )
return false
}
2023-01-05 01:13:46 +01:00
let numEpisodesUpdated = 0
2022-09-16 01:35:56 +02:00
for ( const episode of episodesToQuickMatch ) {
const episodeMatches = findMatchingEpisodesInFeed ( feed , episode . title )
if ( episodeMatches && episodeMatches . length ) {
const wasUpdated = this . updateEpisodeWithMatch ( libraryItem , episode , episodeMatches [ 0 ] . episode , options )
2023-01-05 01:13:46 +01:00
if ( wasUpdated ) numEpisodesUpdated ++
2022-09-16 01:35:56 +02:00
}
}
2023-01-05 01:13:46 +01:00
return numEpisodesUpdated
2022-09-16 01:35:56 +02:00
}
updateEpisodeWithMatch ( libraryItem , episode , episodeToMatch , options = { } ) {
Logger . debug ( ` [Scanner] quickMatchPodcastEpisodes: Found episode match for " ${ episode . title } " => ${ episodeToMatch . title } ` )
const matchDataTransformed = {
title : episodeToMatch . title || '' ,
subtitle : episodeToMatch . subtitle || '' ,
description : episodeToMatch . description || '' ,
enclosure : episodeToMatch . enclosure || null ,
episode : episodeToMatch . episode || '' ,
2023-02-22 19:48:36 +01:00
episodeType : episodeToMatch . episodeType || 'full' ,
2022-09-16 01:35:56 +02:00
season : episodeToMatch . season || '' ,
pubDate : episodeToMatch . pubDate || '' ,
publishedAt : episodeToMatch . publishedAt
}
const updatePayload = { }
for ( const key in matchDataTransformed ) {
if ( matchDataTransformed [ key ] ) {
if ( key === 'enclosure' ) {
if ( ! episode . enclosure || JSON . stringify ( episode . enclosure ) !== JSON . stringify ( matchDataTransformed . enclosure ) ) {
updatePayload [ key ] = {
... matchDataTransformed . enclosure
}
}
} else if ( episode [ key ] !== matchDataTransformed [ key ] && ( ! episode [ key ] || options . overrideDetails ) ) {
updatePayload [ key ] = matchDataTransformed [ key ]
}
}
}
if ( Object . keys ( updatePayload ) . length ) {
return libraryItem . media . updateEpisode ( episode . id , updatePayload )
}
return false
}
2022-04-21 01:05:09 +02:00
async matchLibraryItems ( library ) {
if ( library . mediaType === 'podcast' ) {
Logger . error ( ` [Scanner] matchLibraryItems: Match all not supported for podcasts yet ` )
return
}
if ( this . isLibraryScanning ( library . id ) ) {
Logger . error ( ` [Scanner] matchLibraryItems: Already scanning ${ library . id } ` )
return
}
var itemsInLibrary = this . db . getLibraryItemsInLibrary ( library . id )
if ( ! itemsInLibrary . length ) {
Logger . error ( ` [Scanner] matchLibraryItems: Library has no items ${ library . id } ` )
return
}
const provider = library . provider
var libraryScan = new LibraryScan ( )
libraryScan . setData ( library , null , 'match' )
this . librariesScanning . push ( libraryScan . getScanEmitData )
2022-11-24 22:53:58 +01:00
SocketAuthority . emitter ( 'scan_start' , libraryScan . getScanEmitData )
2022-04-21 01:05:09 +02:00
Logger . info ( ` [Scanner] matchLibraryItems: Starting library match scan ${ libraryScan . id } for ${ libraryScan . libraryName } ` )
for ( let i = 0 ; i < itemsInLibrary . length ; i ++ ) {
var libraryItem = itemsInLibrary [ i ]
2022-04-27 02:36:29 +02:00
if ( libraryItem . media . metadata . asin && library . settings . skipMatchingMediaWithAsin ) {
2022-05-09 14:23:29 +02:00
Logger . debug ( ` [Scanner] matchLibraryItems: Skipping " ${ libraryItem . media . metadata . title
} " because it already has an ASIN ( $ { i + 1 } of $ { itemsInLibrary . length } ) ` )
2022-04-27 02:36:29 +02:00
continue ;
}
if ( libraryItem . media . metadata . isbn && library . settings . skipMatchingMediaWithIsbn ) {
2022-05-09 14:23:29 +02:00
Logger . debug ( ` [Scanner] matchLibraryItems: Skipping " ${ libraryItem . media . metadata . title
} " because it already has an ISBN ( $ { i + 1 } of $ { itemsInLibrary . length } ) ` )
2022-04-27 02:36:29 +02:00
continue ;
}
2022-04-21 01:05:09 +02:00
Logger . debug ( ` [Scanner] matchLibraryItems: Quick matching " ${ libraryItem . media . metadata . title } " ( ${ i + 1 } of ${ itemsInLibrary . length } ) ` )
var result = await this . quickMatchLibraryItem ( libraryItem , { provider } )
if ( result . warning ) {
Logger . warn ( ` [Scanner] matchLibraryItems: Match warning ${ result . warning } for library item " ${ libraryItem . media . metadata . title } " ` )
} else if ( result . updated ) {
libraryScan . resultsUpdated ++
}
if ( this . cancelLibraryScan [ libraryScan . libraryId ] ) {
Logger . info ( ` [Scanner] matchLibraryItems: Library match scan canceled for " ${ libraryScan . libraryName } " ` )
delete this . cancelLibraryScan [ libraryScan . libraryId ]
var scanData = libraryScan . getScanEmitData
2022-12-22 23:26:11 +01:00
scanData . results = null
2022-11-24 22:53:58 +01:00
SocketAuthority . emitter ( 'scan_complete' , scanData )
2022-04-21 01:05:09 +02:00
this . librariesScanning = this . librariesScanning . filter ( ls => ls . id !== library . id )
return
}
}
this . librariesScanning = this . librariesScanning . filter ( ls => ls . id !== library . id )
2022-11-24 22:53:58 +01:00
SocketAuthority . emitter ( 'scan_complete' , libraryScan . getScanEmitData )
2022-02-16 01:33:33 +01:00
}
2022-10-02 22:24:32 +02:00
probeAudioFileWithTone ( audioFile ) {
return MediaFileScanner . probeAudioFileWithTone ( audioFile )
}
2021-11-23 02:58:20 +01:00
}
2023-02-22 19:48:36 +01:00
module . exports = Scanner