2022-11-24 22:53:58 +01:00
const Logger = require ( '../Logger' )
const SocketAuthority = require ( '../SocketAuthority' )
2023-07-05 01:14:44 +02:00
const Database = require ( '../Database' )
2024-09-09 17:36:12 +02:00
const { getTitleIgnorePrefix } = require ( '../utils/index' )
2021-11-23 02:58:20 +01:00
// Utils
2022-09-16 01:35:56 +02:00
const { findMatchingEpisodesInFeed , getPodcastFeed } = require ( '../utils/podcastUtils' )
2021-11-23 02:58:20 +01:00
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' )
2021-11-23 02:58:20 +01:00
const LibraryScan = require ( './LibraryScan' )
2023-09-04 23:33:55 +02:00
const LibraryScanner = require ( './LibraryScanner' )
2023-09-09 15:57:59 +02:00
const CoverManager = require ( '../managers/CoverManager' )
2023-10-21 20:53:00 +02:00
const TaskManager = require ( '../managers/TaskManager' )
2022-03-13 00:45:32 +01:00
2021-11-23 02:58:20 +01:00
class Scanner {
2024-08-29 00:26:23 +02:00
constructor ( ) { }
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
2024-08-29 00:26:23 +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.
2024-08-29 00:26:23 +02:00
if ( overrideDefaults == false && Database . 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
2023-12-08 23:33:06 +01:00
var results = await BookFinder . search ( libraryItem , provider , searchTitle , searchAuthor , searchISBN , searchASIN , { maxFuzzySearches : 2 } )
2022-09-03 00:50:09 +02:00
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 } " ` )
2023-09-07 00:48:50 +02:00
var coverResult = await CoverManager . downloadCoverFromUrl ( libraryItem , matchData . cover )
2022-09-03 00:50:09 +02:00
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 )
2024-08-29 00:26:23 +02:00
} else if ( libraryItem . isPodcast ) {
// Podcast quick match
2023-09-04 23:33:55 +02:00
var results = await PodcastFinder . search ( searchTitle )
2022-09-03 00:50:09 +02:00
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 } " ` )
2023-09-07 00:48:50 +02:00
var coverResult = await CoverManager . downloadCoverFromUrl ( libraryItem , matchData . cover )
2022-09-03 00:50:09 +02:00
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 ) {
2024-08-29 00:26:23 +02:00
if ( libraryItem . isPodcast && libraryItem . media . metadata . feedUrl ) {
// Quick match all unmatched podcast episodes
2022-09-16 01:35:56 +02:00
await this . quickMatchPodcastEpisodes ( libraryItem , options )
}
2023-07-05 01:14:44 +02:00
await Database . 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' ) {
2024-08-29 00:26:23 +02:00
if ( ! libraryItem . media . metadata . genres . length || options . overrideDetails ) {
2022-11-09 23:50:26 +01:00
var genresArray = [ ]
if ( Array . isArray ( matchDataTransformed [ key ] ) ) genresArray = [ ... matchDataTransformed [ key ] ]
2024-08-29 00:26:23 +02:00
else {
// Genres should always be passed in as an array but just incase handle a string
2022-11-09 23:50:26 +01:00
Logger . warn ( ` [Scanner] quickMatch genres is not an array ${ matchDataTransformed [ key ] } ` )
2024-08-29 00:26:23 +02:00
genresArray = matchDataTransformed [ key ]
. split ( ',' )
. map ( ( v ) => v . trim ( ) )
. filter ( ( v ) => ! ! v )
2022-11-09 23:50:26 +01:00
}
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' ) {
2024-08-29 00:26:23 +02:00
if ( ! libraryItem . media . metadata . narratorName || options . overrideDetails ) {
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' ) {
2024-08-29 00:26:23 +02:00
if ( ! libraryItem . media . metadata . genres . length || options . overrideDetails ) {
2022-10-01 23:51:22 +02:00
var genresArray = [ ]
if ( Array . isArray ( matchData [ key ] ) ) genresArray = [ ... matchData [ key ] ]
2024-08-29 00:26:23 +02:00
else {
// Genres should always be passed in as an array but just incase handle a string
2022-10-01 23:51:22 +02:00
Logger . warn ( ` [Scanner] quickMatch genres is not an array ${ matchData [ key ] } ` )
2024-08-29 00:26:23 +02:00
genresArray = matchData [ key ]
. split ( ',' )
. map ( ( v ) => v . trim ( ) )
. filter ( ( v ) => ! ! v )
2022-10-01 23:51:22 +02:00
}
updatePayload . metadata [ key ] = genresArray
2022-05-23 04:56:51 +02:00
}
} else if ( key === 'tags' ) {
2024-08-29 00:26:23 +02:00
if ( ! libraryItem . media . tags . length || options . overrideDetails ) {
2022-10-01 23:51:22 +02:00
var tagsArray = [ ]
if ( Array . isArray ( matchData [ key ] ) ) tagsArray = [ ... matchData [ key ] ]
2024-08-29 00:26:23 +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
}
2024-08-29 00:26:23 +02: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 ) ) {
2024-08-29 00:26:23 +02:00
matchData . author = matchData . author
. split ( ',' )
. map ( ( au ) => au . trim ( ) )
. filter ( ( au ) => ! ! au )
2022-07-06 00:26:14 +02:00
}
2022-05-23 04:56:51 +02:00
const authorPayload = [ ]
2023-07-08 16:57:32 +02:00
for ( const authorName of matchData . author ) {
2024-08-31 20:27:48 +02:00
let author = await Database . authorModel . getByNameAndLibrary ( authorName , libraryItem . libraryId )
2022-05-23 04:56:51 +02:00
if ( ! author ) {
2024-08-31 20:27:48 +02:00
author = await Database . authorModel . create ( {
name : authorName ,
2024-09-01 22:08:56 +02:00
lastFirst : Database . authorModel . getLastFirst ( authorName ) ,
2024-08-31 20:27:48 +02:00
libraryId : libraryItem . libraryId
} )
SocketAuthority . emitter ( 'author_added' , author . toOldJSON ( ) )
2023-08-18 21:40:36 +02:00
// Update filter data
Database . addAuthorToFilterData ( libraryItem . libraryId , author . name , author . id )
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 = [ ]
2023-07-08 16:57:32 +02:00
for ( const seriesMatchItem of matchData . series ) {
2024-09-01 22:08:56 +02:00
let seriesItem = await Database . seriesModel . getByNameAndLibrary ( seriesMatchItem . series , libraryItem . libraryId )
2022-05-23 04:56:51 +02:00
if ( ! seriesItem ) {
2024-09-01 22:08:56 +02:00
seriesItem = await Database . seriesModel . create ( {
name : seriesMatchItem . series ,
nameIgnorePrefix : getTitleIgnorePrefix ( seriesMatchItem . series ) ,
2024-09-09 17:36:12 +02:00
libraryId : libraryItem . libraryId
2024-09-01 22:08:56 +02:00
} )
2023-08-18 21:40:36 +02:00
// Update filter data
Database . addSeriesToFilterData ( libraryItem . libraryId , seriesItem . name , seriesItem . id )
2024-09-01 22:08:56 +02:00
SocketAuthority . emitter ( 'series_added' , seriesItem . toOldJSON ( ) )
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 = { } ) {
2024-08-29 00:26:23 +02:00
const episodesToQuickMatch = libraryItem . media . episodes . filter ( ( ep ) => ! ep . enclosureUrl ) // Only quick match episodes without enclosure
2022-09-16 01:35:56 +02:00
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
}
2023-10-21 20:53:00 +02:00
/ * *
* Quick match library items
2024-08-29 00:26:23 +02:00
*
* @ param { import ( '../models/Library' ) } library
* @ param { import ( '../objects/LibraryItem' ) [ ] } libraryItems
* @ param { LibraryScan } libraryScan
2023-10-21 20:53:00 +02:00
* @ returns { Promise < boolean > } false if scan canceled
* /
2023-09-04 23:33:55 +02:00
async matchLibraryItemsChunk ( library , libraryItems , libraryScan ) {
for ( let i = 0 ; i < libraryItems . length ; i ++ ) {
const libraryItem = libraryItems [ i ]
2022-04-27 02:36:29 +02:00
if ( libraryItem . media . metadata . asin && library . settings . skipMatchingMediaWithAsin ) {
2024-08-29 00:26:23 +02:00
Logger . debug ( ` [Scanner] matchLibraryItems: Skipping " ${ libraryItem . media . metadata . title } " because it already has an ASIN ( ${ i + 1 } of ${ libraryItems . length } ) ` )
2023-09-04 23:33:55 +02:00
continue
2022-04-27 02:36:29 +02:00
}
if ( libraryItem . media . metadata . isbn && library . settings . skipMatchingMediaWithIsbn ) {
2024-08-29 00:26:23 +02:00
Logger . debug ( ` [Scanner] matchLibraryItems: Skipping " ${ libraryItem . media . metadata . title } " because it already has an ISBN ( ${ i + 1 } of ${ libraryItems . length } ) ` )
2023-09-04 23:33:55 +02:00
continue
2022-04-27 02:36:29 +02:00
}
2023-09-04 23:33:55 +02:00
Logger . debug ( ` [Scanner] matchLibraryItems: Quick matching " ${ libraryItem . media . metadata . title } " ( ${ i + 1 } of ${ libraryItems . length } ) ` )
const result = await this . quickMatchLibraryItem ( libraryItem , { provider : library . provider } )
2022-04-21 01:05:09 +02:00
if ( result . warning ) {
Logger . warn ( ` [Scanner] matchLibraryItems: Match warning ${ result . warning } for library item " ${ libraryItem . media . metadata . title } " ` )
} else if ( result . updated ) {
libraryScan . resultsUpdated ++
}
2023-09-04 23:33:55 +02:00
if ( LibraryScanner . cancelLibraryScan [ libraryScan . libraryId ] ) {
2022-04-21 01:05:09 +02:00
Logger . info ( ` [Scanner] matchLibraryItems: Library match scan canceled for " ${ libraryScan . libraryName } " ` )
2023-09-04 23:33:55 +02:00
return false
}
}
return true
}
2023-10-21 20:53:00 +02:00
/ * *
* Quick match all library items for library
2024-08-29 00:26:23 +02:00
*
* @ param { import ( '../models/Library' ) } library
2023-10-21 20:53:00 +02:00
* /
2023-09-04 23:33:55 +02:00
async matchLibraryItems ( library ) {
if ( library . mediaType === 'podcast' ) {
Logger . error ( ` [Scanner] matchLibraryItems: Match all not supported for podcasts yet ` )
return
}
if ( LibraryScanner . isLibraryScanning ( library . id ) ) {
Logger . error ( ` [Scanner] Library " ${ library . name } " is already scanning ` )
return
}
const limit = 100
let offset = 0
const libraryScan = new LibraryScan ( )
2023-09-19 00:38:45 +02:00
libraryScan . setData ( library , 'match' )
2023-09-04 23:33:55 +02:00
LibraryScanner . librariesScanning . push ( libraryScan . getScanEmitData )
2023-10-21 20:53:00 +02:00
const taskData = {
libraryId : library . id
}
const task = TaskManager . createAndAddTask ( 'library-match-all' , ` Matching books in " ${ library . name } " ` , null , true , taskData )
2023-09-04 23:33:55 +02:00
Logger . info ( ` [Scanner] matchLibraryItems: Starting library match scan ${ libraryScan . id } for ${ libraryScan . libraryName } ` )
let hasMoreChunks = true
2023-10-21 20:53:00 +02:00
let isCanceled = false
2023-09-04 23:33:55 +02:00
while ( hasMoreChunks ) {
const libraryItems = await Database . libraryItemModel . getLibraryItemsIncrement ( offset , limit , { libraryId : library . id } )
if ( ! libraryItems . length ) {
2023-09-19 00:38:45 +02:00
break
2022-04-21 01:05:09 +02:00
}
2023-09-19 00:38:45 +02:00
2023-09-04 23:33:55 +02:00
offset += limit
2024-04-15 00:19:21 +02:00
hasMoreChunks = libraryItems . length === limit
2024-08-29 00:26:23 +02:00
let oldLibraryItems = libraryItems . map ( ( li ) => Database . libraryItemModel . getOldLibraryItem ( li ) )
2023-09-04 23:33:55 +02:00
const shouldContinue = await this . matchLibraryItemsChunk ( library , oldLibraryItems , libraryScan )
if ( ! shouldContinue ) {
2023-10-21 20:53:00 +02:00
isCanceled = true
2023-09-04 23:33:55 +02:00
break
}
2022-04-21 01:05:09 +02:00
}
2023-09-19 00:38:45 +02:00
if ( offset === 0 ) {
Logger . error ( ` [Scanner] matchLibraryItems: Library has no items ${ library . id } ` )
libraryScan . setComplete ( 'Library has no items' )
2023-10-21 20:53:00 +02:00
task . setFailed ( libraryScan . error )
2023-09-19 00:38:45 +02:00
} else {
libraryScan . setComplete ( )
2023-10-21 20:53:00 +02:00
task . setFinished ( isCanceled ? 'Canceled' : libraryScan . scanResultsString )
2023-09-19 00:38:45 +02:00
}
2023-09-04 23:33:55 +02:00
delete LibraryScanner . cancelLibraryScan [ libraryScan . libraryId ]
2024-08-29 00:26:23 +02:00
LibraryScanner . librariesScanning = LibraryScanner . librariesScanning . filter ( ( ls ) => ls . id !== library . id )
2023-10-21 20:53:00 +02:00
TaskManager . taskFinished ( task )
2022-02-16 01:33:33 +01:00
}
2021-11-23 02:58:20 +01:00
}
2023-09-07 00:48:50 +02:00
module . exports = new Scanner ( )