mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-26 16:48:53 +01:00
Add:Parse abmetadata metadata.abs file, save metadata.abs on all audiobook insert/update
This commit is contained in:
parent
295c6b0c74
commit
779d22bf55
31
server/Db.js
31
server/Db.js
@ -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
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
}
|
||||
|
||||
|
@ -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
|
Loading…
Reference in New Issue
Block a user