feat: Radarr & Sonarr Sync (#734)

This commit is contained in:
sct
2021-01-27 23:52:37 +09:00
committed by GitHub
parent 86efcd82c3
commit ec5fb83678
32 changed files with 2394 additions and 425 deletions

View File

@@ -1,4 +1,5 @@
import Axios, { AxiosInstance } from 'axios';
import { RadarrSettings } from '../lib/settings';
import logger from '../logger';
interface RadarrMovieOptions {
@@ -13,12 +14,13 @@ interface RadarrMovieOptions {
searchNow?: boolean;
}
interface RadarrMovie {
export interface RadarrMovie {
id: number;
title: string;
isAvailable: boolean;
monitored: boolean;
tmdbId: number;
imdbId: string;
titleSlug: string;
folderName: string;
path: string;
@@ -45,7 +47,39 @@ export interface RadarrProfile {
name: string;
}
interface QueueItem {
movieId: number;
size: number;
title: string;
sizeleft: number;
timeleft: string;
estimatedCompletionTime: string;
status: string;
trackedDownloadStatus: string;
trackedDownloadState: string;
downloadId: string;
protocol: string;
downloadClient: string;
indexer: string;
id: number;
}
interface QueueResponse {
page: number;
pageSize: number;
sortKey: string;
sortDirection: string;
totalRecords: number;
records: QueueItem[];
}
class RadarrAPI {
static buildRadarrUrl(radarrSettings: RadarrSettings, path?: string): string {
return `${radarrSettings.useSsl ? 'https' : 'http'}://${
radarrSettings.hostname
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}${path}`;
}
private axios: AxiosInstance;
constructor({ url, apiKey }: { url: string; apiKey: string }) {
this.axios = Axios.create({
@@ -76,8 +110,43 @@ class RadarrAPI {
}
};
public addMovie = async (options: RadarrMovieOptions): Promise<boolean> => {
public async getMovieByTmdbId(id: number): Promise<RadarrMovie> {
try {
const response = await this.axios.get<RadarrMovie[]>('/movie/lookup', {
params: {
term: `tmdb:${id}`,
},
});
if (!response.data[0]) {
throw new Error('Movie not found');
}
return response.data[0];
} catch (e) {
logger.error('Error retrieving movie by TMDb ID', {
label: 'Radarr API',
message: e.message,
});
throw new Error('Movie not found');
}
}
public addMovie = async (
options: RadarrMovieOptions
): Promise<RadarrMovie> => {
try {
// Check if movie already exists
const existing = await this.getMovieByTmdbId(options.tmdbId);
if (existing) {
logger.info(
'Movie already exists in Radarr. Skipping add and returning success',
{ label: 'Radarr' }
);
return existing;
}
const response = await this.axios.post<RadarrMovie>(`/movie`, {
title: options.title,
qualityProfileId: options.qualityProfileId,
@@ -104,9 +173,9 @@ class RadarrAPI {
label: 'Radarr',
options,
});
return false;
throw new Error('Failed to add movie to Radarr');
}
return true;
return response.data;
} catch (e) {
logger.error(
'Failed to add movie to Radarr. This might happen if the movie already exists, in which case you can safely ignore this error.',
@@ -117,10 +186,7 @@ class RadarrAPI {
response: e?.response?.data,
}
);
if (e?.response?.data?.[0]?.errorCode === 'MovieExistsValidator') {
return true;
}
return false;
throw new Error('Failed to add movie to Radarr');
}
};
@@ -143,6 +209,16 @@ class RadarrAPI {
throw new Error(`[Radarr] Failed to retrieve root folders: ${e.message}`);
}
};
public getQueue = async (): Promise<QueueItem[]> => {
try {
const response = await this.axios.get<QueueResponse>(`/queue`);
return response.data.records;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve queue: ${e.message}`);
}
};
}
export default RadarrAPI;

View File

@@ -1,9 +1,18 @@
import Axios, { AxiosInstance } from 'axios';
import { SonarrSettings } from '../lib/settings';
import logger from '../logger';
interface SonarrSeason {
seasonNumber: number;
monitored: boolean;
statistics?: {
previousAiring?: string;
episodeFileCount: number;
episodeCount: number;
totalEpisodeCount: number;
sizeOnDisk: number;
percentOfEpisodes: number;
};
}
export interface SonarrSeries {
@@ -55,6 +64,33 @@ export interface SonarrSeries {
};
}
interface QueueItem {
seriesId: number;
episodeId: number;
size: number;
title: string;
sizeleft: number;
timeleft: string;
estimatedCompletionTime: string;
status: string;
trackedDownloadStatus: string;
trackedDownloadState: string;
downloadId: string;
protocol: string;
downloadClient: string;
indexer: string;
id: number;
}
interface QueueResponse {
page: number;
pageSize: number;
sortKey: string;
sortDirection: string;
totalRecords: number;
records: QueueItem[];
}
interface SonarrProfile {
id: number;
name: string;
@@ -84,6 +120,12 @@ interface AddSeriesOptions {
}
class SonarrAPI {
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
sonarrSettings.hostname
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}${path}`;
}
private axios: AxiosInstance;
constructor({ url, apiKey }: { url: string; apiKey: string }) {
this.axios = Axios.create({
@@ -94,6 +136,16 @@ class SonarrAPI {
});
}
public async getSeries(): Promise<SonarrSeries[]> {
try {
const response = await this.axios.get<SonarrSeries[]>('/series');
return response.data;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve series: ${e.message}`);
}
}
public async getSeriesByTitle(title: string): Promise<SonarrSeries[]> {
try {
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
@@ -138,7 +190,7 @@ class SonarrAPI {
}
}
public async addSeries(options: AddSeriesOptions): Promise<boolean> {
public async addSeries(options: AddSeriesOptions): Promise<SonarrSeries> {
try {
const series = await this.getSeriesByTvdbId(options.tvdbid);
@@ -160,19 +212,19 @@ class SonarrAPI {
logger.info('Sonarr accepted request. Updated existing series', {
label: 'Sonarr',
});
logger.debug('Sonarr add details', {
logger.debug('Sonarr update details', {
label: 'Sonarr',
movie: newSeriesResponse.data,
});
} else {
logger.error('Failed to add movie to Sonarr', {
logger.error('Failed to update series in Sonarr', {
label: 'Sonarr',
options,
});
return false;
throw new Error('Failed to update series in Sonarr');
}
return true;
return newSeriesResponse.data;
}
const createdSeriesResponse = await this.axios.post<SonarrSeries>(
@@ -211,10 +263,10 @@ class SonarrAPI {
label: 'Sonarr',
options,
});
return false;
throw new Error('Failed to add series to Sonarr');
}
return true;
return createdSeriesResponse.data;
} catch (e) {
logger.error('Something went wrong while adding a series to Sonarr.', {
label: 'Sonarr API',
@@ -222,7 +274,7 @@ class SonarrAPI {
error: e,
response: e?.response?.data,
});
return false;
throw new Error('Failed to add series');
}
}
@@ -282,6 +334,16 @@ class SonarrAPI {
return newSeasons;
}
public getQueue = async (): Promise<QueueItem[]> => {
try {
const response = await this.axios.get<QueueResponse>(`/queue`);
return response.data.records;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve queue: ${e.message}`);
}
};
}
export default SonarrAPI;