mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-27 09:08:57 +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) => {
|
return this.audiobooksDb.update((record) => record.id === audiobook.id, () => audiobook).then((results) => {
|
||||||
Logger.debug(`[DB] Audiobook updated ${results.updated}`)
|
Logger.debug(`[DB] Audiobook updated ${results.updated}`)
|
||||||
return true
|
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) {
|
updateUserStream(userId, streamId) {
|
||||||
return this.usersDb.update((record) => record.id === userId, (user) => {
|
return this.usersDb.update((record) => record.id === userId, (user) => {
|
||||||
user.stream = streamId
|
user.stream = streamId
|
||||||
|
@ -428,10 +428,6 @@ class Audiobook {
|
|||||||
|
|
||||||
if (payload.book && this.book.update(payload.book)) {
|
if (payload.book && this.book.update(payload.book)) {
|
||||||
hasUpdates = true
|
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) {
|
if (hasUpdates) {
|
||||||
@ -526,12 +522,13 @@ class Audiobook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// On scan check other files found with other files saved
|
// On scan check other files found with other files saved
|
||||||
async syncOtherFiles(newOtherFiles, opfMetadataOverrideDetails, forceRescan = false) {
|
async syncOtherFiles(newOtherFiles, opfMetadataOverrideDetails) {
|
||||||
var hasUpdates = false
|
var hasUpdates = false
|
||||||
|
|
||||||
var currOtherFileNum = this.otherFiles.length
|
var currOtherFileNum = this.otherFiles.length
|
||||||
|
|
||||||
var otherFilenamesAlreadyInBook = this.otherFiles.map(ofile => ofile.filename)
|
var otherFilenamesAlreadyInBook = this.otherFiles.map(ofile => ofile.filename)
|
||||||
|
var alreadyHasAbsMetadata = otherFilenamesAlreadyInBook.includes('metadata.abs')
|
||||||
var alreadyHasDescTxt = otherFilenamesAlreadyInBook.includes('desc.txt')
|
var alreadyHasDescTxt = otherFilenamesAlreadyInBook.includes('desc.txt')
|
||||||
var alreadyHasReaderTxt = otherFilenamesAlreadyInBook.includes('reader.txt')
|
var alreadyHasReaderTxt = otherFilenamesAlreadyInBook.includes('reader.txt')
|
||||||
|
|
||||||
@ -543,9 +540,9 @@ class Audiobook {
|
|||||||
hasUpdates = true
|
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')
|
var descriptionTxt = newOtherFiles.find(file => file.filename === 'desc.txt')
|
||||||
if (descriptionTxt && (!alreadyHasDescTxt || forceRescan)) {
|
if (descriptionTxt && !alreadyHasDescTxt) {
|
||||||
var newDescription = await readTextFile(descriptionTxt.fullPath)
|
var newDescription = await readTextFile(descriptionTxt.fullPath)
|
||||||
if (newDescription) {
|
if (newDescription) {
|
||||||
Logger.debug(`[Audiobook] Sync Other File desc.txt: ${newDescription}`)
|
Logger.debug(`[Audiobook] Sync Other File desc.txt: ${newDescription}`)
|
||||||
@ -553,9 +550,9 @@ class Audiobook {
|
|||||||
hasUpdates = true
|
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')
|
var readerTxt = newOtherFiles.find(file => file.filename === 'reader.txt')
|
||||||
if (readerTxt && (!alreadyHasReaderTxt || forceRescan)) {
|
if (readerTxt && !alreadyHasReaderTxt) {
|
||||||
var newReader = await readTextFile(readerTxt.fullPath)
|
var newReader = await readTextFile(readerTxt.fullPath)
|
||||||
if (newReader) {
|
if (newReader) {
|
||||||
Logger.debug(`[Audiobook] Sync Other File reader.txt: ${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')
|
var metadataOpf = newOtherFiles.find(file => file.ext === '.opf' || file.filename === 'metadata.xml')
|
||||||
if (metadataOpf && (!otherFilenamesAlreadyInBook.includes(metadataOpf.filename) || opfMetadataOverrideDetails)) {
|
if (metadataOpf && (!otherFilenamesAlreadyInBook.includes(metadataOpf.filename) || opfMetadataOverrideDetails)) {
|
||||||
var xmlText = await readTextFile(metadataOpf.fullPath)
|
var xmlText = await readTextFile(metadataOpf.fullPath)
|
||||||
@ -800,9 +814,10 @@ class Audiobook {
|
|||||||
return false
|
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) {
|
async saveDataFromTextFiles(opfMetadataOverrideDetails) {
|
||||||
var bookUpdatePayload = {}
|
var bookUpdatePayload = {}
|
||||||
|
|
||||||
var descriptionText = await this.fetchTextFromTextFile('desc.txt')
|
var descriptionText = await this.fetchTextFromTextFile('desc.txt')
|
||||||
if (descriptionText) {
|
if (descriptionText) {
|
||||||
Logger.debug(`[Audiobook] "${this.title}" found desc.txt updating description with "${descriptionText.slice(0, 20)}..."`)
|
Logger.debug(`[Audiobook] "${this.title}" found desc.txt updating description with "${descriptionText.slice(0, 20)}..."`)
|
||||||
@ -814,6 +829,22 @@ class Audiobook {
|
|||||||
bookUpdatePayload.narrator = readerText
|
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')
|
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)
|
||||||
@ -1048,6 +1079,7 @@ class Audiobook {
|
|||||||
metadataPath = Path.join(metadataPath, 'metadata.abs')
|
metadataPath = Path.join(metadataPath, 'metadata.abs')
|
||||||
|
|
||||||
return abmetadataGenerator.generate(this, metadataPath).then((success) => {
|
return abmetadataGenerator.generate(this, metadataPath).then((success) => {
|
||||||
|
this.isSavingMetadata = false
|
||||||
if (!success) Logger.error(`[Audiobook] Failed saving abmetadata to "${metadataPath}"`)
|
if (!success) Logger.error(`[Audiobook] Failed saving abmetadata to "${metadataPath}"`)
|
||||||
else Logger.debug(`[Audiobook] Success saving abmetadata to "${metadataPath}"`)
|
else Logger.debug(`[Audiobook] Success saving abmetadata to "${metadataPath}"`)
|
||||||
return success
|
return success
|
||||||
|
@ -117,7 +117,7 @@ class Scanner {
|
|||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||||
await this.db.updateEntity('audiobook', audiobook)
|
await this.db.updateAudiobook(audiobook)
|
||||||
return ScanResult.UPDATED
|
return ScanResult.UPDATED
|
||||||
}
|
}
|
||||||
return ScanResult.UPTODATE
|
return ScanResult.UPTODATE
|
||||||
@ -314,7 +314,7 @@ class Scanner {
|
|||||||
}))
|
}))
|
||||||
newAudiobooks = newAudiobooks.filter(ab => ab) // Filter out nulls
|
newAudiobooks = newAudiobooks.filter(ab => ab) // Filter out nulls
|
||||||
libraryScan.resultsAdded += newAudiobooks.length
|
libraryScan.resultsAdded += newAudiobooks.length
|
||||||
await this.db.insertEntities('audiobook', newAudiobooks)
|
await this.db.insertAudiobooks(newAudiobooks)
|
||||||
this.emitter('audiobooks_added', newAudiobooks.map(ab => ab.toJSONExpanded()))
|
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}"`)
|
Logger.debug(`[Scanner] Folder update group must be a new book "${bookDir}" in library "${library.name}"`)
|
||||||
var newAudiobook = await this.scanPotentialNewAudiobook(folder, fullPath)
|
var newAudiobook = await this.scanPotentialNewAudiobook(folder, fullPath)
|
||||||
if (newAudiobook) {
|
if (newAudiobook) {
|
||||||
await this.db.insertEntity('audiobook', newAudiobook)
|
await this.db.insertAudiobook(newAudiobook)
|
||||||
this.emitter('audiobook_added', newAudiobook.toJSONExpanded())
|
this.emitter('audiobook_added', newAudiobook.toJSONExpanded())
|
||||||
}
|
}
|
||||||
bookGroupingResults[bookDir] = newAudiobook ? ScanResult.ADDED : ScanResult.NOTHING
|
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)
|
Logger.warn('Found duplicate ID - updating from', ab.id, 'to', abCopy.id)
|
||||||
await this.db.removeEntity('audiobook', ab.id)
|
await this.db.removeEntity('audiobook', ab.id)
|
||||||
await this.db.insertEntity('audiobook', abCopy)
|
await this.db.insertAudiobook(abCopy)
|
||||||
audiobooksUpdated++
|
audiobooksUpdated++
|
||||||
} else {
|
} else {
|
||||||
ids[ab.id] = true
|
ids[ab.id] = true
|
||||||
@ -665,7 +665,7 @@ class Scanner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (hasUpdated) {
|
if (hasUpdated) {
|
||||||
await this.db.updateEntity('audiobook', audiobook)
|
await this.db.updateAudiobook(audiobook)
|
||||||
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
this.emitter('audiobook_updated', audiobook.toJSONExpanded())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,4 +45,56 @@ function generate(audiobook, outputPath) {
|
|||||||
return false
|
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