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:
root
2026-04-03 21:05:21 -05:00
parent dc40ca413c
commit 1cf0d541d6
11 changed files with 985 additions and 0 deletions

View File

@@ -0,0 +1,271 @@
import logger from '@server/logger';
import ServarrBase from './base';
export interface LidarrArtistOptions {
artistName: string;
qualityProfileId: number;
metadataProfileId: number;
tags: number[];
rootFolderPath: string;
foreignArtistId: string; // MusicBrainz ID
monitored?: boolean;
searchNow?: boolean;
}
export interface LidarrAlbumOptions {
foreignAlbumId: string; // MusicBrainz Album ID
monitored?: boolean;
searchNow?: boolean;
}
export interface LidarrArtist {
id: number;
artistName: string;
foreignArtistId: string;
monitored: boolean;
path: string;
qualityProfileId: number;
metadataProfileId: number;
rootFolderPath: string;
tags: number[];
added: string;
status: string;
ended: boolean;
artistType: string;
disambiguation: string;
images: {
coverType: string;
url: string;
remoteUrl: string;
}[];
genres: string[];
statistics?: {
albumCount: number;
trackFileCount: number;
trackCount: number;
totalTrackCount: number;
sizeOnDisk: number;
percentOfTracks: number;
};
}
export interface LidarrAlbum {
id: number;
title: string;
foreignAlbumId: string;
artistId: number;
monitored: boolean;
albumType: string;
duration: number;
releaseDate: string;
genres: string[];
images: {
coverType: string;
url: string;
remoteUrl: string;
}[];
artist: LidarrArtist;
statistics?: {
trackFileCount: number;
trackCount: number;
totalTrackCount: number;
sizeOnDisk: number;
percentOfTracks: number;
};
}
export interface MetadataProfile {
id: number;
name: string;
}
class LidarrAPI extends ServarrBase<{ artistId: number }> {
constructor({ url, apiKey }: { url: string; apiKey: string }) {
super({ url, apiKey, cacheName: 'lidarr', apiName: 'Lidarr' });
}
public getArtists = async (): Promise<LidarrArtist[]> => {
try {
const response = await this.axios.get<LidarrArtist[]>('/artist');
return response.data;
} catch (e) {
throw new Error(`[Lidarr] Failed to retrieve artists: ${e.message}`, {
cause: e,
});
}
};
public getArtist = async ({ id }: { id: number }): Promise<LidarrArtist> => {
try {
const response = await this.axios.get<LidarrArtist>(`/artist/${id}`);
return response.data;
} catch (e) {
throw new Error(`[Lidarr] Failed to retrieve artist: ${e.message}`, {
cause: e,
});
}
};
public async getArtistByMbId(mbId: string): Promise<LidarrArtist | null> {
try {
const artists = await this.getArtists();
return artists.find((a) => a.foreignArtistId === mbId) || null;
} catch (e) {
logger.error('Error retrieving artist by MusicBrainz ID', {
label: 'Lidarr API',
errorMessage: e.message,
mbId,
});
return null;
}
}
public searchArtist = async (term: string): Promise<LidarrArtist[]> => {
try {
const response = await this.axios.get<LidarrArtist[]>(
'/artist/lookup',
{ params: { term } }
);
return response.data;
} catch (e) {
throw new Error(`[Lidarr] Failed to search artists: ${e.message}`, {
cause: e,
});
}
};
public searchAlbum = async (term: string): Promise<LidarrAlbum[]> => {
try {
const response = await this.axios.get<LidarrAlbum[]>('/album/lookup', {
params: { term },
});
return response.data;
} catch (e) {
throw new Error(`[Lidarr] Failed to search albums: ${e.message}`, {
cause: e,
});
}
};
public getAlbums = async (artistId: number): Promise<LidarrAlbum[]> => {
try {
const response = await this.axios.get<LidarrAlbum[]>('/album', {
params: { artistId },
});
return response.data;
} catch (e) {
throw new Error(`[Lidarr] Failed to retrieve albums: ${e.message}`, {
cause: e,
});
}
};
public getAlbum = async ({ id }: { id: number }): Promise<LidarrAlbum> => {
try {
const response = await this.axios.get<LidarrAlbum>(`/album/${id}`);
return response.data;
} catch (e) {
throw new Error(`[Lidarr] Failed to retrieve album: ${e.message}`, {
cause: e,
});
}
};
public addArtist = async (
options: LidarrArtistOptions
): Promise<LidarrArtist> => {
try {
// Check if artist already exists
const existing = await this.getArtistByMbId(options.foreignArtistId);
if (existing) {
logger.info('Artist already exists in Lidarr.', {
label: 'Lidarr',
artistId: existing.id,
artistName: existing.artistName,
});
return existing;
}
// Look up artist details
const lookupResults = await this.searchArtist(
`lidarr:${options.foreignArtistId}`
);
const lookupArtist = lookupResults[0];
const response = await this.axios.post<LidarrArtist>('/artist', {
...lookupArtist,
artistName: options.artistName,
qualityProfileId: options.qualityProfileId,
metadataProfileId: options.metadataProfileId,
rootFolderPath: options.rootFolderPath,
monitored: options.monitored ?? true,
tags: options.tags,
addOptions: {
monitor: 'all',
searchForMissingAlbums: options.searchNow ?? true,
},
});
if (response.data.id) {
logger.info('Lidarr accepted request', {
label: 'Lidarr',
artistId: response.data.id,
artistName: response.data.artistName,
});
}
return response.data;
} catch (e) {
logger.error('Failed to add artist to Lidarr', {
label: 'Lidarr',
errorMessage: e.message,
options,
});
throw new Error('Failed to add artist to Lidarr', { cause: e });
}
};
public async searchArtistCommand(artistId: number): Promise<void> {
logger.info('Executing artist search command', {
label: 'Lidarr API',
artistId,
});
try {
await this.runCommand('ArtistSearch', { artistId });
} catch (e) {
logger.error('Something went wrong executing Lidarr artist search.', {
label: 'Lidarr API',
errorMessage: e.message,
artistId,
});
}
}
public getMetadataProfiles = async (): Promise<MetadataProfile[]> => {
try {
const response =
await this.axios.get<MetadataProfile[]>('/metadataprofile');
return response.data;
} catch (e) {
throw new Error(
`[Lidarr] Failed to retrieve metadata profiles: ${e.message}`,
{ cause: e }
);
}
};
public removeArtist = async (artistId: number): Promise<void> => {
try {
await this.axios.delete(`/artist/${artistId}`, {
params: { deleteFiles: true, addImportListExclusion: false },
});
logger.info(`[Lidarr] Removed artist ${artistId}`);
} catch (e) {
throw new Error(`[Lidarr] Failed to remove artist: ${e.message}`, {
cause: e,
});
}
};
}
export default LidarrAPI;