You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
overseerr/server/subscriber/MediaSubscriber.ts

267 lines
8.1 KiB

import TheMovieDb from '@server/api/themoviedb';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media';
import { MediaRequest } from '@server/entity/MediaRequest';
import Season from '@server/entity/Season';
import notificationManager, { Notification } from '@server/lib/notifications';
import logger from '@server/logger';
import { truncate } from 'lodash';
import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
import { EventSubscriber, In, Not } from 'typeorm';
@EventSubscriber()
export class MediaSubscriber implements EntitySubscriberInterface<Media> {
private async notifyAvailableMovie(
entity: Media,
dbEntity: Media,
is4k: boolean
) {
if (
entity[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE &&
dbEntity[is4k ? 'status4k' : 'status'] !== MediaStatus.AVAILABLE
) {
if (entity.mediaType === MediaType.MOVIE) {
const requestRepository = getRepository(MediaRequest);
const relatedRequests = await requestRepository.find({
where: {
media: {
id: entity.id,
},
is4k,
status: Not(MediaRequestStatus.DECLINED),
},
});
if (relatedRequests.length > 0) {
const tmdb = new TheMovieDb();
try {
const movie = await tmdb.getMovie({
movieId: Number(entity.tmdbId),
});
relatedRequests.forEach((request) => {
notificationManager.sendNotification(
Notification.MEDIA_AVAILABLE,
{
event: `${is4k ? '4K ' : ''}Movie Request Now Available`,
notifyAdmin: false,
notifySystem: true,
notifyUser: request.requestedBy,
subject: `${movie.title}${
movie.release_date
? ` (${movie.release_date.slice(0, 4)})`
: ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media: entity,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request,
}
);
});
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
mediaId: entity.id,
});
}
}
}
}
}
private async notifyAvailableSeries(
entity: Media,
dbEntity: Media,
is4k: boolean
) {
const seasonRepository = getRepository(Season);
const newAvailableSeasons = entity.seasons
.filter(
(season) =>
season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
)
.map((season) => season.seasonNumber);
const oldSeasonIds = dbEntity.seasons.map((season) => season.id);
const oldSeasons = await seasonRepository.findBy({ id: In(oldSeasonIds) });
const oldAvailableSeasons = oldSeasons
.filter(
(season) =>
season[is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
)
.map((season) => season.seasonNumber);
const changedSeasons = newAvailableSeasons.filter(
(seasonNumber) => !oldAvailableSeasons.includes(seasonNumber)
);
if (changedSeasons.length > 0) {
const tmdb = new TheMovieDb();
const requestRepository = getRepository(MediaRequest);
const processedSeasons: number[] = [];
for (const changedSeasonNumber of changedSeasons) {
const requests = await requestRepository.find({
where: {
media: {
id: entity.id,
},
is4k,
status: Not(MediaRequestStatus.DECLINED),
},
});
const request = requests.find(
(request) =>
// Check if the season is complete AND it contains the current season that was just marked available
request.seasons.every((season) =>
newAvailableSeasons.includes(season.seasonNumber)
) &&
request.seasons.some(
(season) => season.seasonNumber === changedSeasonNumber
)
);
if (request && !processedSeasons.includes(changedSeasonNumber)) {
processedSeasons.push(
...request.seasons.map((season) => season.seasonNumber)
);
try {
const tv = await tmdb.getTvShow({ tvId: Number(entity.tmdbId) });
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
event: `${is4k ? '4K ' : ''}Series Request Now Available`,
subject: `${tv.name}${
tv.first_air_date ? ` (${tv.first_air_date.slice(0, 4)})` : ''
}`,
message: truncate(tv.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
notifyAdmin: false,
notifySystem: true,
notifyUser: request.requestedBy,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media: entity,
extra: [
{
name: 'Requested Seasons',
value: request.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
request,
});
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
mediaId: entity.id,
});
}
}
}
}
}
private async updateChildRequestStatus(event: Media, is4k: boolean) {
const requestRepository = getRepository(MediaRequest);
const requests = await requestRepository.find({
where: { media: { id: event.id } },
});
for (const request of requests) {
if (
request.is4k === is4k &&
request.status === MediaRequestStatus.PENDING
) {
request.status = MediaRequestStatus.APPROVED;
await requestRepository.save(request);
}
}
}
public beforeUpdate(event: UpdateEvent<Media>): void {
if (!event.entity) {
return;
}
if (
event.entity.mediaType === MediaType.MOVIE &&
event.entity.status === MediaStatus.AVAILABLE
) {
this.notifyAvailableMovie(
event.entity as Media,
event.databaseEntity,
false
);
}
if (
event.entity.mediaType === MediaType.MOVIE &&
event.entity.status4k === MediaStatus.AVAILABLE
) {
this.notifyAvailableMovie(
event.entity as Media,
event.databaseEntity,
true
);
}
if (
event.entity.mediaType === MediaType.TV &&
(event.entity.status === MediaStatus.AVAILABLE ||
event.entity.status === MediaStatus.PARTIALLY_AVAILABLE)
) {
this.notifyAvailableSeries(
event.entity as Media,
event.databaseEntity,
false
);
}
if (
event.entity.mediaType === MediaType.TV &&
(event.entity.status4k === MediaStatus.AVAILABLE ||
event.entity.status4k === MediaStatus.PARTIALLY_AVAILABLE)
) {
this.notifyAvailableSeries(
event.entity as Media,
event.databaseEntity,
true
);
}
if (
event.entity.status === MediaStatus.AVAILABLE &&
event.databaseEntity.status === MediaStatus.PENDING
) {
this.updateChildRequestStatus(event.entity as Media, false);
}
if (
event.entity.status4k === MediaStatus.AVAILABLE &&
event.databaseEntity.status4k === MediaStatus.PENDING
) {
this.updateChildRequestStatus(event.entity as Media, true);
}
}
public listenTo(): typeof Media {
return Media;
}
}