Add:Parse abmetadata metadata.abs file, save metadata.abs on all audiobook insert/update

This commit is contained in:
advplyr 2022-02-27 16:14:57 -06:00
parent 295c6b0c74
commit 779d22bf55
4 changed files with 131 additions and 18 deletions

View File

@ -172,7 +172,15 @@ class Db {
}
}
updateAudiobook(audiobook) {
async updateAudiobook(audiobook) {
if (audiobook && audiobook.saveAbMetadata) {
// TODO: Book may have updates where this save is not necessary
// add check first if metadata update is needed
await audiobook.saveAbMetadata()
} else {
Logger.error(`[Db] Invalid audiobook object passed to updateAudiobook`, audiobook)
}
return this.audiobooksDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => {
Logger.debug(`[DB] Audiobook updated ${results.updated}`)
return true
@ -182,6 +190,27 @@ class Db {
})
}
insertAudiobook(audiobook) {
return this.insertAudiobooks([audiobook])
}
async insertAudiobooks(audiobooks) {
// TODO: Books may have updates where this save is not necessary
// add check first if metadata update is needed
await Promise.all(audiobooks.map(async (ab) => {
if (ab && ab.saveAbMetadata) return ab.saveAbMetadata()
return null
}))
return this.audiobooksDb.insert(audiobooks).then((results) => {
Logger.debug(`[DB] Audiobooks inserted ${results.updated}`)
return true
}).catch((error) => {
Logger.error(`[DB] Audiobooks insert failed ${error}`)
return false
})
}
updateUserStream(userId, streamId) {
return this.usersDb.update((record) => record.id === userId, (user) => {
user.stream = streamId

View File

@ -428,10 +428,6 @@ class Audiobook {
if (payload.book && this.book.update(payload.book)) {
hasUpdates = true
// TODO: Book may have updates where this save is not necessary
// add check first if metadata update is needed
this.saveAbMetadata()
}
if (hasUpdates) {
@ -526,12 +522,13 @@ class Audiobook {
}
// On scan check other files found with other files saved
async syncOtherFiles(newOtherFiles, opfMetadataOverrideDetails, forceRescan = false) {
async syncOtherFiles(newOtherFiles, opfMetadataOverrideDetails) {
var hasUpdates = false
var currOtherFileNum = this.otherFiles.length
var otherFilenamesAlreadyInBook = this.otherFiles.map(ofile => ofile.filename)
var alreadyHasAbsMetadata = otherFilenamesAlreadyInBook.includes('metadata.abs')
var alreadyHasDescTxt = otherFilenamesAlreadyInBook.includes('desc.txt')
var alreadyHasReaderTxt = otherFilenamesAlreadyInBook.includes('reader.txt')
@ -543,9 +540,9 @@ class Audiobook {
hasUpdates = true
}
// If desc.txt is new or forcing rescan then read it and update description (will overwrite)
// If desc.txt is new then read it and update description (will overwrite)
var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')
if (descriptionTxt && (!alreadyHasDescTxt || forceRescan)) {
if (descriptionTxt && !alreadyHasDescTxt) {
var newDescription = await readTextFile(descriptionTxt.fullPath)
if (newDescription) {
Logger.debug(`[Audiobook] Sync Other File desc.txt: ${newDescription}`)
@ -553,9 +550,9 @@ class Audiobook {
hasUpdates = true
}
}
// If reader.txt is new or forcing rescan then read it and update narrator (will overwrite)
// If reader.txt is new then read it and update narrator (will overwrite)
var readerTxt = newOtherFiles.find(file => file.filename === 'reader.txt')
if (readerTxt && (!alreadyHasReaderTxt || forceRescan)) {
if (readerTxt && !alreadyHasReaderTxt) {
var newReader = await readTextFile(readerTxt.fullPath)
if (newReader) {
Logger.debug(`[Audiobook] Sync Other File reader.txt: ${newReader}`)
@ -564,7 +561,24 @@ class Audiobook {
}
}
// If OPF file and was not already there
// If metadata.abs is new then read it and set all defined keys (will overwrite)
var metadataAbs = newOtherFiles.find(file => file.filename === 'metadata.abs')
if (metadataAbs && !alreadyHasAbsMetadata) {
var abmetadataText = await readTextFile(metadataAbs.fullPath)
if (abmetadataText) {
var metadataUpdateObject = abmetadataGenerator.parse(abmetadataText)
if (metadataUpdateObject && metadataUpdateObject.book) {
Logger.debug(`[Audiobook] Updating book "${this.title}" details from metadata.abs file`, metadataUpdateObject)
if (this.update(metadataUpdateObject)) {
Logger.debug(`[Audiobook] Some details were updated from metadata.abs for "${this.title}"`)
hasUpdates = true
}
}
}
}
// If OPF file and was not already there OR prefer opf metadata
var metadataOpf = newOtherFiles.find(file => file.ext === '.opf' || file.filename === 'metadata.xml')
if (metadataOpf && (!otherFilenamesAlreadyInBook.includes(metadataOpf.filename) || opfMetadataOverrideDetails)) {
var xmlText = await readTextFile(metadataOpf.fullPath)
@ -800,9 +814,10 @@ class Audiobook {
return false
}
// Look for desc.txt and reader.txt and update details if found
// Look for desc.txt, reader.txt, metadata.abs and opf file then update details if found
async saveDataFromTextFiles(opfMetadataOverrideDetails) {
var bookUpdatePayload = {}
var descriptionText = await this.fetchTextFromTextFile('desc.txt')
if (descriptionText) {
Logger.debug(`[Audiobook] "${this.title}" found desc.txt updating description with "${descriptionText.slice(0, 20)}..."`)
@ -814,6 +829,22 @@ class Audiobook {
bookUpdatePayload.narrator = readerText
}
// abmetadata will always overwrite
var abmetadataText = await this.fetchTextFromTextFile('metadata.abs')
if (abmetadataText) {
var metadataUpdateObject = abmetadataGenerator.parse(abmetadataText)
if (metadataUpdateObject && metadataUpdateObject.book) {
Logger.debug(`[Audiobook] "${this.title}" found book details from metadata.abs file`, metadataUpdateObject)
for (const key in metadataUpdateObject.book) {
var value = metadataUpdateObject.book[key]
if (key && value !== undefined) {
bookUpdatePayload[key] = value
}
}
}
}
// Opf only overwrites if detail is empty
var metadataOpf = this.otherFiles.find(file => file.isOPFFile || file.filename === 'metadata.xml')
if (metadataOpf) {
var xmlText = await readTextFile(metadataOpf.fullPath)
@ -1048,6 +1079,7 @@ class Audiobook {
metadataPath = Path.join(metadataPath, 'metadata.abs')
return abmetadataGenerator.generate(this, metadataPath).then((success) => {
this.isSavingMetadata = false
if (!success) Logger.error(`[Audiobook] Failed saving abmetadata to "${metadataPath}"`)
else Logger.debug(`[Audiobook] Success saving abmetadata to "${metadataPath}"`)
return success

View File

@ -117,7 +117,7 @@ class Scanner {
if (hasUpdated) {
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
await this.db.updateEntity('audiobook', audiobook)
await this.db.updateAudiobook(audiobook)
return ScanResult.UPDATED
}
return ScanResult.UPTODATE
@ -314,7 +314,7 @@ class Scanner {
}))
newAudiobooks = newAudiobooks.filter(ab => ab) // Filter out nulls
libraryScan.resultsAdded += newAudiobooks.length
await this.db.insertEntities('audiobook', newAudiobooks)
await this.db.insertAudiobooks(newAudiobooks)
this.emitter('audiobooks_added', newAudiobooks.map(ab => ab.toJSONExpanded()))
}
@ -522,7 +522,7 @@ class Scanner {
Logger.debug(`[Scanner] Folder update group must be a new book "${bookDir}" in library "${library.name}"`)
var newAudiobook = await this.scanPotentialNewAudiobook(folder, fullPath)
if (newAudiobook) {
await this.db.insertEntity('audiobook', newAudiobook)
await this.db.insertAudiobook(newAudiobook)
this.emitter('audiobook_added', newAudiobook.toJSONExpanded())
}
bookGroupingResults[bookDir] = newAudiobook ? ScanResult.ADDED : ScanResult.NOTHING
@ -612,7 +612,7 @@ class Scanner {
}
Logger.warn('Found duplicate ID - updating from', ab.id, 'to', abCopy.id)
await this.db.removeEntity('audiobook', ab.id)
await this.db.insertEntity('audiobook', abCopy)
await this.db.insertAudiobook(abCopy)
audiobooksUpdated++
} else {
ids[ab.id] = true
@ -665,7 +665,7 @@ class Scanner {
}
if (hasUpdated) {
await this.db.updateEntity('audiobook', audiobook)
await this.db.updateAudiobook(audiobook)
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
}

View File

@ -45,4 +45,56 @@ function generate(audiobook, outputPath) {
return false
})
}
module.exports.generate = generate
module.exports.generate = generate
function parseAbMetadataText(text) {
if (!text) return null
var lines = text.split(/\r?\n/)
// Check first line and get abmetadata version number
var firstLine = lines.shift().toLowerCase()
if (!firstLine.startsWith(';abmetadata')) {
Logger.error(`Invalid abmetadata file first line is not ;abmetadata "${firstLine}"`)
return null
}
var abmetadataVersion = Number(firstLine.replace(';abmetadata', '').trim())
if (isNaN(abmetadataVersion)) {
Logger.warn(`Invalid abmetadata version ${abmetadataVersion} - using 1`)
abmetadataVersion = 1
}
// Remove comments and empty lines
const ignoreFirstChars = [' ', '#', ';'] // Ignore any line starting with the following
lines = lines.filter(line => !!line.trim() && !ignoreFirstChars.includes(line[0]))
// Get lines that map to book details (all lines before the first chapter section)
var firstSectionLine = lines.findIndex(l => l.startsWith('['))
var detailLines = firstSectionLine > 0 ? lines.slice(0, firstSectionLine) : lines
// Put valid book detail values into map
const bookDetails = {}
for (let i = 0; i < detailLines.length; i++) {
var line = detailLines[i]
var keyValue = line.split('=')
if (keyValue.length < 2) {
Logger.warn('abmetadata invalid line has no =', line)
} else if (!bookKeyMap[keyValue[0].trim()]) {
Logger.warn(`abmetadata key "${keyValue[0].trim()}" is not a valid book detail key`)
} else {
var key = keyValue[0].trim()
bookDetails[key] = keyValue[1].trim()
// Genres convert to array of strings
if (key === 'genres' && bookDetails[key]) {
bookDetails[key] = bookDetails[key].split(',').map(genre => genre.trim())
}
}
}
// TODO: Chapter support
return {
book: bookDetails
}
}
module.exports.parse = parseAbMetadataText