Files
requestarr/server/api/servarr/readarr.ts
root 1cf0d541d6 Add Lidarr/Readarr backend support
- Add MUSIC and BOOK to MediaType enum
- Add permission flags for music/book requests
- Create Lidarr API adapter (artist/album search, add, remove)
- Create Readarr API adapter (book/author search, add, remove)
- Add Lidarr/Readarr settings interfaces and routes
- Add music and book API routes for search/detail
- Register all new routes in main router and settings router
2026-04-03 21:05:21 -05:00

226 lines
5.5 KiB
TypeScript

import logger from '@server/logger';
import ServarrBase from './base';
export interface ReadarrBookOptions {
title: string;
qualityProfileId: number;
metadataProfileId: number;
tags: number[];
rootFolderPath: string;
foreignBookId: string; // GoodReads/Edition ID
authorId?: number;
monitored?: boolean;
searchNow?: boolean;
}
export interface ReadarrAuthor {
id: number;
authorName: string;
foreignAuthorId: string;
monitored: boolean;
path: string;
qualityProfileId: number;
metadataProfileId: number;
rootFolderPath: string;
tags: number[];
added: string;
status: string;
ended: boolean;
images: {
coverType: string;
url: string;
remoteUrl: string;
}[];
genres: string[];
statistics?: {
bookFileCount: number;
bookCount: number;
totalBookCount: number;
sizeOnDisk: number;
percentOfBooks: number;
};
}
export interface ReadarrBook {
id: number;
title: string;
foreignBookId: string;
authorId: number;
monitored: boolean;
releaseDate: string;
genres: string[];
images: {
coverType: string;
url: string;
remoteUrl: string;
}[];
author: ReadarrAuthor;
overview: string;
pageCount: number;
statistics?: {
bookFileCount: number;
sizeOnDisk: number;
};
}
export interface ReadarrMetadataProfile {
id: number;
name: string;
}
class ReadarrAPI extends ServarrBase<{ authorId: number }> {
constructor({ url, apiKey }: { url: string; apiKey: string }) {
super({ url, apiKey, cacheName: 'readarr', apiName: 'Readarr' });
}
public getAuthors = async (): Promise<ReadarrAuthor[]> => {
try {
const response = await this.axios.get<ReadarrAuthor[]>('/author');
return response.data;
} catch (e) {
throw new Error(`[Readarr] Failed to retrieve authors: ${e.message}`, {
cause: e,
});
}
};
public getAuthor = async ({
id,
}: {
id: number;
}): Promise<ReadarrAuthor> => {
try {
const response = await this.axios.get<ReadarrAuthor>(`/author/${id}`);
return response.data;
} catch (e) {
throw new Error(`[Readarr] Failed to retrieve author: ${e.message}`, {
cause: e,
});
}
};
public searchBook = async (term: string): Promise<ReadarrBook[]> => {
try {
const response = await this.axios.get<ReadarrBook[]>('/book/lookup', {
params: { term },
});
return response.data;
} catch (e) {
throw new Error(`[Readarr] Failed to search books: ${e.message}`, {
cause: e,
});
}
};
public searchAuthor = async (term: string): Promise<ReadarrAuthor[]> => {
try {
const response = await this.axios.get<ReadarrAuthor[]>(
'/author/lookup',
{ params: { term } }
);
return response.data;
} catch (e) {
throw new Error(`[Readarr] Failed to search authors: ${e.message}`, {
cause: e,
});
}
};
public getBooks = async (authorId: number): Promise<ReadarrBook[]> => {
try {
const response = await this.axios.get<ReadarrBook[]>('/book', {
params: { authorId },
});
return response.data;
} catch (e) {
throw new Error(`[Readarr] Failed to retrieve books: ${e.message}`, {
cause: e,
});
}
};
public getBook = async ({ id }: { id: number }): Promise<ReadarrBook> => {
try {
const response = await this.axios.get<ReadarrBook>(`/book/${id}`);
return response.data;
} catch (e) {
throw new Error(`[Readarr] Failed to retrieve book: ${e.message}`, {
cause: e,
});
}
};
public addBook = async (
options: ReadarrBookOptions
): Promise<ReadarrBook> => {
try {
const lookupResults = await this.searchBook(
`readarr:${options.foreignBookId}`
);
const lookupBook = lookupResults[0];
if (!lookupBook) {
throw new Error('Book not found in lookup');
}
const response = await this.axios.post<ReadarrBook>('/book', {
...lookupBook,
qualityProfileId: options.qualityProfileId,
metadataProfileId: options.metadataProfileId,
rootFolderPath: options.rootFolderPath,
monitored: options.monitored ?? true,
tags: options.tags,
addOptions: {
searchForNewBook: options.searchNow ?? true,
},
});
if (response.data.id) {
logger.info('Readarr accepted request', {
label: 'Readarr',
bookId: response.data.id,
title: response.data.title,
});
}
return response.data;
} catch (e) {
logger.error('Failed to add book to Readarr', {
label: 'Readarr',
errorMessage: e.message,
options,
});
throw new Error('Failed to add book to Readarr', { cause: e });
}
};
public getMetadataProfiles = async (): Promise<
ReadarrMetadataProfile[]
> => {
try {
const response =
await this.axios.get<ReadarrMetadataProfile[]>('/metadataprofile');
return response.data;
} catch (e) {
throw new Error(
`[Readarr] Failed to retrieve metadata profiles: ${e.message}`,
{ cause: e }
);
}
};
public removeBook = async (bookId: number): Promise<void> => {
try {
await this.axios.delete(`/book/${bookId}`, {
params: { deleteFiles: true, addImportListExclusion: false },
});
logger.info(`[Readarr] Removed book ${bookId}`);
} catch (e) {
throw new Error(`[Readarr] Failed to remove book: ${e.message}`, {
cause: e,
});
}
};
}
export default ReadarrAPI;