mirror of
https://github.com/advplyr/audiobookshelf.git
synced 2025-04-30 01:15:24 +02:00
Fix:Add timeout to provider matching default to 30s #3000
This commit is contained in:
parent
30d3e41542
commit
6fa49e0aab
@ -10,6 +10,8 @@ const Logger = require('../Logger')
|
|||||||
const { levenshteinDistance, escapeRegExp } = require('../utils/index')
|
const { levenshteinDistance, escapeRegExp } = require('../utils/index')
|
||||||
|
|
||||||
class BookFinder {
|
class BookFinder {
|
||||||
|
#providerResponseTimeout = 30000
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.openLibrary = new OpenLibrary()
|
this.openLibrary = new OpenLibrary()
|
||||||
this.googleBooks = new GoogleBooks()
|
this.googleBooks = new GoogleBooks()
|
||||||
@ -36,63 +38,75 @@ class BookFinder {
|
|||||||
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
filterSearchResults(books, title, author, maxTitleDistance, maxAuthorDistance) {
|
||||||
var searchTitle = cleanTitleForCompares(title)
|
var searchTitle = cleanTitleForCompares(title)
|
||||||
var searchAuthor = cleanAuthorForCompares(author)
|
var searchAuthor = cleanAuthorForCompares(author)
|
||||||
return books.map(b => {
|
return books
|
||||||
b.cleanedTitle = cleanTitleForCompares(b.title)
|
.map((b) => {
|
||||||
b.titleDistance = levenshteinDistance(b.cleanedTitle, title)
|
b.cleanedTitle = cleanTitleForCompares(b.title)
|
||||||
|
b.titleDistance = levenshteinDistance(b.cleanedTitle, title)
|
||||||
|
|
||||||
// Total length of search (title or both title & author)
|
// Total length of search (title or both title & author)
|
||||||
b.totalPossibleDistance = b.title.length
|
b.totalPossibleDistance = b.title.length
|
||||||
|
|
||||||
if (author) {
|
if (author) {
|
||||||
if (!b.author) {
|
if (!b.author) {
|
||||||
b.authorDistance = author.length
|
b.authorDistance = author.length
|
||||||
} else {
|
} else {
|
||||||
b.totalPossibleDistance += b.author.length
|
b.totalPossibleDistance += b.author.length
|
||||||
b.cleanedAuthor = cleanAuthorForCompares(b.author)
|
b.cleanedAuthor = cleanAuthorForCompares(b.author)
|
||||||
|
|
||||||
var cleanedAuthorDistance = levenshteinDistance(b.cleanedAuthor, searchAuthor)
|
var cleanedAuthorDistance = levenshteinDistance(b.cleanedAuthor, searchAuthor)
|
||||||
var authorDistance = levenshteinDistance(b.author || '', author)
|
var authorDistance = levenshteinDistance(b.author || '', author)
|
||||||
|
|
||||||
// Use best distance
|
// Use best distance
|
||||||
b.authorDistance = Math.min(cleanedAuthorDistance, authorDistance)
|
b.authorDistance = Math.min(cleanedAuthorDistance, authorDistance)
|
||||||
|
|
||||||
// Check book author contains searchAuthor
|
// Check book author contains searchAuthor
|
||||||
if (searchAuthor.length > 4 && b.cleanedAuthor.includes(searchAuthor)) b.includesAuthor = searchAuthor
|
if (searchAuthor.length > 4 && b.cleanedAuthor.includes(searchAuthor)) b.includesAuthor = searchAuthor
|
||||||
else if (author.length > 4 && b.author.includes(author)) b.includesAuthor = author
|
else if (author.length > 4 && b.author.includes(author)) b.includesAuthor = author
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
b.totalDistance = b.titleDistance + (b.authorDistance || 0)
|
||||||
b.totalDistance = b.titleDistance + (b.authorDistance || 0)
|
|
||||||
|
|
||||||
// Check book title contains the searchTitle
|
// Check book title contains the searchTitle
|
||||||
if (searchTitle.length > 4 && b.cleanedTitle.includes(searchTitle)) b.includesTitle = searchTitle
|
if (searchTitle.length > 4 && b.cleanedTitle.includes(searchTitle)) b.includesTitle = searchTitle
|
||||||
else if (title.length > 4 && b.title.includes(title)) b.includesTitle = title
|
else if (title.length > 4 && b.title.includes(title)) b.includesTitle = title
|
||||||
|
|
||||||
return b
|
return b
|
||||||
}).filter(b => {
|
})
|
||||||
if (b.includesTitle) { // If search title was found in result title then skip over leven distance check
|
.filter((b) => {
|
||||||
if (this.verbose) Logger.debug(`Exact title was included in "${b.title}", Search: "${b.includesTitle}"`)
|
if (b.includesTitle) {
|
||||||
} else if (b.titleDistance > maxTitleDistance) {
|
// If search title was found in result title then skip over leven distance check
|
||||||
if (this.verbose) Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`)
|
if (this.verbose) Logger.debug(`Exact title was included in "${b.title}", Search: "${b.includesTitle}"`)
|
||||||
return false
|
} else if (b.titleDistance > maxTitleDistance) {
|
||||||
}
|
if (this.verbose) Logger.debug(`Filtering out search result title distance = ${b.titleDistance}: "${b.cleanedTitle}"/"${searchTitle}"`)
|
||||||
|
|
||||||
if (author) {
|
|
||||||
if (b.includesAuthor) { // If search author was found in result author then skip over leven distance check
|
|
||||||
if (this.verbose) Logger.debug(`Exact author was included in "${b.author}", Search: "${b.includesAuthor}"`)
|
|
||||||
} else if (b.authorDistance > maxAuthorDistance) {
|
|
||||||
if (this.verbose) Logger.debug(`Filtering out search result "${b.author}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`)
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If book total search length < 5 and was not exact match, then filter out
|
if (author) {
|
||||||
if (b.totalPossibleDistance < 5 && b.totalDistance > 0) return false
|
if (b.includesAuthor) {
|
||||||
return true
|
// If search author was found in result author then skip over leven distance check
|
||||||
})
|
if (this.verbose) Logger.debug(`Exact author was included in "${b.author}", Search: "${b.includesAuthor}"`)
|
||||||
|
} else if (b.authorDistance > maxAuthorDistance) {
|
||||||
|
if (this.verbose) Logger.debug(`Filtering out search result "${b.author}", author distance = ${b.authorDistance}: "${b.author}"/"${author}"`)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If book total search length < 5 and was not exact match, then filter out
|
||||||
|
if (b.totalPossibleDistance < 5 && b.totalDistance > 0) return false
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @param {number} maxTitleDistance
|
||||||
|
* @param {number} maxAuthorDistance
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
async getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance) {
|
||||||
var books = await this.openLibrary.searchTitle(title)
|
var books = await this.openLibrary.searchTitle(title, this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`OpenLib Book Search Results: ${books.length || 0}`)
|
||||||
if (books.errorCode) {
|
if (books.errorCode) {
|
||||||
Logger.error(`OpenLib Search Error ${books.errorCode}`)
|
Logger.error(`OpenLib Search Error ${books.errorCode}`)
|
||||||
@ -109,8 +123,14 @@ class BookFinder {
|
|||||||
return booksFiltered
|
return booksFiltered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
async getGoogleBooksResults(title, author) {
|
async getGoogleBooksResults(title, author) {
|
||||||
var books = await this.googleBooks.search(title, author)
|
var books = await this.googleBooks.search(title, author, this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`GoogleBooks Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`GoogleBooks Book Search Results: ${books.length || 0}`)
|
||||||
if (books.errorCode) {
|
if (books.errorCode) {
|
||||||
Logger.error(`GoogleBooks Search Error ${books.errorCode}`)
|
Logger.error(`GoogleBooks Search Error ${books.errorCode}`)
|
||||||
@ -120,8 +140,14 @@ class BookFinder {
|
|||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
async getFantLabResults(title, author) {
|
async getFantLabResults(title, author) {
|
||||||
var books = await this.fantLab.search(title, author)
|
var books = await this.fantLab.search(title, author, this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`FantLab Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`FantLab Book Search Results: ${books.length || 0}`)
|
||||||
if (books.errorCode) {
|
if (books.errorCode) {
|
||||||
Logger.error(`FantLab Search Error ${books.errorCode}`)
|
Logger.error(`FantLab Search Error ${books.errorCode}`)
|
||||||
@ -131,19 +157,37 @@ class BookFinder {
|
|||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} search
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
async getAudiobookCoversResults(search) {
|
async getAudiobookCoversResults(search) {
|
||||||
const covers = await this.audiobookCovers.search(search)
|
const covers = await this.audiobookCovers.search(search, this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`AudiobookCovers Search Results: ${covers.length || 0}`)
|
if (this.verbose) Logger.debug(`AudiobookCovers Search Results: ${covers.length || 0}`)
|
||||||
return covers || []
|
return covers || []
|
||||||
}
|
}
|
||||||
|
|
||||||
async getiTunesAudiobooksResults(title, author) {
|
/**
|
||||||
return this.iTunesApi.searchAudiobooks(title)
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
async getiTunesAudiobooksResults(title) {
|
||||||
|
return this.iTunesApi.searchAudiobooks(title, this.#providerResponseTimeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @param {string} asin
|
||||||
|
* @param {string} provider
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
async getAudibleResults(title, author, asin, provider) {
|
async getAudibleResults(title, author, asin, provider) {
|
||||||
const region = provider.includes('.') ? provider.split('.').pop() : ''
|
const region = provider.includes('.') ? provider.split('.').pop() : ''
|
||||||
const books = await this.audible.search(title, author, asin, region)
|
const books = await this.audible.search(title, author, asin, region, this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`Audible Book Search Results: ${books.length || 0}`)
|
||||||
if (!books) return []
|
if (!books) return []
|
||||||
return books
|
return books
|
||||||
@ -158,14 +202,13 @@ class BookFinder {
|
|||||||
* @returns {Promise<Object[]>}
|
* @returns {Promise<Object[]>}
|
||||||
*/
|
*/
|
||||||
async getCustomProviderResults(title, author, isbn, providerSlug) {
|
async getCustomProviderResults(title, author, isbn, providerSlug) {
|
||||||
const books = await this.customProviderAdapter.search(title, author, isbn, providerSlug, 'book')
|
const books = await this.customProviderAdapter.search(title, author, isbn, providerSlug, 'book', this.#providerResponseTimeout)
|
||||||
if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`)
|
if (this.verbose) Logger.debug(`Custom provider '${providerSlug}' Search Results: ${books.length || 0}`)
|
||||||
|
|
||||||
return books
|
return books
|
||||||
}
|
}
|
||||||
|
|
||||||
static TitleCandidates = class {
|
static TitleCandidates = class {
|
||||||
|
|
||||||
constructor(cleanAuthor) {
|
constructor(cleanAuthor) {
|
||||||
this.candidates = new Set()
|
this.candidates = new Set()
|
||||||
this.cleanAuthor = cleanAuthor
|
this.cleanAuthor = cleanAuthor
|
||||||
@ -179,13 +222,13 @@ class BookFinder {
|
|||||||
title = this.#removeAuthorFromTitle(title)
|
title = this.#removeAuthorFromTitle(title)
|
||||||
|
|
||||||
const titleTransformers = [
|
const titleTransformers = [
|
||||||
[/([,:;_]| by ).*/g, ''], // Remove subtitle
|
[/([,:;_]| by ).*/g, ''], // Remove subtitle
|
||||||
[/(^| )\d+k(bps)?( |$)/, ' '], // Remove bitrate
|
[/(^| )\d+k(bps)?( |$)/, ' '], // Remove bitrate
|
||||||
[/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''], // Remove edition
|
[/ (2nd|3rd|\d+th)\s+ed(\.|ition)?/g, ''], // Remove edition
|
||||||
[/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type
|
[/(^| |\.)(m4b|m4a|mp3)( |$)/g, ''], // Remove file-type
|
||||||
[/ a novel.*$/g, ''], // Remove "a novel"
|
[/ a novel.*$/g, ''], // Remove "a novel"
|
||||||
[/(^| )(un)?abridged( |$)/g, ' '], // Remove "unabridged/abridged"
|
[/(^| )(un)?abridged( |$)/g, ' '], // Remove "unabridged/abridged"
|
||||||
[/^\d+ | \d+$/g, ''], // Remove preceding/trailing numbers
|
[/^\d+ | \d+$/g, ''] // Remove preceding/trailing numbers
|
||||||
]
|
]
|
||||||
|
|
||||||
// Main variant
|
// Main variant
|
||||||
@ -197,8 +240,7 @@ class BookFinder {
|
|||||||
|
|
||||||
let candidate = cleanTitle
|
let candidate = cleanTitle
|
||||||
|
|
||||||
for (const transformer of titleTransformers)
|
for (const transformer of titleTransformers) candidate = candidate.replace(transformer[0], transformer[1]).trim()
|
||||||
candidate = candidate.replace(transformer[0], transformer[1]).trim()
|
|
||||||
|
|
||||||
if (candidate != cleanTitle) {
|
if (candidate != cleanTitle) {
|
||||||
if (candidate) {
|
if (candidate) {
|
||||||
@ -240,7 +282,7 @@ class BookFinder {
|
|||||||
|
|
||||||
#removeAuthorFromTitle(title) {
|
#removeAuthorFromTitle(title) {
|
||||||
if (!this.cleanAuthor) return title
|
if (!this.cleanAuthor) return title
|
||||||
const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, "g")
|
const authorRe = new RegExp(`(^| | by |)${escapeRegExp(this.cleanAuthor)}(?= |$)`, 'g')
|
||||||
const authorCleanedTitle = cleanAuthorForCompares(title)
|
const authorCleanedTitle = cleanAuthorForCompares(title)
|
||||||
const authorCleanedTitleWithoutAuthor = authorCleanedTitle.replace(authorRe, '')
|
const authorCleanedTitleWithoutAuthor = authorCleanedTitle.replace(authorRe, '')
|
||||||
if (authorCleanedTitleWithoutAuthor !== authorCleanedTitle) {
|
if (authorCleanedTitleWithoutAuthor !== authorCleanedTitle) {
|
||||||
@ -297,7 +339,7 @@ class BookFinder {
|
|||||||
promises.push(this.validateAuthor(candidate))
|
promises.push(this.validateAuthor(candidate))
|
||||||
}
|
}
|
||||||
const results = [...new Set(await Promise.all(promises))]
|
const results = [...new Set(await Promise.all(promises))]
|
||||||
filteredCandidates = results.filter(author => author)
|
filteredCandidates = results.filter((author) => author)
|
||||||
// If no valid candidates were found, add back an aggresively cleaned author version
|
// If no valid candidates were found, add back an aggresively cleaned author version
|
||||||
if (!filteredCandidates.length && this.cleanAuthor) filteredCandidates.push(this.agressivelyCleanAuthor)
|
if (!filteredCandidates.length && this.cleanAuthor) filteredCandidates.push(this.agressivelyCleanAuthor)
|
||||||
// Always add an empty author candidate
|
// Always add an empty author candidate
|
||||||
@ -312,7 +354,6 @@ class BookFinder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search for books including fuzzy searches
|
* Search for books including fuzzy searches
|
||||||
*
|
*
|
||||||
@ -337,8 +378,7 @@ class BookFinder {
|
|||||||
return this.getCustomProviderResults(title, author, isbn, provider)
|
return this.getCustomProviderResults(title, author, isbn, provider)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!title)
|
if (!title) return books
|
||||||
return books
|
|
||||||
|
|
||||||
books = await this.runSearch(title, author, provider, asin, maxTitleDistance, maxAuthorDistance)
|
books = await this.runSearch(title, author, provider, asin, maxTitleDistance, maxAuthorDistance)
|
||||||
|
|
||||||
@ -353,17 +393,14 @@ class BookFinder {
|
|||||||
let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus)
|
let authorCandidates = new BookFinder.AuthorCandidates(cleanAuthor, this.audnexus)
|
||||||
|
|
||||||
// Remove underscores and parentheses with their contents, and replace with a separator
|
// Remove underscores and parentheses with their contents, and replace with a separator
|
||||||
const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, " - ")
|
const cleanTitle = title.replace(/\[.*?\]|\(.*?\)|{.*?}|_/g, ' - ')
|
||||||
// Split title into hypen-separated parts
|
// Split title into hypen-separated parts
|
||||||
const titleParts = cleanTitle.split(/ - | -|- /)
|
const titleParts = cleanTitle.split(/ - | -|- /)
|
||||||
for (const titlePart of titleParts)
|
for (const titlePart of titleParts) authorCandidates.add(titlePart)
|
||||||
authorCandidates.add(titlePart)
|
|
||||||
authorCandidates = await authorCandidates.getCandidates()
|
authorCandidates = await authorCandidates.getCandidates()
|
||||||
loop_author:
|
loop_author: for (const authorCandidate of authorCandidates) {
|
||||||
for (const authorCandidate of authorCandidates) {
|
|
||||||
let titleCandidates = new BookFinder.TitleCandidates(authorCandidate)
|
let titleCandidates = new BookFinder.TitleCandidates(authorCandidate)
|
||||||
for (const titlePart of titleParts)
|
for (const titlePart of titleParts) titleCandidates.add(titlePart)
|
||||||
titleCandidates.add(titlePart)
|
|
||||||
titleCandidates = titleCandidates.getCandidates()
|
titleCandidates = titleCandidates.getCandidates()
|
||||||
for (const titleCandidate of titleCandidates) {
|
for (const titleCandidate of titleCandidates) {
|
||||||
if (titleCandidate == title && authorCandidate == author) continue // We already tried this
|
if (titleCandidate == title && authorCandidate == author) continue // We already tried this
|
||||||
@ -412,7 +449,7 @@ class BookFinder {
|
|||||||
} else if (provider.startsWith('audible')) {
|
} else if (provider.startsWith('audible')) {
|
||||||
books = await this.getAudibleResults(title, author, asin, provider)
|
books = await this.getAudibleResults(title, author, asin, provider)
|
||||||
} else if (provider === 'itunes') {
|
} else if (provider === 'itunes') {
|
||||||
books = await this.getiTunesAudiobooksResults(title, author)
|
books = await this.getiTunesAudiobooksResults(title)
|
||||||
} else if (provider === 'openlibrary') {
|
} else if (provider === 'openlibrary') {
|
||||||
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
books = await this.getOpenLibResults(title, author, maxTitleDistance, maxAuthorDistance)
|
||||||
} else if (provider === 'fantlab') {
|
} else if (provider === 'fantlab') {
|
||||||
@ -448,7 +485,7 @@ class BookFinder {
|
|||||||
covers.push(result.cover)
|
covers.push(result.cover)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return [...(new Set(covers))]
|
return [...new Set(covers)]
|
||||||
}
|
}
|
||||||
|
|
||||||
findChapters(asin, region) {
|
findChapters(asin, region) {
|
||||||
@ -468,7 +505,7 @@ function stripSubtitle(title) {
|
|||||||
|
|
||||||
function replaceAccentedChars(str) {
|
function replaceAccentedChars(str) {
|
||||||
try {
|
try {
|
||||||
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, "")
|
return str.normalize('NFD').replace(/[\u0300-\u036f]/g, '')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
Logger.error('[BookFinder] str normalize error', error)
|
Logger.error('[BookFinder] str normalize error', error)
|
||||||
return str
|
return str
|
||||||
@ -483,7 +520,7 @@ function cleanTitleForCompares(title) {
|
|||||||
let stripped = stripSubtitle(title)
|
let stripped = stripSubtitle(title)
|
||||||
|
|
||||||
// Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
|
// Remove text in paranthesis (i.e. "Ender's Game (Ender's Saga)" becomes "Ender's Game")
|
||||||
let cleaned = stripped.replace(/ *\([^)]*\) */g, "")
|
let cleaned = stripped.replace(/ *\([^)]*\) */g, '')
|
||||||
|
|
||||||
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
// Remove single quotes (i.e. "Ender's Game" becomes "Enders Game")
|
||||||
cleaned = cleaned.replace(/'/g, '')
|
cleaned = cleaned.replace(/'/g, '')
|
||||||
|
@ -1,145 +1,176 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios').default
|
||||||
const htmlSanitizer = require('../utils/htmlSanitizer')
|
const htmlSanitizer = require('../utils/htmlSanitizer')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class Audible {
|
class Audible {
|
||||||
constructor() {
|
#responseTimeout = 30000
|
||||||
this.regionMap = {
|
|
||||||
'us': '.com',
|
constructor() {
|
||||||
'ca': '.ca',
|
this.regionMap = {
|
||||||
'uk': '.co.uk',
|
us: '.com',
|
||||||
'au': '.com.au',
|
ca: '.ca',
|
||||||
'fr': '.fr',
|
uk: '.co.uk',
|
||||||
'de': '.de',
|
au: '.com.au',
|
||||||
'jp': '.co.jp',
|
fr: '.fr',
|
||||||
'it': '.it',
|
de: '.de',
|
||||||
'in': '.in',
|
jp: '.co.jp',
|
||||||
'es': '.es'
|
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 } = item
|
||||||
|
|
||||||
|
const series = []
|
||||||
|
if (seriesPrimary) {
|
||||||
|
series.push({
|
||||||
|
series: seriesPrimary.name,
|
||||||
|
sequence: this.cleanSeriesSequence(seriesPrimary.name, seriesPrimary.position || '')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (seriesSecondary) {
|
||||||
|
series.push({
|
||||||
|
series: seriesSecondary.name,
|
||||||
|
sequence: this.cleanSeriesSequence(seriesSecondary.name, seriesSecondary.position || '')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const genresFiltered = genres ? genres.filter((g) => g.type == 'genre').map((g) => g.name) : []
|
||||||
* Audible will sometimes send sequences with "Book 1" or "2, Dramatized Adaptation"
|
const tagsFiltered = genres ? genres.filter((g) => g.type == 'tag').map((g) => g.name) : []
|
||||||
* @see https://github.com/advplyr/audiobookshelf/issues/2380
|
|
||||||
* @see https://github.com/advplyr/audiobookshelf/issues/1339
|
return {
|
||||||
*
|
title,
|
||||||
* @param {string} seriesName
|
subtitle: subtitle || null,
|
||||||
* @param {string} sequence
|
author: authors ? authors.map(({ name }) => name).join(', ') : null,
|
||||||
* @returns {string}
|
narrator: narrators ? narrators.map(({ name }) => name).join(', ') : null,
|
||||||
*/
|
publisher: publisherName,
|
||||||
cleanSeriesSequence(seriesName, sequence) {
|
publishedYear: releaseDate ? releaseDate.split('-')[0] : null,
|
||||||
if (!sequence) return ''
|
description: summary ? htmlSanitizer.stripAllTags(summary) : null,
|
||||||
// match any number with optional decimal (e.g, 1 or 1.5 or .5)
|
cover: image,
|
||||||
let numberFound = sequence.match(/\.\d+|\d+(?:\.\d+)?/)
|
asin,
|
||||||
let updatedSequence = numberFound ? numberFound[0] : sequence
|
genres: genresFiltered.length ? genresFiltered : null,
|
||||||
if (sequence !== updatedSequence) {
|
tags: tagsFiltered.length ? tagsFiltered.join(', ') : null,
|
||||||
Logger.debug(`[Audible] Series "${seriesName}" sequence was cleaned from "${sequence}" to "${updatedSequence}"`)
|
series: series.length ? series : null,
|
||||||
}
|
language: language ? language.charAt(0).toUpperCase() + language.slice(1) : null,
|
||||||
return updatedSequence
|
duration: runtimeLengthMin && !isNaN(runtimeLengthMin) ? Number(runtimeLengthMin) : 0,
|
||||||
|
region: item.region || null,
|
||||||
|
rating: item.rating || null,
|
||||||
|
abridged: formatType === 'abridged'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if a search title matches an ASIN. Supports lowercase letters
|
||||||
|
*
|
||||||
|
* @param {string} title
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isProbablyAsin(title) {
|
||||||
|
return /^[0-9A-Za-z]{10}$/.test(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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 []
|
||||||
|
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 || !res.data || !res.data.asin) return null
|
||||||
|
return res.data
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error('[Audible] ASIN search error', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @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) {
|
||||||
|
items = [await this.asinSearch(asin, region, timeout)]
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanResult(item) {
|
if (!items && this.isProbablyAsin(title)) {
|
||||||
const { title, subtitle, asin, authors, narrators, publisherName, summary, releaseDate, image, genres, seriesPrimary, seriesSecondary, language, runtimeLengthMin, formatType } = item
|
items = [await this.asinSearch(title, region, timeout)]
|
||||||
|
|
||||||
const series = []
|
|
||||||
if (seriesPrimary) {
|
|
||||||
series.push({
|
|
||||||
series: seriesPrimary.name,
|
|
||||||
sequence: this.cleanSeriesSequence(seriesPrimary.name, seriesPrimary.position || '')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (seriesSecondary) {
|
|
||||||
series.push({
|
|
||||||
series: seriesSecondary.name,
|
|
||||||
sequence: this.cleanSeriesSequence(seriesSecondary.name, seriesSecondary.position || '')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const genresFiltered = genres ? genres.filter(g => g.type == "genre").map(g => g.name) : []
|
|
||||||
const tagsFiltered = genres ? 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 ? htmlSanitizer.stripAllTags(summary) : null,
|
|
||||||
cover: image,
|
|
||||||
asin,
|
|
||||||
genres: genresFiltered.length ? genresFiltered : null,
|
|
||||||
tags: tagsFiltered.length ? tagsFiltered.join(', ') : 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'
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
if (!items) {
|
||||||
* Test if a search title matches an ASIN. Supports lowercase letters
|
const queryObj = {
|
||||||
*
|
num_results: '10',
|
||||||
* @param {string} title
|
products_sort_by: 'Relevance',
|
||||||
* @returns {boolean}
|
title: title
|
||||||
*/
|
}
|
||||||
isProbablyAsin(title) {
|
if (author) queryObj.author = author
|
||||||
return /^[0-9A-Za-z]{10}$/.test(title)
|
const queryString = new URLSearchParams(queryObj).toString()
|
||||||
}
|
const tld = region ? this.regionMap[region] : '.com'
|
||||||
|
const url = `https://api.audible${tld}/1.0/catalog/products?${queryString}`
|
||||||
asinSearch(asin, region) {
|
Logger.debug(`[Audible] Search url: ${url}`)
|
||||||
if (!asin) return []
|
items = await axios
|
||||||
asin = encodeURIComponent(asin.toUpperCase())
|
.get(url, {
|
||||||
var regionQuery = region ? `?region=${region}` : ''
|
timeout
|
||||||
var url = `https://api.audnex.us/books/${asin}${regionQuery}`
|
})
|
||||||
Logger.debug(`[Audible] ASIN url: ${url}`)
|
.then((res) => {
|
||||||
return axios.get(url).then((res) => {
|
if (!res?.data?.products) return null
|
||||||
if (!res || !res.data || !res.data.asin) return null
|
return Promise.all(res.data.products.map((result) => this.asinSearch(result.asin, region, timeout)))
|
||||||
return res.data
|
})
|
||||||
}).catch(error => {
|
.catch((error) => {
|
||||||
Logger.error('[Audible] ASIN search error', error)
|
Logger.error('[Audible] query search error', error)
|
||||||
return []
|
return []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return items?.map((item) => this.cleanResult(item)) || []
|
||||||
async search(title, author, asin, region) {
|
}
|
||||||
if (region && !this.regionMap[region]) {
|
|
||||||
Logger.error(`[Audible] search: Invalid region ${region}`)
|
|
||||||
region = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
let items
|
|
||||||
if (asin) {
|
|
||||||
items = [await this.asinSearch(asin, region)]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!items && this.isProbablyAsin(title)) {
|
|
||||||
items = [await this.asinSearch(title, region)]
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!items) {
|
|
||||||
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).then((res) => {
|
|
||||||
if (!res?.data?.products) return null
|
|
||||||
return Promise.all(res.data.products.map(result => this.asinSearch(result.asin, region)))
|
|
||||||
}).catch(error => {
|
|
||||||
Logger.error('[Audible] query search error', error)
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return items ? items.map(item => this.cleanResult(item)) : []
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Audible
|
module.exports = Audible
|
@ -2,22 +2,32 @@ const axios = require('axios')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class AudiobookCovers {
|
class AudiobookCovers {
|
||||||
constructor() { }
|
#responseTimeout = 30000
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} search
|
||||||
|
* @param {number} [timeout]
|
||||||
|
* @returns {Promise<{cover: string}[]>}
|
||||||
|
*/
|
||||||
|
async search(search, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
async search(search) {
|
|
||||||
const url = `https://api.audiobookcovers.com/cover/bytext/`
|
const url = `https://api.audiobookcovers.com/cover/bytext/`
|
||||||
const params = new URLSearchParams([['q', search]])
|
const params = new URLSearchParams([['q', search]])
|
||||||
const items = await axios.get(url, { params }).then((res) => {
|
const items = await axios
|
||||||
if (!res || !res.data) return []
|
.get(url, {
|
||||||
return res.data
|
params,
|
||||||
}).catch(error => {
|
timeout
|
||||||
Logger.error('[AudiobookCovers] Cover search error', error)
|
})
|
||||||
return []
|
.then((res) => res?.data || [])
|
||||||
})
|
.catch((error) => {
|
||||||
return items.map(item => ({ cover: item.versions.png.original }))
|
Logger.error('[AudiobookCovers] Cover search error', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
return items.map((item) => ({ cover: item.versions.png.original }))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
module.exports = AudiobookCovers
|
module.exports = AudiobookCovers
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
const axios = require('axios')
|
const axios = require('axios').default
|
||||||
const { levenshteinDistance } = require('../utils/index')
|
const { levenshteinDistance } = require('../utils/index')
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
@ -27,12 +27,15 @@ class Audnexus {
|
|||||||
if (region) searchParams.set('region', region)
|
if (region) searchParams.set('region', region)
|
||||||
const authorRequestUrl = `${this.baseUrl}/authors?${searchParams.toString()}`
|
const authorRequestUrl = `${this.baseUrl}/authors?${searchParams.toString()}`
|
||||||
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
|
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
|
||||||
return axios.get(authorRequestUrl).then((res) => {
|
return axios
|
||||||
return res.data || []
|
.get(authorRequestUrl)
|
||||||
}).catch((error) => {
|
.then((res) => {
|
||||||
Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error)
|
return res.data || []
|
||||||
return []
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
|
Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -46,12 +49,15 @@ class Audnexus {
|
|||||||
const regionQuery = region ? `?region=${region}` : ''
|
const regionQuery = region ? `?region=${region}` : ''
|
||||||
const authorRequestUrl = `${this.baseUrl}/authors/${asin}${regionQuery}`
|
const authorRequestUrl = `${this.baseUrl}/authors/${asin}${regionQuery}`
|
||||||
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
|
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
|
||||||
return axios.get(authorRequestUrl).then((res) => {
|
return axios
|
||||||
return res.data
|
.get(authorRequestUrl)
|
||||||
}).catch((error) => {
|
.then((res) => {
|
||||||
Logger.error(`[Audnexus] Author request failed for ${asin}`, error)
|
return res.data
|
||||||
return null
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
|
Logger.error(`[Audnexus] Author request failed for ${asin}`, error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -108,12 +114,15 @@ class Audnexus {
|
|||||||
|
|
||||||
getChaptersByASIN(asin, region) {
|
getChaptersByASIN(asin, region) {
|
||||||
Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`)
|
Logger.debug(`[Audnexus] Get chapters for ASIN ${asin}/${region}`)
|
||||||
return axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`).then((res) => {
|
return axios
|
||||||
return res.data
|
.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`)
|
||||||
}).catch((error) => {
|
.then((res) => {
|
||||||
Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error)
|
return res.data
|
||||||
return null
|
})
|
||||||
})
|
.catch((error) => {
|
||||||
|
Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
module.exports = Audnexus
|
module.exports = Audnexus
|
@ -1,97 +1,91 @@
|
|||||||
|
const axios = require('axios').default
|
||||||
const Database = require('../Database')
|
const Database = require('../Database')
|
||||||
const axios = require('axios')
|
|
||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class CustomProviderAdapter {
|
class CustomProviderAdapter {
|
||||||
constructor() { }
|
#responseTimeout = 30000
|
||||||
|
|
||||||
/**
|
constructor() {}
|
||||||
*
|
|
||||||
* @param {string} title
|
|
||||||
* @param {string} author
|
|
||||||
* @param {string} isbn
|
|
||||||
* @param {string} providerSlug
|
|
||||||
* @param {string} mediaType
|
|
||||||
* @returns {Promise<Object[]>}
|
|
||||||
*/
|
|
||||||
async search(title, author, isbn, providerSlug, mediaType) {
|
|
||||||
const providerId = providerSlug.split('custom-')[1]
|
|
||||||
const provider = await Database.customMetadataProviderModel.findByPk(providerId)
|
|
||||||
|
|
||||||
if (!provider) {
|
/**
|
||||||
throw new Error("Custom provider not found for the given id")
|
*
|
||||||
}
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @param {string} isbn
|
||||||
|
* @param {string} providerSlug
|
||||||
|
* @param {string} mediaType
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
async search(title, author, isbn, providerSlug, mediaType, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
// Setup query params
|
const providerId = providerSlug.split('custom-')[1]
|
||||||
const queryObj = {
|
const provider = await Database.customMetadataProviderModel.findByPk(providerId)
|
||||||
mediaType,
|
|
||||||
query: title
|
|
||||||
}
|
|
||||||
if (author) {
|
|
||||||
queryObj.author = author
|
|
||||||
}
|
|
||||||
if (isbn) {
|
|
||||||
queryObj.isbn = isbn
|
|
||||||
}
|
|
||||||
const queryString = (new URLSearchParams(queryObj)).toString()
|
|
||||||
|
|
||||||
// Setup headers
|
if (!provider) {
|
||||||
const axiosOptions = {}
|
throw new Error('Custom provider not found for the given id')
|
||||||
if (provider.authHeaderValue) {
|
|
||||||
axiosOptions.headers = {
|
|
||||||
'Authorization': provider.authHeaderValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const matches = await axios.get(`${provider.url}/search?${queryString}`, axiosOptions).then((res) => {
|
|
||||||
if (!res?.data || !Array.isArray(res.data.matches)) return null
|
|
||||||
return res.data.matches
|
|
||||||
}).catch(error => {
|
|
||||||
Logger.error('[CustomMetadataProvider] Search error', error)
|
|
||||||
return []
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!matches) {
|
|
||||||
throw new Error("Custom provider returned malformed response")
|
|
||||||
}
|
|
||||||
|
|
||||||
// re-map keys to throw out
|
|
||||||
return matches.map(({
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
author,
|
|
||||||
narrator,
|
|
||||||
publisher,
|
|
||||||
publishedYear,
|
|
||||||
description,
|
|
||||||
cover,
|
|
||||||
isbn,
|
|
||||||
asin,
|
|
||||||
genres,
|
|
||||||
tags,
|
|
||||||
series,
|
|
||||||
language,
|
|
||||||
duration
|
|
||||||
}) => {
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
subtitle,
|
|
||||||
author,
|
|
||||||
narrator,
|
|
||||||
publisher,
|
|
||||||
publishedYear,
|
|
||||||
description,
|
|
||||||
cover,
|
|
||||||
isbn,
|
|
||||||
asin,
|
|
||||||
genres,
|
|
||||||
tags: tags?.join(',') || null,
|
|
||||||
series: series?.length ? series : null,
|
|
||||||
language,
|
|
||||||
duration
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup query params
|
||||||
|
const queryObj = {
|
||||||
|
mediaType,
|
||||||
|
query: title
|
||||||
|
}
|
||||||
|
if (author) {
|
||||||
|
queryObj.author = author
|
||||||
|
}
|
||||||
|
if (isbn) {
|
||||||
|
queryObj.isbn = isbn
|
||||||
|
}
|
||||||
|
const queryString = new URLSearchParams(queryObj).toString()
|
||||||
|
|
||||||
|
// Setup headers
|
||||||
|
const axiosOptions = {
|
||||||
|
timeout
|
||||||
|
}
|
||||||
|
if (provider.authHeaderValue) {
|
||||||
|
axiosOptions.headers = {
|
||||||
|
Authorization: provider.authHeaderValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = await axios
|
||||||
|
.get(`${provider.url}/search?${queryString}`, axiosOptions)
|
||||||
|
.then((res) => {
|
||||||
|
if (!res?.data || !Array.isArray(res.data.matches)) return null
|
||||||
|
return res.data.matches
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error('[CustomMetadataProvider] Search error', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!matches) {
|
||||||
|
throw new Error('Custom provider returned malformed response')
|
||||||
|
}
|
||||||
|
|
||||||
|
// re-map keys to throw out
|
||||||
|
return matches.map(({ title, subtitle, author, narrator, publisher, publishedYear, description, cover, isbn, asin, genres, tags, series, language, duration }) => {
|
||||||
|
return {
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
author,
|
||||||
|
narrator,
|
||||||
|
publisher,
|
||||||
|
publishedYear,
|
||||||
|
description,
|
||||||
|
cover,
|
||||||
|
isbn,
|
||||||
|
asin,
|
||||||
|
genres,
|
||||||
|
tags: tags?.join(',') || null,
|
||||||
|
series: series?.length ? series : null,
|
||||||
|
language,
|
||||||
|
duration
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = CustomProviderAdapter
|
module.exports = CustomProviderAdapter
|
@ -2,6 +2,7 @@ const axios = require('axios')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class FantLab {
|
class FantLab {
|
||||||
|
#responseTimeout = 30000
|
||||||
// 7 - other
|
// 7 - other
|
||||||
// 11 - essay
|
// 11 - essay
|
||||||
// 12 - article
|
// 12 - article
|
||||||
@ -22,28 +23,47 @@ class FantLab {
|
|||||||
_filterWorkType = [7, 11, 12, 22, 23, 24, 25, 26, 46, 47, 49, 51, 52, 55, 56, 57]
|
_filterWorkType = [7, 11, 12, 22, 23, 24, 25, 26, 46, 47, 49, 51, 52, 55, 56, 57]
|
||||||
_baseUrl = 'https://api.fantlab.ru'
|
_baseUrl = 'https://api.fantlab.ru'
|
||||||
|
|
||||||
constructor() { }
|
constructor() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author'
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
**/
|
||||||
|
async search(title, author, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
async search(title, author) {
|
|
||||||
let searchString = encodeURIComponent(title)
|
let searchString = encodeURIComponent(title)
|
||||||
if (author) {
|
if (author) {
|
||||||
searchString += encodeURIComponent(' ' + author)
|
searchString += encodeURIComponent(' ' + author)
|
||||||
}
|
}
|
||||||
const url = `${this._baseUrl}/search-works?q=${searchString}&page=1&onlymatches=1`
|
const url = `${this._baseUrl}/search-works?q=${searchString}&page=1&onlymatches=1`
|
||||||
Logger.debug(`[FantLab] Search url: ${url}`)
|
Logger.debug(`[FantLab] Search url: ${url}`)
|
||||||
const items = await axios.get(url).then((res) => {
|
const items = await axios
|
||||||
return res.data || []
|
.get(url, {
|
||||||
}).catch(error => {
|
timeout
|
||||||
Logger.error('[FantLab] search error', error)
|
})
|
||||||
return []
|
.then((res) => {
|
||||||
})
|
return res.data || []
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error('[FantLab] search error', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
|
||||||
return Promise.all(items.map(async item => await this.getWork(item))).then(resArray => {
|
return Promise.all(items.map(async (item) => await this.getWork(item, timeout))).then((resArray) => {
|
||||||
return resArray.filter(res => res)
|
return resArray.filter((res) => res)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async getWork(item) {
|
/**
|
||||||
|
* @param {Object} item
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
**/
|
||||||
|
async getWork(item, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
const { work_id, work_type_id } = item
|
const { work_id, work_type_id } = item
|
||||||
|
|
||||||
if (this._filterWorkType.includes(work_type_id)) {
|
if (this._filterWorkType.includes(work_type_id)) {
|
||||||
@ -51,23 +71,34 @@ class FantLab {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const url = `${this._baseUrl}/work/${work_id}/extended`
|
const url = `${this._baseUrl}/work/${work_id}/extended`
|
||||||
const bookData = await axios.get(url).then((resp) => {
|
const bookData = await axios
|
||||||
return resp.data || null
|
.get(url, {
|
||||||
}).catch((error) => {
|
timeout
|
||||||
Logger.error(`[FantLab] work info request for url "${url}" error`, error)
|
})
|
||||||
return null
|
.then((resp) => {
|
||||||
})
|
return resp.data || null
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error(`[FantLab] work info request for url "${url}" error`, error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
return this.cleanBookData(bookData)
|
return this.cleanBookData(bookData, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
async cleanBookData(bookData) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} bookData
|
||||||
|
* @param {number} [timeout]
|
||||||
|
* @returns {Promise<Object>}
|
||||||
|
*/
|
||||||
|
async cleanBookData(bookData, timeout = this.#responseTimeout) {
|
||||||
let { authors, work_name_alts, work_id, work_name, work_year, work_description, image, classificatory, editions_blocks } = bookData
|
let { authors, work_name_alts, work_id, work_name, work_year, work_description, image, classificatory, editions_blocks } = bookData
|
||||||
|
|
||||||
const subtitle = Array.isArray(work_name_alts) ? work_name_alts[0] : null
|
const subtitle = Array.isArray(work_name_alts) ? work_name_alts[0] : null
|
||||||
const authorNames = authors.map(au => (au.name || '').trim()).filter(au => au)
|
const authorNames = authors.map((au) => (au.name || '').trim()).filter((au) => au)
|
||||||
|
|
||||||
const imageAndIsbn = await this.tryGetCoverFromEditions(editions_blocks)
|
const imageAndIsbn = await this.tryGetCoverFromEditions(editions_blocks, timeout)
|
||||||
|
|
||||||
const imageToUse = imageAndIsbn?.imageUrl || image
|
const imageToUse = imageAndIsbn?.imageUrl || image
|
||||||
|
|
||||||
@ -88,7 +119,7 @@ class FantLab {
|
|||||||
tryGetGenres(classificatory) {
|
tryGetGenres(classificatory) {
|
||||||
if (!classificatory || !classificatory.genre_group) return []
|
if (!classificatory || !classificatory.genre_group) return []
|
||||||
|
|
||||||
const genresGroup = classificatory.genre_group.find(group => group.genre_group_id == 1) // genres and subgenres
|
const genresGroup = classificatory.genre_group.find((group) => group.genre_group_id == 1) // genres and subgenres
|
||||||
|
|
||||||
// genre_group_id=2 - General Characteristics
|
// genre_group_id=2 - General Characteristics
|
||||||
// genre_group_id=3 - Arena
|
// genre_group_id=3 - Arena
|
||||||
@ -108,10 +139,16 @@ class FantLab {
|
|||||||
|
|
||||||
tryGetSubGenres(rootGenre) {
|
tryGetSubGenres(rootGenre) {
|
||||||
if (!rootGenre.genre || !rootGenre.genre.length) return []
|
if (!rootGenre.genre || !rootGenre.genre.length) return []
|
||||||
return rootGenre.genre.map(g => g.label).filter(g => g)
|
return rootGenre.genre.map((g) => g.label).filter((g) => g)
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryGetCoverFromEditions(editions) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} editions
|
||||||
|
* @param {number} [timeout]
|
||||||
|
* @returns {Promise<{imageUrl: string, isbn: string}>
|
||||||
|
*/
|
||||||
|
async tryGetCoverFromEditions(editions, timeout = this.#responseTimeout) {
|
||||||
if (!editions) {
|
if (!editions) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -129,21 +166,34 @@ class FantLab {
|
|||||||
const isbn = lastEdition['isbn'] || null // get only from paper edition
|
const isbn = lastEdition['isbn'] || null // get only from paper edition
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imageUrl: await this.getCoverFromEdition(editionId),
|
imageUrl: await this.getCoverFromEdition(editionId, timeout),
|
||||||
isbn
|
isbn
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCoverFromEdition(editionId) {
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} editionId
|
||||||
|
* @param {number} [timeout]
|
||||||
|
* @returns {Promise<string>}
|
||||||
|
*/
|
||||||
|
async getCoverFromEdition(editionId, timeout = this.#responseTimeout) {
|
||||||
if (!editionId) return null
|
if (!editionId) return null
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
const url = `${this._baseUrl}/edition/${editionId}`
|
const url = `${this._baseUrl}/edition/${editionId}`
|
||||||
|
|
||||||
const editionInfo = await axios.get(url).then((resp) => {
|
const editionInfo = await axios
|
||||||
return resp.data || null
|
.get(url, {
|
||||||
}).catch(error => {
|
timeout
|
||||||
Logger.error(`[FantLab] search cover from edition with url "${url}" error`, error)
|
})
|
||||||
return null
|
.then((resp) => {
|
||||||
})
|
return resp.data || null
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error(`[FantLab] search cover from edition with url "${url}" error`, error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
|
||||||
return editionInfo?.image || null
|
return editionInfo?.image || null
|
||||||
}
|
}
|
||||||
|
@ -2,12 +2,14 @@ const axios = require('axios')
|
|||||||
const Logger = require('../Logger')
|
const Logger = require('../Logger')
|
||||||
|
|
||||||
class GoogleBooks {
|
class GoogleBooks {
|
||||||
constructor() { }
|
#responseTimeout = 30000
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
extractIsbn(industryIdentifiers) {
|
extractIsbn(industryIdentifiers) {
|
||||||
if (!industryIdentifiers || !industryIdentifiers.length) return null
|
if (!industryIdentifiers || !industryIdentifiers.length) return null
|
||||||
|
|
||||||
var isbnObj = industryIdentifiers.find(i => i.type === 'ISBN_13') || industryIdentifiers.find(i => i.type === 'ISBN_10')
|
var isbnObj = industryIdentifiers.find((i) => i.type === 'ISBN_13') || industryIdentifiers.find((i) => i.type === 'ISBN_10')
|
||||||
if (isbnObj && isbnObj.identifier) return isbnObj.identifier
|
if (isbnObj && isbnObj.identifier) return isbnObj.identifier
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -38,23 +40,37 @@ class GoogleBooks {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(title, author) {
|
/**
|
||||||
|
* Search for a book by title and author
|
||||||
|
* @param {string} title
|
||||||
|
* @param {string} author
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
**/
|
||||||
|
async search(title, author, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
title = encodeURIComponent(title)
|
title = encodeURIComponent(title)
|
||||||
var queryString = `q=intitle:${title}`
|
let queryString = `q=intitle:${title}`
|
||||||
if (author) {
|
if (author) {
|
||||||
author = encodeURIComponent(author)
|
author = encodeURIComponent(author)
|
||||||
queryString += `+inauthor:${author}`
|
queryString += `+inauthor:${author}`
|
||||||
}
|
}
|
||||||
var url = `https://www.googleapis.com/books/v1/volumes?${queryString}`
|
const url = `https://www.googleapis.com/books/v1/volumes?${queryString}`
|
||||||
Logger.debug(`[GoogleBooks] Search url: ${url}`)
|
Logger.debug(`[GoogleBooks] Search url: ${url}`)
|
||||||
var items = await axios.get(url).then((res) => {
|
const items = await axios
|
||||||
if (!res || !res.data || !res.data.items) return []
|
.get(url, {
|
||||||
return res.data.items
|
timeout
|
||||||
}).catch(error => {
|
})
|
||||||
Logger.error('[GoogleBooks] Volume search error', error)
|
.then((res) => {
|
||||||
return []
|
if (!res || !res.data || !res.data.items) return []
|
||||||
})
|
return res.data.items
|
||||||
return items.map(item => this.cleanResult(item))
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error('[GoogleBooks] Volume search error', error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
|
return items.map((item) => this.cleanResult(item))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,17 +1,31 @@
|
|||||||
var axios = require('axios')
|
const axios = require('axios').default
|
||||||
|
|
||||||
class OpenLibrary {
|
class OpenLibrary {
|
||||||
|
#responseTimeout = 30000
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.baseUrl = 'https://openlibrary.org'
|
this.baseUrl = 'https://openlibrary.org'
|
||||||
}
|
}
|
||||||
|
|
||||||
get(uri) {
|
/**
|
||||||
return axios.get(`${this.baseUrl}/${uri}`).then((res) => {
|
*
|
||||||
return res.data
|
* @param {string} uri
|
||||||
}).catch((error) => {
|
* @param {number} timeout
|
||||||
console.error('Failed', error)
|
* @returns {Promise<Object>}
|
||||||
return false
|
*/
|
||||||
})
|
get(uri, timeout = this.#responseTimeout) {
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
return axios
|
||||||
|
.get(`${this.baseUrl}/${uri}`, {
|
||||||
|
timeout
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
return res.data
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('Failed', error)
|
||||||
|
return null
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async isbnLookup(isbn) {
|
async isbnLookup(isbn) {
|
||||||
@ -33,7 +47,7 @@ class OpenLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!worksData.covers) worksData.covers = []
|
if (!worksData.covers) worksData.covers = []
|
||||||
var coverImages = worksData.covers.filter(c => c > 0).map(c => `https://covers.openlibrary.org/b/id/${c}-L.jpg`)
|
var coverImages = worksData.covers.filter((c) => c > 0).map((c) => `https://covers.openlibrary.org/b/id/${c}-L.jpg`)
|
||||||
var description = null
|
var description = null
|
||||||
if (worksData.description) {
|
if (worksData.description) {
|
||||||
if (typeof worksData.description === 'string') {
|
if (typeof worksData.description === 'string') {
|
||||||
@ -73,26 +87,34 @@ class OpenLibrary {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async search(query) {
|
async search(query) {
|
||||||
var queryString = Object.keys(query).map(key => key + '=' + query[key]).join('&')
|
var queryString = Object.keys(query)
|
||||||
|
.map((key) => key + '=' + query[key])
|
||||||
|
.join('&')
|
||||||
var lookupData = await this.get(`/search.json?${queryString}`)
|
var lookupData = await this.get(`/search.json?${queryString}`)
|
||||||
if (!lookupData) {
|
if (!lookupData) {
|
||||||
return {
|
return {
|
||||||
errorCode: 404
|
errorCode: 404
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var searchDocs = await Promise.all(lookupData.docs.map(d => this.cleanSearchDoc(d)))
|
var searchDocs = await Promise.all(lookupData.docs.map((d) => this.cleanSearchDoc(d)))
|
||||||
return searchDocs
|
return searchDocs
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchTitle(title) {
|
/**
|
||||||
title = encodeURIComponent(title);
|
*
|
||||||
var lookupData = await this.get(`/search.json?title=${title}`)
|
* @param {string} title
|
||||||
|
* @param {number} timeout
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
async searchTitle(title, timeout = this.#responseTimeout) {
|
||||||
|
title = encodeURIComponent(title)
|
||||||
|
var lookupData = await this.get(`/search.json?title=${title}`, timeout)
|
||||||
if (!lookupData) {
|
if (!lookupData) {
|
||||||
return {
|
return {
|
||||||
errorCode: 404
|
errorCode: 404
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var searchDocs = await Promise.all(lookupData.docs.map(d => this.cleanSearchDoc(d)))
|
var searchDocs = await Promise.all(lookupData.docs.map((d) => this.cleanSearchDoc(d)))
|
||||||
return searchDocs
|
return searchDocs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,19 +28,24 @@ const htmlSanitizer = require('../utils/htmlSanitizer')
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
class iTunes {
|
class iTunes {
|
||||||
constructor() { }
|
#responseTimeout = 30000
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/Searching.html
|
* @see https://developer.apple.com/library/archive/documentation/AudioVideo/Conceptual/iTuneSearchAPI/Searching.html
|
||||||
*
|
*
|
||||||
* @param {iTunesSearchParams} options
|
* @param {iTunesSearchParams} options
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
* @returns {Promise<Object[]>}
|
* @returns {Promise<Object[]>}
|
||||||
*/
|
*/
|
||||||
search(options) {
|
search(options, timeout = this.#responseTimeout) {
|
||||||
if (!options.term) {
|
if (!options.term) {
|
||||||
Logger.error('[iTunes] Invalid search options - no term')
|
Logger.error('[iTunes] Invalid search options - no term')
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
|
||||||
|
|
||||||
const query = {
|
const query = {
|
||||||
term: options.term,
|
term: options.term,
|
||||||
media: options.media,
|
media: options.media,
|
||||||
@ -49,12 +54,18 @@ class iTunes {
|
|||||||
limit: options.limit,
|
limit: options.limit,
|
||||||
country: options.country
|
country: options.country
|
||||||
}
|
}
|
||||||
return axios.get('https://itunes.apple.com/search', { params: query }).then((response) => {
|
return axios
|
||||||
return response.data.results || []
|
.get('https://itunes.apple.com/search', {
|
||||||
}).catch((error) => {
|
params: query,
|
||||||
Logger.error(`[iTunes] search request error`, error)
|
timeout
|
||||||
return []
|
})
|
||||||
})
|
.then((response) => {
|
||||||
|
return response.data.results || []
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
Logger.error(`[iTunes] search request error`, error)
|
||||||
|
return []
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example cover art: https://is1-ssl.mzstatic.com/image/thumb/Music118/v4/cb/ea/73/cbea739b-ff3b-11c4-fb93-7889fbec7390/9781598874983_cover.jpg/100x100bb.jpg
|
// Example cover art: https://is1-ssl.mzstatic.com/image/thumb/Music118/v4/cb/ea/73/cbea739b-ff3b-11c4-fb93-7889fbec7390/9781598874983_cover.jpg/100x100bb.jpg
|
||||||
@ -65,20 +76,22 @@ class iTunes {
|
|||||||
return data.artworkUrl600
|
return data.artworkUrl600
|
||||||
}
|
}
|
||||||
// Should already be sorted from small to large
|
// Should already be sorted from small to large
|
||||||
var artworkSizes = Object.keys(data).filter(key => key.startsWith('artworkUrl')).map(key => {
|
var artworkSizes = Object.keys(data)
|
||||||
return {
|
.filter((key) => key.startsWith('artworkUrl'))
|
||||||
url: data[key],
|
.map((key) => {
|
||||||
size: Number(key.replace('artworkUrl', ''))
|
return {
|
||||||
}
|
url: data[key],
|
||||||
})
|
size: Number(key.replace('artworkUrl', ''))
|
||||||
|
}
|
||||||
|
})
|
||||||
if (!artworkSizes.length) return null
|
if (!artworkSizes.length) return null
|
||||||
|
|
||||||
// Return next biggest size > 600
|
// Return next biggest size > 600
|
||||||
var nextBestSize = artworkSizes.find(size => size.size > 600)
|
var nextBestSize = artworkSizes.find((size) => size.size > 600)
|
||||||
if (nextBestSize) return nextBestSize.url
|
if (nextBestSize) return nextBestSize.url
|
||||||
|
|
||||||
// Find square artwork
|
// Find square artwork
|
||||||
var squareArtwork = artworkSizes.find(size => size.url.includes(`${size.size}x${size.size}bb`))
|
var squareArtwork = artworkSizes.find((size) => size.url.includes(`${size.size}x${size.size}bb`))
|
||||||
|
|
||||||
// Square cover replace with 600x600bb
|
// Square cover replace with 600x600bb
|
||||||
if (squareArtwork) {
|
if (squareArtwork) {
|
||||||
@ -106,8 +119,14 @@ class iTunes {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchAudiobooks(term) {
|
/**
|
||||||
return this.search({ term, entity: 'audiobook', media: 'audiobook' }).then((results) => {
|
*
|
||||||
|
* @param {string} term
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
|
* @returns {Promise<Object[]>}
|
||||||
|
*/
|
||||||
|
searchAudiobooks(term, timeout = this.#responseTimeout) {
|
||||||
|
return this.search({ term, entity: 'audiobook', media: 'audiobook' }, timeout).then((results) => {
|
||||||
return results.map(this.cleanAudiobook.bind(this))
|
return results.map(this.cleanAudiobook.bind(this))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -139,10 +158,11 @@ class iTunes {
|
|||||||
*
|
*
|
||||||
* @param {string} term
|
* @param {string} term
|
||||||
* @param {{country:string}} options
|
* @param {{country:string}} options
|
||||||
|
* @param {number} [timeout] response timeout in ms
|
||||||
* @returns {Promise<iTunesPodcastSearchResult[]>}
|
* @returns {Promise<iTunesPodcastSearchResult[]>}
|
||||||
*/
|
*/
|
||||||
searchPodcasts(term, options = {}) {
|
searchPodcasts(term, options = {}, timeout = this.#responseTimeout) {
|
||||||
return this.search({ term, entity: 'podcast', media: 'podcast', ...options }).then((results) => {
|
return this.search({ term, entity: 'podcast', media: 'podcast', ...options }, timeout).then((results) => {
|
||||||
return results.map(this.cleanPodcast.bind(this))
|
return results.map(this.cleanPodcast.bind(this))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user