mirror of
https://github.com/kikootwo/ReadMeABook.git
synced 2026-06-03 04:40:09 +00:00
95c25ff73a
Introduces optional e-book sidecar downloads from Anna's Archive, including admin UI, settings API, FlareSolverr integration, and documentation. Enhances request creation logic to prevent duplicate downloads by checking for 'downloaded' and 'available' statuses, updates UI to reflect processing state, and adds SABnzbd support to download and cleanup flows. Also updates ranking algorithm documentation and improves cache invalidation for recent requests.
128 lines
3.5 KiB
TypeScript
128 lines
3.5 KiB
TypeScript
/**
|
|
* 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<T>(endpoint: string, options: ABSRequestOptions = {}): Promise<T> {
|
|
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<any>(`/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);
|
|
}
|
|
}
|