diff --git a/documentation/integrations/audible.md b/documentation/integrations/audible.md index 91814d8..2a99641 100644 --- a/documentation/integrations/audible.md +++ b/documentation/integrations/audible.md @@ -218,3 +218,10 @@ interface EnrichedAudibleAudiobook extends AudibleAudiobook { - **Fix:** Updated ASIN regex to match both `/pd/` and `/ac/` URL patterns: `/\/(?:pd|ac)\/[^\/]+\/([A-Z0-9]{10})/` - **Location:** `src/lib/integrations/audible.service.ts:75, 161, 240` - **Affects:** `getPopularAudiobooks()`, `getNewReleases()`, `search()` methods + +**Audiobookshelf metadata matching not respecting configured region (2026-01-28)** +- **Problem:** `triggerABSItemMatch()` hardcoded `'audible'` provider (audible.com) instead of respecting user's configured Audible region +- **Impact:** Users with non-US regions (CA, UK, AU, IN) had incorrect metadata matching in Audiobookshelf, causing wrong ASINs and poor search results +- **Fix:** Added `mapRegionToABSProvider()` to convert RMAB region codes to AudiobookShelf provider values. US → `'audible'`, others → `'audible.{region}'` (e.g., `'audible.ca'`, `'audible.uk'`) +- **Location:** `src/lib/services/audiobookshelf/api.ts:14, 147` +- **Affects:** All Audiobookshelf metadata matching operations diff --git a/src/lib/services/audiobookshelf/api.ts b/src/lib/services/audiobookshelf/api.ts index 7fdea66..eacd939 100644 --- a/src/lib/services/audiobookshelf/api.ts +++ b/src/lib/services/audiobookshelf/api.ts @@ -5,9 +5,18 @@ import { getConfigService } from '../config.service'; import { RMABLogger } from '@/lib/utils/logger'; +import { AudibleRegion } from '@/lib/types/audible'; const logger = RMABLogger.create('Audiobookshelf'); +/** + * Map RMAB Audible region to Audiobookshelf provider value + */ +function mapRegionToABSProvider(region: AudibleRegion): string { + // US uses 'audible' (audible.com), all others use 'audible.{region}' + return region === 'us' ? 'audible' : `audible.${region}`; +} + interface ABSRequestOptions { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: any; @@ -133,8 +142,13 @@ export async function triggerABSScan(libraryId: string) { */ export async function triggerABSItemMatch(itemId: string, asin?: string) { try { + // Get configured Audible region to use correct ABS provider + const configService = getConfigService(); + const region = await configService.getAudibleRegion(); + const provider = mapRegionToABSProvider(region); + const body: any = { - provider: 'audible', // Use Audible as the metadata provider + provider, // Use region-specific Audible provider (e.g., 'audible.ca' for Canada) }; // If we have an ASIN, we can do a direct match with 100% confidence diff --git a/tests/services/audiobookshelf-api.test.ts b/tests/services/audiobookshelf-api.test.ts index 70f4f98..19d9338 100644 --- a/tests/services/audiobookshelf-api.test.ts +++ b/tests/services/audiobookshelf-api.test.ts @@ -18,6 +18,7 @@ import { const configServiceMock = vi.hoisted(() => ({ get: vi.fn(), + getAudibleRegion: vi.fn(), })); const fetchMock = vi.hoisted(() => vi.fn()); @@ -139,12 +140,13 @@ describe('Audiobookshelf API client', () => { })); }); - it('includes ASIN overrides in metadata match requests', async () => { + it('includes ASIN overrides in metadata match requests with US region', async () => { configServiceMock.get.mockImplementation(async (key: string) => { if (key === 'audiobookshelf.server_url') return 'http://abs'; if (key === 'audiobookshelf.api_token') return 'token'; return null; }); + configServiceMock.getAudibleRegion.mockResolvedValue('us'); fetchMock.mockResolvedValue({ ok: true, json: async () => ({}), @@ -154,18 +156,105 @@ describe('Audiobookshelf API client', () => { const body = JSON.parse(fetchMock.mock.calls[0][1].body); expect(body).toEqual({ - provider: 'audible', + provider: 'audible', // US uses 'audible' asin: 'ASIN123', overrideDefaults: true, }); }); + it('uses region-specific provider for Canada', async () => { + configServiceMock.get.mockImplementation(async (key: string) => { + if (key === 'audiobookshelf.server_url') return 'http://abs'; + if (key === 'audiobookshelf.api_token') return 'token'; + return null; + }); + configServiceMock.getAudibleRegion.mockResolvedValue('ca'); + fetchMock.mockResolvedValue({ + ok: true, + json: async () => ({}), + }); + + await triggerABSItemMatch('item-1', 'ASIN123'); + + const body = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(body).toEqual({ + provider: 'audible.ca', + asin: 'ASIN123', + overrideDefaults: true, + }); + }); + + it('uses region-specific provider for UK', async () => { + configServiceMock.get.mockImplementation(async (key: string) => { + if (key === 'audiobookshelf.server_url') return 'http://abs'; + if (key === 'audiobookshelf.api_token') return 'token'; + return null; + }); + configServiceMock.getAudibleRegion.mockResolvedValue('uk'); + fetchMock.mockResolvedValue({ + ok: true, + json: async () => ({}), + }); + + await triggerABSItemMatch('item-1'); + + const body = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(body).toEqual({ + provider: 'audible.uk', + }); + }); + + it('uses region-specific provider for Australia', async () => { + configServiceMock.get.mockImplementation(async (key: string) => { + if (key === 'audiobookshelf.server_url') return 'http://abs'; + if (key === 'audiobookshelf.api_token') return 'token'; + return null; + }); + configServiceMock.getAudibleRegion.mockResolvedValue('au'); + fetchMock.mockResolvedValue({ + ok: true, + json: async () => ({}), + }); + + await triggerABSItemMatch('item-1', 'ASIN456'); + + const body = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(body).toEqual({ + provider: 'audible.au', + asin: 'ASIN456', + overrideDefaults: true, + }); + }); + + it('uses region-specific provider for India', async () => { + configServiceMock.get.mockImplementation(async (key: string) => { + if (key === 'audiobookshelf.server_url') return 'http://abs'; + if (key === 'audiobookshelf.api_token') return 'token'; + return null; + }); + configServiceMock.getAudibleRegion.mockResolvedValue('in'); + fetchMock.mockResolvedValue({ + ok: true, + json: async () => ({}), + }); + + await triggerABSItemMatch('item-1', 'ASIN789'); + + const body = JSON.parse(fetchMock.mock.calls[0][1].body); + expect(body).toEqual({ + provider: 'audible.in', + asin: 'ASIN789', + overrideDefaults: true, + }); + }); + it('suppresses errors when metadata match fails', async () => { configServiceMock.get.mockImplementation(async (key: string) => { if (key === 'audiobookshelf.server_url') return 'http://abs'; if (key === 'audiobookshelf.api_token') return 'token'; return null; }); + configServiceMock.getAudibleRegion.mockResolvedValue('us'); fetchMock.mockResolvedValue({ ok: false, status: 500,