fix: prevented notifications from sending to old deleted requests

pull/3460/head
Brandon Cohen 1 year ago committed by OwsleyJr
parent 82bf2aa4df
commit 1cb691f9cd

@ -329,6 +329,9 @@ class Media {
}, },
}); });
// Check the media entity status and if
// available or deleted, set the related request
// to completed
if (relatedRequests.length > 0) { if (relatedRequests.length > 0) {
relatedRequests.forEach((request) => { relatedRequests.forEach((request) => {
if ( if (

@ -53,6 +53,8 @@ class Season {
}, },
}); });
// Check seasons when/if they become available or deleted,
// then set the related season request to completed
relatedSeasonRequests.forEach((seasonRequest) => { relatedSeasonRequests.forEach((seasonRequest) => {
if ( if (
this.seasonNumber === seasonRequest.seasonNumber && this.seasonNumber === seasonRequest.seasonNumber &&

@ -45,6 +45,9 @@ class SeasonRequest {
where: { id: this.request.id }, where: { id: this.request.id },
}); });
// Check the parent of the season request and
// if every season request is complete
// set the parent request to complete as well
const isRequestComplete = relatedRequest?.seasons.every( const isRequestComplete = relatedRequest?.seasons.every(
(seasonRequest) => seasonRequest.status === MediaRequestStatus.COMPLETED (seasonRequest) => seasonRequest.status === MediaRequestStatus.COMPLETED
); );

@ -725,4 +725,5 @@ class AvailabilitySync {
} }
const availabilitySync = new AvailabilitySync(); const availabilitySync = new AvailabilitySync();
export default availabilitySync; export default availabilitySync;

@ -163,7 +163,6 @@ class BaseScanner<T> {
if (changedExisting) { if (changedExisting) {
await mediaRepository.save(existing); await mediaRepository.save(existing);
this.log( this.log(
`Media for ${title} exists. Changes were detected and the title will be updated.`, `Media for ${title} exists. Changes were detected and the title will be updated.`,
'info' 'info'
@ -204,7 +203,6 @@ class BaseScanner<T> {
newMedia.ratingKey4k = newMedia.ratingKey4k =
is4k && this.enable4kMovie ? ratingKey : undefined; is4k && this.enable4kMovie ? ratingKey : undefined;
} }
await mediaRepository.save(newMedia); await mediaRepository.save(newMedia);
this.log(`Saved new media: ${title}`); this.log(`Saved new media: ${title}`);
} }
@ -454,9 +452,7 @@ class BaseScanner<T> {
: media.status4k === MediaStatus.DELETED : media.status4k === MediaStatus.DELETED
? MediaStatus.DELETED ? MediaStatus.DELETED
: MediaStatus.UNKNOWN; : MediaStatus.UNKNOWN;
await mediaRepository.save(media); await mediaRepository.save(media);
this.log(`Updating existing title: ${title}`); this.log(`Updating existing title: ${title}`);
} else { } else {
const newMedia = new Media({ const newMedia = new Media({
@ -516,9 +512,7 @@ class BaseScanner<T> {
? MediaStatus.PROCESSING ? MediaStatus.PROCESSING
: MediaStatus.UNKNOWN, : MediaStatus.UNKNOWN,
}); });
await mediaRepository.save(newMedia); await mediaRepository.save(newMedia);
this.log(`Saved ${title}`); this.log(`Saved ${title}`);
} }
}); });

@ -147,6 +147,9 @@ mediaRoutes.post<
} }
if (req.params.status === 'available') { if (req.params.status === 'available') {
// Here we check all related media requests and
// then set to completed if the media is marked
// as available
const requests = await requestRepository.find({ const requests = await requestRepository.find({
relations: { relations: {
media: true, media: true,

@ -0,0 +1,128 @@
import TheMovieDb from '@server/api/themoviedb';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { MediaRequest } from '@server/entity/MediaRequest';
import notificationManager, { Notification } from '@server/lib/notifications';
import logger from '@server/logger';
import { truncate } from 'lodash';
import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
import { EventSubscriber } from 'typeorm';
@EventSubscriber()
export class MediaRequestSubscriber
implements EntitySubscriberInterface<MediaRequest>
{
private async notifyAvailableMovie(entity: MediaRequest) {
if (entity.media.status === MediaStatus.AVAILABLE) {
const tmdb = new TheMovieDb();
try {
const movie = await tmdb.getMovie({
movieId: entity.media.tmdbId,
});
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
event: `${entity.is4k ? '4K ' : ''}Movie Request Now Available`,
notifyAdmin: false,
notifySystem: true,
notifyUser: entity.requestedBy,
subject: `${movie.title}${
movie.release_date ? ` (${movie.release_date.slice(0, 4)})` : ''
}`,
message: truncate(movie.overview, {
length: 500,
separator: /\s/,
omission: '…',
}),
media: entity.media,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`,
request: entity,
});
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
mediaId: entity.id,
});
}
}
}
private async notifyAvailableSeries(entity: MediaRequest) {
// Find all seasons in the related media entity
// and see if they are available, then we can check
// if the request contains the same seasons
const isMediaAvailable = entity.media.seasons
.filter(
(season) =>
season[entity.is4k ? 'status4k' : 'status'] === MediaStatus.AVAILABLE
)
.every((seasonRequest) =>
entity.seasons.find(
(season) => season.seasonNumber === seasonRequest.seasonNumber
)
);
if (entity.media.status === MediaStatus.AVAILABLE || isMediaAvailable) {
const tmdb = new TheMovieDb();
try {
const tv = await tmdb.getTvShow({ tvId: entity.media.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
event: `${entity.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: entity.requestedBy,
image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`,
media: entity.media,
extra: [
{
name: 'Requested Seasons',
value: entity.seasons
.map((season) => season.seasonNumber)
.join(', '),
},
],
request: entity,
});
} catch (e) {
logger.error('Something went wrong sending media notification(s)', {
label: 'Notifications',
errorMessage: e.message,
mediaId: entity.id,
});
}
}
}
public afterUpdate(event: UpdateEvent<MediaRequest>): void {
if (!event.entity) {
return;
}
if (event.entity.status === MediaRequestStatus.COMPLETED) {
if (event.entity.media.mediaType === MediaType.MOVIE) {
this.notifyAvailableMovie(event.entity as MediaRequest);
}
if (event.entity.media.mediaType === MediaType.TV) {
this.notifyAvailableSeries(event.entity as MediaRequest);
}
}
}
public listenTo(): typeof MediaRequest {
return MediaRequest;
}
}

@ -1,174 +1,12 @@
import TheMovieDb from '@server/api/themoviedb'; import { MediaRequestStatus, MediaStatus } from '@server/constants/media';
import {
MediaRequestStatus,
MediaStatus,
MediaType,
} from '@server/constants/media';
import { getRepository } from '@server/datasource'; import { getRepository } from '@server/datasource';
import Media from '@server/entity/Media'; import Media from '@server/entity/Media';
import { MediaRequest } from '@server/entity/MediaRequest'; 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 type { EntitySubscriberInterface, UpdateEvent } from 'typeorm';
import { EventSubscriber, In } from 'typeorm'; import { EventSubscriber } from 'typeorm';
@EventSubscriber() @EventSubscriber()
export class MediaSubscriber implements EntitySubscriberInterface<Media> { 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 relatedRequest = await requestRepository.findOne({
where: {
media: {
id: entity.id,
},
is4k,
status: MediaRequestStatus.COMPLETED,
},
order: { id: 'DESC' },
});
const tmdb = new TheMovieDb();
if (relatedRequest) {
try {
const movie = await tmdb.getMovie({ movieId: entity.tmdbId });
notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, {
event: `${is4k ? '4K ' : ''}Movie Request Now Available`,
notifyAdmin: false,
notifySystem: true,
notifyUser: relatedRequest.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: relatedRequest,
});
} 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: MediaRequestStatus.COMPLETED,
},
order: { id: 'DESC' },
});
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: 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) { private async updateChildRequestStatus(event: Media, is4k: boolean) {
const requestRepository = getRepository(MediaRequest); const requestRepository = getRepository(MediaRequest);
@ -192,52 +30,6 @@ export class MediaSubscriber implements EntitySubscriberInterface<Media> {
return; 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 ( if (
event.entity.status === MediaStatus.AVAILABLE && event.entity.status === MediaStatus.AVAILABLE &&
event.databaseEntity.status === MediaStatus.PENDING event.databaseEntity.status === MediaStatus.PENDING

@ -1190,9 +1190,11 @@
"i18n.canceling": "Canceling…", "i18n.canceling": "Canceling…",
"i18n.close": "Close", "i18n.close": "Close",
"i18n.collection": "Collection", "i18n.collection": "Collection",
"i18n.completed": "Completed",
"i18n.decline": "Decline", "i18n.decline": "Decline",
"i18n.declined": "Declined", "i18n.declined": "Declined",
"i18n.delete": "Delete", "i18n.delete": "Delete",
"i18n.deleted": "Deleted",
"i18n.deleting": "Deleting…", "i18n.deleting": "Deleting…",
"i18n.delimitedlist": "{a}, {b}", "i18n.delimitedlist": "{a}, {b}",
"i18n.edit": "Edit", "i18n.edit": "Edit",

Loading…
Cancel
Save