Support language/format/publisher for Audible

Expose language, formatType, and publisherName from the Audible catalog. Update audible.service to map format_type and publisher_name (and language) into the AudibleAudiobook model, update AudiobookDetailsModal to display language and format using the CSS "capitalize" class, and update documentation to list the new fields. Add unit tests to verify the mappings, details propagation, and behavior when fields are omitted.
This commit is contained in:
kikootwo
2026-05-14 15:33:30 -04:00
parent 18752dd02b
commit 6c8ca9647d
5 changed files with 60 additions and 3 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ src/components/
**Audiobooks**
- **AudiobookCard** ✅ - Cover, title, author, narrator, duration, request button, clickable to open details modal. Shows "Requested by [username]" when someone else has requested the book, "Requested" when current user has requested it
- **AudiobookGrid** - Responsive grid (1/2/3/4 cols)
- **AudiobookDetailsModal** ✅ - Full-screen modal with comprehensive metadata (description, genres, rating, release date, narrator, request functionality). Shows requesting user's name when applicable
- **AudiobookDetailsModal** ✅ - Full-screen modal with comprehensive metadata (description, genres, rating, release date, narrator, language, format, publisher, request functionality). Shows requesting user's name when applicable
**Requests**
- **RequestCard** ✅ - Cover, title, author, status badge, progress bar, timestamps, action buttons (cancel, manual search, interactive search)
+3
View File
@@ -250,6 +250,9 @@ interface AudibleAudiobook {
series?: string;
seriesPart?: string;
seriesAsin?: string;
language?: string;
formatType?: string;
publisherName?: string;
}
interface EnrichedAudibleAudiobook extends AudibleAudiobook {
@@ -552,7 +552,7 @@ export function AudiobookDetailsModal({
{audiobook.language && (
<div>
<p className="text-gray-500 dark:text-gray-400">Language</p>
<p className="text-gray-900 dark:text-gray-100">{audiobook.language.charAt(0).toUpperCase() + audiobook.language.slice(1)}</p>
<p className="text-gray-900 dark:text-gray-100 capitalize">{audiobook.language}</p>
</div>
)}
@@ -560,7 +560,7 @@ export function AudiobookDetailsModal({
{audiobook.formatType && (
<div>
<p className="text-gray-500 dark:text-gray-400">Format</p>
<p className="text-gray-900 dark:text-gray-100">{audiobook.formatType.charAt(0).toUpperCase() + audiobook.formatType.slice(1)}</p>
<p className="text-gray-900 dark:text-gray-100 capitalize">{audiobook.formatType}</p>
</div>
)}
+5
View File
@@ -108,6 +108,8 @@ interface CatalogProduct {
runtime_length_min?: number;
release_date?: string;
language?: string;
format_type?: string;
publisher_name?: string;
rating?: {
overall_distribution?: {
display_stars?: number;
@@ -198,6 +200,9 @@ function mapCatalogProduct(product: CatalogProduct): AudibleAudiobook {
series,
seriesPart,
seriesAsin,
language: product.language ?? undefined,
formatType: product.format_type ?? undefined,
publisherName: product.publisher_name ?? undefined,
};
}
@@ -49,6 +49,8 @@ interface ProductOverrides {
runtime_length_min?: number;
release_date?: string;
language?: string;
format_type?: string;
publisher_name?: string;
rating?: { overall_distribution?: { display_stars?: number } };
category_ladders?: Array<{ ladder: Array<{ name: string }> }>;
series?: Array<{ asin?: string; title?: string; sequence?: string }>;
@@ -615,6 +617,47 @@ describe('AudibleService', () => {
const genreSet = new Set(results[0].genres);
expect(genreSet.size).toBe(5);
});
it('maps language from catalog product', async () => {
const products = [makeProduct({ language: 'english' })];
apiClientMock.get.mockResolvedValue(apiResponse(makeProductsResponse(products)));
const service = new AudibleService();
const { results } = await service.search('test', 1);
expect(results[0].language).toBe('english');
});
it('maps format_type to formatType from catalog product', async () => {
const products = [makeProduct({ format_type: 'unabridged' })];
apiClientMock.get.mockResolvedValue(apiResponse(makeProductsResponse(products)));
const service = new AudibleService();
const { results } = await service.search('test', 1);
expect(results[0].formatType).toBe('unabridged');
});
it('maps publisher_name to publisherName from catalog product', async () => {
const products = [makeProduct({ publisher_name: 'Penguin Random House Audio' })];
apiClientMock.get.mockResolvedValue(apiResponse(makeProductsResponse(products)));
const service = new AudibleService();
const { results } = await service.search('test', 1);
expect(results[0].publisherName).toBe('Penguin Random House Audio');
});
it('leaves formatType and publisherName undefined when catalog product omits them', async () => {
const products = [makeProduct()];
apiClientMock.get.mockResolvedValue(apiResponse(makeProductsResponse(products)));
const service = new AudibleService();
const { results } = await service.search('test', 1);
expect(results[0].formatType).toBeUndefined();
expect(results[0].publisherName).toBeUndefined();
});
});
// -------------------------------------------------------------------------
@@ -1262,6 +1305,9 @@ describe('AudibleService', () => {
runtimeLengthMin: '300',
genres: ['Fiction'],
rating: '4.7',
language: 'english',
formatType: 'unabridged',
publisherName: 'Test Publisher',
},
});
@@ -1271,6 +1317,9 @@ describe('AudibleService', () => {
expect(details?.title).toBe('Audnexus Book');
expect(details?.author).toBe('Author A');
expect(details?.durationMinutes).toBe(300);
expect(details?.language).toBe('english');
expect(details?.formatType).toBe('unabridged');
expect(details?.publisherName).toBe('Test Publisher');
// Catalog API should NOT be called when Audnexus succeeds.
expect(apiClientMock.get).not.toHaveBeenCalled();
});