mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2026-03-01 05:29:41 +00:00
- Update Audible provider to return series ASIN in search results - Pass series ASIN through Match.vue when selecting metadata - Update Book.updateSeriesFromRequest to forward ASIN to Series model - Update Scanner to use series ASIN during quick match When using the Audible metadata provider, the series ASIN is now automatically captured and stored with the series.
176 lines
5.5 KiB
JavaScript
176 lines
5.5 KiB
JavaScript
const axios = require('axios').default
|
|
const Logger = require('../Logger')
|
|
const { isValidASIN } = require('../utils/index')
|
|
|
|
class Audible {
|
|
#responseTimeout = 10000
|
|
|
|
constructor() {
|
|
this.regionMap = {
|
|
us: '.com',
|
|
ca: '.ca',
|
|
uk: '.co.uk',
|
|
au: '.com.au',
|
|
fr: '.fr',
|
|
de: '.de',
|
|
jp: '.co.jp',
|
|
it: '.it',
|
|
in: '.in',
|
|
es: '.es'
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Audible will sometimes send sequences with "Book 1" or "2, Dramatized Adaptation"
|
|
* @see https://github.com/advplyr/audiobookshelf/issues/2380
|
|
* @see https://github.com/advplyr/audiobookshelf/issues/1339
|
|
*
|
|
* @param {string} seriesName
|
|
* @param {string} sequence
|
|
* @returns {string}
|
|
*/
|
|
cleanSeriesSequence(seriesName, sequence) {
|
|
if (!sequence) return ''
|
|
// match any number with optional decimal (e.g, 1 or 1.5 or .5)
|
|
let numberFound = sequence.match(/\.\d+|\d+(?:\.\d+)?/)
|
|
let updatedSequence = numberFound ? numberFound[0] : sequence
|
|
if (sequence !== updatedSequence) {
|
|
Logger.debug(`[Audible] Series "${seriesName}" sequence was cleaned from "${sequence}" to "${updatedSequence}"`)
|
|
}
|
|
return updatedSequence
|
|
}
|
|
|
|
cleanResult(item) {
|
|
const { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language, runtimeLengthMin, formatType, isbn } = item
|
|
|
|
const series = []
|
|
if (seriesPrimary) {
|
|
series.push({
|
|
series: seriesPrimary.name,
|
|
sequence: this.cleanSeriesSequence(seriesPrimary.name, seriesPrimary.position || ''),
|
|
asin: seriesPrimary.asin || null
|
|
})
|
|
}
|
|
if (seriesSecondary) {
|
|
series.push({
|
|
series: seriesSecondary.name,
|
|
sequence: this.cleanSeriesSequence(seriesSecondary.name, seriesSecondary.position || ''),
|
|
asin: seriesSecondary.asin || null
|
|
})
|
|
}
|
|
|
|
let genresCleaned = []
|
|
let tagsCleaned = []
|
|
|
|
if (genres && Array.isArray(genres)) {
|
|
genresCleaned = [...new Set(genres.filter((g) => g.type == 'genre').map((g) => g.name))]
|
|
tagsCleaned = [...new Set(genres.filter((g) => g.type == 'tag').map((g) => g.name))]
|
|
}
|
|
|
|
return {
|
|
title,
|
|
subtitle: subtitle || null,
|
|
author: authors ? authors.map(({ name }) => name).join(', ') : null,
|
|
narrator: narrators ? narrators.map(({ name }) => name).join(', ') : null,
|
|
publisher: publisherName,
|
|
publishedYear: releaseDate ? releaseDate.split('-')[0] : null,
|
|
description: summary || null,
|
|
cover: image,
|
|
asin,
|
|
isbn,
|
|
genres: genresCleaned.length ? genresCleaned : null,
|
|
tags: tagsCleaned.length ? tagsCleaned : null,
|
|
series: series.length ? series : null,
|
|
language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null,
|
|
duration: runtimeLengthMin && !isNaN(runtimeLengthMin) ? Number(runtimeLengthMin) : 0,
|
|
region: item.region || null,
|
|
rating: item.rating || null,
|
|
abridged: formatType === 'abridged'
|
|
}
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} asin
|
|
* @param {string} region
|
|
* @param {number} [timeout] response timeout in ms
|
|
* @returns {Promise<Object>}
|
|
*/
|
|
asinSearch(asin, region, timeout = this.#responseTimeout) {
|
|
if (!asin) return null
|
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
|
|
|
asin = encodeURIComponent(asin.toUpperCase())
|
|
var regionQuery = region ? `?region=${region}` : ''
|
|
var url = `https://api.audnex.us/books/${asin}${regionQuery}`
|
|
Logger.debug(`[Audible] ASIN url: ${url}`)
|
|
return axios
|
|
.get(url, {
|
|
timeout
|
|
})
|
|
.then((res) => {
|
|
if (!res?.data?.asin) return null
|
|
return res.data
|
|
})
|
|
.catch((error) => {
|
|
Logger.error('[Audible] ASIN search error', error.message)
|
|
return null
|
|
})
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} title
|
|
* @param {string} author
|
|
* @param {string} asin
|
|
* @param {string} region
|
|
* @param {number} [timeout] response timeout in ms
|
|
* @returns {Promise<Object[]>}
|
|
*/
|
|
async search(title, author, asin, region, timeout = this.#responseTimeout) {
|
|
if (region && !this.regionMap[region]) {
|
|
Logger.error(`[Audible] search: Invalid region ${region}`)
|
|
region = ''
|
|
}
|
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
|
|
|
let items = []
|
|
if (asin && isValidASIN(asin.toUpperCase())) {
|
|
const item = await this.asinSearch(asin, region, timeout)
|
|
if (item) items.push(item)
|
|
}
|
|
|
|
if (!items.length && isValidASIN(title.toUpperCase())) {
|
|
const item = await this.asinSearch(title, region, timeout)
|
|
if (item) items.push(item)
|
|
}
|
|
|
|
if (!items.length) {
|
|
const queryObj = {
|
|
num_results: '10',
|
|
products_sort_by: 'Relevance',
|
|
title: title
|
|
}
|
|
if (author) queryObj.author = author
|
|
const queryString = new URLSearchParams(queryObj).toString()
|
|
const tld = region ? this.regionMap[region] : '.com'
|
|
const url = `https://api.audible${tld}/1.0/catalog/products?${queryString}`
|
|
Logger.debug(`[Audible] Search url: ${url}`)
|
|
items = await axios
|
|
.get(url, {
|
|
timeout
|
|
})
|
|
.then((res) => {
|
|
if (!res?.data?.products) return null
|
|
return Promise.all(res.data.products.map((result) => this.asinSearch(result.asin, region, timeout)))
|
|
})
|
|
.catch((error) => {
|
|
Logger.error('[Audible] query search error', error.message)
|
|
return []
|
|
})
|
|
}
|
|
return items.filter(Boolean).map((item) => this.cleanResult(item)) || []
|
|
}
|
|
}
|
|
|
|
module.exports = Audible
|