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(); });