Merge pull request #4716 from mikiher/async-cover-search

Async Cover Search
This commit is contained in:
advplyr 2025-10-03 16:43:05 -05:00 committed by GitHub
commit 123351e08a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 531 additions and 40 deletions

View File

@ -51,19 +51,21 @@
<form @submit.prevent="submitSearchForm"> <form @submit.prevent="submitSearchForm">
<div class="flex flex-wrap sm:flex-nowrap items-center justify-start -mx-1"> <div class="flex flex-wrap sm:flex-nowrap items-center justify-start -mx-1">
<div class="w-48 grow p-1"> <div class="w-48 grow p-1">
<ui-dropdown v-model="provider" :items="providers" :label="$strings.LabelProvider" small /> <ui-dropdown v-model="provider" :items="providers" :disabled="searchInProgress" :label="$strings.LabelProvider" small />
</div> </div>
<div class="w-72 grow p-1"> <div class="w-72 grow p-1">
<ui-text-input-with-label v-model="searchTitle" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" /> <ui-text-input-with-label v-model="searchTitle" :disabled="searchInProgress" :label="searchTitleLabel" :placeholder="$strings.PlaceholderSearch" />
</div> </div>
<div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 grow p-1"> <div v-show="provider != 'itunes' && provider != 'audiobookcovers'" class="w-72 grow p-1">
<ui-text-input-with-label v-model="searchAuthor" :label="$strings.LabelAuthor" /> <ui-text-input-with-label v-model="searchAuthor" :disabled="searchInProgress" :label="$strings.LabelAuthor" />
</div> </div>
<ui-btn class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn> <ui-btn v-if="!searchInProgress" class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="submit">{{ $strings.ButtonSearch }}</ui-btn>
<ui-btn v-else class="mt-5 ml-1 md:min-w-24" :padding-x="4" type="button" color="bg-error" @click.prevent="cancelCurrentSearch">{{ $strings.ButtonCancel }}</ui-btn>
</div> </div>
</form> </form>
<div v-if="hasSearched" class="flex items-center flex-wrap justify-center sm:max-h-80 sm:overflow-y-scroll mt-2 max-w-full"> <div v-if="hasSearched" class="flex items-center flex-wrap justify-center sm:max-h-80 sm:overflow-y-scroll mt-2 max-w-full">
<p v-if="!coversFound.length">{{ $strings.MessageNoCoversFound }}</p> <p v-if="searchInProgress && !coversFound.length" class="text-gray-300 py-4">{{ $strings.MessageLoading }}</p>
<p v-else-if="!searchInProgress && !coversFound.length" class="text-gray-300 py-4">{{ $strings.MessageNoCoversFound }}</p>
<template v-for="cover in coversFound"> <template v-for="cover in coversFound">
<div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)"> <div :key="cover" class="m-0.5 mb-5 border-2 border-transparent hover:border-yellow-300 cursor-pointer" :class="cover === coverPath ? 'border-yellow-300' : ''" @click="updateCover(cover)">
<covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" /> <covers-preview-cover :src="cover" :width="80" show-open-new-tab :book-cover-aspect-ratio="bookCoverAspectRatio" />
@ -105,7 +107,10 @@ export default {
showLocalCovers: false, showLocalCovers: false,
previewUpload: null, previewUpload: null,
selectedFile: null, selectedFile: null,
provider: 'google' provider: 'google',
currentSearchRequestId: null,
searchInProgress: false,
socketListenersActive: false
} }
}, },
watch: { watch: {
@ -129,7 +134,7 @@ export default {
}, },
providers() { providers() {
if (this.isPodcast) return this.$store.state.scanners.podcastProviders if (this.isPodcast) return this.$store.state.scanners.podcastProviders
return [{ text: 'All', value: 'all' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders] return [{ text: 'Best', value: 'best' }, ...this.$store.state.scanners.providers, ...this.$store.state.scanners.coverOnlyProviders, { text: 'All', value: 'all' }]
}, },
searchTitleLabel() { searchTitleLabel() {
if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN if (this.provider.startsWith('audible')) return this.$strings.LabelSearchTitleOrASIN
@ -186,6 +191,9 @@ export default {
_file.localPath = `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${file.ino}?token=${this.userToken}` _file.localPath = `${process.env.serverUrl}/api/items/${this.libraryItemId}/file/${file.ino}?token=${this.userToken}`
return _file return _file
}) })
},
socket() {
return this.$root.socket
} }
}, },
methods: { methods: {
@ -235,7 +243,19 @@ export default {
this.searchTitle = this.mediaMetadata.title || '' this.searchTitle = this.mediaMetadata.title || ''
this.searchAuthor = this.mediaMetadata.authorName || '' this.searchAuthor = this.mediaMetadata.authorName || ''
if (this.isPodcast) this.provider = 'itunes' if (this.isPodcast) this.provider = 'itunes'
else this.provider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google' else {
// Migrate from 'all' to 'best' (only once)
const migrationKey = 'book-cover-provider-migrated'
const currentProvider = localStorage.getItem('book-cover-provider') || localStorage.getItem('book-provider') || 'google'
if (!localStorage.getItem(migrationKey) && currentProvider === 'all') {
localStorage.setItem('book-cover-provider', 'best')
localStorage.setItem(migrationKey, 'true')
this.provider = 'best'
} else {
this.provider = currentProvider
}
}
}, },
removeCover() { removeCover() {
if (!this.coverPath) { if (!this.coverPath) {
@ -291,22 +311,116 @@ export default {
console.error('PersistProvider', error) console.error('PersistProvider', error)
} }
}, },
generateRequestId() {
return `cover-search-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
},
addSocketListeners() {
if (!this.socket || this.socketListenersActive) return
this.socket.on('cover_search_result', this.handleSearchResult)
this.socket.on('cover_search_complete', this.handleSearchComplete)
this.socket.on('cover_search_error', this.handleSearchError)
this.socket.on('cover_search_provider_error', this.handleProviderError)
this.socket.on('cover_search_cancelled', this.handleSearchCancelled)
this.socket.on('disconnect', this.handleSocketDisconnect)
this.socketListenersActive = true
},
removeSocketListeners() {
if (!this.socket || !this.socketListenersActive) return
this.socket.off('cover_search_result', this.handleSearchResult)
this.socket.off('cover_search_complete', this.handleSearchComplete)
this.socket.off('cover_search_error', this.handleSearchError)
this.socket.off('cover_search_provider_error', this.handleProviderError)
this.socket.off('cover_search_cancelled', this.handleSearchCancelled)
this.socket.off('disconnect', this.handleSocketDisconnect)
this.socketListenersActive = false
},
handleSearchResult(data) {
if (data.requestId !== this.currentSearchRequestId) return
// Add new covers to the list (avoiding duplicates)
const newCovers = data.covers.filter((cover) => !this.coversFound.includes(cover))
this.coversFound.push(...newCovers)
},
handleSearchComplete(data) {
if (data.requestId !== this.currentSearchRequestId) return
this.searchInProgress = false
this.currentSearchRequestId = null
},
handleSearchError(data) {
if (data.requestId !== this.currentSearchRequestId) return
console.error('[Cover Search] Search error:', data.error)
this.$toast.error(this.$strings.ToastCoverSearchFailed)
this.searchInProgress = false
this.currentSearchRequestId = null
},
handleProviderError(data) {
if (data.requestId !== this.currentSearchRequestId) return
console.warn(`[Cover Search] Provider ${data.provider} failed:`, data.error)
},
handleSearchCancelled(data) {
if (data.requestId !== this.currentSearchRequestId) return
this.searchInProgress = false
this.currentSearchRequestId = null
},
handleSocketDisconnect() {
// If we were in the middle of a search, cancel it (server can't send results anymore)
if (this.searchInProgress && this.currentSearchRequestId) {
this.searchInProgress = false
this.currentSearchRequestId = null
}
},
cancelCurrentSearch() {
if (!this.currentSearchRequestId || !this.socket?.connected) {
console.error('[Cover Search] Socket not connected')
this.$toast.error(this.$strings.ToastConnectionNotAvailable)
return
}
this.socket.emit('cancel_cover_search', this.currentSearchRequestId)
this.currentSearchRequestId = null
this.searchInProgress = false
},
async submitSearchForm() { async submitSearchForm() {
if (!this.socket?.connected) {
console.error('[Cover Search] Socket not connected')
this.$toast.error(this.$strings.ToastConnectionNotAvailable)
return
}
// Cancel any existing search
if (this.searchInProgress) {
this.cancelCurrentSearch()
}
// Store provider in local storage // Store provider in local storage
this.persistProvider() this.persistProvider()
this.isProcessing = true // Setup socket listeners if not already done
const searchQuery = this.getSearchQuery() this.addSocketListeners()
const results = await this.$axios
.$get(`/api/search/covers?${searchQuery}`) // Clear previous results
.then((res) => res.results) this.coversFound = []
.catch((error) => {
console.error('Failed', error)
return []
})
this.coversFound = results
this.isProcessing = false
this.hasSearched = true this.hasSearched = true
this.searchInProgress = true
// Generate unique request ID
const requestId = this.generateRequestId()
this.currentSearchRequestId = requestId
// Emit search request via WebSocket
this.socket.emit('search_covers', {
requestId,
title: this.searchTitle,
author: this.searchAuthor || '',
provider: this.provider,
podcast: this.isPodcast
})
}, },
setCover(coverFile) { setCover(coverFile) {
this.isProcessing = true this.isProcessing = true
@ -320,6 +434,18 @@ export default {
this.isProcessing = false this.isProcessing = false
}) })
} }
},
mounted() {
// Setup socket listeners when component is mounted
this.addSocketListeners()
},
beforeDestroy() {
// Cancel any ongoing search when component is destroyed
if (this.searchInProgress) {
this.cancelCurrentSearch()
}
// Remove socket listeners
this.removeSocketListeners()
} }
} }
</script> </script>

View File

@ -1026,6 +1026,8 @@
"ToastCollectionItemsAddFailed": "Item(s) added to collection failed", "ToastCollectionItemsAddFailed": "Item(s) added to collection failed",
"ToastCollectionRemoveSuccess": "Collection removed", "ToastCollectionRemoveSuccess": "Collection removed",
"ToastCollectionUpdateSuccess": "Collection updated", "ToastCollectionUpdateSuccess": "Collection updated",
"ToastConnectionNotAvailable": "Connection not available. Please try again later",
"ToastCoverSearchFailed": "Cover search failed",
"ToastCoverUpdateFailed": "Cover update failed", "ToastCoverUpdateFailed": "Cover update failed",
"ToastDateTimeInvalidOrIncomplete": "Date and time is invalid or incomplete", "ToastDateTimeInvalidOrIncomplete": "Date and time is invalid or incomplete",
"ToastDeleteFileFailed": "Failed to delete file", "ToastDeleteFileFailed": "Failed to delete file",

View File

@ -2,6 +2,7 @@ const SocketIO = require('socket.io')
const Logger = require('./Logger') const Logger = require('./Logger')
const Database = require('./Database') const Database = require('./Database')
const TokenManager = require('./auth/TokenManager') const TokenManager = require('./auth/TokenManager')
const CoverSearchManager = require('./managers/CoverSearchManager')
/** /**
* @typedef SocketClient * @typedef SocketClient
@ -180,6 +181,10 @@ class SocketAuthority {
// Scanning // Scanning
socket.on('cancel_scan', (libraryId) => this.cancelScan(libraryId)) socket.on('cancel_scan', (libraryId) => this.cancelScan(libraryId))
// Cover search streaming
socket.on('search_covers', (payload) => this.handleCoverSearch(socket, payload))
socket.on('cancel_cover_search', (requestId) => this.handleCancelCoverSearch(socket, requestId))
// Logs // Logs
socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level)) socket.on('set_log_listener', (level) => Logger.addSocketListener(socket, level))
socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id)) socket.on('remove_log_listener', () => Logger.removeSocketListener(socket.id))
@ -200,6 +205,10 @@ class SocketAuthority {
const disconnectTime = Date.now() - _client.connected_at const disconnectTime = Date.now() - _client.connected_at
Logger.info(`[SocketAuthority] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`) Logger.info(`[SocketAuthority] Socket ${socket.id} disconnected from client "${_client.user.username}" after ${disconnectTime}ms (Reason: ${reason})`)
// Cancel any active cover searches for this socket
this.cancelSocketCoverSearches(socket.id)
delete this.clients[socket.id] delete this.clients[socket.id]
} }
}) })
@ -300,5 +309,100 @@ class SocketAuthority {
Logger.debug('[SocketAuthority] Cancel scan', id) Logger.debug('[SocketAuthority] Cancel scan', id)
this.Server.cancelLibraryScan(id) this.Server.cancelLibraryScan(id)
} }
/**
* Handle cover search request via WebSocket
* @param {SocketIO.Socket} socket
* @param {Object} payload
*/
async handleCoverSearch(socket, payload) {
const client = this.clients[socket.id]
if (!client?.user) {
Logger.error('[SocketAuthority] Unauthorized cover search request')
socket.emit('cover_search_error', {
requestId: payload.requestId,
error: 'Unauthorized'
})
return
}
const { requestId, title, author, provider, podcast } = payload
if (!requestId || !title) {
Logger.error('[SocketAuthority] Invalid cover search request')
socket.emit('cover_search_error', {
requestId,
error: 'Invalid request parameters'
})
return
}
Logger.info(`[SocketAuthority] User ${client.user.username} initiated cover search ${requestId}`)
// Callback for streaming results to client
const onResult = (result) => {
socket.emit('cover_search_result', {
requestId,
provider: result.provider,
covers: result.covers,
total: result.total
})
}
// Callback when search completes
const onComplete = () => {
Logger.info(`[SocketAuthority] Cover search ${requestId} completed`)
socket.emit('cover_search_complete', { requestId })
}
// Callback for provider errors
const onError = (provider, errorMessage) => {
socket.emit('cover_search_provider_error', {
requestId,
provider,
error: errorMessage
})
}
// Start the search
CoverSearchManager.startSearch(requestId, { title, author, provider, podcast }, onResult, onComplete, onError).catch((error) => {
Logger.error(`[SocketAuthority] Cover search ${requestId} failed:`, error)
socket.emit('cover_search_error', {
requestId,
error: error.message
})
})
}
/**
* Handle cancel cover search request
* @param {SocketIO.Socket} socket
* @param {string} requestId
*/
handleCancelCoverSearch(socket, requestId) {
const client = this.clients[socket.id]
if (!client?.user) {
Logger.error('[SocketAuthority] Unauthorized cancel cover search request')
return
}
Logger.info(`[SocketAuthority] User ${client.user.username} cancelled cover search ${requestId}`)
const cancelled = CoverSearchManager.cancelSearch(requestId)
if (cancelled) {
socket.emit('cover_search_cancelled', { requestId })
}
}
/**
* Cancel all cover searches associated with a socket (called on disconnect)
* @param {string} socketId
*/
cancelSocketCoverSearches(socketId) {
// Get all active search request IDs and cancel those that might belong to this socket
// Since we don't track socket-to-request mapping, we log this for debugging
// The client will handle reconnection gracefully
Logger.debug(`[SocketAuthority] Socket ${socketId} disconnected, any active searches will timeout`)
}
} }
module.exports = new SocketAuthority() module.exports = new SocketAuthority()

View File

@ -11,7 +11,7 @@ const { levenshteinDistance, levenshteinSimilarity, escapeRegExp, isValidASIN }
const htmlSanitizer = require('../utils/htmlSanitizer') const htmlSanitizer = require('../utils/htmlSanitizer')
class BookFinder { class BookFinder {
#providerResponseTimeout = 30000 #providerResponseTimeout = 10000
constructor() { constructor() {
this.openLibrary = new OpenLibrary() this.openLibrary = new OpenLibrary()
@ -608,6 +608,14 @@ class BookFinder {
Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`) Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`)
searchResults.push(...providerResults) searchResults.push(...providerResults)
} }
} else if (provider === 'best') {
// Best providers: google, fantlab, and audible.com
const bestProviders = ['google', 'fantlab', 'audible']
for (const providerString of bestProviders) {
const providerResults = await this.search(null, providerString, title, author, options)
Logger.debug(`[BookFinder] Found ${providerResults.length} covers from ${providerString}`)
searchResults.push(...providerResults)
}
} else { } else {
searchResults = await this.search(null, provider, title, author, options) searchResults = await this.search(null, provider, title, author, options)
} }

View File

@ -0,0 +1,251 @@
const { setMaxListeners } = require('events')
const Logger = require('../Logger')
const BookFinder = require('../finders/BookFinder')
const PodcastFinder = require('../finders/PodcastFinder')
/**
* Manager for handling streaming cover search across multiple providers
*/
class CoverSearchManager {
constructor() {
/** @type {Map<string, AbortController>} Map of requestId to AbortController */
this.activeSearches = new Map()
// Default timeout for each provider search
this.providerTimeout = 10000 // 10 seconds
// Set to 0 to disable the max listeners limit
// We need one listener per provider (15+) and may have multiple concurrent searches
this.maxListeners = 0
}
/**
* Start a streaming cover search
* @param {string} requestId - Unique identifier for this search request
* @param {Object} searchParams - Search parameters
* @param {string} searchParams.title - Title to search for
* @param {string} searchParams.author - Author to search for (optional)
* @param {string} searchParams.provider - Provider to search (or 'all')
* @param {boolean} searchParams.podcast - Whether this is a podcast search
* @param {Function} onResult - Callback for each result chunk
* @param {Function} onComplete - Callback when search completes
* @param {Function} onError - Callback for errors
*/
async startSearch(requestId, searchParams, onResult, onComplete, onError) {
if (this.activeSearches.has(requestId)) {
Logger.warn(`[CoverSearchManager] Search with requestId ${requestId} already exists`)
return
}
const abortController = new AbortController()
// Increase max listeners on this signal to accommodate parallel provider searches
// AbortSignal is an EventTarget, so we use the events module's setMaxListeners
setMaxListeners(this.maxListeners, abortController.signal)
this.activeSearches.set(requestId, abortController)
Logger.info(`[CoverSearchManager] Starting search ${requestId} with params:`, searchParams)
try {
const { title, author, provider, podcast } = searchParams
if (podcast) {
await this.searchPodcastCovers(requestId, title, abortController.signal, onResult, onError)
} else {
await this.searchBookCovers(requestId, provider, title, author, abortController.signal, onResult, onError)
}
if (!abortController.signal.aborted) {
onComplete()
}
} catch (error) {
if (error.name === 'AbortError') {
Logger.info(`[CoverSearchManager] Search ${requestId} was cancelled`)
} else {
Logger.error(`[CoverSearchManager] Search ${requestId} failed:`, error)
onError(error.message)
}
} finally {
this.activeSearches.delete(requestId)
}
}
/**
* Cancel an active search
* @param {string} requestId - Request ID to cancel
*/
cancelSearch(requestId) {
const abortController = this.activeSearches.get(requestId)
if (abortController) {
Logger.info(`[CoverSearchManager] Cancelling search ${requestId}`)
abortController.abort()
this.activeSearches.delete(requestId)
return true
}
return false
}
/**
* Search for podcast covers
*/
async searchPodcastCovers(requestId, title, signal, onResult, onError) {
try {
const results = await this.executeWithTimeout(() => PodcastFinder.findCovers(title), this.providerTimeout, signal)
if (signal.aborted) return
const covers = this.extractCoversFromResults(results)
if (covers.length > 0) {
onResult({
provider: 'itunes',
covers,
total: covers.length
})
}
} catch (error) {
if (error.name !== 'AbortError') {
Logger.error(`[CoverSearchManager] Podcast search failed:`, error)
onError('itunes', error.message)
}
}
}
/**
* Search for book covers across providers
*/
async searchBookCovers(requestId, provider, title, author, signal, onResult, onError) {
let providers = []
if (provider === 'all') {
providers = [...BookFinder.providers]
} else if (provider === 'best') {
// Best providers: google, fantlab, and audible.com
providers = ['google', 'fantlab', 'audible']
} else {
providers = [provider]
}
Logger.debug(`[CoverSearchManager] Searching ${providers.length} providers in parallel`)
// Search all providers in parallel
const searchPromises = providers.map(async (providerName) => {
if (signal.aborted) return
try {
const searchResults = await this.executeWithTimeout(() => BookFinder.search(null, providerName, title, author || ''), this.providerTimeout, signal)
if (signal.aborted) return
const covers = this.extractCoversFromResults(searchResults)
Logger.debug(`[CoverSearchManager] Found ${covers.length} covers from ${providerName}`)
if (covers.length > 0) {
onResult({
provider: providerName,
covers,
total: covers.length
})
}
} catch (error) {
if (error.name !== 'AbortError') {
Logger.warn(`[CoverSearchManager] Provider ${providerName} failed:`, error.message)
onError(providerName, error.message)
}
}
})
await Promise.allSettled(searchPromises)
}
/**
* Execute a promise with timeout and abort signal
*/
async executeWithTimeout(fn, timeout, signal) {
return new Promise(async (resolve, reject) => {
let abortHandler = null
let timeoutId = null
// Cleanup function to ensure we always remove listeners
const cleanup = () => {
if (timeoutId) {
clearTimeout(timeoutId)
timeoutId = null
}
if (abortHandler) {
signal.removeEventListener('abort', abortHandler)
abortHandler = null
}
}
// Set up timeout
timeoutId = setTimeout(() => {
cleanup()
const error = new Error('Provider timeout')
error.name = 'TimeoutError'
reject(error)
}, timeout)
// Check if already aborted
if (signal.aborted) {
cleanup()
const error = new Error('Search cancelled')
error.name = 'AbortError'
reject(error)
return
}
// Set up abort handler
abortHandler = () => {
cleanup()
const error = new Error('Search cancelled')
error.name = 'AbortError'
reject(error)
}
signal.addEventListener('abort', abortHandler)
try {
const result = await fn()
cleanup()
resolve(result)
} catch (error) {
cleanup()
reject(error)
}
})
}
/**
* Extract cover URLs from search results
*/
extractCoversFromResults(results) {
const covers = []
if (!Array.isArray(results)) return covers
results.forEach((result) => {
if (result.covers && Array.isArray(result.covers)) {
covers.push(...result.covers)
}
if (result.cover) {
covers.push(result.cover)
}
})
// Remove duplicates
return [...new Set(covers)]
}
/**
* Cancel all active searches (cleanup on server shutdown)
*/
cancelAllSearches() {
Logger.info(`[CoverSearchManager] Cancelling ${this.activeSearches.size} active searches`)
for (const [requestId, abortController] of this.activeSearches.entries()) {
abortController.abort()
}
this.activeSearches.clear()
}
}
module.exports = new CoverSearchManager()

View File

@ -3,7 +3,7 @@ const Logger = require('../Logger')
const { isValidASIN } = require('../utils/index') const { isValidASIN } = require('../utils/index')
class Audible { class Audible {
#responseTimeout = 30000 #responseTimeout = 10000
constructor() { constructor() {
this.regionMap = { this.regionMap = {
@ -106,7 +106,7 @@ class Audible {
return res.data return res.data
}) })
.catch((error) => { .catch((error) => {
Logger.error('[Audible] ASIN search error', error) Logger.error('[Audible] ASIN search error', error.message)
return null return null
}) })
} }
@ -158,7 +158,7 @@ class Audible {
return Promise.all(res.data.products.map((result) => this.asinSearch(result.asin, region, timeout))) return Promise.all(res.data.products.map((result) => this.asinSearch(result.asin, region, timeout)))
}) })
.catch((error) => { .catch((error) => {
Logger.error('[Audible] query search error', error) Logger.error('[Audible] query search error', error.message)
return [] return []
}) })
} }

View File

@ -2,7 +2,7 @@ const axios = require('axios')
const Logger = require('../Logger') const Logger = require('../Logger')
class AudiobookCovers { class AudiobookCovers {
#responseTimeout = 30000 #responseTimeout = 10000
constructor() {} constructor() {}
@ -24,7 +24,7 @@ class AudiobookCovers {
}) })
.then((res) => res?.data || []) .then((res) => res?.data || [])
.catch((error) => { .catch((error) => {
Logger.error('[AudiobookCovers] Cover search error', error) Logger.error('[AudiobookCovers] Cover search error', error.message)
return [] return []
}) })
return items.map((item) => ({ cover: item.versions.png.original })) return items.map((item) => ({ cover: item.versions.png.original }))

View File

@ -55,7 +55,7 @@ class Audnexus {
return this._processRequest(this.limiter(() => axios.get(authorRequestUrl))) return this._processRequest(this.limiter(() => axios.get(authorRequestUrl)))
.then((res) => res.data || []) .then((res) => res.data || [])
.catch((error) => { .catch((error) => {
Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error) Logger.error(`[Audnexus] Author ASIN request failed for ${name}`, error.message)
return [] return []
}) })
} }
@ -82,7 +82,7 @@ class Audnexus {
return this._processRequest(this.limiter(() => axios.get(authorRequestUrl.toString()))) 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.message)
return null return null
}) })
} }
@ -158,7 +158,7 @@ class Audnexus {
return this._processRequest(this.limiter(() => axios.get(chaptersRequestUrl.toString()))) 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.message)
return null return null
}) })
} }

View File

@ -4,7 +4,7 @@ const Logger = require('../Logger')
const htmlSanitizer = require('../utils/htmlSanitizer') const htmlSanitizer = require('../utils/htmlSanitizer')
class CustomProviderAdapter { class CustomProviderAdapter {
#responseTimeout = 30000 #responseTimeout = 10000
constructor() {} constructor() {}
@ -61,7 +61,7 @@ class CustomProviderAdapter {
return res.data.matches return res.data.matches
}) })
.catch((error) => { .catch((error) => {
Logger.error('[CustomMetadataProvider] Search error', error) Logger.error('[CustomMetadataProvider] Search error', error.message)
return [] return []
}) })

View File

@ -2,7 +2,7 @@ const axios = require('axios')
const Logger = require('../Logger') const Logger = require('../Logger')
class FantLab { class FantLab {
#responseTimeout = 30000 #responseTimeout = 10000
// 7 - other // 7 - other
// 11 - essay // 11 - essay
// 12 - article // 12 - article
@ -48,7 +48,7 @@ class FantLab {
return res.data || [] return res.data || []
}) })
.catch((error) => { .catch((error) => {
Logger.error('[FantLab] search error', error) Logger.error('[FantLab] search error', error.message)
return [] return []
}) })
@ -77,7 +77,7 @@ class FantLab {
return resp.data || null return resp.data || null
}) })
.catch((error) => { .catch((error) => {
Logger.error(`[FantLab] work info request for url "${url}" error`, error) Logger.error(`[FantLab] work info request for url "${url}" error`, error.message)
return null return null
}) })
@ -193,7 +193,7 @@ class FantLab {
return resp.data || null return resp.data || null
}) })
.catch((error) => { .catch((error) => {
Logger.error(`[FantLab] search cover from edition with url "${url}" error`, error) Logger.error(`[FantLab] search cover from edition with url "${url}" error`, error.message)
return null return null
}) })

View File

@ -2,7 +2,7 @@ const axios = require('axios')
const Logger = require('../Logger') const Logger = require('../Logger')
class GoogleBooks { class GoogleBooks {
#responseTimeout = 30000 #responseTimeout = 10000
constructor() {} constructor() {}
@ -67,7 +67,7 @@ class GoogleBooks {
return res.data.items return res.data.items
}) })
.catch((error) => { .catch((error) => {
Logger.error('[GoogleBooks] Volume search error', error) Logger.error('[GoogleBooks] Volume search error', error.message)
return [] return []
}) })
return items.map((item) => this.cleanResult(item)) return items.map((item) => this.cleanResult(item))

View File

@ -1,7 +1,7 @@
const axios = require('axios').default const axios = require('axios').default
class OpenLibrary { class OpenLibrary {
#responseTimeout = 30000 #responseTimeout = 10000
constructor() { constructor() {
this.baseUrl = 'https://openlibrary.org' this.baseUrl = 'https://openlibrary.org'
@ -23,7 +23,7 @@ class OpenLibrary {
return res.data return res.data
}) })
.catch((error) => { .catch((error) => {
console.error('Failed', error) console.error('Failed', error.message)
return null return null
}) })
} }

View File

@ -28,7 +28,7 @@ const htmlSanitizer = require('../utils/htmlSanitizer')
*/ */
class iTunes { class iTunes {
#responseTimeout = 30000 #responseTimeout = 10000
constructor() {} constructor() {}
@ -63,7 +63,7 @@ class iTunes {
return response.data.results || [] return response.data.results || []
}) })
.catch((error) => { .catch((error) => {
Logger.error(`[iTunes] search request error`, error) Logger.error(`[iTunes] search request error`, error.message)
return [] return []
}) })
} }