mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-05-27 01:18:24 +02:00
Merge ad2c5c29e1
into fd84cd0d7f
This commit is contained in:
commit
884274ab4b
@ -12,42 +12,82 @@ export const state = () => ({
|
||||
text: 'iTunes',
|
||||
value: 'itunes'
|
||||
},
|
||||
{
|
||||
text: 'AudiMeta.com (Audible)',
|
||||
value: 'audimeta.us'
|
||||
},
|
||||
{
|
||||
text: 'Audible.com',
|
||||
value: 'audible'
|
||||
},
|
||||
{
|
||||
text: 'AudiMeta.ca (Audible)',
|
||||
value: 'audimeta.ca'
|
||||
},
|
||||
{
|
||||
text: 'Audible.ca',
|
||||
value: 'audible.ca'
|
||||
},
|
||||
{
|
||||
text: 'AudiMeta.co.uk (Audible)',
|
||||
value: 'audimeta.uk'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.uk',
|
||||
value: 'audible.uk'
|
||||
},
|
||||
{
|
||||
text: 'AudiMeta.com.au (Audible)',
|
||||
value: 'audimeta.au'
|
||||
},
|
||||
{
|
||||
text: 'Audible.com.au',
|
||||
value: 'audible.au'
|
||||
},
|
||||
{
|
||||
text: 'AudiMeta.fr (Audible)',
|
||||
value: 'audimeta.fr'
|
||||
},
|
||||
{
|
||||
text: 'Audible.fr',
|
||||
value: 'audible.fr'
|
||||
},
|
||||
{
|
||||
text: 'AudiMeta.de (Audible)',
|
||||
value: 'audimeta.de'
|
||||
},
|
||||
{
|
||||
text: 'Audible.de',
|
||||
value: 'audible.de'
|
||||
},
|
||||
{
|
||||
text: 'AudiMeta.jp (Audible)',
|
||||
value: 'audimeta.jp'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.jp',
|
||||
value: 'audible.jp'
|
||||
},
|
||||
{
|
||||
text: 'AudiMeta.it (Audible)',
|
||||
value: 'audimeta.it'
|
||||
},
|
||||
{
|
||||
text: 'Audible.it',
|
||||
value: 'audible.it'
|
||||
},
|
||||
{
|
||||
text: 'AudiMeta.co.in (Audible)',
|
||||
value: 'audimeta.in'
|
||||
},
|
||||
{
|
||||
text: 'Audible.co.in',
|
||||
value: 'audible.in'
|
||||
},
|
||||
{
|
||||
text: 'AudiMeta.es (Audible)',
|
||||
value: 'audimeta.es'
|
||||
},
|
||||
{
|
||||
text: 'Audible.es',
|
||||
value: 'audible.es'
|
||||
@ -72,11 +112,11 @@ export const state = () => ({
|
||||
})
|
||||
|
||||
export const getters = {
|
||||
checkBookProviderExists: state => (providerValue) => {
|
||||
return state.providers.some(p => p.value === providerValue)
|
||||
checkBookProviderExists: (state) => (providerValue) => {
|
||||
return state.providers.some((p) => p.value === providerValue)
|
||||
},
|
||||
checkPodcastProviderExists: state => (providerValue) => {
|
||||
return state.podcastProviders.some(p => p.value === providerValue)
|
||||
checkPodcastProviderExists: (state) => (providerValue) => {
|
||||
return state.podcastProviders.some((p) => p.value === providerValue)
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,13 +125,13 @@ export const actions = {}
|
||||
export const mutations = {
|
||||
addCustomMetadataProvider(state, provider) {
|
||||
if (provider.mediaType === 'book') {
|
||||
if (state.providers.some(p => p.value === provider.slug)) return
|
||||
if (state.providers.some((p) => p.value === provider.slug)) return
|
||||
state.providers.push({
|
||||
text: provider.name,
|
||||
value: provider.slug
|
||||
})
|
||||
} else {
|
||||
if (state.podcastProviders.some(p => p.value === provider.slug)) return
|
||||
if (state.podcastProviders.some((p) => p.value === provider.slug)) return
|
||||
state.podcastProviders.push({
|
||||
text: provider.name,
|
||||
value: provider.slug
|
||||
@ -100,9 +140,9 @@ export const mutations = {
|
||||
},
|
||||
removeCustomMetadataProvider(state, provider) {
|
||||
if (provider.mediaType === 'book') {
|
||||
state.providers = state.providers.filter(p => p.value !== provider.slug)
|
||||
state.providers = state.providers.filter((p) => p.value !== provider.slug)
|
||||
} else {
|
||||
state.podcastProviders = state.podcastProviders.filter(p => p.value !== provider.slug)
|
||||
state.podcastProviders = state.podcastProviders.filter((p) => p.value !== provider.slug)
|
||||
}
|
||||
},
|
||||
setCustomMetadataProviders(state, providers) {
|
||||
@ -123,4 +163,4 @@ export const mutations = {
|
||||
// Podcast providers not supported yet
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ const CustomProviderAdapter = require('../providers/CustomProviderAdapter')
|
||||
const Logger = require('../Logger')
|
||||
const { levenshteinDistance, escapeRegExp } = require('../utils/index')
|
||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||
const AudiMeta = require('../providers/AudiMeta')
|
||||
|
||||
class BookFinder {
|
||||
#providerResponseTimeout = 30000
|
||||
@ -17,13 +18,14 @@ class BookFinder {
|
||||
this.openLibrary = new OpenLibrary()
|
||||
this.googleBooks = new GoogleBooks()
|
||||
this.audible = new Audible()
|
||||
this.audiMeta = new AudiMeta()
|
||||
this.iTunesApi = new iTunes()
|
||||
this.audnexus = new Audnexus()
|
||||
this.fantLab = new FantLab()
|
||||
this.audiobookCovers = new AudiobookCovers()
|
||||
this.customProviderAdapter = new CustomProviderAdapter()
|
||||
|
||||
this.providers = ['google', 'itunes', 'openlibrary', 'fantlab', 'audiobookcovers', 'audible', 'audible.ca', 'audible.uk', 'audible.au', 'audible.fr', 'audible.de', 'audible.jp', 'audible.it', 'audible.in', 'audible.es']
|
||||
this.providers = ['google', 'itunes', 'openlibrary', 'fantlab', 'audiobookcovers', 'audimeta.us', 'audible', 'audimeta.ca', 'audible.ca', 'audimeta.uk', 'audible.uk', 'audimeta.au', 'audible.au', 'audimeta.fr', 'audible.fr', 'audimeta.de', 'audible.de', 'audimeta.jp', 'audible.jp', 'audimeta.it', 'audible.it', 'audimeta.in', 'audible.in', 'audimeta.es', 'audible.es']
|
||||
|
||||
this.verbose = false
|
||||
}
|
||||
@ -194,6 +196,24 @@ class BookFinder {
|
||||
return books
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} title
|
||||
* @param {string} author
|
||||
* @param {string} asin
|
||||
* @param {string} provider
|
||||
* @returns {Promise<Object[]>}
|
||||
*/
|
||||
async getAudiMetaResults(title, author, asin, provider) {
|
||||
// Ensure provider is a string (See CodeQL) even though it should be a string anyway
|
||||
const providerStr = (typeof provider === 'string' ? provider : Array.isArray(provider) ? provider[0]?.toString() || '' : '').toString()
|
||||
|
||||
const region = providerStr.includes('.') ? providerStr.split('.').pop() : ''
|
||||
const books = await this.audiMeta.search(title, author, asin, region, this.#providerResponseTimeout)
|
||||
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
||||
if (!books) return []
|
||||
return books
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} title
|
||||
@ -453,6 +473,8 @@ class BookFinder {
|
||||
books = await this.getGoogleBooksResults(title, author)
|
||||
} else if (provider.startsWith('audible')) {
|
||||
books = await this.getAudibleResults(title, author, asin, provider)
|
||||
} else if (provider.startsWith('audimeta')) {
|
||||
books = await this.getAudiMetaResults(title, author, asin, provider)
|
||||
} else if (provider === 'itunes') {
|
||||
books = await this.getiTunesAudiobooksResults(title)
|
||||
} else if (provider === 'openlibrary') {
|
||||
|
163
server/providers/AudiMeta.js
Normal file
163
server/providers/AudiMeta.js
Normal file
@ -0,0 +1,163 @@
|
||||
const axios = require('axios').default
|
||||
const Logger = require('../Logger')
|
||||
const { isValidASIN } = require('../utils/index')
|
||||
|
||||
class AudiMeta {
|
||||
#responseTimeout = 30000
|
||||
|
||||
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(`[AudiMeta] Series "${seriesName}" sequence was cleaned from "${sequence}" to "${updatedSequence}"`)
|
||||
}
|
||||
return updatedSequence
|
||||
}
|
||||
|
||||
cleanResult(item) {
|
||||
const { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, imageUrl, genres, series, language, lengthMinutes, bookFormat } = item
|
||||
|
||||
const seriesList = []
|
||||
|
||||
series.forEach((s) => {
|
||||
seriesList.push({
|
||||
series: s.name,
|
||||
sequence: this.cleanSeriesSequence(s.name, (s.position || '').toString())
|
||||
})
|
||||
})
|
||||
|
||||
// Tags and Genres are flipped for AudiMeta
|
||||
const genresFiltered = genres ? genres.filter((g) => g.type == 'Tags').map((g) => g.name) : []
|
||||
const tagsFiltered = genres ? genres.filter((g) => g.type == 'Genres').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: imageUrl,
|
||||
asin,
|
||||
genres: genresFiltered.length ? genresFiltered : null,
|
||||
tags: tagsFiltered.length ? tagsFiltered.join(', ') : null,
|
||||
series: seriesList.length ? seriesList : null,
|
||||
language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null,
|
||||
duration: lengthMinutes && !isNaN(lengthMinutes) ? Number(lengthMinutes) : 0,
|
||||
region: item.region || null,
|
||||
rating: item.rating || null,
|
||||
abridged: bookFormat === '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())
|
||||
let regionQuery = region ? `?region=${region}` : ''
|
||||
let url = `https://audimeta.de/book/${asin}${regionQuery}`
|
||||
Logger.debug(`[AudiMeta] 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)
|
||||
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(`[AudiMeta] 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.toUpperCase(), region, timeout)
|
||||
if (item) items.push(item)
|
||||
}
|
||||
|
||||
if (!items.length && isValidASIN(title.toUpperCase())) {
|
||||
const item = await this.asinSearch(title.toUpperCase(), region, timeout)
|
||||
if (item) items.push(item)
|
||||
}
|
||||
|
||||
if (!items.length) {
|
||||
const queryObj = {
|
||||
title: title,
|
||||
region: region,
|
||||
limit: '10'
|
||||
}
|
||||
if (author) queryObj.author = author
|
||||
const queryString = new URLSearchParams(queryObj).toString()
|
||||
|
||||
const url = `https://audimeta.de/search?${queryString}`
|
||||
Logger.debug(`[AudiMeta] Search url: ${url}`)
|
||||
items = await axios
|
||||
.get(url, {
|
||||
timeout
|
||||
})
|
||||
.then((res) => {
|
||||
return res.data
|
||||
})
|
||||
.catch((error) => {
|
||||
Logger.error('[AudiMeta] query search error', error)
|
||||
return []
|
||||
})
|
||||
}
|
||||
return items.filter(Boolean).map((item) => this.cleanResult(item)) || []
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AudiMeta
|
Loading…
Reference in New Issue
Block a user