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
This commit is contained in:
225
server/api/servarr/readarr.ts
Normal file
225
server/api/servarr/readarr.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user