- 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
272 lines
7.0 KiB
TypeScript
272 lines
7.0 KiB
TypeScript
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;
|