mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2024-12-28 09:38:56 +01:00
This commit is contained in:
parent
c7b0e1e2a2
commit
fa8d02c729
@ -36,9 +36,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex mt-2 -mx-1">
|
<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" />
|
<ui-text-input-with-label v-model="details.narrator" label="Narrator" />
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -83,6 +89,8 @@ export default {
|
|||||||
series: null,
|
series: null,
|
||||||
volumeNumber: null,
|
volumeNumber: null,
|
||||||
publishYear: null,
|
publishYear: null,
|
||||||
|
publisher: null,
|
||||||
|
isbn: null,
|
||||||
genres: []
|
genres: []
|
||||||
},
|
},
|
||||||
newTags: [],
|
newTags: [],
|
||||||
@ -207,6 +215,8 @@ export default {
|
|||||||
this.details.series = this.book.series
|
this.details.series = this.book.series
|
||||||
this.details.volumeNumber = this.book.volumeNumber
|
this.details.volumeNumber = this.book.volumeNumber
|
||||||
this.details.publishYear = this.book.publishYear
|
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 || []
|
this.newTags = this.audiobook.tags || []
|
||||||
},
|
},
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf-client",
|
"name": "audiobookshelf-client",
|
||||||
"version": "1.6.12",
|
"version": "1.6.13",
|
||||||
"description": "Audiobook manager and player",
|
"description": "Audiobook manager and player",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
@ -86,7 +86,7 @@ export default {
|
|||||||
return Object.values(this.$store.state.user.user.audiobooks || {})
|
return Object.values(this.$store.state.user.user.audiobooks || {})
|
||||||
},
|
},
|
||||||
userAudiobooksRead() {
|
userAudiobooksRead() {
|
||||||
return this.userAudiobooks.map((ab) => !!ab.isRead)
|
return this.userAudiobooks.filter((ab) => !!ab.isRead)
|
||||||
},
|
},
|
||||||
genresWithCount() {
|
genresWithCount() {
|
||||||
var genresMap = {}
|
var genresMap = {}
|
||||||
|
2
package-lock.json
generated
2
package-lock.json
generated
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.6.8",
|
"version": "1.6.12",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "audiobookshelf",
|
"name": "audiobookshelf",
|
||||||
"version": "1.6.12",
|
"version": "1.6.13",
|
||||||
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
"description": "Self-hosted audiobook server for managing and playing audiobooks",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@ -45,7 +45,8 @@
|
|||||||
"read-chunk": "^3.1.0",
|
"read-chunk": "^3.1.0",
|
||||||
"recursive-readdir-async": "^1.1.8",
|
"recursive-readdir-async": "^1.1.8",
|
||||||
"socket.io": "^4.1.3",
|
"socket.io": "^4.1.3",
|
||||||
"watcher": "^1.2.0"
|
"watcher": "^1.2.0",
|
||||||
|
"xml2js": "^0.4.23"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
}
|
}
|
@ -2,6 +2,7 @@ const Path = require('path')
|
|||||||
const fs = require('fs-extra')
|
const fs = require('fs-extra')
|
||||||
const { bytesPretty, elapsedPretty, readTextFile } = require('../utils/fileUtils')
|
const { bytesPretty, elapsedPretty, readTextFile } = require('../utils/fileUtils')
|
||||||
const { comparePaths, getIno } = require('../utils/index')
|
const { comparePaths, getIno } = require('../utils/index')
|
||||||
|
const { parseOpfMetadataXML } = require('../utils/parseOpfMetadata')
|
||||||
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
const { extractCoverArt } = require('../utils/ffmpegHelpers')
|
||||||
const nfoGenerator = require('../utils/nfoGenerator')
|
const nfoGenerator = require('../utils/nfoGenerator')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
@ -501,6 +502,7 @@ 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))
|
||||||
@ -531,6 +533,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 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) => {
|
newOtherFiles.forEach((file) => {
|
||||||
var existingOtherFile = this.otherFiles.find(f => f.ino === file.ino)
|
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}"`)
|
Logger.debug(`[Audiobook] "${this.title}" found reader.txt updating narrator with "${readerText}"`)
|
||||||
bookUpdatePayload.narrator = 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) {
|
if (Object.keys(bookUpdatePayload).length) {
|
||||||
return this.update({ book: bookUpdatePayload })
|
return this.update({ book: bookUpdatePayload })
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
const Path = require('path')
|
const Path = require('path')
|
||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
const { parseString } = require("xml2js")
|
||||||
|
|
||||||
const levenshteinDistance = (str1, str2, caseSensitive = false) => {
|
const levenshteinDistance = (str1, str2, caseSensitive = false) => {
|
||||||
if (!caseSensitive) {
|
if (!caseSensitive) {
|
||||||
@ -43,3 +44,17 @@ module.exports.getIno = (path) => {
|
|||||||
return null
|
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
|
||||||
|
78
server/utils/parseOpfMetadata.js
Normal file
78
server/utils/parseOpfMetadata.js
Normal 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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user