Merge branch 'advplyr:master' into bookfinder-improvements

This commit is contained in:
mikiher 2023-11-30 21:37:01 +02:00 committed by GitHub
commit 3c6d6bf688
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 446 additions and 54 deletions

View file

@ -363,12 +363,50 @@ class Auth {
req.session[sessionKey].code_verifier = req.query.code_verifier
}
function handleAuthError(isMobile, errorCode, errorMessage, logMessage, response) {
Logger.error(logMessage)
if (response) {
// Depending on the error, it can also have a body
// We also log the request header the passport plugin sents for the URL
const header = response.req?._header.replace(/Authorization: [^\r\n]*/i, 'Authorization: REDACTED')
Logger.debug(header + '\n' + response.body?.toString())
}
if (isMobile) {
return res.status(errorCode).send(errorMessage)
} else {
return res.redirect(`/login?error=${encodeURIComponent(errorMessage)}&autoLaunch=0`)
}
}
function passportCallback(req, res, next) {
return (err, user, info) => {
const isMobile = req.session[sessionKey]?.mobile === true
if (err) {
return handleAuthError(isMobile, 500, 'Error in callback', `[Auth] Error in openid callback - ${err}`, err?.response)
}
if (!user) {
// Info usually contains the error message from the SSO provider
return handleAuthError(isMobile, 401, 'Unauthorized', `[Auth] No data in openid callback - ${info}`, info?.response)
}
req.logIn(user, (loginError) => {
if (loginError) {
return handleAuthError(isMobile, 500, 'Error during login', `[Auth] Error in openid callback: ${loginError}`)
}
next()
})
}
}
// While not required by the standard, the passport plugin re-sends the original redirect_uri in the token request
// We need to set it correctly, as some SSO providers (e.g. keycloak) check that parameter when it is provided
if (req.session[sessionKey].mobile) {
return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' })(req, res, next)
return passport.authenticate('openid-client', { redirect_uri: 'audiobookshelf://oauth' }, passportCallback(req, res, next))(req, res, next)
} else {
return passport.authenticate('openid-client', { failureRedirect: '/login?error=Unauthorized&autoLaunch=0' })(req, res, next)
return passport.authenticate('openid-client', passportCallback(req, res, next))(req, res, next)
}
},
// on a successfull login: read the cookies and react like the client requested (callback or json)

View file

@ -5,6 +5,7 @@ class Logger {
constructor() {
this.isDev = process.env.NODE_ENV !== 'production'
this.logLevel = !this.isDev ? LogLevel.INFO : LogLevel.TRACE
this.hideDevLogs = process.env.HIDE_DEV_LOGS === undefined ? !this.isDev : process.env.HIDE_DEV_LOGS === '1'
this.socketListeners = []
this.logManager = null
@ -92,7 +93,7 @@ class Logger {
* @param {...any} args
*/
dev(...args) {
if (!this.isDev || process.env.HIDE_DEV_LOGS === '1') return
if (this.hideDevLogs) return
console.log(`[${this.timestamp}] DEV:`, ...args)
}

View file

@ -231,6 +231,7 @@ class Server {
'/library/:library/search',
'/library/:library/bookshelf/:id?',
'/library/:library/authors',
'/library/:library/narrators',
'/library/:library/series/:id?',
'/library/:library/podcast/search',
'/library/:library/podcast/latest',

View file

@ -9,7 +9,7 @@ class LibrarySettings {
this.autoScanCronExpression = null
this.audiobooksOnly = false
this.hideSingleBookSeries = false // Do not show series that only have 1 book
this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata']
this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
if (settings) {
this.construct(settings)
@ -28,7 +28,7 @@ class LibrarySettings {
this.metadataPrecedence = [...settings.metadataPrecedence]
} else {
// Added in v2.4.5
this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata']
this.metadataPrecedence = ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
}
}

View file

@ -18,6 +18,7 @@ const BookFinder = require('../finders/BookFinder')
const LibraryScan = require("./LibraryScan")
const OpfFileScanner = require('./OpfFileScanner')
const NfoFileScanner = require('./NfoFileScanner')
const AbsMetadataFileScanner = require('./AbsMetadataFileScanner')
/**
@ -593,7 +594,7 @@ class BookScanner {
}
const bookMetadataSourceHandler = new BookScanner.BookMetadataSourceHandler(bookMetadata, audioFiles, libraryItemData, libraryScan, existingLibraryItemId)
const metadataPrecedence = librarySettings.metadataPrecedence || ['folderStructure', 'audioMetatags', 'txtFiles', 'opfFile', 'absMetadata']
const metadataPrecedence = librarySettings.metadataPrecedence || ['folderStructure', 'audioMetatags', 'nfoFile', 'txtFiles', 'opfFile', 'absMetadata']
libraryScan.addLog(LogLevel.DEBUG, `"${bookMetadata.title}" Getting metadata with precedence [${metadataPrecedence.join(', ')}]`)
for (const metadataSource of metadataPrecedence) {
if (bookMetadataSourceHandler[metadataSource]) {
@ -649,6 +650,14 @@ class BookScanner {
AudioFileScanner.setBookMetadataFromAudioMetaTags(bookTitle, this.audioFiles, this.bookMetadata, this.libraryScan)
}
/**
* Metadata from .nfo file
*/
async nfoFile() {
if (!this.libraryItemData.metadataNfoLibraryFile) return
await NfoFileScanner.scanBookNfoFile(this.libraryItemData.metadataNfoLibraryFile, this.bookMetadata)
}
/**
* Description from desc.txt and narrator from reader.txt
*/

View file

@ -132,6 +132,11 @@ class LibraryItemScanData {
return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.opf')
}
/** @type {LibraryItem.LibraryFileObject} */
get metadataNfoLibraryFile() {
return this.libraryFiles.find(lf => lf.metadata.ext.toLowerCase() === '.nfo')
}
/**
*
* @param {LibraryItem} existingLibraryItem

View file

@ -0,0 +1,48 @@
const { parseNfoMetadata } = require('../utils/parsers/parseNfoMetadata')
const { readTextFile } = require('../utils/fileUtils')
class NfoFileScanner {
constructor() { }
/**
* Parse metadata from .nfo file found in library scan and update bookMetadata
*
* @param {import('../models/LibraryItem').LibraryFileObject} nfoLibraryFileObj
* @param {Object} bookMetadata
*/
async scanBookNfoFile(nfoLibraryFileObj, bookMetadata) {
const nfoText = await readTextFile(nfoLibraryFileObj.metadata.path)
const nfoMetadata = nfoText ? await parseNfoMetadata(nfoText) : null
if (nfoMetadata) {
for (const key in nfoMetadata) {
if (key === 'tags') { // Add tags only if tags are empty
if (nfoMetadata.tags.length) {
bookMetadata.tags = nfoMetadata.tags
}
} else if (key === 'genres') { // Add genres only if genres are empty
if (nfoMetadata.genres.length) {
bookMetadata.genres = nfoMetadata.genres
}
} else if (key === 'authors') {
if (nfoMetadata.authors?.length) {
bookMetadata.authors = nfoMetadata.authors
}
} else if (key === 'narrators') {
if (nfoMetadata.narrators?.length) {
bookMetadata.narrators = nfoMetadata.narrators
}
} else if (key === 'series') {
if (nfoMetadata.series) {
bookMetadata.series = [{
name: nfoMetadata.series,
sequence: nfoMetadata.sequence || null
}]
}
} else if (nfoMetadata[key] && key !== 'sequence') {
bookMetadata[key] = nfoMetadata[key]
}
}
}
}
}
module.exports = new NfoFileScanner()

View file

@ -0,0 +1,100 @@
function parseNfoMetadata(nfoText) {
if (!nfoText) return null
const lines = nfoText.split(/\r?\n/)
const metadata = {}
let insideBookDescription = false
lines.forEach(line => {
if (line.search(/^\s*book description\s*$/i) !== -1) {
insideBookDescription = true
return
}
if (insideBookDescription) {
if (line.search(/^\s*=+\s*$/i) !== -1) return
metadata.description = metadata.description || ''
metadata.description += line + '\n'
return
}
const match = line.match(/^(.*?):(.*)$/)
if (match) {
const key = match[1].toLowerCase().trim()
const value = match[2].trim()
if (!value) return
switch (key) {
case 'title':
{
const titleMatch = value.match(/^(.*?):(.*)$/)
if (titleMatch) {
metadata.title = titleMatch[1].trim()
metadata.subtitle = titleMatch[2].trim()
} else {
metadata.title = value
}
}
break
case 'author':
metadata.authors = value.split(/\s*,\s*/).filter(v => v)
break
case 'narrator':
case 'read by':
metadata.narrators = value.split(/\s*,\s*/).filter(v => v)
break
case 'series name':
metadata.series = value
break
case 'genre':
metadata.genres = value.split(/\s*,\s*/).filter(v => v)
break
case 'tags':
metadata.tags = value.split(/\s*,\s*/).filter(v => v)
break
case 'copyright':
case 'audible.com release':
case 'audiobook copyright':
case 'book copyright':
case 'recording copyright':
case 'release date':
case 'date':
{
const year = extractYear(value)
if (year) {
metadata.publishedYear = year
}
}
break
case 'position in series':
metadata.sequence = value
break
case 'unabridged':
metadata.abridged = value.toLowerCase() === 'yes' ? false : true
break
case 'abridged':
metadata.abridged = value.toLowerCase() === 'no' ? false : true
break
case 'publisher':
metadata.publisher = value
break
case 'asin':
metadata.asin = value
break
case 'isbn':
case 'isbn-10':
case 'isbn-13':
metadata.isbn = value
break
}
}
})
// Trim leading/trailing whitespace for description
if (metadata.description) {
metadata.description = metadata.description.trim()
}
return metadata
}
module.exports = { parseNfoMetadata }
function extractYear(str) {
const match = str.match(/\d{4}/g)
return match ? match[match.length - 1] : null
}