Fix: books read stat #167, Add: scanner parse metadata.opf and metadata.xml and use data #141

This commit is contained in:
advplyr 2021-11-08 20:05:12 -06:00
parent c7b0e1e2a2
commit fa8d02c729
8 changed files with 150 additions and 6 deletions

View File

@ -36,9 +36,15 @@
</div>
<div class="flex mt-2 -mx-1">
<div class="w-1/2 px-1">
<div class="w-1/3 px-1">
<ui-text-input-with-label v-model="details.narrator" label="Narrator" />
</div>
<div class="w-1/3 px-1">
<ui-text-input-with-label v-model="details.publisher" label="Publisher" />
</div>
<div class="flex-grow px-1">
<ui-text-input-with-label v-model="details.isbn" label="ISBN" />
</div>
</div>
</div>
@ -83,6 +89,8 @@ export default {
series: null,
volumeNumber: null,
publishYear: null,
publisher: null,
isbn: null,
genres: []
},
newTags: [],
@ -207,6 +215,8 @@ export default {
this.details.series = this.book.series
this.details.volumeNumber = this.book.volumeNumber
this.details.publishYear = this.book.publishYear
this.details.publisher = this.book.publisher || null
this.details.isbn = this.book.isbn || null
this.newTags = this.audiobook.tags || []
},

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf-client",
"version": "1.6.12",
"version": "1.6.13",
"description": "Audiobook manager and player",
"main": "index.js",
"scripts": {

View File

@ -86,7 +86,7 @@ export default {
return Object.values(this.$store.state.user.user.audiobooks || {})
},
userAudiobooksRead() {
return this.userAudiobooks.map((ab) => !!ab.isRead)
return this.userAudiobooks.filter((ab) => !!ab.isRead)
},
genresWithCount() {
var genresMap = {}

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.6.8",
"version": "1.6.12",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "audiobookshelf",
"version": "1.6.12",
"version": "1.6.13",
"description": "Self-hosted audiobook server for managing and playing audiobooks",
"main": "index.js",
"scripts": {
@ -45,7 +45,8 @@
"read-chunk": "^3.1.0",
"recursive-readdir-async": "^1.1.8",
"socket.io": "^4.1.3",
"watcher": "^1.2.0"
"watcher": "^1.2.0",
"xml2js": "^0.4.23"
},
"devDependencies": {}
}

View File

@ -2,6 +2,7 @@ const Path = require('path')
const fs = require('fs-extra')
const { bytesPretty, elapsedPretty, readTextFile } = require('../utils/fileUtils')
const { comparePaths, getIno } = require('../utils/index')
const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
const { extractCoverArt } = require('../utils/ffmpegHelpers')
const nfoGenerator = require('../utils/nfoGenerator')
const Logger = require('../Logger')
@ -501,6 +502,7 @@ class Audiobook {
var alreadyHasDescTxt = this.otherFiles.find(of => of.filename === 'desc.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)
this.otherFiles = this.otherFiles.filter(f => newOtherFilePaths.includes(f.path))
@ -531,6 +533,27 @@ class Audiobook {
hasUpdates = true
}
}
var metadataOpf = newOtherFiles.find(file => file.filename === 'metadata.opf' || file.filename === 'metadata.xml')
if (metadataOpf && (!alreadyHasMetadataOpf || forceRescan)) {
var xmlText = await readTextFile(metadataOpf.fullPath)
if (xmlText) {
var opfMetadata = await parseOpfMetadataXML(xmlText)
Logger.debug(`[Audiobook] Sync Other File ${metadataOpf.filename} parsed:`, opfMetadata)
if (opfMetadata) {
const bookUpdatePayload = {}
for (const key in opfMetadata) {
if (opfMetadata[key] && !this.book[key]) {
bookUpdatePayload[key] = opfMetadata[key]
}
}
if (Object.keys(bookUpdatePayload).length) {
Logger.debug(`[Audiobook] Using data found in metadata opf/xml`, bookUpdatePayload)
this.update({ book: bookUpdatePayload })
hasUpdates = true
}
}
}
}
newOtherFiles.forEach((file) => {
var existingOtherFile = this.otherFiles.find(f => f.ino === file.ino)
@ -754,6 +777,23 @@ class Audiobook {
Logger.debug(`[Audiobook] "${this.title}" found reader.txt updating narrator with "${readerText}"`)
bookUpdatePayload.narrator = readerText
}
var metadataOpf = this.otherFiles.find(file => file.filename === 'metadata.opf' || file.filename === 'metadata.xml')
if (metadataOpf) {
var xmlText = await readTextFile(metadataOpf.fullPath)
if (xmlText) {
var opfMetadata = await parseOpfMetadataXML(xmlText)
Logger.debug(`[Audiobook] "${this.title}" found ${metadataOpf.filename} parsed:`, opfMetadata)
if (opfMetadata) {
for (const key in opfMetadata) {
if (opfMetadata[key] && !this.book[key] && !bookUpdatePayload[key]) {
bookUpdatePayload[key] = opfMetadata[key]
}
}
}
}
}
if (Object.keys(bookUpdatePayload).length) {
return this.update({ book: bookUpdatePayload })
}

View File

@ -1,6 +1,7 @@
const Path = require('path')
const fs = require('fs')
const Logger = require('../Logger')
const { parseString } = require("xml2js")
const levenshteinDistance = (str1, str2, caseSensitive = false) => {
if (!caseSensitive) {
@ -43,3 +44,17 @@ module.exports.getIno = (path) => {
return null
})
}
const xmlToJSON = (xml) => {
return new Promise((resolve, reject) => {
parseString(xml, (err, results) => {
if (err) {
Logger.error(`[xmlToJSON] Error`, err)
resolve(null)
} else {
resolve(results)
}
})
})
}
module.exports.xmlToJSON = xmlToJSON

View File

@ -0,0 +1,78 @@
const { xmlToJSON } = require('./index')
function parseCreators(metadata) {
if (!metadata['dc:creator']) return null
var creators = metadata['dc:creator']
if (!creators.length) return null
return creators.map(c => {
if (typeof c !== 'object' || !c['$'] || !c['_']) return false
return {
value: c['_'],
role: c['$']['opf:role'] || null,
fileAs: c['$']['opf:file-as'] || null
}
})
}
function fetchCreator(creators, role) {
if (!creators || !creators.length) return null
var creator = creators.find(c => c.role === role)
return creator ? creator.value : null
}
function fetchDate(metadata) {
if (!metadata['dc:date']) return null
var dates = metadata['dc:date']
if (!dates.length || typeof dates[0] !== 'string') return null
var dateSplit = dates[0].split('-')
if (!dateSplit.length || dateSplit[0].length !== 4 || isNaN(dateSplit[0])) return null
return dateSplit[0]
}
function fetchPublisher(metadata) {
if (!metadata['dc:publisher']) return null
var publishers = metadata['dc:publisher']
if (!publishers.length || typeof publishers[0] !== 'string') return null
return publishers[0]
}
function fetchISBN(metadata) {
if (!metadata['dc:identifier'] || !metadata['dc:identifier'].length) return null
var identifiers = metadata['dc:identifier']
var isbnObj = identifiers.find(i => i['$'] && i['$']['opf:scheme'] === 'ISBN')
return isbnObj ? isbnObj['_'] || null : null
}
function fetchTitle(metadata) {
if (!metadata['dc:title']) return null
var titles = metadata['dc:title']
if (!titles.length) return null
if (typeof titles[0] === 'string') {
return titles[0]
}
if (titles[0]['_']) {
return titles[0]['_']
}
return null
}
module.exports.parseOpfMetadataXML = async (xml) => {
var json = await xmlToJSON(xml)
if (!json || !json.package || !json.package.metadata) return null
var metadata = json.package.metadata
if (Array.isArray(metadata)) {
if (!metadata.length) return null
metadata = metadata[0]
}
var creators = parseCreators(metadata)
var data = {
title: fetchTitle(metadata),
author: fetchCreator(creators, 'aut'),
narrator: fetchCreator(creators, 'nrt'),
publishYear: fetchDate(metadata),
publisher: fetchPublisher(metadata),
isbn: fetchISBN(metadata)
}
return data
}