/** * Component: Audiobookshelf API Client * Documentation: documentation/features/audiobookshelf-integration.md */ import { getConfigService } from '../config.service'; interface ABSRequestOptions { method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; body?: any; } /** * Make a request to the Audiobookshelf API */ export async function absRequest(endpoint: string, options: ABSRequestOptions = {}): Promise { const configService = getConfigService(); const serverUrl = await configService.get('audiobookshelf.server_url'); const apiToken = await configService.get('audiobookshelf.api_token'); if (!serverUrl || !apiToken) { throw new Error('Audiobookshelf not configured'); } const url = `${serverUrl.replace(/\/$/, '')}/api${endpoint}`; const response = await fetch(url, { method: options.method || 'GET', headers: { 'Authorization': `Bearer ${apiToken}`, 'Content-Type': 'application/json', }, body: options.body ? JSON.stringify(options.body) : undefined, }); if (!response.ok) { throw new Error(`ABS API error: ${response.status} ${response.statusText}`); } return response.json(); } /** * Get Audiobookshelf server status/info */ export async function getABSServerInfo() { return absRequest<{ version: string; name: string }>('/status'); } /** * Get all libraries from Audiobookshelf */ export async function getABSLibraries() { const result = await absRequest<{ libraries: any[] }>('/libraries'); return result.libraries; } /** * Get all items in a library */ export async function getABSLibraryItems(libraryId: string) { const result = await absRequest<{ results: any[] }>(`/libraries/${libraryId}/items`); return result.results; } /** * Get recently added items in a library */ export async function getABSRecentItems(libraryId: string, limit: number) { const result = await absRequest<{ results: any[] }>( `/libraries/${libraryId}/items?sort=addedAt&desc=1&limit=${limit}` ); return result.results; } /** * Get a single item by ID */ export async function getABSItem(itemId: string) { return absRequest(`/items/${itemId}`); } /** * Search for items in a library */ export async function searchABSItems(libraryId: string, query: string) { const result = await absRequest<{ book: any[] }>( `/libraries/${libraryId}/search?q=${encodeURIComponent(query)}` ); return result.book || []; } /** * Trigger a library scan */ export async function triggerABSScan(libraryId: string) { await absRequest(`/libraries/${libraryId}/scan`, { method: 'POST' }); } /** * Trigger metadata match for a specific library item * This tells Audiobookshelf to automatically match and populate metadata from providers * * @param itemId - The Audiobookshelf item ID * @param asin - Optional ASIN for direct Audible matching (100% accurate when provided) */ export async function triggerABSItemMatch(itemId: string, asin?: string) { try { const body: any = { provider: 'audible', // Use Audible as the metadata provider }; // If we have an ASIN, we can do a direct match with 100% confidence if (asin) { body.asin = asin; body.overrideDefaults = true; // Override defaults since we have exact ASIN match } await absRequest(`/items/${itemId}/match`, { method: 'POST', body, }); } catch (error) { // Don't throw - matching is best-effort, scan should continue even if match fails console.error(`[ABS] Failed to trigger match for item ${itemId}:`, error instanceof Error ? error.message : error); } }