Update:Validate ASIN for author, chapter and match requests

This commit is contained in:
advplyr 2024-06-09 13:43:03 -05:00
parent ee501f70ed
commit a018374d26
5 changed files with 72 additions and 45 deletions

View File

@ -9,7 +9,7 @@ const CacheManager = require('../managers/CacheManager')
const CoverManager = require('../managers/CoverManager') const CoverManager = require('../managers/CoverManager')
const AuthorFinder = require('../finders/AuthorFinder') const AuthorFinder = require('../finders/AuthorFinder')
const { reqSupportsWebp } = require('../utils/index') const { reqSupportsWebp, isValidASIN } = require('../utils/index')
const naturalSort = createNewSortInstance({ const naturalSort = createNewSortInstance({
comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare comparer: new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
@ -252,7 +252,7 @@ class AuthorController {
async match(req, res) { async match(req, res) {
let authorData = null let authorData = null
const region = req.body.region || 'us' const region = req.body.region || 'us'
if (req.body.asin) { if (req.body.asin && isValidASIN(req.body.asin.toUpperCase?.())) {
authorData = await AuthorFinder.findAuthorByASIN(req.body.asin, region) authorData = await AuthorFinder.findAuthorByASIN(req.body.asin, region)
} else { } else {
authorData = await AuthorFinder.findAuthorByName(req.body.q, region) authorData = await AuthorFinder.findAuthorByName(req.body.q, region)

View File

@ -1,12 +1,13 @@
const Logger = require("../Logger") const Logger = require('../Logger')
const BookFinder = require('../finders/BookFinder') const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder') const PodcastFinder = require('../finders/PodcastFinder')
const AuthorFinder = require('../finders/AuthorFinder') const AuthorFinder = require('../finders/AuthorFinder')
const MusicFinder = require('../finders/MusicFinder') const MusicFinder = require('../finders/MusicFinder')
const Database = require("../Database") const Database = require('../Database')
const { isValidASIN } = require('../utils')
class SearchController { class SearchController {
constructor() { } constructor() {}
async findBooks(req, res) { async findBooks(req, res) {
const id = req.query.id const id = req.query.id
@ -63,6 +64,9 @@ class SearchController {
async findChapters(req, res) { async findChapters(req, res) {
const asin = req.query.asin const asin = req.query.asin
if (!isValidASIN(asin.toUpperCase())) {
return res.json({ error: 'Invalid ASIN' })
}
const region = (req.query.region || 'us').toLowerCase() const region = (req.query.region || 'us').toLowerCase()
const chapterData = await BookFinder.findChapters(asin, region) const chapterData = await BookFinder.findChapters(asin, region)
if (!chapterData) { if (!chapterData) {

View File

@ -1,6 +1,7 @@
const axios = require('axios').default const axios = require('axios').default
const htmlSanitizer = require('../utils/htmlSanitizer') const htmlSanitizer = require('../utils/htmlSanitizer')
const Logger = require('../Logger') const Logger = require('../Logger')
const { isValidASIN } = require('../utils/index')
class Audible { class Audible {
#responseTimeout = 30000 #responseTimeout = 30000
@ -81,16 +82,6 @@ class Audible {
} }
} }
/**
* 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} asin
@ -137,11 +128,11 @@ class Audible {
if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout if (!timeout || isNaN(timeout)) timeout = this.#responseTimeout
let items let items
if (asin) { if (asin && isValidASIN(asin.toUpperCase())) {
items = [await this.asinSearch(asin, region, timeout)] items = [await this.asinSearch(asin, region, timeout)]
} }
if (!items && this.isProbablyAsin(title)) { if (!items && isValidASIN(title.toUpperCase())) {
items = [await this.asinSearch(title, region, timeout)] items = [await this.asinSearch(title, region, timeout)]
} }

View File

@ -1,7 +1,8 @@
const axios = require('axios').default const axios = require('axios').default
const { levenshteinDistance } = require('../utils/index')
const Logger = require('../Logger')
const Throttle = require('p-throttle') const Throttle = require('p-throttle')
const Logger = require('../Logger')
const { levenshteinDistance } = require('../utils/index')
const { isValidASIN } = require('../utils/index')
/** /**
* @typedef AuthorSearchObj * @typedef AuthorSearchObj
@ -66,13 +67,19 @@ class Audnexus {
* @returns {Promise<AuthorSearchObj>} * @returns {Promise<AuthorSearchObj>}
*/ */
authorRequest(asin, region) { authorRequest(asin, region) {
asin = encodeURIComponent(asin) if (!isValidASIN(asin?.toUpperCase?.())) {
const regionQuery = region ? `?region=${region}` : '' Logger.error(`[Audnexus] Invalid ASIN ${asin}`)
const authorRequestUrl = `${this.baseUrl}/authors/${asin}${regionQuery}` return null
}
asin = encodeURIComponent(asin.toUpperCase())
const authorRequestUrl = new URL(`${this.baseUrl}/authors/${asin}`)
if (region) authorRequestUrl.searchParams.set('region', region)
Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`) Logger.info(`[Audnexus] Searching for author "${authorRequestUrl}"`)
return this._processRequest(this.limiter(() => axios.get(authorRequestUrl))) return this._processRequest(this.limiter(() => axios.get(authorRequestUrl.toString())))
.then((res) => res.data) .then((res) => res.data)
.catch((error) => { .catch((error) => {
Logger.error(`[Audnexus] Author request failed for ${asin}`, error) Logger.error(`[Audnexus] Author request failed for ${asin}`, error)
@ -135,10 +142,20 @@ class Audnexus {
} }
} }
/**
*
* @param {string} asin
* @param {string} region
* @returns {Promise<Object>}
*/
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 this._processRequest(this.limiter(() => axios.get(`${this.baseUrl}/books/${asin}/chapters?region=${region}`))) asin = encodeURIComponent(asin.toUpperCase())
const chaptersRequestUrl = new URL(`${this.baseUrl}/books/${asin}/chapters`)
if (region) chaptersRequestUrl.searchParams.set('region', region)
return this._processRequest(this.limiter(() => axios.get(chaptersRequestUrl.toString())))
.then((res) => res.data) .then((res) => res.data)
.catch((error) => { .catch((error) => {
Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error) Logger.error(`[Audnexus] Chapter ASIN request failed for ${asin}/${region}`, error)

View File

@ -1,7 +1,7 @@
const Path = require('path') const Path = require('path')
const uuid = require('uuid') const uuid = require('uuid')
const Logger = require('../Logger') const Logger = require('../Logger')
const { parseString } = require("xml2js") const { parseString } = require('xml2js')
const areEquivalent = require('./areEquivalent') const areEquivalent = require('./areEquivalent')
const levenshteinDistance = (str1, str2, caseSensitive = false) => { const levenshteinDistance = (str1, str2, caseSensitive = false) => {
@ -11,8 +11,9 @@ const levenshteinDistance = (str1, str2, caseSensitive = false) => {
str1 = str1.toLowerCase() str1 = str1.toLowerCase()
str2 = str2.toLowerCase() str2 = str2.toLowerCase()
} }
const track = Array(str2.length + 1).fill(null).map(() => const track = Array(str2.length + 1)
Array(str1.length + 1).fill(null)) .fill(null)
.map(() => Array(str1.length + 1).fill(null))
for (let i = 0; i <= str1.length; i += 1) { for (let i = 0; i <= str1.length; i += 1) {
track[0][i] = i track[0][i] = i
} }
@ -25,7 +26,7 @@ const levenshteinDistance = (str1, str2, caseSensitive = false) => {
track[j][i] = Math.min( track[j][i] = Math.min(
track[j][i - 1] + 1, // deletion track[j][i - 1] + 1, // deletion
track[j - 1][i] + 1, // insertion track[j - 1][i] + 1, // insertion
track[j - 1][i - 1] + indicator, // substitution track[j - 1][i - 1] + indicator // substitution
) )
} }
} }
@ -138,7 +139,10 @@ module.exports.toNumber = (val, fallback = 0) => {
module.exports.cleanStringForSearch = (str) => { module.exports.cleanStringForSearch = (str) => {
if (!str) return '' if (!str) return ''
// Remove ' . ` " , // Remove ' . ` " ,
return str.toLowerCase().replace(/[\'\.\`\",]/g, '').trim() return str
.toLowerCase()
.replace(/[\'\.\`\",]/g, '')
.trim()
} }
const getTitleParts = (title) => { const getTitleParts = (title) => {
@ -235,3 +239,14 @@ module.exports.isUUID = (str) => {
if (!str || typeof str !== 'string') return false if (!str || typeof str !== 'string') return false
return uuid.validate(str) return uuid.validate(str)
} }
/**
* Check if a string is a valid ASIN
*
* @param {string} str
* @returns {boolean}
*/
module.exports.isValidASIN = (str) => {
if (!str || typeof str !== 'string') return false
return /^[A-Z0-9]{10}$/.test(str)
}