feat(api): sonarr api wrapper / send to sonarr

pull/157/head
sct 4 years ago
parent 14b9cb610c
commit 9385592362

@ -159,14 +159,14 @@ components:
example: false
baseUrl:
type: string
activeProfile:
type: string
example: '1080p'
activeProfileId:
type: number
example: 1
activeDirectory:
type: string
example: '/movies'
activeAnimeProfile:
type: string
example: '/tv/'
activeAnimeProfileId:
type: number
nullable: true
activeAnimeDirectory:
type: string
@ -183,7 +183,7 @@ components:
- port
- apiKey
- useSsl
- activeProfile
- activeProfileId
- activeDirectory
- is4k
- enableSeasonFolders

@ -0,0 +1,226 @@
import Axios, { AxiosInstance } from 'axios';
import logger from '../logger';
interface SonarrSeason {
seasonNumber: number;
monitored: boolean;
}
interface SonarrSeries {
title: string;
sortTitle: string;
seasonCount: number;
status: string;
overview: string;
network: string;
airTime: string;
images: {
coverType: string;
url: string;
}[];
remotePoster: string;
seasons: SonarrSeason[];
year: number;
path: string;
profileId: number;
languageProfileId: number;
seasonFolder: boolean;
monitored: boolean;
useSceneNumbering: boolean;
runtime: number;
tvdbId: number;
tvRageId: number;
tvMazeId: number;
firstAired: string;
lastInfoSync?: string;
seriesType: string;
cleanTitle: string;
imdbId: string;
titleSlug: string;
certification: string;
genres: string[];
tags: string[];
added: string;
ratings: {
votes: number;
value: number;
};
qualityProfileId: number;
id?: number;
rootFolderPath?: string;
addOptions?: {
ignoreEpisodesWithFiles?: boolean;
ignoreEpisodesWithoutFiles?: boolean;
searchForMissingEpisodes?: boolean;
};
}
interface SonarrProfile {
id: number;
name: string;
}
interface SonarrRootFolder {
id: number;
path: string;
freeSpace: number;
totalSpace: number;
unmappedFolders: {
name: string;
path: string;
}[];
}
interface AddSeriesOptions {
tvdbid: number;
title: string;
profileId: number;
seasons: number[];
rootFolderPath: string;
monitored?: boolean;
searchNow?: boolean;
}
class SonarrAPI {
private axios: AxiosInstance;
constructor({ url, apiKey }: { url: string; apiKey: string }) {
this.axios = Axios.create({
baseURL: url,
params: {
apikey: apiKey,
},
});
}
public async getSeriesByTvdbId(id: number): Promise<SonarrSeries> {
try {
const response = await this.axios.get<SonarrSeries[]>('/series/lookup', {
params: {
term: `tvdb:${id}`,
},
});
if (!response.data[0]) {
throw new Error('Series not found');
}
return response.data[0];
} catch (e) {
logger.error('Error retrieving series by tvdb ID', {
label: 'Sonarr API',
message: e.message,
});
throw new Error('Series not found');
}
}
public async addSeries(options: AddSeriesOptions): Promise<SonarrSeries> {
try {
const series = await this.getSeriesByTvdbId(options.tvdbid);
// If the series already exists, we will simply just update it
if (series.id) {
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
series.addOptions = {
ignoreEpisodesWithFiles: true,
searchForMissingEpisodes: true,
};
const newSeriesResponse = await this.axios.put<SonarrSeries>(
'/series',
series
);
return newSeriesResponse.data;
}
const createdSeriesResponse = await this.axios.post<SonarrSeries>(
'/series',
{
tvdbId: options.tvdbid,
title: options.title,
profileId: options.profileId,
seasons: this.buildSeasonList(
options.seasons,
series.seasons.map((season) => ({
seasonNumber: season.seasonNumber,
// We force all seasons to false if its the first request
monitored: false,
}))
),
monitored: options.monitored,
rootFolderPath: options.rootFolderPath,
addOptions: {
ignoreEpisodesWithFiles: true,
searchForMissingEpisodes: options.searchNow,
},
} as Partial<SonarrSeries>
);
return createdSeriesResponse.data;
} catch (e) {
logger.error('Something went wrong adding a series to Sonarr', {
label: 'Sonarr API',
message: e.message,
error: e,
});
throw new Error('Failed to add series');
}
}
public async getProfiles(): Promise<SonarrProfile[]> {
try {
const response = await this.axios.get<SonarrProfile[]>('/profile');
return response.data;
} catch (e) {
logger.error('Something went wrong retrieving Sonarr profiles', {
label: 'Sonarr API',
message: e.message,
});
throw new Error('Failed to get profiles');
}
}
public async getRootFolders(): Promise<SonarrRootFolder[]> {
try {
const response = await this.axios.get<SonarrRootFolder[]>('/rootfolder');
return response.data;
} catch (e) {
logger.error('Something went wrong retrieving Sonarr root folders', {
label: 'Sonarr API',
message: e.message,
});
throw new Error('Failed to get root folders');
}
}
private buildSeasonList(
seasons: number[],
existingSeasons?: SonarrSeason[]
): SonarrSeason[] {
if (existingSeasons) {
const newSeasons = existingSeasons.map((season) => {
if (seasons.includes(season.seasonNumber)) {
season.monitored = true;
}
return season;
});
return newSeasons;
}
const newSeasons = seasons.map(
(seasonNumber): SonarrSeason => ({
seasonNumber,
monitored: true,
})
);
return newSeasons;
}
}
export default SonarrAPI;

@ -19,6 +19,7 @@ import TheMovieDb from '../api/themoviedb';
import RadarrAPI from '../api/radarr';
import logger from '../logger';
import SeasonRequest from './SeasonRequest';
import SonarrAPI from '../api/sonarr';
@Entity()
export class MediaRequest {
@ -168,4 +169,63 @@ export class MediaRequest {
}
}
}
@AfterUpdate()
@AfterInsert()
private async sendToSonarr() {
if (
this.status === MediaRequestStatus.APPROVED &&
this.type === MediaType.TV
) {
try {
const mediaRepository = getRepository(Media);
const settings = getSettings();
if (settings.sonarr.length === 0 && !settings.sonarr[0]) {
logger.info(
'Skipped sonarr request as there is no sonarr configured',
{ label: 'Media Request' }
);
return;
}
const media = await mediaRepository.findOne({
where: { id: this.media.id },
relations: ['requests'],
});
if (!media) {
throw new Error('Media data is missing');
}
const tmdb = new TheMovieDb();
const sonarrSettings = settings.sonarr[0];
const sonarr = new SonarrAPI({
apiKey: sonarrSettings.apiKey,
url: `${sonarrSettings.useSsl ? 'https' : 'http'}://${
sonarrSettings.hostname
}:${sonarrSettings.port}/api`,
});
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
if (!series.external_ids.tvdb_id) {
throw new Error('Series was missing tvdb id');
}
await sonarr.addSeries({
profileId: sonarrSettings.activeProfileId,
rootFolderPath: sonarrSettings.activeDirectory,
title: series.name,
tvdbid: series.external_ids.tvdb_id,
seasons: this.seasons.map((season) => season.seasonNumber),
monitored: true,
searchNow: true,
});
logger.info('Sent request to Sonarr', { label: 'Media Request' });
} catch (e) {
throw new Error(
`[MediaRequest] Request failed to send to sonarr: ${e.message}`
);
}
}
}
}

@ -34,7 +34,7 @@ export interface RadarrSettings extends DVRSettings {
}
export interface SonarrSettings extends DVRSettings {
activeAnimeProfile?: string;
activeAnimeProfileId?: number;
activeAnimeDirectory?: string;
enableSeasonFolders: boolean;
}

Loading…
Cancel
Save