feat(requests): Request Overrides & Request Editing (#653)

This commit is contained in:
sct
2021-01-17 22:52:50 +09:00
committed by GitHub
parent d9919abb89
commit bdb33722e6
24 changed files with 1256 additions and 63 deletions

View File

@@ -29,7 +29,7 @@ interface RadarrMovie {
hasFile: boolean;
}
interface RadarrRootFolder {
export interface RadarrRootFolder {
id: number;
path: string;
freeSpace: number;
@@ -40,7 +40,7 @@ interface RadarrRootFolder {
}[];
}
interface RadarrProfile {
export interface RadarrProfile {
id: number;
name: string;
}

View File

@@ -202,7 +202,7 @@ export class MediaRequest {
}
if (
this.media.mediaType === MediaType.MOVIE &&
media.mediaType === MediaType.MOVIE &&
this.status === MediaRequestStatus.DECLINED
) {
if (this.is4k) {
@@ -284,10 +284,24 @@ export class MediaRequest {
return;
}
const radarrSettings = settings.radarr.find(
let radarrSettings = settings.radarr.find(
(radarr) => radarr.isDefault && radarr.is4k === this.is4k
);
if (
this.serverId !== null &&
this.serverId >= 0 &&
radarrSettings?.id !== this.serverId
) {
radarrSettings = settings.radarr.find(
(radarr) => radarr.id === this.serverId
);
logger.info(
`Request has an override server: ${radarrSettings?.name}`,
{ label: 'Media Request' }
);
}
if (!radarrSettings) {
logger.info(
`There is no default ${
@@ -298,6 +312,30 @@ export class MediaRequest {
return;
}
let rootFolder = radarrSettings.activeDirectory;
let qualityProfile = radarrSettings.activeProfileId;
if (
this.rootFolder &&
this.rootFolder !== '' &&
this.rootFolder !== radarrSettings.activeDirectory
) {
rootFolder = this.rootFolder;
logger.info(`Request has an override root folder: ${rootFolder}`, {
label: 'Media Request',
});
}
if (
this.profileId &&
this.profileId !== radarrSettings.activeProfileId
) {
qualityProfile = this.profileId;
logger.info(`Request has an override profile id: ${qualityProfile}`, {
label: 'Media Request',
});
}
const tmdb = new TheMovieDb();
const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey,
@@ -310,9 +348,9 @@ export class MediaRequest {
// Run this asynchronously so we don't wait for it on the UI side
radarr
.addMovie({
profileId: radarrSettings.activeProfileId,
qualityProfileId: radarrSettings.activeProfileId,
rootFolderPath: radarrSettings.activeDirectory,
profileId: qualityProfile,
qualityProfileId: qualityProfile,
rootFolderPath: rootFolder,
minimumAvailability: radarrSettings.minimumAvailability,
title: movie.title,
tmdbId: movie.id,
@@ -376,10 +414,24 @@ export class MediaRequest {
return;
}
const sonarrSettings = settings.sonarr.find(
let sonarrSettings = settings.sonarr.find(
(sonarr) => sonarr.isDefault && sonarr.is4k === this.is4k
);
if (
this.serverId !== null &&
this.serverId >= 0 &&
sonarrSettings?.id !== this.serverId
) {
sonarrSettings = settings.sonarr.find(
(sonarr) => sonarr.id === this.serverId
);
logger.info(
`Request has an override server: ${sonarrSettings?.name}`,
{ label: 'Media Request' }
);
}
if (!sonarrSettings) {
logger.info(
`There is no default ${
@@ -423,17 +475,38 @@ export class MediaRequest {
seriesType = 'anime';
}
let rootFolder =
seriesType === 'anime' && sonarrSettings.activeAnimeDirectory
? sonarrSettings.activeAnimeDirectory
: sonarrSettings.activeDirectory;
let qualityProfile =
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
? sonarrSettings.activeAnimeProfileId
: sonarrSettings.activeProfileId;
if (
this.rootFolder &&
this.rootFolder !== '' &&
this.rootFolder !== rootFolder
) {
rootFolder = this.rootFolder;
logger.info(`Request has an override root folder: ${rootFolder}`, {
label: 'Media Request',
});
}
if (this.profileId && this.profileId !== qualityProfile) {
qualityProfile = this.profileId;
logger.info(`Request has an override profile id: ${qualityProfile}`, {
label: 'Media Request',
});
}
// Run this asynchronously so we don't wait for it on the UI side
sonarr
.addSeries({
profileId:
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
? sonarrSettings.activeAnimeProfileId
: sonarrSettings.activeProfileId,
rootFolderPath:
seriesType === 'anime' && sonarrSettings.activeAnimeDirectory
? sonarrSettings.activeAnimeDirectory
: sonarrSettings.activeDirectory,
profileId: qualityProfile,
rootFolderPath: rootFolder,
title: series.name,
tvdbid: series.external_ids.tvdb_id,
seasons: this.seasons.map((season) => season.seasonNumber),

View File

@@ -0,0 +1,18 @@
import { RadarrProfile, RadarrRootFolder } from '../../api/radarr';
export interface ServiceCommonServer {
id: number;
name: string;
is4k: boolean;
isDefault: boolean;
activeProfileId: number;
activeDirectory: string;
activeAnimeProfileId?: number;
activeAnimeDirectory?: string;
}
export interface ServiceCommonServerWithDetails {
server: ServiceCommonServer;
profiles: RadarrProfile[];
rootFolders: Partial<RadarrRootFolder>[];
}

View File

@@ -12,6 +12,7 @@ export enum Permission {
REQUEST_4K = 1024,
REQUEST_4K_MOVIE = 2048,
REQUEST_4K_TV = 4096,
REQUEST_ADVANCED = 8192,
}
/**

View File

@@ -14,6 +14,7 @@ import mediaRoutes from './media';
import personRoutes from './person';
import collectionRoutes from './collection';
import { getAppVersion, getCommitTag } from '../utils/appVersion';
import serviceRoutes from './service';
const router = Router();
@@ -45,6 +46,7 @@ router.use('/tv', isAuthenticated(), tvRoutes);
router.use('/media', isAuthenticated(), mediaRoutes);
router.use('/person', isAuthenticated(), personRoutes);
router.use('/collection', isAuthenticated(), collectionRoutes);
router.use('/service', isAuthenticated(), serviceRoutes);
router.use('/auth', authRoutes);
router.get('/', (_req, res) => {

View File

@@ -199,6 +199,9 @@ requestRoutes.post(
? req.user
: undefined,
is4k: req.body.is4k,
serverId: req.body.serverId,
profileId: req.body.profileId,
rootFolder: req.body.rootFolder,
seasons: finalSeasons.map(
(sn) =>
new SeasonRequest({
@@ -238,6 +241,102 @@ requestRoutes.get('/:requestId', async (req, res, next) => {
}
});
requestRoutes.put<{ requestId: string }>(
'/:requestId',
isAuthenticated(Permission.MANAGE_REQUESTS),
async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);
try {
const request = await requestRepository.findOne(
Number(req.params.requestId)
);
if (!request) {
return next({ status: 404, message: 'Request not found' });
}
if (req.body.mediaType === 'movie') {
request.serverId = req.body.serverId;
request.profileId = req.body.profileId;
request.rootFolder = req.body.rootFolder;
requestRepository.save(request);
} else if (req.body.mediaType === 'tv') {
const mediaRepository = getRepository(Media);
request.serverId = req.body.serverId;
request.profileId = req.body.profileId;
request.rootFolder = req.body.rootFolder;
const requestedSeasons = req.body.seasons as number[] | undefined;
if (!requestedSeasons || requestedSeasons.length === 0) {
throw new Error(
'Missing seasons. If you want to cancel a tv request, use the DELETE method.'
);
}
// Get existing media so we can work with all the requests
const media = await mediaRepository.findOneOrFail({
where: { tmdbId: request.media.tmdbId, mediaType: MediaType.TV },
relations: ['requests'],
});
// Get all requested seasons that are not part of this request we are editing
const existingSeasons = media.requests
.filter((r) => r.is4k === request.is4k && r.id !== request.id)
.reduce((seasons, r) => {
const combinedSeasons = r.seasons.map(
(season) => season.seasonNumber
);
return [...seasons, ...combinedSeasons];
}, [] as number[]);
const filteredSeasons = requestedSeasons.filter(
(rs) => !existingSeasons.includes(rs)
);
if (filteredSeasons.length === 0) {
return next({
status: 202,
message: 'No seasons available to request',
});
}
const newSeasons = requestedSeasons.filter(
(sn) => !request.seasons.map((s) => s.seasonNumber).includes(sn)
);
request.seasons = request.seasons.filter((rs) =>
filteredSeasons.includes(rs.seasonNumber)
);
if (newSeasons.length > 0) {
logger.debug('Adding new seasons to request', {
label: 'Media Request',
newSeasons,
});
request.seasons.push(
...newSeasons.map(
(ns) =>
new SeasonRequest({
seasonNumber: ns,
status: MediaRequestStatus.PENDING,
})
)
);
}
await requestRepository.save(request);
}
return res.status(200).json(request);
} catch (e) {
next({ status: 500, message: e.message });
}
}
);
requestRoutes.delete('/:requestId', async (req, res, next) => {
const requestRepository = getRepository(MediaRequest);

148
server/routes/service.ts Normal file
View File

@@ -0,0 +1,148 @@
import { Router } from 'express';
import RadarrAPI from '../api/radarr';
import SonarrAPI from '../api/sonarr';
import {
ServiceCommonServer,
ServiceCommonServerWithDetails,
} from '../interfaces/api/serviceInterfaces';
import { getSettings } from '../lib/settings';
const serviceRoutes = Router();
serviceRoutes.get('/radarr', async (req, res) => {
const settings = getSettings();
const filteredRadarrServers: ServiceCommonServer[] = settings.radarr.map(
(radarr) => ({
id: radarr.id,
name: radarr.name,
is4k: radarr.is4k,
isDefault: radarr.isDefault,
activeDirectory: radarr.activeDirectory,
activeProfileId: radarr.activeProfileId,
})
);
return res.status(200).json(filteredRadarrServers);
});
serviceRoutes.get<{ radarrId: string }>(
'/radarr/:radarrId',
async (req, res, next) => {
const settings = getSettings();
const radarrSettings = settings.radarr.find(
(radarr) => radarr.id === Number(req.params.radarrId)
);
if (!radarrSettings) {
return next({
status: 404,
message: 'Radarr server with provided ID does not exist.',
});
}
const radarr = new RadarrAPI({
apiKey: radarrSettings.apiKey,
url: `${radarrSettings.useSsl ? 'https' : 'http'}://${
radarrSettings.hostname
}:${radarrSettings.port}${radarrSettings.baseUrl ?? ''}/api`,
});
const profiles = await radarr.getProfiles();
const rootFolders = await radarr.getRootFolders();
return res.status(200).json({
server: {
id: radarrSettings.id,
name: radarrSettings.name,
is4k: radarrSettings.is4k,
isDefault: radarrSettings.isDefault,
activeDirectory: radarrSettings.activeDirectory,
activeProfileId: radarrSettings.activeProfileId,
},
profiles: profiles.map((profile) => ({
id: profile.id,
name: profile.name,
})),
rootFolders: rootFolders.map((folder) => ({
id: folder.id,
freeSpace: folder.freeSpace,
path: folder.path,
totalSpace: folder.totalSpace,
})),
} as ServiceCommonServerWithDetails);
}
);
serviceRoutes.get('/sonarr', async (req, res) => {
const settings = getSettings();
const filteredSonarrServers: ServiceCommonServer[] = settings.sonarr.map(
(sonarr) => ({
id: sonarr.id,
name: sonarr.name,
is4k: sonarr.is4k,
isDefault: sonarr.isDefault,
activeDirectory: sonarr.activeDirectory,
activeProfileId: sonarr.activeProfileId,
activeAnimeProfileId: sonarr.activeAnimeProfileId,
activeAnimeDirectory: sonarr.activeAnimeDirectory,
})
);
return res.status(200).json(filteredSonarrServers);
});
serviceRoutes.get<{ sonarrId: string }>(
'/sonarr/:sonarrId',
async (req, res, next) => {
const settings = getSettings();
const sonarrSettings = settings.sonarr.find(
(radarr) => radarr.id === Number(req.params.sonarrId)
);
if (!sonarrSettings) {
return next({
status: 404,
message: 'Radarr server with provided ID does not exist.',
});
}
const sonarr = new SonarrAPI({
apiKey: sonarrSettings.apiKey,
url: `${sonarrSettings.useSsl ? 'https' : 'http'}://${
sonarrSettings.hostname
}:${sonarrSettings.port}${sonarrSettings.baseUrl ?? ''}/api`,
});
const profiles = await sonarr.getProfiles();
const rootFolders = await sonarr.getRootFolders();
return res.status(200).json({
server: {
id: sonarrSettings.id,
name: sonarrSettings.name,
is4k: sonarrSettings.is4k,
isDefault: sonarrSettings.isDefault,
activeDirectory: sonarrSettings.activeDirectory,
activeProfileId: sonarrSettings.activeProfileId,
activeAnimeProfileId: sonarrSettings.activeAnimeProfileId,
activeAnimeDirectory: sonarrSettings.activeAnimeDirectory,
},
profiles: profiles.map((profile) => ({
id: profile.id,
name: profile.name,
})),
rootFolders: rootFolders.map((folder) => ({
id: folder.id,
freeSpace: folder.freeSpace,
path: folder.path,
totalSpace: folder.totalSpace,
})),
} as ServiceCommonServerWithDetails);
}
);
export default serviceRoutes;

View File

@@ -103,6 +103,10 @@ export class MediaSubscriber implements EntitySubscriberInterface {
}
public beforeUpdate(event: UpdateEvent<Media>): void {
if (!event.entity) {
return;
}
if (
event.entity.mediaType === MediaType.MOVIE &&
event.entity.status === MediaStatus.AVAILABLE