Change: scanner uses any .opf file, use description if plain text, use genres #141, Add: language book detail

This commit is contained in:
advplyr 2021-11-09 17:54:28 -06:00
parent 3eb0dc9ac3
commit 7141f70aa5
6 changed files with 76 additions and 37 deletions

View File

@ -194,6 +194,14 @@ class Scanner {
} }
}) })
// Sync other files (all files that are not audio files) - Updates cover path
var hasOtherFileUpdates = false
var otherFilesUpdated = await existingAudiobook.syncOtherFiles(audiobookData.otherFiles, this.MetadataPath, forceAudioFileScan)
if (otherFilesUpdated) {
hasOtherFileUpdates = true
}
// Rescan audio file metadata // Rescan audio file metadata
if (forceAudioFileScan) { if (forceAudioFileScan) {
Logger.info(`[Scanner] Rescanning ${existingAudiobook.audioFiles.length} audio files for "${existingAudiobook.title}"`) Logger.info(`[Scanner] Rescanning ${existingAudiobook.audioFiles.length} audio files for "${existingAudiobook.title}"`)
@ -240,7 +248,7 @@ class Scanner {
return ScanResult.UPDATED return ScanResult.UPDATED
} }
var hasUpdates = hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles var hasUpdates = hasOtherFileUpdates || hasUpdatedIno || hasUpdatedLibraryOrFolder || removedAudioFiles.length || removedAudioTracks.length || newAudioFiles.length || hasUpdatedAudioFiles
// Check that audio tracks are in sequential order with no gaps // Check that audio tracks are in sequential order with no gaps
if (existingAudiobook.checkUpdateMissingParts()) { if (existingAudiobook.checkUpdateMissingParts()) {
@ -248,12 +256,6 @@ class Scanner {
hasUpdates = true hasUpdates = true
} }
// Sync other files (all files that are not audio files) - Updates cover path
var otherFilesUpdated = await existingAudiobook.syncOtherFiles(audiobookData.otherFiles, this.MetadataPath, forceAudioFileScan)
if (otherFilesUpdated) {
hasUpdates = true
}
// Syncs path and fullPath // Syncs path and fullPath
if (existingAudiobook.syncPaths(audiobookData)) { if (existingAudiobook.syncPaths(audiobookData)) {
hasUpdates = true hasUpdates = true

View File

@ -502,7 +502,6 @@ class Audiobook {
var alreadyHasDescTxt = this.otherFiles.find(of => of.filename === 'desc.txt') var alreadyHasDescTxt = this.otherFiles.find(of => of.filename === 'desc.txt')
var alreadyHasReaderTxt = this.otherFiles.find(of => of.filename === 'reader.txt') var alreadyHasReaderTxt = this.otherFiles.find(of => of.filename === 'reader.txt')
var alreadyHasMetadataOpf = this.otherFiles.find(of => of.filename === 'metadata.opf')
var newOtherFilePaths = newOtherFiles.map(f => f.path) var newOtherFilePaths = newOtherFiles.map(f => f.path)
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path)) this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
@ -533,21 +532,27 @@ class Audiobook {
hasUpdates = true hasUpdates = true
} }
} }
var metadataOpf = newOtherFiles.find(file => file.filename === 'metadata.opf' || file.filename === 'metadata.xml')
if (metadataOpf && (!alreadyHasMetadataOpf || forceRescan)) { var metadataOpf = newOtherFiles.find(file => file.ext === '.opf' || file.filename === 'metadata.xml')
if (metadataOpf) {
var xmlText = await readTextFile(metadataOpf.fullPath) var xmlText = await readTextFile(metadataOpf.fullPath)
if (xmlText) { if (xmlText) {
var opfMetadata = await parseOpfMetadataXML(xmlText) var opfMetadata = await parseOpfMetadataXML(xmlText)
Logger.debug(`[Audiobook] Sync Other File ${metadataOpf.filename} parsed:`, opfMetadata) Logger.debug(`[Audiobook] Sync Other File "${metadataOpf.filename}" parsed:`, opfMetadata)
if (opfMetadata) { if (opfMetadata) {
const bookUpdatePayload = {} const bookUpdatePayload = {}
for (const key in opfMetadata) { for (const key in opfMetadata) {
if (opfMetadata[key] && !this.book[key]) { // Add genres only if genres are empty
if (key === 'genres') {
if (opfMetadata.genres.length && !this.book._genres.length) {
bookUpdatePayload[key] = opfMetadata.genres
}
} else if (opfMetadata[key] && !this.book[key]) {
bookUpdatePayload[key] = opfMetadata[key] bookUpdatePayload[key] = opfMetadata[key]
} }
} }
if (Object.keys(bookUpdatePayload).length) { if (Object.keys(bookUpdatePayload).length) {
Logger.debug(`[Audiobook] Using data found in metadata opf/xml`, bookUpdatePayload) Logger.debug(`[Audiobook] Using data found in OPF "${metadataOpf.filename}"`, bookUpdatePayload)
this.update({ book: bookUpdatePayload }) this.update({ book: bookUpdatePayload })
hasUpdates = true hasUpdates = true
} }
@ -778,15 +783,20 @@ class Audiobook {
bookUpdatePayload.narrator = readerText bookUpdatePayload.narrator = readerText
} }
var metadataOpf = this.otherFiles.find(file => file.filename === 'metadata.opf' || file.filename === 'metadata.xml') var metadataOpf = this.otherFiles.find(file => file.isOPFFile || file.filename === 'metadata.xml')
if (metadataOpf) { if (metadataOpf) {
var xmlText = await readTextFile(metadataOpf.fullPath) var xmlText = await readTextFile(metadataOpf.fullPath)
if (xmlText) { if (xmlText) {
var opfMetadata = await parseOpfMetadataXML(xmlText) var opfMetadata = await parseOpfMetadataXML(xmlText)
Logger.debug(`[Audiobook] "${this.title}" found ${metadataOpf.filename} parsed:`, opfMetadata) Logger.debug(`[Audiobook] "${this.title}" found "${metadataOpf.filename}" parsed:`, opfMetadata)
if (opfMetadata) { if (opfMetadata) {
for (const key in opfMetadata) { for (const key in opfMetadata) {
if (opfMetadata[key] && !this.book[key] && !bookUpdatePayload[key]) { // Add genres only if genres are empty
if (key === 'genres') {
if (opfMetadata.genres.length && !this.book._genres.length) {
bookUpdatePayload[key] = opfMetadata.genres
}
} else if (opfMetadata[key] && !this.book[key] && !bookUpdatePayload[key]) {
bookUpdatePayload[key] = opfMetadata[key] bookUpdatePayload[key] = opfMetadata[key]
} }
} }

View File

@ -13,6 +13,10 @@ class AudiobookFile {
} }
} }
get isOPFFile() {
return this.ext ? this.ext.toLowerCase() === '.opf' : false
}
toJSON() { toJSON() {
return { return {
ino: this.ino || null, ino: this.ino || null,

View File

@ -16,6 +16,7 @@ class Book {
this.publisher = null this.publisher = null
this.description = null this.description = null
this.isbn = null this.isbn = null
this.langauge = null
this.cover = null this.cover = null
this.coverFullPath = null this.coverFullPath = null
this.genres = [] this.genres = []
@ -38,6 +39,7 @@ class Book {
get _author() { return this.authorFL || '' } get _author() { return this.authorFL || '' }
get _series() { return this.series || '' } get _series() { return this.series || '' }
get _authorsList() { return this._author.split(', ') } get _authorsList() { return this._author.split(', ') }
get _genres() { return this.genres || [] }
get shouldSearchForCover() { get shouldSearchForCover() {
if (this.authorFL !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true if (this.authorFL !== this.lastCoverSearchAuthor || this.title !== this.lastCoverSearchTitle || !this.lastCoverSearch) return true
@ -58,6 +60,7 @@ class Book {
this.publisher = book.publisher this.publisher = book.publisher
this.description = book.description this.description = book.description
this.isbn = book.isbn || null this.isbn = book.isbn || null
this.language = book.language || null
this.cover = book.cover this.cover = book.cover
this.coverFullPath = book.coverFullPath || null this.coverFullPath = book.coverFullPath || null
this.genres = book.genres this.genres = book.genres
@ -81,6 +84,7 @@ class Book {
publisher: this.publisher, publisher: this.publisher,
description: this.description, description: this.description,
isbn: this.isbn, isbn: this.isbn,
language: this.language,
cover: this.cover, cover: this.cover,
coverFullPath: this.coverFullPath, coverFullPath: this.coverFullPath,
genres: this.genres, genres: this.genres,
@ -120,6 +124,7 @@ class Book {
this.publishYear = data.publishYear || null this.publishYear = data.publishYear || null
this.description = data.description || null this.description = data.description || null
this.isbn = data.isbn || null this.isbn = data.isbn || null
this.language = data.language || null
this.cover = data.cover || null this.cover = data.cover || null
this.coverFullPath = data.coverFullPath || null this.coverFullPath = data.coverFullPath || null
this.genres = data.genres || [] this.genres = data.genres || []

View File

@ -20,20 +20,23 @@ function fetchCreator(creators, role) {
return creator ? creator.value : null return creator ? creator.value : null
} }
function fetchTagString(metadata, tag) {
if (!metadata[tag] || !metadata[tag].length) return null
var tag = metadata[tag][0]
if (typeof tag !== 'string') return null
return tag
}
function fetchDate(metadata) { function fetchDate(metadata) {
if (!metadata['dc:date']) return null var date = fetchTagString(metadata, 'dc:date')
var dates = metadata['dc:date'] if (!date) return null
if (!dates.length || typeof dates[0] !== 'string') return null var dateSplit = date.split('-')
var dateSplit = dates[0].split('-')
if (!dateSplit.length || dateSplit[0].length !== 4 || isNaN(dateSplit[0])) return null if (!dateSplit.length || dateSplit[0].length !== 4 || isNaN(dateSplit[0])) return null
return dateSplit[0] return dateSplit[0]
} }
function fetchPublisher(metadata) { function fetchPublisher(metadata) {
if (!metadata['dc:publisher']) return null return fetchTagString(metadata, 'dc:publisher')
var publishers = metadata['dc:publisher']
if (!publishers.length || typeof publishers[0] !== 'string') return null
return publishers[0]
} }
function fetchISBN(metadata) { function fetchISBN(metadata) {
@ -44,22 +47,33 @@ function fetchISBN(metadata) {
} }
function fetchTitle(metadata) { function fetchTitle(metadata) {
if (!metadata['dc:title']) return null return fetchTagString(metadata, 'dc:title')
var titles = metadata['dc:title'] }
if (!titles.length) return null
if (typeof titles[0] === 'string') { function fetchDescription(metadata) {
return titles[0] var description = fetchTagString(metadata, 'dc:description')
} if (!description) return null
if (titles[0]['_']) { // check if description is HTML or plain text. only plain text allowed
return titles[0]['_'] // calibre stores < and > as &lt; and &gt;
} description = description.replace(/&lt;/g, '<').replace(/&gt;/g, '>')
return null if (description.match(/<!DOCTYPE html>|<\/?\s*[a-z-][^>]*\s*>|(\&(?:[\w\d]+|#\d+|#x[a-f\d]+);)/)) return null
return description
}
function fetchGenres(metadata) {
if (!metadata['dc:subject'] || !metadata['dc:subject'].length) return []
return metadata['dc:subject'].map(g => typeof g === 'string' ? g : null).filter(g => !!g)
}
function fetchLanguage(metadata) {
return fetchTagString(metadata, 'dc:language')
} }
module.exports.parseOpfMetadataXML = async (xml) => { module.exports.parseOpfMetadataXML = async (xml) => {
var json = await xmlToJSON(xml) var json = await xmlToJSON(xml)
if (!json || !json.package || !json.package.metadata) return null if (!json || !json.package || !json.package.metadata) return null
var metadata = json.package.metadata var metadata = json.package.metadata
if (Array.isArray(metadata)) { if (Array.isArray(metadata)) {
if (!metadata.length) return null if (!metadata.length) return null
metadata = metadata[0] metadata = metadata[0]
@ -72,7 +86,10 @@ module.exports.parseOpfMetadataXML = async (xml) => {
narrator: fetchCreator(creators, 'nrt'), narrator: fetchCreator(creators, 'nrt'),
publishYear: fetchDate(metadata), publishYear: fetchDate(metadata),
publisher: fetchPublisher(metadata), publisher: fetchPublisher(metadata),
isbn: fetchISBN(metadata) isbn: fetchISBN(metadata),
description: fetchDescription(metadata),
genres: fetchGenres(metadata),
language: fetchLanguage(metadata)
} }
return data return data
} }

View File

@ -130,10 +130,11 @@ function getFileType(ext) {
var ext_cleaned = ext.toLowerCase() var ext_cleaned = ext.toLowerCase()
if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1) if (ext_cleaned.startsWith('.')) ext_cleaned = ext_cleaned.slice(1)
if (globals.SupportedAudioTypes.includes(ext_cleaned)) return 'audio' if (globals.SupportedAudioTypes.includes(ext_cleaned)) return 'audio'
if (ext_cleaned === 'nfo') return 'info'
if (ext_cleaned === 'txt') return 'text'
if (globals.SupportedImageTypes.includes(ext_cleaned)) return 'image' if (globals.SupportedImageTypes.includes(ext_cleaned)) return 'image'
if (globals.SupportedEbookTypes.includes(ext_cleaned)) return 'ebook' if (globals.SupportedEbookTypes.includes(ext_cleaned)) return 'ebook'
if (ext_cleaned === 'nfo') return 'info'
if (ext_cleaned === 'txt') return 'text'
if (ext_cleaned === 'opf') return 'opf'
return 'unknown' return 'unknown'
} }