- 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
226 lines
5.5 KiB
TypeScript
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;
|