feat: radarr/sonarr tag support (#1366)

This commit is contained in:
sct
2021-04-05 21:16:55 +09:00
committed by GitHub
parent db5af6d24b
commit a306ebc2d1
24 changed files with 760 additions and 366 deletions

169
server/api/servarr/base.ts Normal file
View File

@@ -0,0 +1,169 @@
import cacheManager, { AvailableCacheIds } from '../../lib/cache';
import { DVRSettings } from '../../lib/settings';
import ExternalAPI from '../externalapi';
export interface RootFolder {
id: number;
path: string;
freeSpace: number;
totalSpace: number;
unmappedFolders: {
name: string;
path: string;
}[];
}
export interface QualityProfile {
id: number;
name: string;
}
interface QueueItem {
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;
}
export interface Tag {
id: number;
label: string;
}
interface QueueResponse<QueueItemAppendT> {
page: number;
pageSize: number;
sortKey: string;
sortDirection: string;
totalRecords: number;
records: (QueueItem & QueueItemAppendT)[];
}
class ServarrBase<QueueItemAppendT> extends ExternalAPI {
static buildUrl(settings: DVRSettings, path?: string): string {
return `${settings.useSsl ? 'https' : 'http'}://${settings.hostname}:${
settings.port
}${settings.baseUrl ?? ''}${path}`;
}
protected apiName: string;
constructor({
url,
apiKey,
cacheName,
apiName,
}: {
url: string;
apiKey: string;
cacheName: AvailableCacheIds;
apiName: string;
}) {
super(
url,
{
apikey: apiKey,
},
{
nodeCache: cacheManager.getCache(cacheName).data,
}
);
this.apiName = apiName;
}
public getProfiles = async (): Promise<QualityProfile[]> => {
try {
const data = await this.getRolling<QualityProfile[]>(
`/qualityProfile`,
undefined,
3600
);
return data;
} catch (e) {
throw new Error(
`[${this.apiName}] Failed to retrieve profiles: ${e.message}`
);
}
};
public getRootFolders = async (): Promise<RootFolder[]> => {
try {
const data = await this.getRolling<RootFolder[]>(
`/rootfolder`,
undefined,
3600
);
return data;
} catch (e) {
throw new Error(
`[${this.apiName}] Failed to retrieve root folders: ${e.message}`
);
}
};
public getQueue = async (): Promise<(QueueItem & QueueItemAppendT)[]> => {
try {
const response = await this.axios.get<QueueResponse<QueueItemAppendT>>(
`/queue`
);
return response.data.records;
} catch (e) {
throw new Error(
`[${this.apiName}] Failed to retrieve queue: ${e.message}`
);
}
};
public getTags = async (): Promise<Tag[]> => {
try {
const response = await this.axios.get<Tag[]>(`/tag`);
return response.data;
} catch (e) {
throw new Error(
`[${this.apiName}] Failed to retrieve tags: ${e.message}`
);
}
};
public createTag = async ({ label }: { label: string }): Promise<Tag> => {
try {
const response = await this.axios.post<Tag>(`/tag`, {
label,
});
return response.data;
} catch (e) {
throw new Error(`[${this.apiName}] Failed to create tag: ${e.message}`);
}
};
protected async runCommand(
commandName: string,
options: Record<string, unknown>
): Promise<void> {
try {
await this.axios.post(`/command`, {
name: commandName,
...options,
});
} catch (e) {
throw new Error(`[${this.apiName}] Failed to run command: ${e.message}`);
}
}
}
export default ServarrBase;

View File

@@ -1,12 +1,11 @@
import cacheManager from '../lib/cache';
import { RadarrSettings } from '../lib/settings';
import logger from '../logger';
import ExternalAPI from './externalapi';
import logger from '../../logger';
import ServarrBase from './base';
interface RadarrMovieOptions {
title: string;
qualityProfileId: number;
minimumAvailability: string;
tags: number[];
profileId: number;
year: number;
rootFolderPath: string;
@@ -32,65 +31,9 @@ export interface RadarrMovie {
hasFile: boolean;
}
export interface RadarrRootFolder {
id: number;
path: string;
freeSpace: number;
totalSpace: number;
unmappedFolders: {
name: string;
path: string;
}[];
}
export interface RadarrProfile {
id: number;
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 extends ExternalAPI {
static buildRadarrUrl(radarrSettings: RadarrSettings, path?: string): string {
return `${radarrSettings.useSsl ? 'https' : 'http'}://${
radarrSettings.hostname
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}${path}`;
}
class RadarrAPI extends ServarrBase<{ movieId: number }> {
constructor({ url, apiKey }: { url: string; apiKey: string }) {
super(
url,
{
apikey: apiKey,
},
{
nodeCache: cacheManager.getCache('radarr').data,
}
);
super({ url, apiKey, cacheName: 'radarr', apiName: 'Radarr' });
}
public getMovies = async (): Promise<RadarrMovie[]> => {
@@ -162,6 +105,7 @@ class RadarrAPI extends ExternalAPI {
minimumAvailability: options.minimumAvailability,
tmdbId: options.tmdbId,
year: options.year,
tags: options.tags,
rootFolderPath: options.rootFolderPath,
monitored: options.monitored,
addOptions: {
@@ -206,6 +150,7 @@ class RadarrAPI extends ExternalAPI {
year: options.year,
rootFolderPath: options.rootFolderPath,
monitored: options.monitored,
tags: options.tags,
addOptions: {
searchForMovie: options.searchNow,
},
@@ -238,44 +183,6 @@ class RadarrAPI extends ExternalAPI {
throw new Error('Failed to add movie to Radarr');
}
};
public getProfiles = async (): Promise<RadarrProfile[]> => {
try {
const data = await this.getRolling<RadarrProfile[]>(
`/qualityProfile`,
undefined,
3600
);
return data;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve profiles: ${e.message}`);
}
};
public getRootFolders = async (): Promise<RadarrRootFolder[]> => {
try {
const data = await this.getRolling<RadarrRootFolder[]>(
`/rootfolder`,
undefined,
3600
);
return data;
} catch (e) {
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,7 +1,5 @@
import cacheManager from '../lib/cache';
import { SonarrSettings } from '../lib/settings';
import logger from '../logger';
import ExternalAPI from './externalapi';
import logger from '../../logger';
import ServarrBase from './base';
interface SonarrSeason {
seasonNumber: number;
@@ -49,7 +47,7 @@ export interface SonarrSeries {
titleSlug: string;
certification: string;
genres: string[];
tags: string[];
tags: number[];
added: string;
ratings: {
votes: number;
@@ -65,49 +63,6 @@ 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;
}
interface SonarrRootFolder {
id: number;
path: string;
freeSpace: number;
totalSpace: number;
unmappedFolders: {
name: string;
path: string;
}[];
}
interface AddSeriesOptions {
tvdbid: number;
title: string;
@@ -116,6 +71,7 @@ interface AddSeriesOptions {
seasons: number[];
seasonFolder: boolean;
rootFolderPath: string;
tags?: number[];
seriesType: SonarrSeries['seriesType'];
monitored?: boolean;
searchNow?: boolean;
@@ -126,23 +82,9 @@ export interface LanguageProfile {
name: string;
}
class SonarrAPI extends ExternalAPI {
static buildSonarrUrl(sonarrSettings: SonarrSettings, path?: string): string {
return `${sonarrSettings.useSsl ? 'https' : 'http'}://${
sonarrSettings.hostname
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}${path}`;
}
class SonarrAPI extends ServarrBase<{ seriesId: number; episodeId: number }> {
constructor({ url, apiKey }: { url: string; apiKey: string }) {
super(
url,
{
apikey: apiKey,
},
{
nodeCache: cacheManager.getCache('sonarr').data,
}
);
super({ url, apiKey, apiName: 'Sonarr', cacheName: 'sonarr' });
}
public async getSeries(): Promise<SonarrSeries[]> {
@@ -151,7 +93,7 @@ class SonarrAPI extends ExternalAPI {
return response.data;
} catch (e) {
throw new Error(`[Radarr] Failed to retrieve series: ${e.message}`);
throw new Error(`[Sonarr] Failed to retrieve series: ${e.message}`);
}
}
@@ -205,6 +147,7 @@ class SonarrAPI extends ExternalAPI {
// If the series already exists, we will simply just update it
if (series.id) {
series.tags = options.tags ?? series.tags;
series.seasons = this.buildSeasonList(options.seasons, series.seasons);
const newSeriesResponse = await this.axios.put<SonarrSeries>(
@@ -249,6 +192,7 @@ class SonarrAPI extends ExternalAPI {
monitored: false,
}))
),
tags: options.tags,
seasonFolder: options.seasonFolder,
monitored: options.monitored,
rootFolderPath: options.rootFolderPath,
@@ -286,46 +230,6 @@ class SonarrAPI extends ExternalAPI {
}
}
public async getProfiles(): Promise<SonarrProfile[]> {
try {
const data = await this.getRolling<SonarrProfile[]>(
'/qualityProfile',
undefined,
3600
);
return data;
} catch (e) {
logger.error('Something went wrong while retrieving Sonarr profiles.', {
label: 'Sonarr API',
message: e.message,
});
throw new Error('Failed to get profiles');
}
}
public async getRootFolders(): Promise<SonarrRootFolder[]> {
try {
const data = await this.getRolling<SonarrRootFolder[]>(
'/rootfolder',
undefined,
3600
);
return data;
} catch (e) {
logger.error(
'Something went wrong while retrieving Sonarr root folders.',
{
label: 'Sonarr API',
message: e.message,
}
);
throw new Error('Failed to get root folders');
}
}
public async getLanguageProfiles(): Promise<LanguageProfile[]> {
try {
const data = await this.getRolling<LanguageProfile[]>(
@@ -356,25 +260,6 @@ class SonarrAPI extends ExternalAPI {
await this.runCommand('SeriesSearch', { seriesId });
}
private async runCommand(
commandName: string,
options: Record<string, unknown>
): Promise<void> {
try {
await this.axios.post(`/command`, {
name: commandName,
...options,
});
} catch (e) {
logger.error('Something went wrong attempting to run a Sonarr command.', {
label: 'Sonarr API',
message: e.message,
});
throw new Error('Failed to run Sonarr command.');
}
}
private buildSeasonList(
seasons: number[],
existingSeasons?: SonarrSeason[]
@@ -399,16 +284,6 @@ class SonarrAPI extends ExternalAPI {
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;

View File

@@ -1,23 +1,23 @@
import {
Entity,
PrimaryGeneratedColumn,
AfterLoad,
Column,
Index,
OneToMany,
CreateDateColumn,
UpdateDateColumn,
Entity,
getRepository,
In,
AfterLoad,
Index,
OneToMany,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { MediaRequest } from './MediaRequest';
import RadarrAPI from '../api/servarr/radarr';
import SonarrAPI from '../api/servarr/sonarr';
import { MediaStatus, MediaType } from '../constants/media';
import logger from '../logger';
import Season from './Season';
import { getSettings } from '../lib/settings';
import RadarrAPI from '../api/radarr';
import downloadTracker, { DownloadingItem } from '../lib/downloadtracker';
import SonarrAPI from '../api/sonarr';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import { MediaRequest } from './MediaRequest';
import Season from './Season';
@Entity()
class Media {
@@ -168,10 +168,7 @@ class Media {
if (server) {
this.serviceUrl = server.externalUrl
? `${server.externalUrl}/movie/${this.externalServiceSlug}`
: RadarrAPI.buildRadarrUrl(
server,
`/movie/${this.externalServiceSlug}`
);
: RadarrAPI.buildUrl(server, `/movie/${this.externalServiceSlug}`);
}
}
@@ -184,7 +181,7 @@ class Media {
if (server) {
this.serviceUrl4k = server.externalUrl
? `${server.externalUrl}/movie/${this.externalServiceSlug4k}`
: RadarrAPI.buildRadarrUrl(
: RadarrAPI.buildUrl(
server,
`/movie/${this.externalServiceSlug4k}`
);
@@ -202,10 +199,7 @@ class Media {
if (server) {
this.serviceUrl = server.externalUrl
? `${server.externalUrl}/series/${this.externalServiceSlug}`
: SonarrAPI.buildSonarrUrl(
server,
`/series/${this.externalServiceSlug}`
);
: SonarrAPI.buildUrl(server, `/series/${this.externalServiceSlug}`);
}
}
@@ -218,7 +212,7 @@ class Media {
if (server) {
this.serviceUrl4k = server.externalUrl
? `${server.externalUrl}/series/${this.externalServiceSlug4k}`
: SonarrAPI.buildSonarrUrl(
: SonarrAPI.buildUrl(
server,
`/series/${this.externalServiceSlug4k}`
);

View File

@@ -1,28 +1,29 @@
import { isEqual } from 'lodash';
import {
Entity,
PrimaryGeneratedColumn,
ManyToOne,
AfterInsert,
AfterRemove,
AfterUpdate,
Column,
CreateDateColumn,
UpdateDateColumn,
AfterUpdate,
AfterInsert,
Entity,
getRepository,
ManyToOne,
OneToMany,
AfterRemove,
PrimaryGeneratedColumn,
RelationCount,
UpdateDateColumn,
} from 'typeorm';
import { User } from './User';
import Media from './Media';
import { MediaStatus, MediaRequestStatus, MediaType } from '../constants/media';
import { getSettings } from '../lib/settings';
import RadarrAPI from '../api/servarr/radarr';
import SonarrAPI, { SonarrSeries } from '../api/servarr/sonarr';
import TheMovieDb from '../api/themoviedb';
import { ANIME_KEYWORD_ID } from '../api/themoviedb/constants';
import RadarrAPI from '../api/radarr';
import logger from '../logger';
import SeasonRequest from './SeasonRequest';
import SonarrAPI, { SonarrSeries } from '../api/sonarr';
import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media';
import notificationManager, { Notification } from '../lib/notifications';
import { getSettings } from '../lib/settings';
import logger from '../logger';
import Media from './Media';
import SeasonRequest from './SeasonRequest';
import { User } from './User';
@Entity()
export class MediaRequest {
@@ -85,6 +86,37 @@ export class MediaRequest {
@Column({ nullable: true })
public languageProfileId: number;
@Column({
type: 'text',
nullable: true,
transformer: {
from: (value: string | null): number[] | null => {
if (value) {
if (value === 'none') {
return [];
}
return value.split(',').map((v) => Number(v));
}
return null;
},
to: (value: number[] | null): string | null => {
if (value) {
const finalValue = value.join(',');
// We want to keep the actual state of an "empty array" so we use
// the keyword "none" to track this.
if (!finalValue) {
return 'none';
}
return finalValue;
}
return null;
},
},
})
public tags?: number[];
constructor(init?: Partial<MediaRequest>) {
Object.assign(this, init);
}
@@ -365,6 +397,7 @@ export class MediaRequest {
let rootFolder = radarrSettings.activeDirectory;
let qualityProfile = radarrSettings.activeProfileId;
let tags = radarrSettings.tags;
if (
this.rootFolder &&
@@ -387,10 +420,22 @@ export class MediaRequest {
});
}
if (
this.tags &&
(radarrSettings.tags.length !== (this.tags?.length ?? 0) ||
radarrSettings.tags.every((num) => (this.tags ?? []).includes(num)))
) {
tags = this.tags;
logger.info(`Request has override tags`, {
label: 'Media Request',
tagIds: tags,
});
}
const tmdb = new TheMovieDb();
const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey,
url: RadarrAPI.buildRadarrUrl(radarrSettings, '/api/v3'),
url: RadarrAPI.buildUrl(radarrSettings, '/api/v3'),
});
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
@@ -420,6 +465,7 @@ export class MediaRequest {
tmdbId: movie.id,
year: Number(movie.release_date.slice(0, 4)),
monitored: true,
tags,
searchNow: !radarrSettings.preventSearch,
})
.then(async (radarrMovie) => {
@@ -531,7 +577,7 @@ export class MediaRequest {
const tmdb = new TheMovieDb();
const sonarr = new SonarrAPI({
apiKey: sonarrSettings.apiKey,
url: SonarrAPI.buildSonarrUrl(sonarrSettings, '/api/v3'),
url: SonarrAPI.buildUrl(sonarrSettings, '/api/v3'),
});
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
const tvdbId = series.external_ids.tvdb_id ?? media.tvdbId;
@@ -568,6 +614,11 @@ export class MediaRequest {
? sonarrSettings.activeAnimeLanguageProfileId
: sonarrSettings.activeLanguageProfileId;
let tags =
seriesType === 'anime'
? sonarrSettings.animeTags
: sonarrSettings.tags;
if (
this.rootFolder &&
this.rootFolder !== '' &&
@@ -599,6 +650,14 @@ export class MediaRequest {
);
}
if (this.tags && !isEqual(this.tags, tags)) {
tags = this.tags;
logger.info(`Request has override tags`, {
label: 'Media Request',
tags,
});
}
// Run this asynchronously so we don't wait for it on the UI side
sonarr
.addSeries({
@@ -610,6 +669,7 @@ export class MediaRequest {
seasons: this.seasons.map((season) => season.seasonNumber),
seasonFolder: sonarrSettings.enableSeasonFolders,
seriesType,
tags,
monitored: true,
searchNow: !sonarrSettings.preventSearch,
})

View File

@@ -1,5 +1,5 @@
import { RadarrProfile, RadarrRootFolder } from '../../api/radarr';
import { LanguageProfile } from '../../api/sonarr';
import { QualityProfile, RootFolder, Tag } from '../../api/servarr/base';
import { LanguageProfile } from '../../api/servarr/sonarr';
export interface ServiceCommonServer {
id: number;
@@ -12,11 +12,14 @@ export interface ServiceCommonServer {
activeAnimeProfileId?: number;
activeAnimeDirectory?: string;
activeAnimeLanguageProfileId?: number;
activeTags: number[];
activeAnimeTags?: number[];
}
export interface ServiceCommonServerWithDetails {
server: ServiceCommonServer;
profiles: RadarrProfile[];
rootFolders: Partial<RadarrRootFolder>[];
profiles: QualityProfile[];
rootFolders: Partial<RootFolder>[];
languageProfiles?: LanguageProfile[];
tags: Tag[];
}

View File

@@ -1,6 +1,6 @@
import { uniqWith } from 'lodash';
import RadarrAPI from '../api/radarr';
import SonarrAPI from '../api/sonarr';
import RadarrAPI from '../api/servarr/radarr';
import SonarrAPI from '../api/servarr/sonarr';
import { MediaType } from '../constants/media';
import logger from '../logger';
import { getSettings } from './settings';
@@ -73,7 +73,7 @@ class DownloadTracker {
if (server.syncEnabled) {
const radarr = new RadarrAPI({
apiKey: server.apiKey,
url: RadarrAPI.buildRadarrUrl(server, '/api/v3'),
url: RadarrAPI.buildUrl(server, '/api/v3'),
});
const queueItems = await radarr.getQueue();
@@ -140,7 +140,7 @@ class DownloadTracker {
if (server.syncEnabled) {
const radarr = new SonarrAPI({
apiKey: server.apiKey,
url: SonarrAPI.buildSonarrUrl(server, '/api/v3'),
url: SonarrAPI.buildUrl(server, '/api/v3'),
});
const queueItems = await radarr.getQueue();

View File

@@ -1,5 +1,5 @@
import { uniqWith } from 'lodash';
import RadarrAPI, { RadarrMovie } from '../../../api/radarr';
import RadarrAPI, { RadarrMovie } from '../../../api/servarr/radarr';
import { getSettings, RadarrSettings } from '../../settings';
import BaseScanner, { RunnableScanner, StatusBase } from '../baseScanner';
@@ -52,7 +52,7 @@ class RadarrScanner
this.radarrApi = new RadarrAPI({
apiKey: server.apiKey,
url: RadarrAPI.buildRadarrUrl(server, '/api/v3'),
url: RadarrAPI.buildUrl(server, '/api/v3'),
});
this.items = await this.radarrApi.getMovies();

View File

@@ -1,6 +1,6 @@
import { uniqWith } from 'lodash';
import { getRepository } from 'typeorm';
import SonarrAPI, { SonarrSeries } from '../../../api/sonarr';
import SonarrAPI, { SonarrSeries } from '../../../api/servarr/sonarr';
import Media from '../../../entity/Media';
import { getSettings, SonarrSettings } from '../../settings';
import BaseScanner, {
@@ -58,7 +58,7 @@ class SonarrScanner
this.sonarrApi = new SonarrAPI({
apiKey: server.apiKey,
url: SonarrAPI.buildSonarrUrl(server, '/api/v3'),
url: SonarrAPI.buildUrl(server, '/api/v3'),
});
this.items = await this.sonarrApi.getSeries();

View File

@@ -30,7 +30,7 @@ export interface PlexSettings {
libraries: Library[];
}
interface DVRSettings {
export interface DVRSettings {
id: number;
name: string;
hostname: string;
@@ -41,6 +41,7 @@ interface DVRSettings {
activeProfileId: number;
activeProfileName: string;
activeDirectory: string;
tags: number[];
is4k: boolean;
isDefault: boolean;
externalUrl?: string;
@@ -58,6 +59,7 @@ export interface SonarrSettings extends DVRSettings {
activeAnimeDirectory?: string;
activeAnimeLanguageProfileId?: number;
activeLanguageProfileId?: number;
animeTags?: number[];
enableSeasonFolders: boolean;
}

View File

@@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateTagsFieldonMediaRequest1617624225464
implements MigrationInterface {
name = 'CreateTagsFieldonMediaRequest1617624225464';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TABLE "temporary_media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, "tags" text, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "temporary_media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId" FROM "media_request"`
);
await queryRunner.query(`DROP TABLE "media_request"`);
await queryRunner.query(
`ALTER TABLE "temporary_media_request" RENAME TO "media_request"`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "media_request" RENAME TO "temporary_media_request"`
);
await queryRunner.query(
`CREATE TABLE "media_request" ("id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "status" integer NOT NULL, "createdAt" datetime NOT NULL DEFAULT (datetime('now')), "updatedAt" datetime NOT NULL DEFAULT (datetime('now')), "type" varchar NOT NULL, "mediaId" integer, "requestedById" integer, "modifiedById" integer, "is4k" boolean NOT NULL DEFAULT (0), "serverId" integer, "profileId" integer, "rootFolder" varchar, "languageProfileId" integer, CONSTRAINT "FK_f4fc4efa14c3ba2b29c4525fa15" FOREIGN KEY ("modifiedById") REFERENCES "user" ("id") ON DELETE SET NULL ON UPDATE NO ACTION, CONSTRAINT "FK_6997bee94720f1ecb7f31137095" FOREIGN KEY ("requestedById") REFERENCES "user" ("id") ON DELETE CASCADE ON UPDATE NO ACTION, CONSTRAINT "FK_a1aa713f41c99e9d10c48da75a0" FOREIGN KEY ("mediaId") REFERENCES "media" ("id") ON DELETE CASCADE ON UPDATE NO ACTION)`
);
await queryRunner.query(
`INSERT INTO "media_request"("id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId") SELECT "id", "status", "createdAt", "updatedAt", "type", "mediaId", "requestedById", "modifiedById", "is4k", "serverId", "profileId", "rootFolder", "languageProfileId" FROM "temporary_media_request"`
);
await queryRunner.query(`DROP TABLE "temporary_media_request"`);
}
}

View File

@@ -278,6 +278,7 @@ requestRoutes.post(
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
tags: req.body.tags,
});
await requestRepository.save(request);
@@ -356,6 +357,7 @@ requestRoutes.post(
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
languageProfileId: req.body.languageProfileId,
tags: req.body.tags,
seasons: finalSeasons.map(
(sn) =>
new SeasonRequest({
@@ -497,6 +499,7 @@ requestRoutes.put<{ requestId: string }>(
request.serverId = req.body.serverId;
request.profileId = req.body.profileId;
request.rootFolder = req.body.rootFolder;
request.tags = req.body.tags;
request.requestedBy = requestUser as User;
requestRepository.save(request);
@@ -505,6 +508,8 @@ requestRoutes.put<{ requestId: string }>(
request.serverId = req.body.serverId;
request.profileId = req.body.profileId;
request.rootFolder = req.body.rootFolder;
request.languageProfileId = req.body.languageProfileId;
request.tags = req.body.tags;
request.requestedBy = requestUser as User;
const requestedSeasons = req.body.seasons as number[] | undefined;

View File

@@ -1,12 +1,12 @@
import { Router } from 'express';
import RadarrAPI from '../api/radarr';
import SonarrAPI from '../api/sonarr';
import RadarrAPI from '../api/servarr/radarr';
import SonarrAPI from '../api/servarr/sonarr';
import TheMovieDb from '../api/themoviedb';
import {
ServiceCommonServer,
ServiceCommonServerWithDetails,
} from '../interfaces/api/serviceInterfaces';
import { getSettings } from '../lib/settings';
import TheMovieDb from '../api/themoviedb';
import logger from '../logger';
const serviceRoutes = Router();
@@ -22,6 +22,7 @@ serviceRoutes.get('/radarr', async (req, res) => {
isDefault: radarr.isDefault,
activeDirectory: radarr.activeDirectory,
activeProfileId: radarr.activeProfileId,
activeTags: radarr.tags ?? [],
})
);
@@ -46,11 +47,12 @@ serviceRoutes.get<{ radarrId: string }>(
const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey,
url: RadarrAPI.buildRadarrUrl(radarrSettings, '/api/v3'),
url: RadarrAPI.buildUrl(radarrSettings, '/api/v3'),
});
const profiles = await radarr.getProfiles();
const rootFolders = await radarr.getRootFolders();
const tags = await radarr.getTags();
return res.status(200).json({
server: {
@@ -60,6 +62,7 @@ serviceRoutes.get<{ radarrId: string }>(
isDefault: radarrSettings.isDefault,
activeDirectory: radarrSettings.activeDirectory,
activeProfileId: radarrSettings.activeProfileId,
activeTags: radarrSettings.tags,
},
profiles: profiles.map((profile) => ({
id: profile.id,
@@ -71,6 +74,7 @@ serviceRoutes.get<{ radarrId: string }>(
path: folder.path,
totalSpace: folder.totalSpace,
})),
tags,
} as ServiceCommonServerWithDetails);
}
);
@@ -90,6 +94,7 @@ serviceRoutes.get('/sonarr', async (req, res) => {
activeAnimeDirectory: sonarr.activeAnimeDirectory,
activeLanguageProfileId: sonarr.activeLanguageProfileId,
activeAnimeLanguageProfileId: sonarr.activeAnimeLanguageProfileId,
activeTags: [],
})
);
@@ -114,13 +119,14 @@ serviceRoutes.get<{ sonarrId: string }>(
const sonarr = new SonarrAPI({
apiKey: sonarrSettings.apiKey,
url: SonarrAPI.buildSonarrUrl(sonarrSettings, '/api/v3'),
url: SonarrAPI.buildUrl(sonarrSettings, '/api/v3'),
});
try {
const profiles = await sonarr.getProfiles();
const rootFolders = await sonarr.getRootFolders();
const languageProfiles = await sonarr.getLanguageProfiles();
const tags = await sonarr.getTags();
return res.status(200).json({
server: {
@@ -135,6 +141,8 @@ serviceRoutes.get<{ sonarrId: string }>(
activeLanguageProfileId: sonarrSettings.activeLanguageProfileId,
activeAnimeLanguageProfileId:
sonarrSettings.activeAnimeLanguageProfileId,
activeTags: sonarrSettings.tags,
activeAnimeTags: sonarrSettings.animeTags,
},
profiles: profiles.map((profile) => ({
id: profile.id,
@@ -147,6 +155,7 @@ serviceRoutes.get<{ sonarrId: string }>(
totalSpace: folder.totalSpace,
})),
languageProfiles: languageProfiles,
tags,
} as ServiceCommonServerWithDetails);
} catch (e) {
next({ status: 500, message: e.message });

View File

@@ -1,5 +1,5 @@
import { Router } from 'express';
import RadarrAPI from '../../api/radarr';
import RadarrAPI from '../../api/servarr/radarr';
import { getSettings, RadarrSettings } from '../../lib/settings';
import logger from '../../logger';
@@ -35,15 +35,20 @@ radarrRoutes.post('/', (req, res) => {
return res.status(201).json(newRadarr);
});
radarrRoutes.post('/test', async (req, res, next) => {
radarrRoutes.post<
undefined,
Record<string, unknown>,
RadarrSettings & { tagLabel?: string }
>('/test', async (req, res, next) => {
try {
const radarr = new RadarrAPI({
apiKey: req.body.apiKey,
url: RadarrAPI.buildRadarrUrl(req.body, '/api/v3'),
url: RadarrAPI.buildUrl(req.body, '/api/v3'),
});
const profiles = await radarr.getProfiles();
const folders = await radarr.getRootFolders();
const tags = await radarr.getTags();
return res.status(200).json({
profiles,
@@ -51,6 +56,7 @@ radarrRoutes.post('/test', async (req, res, next) => {
id: folder.id,
path: folder.path,
})),
tags,
});
} catch (e) {
logger.error('Failed to test Radarr', {
@@ -62,40 +68,41 @@ radarrRoutes.post('/test', async (req, res, next) => {
}
});
radarrRoutes.put<{ id: string }>('/:id', (req, res) => {
const settings = getSettings();
radarrRoutes.put<{ id: string }, RadarrSettings, RadarrSettings>(
'/:id',
(req, res, next) => {
const settings = getSettings();
const radarrIndex = settings.radarr.findIndex(
(r) => r.id === Number(req.params.id)
);
const radarrIndex = settings.radarr.findIndex(
(r) => r.id === Number(req.params.id)
);
if (radarrIndex === -1) {
return res
.status(404)
.json({ status: '404', message: 'Settings instance not found' });
if (radarrIndex === -1) {
return next({ status: '404', message: 'Settings instance not found' });
}
// If we are setting this as the default, clear any previous defaults for the same type first
// ex: if is4k is true, it will only remove defaults for other servers that have is4k set to true
// and are the default
if (req.body.isDefault) {
settings.radarr
.filter((radarrInstance) => radarrInstance.is4k === req.body.is4k)
.forEach((radarrInstance) => {
radarrInstance.isDefault = false;
});
}
settings.radarr[radarrIndex] = {
...req.body,
id: Number(req.params.id),
} as RadarrSettings;
settings.save();
return res.status(200).json(settings.radarr[radarrIndex]);
}
);
// If we are setting this as the default, clear any previous defaults for the same type first
// ex: if is4k is true, it will only remove defaults for other servers that have is4k set to true
// and are the default
if (req.body.isDefault) {
settings.radarr
.filter((radarrInstance) => radarrInstance.is4k === req.body.is4k)
.forEach((radarrInstance) => {
radarrInstance.isDefault = false;
});
}
settings.radarr[radarrIndex] = {
...req.body,
id: Number(req.params.id),
} as RadarrSettings;
settings.save();
return res.status(200).json(settings.radarr[radarrIndex]);
});
radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res) => {
radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res, next) => {
const settings = getSettings();
const radarrSettings = settings.radarr.find(
@@ -103,14 +110,12 @@ radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res) => {
);
if (!radarrSettings) {
return res
.status(404)
.json({ status: '404', message: 'Settings instance not found' });
return next({ status: '404', message: 'Settings instance not found' });
}
const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey,
url: RadarrAPI.buildRadarrUrl(radarrSettings, '/api/v3'),
url: RadarrAPI.buildUrl(radarrSettings, '/api/v3'),
});
const profiles = await radarr.getProfiles();
@@ -123,7 +128,7 @@ radarrRoutes.get<{ id: string }>('/:id/profiles', async (req, res) => {
);
});
radarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
radarrRoutes.delete<{ id: string }>('/:id', (req, res, next) => {
const settings = getSettings();
const radarrIndex = settings.radarr.findIndex(
@@ -131,9 +136,7 @@ radarrRoutes.delete<{ id: string }>('/:id', (req, res) => {
);
if (radarrIndex === -1) {
return res
.status(404)
.json({ status: '404', message: 'Settings instance not found' });
return next({ status: '404', message: 'Settings instance not found' });
}
const removed = settings.radarr.splice(radarrIndex, 1);

View File

@@ -1,5 +1,5 @@
import { Router } from 'express';
import SonarrAPI from '../../api/sonarr';
import SonarrAPI from '../../api/servarr/sonarr';
import { getSettings, SonarrSettings } from '../../lib/settings';
import logger from '../../logger';
@@ -39,12 +39,13 @@ sonarrRoutes.post('/test', async (req, res, next) => {
try {
const sonarr = new SonarrAPI({
apiKey: req.body.apiKey,
url: SonarrAPI.buildSonarrUrl(req.body, '/api/v3'),
url: SonarrAPI.buildUrl(req.body, '/api/v3'),
});
const profiles = await sonarr.getProfiles();
const folders = await sonarr.getRootFolders();
const languageProfiles = await sonarr.getLanguageProfiles();
const tags = await sonarr.getTags();
return res.status(200).json({
profiles,
@@ -53,6 +54,7 @@ sonarrRoutes.post('/test', async (req, res, next) => {
path: folder.path,
})),
languageProfiles,
tags,
});
} catch (e) {
logger.error('Failed to test Sonarr', {