mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-05-28 14:21:34 +00:00
parent
c009db9f28
commit
1bad2d9072
4 changed files with 145 additions and 33 deletions
|
|
@ -5,15 +5,15 @@ const { LogLevel } = require('../utils/constants')
|
||||||
const abmetadataGenerator = require('../utils/generators/abmetadataGenerator')
|
const abmetadataGenerator = require('../utils/generators/abmetadataGenerator')
|
||||||
|
|
||||||
class AbsMetadataFileScanner {
|
class AbsMetadataFileScanner {
|
||||||
constructor() { }
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for metadata.json file and set book metadata
|
* Check for metadata.json file and set book metadata
|
||||||
*
|
*
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||||
* @param {Object} bookMetadata
|
* @param {Object} bookMetadata
|
||||||
* @param {string} [existingLibraryItemId]
|
* @param {string} [existingLibraryItemId]
|
||||||
*/
|
*/
|
||||||
async scanBookMetadataFile(libraryScan, libraryItemData, bookMetadata, existingLibraryItemId = null) {
|
async scanBookMetadataFile(libraryScan, libraryItemData, bookMetadata, existingLibraryItemId = null) {
|
||||||
const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile
|
const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile
|
||||||
|
|
@ -32,7 +32,8 @@ class AbsMetadataFileScanner {
|
||||||
|
|
||||||
if (metadataText) {
|
if (metadataText) {
|
||||||
libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}"`)
|
libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}"`)
|
||||||
const abMetadata = abmetadataGenerator.parseJson(metadataText) || {}
|
const abMetadata = abmetadataGenerator.parseJson(metadataText, 'book') || {}
|
||||||
|
|
||||||
for (const key in abMetadata) {
|
for (const key in abMetadata) {
|
||||||
// TODO: When to override with null or empty arrays?
|
// TODO: When to override with null or empty arrays?
|
||||||
if (abMetadata[key] === undefined || abMetadata[key] === null) continue
|
if (abMetadata[key] === undefined || abMetadata[key] === null) continue
|
||||||
|
|
@ -48,11 +49,11 @@ class AbsMetadataFileScanner {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for metadata.json file and set podcast metadata
|
* Check for metadata.json file and set podcast metadata
|
||||||
*
|
*
|
||||||
* @param {import('./LibraryScan')} libraryScan
|
* @param {import('./LibraryScan')} libraryScan
|
||||||
* @param {import('./LibraryItemScanData')} libraryItemData
|
* @param {import('./LibraryItemScanData')} libraryItemData
|
||||||
* @param {Object} podcastMetadata
|
* @param {Object} podcastMetadata
|
||||||
* @param {string} [existingLibraryItemId]
|
* @param {string} [existingLibraryItemId]
|
||||||
*/
|
*/
|
||||||
async scanPodcastMetadataFile(libraryScan, libraryItemData, podcastMetadata, existingLibraryItemId = null) {
|
async scanPodcastMetadataFile(libraryScan, libraryItemData, podcastMetadata, existingLibraryItemId = null) {
|
||||||
const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile
|
const metadataLibraryFile = libraryItemData.metadataJsonLibraryFile
|
||||||
|
|
@ -71,7 +72,7 @@ class AbsMetadataFileScanner {
|
||||||
|
|
||||||
if (metadataText) {
|
if (metadataText) {
|
||||||
libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}"`)
|
libraryScan.addLog(LogLevel.INFO, `Found metadata file "${metadataFilePath}"`)
|
||||||
const abMetadata = abmetadataGenerator.parseJson(metadataText) || {}
|
const abMetadata = abmetadataGenerator.parseJson(metadataText, 'podcast') || {}
|
||||||
for (const key in abMetadata) {
|
for (const key in abMetadata) {
|
||||||
if (abMetadata[key] === undefined || abMetadata[key] === null) continue
|
if (abMetadata[key] === undefined || abMetadata[key] === null) continue
|
||||||
if (key === 'tags' && !abMetadata.tags?.length) continue
|
if (key === 'tags' && !abMetadata.tags?.length) continue
|
||||||
|
|
@ -81,4 +82,4 @@ class AbsMetadataFileScanner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = new AbsMetadataFileScanner()
|
module.exports = new AbsMetadataFileScanner()
|
||||||
|
|
|
||||||
|
|
@ -825,6 +825,9 @@ class BookScanner {
|
||||||
|
|
||||||
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
|
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys must match abmetadataGenerator.js
|
||||||
|
*/
|
||||||
const jsonObject = {
|
const jsonObject = {
|
||||||
tags: libraryItem.media.tags || [],
|
tags: libraryItem.media.tags || [],
|
||||||
chapters: libraryItem.media.chapters?.map((c) => ({ ...c })) || [],
|
chapters: libraryItem.media.chapters?.map((c) => ({ ...c })) || [],
|
||||||
|
|
|
||||||
|
|
@ -425,6 +425,9 @@ class PodcastScanner {
|
||||||
|
|
||||||
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
|
const metadataFilePath = Path.join(metadataPath, `metadata.${global.ServerSettings.metadataFileFormat}`)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keys must match abmetadataGenerator.js
|
||||||
|
*/
|
||||||
const jsonObject = {
|
const jsonObject = {
|
||||||
tags: libraryItem.media.tags || [],
|
tags: libraryItem.media.tags || [],
|
||||||
title: libraryItem.media.title,
|
title: libraryItem.media.title,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,51 @@
|
||||||
const Logger = require('../../Logger')
|
const Logger = require('../../Logger')
|
||||||
const parseSeriesString = require('../parsers/parseSeriesString')
|
const parseSeriesString = require('../parsers/parseSeriesString')
|
||||||
|
|
||||||
function parseJsonMetadataText(text) {
|
const mediaTypeKeys = {
|
||||||
|
book: {
|
||||||
|
tags: 'stringArray',
|
||||||
|
title: 'string',
|
||||||
|
subtitle: 'string',
|
||||||
|
authors: 'stringArray',
|
||||||
|
narrators: 'stringArray',
|
||||||
|
series: 'stringArray',
|
||||||
|
genres: 'stringArray',
|
||||||
|
publishedYear: 'string',
|
||||||
|
publishedDate: 'string',
|
||||||
|
publisher: 'string',
|
||||||
|
description: 'string',
|
||||||
|
isbn: 'string',
|
||||||
|
asin: 'string',
|
||||||
|
language: 'string',
|
||||||
|
explicit: 'boolean',
|
||||||
|
abridged: 'boolean'
|
||||||
|
},
|
||||||
|
podcast: {
|
||||||
|
tags: 'stringArray',
|
||||||
|
title: 'string',
|
||||||
|
author: 'string',
|
||||||
|
description: 'string',
|
||||||
|
releaseDate: 'string',
|
||||||
|
genres: 'stringArray',
|
||||||
|
feedURL: 'string',
|
||||||
|
imageURL: 'string',
|
||||||
|
itunesPageURL: 'string',
|
||||||
|
itunesId: 'string',
|
||||||
|
itunesArtistId: 'string',
|
||||||
|
asin: 'string',
|
||||||
|
language: 'string',
|
||||||
|
explicit: 'boolean',
|
||||||
|
podcastType: 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} text
|
||||||
|
* @param {"book" | "podcast"} mediaType
|
||||||
|
* @returns {Object}
|
||||||
|
*/
|
||||||
|
function parseJsonMetadataText(text, mediaType) {
|
||||||
try {
|
try {
|
||||||
const abmetadataData = JSON.parse(text)
|
const abmetadataData = JSON.parse(text)
|
||||||
|
|
||||||
|
|
@ -19,28 +63,41 @@ function parseJsonMetadataText(text) {
|
||||||
}
|
}
|
||||||
delete abmetadataData.metadata
|
delete abmetadataData.metadata
|
||||||
|
|
||||||
if (abmetadataData.series?.length) {
|
const expectedKeys = mediaTypeKeys[mediaType]
|
||||||
abmetadataData.series = [...new Set(abmetadataData.series.map((t) => t?.trim()).filter((t) => t))]
|
if (!expectedKeys) {
|
||||||
abmetadataData.series = abmetadataData.series.map((series) => parseSeriesString.parse(series))
|
Logger.error(`[abmetadataGenerator] Invalid media type "${mediaType}"`)
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
// clean tags & remove dupes
|
|
||||||
if (abmetadataData.tags?.length) {
|
const validated = {}
|
||||||
abmetadataData.tags = [...new Set(abmetadataData.tags.map((t) => t?.trim()).filter((t) => t))]
|
for (const key in expectedKeys) {
|
||||||
|
const expectedType = expectedKeys[key]
|
||||||
|
if (!(key in abmetadataData)) continue
|
||||||
|
|
||||||
|
const validatedValue = validateMetadataValue(key, abmetadataData[key], expectedType)
|
||||||
|
if (validatedValue !== undefined) {
|
||||||
|
validated[key] = validatedValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (abmetadataData.chapters?.length) {
|
|
||||||
abmetadataData.chapters = cleanChaptersArray(abmetadataData.chapters, abmetadataData.title)
|
if (validated.series?.length) {
|
||||||
|
validated.series = validated.series.map((series) => parseSeriesString.parse(series)).filter(Boolean)
|
||||||
}
|
}
|
||||||
// clean remove dupes
|
|
||||||
if (abmetadataData.authors?.length) {
|
if (mediaType === 'book' && 'chapters' in abmetadataData) {
|
||||||
abmetadataData.authors = [...new Set(abmetadataData.authors.map((t) => t?.trim()).filter((t) => t))]
|
if (abmetadataData.chapters === null) {
|
||||||
|
validated.chapters = []
|
||||||
|
} else if (Array.isArray(abmetadataData.chapters)) {
|
||||||
|
const cleanedChapters = cleanChaptersArray(abmetadataData.chapters, validated.title ?? abmetadataData.title)
|
||||||
|
if (cleanedChapters) {
|
||||||
|
validated.chapters = cleanedChapters
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Logger.warn(`[abmetadataGenerator] Invalid metadata key "chapters" expected array, got ${typeof abmetadataData.chapters}`)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (abmetadataData.narrators?.length) {
|
|
||||||
abmetadataData.narrators = [...new Set(abmetadataData.narrators.map((t) => t?.trim()).filter((t) => t))]
|
return validated
|
||||||
}
|
|
||||||
if (abmetadataData.genres?.length) {
|
|
||||||
abmetadataData.genres = [...new Set(abmetadataData.genres.map((t) => t?.trim()).filter((t) => t))]
|
|
||||||
}
|
|
||||||
return abmetadataData
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error(`[abmetadataGenerator] Invalid metadata.json JSON`, error)
|
Logger.error(`[abmetadataGenerator] Invalid metadata.json JSON`, error)
|
||||||
return null
|
return null
|
||||||
|
|
@ -48,6 +105,54 @@ function parseJsonMetadataText(text) {
|
||||||
}
|
}
|
||||||
module.exports.parseJson = parseJsonMetadataText
|
module.exports.parseJson = parseJsonMetadataText
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} key
|
||||||
|
* @param {*} value
|
||||||
|
* @param {string} expectedType
|
||||||
|
* @returns {*|undefined} undefined excludes the key
|
||||||
|
*/
|
||||||
|
function validateMetadataValue(key, value, expectedType) {
|
||||||
|
if (expectedType === 'string') {
|
||||||
|
if (value === null) return null
|
||||||
|
if (typeof value === 'number') return String(value)
|
||||||
|
if (typeof value === 'string') return value
|
||||||
|
Logger.warn(`[abmetadataGenerator] Invalid metadata key "${key}" expected string, got ${typeof value}`)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expectedType === 'boolean') {
|
||||||
|
if (value === null) return null
|
||||||
|
if (typeof value === 'boolean') return value
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
const lower = value.toLowerCase()
|
||||||
|
if (lower === 'true') return true
|
||||||
|
if (lower === 'false') return false
|
||||||
|
}
|
||||||
|
Logger.warn(`[abmetadataGenerator] Invalid metadata key "${key}" expected boolean, got ${typeof value}`)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter empty strings and deduplicate
|
||||||
|
if (expectedType === 'stringArray') {
|
||||||
|
if (value === null) return []
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
Logger.warn(`[abmetadataGenerator] Invalid metadata key "${key}" expected string array, got ${typeof value}`)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanedArray = value.filter((t) => typeof t === 'string')
|
||||||
|
return [...new Set(cleanedArray.map((t) => t.trim()).filter((t) => t))]
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger.warn(`[abmetadataGenerator] Unknown expected type "${expectedType}" for key "${key}"`)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {Object[]} chaptersArray
|
||||||
|
* @param {string} mediaTitle
|
||||||
|
* @returns {Object[]}
|
||||||
|
*/
|
||||||
function cleanChaptersArray(chaptersArray, mediaTitle) {
|
function cleanChaptersArray(chaptersArray, mediaTitle) {
|
||||||
const chapters = []
|
const chapters = []
|
||||||
let index = 0
|
let index = 0
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue