From 6c8ca9647d96029cee5437bf58895713a7dea207 Mon Sep 17 00:00:00 2001 From: kikootwo Date: Thu, 14 May 2026 15:33:30 -0400 Subject: [PATCH] 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. --- documentation/frontend/components.md | 2 +- documentation/integrations/audible.md | 3 ++ .../audiobooks/AudiobookDetailsModal.tsx | 4 +- src/lib/integrations/audible.service.ts | 5 ++ tests/integrations/audible.service.test.ts | 49 +++++++++++++++++++ 5 files changed, 60 insertions(+), 3 deletions(-) diff --git a/documentation/frontend/components.md b/documentation/frontend/components.md index 62f8e6f..fca5969 100644 --- a/documentation/frontend/components.md +++ b/documentation/frontend/components.md @@ -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) diff --git a/documentation/integrations/audible.md b/documentation/integrations/audible.md index b7bac3b..b377b75 100644 --- a/documentation/integrations/audible.md +++ b/documentation/integrations/audible.md @@ -250,6 +250,9 @@ interface AudibleAudiobook { series?: string; seriesPart?: string; seriesAsin?: string; + language?: string; + formatType?: string; + publisherName?: string; } interface EnrichedAudibleAudiobook extends AudibleAudiobook { diff --git a/src/components/audiobooks/AudiobookDetailsModal.tsx b/src/components/audiobooks/AudiobookDetailsModal.tsx index 5bf23b8..9a7658c 100644 --- a/src/components/audiobooks/AudiobookDetailsModal.tsx +++ b/src/components/audiobooks/AudiobookDetailsModal.tsx @@ -552,7 +552,7 @@ export function AudiobookDetailsModal({ {audiobook.language && (

Language

-

{audiobook.language.charAt(0).toUpperCase() + audiobook.language.slice(1)}

+

{audiobook.language}

)} @@ -560,7 +560,7 @@ export function AudiobookDetailsModal({ {audiobook.formatType && (

Format

-

{audiobook.formatType.charAt(0).toUpperCase() + audiobook.formatType.slice(1)}

+

{audiobook.formatType}

)} diff --git a/src/lib/integrations/audible.service.ts b/src/lib/integrations/audible.service.ts index b19d721..9934171 100644 --- a/src/lib/integrations/audible.service.ts +++ b/src/lib/integrations/audible.service.ts @@ -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, }; } diff --git a/tests/integrations/audible.service.test.ts b/tests/integrations/audible.service.test.ts index f006031..ea4d955 100644 --- a/tests/integrations/audible.service.test.ts +++ b/tests/integrations/audible.service.test.ts @@ -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(); });