mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
89422fc77a
Introduce full authors browsing/detail feature and enhance notifications to support type-specific titles. - Add server APIs: authors search, author detail, and author books routes (audnexus integration) that require auth and enrich results with library matches. - Add frontend pages/components: /authors listing and /authors/[asin] detail pages; AuthorCard, AuthorGrid, AuthorDetailCard, SimilarAuthorsRow, and related skeletons. - Add hook and integration stubs: new useAuthors hook and audnexus-authors integration; update audible service to expose audibleBaseUrl. - Update AudiobookDetailsModal to use audibleBaseUrl and link author names to author detail pages. - Add header navigation link to Authors. - Notifications: extend docs and code to include requestType (audiobook|ebook), add getEventTitle/getEventMeta helpers, update queue signature and providers/processors/tests to pass/handle requestType so titles can be resolved per request type. - Misc: job queue, processors, provider tests and notification tests updated to reflect new behavior. This change enables browsing authors and provides type-aware notification titles without per-provider changes.
105 lines
3.0 KiB
TypeScript
105 lines
3.0 KiB
TypeScript
/**
|
|
* Component: Audnexus Author API Integration
|
|
* Documentation: documentation/integrations/audible.md
|
|
*
|
|
* Shared utilities for fetching author data from the Audnexus API.
|
|
* Used by author search, author detail, and similar authors routes.
|
|
*/
|
|
|
|
import axios from 'axios';
|
|
import { RMABLogger } from '@/lib/utils/logger';
|
|
|
|
const logger = RMABLogger.create('Audnexus.Authors');
|
|
|
|
const AUDNEXUS_BASE = 'https://api.audnex.us';
|
|
const AUDNEXUS_TIMEOUT = 10000;
|
|
const AUDNEXUS_HEADERS = { 'User-Agent': 'ReadMeABook/1.0' };
|
|
|
|
export interface AudnexusAuthorSearch {
|
|
asin: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface AudnexusAuthorGenre {
|
|
asin: string;
|
|
name: string;
|
|
type: string;
|
|
}
|
|
|
|
export interface AudnexusAuthorSimilar {
|
|
asin: string;
|
|
name: string;
|
|
}
|
|
|
|
export interface AudnexusAuthorDetail {
|
|
asin: string;
|
|
name: string;
|
|
description?: string;
|
|
image?: string;
|
|
region: string;
|
|
genres?: AudnexusAuthorGenre[];
|
|
similar?: AudnexusAuthorSimilar[];
|
|
}
|
|
|
|
/**
|
|
* Fetch with retry and exponential backoff for Audnexus API
|
|
*/
|
|
export async function audnexusFetchWithRetry(url: string, params: Record<string, string>, maxRetries = 3): Promise<any> {
|
|
let lastError: Error | null = null;
|
|
|
|
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
try {
|
|
return await axios.get(url, {
|
|
params,
|
|
timeout: AUDNEXUS_TIMEOUT,
|
|
headers: AUDNEXUS_HEADERS,
|
|
});
|
|
} catch (error: any) {
|
|
lastError = error;
|
|
const status = error.response?.status;
|
|
const isRetryable = !status || status === 503 || status === 429 || status >= 500;
|
|
|
|
if (!isRetryable) throw error;
|
|
if (attempt === maxRetries) break;
|
|
|
|
const backoffMs = Math.pow(2, attempt) * 1000;
|
|
logger.info(`Audnexus request failed (${status || 'network error'}), retrying in ${backoffMs}ms (attempt ${attempt + 1}/${maxRetries})`);
|
|
await new Promise(resolve => setTimeout(resolve, backoffMs));
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error('Audnexus request failed after retries');
|
|
}
|
|
|
|
/**
|
|
* Search authors via Audnexus and return deduplicated results
|
|
*/
|
|
export async function searchAuthors(name: string, region: string): Promise<AudnexusAuthorSearch[]> {
|
|
const response = await audnexusFetchWithRetry(`${AUDNEXUS_BASE}/authors`, { region, name });
|
|
const results: AudnexusAuthorSearch[] = response.data;
|
|
|
|
const seen = new Set<string>();
|
|
return results.filter(author => {
|
|
if (seen.has(author.asin)) return false;
|
|
seen.add(author.asin);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Fetch full author details from Audnexus
|
|
*/
|
|
export async function fetchAuthorDetail(asin: string, region: string): Promise<AudnexusAuthorDetail | null> {
|
|
try {
|
|
const response = await audnexusFetchWithRetry(`${AUDNEXUS_BASE}/authors/${asin}`, { region });
|
|
return response.data;
|
|
} catch (error: any) {
|
|
if (error.response?.status === 404) {
|
|
logger.debug(`Author not found on Audnexus: ${asin}`);
|
|
} else {
|
|
logger.warn(`Failed to fetch author detail: ${asin}`, { error: error.message });
|
|
}
|
|
return null;
|
|
}
|
|
}
|