mirror of
https://github.com/sct/overseerr.git
synced 2025-09-17 17:24:35 +02:00
refactor(MediaRequest): consolidate lifecycle hooks into subscriber & restrict cascade to prevent recursion (#4124)
* fix(mediarequest entity): narrow cascade to insert & remove to prevent hook recursion Restrict cascade options on the MediaRequest→Media relation to only `insert` and `remove`to avoid nested subscriber/AfterUpdate recursion when saving entities. * fix(mediarequest): move methods modifying MediaRequest to its Subscriber * fix(mediasubscriber): use event.manager for parent media updates on remove Replace `getRepository(Media)` calls with `event.manager` in the `afterRemove` hook so that parent-media status resets run within the same transaction/QueryRunner (important for postgresql. Doesnt affect sqlite). * refactor(mediasubscriber): make afterInsert and afterUpdate async and await internal operations This should prevent unhandled promise rejections and ensure sequential execution of lifecycle actions --------- Co-authored-by: Gauthier <mail@gauthierth.fr>
This commit is contained in:
@@ -90,7 +90,9 @@ class Media {
|
|||||||
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
@Column({ type: 'int', default: MediaStatus.UNKNOWN })
|
||||||
public status4k: MediaStatus;
|
public status4k: MediaStatus;
|
||||||
|
|
||||||
@OneToMany(() => MediaRequest, (request) => request.media, { cascade: true })
|
@OneToMany(() => MediaRequest, (request) => request.media, {
|
||||||
|
cascade: ['insert', 'update'],
|
||||||
|
})
|
||||||
public requests: MediaRequest[];
|
public requests: MediaRequest[];
|
||||||
|
|
||||||
@OneToMany(() => Season, (season) => season.media, {
|
@OneToMany(() => Season, (season) => season.media, {
|
||||||
|
@@ -1,12 +1,4 @@
|
|||||||
import type { RadarrMovieOptions } from '@server/api/servarr/radarr';
|
|
||||||
import RadarrAPI from '@server/api/servarr/radarr';
|
|
||||||
import type {
|
|
||||||
AddSeriesOptions,
|
|
||||||
SonarrSeries,
|
|
||||||
} from '@server/api/servarr/sonarr';
|
|
||||||
import SonarrAPI from '@server/api/servarr/sonarr';
|
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
|
||||||
import {
|
import {
|
||||||
MediaRequestStatus,
|
MediaRequestStatus,
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
@@ -16,12 +8,10 @@ import { getRepository } from '@server/datasource';
|
|||||||
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces';
|
||||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||||
import { Permission } from '@server/lib/permissions';
|
import { Permission } from '@server/lib/permissions';
|
||||||
import { getSettings } from '@server/lib/settings';
|
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { isEqual, truncate } from 'lodash';
|
import { truncate } from 'lodash';
|
||||||
import {
|
import {
|
||||||
AfterInsert,
|
AfterInsert,
|
||||||
AfterRemove,
|
|
||||||
AfterUpdate,
|
AfterUpdate,
|
||||||
Column,
|
Column,
|
||||||
CreateDateColumn,
|
CreateDateColumn,
|
||||||
@@ -468,12 +458,6 @@ export class MediaRequest {
|
|||||||
Object.assign(this, init);
|
Object.assign(this, init);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterUpdate()
|
|
||||||
@AfterInsert()
|
|
||||||
public async sendMedia(): Promise<void> {
|
|
||||||
await Promise.all([this.sendToRadarr(), this.sendToSonarr()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterInsert()
|
@AfterInsert()
|
||||||
public async notifyNewRequest(): Promise<void> {
|
public async notifyNewRequest(): Promise<void> {
|
||||||
if (this.status === MediaRequestStatus.PENDING) {
|
if (this.status === MediaRequestStatus.PENDING) {
|
||||||
@@ -490,10 +474,14 @@ export class MediaRequest {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendNotification(media, Notification.MEDIA_PENDING);
|
MediaRequest.sendNotification(this, media, Notification.MEDIA_PENDING);
|
||||||
|
|
||||||
if (this.isAutoRequest) {
|
if (this.isAutoRequest) {
|
||||||
this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED);
|
MediaRequest.sendNotification(
|
||||||
|
this,
|
||||||
|
media,
|
||||||
|
Notification.MEDIA_AUTO_REQUESTED
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -531,7 +519,8 @@ export class MediaRequest {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.sendNotification(
|
MediaRequest.sendNotification(
|
||||||
|
this,
|
||||||
media,
|
media,
|
||||||
this.status === MediaRequestStatus.APPROVED
|
this.status === MediaRequestStatus.APPROVED
|
||||||
? autoApproved
|
? autoApproved
|
||||||
@@ -545,7 +534,11 @@ export class MediaRequest {
|
|||||||
autoApproved &&
|
autoApproved &&
|
||||||
this.isAutoRequest
|
this.isAutoRequest
|
||||||
) {
|
) {
|
||||||
this.sendNotification(media, Notification.MEDIA_AUTO_REQUESTED);
|
MediaRequest.sendNotification(
|
||||||
|
this,
|
||||||
|
media,
|
||||||
|
Notification.MEDIA_AUTO_REQUESTED
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,661 +550,56 @@ export class MediaRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AfterUpdate()
|
static async sendNotification(
|
||||||
@AfterInsert()
|
entity: MediaRequest,
|
||||||
public async updateParentStatus(): Promise<void> {
|
media: Media,
|
||||||
const mediaRepository = getRepository(Media);
|
type: Notification
|
||||||
const media = await mediaRepository.findOne({
|
) {
|
||||||
where: { id: this.media.id },
|
|
||||||
relations: { requests: true },
|
|
||||||
});
|
|
||||||
if (!media) {
|
|
||||||
logger.error('Media data not found', {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const seasonRequestRepository = getRepository(SeasonRequest);
|
|
||||||
if (
|
|
||||||
this.status === MediaRequestStatus.APPROVED &&
|
|
||||||
// Do not update the status if the item is already partially available or available
|
|
||||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
|
|
||||||
media[this.is4k ? 'status4k' : 'status'] !==
|
|
||||||
MediaStatus.PARTIALLY_AVAILABLE
|
|
||||||
) {
|
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
|
|
||||||
mediaRepository.save(media);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
media.mediaType === MediaType.MOVIE &&
|
|
||||||
this.status === MediaRequestStatus.DECLINED &&
|
|
||||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
|
|
||||||
) {
|
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
|
||||||
mediaRepository.save(media);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If the media type is TV, and we are declining a request,
|
|
||||||
* we must check if its the only pending request and that
|
|
||||||
* there the current media status is just pending (meaning no
|
|
||||||
* other requests have yet to be approved)
|
|
||||||
*/
|
|
||||||
if (
|
|
||||||
media.mediaType === MediaType.TV &&
|
|
||||||
this.status === MediaRequestStatus.DECLINED &&
|
|
||||||
media.requests.filter(
|
|
||||||
(request) => request.status === MediaRequestStatus.PENDING
|
|
||||||
).length === 0 &&
|
|
||||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING &&
|
|
||||||
media[this.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
|
|
||||||
) {
|
|
||||||
media[this.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
|
||||||
mediaRepository.save(media);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Approve child seasons if parent is approved
|
|
||||||
if (
|
|
||||||
media.mediaType === MediaType.TV &&
|
|
||||||
this.status === MediaRequestStatus.APPROVED
|
|
||||||
) {
|
|
||||||
this.seasons.forEach((season) => {
|
|
||||||
season.status = MediaRequestStatus.APPROVED;
|
|
||||||
seasonRequestRepository.save(season);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterRemove()
|
|
||||||
public async handleRemoveParentUpdate(): Promise<void> {
|
|
||||||
const mediaRepository = getRepository(Media);
|
|
||||||
const fullMedia = await mediaRepository.findOneOrFail({
|
|
||||||
where: { id: this.media.id },
|
|
||||||
relations: { requests: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
!fullMedia.requests.some((request) => !request.is4k) &&
|
|
||||||
fullMedia.status !== MediaStatus.AVAILABLE
|
|
||||||
) {
|
|
||||||
fullMedia.status = MediaStatus.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!fullMedia.requests.some((request) => request.is4k) &&
|
|
||||||
fullMedia.status4k !== MediaStatus.AVAILABLE
|
|
||||||
) {
|
|
||||||
fullMedia.status4k = MediaStatus.UNKNOWN;
|
|
||||||
}
|
|
||||||
|
|
||||||
mediaRepository.save(fullMedia);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sendToRadarr(): Promise<void> {
|
|
||||||
if (
|
|
||||||
this.status === MediaRequestStatus.APPROVED &&
|
|
||||||
this.type === MediaType.MOVIE
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const mediaRepository = getRepository(Media);
|
|
||||||
const settings = getSettings();
|
|
||||||
if (settings.radarr.length === 0 && !settings.radarr[0]) {
|
|
||||||
logger.info(
|
|
||||||
'No Radarr server configured, skipping request processing',
|
|
||||||
{
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!radarrSettings) {
|
|
||||||
logger.warn(
|
|
||||||
`There is no default ${
|
|
||||||
this.is4k ? '4K ' : ''
|
|
||||||
}Radarr server configured. Did you set any of your ${
|
|
||||||
this.is4k ? '4K ' : ''
|
|
||||||
}Radarr servers as default?`,
|
|
||||||
{
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let rootFolder = radarrSettings.activeDirectory;
|
|
||||||
let qualityProfile = radarrSettings.activeProfileId;
|
|
||||||
let tags = radarrSettings.tags ? [...radarrSettings.tags] : [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.rootFolder &&
|
|
||||||
this.rootFolder !== '' &&
|
|
||||||
this.rootFolder !== radarrSettings.activeDirectory
|
|
||||||
) {
|
|
||||||
rootFolder = this.rootFolder;
|
|
||||||
logger.info(`Request has an override root folder: ${rootFolder}`, {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.profileId &&
|
|
||||||
this.profileId !== radarrSettings.activeProfileId
|
|
||||||
) {
|
|
||||||
qualityProfile = this.profileId;
|
|
||||||
logger.info(
|
|
||||||
`Request has an override quality profile ID: ${qualityProfile}`,
|
|
||||||
{
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.tags && !isEqual(this.tags, radarrSettings.tags)) {
|
|
||||||
tags = this.tags;
|
|
||||||
logger.info(`Request has override tags`, {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
tagIds: tags,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const tmdb = new TheMovieDb();
|
|
||||||
const radarr = new RadarrAPI({
|
|
||||||
apiKey: radarrSettings.apiKey,
|
|
||||||
url: RadarrAPI.buildUrl(radarrSettings, '/api/v3'),
|
|
||||||
});
|
|
||||||
const movie = await tmdb.getMovie({ movieId: this.media.tmdbId });
|
|
||||||
|
|
||||||
const media = await mediaRepository.findOne({
|
|
||||||
where: { id: this.media.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!media) {
|
|
||||||
logger.error('Media data not found', {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (radarrSettings.tagRequests) {
|
|
||||||
let userTag = (await radarr.getTags()).find((v) =>
|
|
||||||
v.label.startsWith(this.requestedBy.id + ' - ')
|
|
||||||
);
|
|
||||||
if (!userTag) {
|
|
||||||
logger.info(`Requester has no active tag. Creating new`, {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
userId: this.requestedBy.id,
|
|
||||||
newTag:
|
|
||||||
this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
|
||||||
});
|
|
||||||
userTag = await radarr.createTag({
|
|
||||||
label: this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (userTag.id) {
|
|
||||||
if (!tags?.find((v) => v === userTag?.id)) {
|
|
||||||
tags?.push(userTag.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn(`Requester has no tag and failed to add one`, {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
userId: this.requestedBy.id,
|
|
||||||
radarrServer: radarrSettings.hostname + ':' + radarrSettings.port,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
|
||||||
) {
|
|
||||||
logger.warn('Media already exists, marking request as APPROVED', {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
|
||||||
this.status = MediaRequestStatus.APPROVED;
|
|
||||||
await requestRepository.save(this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const radarrMovieOptions: RadarrMovieOptions = {
|
|
||||||
profileId: qualityProfile,
|
|
||||||
qualityProfileId: qualityProfile,
|
|
||||||
rootFolderPath: rootFolder,
|
|
||||||
minimumAvailability: radarrSettings.minimumAvailability,
|
|
||||||
title: movie.title,
|
|
||||||
tmdbId: movie.id,
|
|
||||||
year: Number(movie.release_date.slice(0, 4)),
|
|
||||||
monitored: true,
|
|
||||||
tags,
|
|
||||||
searchNow: !radarrSettings.preventSearch,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run this asynchronously so we don't wait for it on the UI side
|
|
||||||
radarr
|
|
||||||
.addMovie(radarrMovieOptions)
|
|
||||||
.then(async (radarrMovie) => {
|
|
||||||
// We grab media again here to make sure we have the latest version of it
|
|
||||||
const media = await mediaRepository.findOne({
|
|
||||||
where: { id: this.media.id },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!media) {
|
|
||||||
throw new Error('Media data not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
|
||||||
radarrMovie.id;
|
|
||||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
|
||||||
radarrMovie.titleSlug;
|
|
||||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = radarrSettings?.id;
|
|
||||||
await mediaRepository.save(media);
|
|
||||||
})
|
|
||||||
.catch(async () => {
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
|
||||||
|
|
||||||
this.status = MediaRequestStatus.FAILED;
|
|
||||||
requestRepository.save(this);
|
|
||||||
|
|
||||||
logger.warn(
|
|
||||||
'Something went wrong sending movie request to Radarr, marking status as FAILED',
|
|
||||||
{
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
radarrMovieOptions,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.sendNotification(media, Notification.MEDIA_FAILED);
|
|
||||||
});
|
|
||||||
logger.info('Sent request to Radarr', {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Something went wrong sending request to Radarr', {
|
|
||||||
label: 'Media Request',
|
|
||||||
errorMessage: e.message,
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
});
|
|
||||||
throw new Error(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async sendToSonarr(): Promise<void> {
|
|
||||||
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.warn(
|
|
||||||
'No Sonarr server configured, skipping request processing',
|
|
||||||
{
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sonarrSettings) {
|
|
||||||
logger.warn(
|
|
||||||
`There is no default ${
|
|
||||||
this.is4k ? '4K ' : ''
|
|
||||||
}Sonarr server configured. Did you set any of your ${
|
|
||||||
this.is4k ? '4K ' : ''
|
|
||||||
}Sonarr servers as default?`,
|
|
||||||
{
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const media = await mediaRepository.findOne({
|
|
||||||
where: { id: this.media.id },
|
|
||||||
relations: { requests: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!media) {
|
|
||||||
throw new Error('Media data not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
media[this.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
|
||||||
) {
|
|
||||||
logger.warn('Media already exists, marking request as APPROVED', {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
});
|
|
||||||
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
|
||||||
this.status = MediaRequestStatus.APPROVED;
|
|
||||||
await requestRepository.save(this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const tmdb = new TheMovieDb();
|
|
||||||
const sonarr = new SonarrAPI({
|
|
||||||
apiKey: sonarrSettings.apiKey,
|
|
||||||
url: SonarrAPI.buildUrl(sonarrSettings, '/api/v3'),
|
|
||||||
});
|
|
||||||
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
|
|
||||||
const tvdbId = series.external_ids.tvdb_id ?? media.tvdbId;
|
|
||||||
|
|
||||||
if (!tvdbId) {
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
|
||||||
await mediaRepository.remove(media);
|
|
||||||
await requestRepository.remove(this);
|
|
||||||
throw new Error('TVDB ID not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
let seriesType: SonarrSeries['seriesType'] = 'standard';
|
|
||||||
|
|
||||||
// Change series type to anime if the anime keyword is present on tmdb
|
|
||||||
if (
|
|
||||||
series.keywords.results.some(
|
|
||||||
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
seriesType = sonarrSettings.animeSeriesType ?? 'anime';
|
|
||||||
}
|
|
||||||
|
|
||||||
let rootFolder =
|
|
||||||
seriesType === 'anime' && sonarrSettings.activeAnimeDirectory
|
|
||||||
? sonarrSettings.activeAnimeDirectory
|
|
||||||
: sonarrSettings.activeDirectory;
|
|
||||||
let qualityProfile =
|
|
||||||
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
|
|
||||||
? sonarrSettings.activeAnimeProfileId
|
|
||||||
: sonarrSettings.activeProfileId;
|
|
||||||
let languageProfile =
|
|
||||||
seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId
|
|
||||||
? sonarrSettings.activeAnimeLanguageProfileId
|
|
||||||
: sonarrSettings.activeLanguageProfileId;
|
|
||||||
let tags =
|
|
||||||
seriesType === 'anime'
|
|
||||||
? sonarrSettings.animeTags
|
|
||||||
? [...sonarrSettings.animeTags]
|
|
||||||
: []
|
|
||||||
: sonarrSettings.tags
|
|
||||||
? [...sonarrSettings.tags]
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.rootFolder &&
|
|
||||||
this.rootFolder !== '' &&
|
|
||||||
this.rootFolder !== rootFolder
|
|
||||||
) {
|
|
||||||
rootFolder = this.rootFolder;
|
|
||||||
logger.info(`Request has an override root folder: ${rootFolder}`, {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.profileId && this.profileId !== qualityProfile) {
|
|
||||||
qualityProfile = this.profileId;
|
|
||||||
logger.info(
|
|
||||||
`Request has an override quality profile ID: ${qualityProfile}`,
|
|
||||||
{
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.languageProfileId &&
|
|
||||||
this.languageProfileId !== languageProfile
|
|
||||||
) {
|
|
||||||
languageProfile = this.languageProfileId;
|
|
||||||
logger.info(
|
|
||||||
`Request has an override language profile ID: ${languageProfile}`,
|
|
||||||
{
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.tags && !isEqual(this.tags, tags)) {
|
|
||||||
tags = this.tags;
|
|
||||||
logger.info(`Request has override tags`, {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
tagIds: tags,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sonarrSettings.tagRequests) {
|
|
||||||
let userTag = (await sonarr.getTags()).find((v) =>
|
|
||||||
v.label.startsWith(this.requestedBy.id + ' - ')
|
|
||||||
);
|
|
||||||
if (!userTag) {
|
|
||||||
logger.info(`Requester has no active tag. Creating new`, {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
userId: this.requestedBy.id,
|
|
||||||
newTag:
|
|
||||||
this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
|
||||||
});
|
|
||||||
userTag = await sonarr.createTag({
|
|
||||||
label: this.requestedBy.id + ' - ' + this.requestedBy.displayName,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (userTag.id) {
|
|
||||||
if (!tags?.find((v) => v === userTag?.id)) {
|
|
||||||
tags?.push(userTag.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.warn(`Requester has no tag and failed to add one`, {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
userId: this.requestedBy.id,
|
|
||||||
sonarrServer: sonarrSettings.hostname + ':' + sonarrSettings.port,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sonarrSeriesOptions: AddSeriesOptions = {
|
|
||||||
profileId: qualityProfile,
|
|
||||||
languageProfileId: languageProfile,
|
|
||||||
rootFolderPath: rootFolder,
|
|
||||||
title: series.name,
|
|
||||||
tvdbid: tvdbId,
|
|
||||||
seasons: this.seasons.map((season) => season.seasonNumber),
|
|
||||||
seasonFolder: sonarrSettings.enableSeasonFolders,
|
|
||||||
seriesType,
|
|
||||||
tags,
|
|
||||||
monitored: true,
|
|
||||||
searchNow: !sonarrSettings.preventSearch,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Run this asynchronously so we don't wait for it on the UI side
|
|
||||||
sonarr
|
|
||||||
.addSeries(sonarrSeriesOptions)
|
|
||||||
.then(async (sonarrSeries) => {
|
|
||||||
// We grab media again here to make sure we have the latest version of it
|
|
||||||
const media = await mediaRepository.findOne({
|
|
||||||
where: { id: this.media.id },
|
|
||||||
relations: { requests: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!media) {
|
|
||||||
throw new Error('Media data not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
media[this.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
|
||||||
sonarrSeries.id;
|
|
||||||
media[this.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'] =
|
|
||||||
sonarrSeries.titleSlug;
|
|
||||||
media[this.is4k ? 'serviceId4k' : 'serviceId'] = sonarrSettings?.id;
|
|
||||||
await mediaRepository.save(media);
|
|
||||||
})
|
|
||||||
.catch(async () => {
|
|
||||||
const requestRepository = getRepository(MediaRequest);
|
|
||||||
|
|
||||||
this.status = MediaRequestStatus.FAILED;
|
|
||||||
requestRepository.save(this);
|
|
||||||
|
|
||||||
logger.warn(
|
|
||||||
'Something went wrong sending series request to Sonarr, marking status as FAILED',
|
|
||||||
{
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
sonarrSeriesOptions,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
this.sendNotification(media, Notification.MEDIA_FAILED);
|
|
||||||
});
|
|
||||||
logger.info('Sent request to Sonarr', {
|
|
||||||
label: 'Media Request',
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Something went wrong sending request to Sonarr', {
|
|
||||||
label: 'Media Request',
|
|
||||||
errorMessage: e.message,
|
|
||||||
requestId: this.id,
|
|
||||||
mediaId: this.media.id,
|
|
||||||
});
|
|
||||||
throw new Error(e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async sendNotification(media: Media, type: Notification) {
|
|
||||||
const tmdb = new TheMovieDb();
|
const tmdb = new TheMovieDb();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mediaType = this.type === MediaType.MOVIE ? 'Movie' : 'Series';
|
const mediaType = entity.type === MediaType.MOVIE ? 'Movie' : 'Series';
|
||||||
let event: string | undefined;
|
let event: string | undefined;
|
||||||
let notifyAdmin = true;
|
let notifyAdmin = true;
|
||||||
let notifySystem = true;
|
let notifySystem = true;
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case Notification.MEDIA_APPROVED:
|
case Notification.MEDIA_APPROVED:
|
||||||
event = `${this.is4k ? '4K ' : ''}${mediaType} Request Approved`;
|
event = `${entity.is4k ? '4K ' : ''}${mediaType} Request Approved`;
|
||||||
notifyAdmin = false;
|
notifyAdmin = false;
|
||||||
break;
|
break;
|
||||||
case Notification.MEDIA_DECLINED:
|
case Notification.MEDIA_DECLINED:
|
||||||
event = `${this.is4k ? '4K ' : ''}${mediaType} Request Declined`;
|
event = `${entity.is4k ? '4K ' : ''}${mediaType} Request Declined`;
|
||||||
notifyAdmin = false;
|
notifyAdmin = false;
|
||||||
break;
|
break;
|
||||||
case Notification.MEDIA_PENDING:
|
case Notification.MEDIA_PENDING:
|
||||||
event = `New ${this.is4k ? '4K ' : ''}${mediaType} Request`;
|
event = `New ${entity.is4k ? '4K ' : ''}${mediaType} Request`;
|
||||||
break;
|
break;
|
||||||
case Notification.MEDIA_AUTO_REQUESTED:
|
case Notification.MEDIA_AUTO_REQUESTED:
|
||||||
event = `${
|
event = `${
|
||||||
this.is4k ? '4K ' : ''
|
entity.is4k ? '4K ' : ''
|
||||||
}${mediaType} Request Automatically Submitted`;
|
}${mediaType} Request Automatically Submitted`;
|
||||||
notifyAdmin = false;
|
notifyAdmin = false;
|
||||||
notifySystem = false;
|
notifySystem = false;
|
||||||
break;
|
break;
|
||||||
case Notification.MEDIA_AUTO_APPROVED:
|
case Notification.MEDIA_AUTO_APPROVED:
|
||||||
event = `${
|
event = `${
|
||||||
this.is4k ? '4K ' : ''
|
entity.is4k ? '4K ' : ''
|
||||||
}${mediaType} Request Automatically Approved`;
|
}${mediaType} Request Automatically Approved`;
|
||||||
break;
|
break;
|
||||||
case Notification.MEDIA_FAILED:
|
case Notification.MEDIA_FAILED:
|
||||||
event = `${this.is4k ? '4K ' : ''}${mediaType} Request Failed`;
|
event = `${entity.is4k ? '4K ' : ''}${mediaType} Request Failed`;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.type === MediaType.MOVIE) {
|
if (entity.type === MediaType.MOVIE) {
|
||||||
const movie = await tmdb.getMovie({ movieId: media.tmdbId });
|
const movie = await tmdb.getMovie({ movieId: media.tmdbId });
|
||||||
notificationManager.sendNotification(type, {
|
notificationManager.sendNotification(type, {
|
||||||
media,
|
media,
|
||||||
request: this,
|
request: entity,
|
||||||
notifyAdmin,
|
notifyAdmin,
|
||||||
notifySystem,
|
notifySystem,
|
||||||
notifyUser: notifyAdmin ? undefined : this.requestedBy,
|
notifyUser: notifyAdmin ? undefined : entity.requestedBy,
|
||||||
event,
|
event,
|
||||||
subject: `${movie.title}${
|
subject: `${movie.title}${
|
||||||
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
|
||||||
@@ -1223,14 +611,14 @@ export class MediaRequest {
|
|||||||
}),
|
}),
|
||||||
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
|
||||||
});
|
});
|
||||||
} else if (this.type === MediaType.TV) {
|
} else if (entity.type === MediaType.TV) {
|
||||||
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
|
const tv = await tmdb.getTvShow({ tvId: media.tmdbId });
|
||||||
notificationManager.sendNotification(type, {
|
notificationManager.sendNotification(type, {
|
||||||
media,
|
media,
|
||||||
request: this,
|
request: entity,
|
||||||
notifyAdmin,
|
notifyAdmin,
|
||||||
notifySystem,
|
notifySystem,
|
||||||
notifyUser: notifyAdmin ? undefined : this.requestedBy,
|
notifyUser: notifyAdmin ? undefined : entity.requestedBy,
|
||||||
event,
|
event,
|
||||||
subject: `${tv.name}${
|
subject: `${tv.name}${
|
||||||
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
|
||||||
@@ -1244,7 +632,7 @@ export class MediaRequest {
|
|||||||
extra: [
|
extra: [
|
||||||
{
|
{
|
||||||
name: 'Requested Seasons',
|
name: 'Requested Seasons',
|
||||||
value: this.seasons
|
value: entity.seasons
|
||||||
.map((season) => season.seasonNumber)
|
.map((season) => season.seasonNumber)
|
||||||
.join(', '),
|
.join(', '),
|
||||||
},
|
},
|
||||||
@@ -1255,8 +643,8 @@ export class MediaRequest {
|
|||||||
logger.error('Something went wrong sending media notification(s)', {
|
logger.error('Something went wrong sending media notification(s)', {
|
||||||
label: 'Notifications',
|
label: 'Notifications',
|
||||||
errorMessage: e.message,
|
errorMessage: e.message,
|
||||||
requestId: this.id,
|
requestId: entity.id,
|
||||||
mediaId: this.media.id,
|
mediaId: entity.media.id,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,32 @@
|
|||||||
|
import type { RadarrMovieOptions } from '@server/api/servarr/radarr';
|
||||||
|
import RadarrAPI from '@server/api/servarr/radarr';
|
||||||
|
import type {
|
||||||
|
AddSeriesOptions,
|
||||||
|
SonarrSeries,
|
||||||
|
} from '@server/api/servarr/sonarr';
|
||||||
|
import SonarrAPI from '@server/api/servarr/sonarr';
|
||||||
import TheMovieDb from '@server/api/themoviedb';
|
import TheMovieDb from '@server/api/themoviedb';
|
||||||
|
import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants';
|
||||||
import {
|
import {
|
||||||
MediaRequestStatus,
|
MediaRequestStatus,
|
||||||
MediaStatus,
|
MediaStatus,
|
||||||
MediaType,
|
MediaType,
|
||||||
} from '@server/constants/media';
|
} from '@server/constants/media';
|
||||||
|
import { getRepository } from '@server/datasource';
|
||||||
|
import Media from '@server/entity/Media';
|
||||||
import { MediaRequest } from '@server/entity/MediaRequest';
|
import { MediaRequest } from '@server/entity/MediaRequest';
|
||||||
|
import SeasonRequest from '@server/entity/SeasonRequest';
|
||||||
import notificationManager, { Notification } from '@server/lib/notifications';
|
import notificationManager, { Notification } from '@server/lib/notifications';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { truncate } from 'lodash';
|
import { isEqual, truncate } from 'lodash';
|
||||||
import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
|
import type {
|
||||||
|
EntityManager,
|
||||||
|
EntitySubscriberInterface,
|
||||||
|
InsertEvent,
|
||||||
|
RemoveEvent,
|
||||||
|
UpdateEvent,
|
||||||
|
} from 'typeorm';
|
||||||
import { EventSubscriber } from 'typeorm';
|
import { EventSubscriber } from 'typeorm';
|
||||||
|
|
||||||
@EventSubscriber()
|
@EventSubscriber()
|
||||||
@@ -110,21 +128,673 @@ export class MediaRequestSubscriber
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public afterUpdate(event: UpdateEvent<MediaRequest>): void {
|
public async sendToRadarr(entity: MediaRequest): Promise<void> {
|
||||||
|
if (
|
||||||
|
entity.status === MediaRequestStatus.APPROVED &&
|
||||||
|
entity.type === MediaType.MOVIE
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const mediaRepository = getRepository(Media);
|
||||||
|
const settings = getSettings();
|
||||||
|
if (settings.radarr.length === 0 && !settings.radarr[0]) {
|
||||||
|
logger.info(
|
||||||
|
'No Radarr server configured, skipping request processing',
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let radarrSettings = settings.radarr.find(
|
||||||
|
(radarr) => radarr.isDefault && radarr.is4k === entity.is4k
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
entity.serverId !== null &&
|
||||||
|
entity.serverId >= 0 &&
|
||||||
|
radarrSettings?.id !== entity.serverId
|
||||||
|
) {
|
||||||
|
radarrSettings = settings.radarr.find(
|
||||||
|
(radarr) => radarr.id === entity.serverId
|
||||||
|
);
|
||||||
|
logger.info(
|
||||||
|
`Request has an override server: ${radarrSettings?.name}`,
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!radarrSettings) {
|
||||||
|
logger.warn(
|
||||||
|
`There is no default ${
|
||||||
|
entity.is4k ? '4K ' : ''
|
||||||
|
}Radarr server configured. Did you set any of your ${
|
||||||
|
entity.is4k ? '4K ' : ''
|
||||||
|
}Radarr servers as default?`,
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootFolder = radarrSettings.activeDirectory;
|
||||||
|
let qualityProfile = radarrSettings.activeProfileId;
|
||||||
|
let tags = radarrSettings.tags ? [...radarrSettings.tags] : [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
entity.rootFolder &&
|
||||||
|
entity.rootFolder !== '' &&
|
||||||
|
entity.rootFolder !== radarrSettings.activeDirectory
|
||||||
|
) {
|
||||||
|
rootFolder = entity.rootFolder;
|
||||||
|
logger.info(`Request has an override root folder: ${rootFolder}`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
entity.profileId &&
|
||||||
|
entity.profileId !== radarrSettings.activeProfileId
|
||||||
|
) {
|
||||||
|
qualityProfile = entity.profileId;
|
||||||
|
logger.info(
|
||||||
|
`Request has an override quality profile ID: ${qualityProfile}`,
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.tags && !isEqual(entity.tags, radarrSettings.tags)) {
|
||||||
|
tags = entity.tags;
|
||||||
|
logger.info(`Request has override tags`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
tagIds: tags,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmdb = new TheMovieDb();
|
||||||
|
const radarr = new RadarrAPI({
|
||||||
|
apiKey: radarrSettings.apiKey,
|
||||||
|
url: RadarrAPI.buildUrl(radarrSettings, '/api/v3'),
|
||||||
|
});
|
||||||
|
const movie = await tmdb.getMovie({ movieId: entity.media.tmdbId });
|
||||||
|
|
||||||
|
const media = await mediaRepository.findOne({
|
||||||
|
where: { id: entity.media.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!media) {
|
||||||
|
logger.error('Media data not found', {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (radarrSettings.tagRequests) {
|
||||||
|
let userTag = (await radarr.getTags()).find((v) =>
|
||||||
|
v.label.startsWith(entity.requestedBy.id + ' - ')
|
||||||
|
);
|
||||||
|
if (!userTag) {
|
||||||
|
logger.info(`Requester has no active tag. Creating new`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
userId: entity.requestedBy.id,
|
||||||
|
newTag:
|
||||||
|
entity.requestedBy.id + ' - ' + entity.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
userTag = await radarr.createTag({
|
||||||
|
label:
|
||||||
|
entity.requestedBy.id + ' - ' + entity.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (userTag.id) {
|
||||||
|
if (!tags?.find((v) => v === userTag?.id)) {
|
||||||
|
tags?.push(userTag.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`Requester has no tag and failed to add one`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
userId: entity.requestedBy.id,
|
||||||
|
radarrServer: radarrSettings.hostname + ':' + radarrSettings.port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||||
|
) {
|
||||||
|
logger.warn('Media already exists, marking request as APPROVED', {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
entity.status = MediaRequestStatus.APPROVED;
|
||||||
|
await requestRepository.save(entity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const radarrMovieOptions: RadarrMovieOptions = {
|
||||||
|
profileId: qualityProfile,
|
||||||
|
qualityProfileId: qualityProfile,
|
||||||
|
rootFolderPath: rootFolder,
|
||||||
|
minimumAvailability: radarrSettings.minimumAvailability,
|
||||||
|
title: movie.title,
|
||||||
|
tmdbId: movie.id,
|
||||||
|
year: Number(movie.release_date.slice(0, 4)),
|
||||||
|
monitored: true,
|
||||||
|
tags,
|
||||||
|
searchNow: !radarrSettings.preventSearch,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run entity asynchronously so we don't wait for it on the UI side
|
||||||
|
radarr
|
||||||
|
.addMovie(radarrMovieOptions)
|
||||||
|
.then(async (radarrMovie) => {
|
||||||
|
// We grab media again here to make sure we have the latest version of it
|
||||||
|
const media = await mediaRepository.findOne({
|
||||||
|
where: { id: entity.media.id },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!media) {
|
||||||
|
throw new Error('Media data not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
media[entity.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||||
|
radarrMovie.id;
|
||||||
|
media[
|
||||||
|
entity.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'
|
||||||
|
] = radarrMovie.titleSlug;
|
||||||
|
media[entity.is4k ? 'serviceId4k' : 'serviceId'] =
|
||||||
|
radarrSettings?.id;
|
||||||
|
await mediaRepository.save(media);
|
||||||
|
})
|
||||||
|
.catch(async () => {
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
|
entity.status = MediaRequestStatus.FAILED;
|
||||||
|
requestRepository.save(entity);
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
'Something went wrong sending movie request to Radarr, marking status as FAILED',
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
radarrMovieOptions,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
MediaRequest.sendNotification(
|
||||||
|
entity,
|
||||||
|
media,
|
||||||
|
Notification.MEDIA_FAILED
|
||||||
|
);
|
||||||
|
});
|
||||||
|
logger.info('Sent request to Radarr', {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Something went wrong sending request to Radarr', {
|
||||||
|
label: 'Media Request',
|
||||||
|
errorMessage: e.message,
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
});
|
||||||
|
throw new Error(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async sendToSonarr(entity: MediaRequest): Promise<void> {
|
||||||
|
if (
|
||||||
|
entity.status === MediaRequestStatus.APPROVED &&
|
||||||
|
entity.type === MediaType.TV
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const mediaRepository = getRepository(Media);
|
||||||
|
const settings = getSettings();
|
||||||
|
if (settings.sonarr.length === 0 && !settings.sonarr[0]) {
|
||||||
|
logger.warn(
|
||||||
|
'No Sonarr server configured, skipping request processing',
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sonarrSettings = settings.sonarr.find(
|
||||||
|
(sonarr) => sonarr.isDefault && sonarr.is4k === entity.is4k
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
entity.serverId !== null &&
|
||||||
|
entity.serverId >= 0 &&
|
||||||
|
sonarrSettings?.id !== entity.serverId
|
||||||
|
) {
|
||||||
|
sonarrSettings = settings.sonarr.find(
|
||||||
|
(sonarr) => sonarr.id === entity.serverId
|
||||||
|
);
|
||||||
|
logger.info(
|
||||||
|
`Request has an override server: ${sonarrSettings?.name}`,
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sonarrSettings) {
|
||||||
|
logger.warn(
|
||||||
|
`There is no default ${
|
||||||
|
entity.is4k ? '4K ' : ''
|
||||||
|
}Sonarr server configured. Did you set any of your ${
|
||||||
|
entity.is4k ? '4K ' : ''
|
||||||
|
}Sonarr servers as default?`,
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const media = await mediaRepository.findOne({
|
||||||
|
where: { id: entity.media.id },
|
||||||
|
relations: { requests: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!media) {
|
||||||
|
throw new Error('Media data not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
|
||||||
|
) {
|
||||||
|
logger.warn('Media already exists, marking request as APPROVED', {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
entity.status = MediaRequestStatus.APPROVED;
|
||||||
|
await requestRepository.save(entity);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tmdb = new TheMovieDb();
|
||||||
|
const sonarr = new SonarrAPI({
|
||||||
|
apiKey: sonarrSettings.apiKey,
|
||||||
|
url: SonarrAPI.buildUrl(sonarrSettings, '/api/v3'),
|
||||||
|
});
|
||||||
|
const series = await tmdb.getTvShow({ tvId: media.tmdbId });
|
||||||
|
const tvdbId = series.external_ids.tvdb_id ?? media.tvdbId;
|
||||||
|
|
||||||
|
if (!tvdbId) {
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
await mediaRepository.remove(media);
|
||||||
|
await requestRepository.remove(entity);
|
||||||
|
throw new Error('TVDB ID not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
let seriesType: SonarrSeries['seriesType'] = 'standard';
|
||||||
|
|
||||||
|
// Change series type to anime if the anime keyword is present on tmdb
|
||||||
|
if (
|
||||||
|
series.keywords.results.some(
|
||||||
|
(keyword) => keyword.id === ANIME_KEYWORD_ID
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
seriesType = sonarrSettings.animeSeriesType ?? 'anime';
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootFolder =
|
||||||
|
seriesType === 'anime' && sonarrSettings.activeAnimeDirectory
|
||||||
|
? sonarrSettings.activeAnimeDirectory
|
||||||
|
: sonarrSettings.activeDirectory;
|
||||||
|
let qualityProfile =
|
||||||
|
seriesType === 'anime' && sonarrSettings.activeAnimeProfileId
|
||||||
|
? sonarrSettings.activeAnimeProfileId
|
||||||
|
: sonarrSettings.activeProfileId;
|
||||||
|
let languageProfile =
|
||||||
|
seriesType === 'anime' && sonarrSettings.activeAnimeLanguageProfileId
|
||||||
|
? sonarrSettings.activeAnimeLanguageProfileId
|
||||||
|
: sonarrSettings.activeLanguageProfileId;
|
||||||
|
let tags =
|
||||||
|
seriesType === 'anime'
|
||||||
|
? sonarrSettings.animeTags
|
||||||
|
? [...sonarrSettings.animeTags]
|
||||||
|
: []
|
||||||
|
: sonarrSettings.tags
|
||||||
|
? [...sonarrSettings.tags]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
entity.rootFolder &&
|
||||||
|
entity.rootFolder !== '' &&
|
||||||
|
entity.rootFolder !== rootFolder
|
||||||
|
) {
|
||||||
|
rootFolder = entity.rootFolder;
|
||||||
|
logger.info(`Request has an override root folder: ${rootFolder}`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.profileId && entity.profileId !== qualityProfile) {
|
||||||
|
qualityProfile = entity.profileId;
|
||||||
|
logger.info(
|
||||||
|
`Request has an override quality profile ID: ${qualityProfile}`,
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
entity.languageProfileId &&
|
||||||
|
entity.languageProfileId !== languageProfile
|
||||||
|
) {
|
||||||
|
languageProfile = entity.languageProfileId;
|
||||||
|
logger.info(
|
||||||
|
`Request has an override language profile ID: ${languageProfile}`,
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.tags && !isEqual(entity.tags, tags)) {
|
||||||
|
tags = entity.tags;
|
||||||
|
logger.info(`Request has override tags`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
tagIds: tags,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sonarrSettings.tagRequests) {
|
||||||
|
let userTag = (await sonarr.getTags()).find((v) =>
|
||||||
|
v.label.startsWith(entity.requestedBy.id + ' - ')
|
||||||
|
);
|
||||||
|
if (!userTag) {
|
||||||
|
logger.info(`Requester has no active tag. Creating new`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
userId: entity.requestedBy.id,
|
||||||
|
newTag:
|
||||||
|
entity.requestedBy.id + ' - ' + entity.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
userTag = await sonarr.createTag({
|
||||||
|
label:
|
||||||
|
entity.requestedBy.id + ' - ' + entity.requestedBy.displayName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (userTag.id) {
|
||||||
|
if (!tags?.find((v) => v === userTag?.id)) {
|
||||||
|
tags?.push(userTag.id);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(`Requester has no tag and failed to add one`, {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
userId: entity.requestedBy.id,
|
||||||
|
sonarrServer: sonarrSettings.hostname + ':' + sonarrSettings.port,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sonarrSeriesOptions: AddSeriesOptions = {
|
||||||
|
profileId: qualityProfile,
|
||||||
|
languageProfileId: languageProfile,
|
||||||
|
rootFolderPath: rootFolder,
|
||||||
|
title: series.name,
|
||||||
|
tvdbid: tvdbId,
|
||||||
|
seasons: entity.seasons.map((season) => season.seasonNumber),
|
||||||
|
seasonFolder: sonarrSettings.enableSeasonFolders,
|
||||||
|
seriesType,
|
||||||
|
tags,
|
||||||
|
monitored: true,
|
||||||
|
searchNow: !sonarrSettings.preventSearch,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Run entity asynchronously so we don't wait for it on the UI side
|
||||||
|
sonarr
|
||||||
|
.addSeries(sonarrSeriesOptions)
|
||||||
|
.then(async (sonarrSeries) => {
|
||||||
|
// We grab media again here to make sure we have the latest version of it
|
||||||
|
const media = await mediaRepository.findOne({
|
||||||
|
where: { id: entity.media.id },
|
||||||
|
relations: { requests: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!media) {
|
||||||
|
throw new Error('Media data not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
media[entity.is4k ? 'externalServiceId4k' : 'externalServiceId'] =
|
||||||
|
sonarrSeries.id;
|
||||||
|
media[
|
||||||
|
entity.is4k ? 'externalServiceSlug4k' : 'externalServiceSlug'
|
||||||
|
] = sonarrSeries.titleSlug;
|
||||||
|
media[entity.is4k ? 'serviceId4k' : 'serviceId'] =
|
||||||
|
sonarrSettings?.id;
|
||||||
|
await mediaRepository.save(media);
|
||||||
|
})
|
||||||
|
.catch(async () => {
|
||||||
|
const requestRepository = getRepository(MediaRequest);
|
||||||
|
|
||||||
|
entity.status = MediaRequestStatus.FAILED;
|
||||||
|
requestRepository.save(entity);
|
||||||
|
|
||||||
|
logger.warn(
|
||||||
|
'Something went wrong sending series request to Sonarr, marking status as FAILED',
|
||||||
|
{
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
sonarrSeriesOptions,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
MediaRequest.sendNotification(
|
||||||
|
entity,
|
||||||
|
media,
|
||||||
|
Notification.MEDIA_FAILED
|
||||||
|
);
|
||||||
|
});
|
||||||
|
logger.info('Sent request to Sonarr', {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Something went wrong sending request to Sonarr', {
|
||||||
|
label: 'Media Request',
|
||||||
|
errorMessage: e.message,
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
});
|
||||||
|
throw new Error(e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async updateParentStatus(entity: MediaRequest): Promise<void> {
|
||||||
|
const mediaRepository = getRepository(Media);
|
||||||
|
const media = await mediaRepository.findOne({
|
||||||
|
where: { id: entity.media.id },
|
||||||
|
relations: { requests: true },
|
||||||
|
});
|
||||||
|
if (!media) {
|
||||||
|
logger.error('Media data not found', {
|
||||||
|
label: 'Media Request',
|
||||||
|
requestId: entity.id,
|
||||||
|
mediaId: entity.media.id,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const seasonRequestRepository = getRepository(SeasonRequest);
|
||||||
|
if (
|
||||||
|
entity.status === MediaRequestStatus.APPROVED &&
|
||||||
|
// Do not update the status if the item is already partially available or available
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE &&
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] !==
|
||||||
|
MediaStatus.PARTIALLY_AVAILABLE &&
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.PROCESSING
|
||||||
|
) {
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.PROCESSING;
|
||||||
|
mediaRepository.save(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
media.mediaType === MediaType.MOVIE &&
|
||||||
|
entity.status === MediaRequestStatus.DECLINED &&
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
|
||||||
|
) {
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
||||||
|
mediaRepository.save(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the media type is TV, and we are declining a request,
|
||||||
|
* we must check if its the only pending request and that
|
||||||
|
* there the current media status is just pending (meaning no
|
||||||
|
* other requests have yet to be approved)
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
media.mediaType === MediaType.TV &&
|
||||||
|
entity.status === MediaRequestStatus.DECLINED &&
|
||||||
|
media.requests.filter(
|
||||||
|
(request) => request.status === MediaRequestStatus.PENDING
|
||||||
|
).length === 0 &&
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] === MediaStatus.PENDING &&
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] !== MediaStatus.DELETED
|
||||||
|
) {
|
||||||
|
media[entity.is4k ? 'status4k' : 'status'] = MediaStatus.UNKNOWN;
|
||||||
|
mediaRepository.save(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Approve child seasons if parent is approved
|
||||||
|
if (
|
||||||
|
media.mediaType === MediaType.TV &&
|
||||||
|
entity.status === MediaRequestStatus.APPROVED
|
||||||
|
) {
|
||||||
|
entity.seasons.forEach((season) => {
|
||||||
|
season.status = MediaRequestStatus.APPROVED;
|
||||||
|
seasonRequestRepository.save(season);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleRemoveParentUpdate(
|
||||||
|
manager: EntityManager,
|
||||||
|
entity: MediaRequest
|
||||||
|
): Promise<void> {
|
||||||
|
const fullMedia = await manager.findOneOrFail(Media, {
|
||||||
|
where: { id: entity.media.id },
|
||||||
|
relations: { requests: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!fullMedia) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
!fullMedia.requests.some((request) => !request.is4k) &&
|
||||||
|
fullMedia.status !== MediaStatus.AVAILABLE
|
||||||
|
) {
|
||||||
|
fullMedia.status = MediaStatus.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!fullMedia.requests.some((request) => request.is4k) &&
|
||||||
|
fullMedia.status4k !== MediaStatus.AVAILABLE
|
||||||
|
) {
|
||||||
|
fullMedia.status4k = MediaStatus.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
await manager.save(fullMedia);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async afterUpdate(event: UpdateEvent<MediaRequest>): Promise<void> {
|
||||||
if (!event.entity) {
|
if (!event.entity) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.sendToRadarr(event.entity as MediaRequest);
|
||||||
|
await this.sendToSonarr(event.entity as MediaRequest);
|
||||||
|
|
||||||
|
await this.updateParentStatus(event.entity as MediaRequest);
|
||||||
|
|
||||||
if (event.entity.status === MediaRequestStatus.COMPLETED) {
|
if (event.entity.status === MediaRequestStatus.COMPLETED) {
|
||||||
if (event.entity.media.mediaType === MediaType.MOVIE) {
|
if (event.entity.media.mediaType === MediaType.MOVIE) {
|
||||||
this.notifyAvailableMovie(event.entity as MediaRequest);
|
await this.notifyAvailableMovie(event.entity as MediaRequest);
|
||||||
}
|
}
|
||||||
if (event.entity.media.mediaType === MediaType.TV) {
|
if (event.entity.media.mediaType === MediaType.TV) {
|
||||||
this.notifyAvailableSeries(event.entity as MediaRequest);
|
await this.notifyAvailableSeries(event.entity as MediaRequest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async afterInsert(event: InsertEvent<MediaRequest>): Promise<void> {
|
||||||
|
if (!event.entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.sendToRadarr(event.entity as MediaRequest);
|
||||||
|
await this.sendToSonarr(event.entity as MediaRequest);
|
||||||
|
|
||||||
|
await this.updateParentStatus(event.entity as MediaRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async afterRemove(event: RemoveEvent<MediaRequest>): Promise<void> {
|
||||||
|
if (!event.entity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.handleRemoveParentUpdate(
|
||||||
|
event.manager as EntityManager,
|
||||||
|
event.entity as MediaRequest
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public listenTo(): typeof MediaRequest {
|
public listenTo(): typeof MediaRequest {
|
||||||
return MediaRequest;
|
return MediaRequest;
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user