mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
feat(api): sonarr api wrapper / send to sonarr
This commit is contained in:
@@ -159,14 +159,14 @@ components:
|
|||||||
example: false
|
example: false
|
||||||
baseUrl:
|
baseUrl:
|
||||||
type: string
|
type: string
|
||||||
activeProfile:
|
activeProfileId:
|
||||||
type: string
|
type: number
|
||||||
example: '1080p'
|
example: 1
|
||||||
activeDirectory:
|
activeDirectory:
|
||||||
type: string
|
type: string
|
||||||
example: '/movies'
|
example: '/tv/'
|
||||||
activeAnimeProfile:
|
activeAnimeProfileId:
|
||||||
type: string
|
type: number
|
||||||
nullable: true
|
nullable: true
|
||||||
activeAnimeDirectory:
|
activeAnimeDirectory:
|
||||||
type: string
|
type: string
|
||||||
@@ -183,7 +183,7 @@ components:
|
|||||||
- port
|
- port
|
||||||
- apiKey
|
- apiKey
|
||||||
- useSsl
|
- useSsl
|
||||||
- activeProfile
|
- activeProfileId
|
||||||
- activeDirectory
|
- activeDirectory
|
||||||
- is4k
|
- is4k
|
||||||
- enableSeasonFolders
|
- enableSeasonFolders
|
||||||
|
226
server/api/sonarr.ts
Normal file
226
server/api/sonarr.ts
Normal file
@@ -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 RadarrAPI from '../api/radarr';
|
||||||
import logger from '../logger';
|
import logger from '../logger';
|
||||||
import SeasonRequest from './SeasonRequest';
|
import SeasonRequest from './SeasonRequest';
|
||||||
|
import SonarrAPI from '../api/sonarr';
|
||||||
|
|
||||||
@Entity()
|
@Entity()
|
||||||
export class MediaRequest {
|
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 {
|
export interface SonarrSettings extends DVRSettings {
|
||||||
activeAnimeProfile?: string;
|
activeAnimeProfileId?: number;
|
||||||
activeAnimeDirectory?: string;
|
activeAnimeDirectory?: string;
|
||||||
enableSeasonFolders: boolean;
|
enableSeasonFolders: boolean;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user