mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-12-20 18:59:37 +00:00
Merge pull request #4750 from mikiher/providers-api
Add metadata providers API and use them on web client
This commit is contained in:
commit
a92ba564bd
18 changed files with 361 additions and 210 deletions
|
|
@ -221,13 +221,11 @@ class LibraryController {
|
|||
const includeArray = (req.query.include || '').split(',')
|
||||
if (includeArray.includes('filterdata')) {
|
||||
const filterdata = await libraryFilters.getFilterData(req.library.mediaType, req.library.id)
|
||||
const customMetadataProviders = await Database.customMetadataProviderModel.getForClientByMediaType(req.library.mediaType)
|
||||
|
||||
return res.json({
|
||||
filterdata,
|
||||
issues: filterdata.numIssues,
|
||||
numUserPlaylists: await Database.playlistModel.getNumPlaylistsForUserAndLibrary(req.user.id, req.library.id),
|
||||
customMetadataProviders,
|
||||
library: req.library.toOldJSON()
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,29 @@ const BookFinder = require('../finders/BookFinder')
|
|||
const PodcastFinder = require('../finders/PodcastFinder')
|
||||
const AuthorFinder = require('../finders/AuthorFinder')
|
||||
const Database = require('../Database')
|
||||
const { isValidASIN } = require('../utils')
|
||||
const { isValidASIN, getQueryParamAsString, ValidationError, NotFoundError } = require('../utils')
|
||||
|
||||
// Provider name mappings for display purposes
|
||||
const providerMap = {
|
||||
all: 'All',
|
||||
best: 'Best',
|
||||
google: 'Google Books',
|
||||
itunes: 'iTunes',
|
||||
openlibrary: 'Open Library',
|
||||
fantlab: 'FantLab.ru',
|
||||
audiobookcovers: 'AudiobookCovers.com',
|
||||
audible: 'Audible.com',
|
||||
'audible.ca': 'Audible.ca',
|
||||
'audible.uk': 'Audible.co.uk',
|
||||
'audible.au': 'Audible.com.au',
|
||||
'audible.fr': 'Audible.fr',
|
||||
'audible.de': 'Audible.de',
|
||||
'audible.jp': 'Audible.co.jp',
|
||||
'audible.it': 'Audible.it',
|
||||
'audible.in': 'Audible.in',
|
||||
'audible.es': 'Audible.es',
|
||||
audnexus: 'Audnexus'
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef RequestUserObject
|
||||
|
|
@ -16,6 +38,44 @@ const { isValidASIN } = require('../utils')
|
|||
class SearchController {
|
||||
constructor() {}
|
||||
|
||||
/**
|
||||
* Fetches a library item by ID
|
||||
* @param {string} id - Library item ID
|
||||
* @param {string} methodName - Name of the calling method for logging
|
||||
* @returns {Promise<import('../models/LibraryItem').LibraryItemExpanded>}
|
||||
*/
|
||||
static async fetchLibraryItem(id) {
|
||||
const libraryItem = await Database.libraryItemModel.getExpandedById(id)
|
||||
if (!libraryItem) {
|
||||
throw new NotFoundError(`library item "${id}" not found`)
|
||||
}
|
||||
return libraryItem
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps custom metadata providers to standardized format
|
||||
* @param {Array} providers - Array of custom provider objects
|
||||
* @returns {Array<{value: string, text: string}>}
|
||||
*/
|
||||
static mapCustomProviders(providers) {
|
||||
return providers.map((provider) => ({
|
||||
value: provider.getSlug(),
|
||||
text: provider.name
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Static helper method to format provider for client (for use in array methods)
|
||||
* @param {string} providerValue - Provider identifier
|
||||
* @returns {{value: string, text: string}}
|
||||
*/
|
||||
static formatProvider(providerValue) {
|
||||
return {
|
||||
value: providerValue,
|
||||
text: providerMap[providerValue] || providerValue
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/search/books
|
||||
*
|
||||
|
|
@ -23,19 +83,25 @@ class SearchController {
|
|||
* @param {Response} res
|
||||
*/
|
||||
async findBooks(req, res) {
|
||||
const id = req.query.id
|
||||
const libraryItem = await Database.libraryItemModel.getExpandedById(id)
|
||||
const provider = req.query.provider || 'google'
|
||||
const title = req.query.title || ''
|
||||
const author = req.query.author || ''
|
||||
try {
|
||||
const query = req.query
|
||||
const provider = getQueryParamAsString(query, 'provider', 'google')
|
||||
const title = getQueryParamAsString(query, 'title', '')
|
||||
const author = getQueryParamAsString(query, 'author', '')
|
||||
const id = getQueryParamAsString(query, 'id', '', true)
|
||||
|
||||
if (typeof provider !== 'string' || typeof title !== 'string' || typeof author !== 'string') {
|
||||
Logger.error(`[SearchController] findBooks: Invalid request query params`)
|
||||
return res.status(400).send('Invalid request query params')
|
||||
// Fetch library item
|
||||
const libraryItem = await SearchController.fetchLibraryItem(id)
|
||||
|
||||
const results = await BookFinder.search(libraryItem, provider, title, author)
|
||||
res.json(results)
|
||||
} catch (error) {
|
||||
Logger.error(`[SearchController] findBooks: ${error.message}`)
|
||||
if (error instanceof ValidationError || error instanceof NotFoundError) {
|
||||
return res.status(error.status).json({ error: error.message })
|
||||
}
|
||||
return res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
|
||||
const results = await BookFinder.search(libraryItem, provider, title, author)
|
||||
res.json(results)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -45,20 +111,24 @@ class SearchController {
|
|||
* @param {Response} res
|
||||
*/
|
||||
async findCovers(req, res) {
|
||||
const query = req.query
|
||||
const podcast = query.podcast == 1
|
||||
try {
|
||||
const query = req.query
|
||||
const podcast = query.podcast === '1' || query.podcast === 1
|
||||
const title = getQueryParamAsString(query, 'title', '', true)
|
||||
const author = getQueryParamAsString(query, 'author', '')
|
||||
const provider = getQueryParamAsString(query, 'provider', 'google')
|
||||
|
||||
if (!query.title || typeof query.title !== 'string') {
|
||||
Logger.error(`[SearchController] findCovers: Invalid title sent in query`)
|
||||
return res.sendStatus(400)
|
||||
let results = null
|
||||
if (podcast) results = await PodcastFinder.findCovers(title)
|
||||
else results = await BookFinder.findCovers(provider, title, author)
|
||||
res.json({ results })
|
||||
} catch (error) {
|
||||
Logger.error(`[SearchController] findCovers: ${error.message}`)
|
||||
if (error instanceof ValidationError) {
|
||||
return res.status(error.status).json({ error: error.message })
|
||||
}
|
||||
return res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
|
||||
let results = null
|
||||
if (podcast) results = await PodcastFinder.findCovers(query.title)
|
||||
else results = await BookFinder.findCovers(query.provider || 'google', query.title, query.author || '')
|
||||
res.json({
|
||||
results
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -69,34 +139,42 @@ class SearchController {
|
|||
* @param {Response} res
|
||||
*/
|
||||
async findPodcasts(req, res) {
|
||||
const term = req.query.term
|
||||
const country = req.query.country || 'us'
|
||||
if (!term) {
|
||||
Logger.error('[SearchController] Invalid request query param "term" is required')
|
||||
return res.status(400).send('Invalid request query param "term" is required')
|
||||
}
|
||||
try {
|
||||
const query = req.query
|
||||
const term = getQueryParamAsString(query, 'term', '', true)
|
||||
const country = getQueryParamAsString(query, 'country', 'us')
|
||||
|
||||
const results = await PodcastFinder.search(term, {
|
||||
country
|
||||
})
|
||||
res.json(results)
|
||||
const results = await PodcastFinder.search(term, { country })
|
||||
res.json(results)
|
||||
} catch (error) {
|
||||
Logger.error(`[SearchController] findPodcasts: ${error.message}`)
|
||||
if (error instanceof ValidationError) {
|
||||
return res.status(error.status).json({ error: error.message })
|
||||
}
|
||||
return res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/search/authors
|
||||
* Note: This endpoint is not currently used in the web client.
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async findAuthor(req, res) {
|
||||
const query = req.query.q
|
||||
if (!query || typeof query !== 'string') {
|
||||
Logger.error(`[SearchController] findAuthor: Invalid query param`)
|
||||
return res.status(400).send('Invalid query param')
|
||||
}
|
||||
try {
|
||||
const query = getQueryParamAsString(req.query, 'q', '', true)
|
||||
|
||||
const author = await AuthorFinder.findAuthorByName(query)
|
||||
res.json(author)
|
||||
const author = await AuthorFinder.findAuthorByName(query)
|
||||
res.json(author)
|
||||
} catch (error) {
|
||||
Logger.error(`[SearchController] findAuthor: ${error.message}`)
|
||||
if (error instanceof ValidationError) {
|
||||
return res.status(error.status).json({ error: error.message })
|
||||
}
|
||||
return res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -106,16 +184,55 @@ class SearchController {
|
|||
* @param {Response} res
|
||||
*/
|
||||
async findChapters(req, res) {
|
||||
const asin = req.query.asin
|
||||
if (!isValidASIN(asin.toUpperCase())) {
|
||||
return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' })
|
||||
try {
|
||||
const query = req.query
|
||||
const asin = getQueryParamAsString(query, 'asin', '', true)
|
||||
const region = getQueryParamAsString(req.query.region, 'us').toLowerCase()
|
||||
|
||||
if (!isValidASIN(asin.toUpperCase())) throw new ValidationError('asin', 'is invalid')
|
||||
|
||||
const chapterData = await BookFinder.findChapters(asin, region)
|
||||
if (!chapterData) {
|
||||
return res.json({ error: 'Chapters not found', stringKey: 'MessageChaptersNotFound' })
|
||||
}
|
||||
res.json(chapterData)
|
||||
} catch (error) {
|
||||
Logger.error(`[SearchController] findChapters: ${error.message}`)
|
||||
if (error instanceof ValidationError) {
|
||||
if (error.paramName === 'asin') {
|
||||
return res.json({ error: 'Invalid ASIN', stringKey: 'MessageInvalidAsin' })
|
||||
}
|
||||
if (error.paramName === 'region') {
|
||||
return res.json({ error: 'Invalid region', stringKey: 'MessageInvalidRegion' })
|
||||
}
|
||||
}
|
||||
return res.status(500).json({ error: 'Internal server error' })
|
||||
}
|
||||
const region = (req.query.region || 'us').toLowerCase()
|
||||
const chapterData = await BookFinder.findChapters(asin, region)
|
||||
if (!chapterData) {
|
||||
return res.json({ error: 'Chapters not found', stringKey: 'MessageChaptersNotFound' })
|
||||
}
|
||||
|
||||
/**
|
||||
* GET: /api/search/providers
|
||||
* Get all available metadata providers
|
||||
*
|
||||
* @param {RequestWithUser} req
|
||||
* @param {Response} res
|
||||
*/
|
||||
async getAllProviders(req, res) {
|
||||
const customProviders = await Database.customMetadataProviderModel.findAll()
|
||||
|
||||
const customBookProviders = customProviders.filter((p) => p.mediaType === 'book')
|
||||
const customPodcastProviders = customProviders.filter((p) => p.mediaType === 'podcast')
|
||||
|
||||
const bookProviders = BookFinder.providers.filter((p) => p !== 'audiobookcovers')
|
||||
|
||||
// Build minimized payload with custom providers merged in
|
||||
const providers = {
|
||||
books: [...bookProviders.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customBookProviders)],
|
||||
booksCovers: [SearchController.formatProvider('best'), ...BookFinder.providers.map((p) => SearchController.formatProvider(p)), ...SearchController.mapCustomProviders(customBookProviders), SearchController.formatProvider('all')],
|
||||
podcasts: [SearchController.formatProvider('itunes'), ...SearchController.mapCustomProviders(customPodcastProviders)]
|
||||
}
|
||||
res.json(chapterData)
|
||||
|
||||
res.json({ providers })
|
||||
}
|
||||
}
|
||||
module.exports = new SearchController()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue