2023-09-02 01:01:17 +02:00
const uuidv4 = require ( "uuid" ) . v4
2023-09-03 22:14:58 +02:00
const Path = require ( 'path' )
2023-09-04 00:51:58 +02:00
const sequelize = require ( 'sequelize' )
2023-09-02 01:01:17 +02:00
const { LogLevel } = require ( '../utils/constants' )
2023-09-03 00:49:28 +02:00
const { getTitleIgnorePrefix , areEquivalent } = require ( '../utils/index' )
2023-09-02 01:01:17 +02:00
const parseNameString = require ( '../utils/parsers/parseNameString' )
2024-01-08 00:51:07 +01:00
const parseEbookMetadata = require ( '../utils/parsers/parseEbookMetadata' )
2023-09-03 00:49:28 +02:00
const globals = require ( '../utils/globals' )
2023-09-02 01:01:17 +02:00
const AudioFileScanner = require ( './AudioFileScanner' )
const Database = require ( '../Database' )
2023-09-03 22:14:58 +02:00
const { readTextFile , filePathToPOSIX , getFileTimestampsWithIno } = require ( '../utils/fileUtils' )
2023-09-02 01:01:17 +02:00
const AudioFile = require ( '../objects/files/AudioFile' )
const CoverManager = require ( '../managers/CoverManager' )
2023-09-03 22:14:58 +02:00
const LibraryFile = require ( '../objects/files/LibraryFile' )
2023-09-04 00:51:58 +02:00
const SocketAuthority = require ( '../SocketAuthority' )
2023-09-03 00:49:28 +02:00
const fsExtra = require ( "../libs/fsExtra" )
2023-09-07 00:48:50 +02:00
const BookFinder = require ( '../finders/BookFinder' )
2023-09-02 01:01:17 +02:00
2023-10-09 00:10:43 +02:00
const LibraryScan = require ( "./LibraryScan" )
const OpfFileScanner = require ( './OpfFileScanner' )
2023-11-12 14:30:23 +01:00
const NfoFileScanner = require ( './NfoFileScanner' )
2023-10-09 00:10:43 +02:00
const AbsMetadataFileScanner = require ( './AbsMetadataFileScanner' )
2024-03-19 18:28:26 +01:00
const EBookFile = require ( "../objects/files/EBookFile" )
2023-10-09 00:10:43 +02:00
2023-09-02 01:01:17 +02:00
/ * *
* Metadata for books pulled from files
* @ typedef BookMetadataObject
* @ property { string } title
* @ property { string } titleIgnorePrefix
* @ property { string } subtitle
* @ property { string } publishedYear
* @ property { string } publisher
* @ property { string } description
* @ property { string } isbn
* @ property { string } asin
* @ property { string } language
* @ property { string [ ] } narrators
* @ property { string [ ] } genres
* @ property { string [ ] } tags
* @ property { string [ ] } authors
* @ property { { name : string , sequence : string } [ ] } series
* @ property { { id : number , start : number , end : number , title : string } [ ] } chapters
* @ property { boolean } explicit
* @ property { boolean } abridged
* @ property { string } coverPath
* /
class BookScanner {
constructor ( ) { }
2023-09-03 00:49:28 +02:00
/ * *
* @ param { import ( '../models/LibraryItem' ) } existingLibraryItem
* @ param { import ( './LibraryItemScanData' ) } libraryItemData
2023-09-04 00:51:58 +02:00
* @ param { import ( '../models/Library' ) . LibrarySettingsObject } librarySettings
2023-09-07 00:48:50 +02:00
* @ param { LibraryScan } libraryScan
2023-10-09 00:10:43 +02:00
* @ returns { Promise < { libraryItem : import ( '../models/LibraryItem' ) , wasUpdated : boolean } > }
2023-09-03 00:49:28 +02:00
* /
2023-09-04 00:51:58 +02:00
async rescanExistingBookLibraryItem ( existingLibraryItem , libraryItemData , librarySettings , libraryScan ) {
2023-09-03 00:49:28 +02:00
/** @type {import('../models/Book')} */
const media = await existingLibraryItem . getMedia ( {
include : [
{
model : Database . authorModel ,
through : {
attributes : [ 'id' , 'createdAt' ]
}
} ,
{
model : Database . seriesModel ,
through : {
attributes : [ 'id' , 'sequence' , 'createdAt' ]
}
}
] ,
order : [
[ Database . authorModel , Database . bookAuthorModel , 'createdAt' , 'ASC' ] ,
[ Database . seriesModel , 'bookSeries' , 'createdAt' , 'ASC' ]
]
} )
2023-10-10 00:09:36 +02:00
let hasMediaChanges = libraryItemData . hasAudioFileChanges || libraryItemData . audioLibraryFiles . length !== media . audioFiles . length
if ( hasMediaChanges ) {
2023-09-03 00:49:28 +02:00
// Filter out audio files that were removed
media . audioFiles = media . audioFiles . filter ( af => ! libraryItemData . checkAudioFileRemoved ( af ) )
// Update audio files that were modified
if ( libraryItemData . audioLibraryFilesModified . length ) {
2024-03-19 18:28:26 +01:00
let scannedAudioFiles = await AudioFileScanner . executeMediaFileScans ( existingLibraryItem . mediaType , libraryItemData , libraryItemData . audioLibraryFilesModified . map ( lf => lf . new ) )
2023-09-03 00:49:28 +02:00
media . audioFiles = media . audioFiles . map ( ( audioFileObj ) => {
let matchedScannedAudioFile = scannedAudioFiles . find ( saf => saf . metadata . path === audioFileObj . metadata . path )
if ( ! matchedScannedAudioFile ) {
matchedScannedAudioFile = scannedAudioFiles . find ( saf => saf . ino === audioFileObj . ino )
}
if ( matchedScannedAudioFile ) {
scannedAudioFiles = scannedAudioFiles . filter ( saf => saf !== matchedScannedAudioFile )
const audioFile = new AudioFile ( audioFileObj )
audioFile . updateFromScan ( matchedScannedAudioFile )
return audioFile . toJSON ( )
}
return audioFileObj
} )
// Modified audio files that were not found on the book
if ( scannedAudioFiles . length ) {
media . audioFiles . push ( ... scannedAudioFiles )
}
}
// Add new audio files scanned in
if ( libraryItemData . audioLibraryFilesAdded . length ) {
const scannedAudioFiles = await AudioFileScanner . executeMediaFileScans ( existingLibraryItem . mediaType , libraryItemData , libraryItemData . audioLibraryFilesAdded )
media . audioFiles . push ( ... scannedAudioFiles )
}
// Add audio library files that are not already set on the book (safety check)
let audioLibraryFilesToAdd = [ ]
for ( const audioLibraryFile of libraryItemData . audioLibraryFiles ) {
if ( ! media . audioFiles . some ( af => af . ino === audioLibraryFile . ino ) ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Existing audio library file " ${ audioLibraryFile . metadata . relPath } " was not set on book " ${ media . title } " so setting it now ` )
audioLibraryFilesToAdd . push ( audioLibraryFile )
}
}
if ( audioLibraryFilesToAdd . length ) {
const scannedAudioFiles = await AudioFileScanner . executeMediaFileScans ( existingLibraryItem . mediaType , libraryItemData , audioLibraryFilesToAdd )
media . audioFiles . push ( ... scannedAudioFiles )
}
media . audioFiles = AudioFileScanner . runSmartTrackOrder ( existingLibraryItem . relPath , media . audioFiles )
media . duration = 0
media . audioFiles . forEach ( ( af ) => {
if ( ! isNaN ( af . duration ) ) {
media . duration += af . duration
}
} )
media . changed ( 'audioFiles' , true )
}
// Check if cover was removed
2024-03-19 18:28:26 +01:00
if ( media . coverPath && libraryItemData . imageLibraryFilesRemoved . some ( lf => lf . metadata . path === media . coverPath ) && ! ( await fsExtra . pathExists ( media . coverPath ) ) ) {
2023-09-03 00:49:28 +02:00
media . coverPath = null
hasMediaChanges = true
}
2024-03-19 18:28:26 +01:00
// Update cover if it was modified
if ( media . coverPath && libraryItemData . imageLibraryFilesModified . length ) {
let coverMatch = libraryItemData . imageLibraryFilesModified . find ( iFile => iFile . old . metadata . path === media . coverPath )
if ( coverMatch ) {
const coverPath = coverMatch . new . metadata . path
if ( coverPath !== media . coverPath ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book cover " ${ media . coverPath } " => " ${ coverPath } " for book " ${ media . title } " ` )
media . coverPath = coverPath
media . changed ( 'coverPath' , true )
hasMediaChanges = true
}
}
}
2023-09-03 00:49:28 +02:00
// Check if cover is not set and image files were found
if ( ! media . coverPath && libraryItemData . imageLibraryFiles . length ) {
// Prefer using a cover image with the name "cover" otherwise use the first image
const coverMatch = libraryItemData . imageLibraryFiles . find ( iFile => / \ / cover \ . [ ^ . \ / ] * $ / . test ( iFile . metadata . path ) )
media . coverPath = coverMatch ? . metadata . path || libraryItemData . imageLibraryFiles [ 0 ] . metadata . path
hasMediaChanges = true
}
// Check if ebook was removed
2023-09-04 00:51:58 +02:00
if ( media . ebookFile && ( librarySettings . audiobooksOnly || libraryItemData . checkEbookFileRemoved ( media . ebookFile ) ) ) {
2023-09-03 00:49:28 +02:00
media . ebookFile = null
hasMediaChanges = true
}
2024-03-19 18:28:26 +01:00
// Update ebook if it was modified
if ( media . ebookFile && libraryItemData . ebookLibraryFilesModified . length ) {
let ebookMatch = libraryItemData . ebookLibraryFilesModified . find ( eFile => eFile . old . metadata . path === media . ebookFile . metadata . path )
if ( ebookMatch ) {
const ebookFile = new EBookFile ( ebookMatch . new )
ebookFile . ebookFormat = ebookFile . metadata . ext . slice ( 1 ) . toLowerCase ( )
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book ebook file " ${ media . ebookFile . metadata . path } " => " ${ ebookFile . metadata . path } " for book " ${ media . title } " ` )
media . ebookFile = ebookFile . toJSON ( )
media . changed ( 'ebookFile' , true )
hasMediaChanges = true
}
}
2023-09-03 00:49:28 +02:00
// Check if ebook is not set and ebooks were found
2023-09-04 00:51:58 +02:00
if ( ! media . ebookFile && ! librarySettings . audiobooksOnly && libraryItemData . ebookLibraryFiles . length ) {
2023-09-03 00:49:28 +02:00
// Prefer to use an epub ebook then fallback to the first ebook found
let ebookLibraryFile = libraryItemData . ebookLibraryFiles . find ( lf => lf . metadata . ext . slice ( 1 ) . toLowerCase ( ) === 'epub' )
if ( ! ebookLibraryFile ) ebookLibraryFile = libraryItemData . ebookLibraryFiles [ 0 ]
2023-09-19 22:42:38 +02:00
ebookLibraryFile = ebookLibraryFile . toJSON ( )
2023-09-03 00:49:28 +02:00
// Ebook file is the same as library file except for additional `ebookFormat`
ebookLibraryFile . ebookFormat = ebookLibraryFile . metadata . ext . slice ( 1 ) . toLowerCase ( )
media . ebookFile = ebookLibraryFile
media . changed ( 'ebookFile' , true )
hasMediaChanges = true
}
2024-01-08 00:51:07 +01:00
const ebookFileScanData = await parseEbookMetadata . parse ( media . ebookFile )
const bookMetadata = await this . getBookMetadataFromScanData ( media . audioFiles , ebookFileScanData , libraryItemData , libraryScan , librarySettings , existingLibraryItem . id )
2023-09-03 00:49:28 +02:00
let authorsUpdated = false
const bookAuthorsRemoved = [ ]
let seriesUpdated = false
const bookSeriesRemoved = [ ]
for ( const key in bookMetadata ) {
// Ignore unset metadata and empty arrays
if ( bookMetadata [ key ] === undefined || ( Array . isArray ( bookMetadata [ key ] ) && ! bookMetadata [ key ] . length ) ) continue
if ( key === 'authors' ) {
// Check for authors added
for ( const authorName of bookMetadata . authors ) {
if ( ! media . authors . some ( au => au . name === authorName ) ) {
2024-03-11 23:07:03 +01:00
const existingAuthorId = await Database . getAuthorIdByName ( libraryItemData . libraryId , authorName )
if ( existingAuthorId ) {
2023-09-03 00:49:28 +02:00
await Database . bookAuthorModel . create ( {
bookId : media . id ,
2024-03-11 23:07:03 +01:00
authorId : existingAuthorId
2023-09-03 00:49:28 +02:00
} )
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book " ${ bookMetadata . title } " added author " ${ authorName } " ` )
authorsUpdated = true
} else {
const newAuthor = await Database . authorModel . create ( {
name : authorName ,
lastFirst : parseNameString . nameToLastFirst ( authorName ) ,
2023-09-04 00:51:58 +02:00
libraryId : libraryItemData . libraryId
2023-09-03 00:49:28 +02:00
} )
await media . addAuthor ( newAuthor )
2023-09-04 00:51:58 +02:00
Database . addAuthorToFilterData ( libraryItemData . libraryId , newAuthor . name , newAuthor . id )
2023-09-03 00:49:28 +02:00
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book " ${ bookMetadata . title } " added new author " ${ authorName } " ` )
authorsUpdated = true
}
}
}
// Check for authors removed
for ( const author of media . authors ) {
if ( ! bookMetadata . authors . includes ( author . name ) ) {
await author . bookAuthor . destroy ( )
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book " ${ bookMetadata . title } " removed author " ${ author . name } " ` )
authorsUpdated = true
bookAuthorsRemoved . push ( author . id )
}
}
} else if ( key === 'series' ) {
// Check for series added
for ( const seriesObj of bookMetadata . series ) {
2023-12-24 18:53:57 +01:00
const existingBookSeries = media . series . find ( se => se . name === seriesObj . name )
if ( ! existingBookSeries ) {
2024-03-11 23:07:03 +01:00
const existingSeriesId = await Database . getSeriesIdByName ( libraryItemData . libraryId , seriesObj . name )
if ( existingSeriesId ) {
2023-09-03 00:49:28 +02:00
await Database . bookSeriesModel . create ( {
bookId : media . id ,
2024-03-11 23:07:03 +01:00
seriesId : existingSeriesId ,
2023-09-03 00:49:28 +02:00
sequence : seriesObj . sequence
} )
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book " ${ bookMetadata . title } " added series " ${ seriesObj . name } " ${ seriesObj . sequence ? ` with sequence " ${ seriesObj . sequence } " ` : '' } ` )
seriesUpdated = true
} else {
const newSeries = await Database . seriesModel . create ( {
name : seriesObj . name ,
nameIgnorePrefix : getTitleIgnorePrefix ( seriesObj . name ) ,
2023-09-04 00:51:58 +02:00
libraryId : libraryItemData . libraryId
2023-09-03 00:49:28 +02:00
} )
2023-09-04 00:51:58 +02:00
await media . addSeries ( newSeries , { through : { sequence : seriesObj . sequence } } )
Database . addSeriesToFilterData ( libraryItemData . libraryId , newSeries . name , newSeries . id )
2023-09-03 00:49:28 +02:00
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book " ${ bookMetadata . title } " added new series " ${ seriesObj . name } " ${ seriesObj . sequence ? ` with sequence " ${ seriesObj . sequence } " ` : '' } ` )
seriesUpdated = true
}
2023-12-24 18:53:57 +01:00
} else if ( seriesObj . sequence && existingBookSeries . bookSeries . sequence !== seriesObj . sequence ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book " ${ bookMetadata . title } " series " ${ seriesObj . name } " sequence " ${ existingBookSeries . bookSeries . sequence || '' } " => " ${ seriesObj . sequence } " ` )
seriesUpdated = true
existingBookSeries . bookSeries . sequence = seriesObj . sequence
await existingBookSeries . bookSeries . save ( )
2023-09-03 00:49:28 +02:00
}
}
// Check for series removed
for ( const series of media . series ) {
if ( ! bookMetadata . series . some ( se => se . name === series . name ) ) {
await series . bookSeries . destroy ( )
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book " ${ bookMetadata . title } " removed series " ${ series . name } " ` )
seriesUpdated = true
bookSeriesRemoved . push ( series . id )
}
}
} else if ( key === 'genres' ) {
const existingGenres = media . genres || [ ]
if ( bookMetadata . genres . some ( g => ! existingGenres . includes ( g ) ) || existingGenres . some ( g => ! bookMetadata . genres . includes ( g ) ) ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book genres " ${ existingGenres . join ( ',' ) } " => " ${ bookMetadata . genres . join ( ',' ) } " for book " ${ bookMetadata . title } " ` )
media . genres = bookMetadata . genres
hasMediaChanges = true
}
} else if ( key === 'tags' ) {
const existingTags = media . tags || [ ]
if ( bookMetadata . tags . some ( t => ! existingTags . includes ( t ) ) || existingTags . some ( t => ! bookMetadata . tags . includes ( t ) ) ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book tags " ${ existingTags . join ( ',' ) } " => " ${ bookMetadata . tags . join ( ',' ) } " for book " ${ bookMetadata . title } " ` )
media . tags = bookMetadata . tags
hasMediaChanges = true
}
} else if ( key === 'narrators' ) {
const existingNarrators = media . narrators || [ ]
if ( bookMetadata . narrators . some ( t => ! existingNarrators . includes ( t ) ) || existingNarrators . some ( t => ! bookMetadata . narrators . includes ( t ) ) ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book narrators " ${ existingNarrators . join ( ',' ) } " => " ${ bookMetadata . narrators . join ( ',' ) } " for book " ${ bookMetadata . title } " ` )
media . narrators = bookMetadata . narrators
hasMediaChanges = true
}
} else if ( key === 'chapters' ) {
if ( ! areEquivalent ( media . chapters , bookMetadata . chapters ) ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book chapters for book " ${ bookMetadata . title } " ` )
media . chapters = bookMetadata . chapters
hasMediaChanges = true
}
} else if ( key === 'coverPath' ) {
if ( media . coverPath && media . coverPath !== bookMetadata . coverPath && ! ( await fsExtra . pathExists ( media . coverPath ) ) ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book cover " ${ media . coverPath } " => " ${ bookMetadata . coverPath } " for book " ${ bookMetadata . title } " - original cover path does not exist ` )
media . coverPath = bookMetadata . coverPath
hasMediaChanges = true
} else if ( ! media . coverPath ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book cover "unset" => " ${ bookMetadata . coverPath } " for book " ${ bookMetadata . title } " ` )
media . coverPath = bookMetadata . coverPath
hasMediaChanges = true
}
} else if ( bookMetadata [ key ] !== media [ key ] ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book ${ key } " ${ media [ key ] } " => " ${ bookMetadata [ key ] } " for book " ${ bookMetadata . title } " ` )
media [ key ] = bookMetadata [ key ]
hasMediaChanges = true
}
}
// Load authors/series again if updated (for sending back to client)
if ( authorsUpdated ) {
media . authors = await media . getAuthors ( {
joinTableAttributes : [ 'createdAt' ] ,
order : [
2023-09-04 00:51:58 +02:00
sequelize . literal ( ` bookAuthor.createdAt ASC ` )
2023-09-03 00:49:28 +02:00
]
} )
}
if ( seriesUpdated ) {
media . series = await media . getSeries ( {
joinTableAttributes : [ 'sequence' , 'createdAt' ] ,
order : [
2023-09-04 00:51:58 +02:00
sequelize . literal ( ` bookSeries.createdAt ASC ` )
2023-09-03 00:49:28 +02:00
]
} )
}
2024-01-08 00:51:07 +01:00
// If no cover then extract cover from audio file OR from ebook
const libraryItemDir = existingLibraryItem . isFile ? null : existingLibraryItem . path
2023-09-07 00:48:50 +02:00
if ( ! media . coverPath ) {
2024-01-08 00:51:07 +01:00
let extractedCoverPath = await CoverManager . saveEmbeddedCoverArt ( media . audioFiles , existingLibraryItem . id , libraryItemDir )
2023-09-07 00:48:50 +02:00
if ( extractedCoverPath ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book " ${ bookMetadata . title } " extracted embedded cover art from audio file to path " ${ extractedCoverPath } " ` )
media . coverPath = extractedCoverPath
hasMediaChanges = true
2024-01-08 00:51:07 +01:00
} else if ( ebookFileScanData ? . ebookCoverPath ) {
extractedCoverPath = await CoverManager . saveEbookCoverArt ( ebookFileScanData , existingLibraryItem . id , libraryItemDir )
if ( extractedCoverPath ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Updating book " ${ bookMetadata . title } " extracted embedded cover art from ebook file to path " ${ extractedCoverPath } " ` )
media . coverPath = extractedCoverPath
2023-09-07 00:48:50 +02:00
hasMediaChanges = true
}
}
}
2024-01-08 00:51:07 +01:00
// If no cover then search for cover if enabled in server settings
if ( ! media . coverPath && Database . serverSettings . scannerFindCovers ) {
const authorName = media . authors . map ( au => au . name ) . filter ( au => au ) . join ( ', ' )
const coverPath = await this . searchForCover ( existingLibraryItem . id , libraryItemDir , media . title , authorName , libraryScan )
if ( coverPath ) {
media . coverPath = coverPath
hasMediaChanges = true
}
}
2023-09-03 22:14:58 +02:00
existingLibraryItem . media = media
let libraryItemUpdated = false
// Save Book changes to db
if ( hasMediaChanges ) {
await media . save ( )
await this . saveMetadataFile ( existingLibraryItem , libraryScan )
libraryItemUpdated = global . ServerSettings . storeMetadataWithItem && ! existingLibraryItem . isFile
}
2023-10-19 00:02:15 +02:00
// If book has no audio files and no ebook then it is considered missing
if ( ! media . audioFiles . length && ! media . ebookFile ) {
if ( ! existingLibraryItem . isMissing ) {
libraryScan . addLog ( LogLevel . INFO , ` Book " ${ bookMetadata . title } " has no audio files and no ebook file. Setting library item as missing ` )
existingLibraryItem . isMissing = true
libraryItemUpdated = true
}
} else if ( existingLibraryItem . isMissing ) {
libraryScan . addLog ( LogLevel . INFO , ` Book " ${ bookMetadata . title } " was missing but now has media files. Setting library item as NOT missing ` )
existingLibraryItem . isMissing = false
libraryItemUpdated = true
}
2023-09-03 22:14:58 +02:00
// Check/update the isSupplementary flag on libraryFiles for the LibraryItem
for ( const libraryFile of existingLibraryItem . libraryFiles ) {
if ( globals . SupportedEbookTypes . includes ( libraryFile . metadata . ext . slice ( 1 ) . toLowerCase ( ) ) ) {
if ( media . ebookFile && libraryFile . ino === media . ebookFile . ino ) {
if ( libraryFile . isSupplementary !== false ) {
libraryFile . isSupplementary = false
libraryItemUpdated = true
}
} else if ( libraryFile . isSupplementary !== true ) {
libraryFile . isSupplementary = true
libraryItemUpdated = true
}
}
}
if ( libraryItemUpdated ) {
existingLibraryItem . changed ( 'libraryFiles' , true )
await existingLibraryItem . save ( )
}
2023-09-03 00:49:28 +02:00
libraryScan . seriesRemovedFromBooks . push ( ... bookSeriesRemoved )
libraryScan . authorsRemovedFromBooks . push ( ... bookAuthorsRemoved )
2023-10-09 00:10:43 +02:00
return {
libraryItem : existingLibraryItem ,
wasUpdated : hasMediaChanges || libraryItemUpdated || seriesUpdated || authorsUpdated
}
2023-09-03 00:49:28 +02:00
}
2023-09-02 01:01:17 +02:00
/ * *
*
* @ param { import ( './LibraryItemScanData' ) } libraryItemData
2023-09-04 00:51:58 +02:00
* @ param { import ( '../models/Library' ) . LibrarySettingsObject } librarySettings
2023-09-07 00:48:50 +02:00
* @ param { LibraryScan } libraryScan
2023-09-04 18:50:55 +02:00
* @ returns { Promise < import ( '../models/LibraryItem' ) > }
2023-09-02 01:01:17 +02:00
* /
2023-09-04 00:51:58 +02:00
async scanNewBookLibraryItem ( libraryItemData , librarySettings , libraryScan ) {
2023-09-02 01:01:17 +02:00
// Scan audio files found
2023-09-04 00:51:58 +02:00
let scannedAudioFiles = await AudioFileScanner . executeMediaFileScans ( libraryItemData . mediaType , libraryItemData , libraryItemData . audioLibraryFiles )
2023-09-02 01:01:17 +02:00
scannedAudioFiles = AudioFileScanner . runSmartTrackOrder ( libraryItemData . relPath , scannedAudioFiles )
// Find ebook file (prefer epub)
2023-09-04 00:51:58 +02:00
let ebookLibraryFile = librarySettings . audiobooksOnly ? null : libraryItemData . ebookLibraryFiles . find ( lf => lf . metadata . ext . slice ( 1 ) . toLowerCase ( ) === 'epub' ) || libraryItemData . ebookLibraryFiles [ 0 ]
2023-09-02 01:01:17 +02:00
// Do not add library items that have no valid audio files and no ebook file
if ( ! ebookLibraryFile && ! scannedAudioFiles . length ) {
libraryScan . addLog ( LogLevel . WARN , ` Library item at path " ${ libraryItemData . relPath } " has no audio files and no ebook file - ignoring ` )
return null
}
2024-01-08 00:51:07 +01:00
let ebookFileScanData = null
2023-09-02 01:01:17 +02:00
if ( ebookLibraryFile ) {
2023-09-19 22:42:38 +02:00
ebookLibraryFile = ebookLibraryFile . toJSON ( )
2023-09-02 01:01:17 +02:00
ebookLibraryFile . ebookFormat = ebookLibraryFile . metadata . ext . slice ( 1 ) . toLowerCase ( )
2024-01-08 00:51:07 +01:00
ebookFileScanData = await parseEbookMetadata . parse ( ebookLibraryFile )
2023-09-02 01:01:17 +02:00
}
2024-01-08 00:51:07 +01:00
const bookMetadata = await this . getBookMetadataFromScanData ( scannedAudioFiles , ebookFileScanData , libraryItemData , libraryScan , librarySettings )
2023-09-03 00:49:28 +02:00
bookMetadata . explicit = ! ! bookMetadata . explicit // Ensure boolean
bookMetadata . abridged = ! ! bookMetadata . abridged // Ensure boolean
2023-09-02 01:01:17 +02:00
let duration = 0
scannedAudioFiles . forEach ( ( af ) => duration += ( ! isNaN ( af . duration ) ? Number ( af . duration ) : 0 ) )
const bookObject = {
... bookMetadata ,
audioFiles : scannedAudioFiles ,
ebookFile : ebookLibraryFile || null ,
duration ,
bookAuthors : [ ] ,
bookSeries : [ ]
}
if ( bookMetadata . authors . length ) {
for ( const authorName of bookMetadata . authors ) {
2024-03-11 23:07:03 +01:00
const matchingAuthorId = await Database . getAuthorIdByName ( libraryItemData . libraryId , authorName )
if ( matchingAuthorId ) {
2023-09-02 01:01:17 +02:00
bookObject . bookAuthors . push ( {
2024-03-11 23:07:03 +01:00
authorId : matchingAuthorId
2023-09-02 01:01:17 +02:00
} )
} else {
// New author
bookObject . bookAuthors . push ( {
author : {
2023-09-04 00:51:58 +02:00
libraryId : libraryItemData . libraryId ,
2023-09-02 01:01:17 +02:00
name : authorName ,
lastFirst : parseNameString . nameToLastFirst ( authorName )
}
} )
}
}
}
if ( bookMetadata . series . length ) {
for ( const seriesObj of bookMetadata . series ) {
if ( ! seriesObj . name ) continue
2024-03-11 23:07:03 +01:00
const matchingSeriesId = await Database . getSeriesIdByName ( libraryItemData . libraryId , seriesObj . name )
if ( matchingSeriesId ) {
2023-09-02 01:01:17 +02:00
bookObject . bookSeries . push ( {
2024-03-11 23:07:03 +01:00
seriesId : matchingSeriesId ,
2023-09-02 01:01:17 +02:00
sequence : seriesObj . sequence
} )
} else {
bookObject . bookSeries . push ( {
sequence : seriesObj . sequence ,
series : {
name : seriesObj . name ,
nameIgnorePrefix : getTitleIgnorePrefix ( seriesObj . name ) ,
2023-09-04 00:51:58 +02:00
libraryId : libraryItemData . libraryId
2023-09-02 01:01:17 +02:00
}
} )
}
}
}
const libraryItemObj = libraryItemData . libraryItemObject
libraryItemObj . id = uuidv4 ( ) // Generate library item id ahead of time to use for saving extracted cover image
2023-09-03 00:49:28 +02:00
libraryItemObj . isMissing = false
libraryItemObj . isInvalid = false
2023-09-04 18:50:55 +02:00
libraryItemObj . extraData = { }
2023-09-03 00:49:28 +02:00
// Set isSupplementary flag on ebook library files
for ( const libraryFile of libraryItemObj . libraryFiles ) {
if ( globals . SupportedEbookTypes . includes ( libraryFile . metadata . ext . slice ( 1 ) . toLowerCase ( ) ) ) {
libraryFile . isSupplementary = libraryFile . ino !== ebookLibraryFile ? . ino
}
}
2023-09-02 01:01:17 +02:00
2024-01-08 00:51:07 +01:00
// If cover was not found in folder then check embedded covers in audio files OR ebook file
const libraryItemDir = libraryItemObj . isFile ? null : libraryItemObj . path
2023-09-07 00:48:50 +02:00
if ( ! bookObject . coverPath ) {
2024-01-08 00:51:07 +01:00
let extractedCoverPath = await CoverManager . saveEmbeddedCoverArt ( scannedAudioFiles , libraryItemObj . id , libraryItemDir )
2023-09-07 00:48:50 +02:00
if ( extractedCoverPath ) {
2024-01-08 00:51:07 +01:00
libraryScan . addLog ( LogLevel . DEBUG , ` Extracted embedded cover from audio file at " ${ extractedCoverPath } " for book " ${ bookObject . title } " ` )
2023-09-07 00:48:50 +02:00
bookObject . coverPath = extractedCoverPath
2024-01-08 00:51:07 +01:00
} else if ( ebookFileScanData ? . ebookCoverPath ) {
extractedCoverPath = await CoverManager . saveEbookCoverArt ( ebookFileScanData , libraryItemObj . id , libraryItemDir )
if ( extractedCoverPath ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Extracted embedded cover from ebook file at " ${ extractedCoverPath } " for book " ${ bookObject . title } " ` )
bookObject . coverPath = extractedCoverPath
}
2023-09-07 00:48:50 +02:00
}
2023-09-02 01:01:17 +02:00
}
2024-01-08 00:51:07 +01:00
// If cover not found then search for cover if enabled in settings
if ( ! bookObject . coverPath && Database . serverSettings . scannerFindCovers ) {
const authorName = bookMetadata . authors . join ( ', ' )
bookObject . coverPath = await this . searchForCover ( libraryItemObj . id , libraryItemDir , bookObject . title , authorName , libraryScan )
}
2023-09-02 01:01:17 +02:00
libraryItemObj . book = bookObject
const libraryItem = await Database . libraryItemModel . create ( libraryItemObj , {
include : {
model : Database . bookModel ,
include : [
{
model : Database . bookSeriesModel ,
include : {
model : Database . seriesModel
}
} ,
{
model : Database . bookAuthorModel ,
include : {
model : Database . authorModel
}
}
]
}
} )
// Update library filter data
if ( libraryItem . book . bookSeries ? . length ) {
for ( const bs of libraryItem . book . bookSeries ) {
if ( bs . series ) {
2023-09-04 00:51:58 +02:00
Database . addSeriesToFilterData ( libraryItemData . libraryId , bs . series . name , bs . series . id )
2023-09-02 01:01:17 +02:00
}
}
}
if ( libraryItem . book . bookAuthors ? . length ) {
for ( const ba of libraryItem . book . bookAuthors ) {
if ( ba . author ) {
2023-09-04 00:51:58 +02:00
Database . addAuthorToFilterData ( libraryItemData . libraryId , ba . author . name , ba . author . id )
2023-09-02 01:01:17 +02:00
}
}
}
2023-09-04 00:51:58 +02:00
Database . addNarratorsToFilterData ( libraryItemData . libraryId , libraryItem . book . narrators )
Database . addGenresToFilterData ( libraryItemData . libraryId , libraryItem . book . genres )
Database . addTagsToFilterData ( libraryItemData . libraryId , libraryItem . book . tags )
Database . addPublisherToFilterData ( libraryItemData . libraryId , libraryItem . book . publisher )
Database . addLanguageToFilterData ( libraryItemData . libraryId , libraryItem . book . language )
2023-09-02 01:01:17 +02:00
2023-09-03 00:49:28 +02:00
// Load for emitting to client
libraryItem . media = await libraryItem . getMedia ( {
include : [
{
model : Database . authorModel ,
through : {
attributes : [ 'id' , 'createdAt' ]
}
} ,
{
model : Database . seriesModel ,
through : {
attributes : [ 'id' , 'sequence' , 'createdAt' ]
}
}
] ,
order : [
[ Database . authorModel , Database . bookAuthorModel , 'createdAt' , 'ASC' ] ,
[ Database . seriesModel , 'bookSeries' , 'createdAt' , 'ASC' ]
]
} )
2023-09-03 22:14:58 +02:00
await this . saveMetadataFile ( libraryItem , libraryScan )
if ( global . ServerSettings . storeMetadataWithItem && ! libraryItem . isFile ) {
libraryItem . changed ( 'libraryFiles' , true )
await libraryItem . save ( )
}
2023-09-02 01:01:17 +02:00
return libraryItem
}
/ * *
*
2023-09-03 00:49:28 +02:00
* @ param { import ( '../models/Book' ) . AudioFileObject [ ] } audioFiles
2024-01-08 00:51:07 +01:00
* @ param { import ( '../utils/parsers/parseEbookMetadata' ) . EBookFileScanData } ebookFileScanData
2023-09-02 01:01:17 +02:00
* @ param { import ( './LibraryItemScanData' ) } libraryItemData
2023-09-07 00:48:50 +02:00
* @ param { LibraryScan } libraryScan
2023-10-09 00:10:43 +02:00
* @ param { import ( '../models/Library' ) . LibrarySettingsObject } librarySettings
2023-09-29 00:23:52 +02:00
* @ param { string } [ existingLibraryItemId ]
2023-09-02 01:01:17 +02:00
* @ returns { Promise < BookMetadataObject > }
* /
2024-01-08 00:51:07 +01:00
async getBookMetadataFromScanData ( audioFiles , ebookFileScanData , libraryItemData , libraryScan , librarySettings , existingLibraryItemId = null ) {
2023-09-02 01:01:17 +02:00
// First set book metadata from folder/file names
const bookMetadata = {
2023-10-09 00:10:43 +02:00
title : libraryItemData . mediaMetadata . title , // required
titleIgnorePrefix : undefined ,
subtitle : undefined ,
publishedYear : undefined ,
2023-09-03 00:49:28 +02:00
publisher : undefined ,
description : undefined ,
isbn : undefined ,
asin : undefined ,
language : undefined ,
2023-10-09 00:10:43 +02:00
narrators : [ ] ,
2023-09-02 01:01:17 +02:00
genres : [ ] ,
tags : [ ] ,
2023-10-09 00:10:43 +02:00
authors : [ ] ,
2023-09-02 01:01:17 +02:00
series : [ ] ,
chapters : [ ] ,
2023-09-03 00:49:28 +02:00
explicit : undefined ,
abridged : undefined ,
coverPath : undefined
2023-09-02 01:01:17 +02:00
}
2023-09-29 00:23:52 +02:00
2024-01-08 00:51:07 +01:00
const bookMetadataSourceHandler = new BookScanner . BookMetadataSourceHandler ( bookMetadata , audioFiles , ebookFileScanData , libraryItemData , libraryScan , existingLibraryItemId )
2023-11-12 14:30:23 +01:00
const metadataPrecedence = librarySettings . metadataPrecedence || [ 'folderStructure' , 'audioMetatags' , 'nfoFile' , 'txtFiles' , 'opfFile' , 'absMetadata' ]
2023-10-09 23:41:43 +02:00
libraryScan . addLog ( LogLevel . DEBUG , ` " ${ bookMetadata . title } " Getting metadata with precedence [ ${ metadataPrecedence . join ( ', ' ) } ] ` )
for ( const metadataSource of metadataPrecedence ) {
2023-10-09 00:10:43 +02:00
if ( bookMetadataSourceHandler [ metadataSource ] ) {
await bookMetadataSourceHandler [ metadataSource ] ( )
2023-09-02 01:01:17 +02:00
} else {
2023-10-09 00:10:43 +02:00
libraryScan . addLog ( LogLevel . ERROR , ` Invalid metadata source " ${ metadataSource } " ` )
2023-09-02 01:01:17 +02:00
}
}
// Set cover from library file if one is found otherwise check audiofile
if ( libraryItemData . imageLibraryFiles . length ) {
const coverMatch = libraryItemData . imageLibraryFiles . find ( iFile => / \ / cover \ . [ ^ . \ / ] * $ / . test ( iFile . metadata . path ) )
bookMetadata . coverPath = coverMatch ? . metadata . path || libraryItemData . imageLibraryFiles [ 0 ] . metadata . path
}
2023-09-04 18:50:55 +02:00
bookMetadata . titleIgnorePrefix = getTitleIgnorePrefix ( bookMetadata . title )
2023-09-02 01:01:17 +02:00
return bookMetadata
}
2023-10-09 00:10:43 +02:00
static BookMetadataSourceHandler = class {
/ * *
*
* @ param { Object } bookMetadata
* @ param { import ( '../models/Book' ) . AudioFileObject [ ] } audioFiles
2024-01-08 00:51:07 +01:00
* @ param { import ( '../utils/parsers/parseEbookMetadata' ) . EBookFileScanData } ebookFileScanData
2023-10-09 00:10:43 +02:00
* @ param { import ( './LibraryItemScanData' ) } libraryItemData
* @ param { LibraryScan } libraryScan
* @ param { string } existingLibraryItemId
* /
2024-01-08 00:51:07 +01:00
constructor ( bookMetadata , audioFiles , ebookFileScanData , libraryItemData , libraryScan , existingLibraryItemId ) {
2023-10-09 00:10:43 +02:00
this . bookMetadata = bookMetadata
this . audioFiles = audioFiles
2024-01-08 00:51:07 +01:00
this . ebookFileScanData = ebookFileScanData
2023-10-09 00:10:43 +02:00
this . libraryItemData = libraryItemData
this . libraryScan = libraryScan
this . existingLibraryItemId = existingLibraryItemId
}
/ * *
* Metadata parsed from folder names / structure
* /
folderStructure ( ) {
this . libraryItemData . setBookMetadataFromFilenames ( this . bookMetadata )
}
/ * *
2024-01-08 00:51:07 +01:00
* Metadata from audio file meta tags OR metadata from ebook file
2023-10-09 00:10:43 +02:00
* /
audioMetatags ( ) {
2024-01-08 00:51:07 +01:00
if ( this . audioFiles . length ) {
// Modifies bookMetadata with metadata mapped from audio file meta tags
const bookTitle = this . bookMetadata . title || this . libraryItemData . mediaMetadata . title
AudioFileScanner . setBookMetadataFromAudioMetaTags ( bookTitle , this . audioFiles , this . bookMetadata , this . libraryScan )
} else if ( this . ebookFileScanData ) {
2024-01-15 00:51:26 +01:00
const ebookMetdataObject = this . ebookFileScanData . metadata || { }
2024-01-08 00:51:07 +01:00
for ( const key in ebookMetdataObject ) {
if ( key === 'tags' ) {
if ( ebookMetdataObject . tags . length ) {
this . bookMetadata . tags = ebookMetdataObject . tags
}
} else if ( key === 'genres' ) {
if ( ebookMetdataObject . genres . length ) {
this . bookMetadata . genres = ebookMetdataObject . genres
}
} else if ( key === 'authors' ) {
if ( ebookMetdataObject . authors ? . length ) {
this . bookMetadata . authors = ebookMetdataObject . authors
}
} else if ( key === 'narrators' ) {
if ( ebookMetdataObject . narrators ? . length ) {
this . bookMetadata . narrators = ebookMetdataObject . narrators
}
} else if ( key === 'series' ) {
if ( ebookMetdataObject . series ? . length ) {
this . bookMetadata . series = ebookMetdataObject . series
}
} else if ( ebookMetdataObject [ key ] && key !== 'sequence' ) {
this . bookMetadata [ key ] = ebookMetdataObject [ key ]
}
}
}
return null
2023-10-09 00:10:43 +02:00
}
2023-11-12 14:30:23 +01:00
/ * *
* Metadata from . nfo file
* /
async nfoFile ( ) {
if ( ! this . libraryItemData . metadataNfoLibraryFile ) return
await NfoFileScanner . scanBookNfoFile ( this . libraryItemData . metadataNfoLibraryFile , this . bookMetadata )
}
2023-12-24 18:53:57 +01:00
2023-10-09 00:10:43 +02:00
/ * *
* Description from desc . txt and narrator from reader . txt
* /
async txtFiles ( ) {
// If desc.txt in library item folder then use this for description
if ( this . libraryItemData . descTxtLibraryFile ) {
const description = await readTextFile ( this . libraryItemData . descTxtLibraryFile . metadata . path )
if ( description . trim ( ) ) this . bookMetadata . description = description . trim ( )
}
2023-09-02 01:01:17 +02:00
2023-10-09 00:10:43 +02:00
// If reader.txt in library item folder then use this for narrator
if ( this . libraryItemData . readerTxtLibraryFile ) {
let narrator = await readTextFile ( this . libraryItemData . readerTxtLibraryFile . metadata . path )
narrator = narrator . split ( /\r?\n/ ) [ 0 ] ? . trim ( ) || '' // Only use first line
if ( narrator ) {
this . bookMetadata . narrators = parseNameString . parse ( narrator ) ? . names || [ ]
}
2023-09-02 01:01:17 +02:00
}
}
2023-10-09 00:10:43 +02:00
/ * *
* Metadata from opf file
* /
async opfFile ( ) {
if ( ! this . libraryItemData . metadataOpfLibraryFile ) return
await OpfFileScanner . scanBookOpfFile ( this . libraryItemData . metadataOpfLibraryFile , this . bookMetadata )
}
2023-09-02 01:01:17 +02:00
2023-10-09 00:10:43 +02:00
/ * *
2023-10-22 22:53:05 +02:00
* Metadata from metadata . json
2023-10-09 00:10:43 +02:00
* /
async absMetadata ( ) {
2023-10-22 22:53:05 +02:00
// If metadata.json use this for metadata
2023-10-09 00:10:43 +02:00
await AbsMetadataFileScanner . scanBookMetadataFile ( this . libraryScan , this . libraryItemData , this . bookMetadata , this . existingLibraryItemId )
2023-09-02 01:01:17 +02:00
}
}
2023-09-03 22:14:58 +02:00
/ * *
*
* @ param { import ( '../models/LibraryItem' ) } libraryItem
2023-09-07 00:48:50 +02:00
* @ param { LibraryScan } libraryScan
2023-09-03 22:14:58 +02:00
* @ returns { Promise }
* /
async saveMetadataFile ( libraryItem , libraryScan ) {
let metadataPath = Path . join ( global . MetadataPath , 'items' , libraryItem . id )
let storeMetadataWithItem = global . ServerSettings . storeMetadataWithItem
if ( storeMetadataWithItem && ! libraryItem . isFile ) {
metadataPath = libraryItem . path
} else {
// Make sure metadata book dir exists
storeMetadataWithItem = false
await fsExtra . ensureDir ( metadataPath )
}
2023-10-22 22:53:05 +02:00
const metadataFilePath = Path . join ( metadataPath , ` metadata. ${ global . ServerSettings . metadataFileFormat } ` )
const jsonObject = {
tags : libraryItem . media . tags || [ ] ,
chapters : libraryItem . media . chapters ? . map ( c => ( { ... c } ) ) || [ ] ,
title : libraryItem . media . title ,
subtitle : libraryItem . media . subtitle ,
authors : libraryItem . media . authors . map ( a => a . name ) ,
narrators : libraryItem . media . narrators ,
series : libraryItem . media . series . map ( se => {
const sequence = se . bookSeries ? . sequence || ''
if ( ! sequence ) return se . name
return ` ${ se . name } # ${ sequence } `
} ) ,
genres : libraryItem . media . genres || [ ] ,
publishedYear : libraryItem . media . publishedYear ,
publishedDate : libraryItem . media . publishedDate ,
publisher : libraryItem . media . publisher ,
description : libraryItem . media . description ,
isbn : libraryItem . media . isbn ,
asin : libraryItem . media . asin ,
language : libraryItem . media . language ,
explicit : ! ! libraryItem . media . explicit ,
abridged : ! ! libraryItem . media . abridged
}
return fsExtra . writeFile ( metadataFilePath , JSON . stringify ( jsonObject , null , 2 ) ) . then ( async ( ) => {
// Add metadata.json to libraryFiles array if it is new
let metadataLibraryFile = libraryItem . libraryFiles . find ( lf => lf . metadata . path === filePathToPOSIX ( metadataFilePath ) )
if ( storeMetadataWithItem ) {
if ( ! metadataLibraryFile ) {
const newLibraryFile = new LibraryFile ( )
await newLibraryFile . setDataFromPath ( metadataFilePath , ` metadata.json ` )
metadataLibraryFile = newLibraryFile . toJSON ( )
libraryItem . libraryFiles . push ( metadataLibraryFile )
} else {
const fileTimestamps = await getFileTimestampsWithIno ( metadataFilePath )
if ( fileTimestamps ) {
metadataLibraryFile . metadata . mtimeMs = fileTimestamps . mtimeMs
metadataLibraryFile . metadata . ctimeMs = fileTimestamps . ctimeMs
metadataLibraryFile . metadata . size = fileTimestamps . size
metadataLibraryFile . ino = fileTimestamps . ino
2023-09-03 22:14:58 +02:00
}
}
2023-10-22 22:53:05 +02:00
const libraryItemDirTimestamps = await getFileTimestampsWithIno ( libraryItem . path )
if ( libraryItemDirTimestamps ) {
libraryItem . mtime = libraryItemDirTimestamps . mtimeMs
libraryItem . ctime = libraryItemDirTimestamps . ctimeMs
let size = 0
libraryItem . libraryFiles . forEach ( ( lf ) => size += ( ! isNaN ( lf . metadata . size ) ? Number ( lf . metadata . size ) : 0 ) )
libraryItem . size = size
}
2023-09-03 22:14:58 +02:00
}
2023-10-22 22:53:05 +02:00
libraryScan . addLog ( LogLevel . DEBUG , ` Success saving abmetadata to " ${ metadataFilePath } " ` )
2023-09-03 22:14:58 +02:00
2023-10-22 22:53:05 +02:00
return metadataLibraryFile
} ) . catch ( ( error ) => {
libraryScan . addLog ( LogLevel . ERROR , ` Failed to save json file at " ${ metadataFilePath } " ` , error )
return null
} )
2023-09-03 22:14:58 +02:00
}
2023-09-04 00:51:58 +02:00
/ * *
* Check authors that were removed from a book and remove them if they no longer have any books
* keep authors without books that have a asin , description or imagePath
* @ param { string } libraryId
* @ param { import ( './ScanLogger' ) } scanLogger
* @ returns { Promise }
* /
async checkAuthorsRemovedFromBooks ( libraryId , scanLogger ) {
const bookAuthorsToRemove = ( await Database . authorModel . findAll ( {
where : [
{
id : scanLogger . authorsRemovedFromBooks ,
asin : {
[ sequelize . Op . or ] : [ null , "" ]
} ,
description : {
[ sequelize . Op . or ] : [ null , "" ]
} ,
imagePath : {
[ sequelize . Op . or ] : [ null , "" ]
}
} ,
sequelize . where ( sequelize . literal ( '(SELECT count(*) FROM bookAuthors ba WHERE ba.authorId = author.id)' ) , 0 )
] ,
attributes : [ 'id' ] ,
raw : true
} ) ) . map ( au => au . id )
if ( bookAuthorsToRemove . length ) {
await Database . authorModel . destroy ( {
where : {
id : bookAuthorsToRemove
}
} )
bookAuthorsToRemove . forEach ( ( authorId ) => {
Database . removeAuthorFromFilterData ( libraryId , authorId )
// TODO: Clients were expecting full author in payload but its unnecessary
SocketAuthority . emitter ( 'author_removed' , { id : authorId , libraryId } )
} )
scanLogger . addLog ( LogLevel . INFO , ` Removed ${ bookAuthorsToRemove . length } authors ` )
}
}
/ * *
* Check series that were removed from books and remove them if they no longer have any books
* @ param { string } libraryId
* @ param { import ( './ScanLogger' ) } scanLogger
* @ returns { Promise }
* /
async checkSeriesRemovedFromBooks ( libraryId , scanLogger ) {
const bookSeriesToRemove = ( await Database . seriesModel . findAll ( {
where : [
{
id : scanLogger . seriesRemovedFromBooks
} ,
sequelize . where ( sequelize . literal ( '(SELECT count(*) FROM bookSeries bs WHERE bs.seriesId = series.id)' ) , 0 )
] ,
attributes : [ 'id' ] ,
raw : true
} ) ) . map ( se => se . id )
if ( bookSeriesToRemove . length ) {
await Database . seriesModel . destroy ( {
where : {
id : bookSeriesToRemove
}
} )
bookSeriesToRemove . forEach ( ( seriesId ) => {
Database . removeSeriesFromFilterData ( libraryId , seriesId )
SocketAuthority . emitter ( 'series_removed' , { id : seriesId , libraryId } )
} )
scanLogger . addLog ( LogLevel . INFO , ` Removed ${ bookSeriesToRemove . length } series ` )
}
}
2023-09-04 23:33:55 +02:00
2023-09-07 00:48:50 +02:00
/ * *
* Search cover provider for matching cover
* @ param { string } libraryItemId
* @ param { string } libraryItemPath null if book isFile
* @ param { string } title
* @ param { string } author
* @ param { LibraryScan } libraryScan
* @ returns { Promise < string > } path to downloaded cover or null if no cover found
* /
async searchForCover ( libraryItemId , libraryItemPath , title , author , libraryScan ) {
const options = {
titleDistance : 2 ,
authorDistance : 2
}
const results = await BookFinder . findCovers ( Database . serverSettings . scannerCoverProvider , title , author , options )
if ( results . length ) {
libraryScan . addLog ( LogLevel . DEBUG , ` Found best cover for " ${ title } " ` )
// If the first cover result fails, attempt to download the second
for ( let i = 0 ; i < results . length && i < 2 ; i ++ ) {
// Downloads and updates the book cover
const result = await CoverManager . downloadCoverFromUrlNew ( results [ i ] , libraryItemId , libraryItemPath )
if ( result . error ) {
2023-10-01 16:03:01 +02:00
libraryScan . addLog ( LogLevel . ERROR , ` Failed to download cover from url " ${ results [ i ] } " | Attempt ${ i + 1 } ` , result . error )
2023-09-07 00:48:50 +02:00
} else if ( result . cover ) {
return result . cover
}
}
}
return null
}
2023-09-02 01:01:17 +02:00
}
module . exports = new BookScanner ( )