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:
271
server/api/servarr/lidarr.ts
Normal file
271
server/api/servarr/lidarr.ts
Normal 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;
|
||||
Reference in New Issue
Block a user