From 1cb691f9cdead48e6302526f214185ee6b8ff7f2 Mon Sep 17 00:00:00 2001 From: Brandon Cohen Date: Fri, 16 Jun 2023 18:31:26 -0400 Subject: [PATCH] fix: prevented notifications from sending to old deleted requests --- server/entity/Media.ts | 3 + server/entity/Season.ts | 2 + server/entity/SeasonRequest.ts | 3 + server/lib/availabilitySync.ts | 1 + server/lib/scanners/baseScanner.ts | 6 - server/routes/media.ts | 3 + server/subscriber/MediaRequestSubscriber.ts | 128 ++++++++++++ server/subscriber/MediaSubscriber.ts | 212 +------------------- src/i18n/locale/en.json | 2 + 9 files changed, 144 insertions(+), 216 deletions(-) create mode 100644 server/subscriber/MediaRequestSubscriber.ts diff --git a/server/entity/Media.ts b/server/entity/Media.ts index d5c788741..c7a3dccd0 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -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) { relatedRequests.forEach((request) => { if ( diff --git a/server/entity/Season.ts b/server/entity/Season.ts index 993fcea10..170f8a0d6 100644 --- a/server/entity/Season.ts +++ b/server/entity/Season.ts @@ -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) => { if ( this.seasonNumber === seasonRequest.seasonNumber && diff --git a/server/entity/SeasonRequest.ts b/server/entity/SeasonRequest.ts index c71445723..f2416ded5 100644 --- a/server/entity/SeasonRequest.ts +++ b/server/entity/SeasonRequest.ts @@ -45,6 +45,9 @@ class SeasonRequest { 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( (seasonRequest) => seasonRequest.status === MediaRequestStatus.COMPLETED ); diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts index 4b1913001..bab387fbf 100644 --- a/server/lib/availabilitySync.ts +++ b/server/lib/availabilitySync.ts @@ -725,4 +725,5 @@ class AvailabilitySync { } const availabilitySync = new AvailabilitySync(); + export default availabilitySync; diff --git a/server/lib/scanners/baseScanner.ts b/server/lib/scanners/baseScanner.ts index 439174f7f..5ac836379 100644 --- a/server/lib/scanners/baseScanner.ts +++ b/server/lib/scanners/baseScanner.ts @@ -163,7 +163,6 @@ class BaseScanner { if (changedExisting) { await mediaRepository.save(existing); - this.log( `Media for ${title} exists. Changes were detected and the title will be updated.`, 'info' @@ -204,7 +203,6 @@ class BaseScanner { newMedia.ratingKey4k = is4k && this.enable4kMovie ? ratingKey : undefined; } - await mediaRepository.save(newMedia); this.log(`Saved new media: ${title}`); } @@ -454,9 +452,7 @@ class BaseScanner { : media.status4k === MediaStatus.DELETED ? MediaStatus.DELETED : MediaStatus.UNKNOWN; - await mediaRepository.save(media); - this.log(`Updating existing title: ${title}`); } else { const newMedia = new Media({ @@ -516,9 +512,7 @@ class BaseScanner { ? MediaStatus.PROCESSING : MediaStatus.UNKNOWN, }); - await mediaRepository.save(newMedia); - this.log(`Saved ${title}`); } }); diff --git a/server/routes/media.ts b/server/routes/media.ts index ee0bc4f48..b0f684c55 100644 --- a/server/routes/media.ts +++ b/server/routes/media.ts @@ -147,6 +147,9 @@ mediaRoutes.post< } 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({ relations: { media: true, diff --git a/server/subscriber/MediaRequestSubscriber.ts b/server/subscriber/MediaRequestSubscriber.ts new file mode 100644 index 000000000..cb63702a1 --- /dev/null +++ b/server/subscriber/MediaRequestSubscriber.ts @@ -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 +{ + 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): 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; + } +} diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts index 3e523250d..b73e4ecbd 100644 --- a/server/subscriber/MediaSubscriber.ts +++ b/server/subscriber/MediaSubscriber.ts @@ -1,174 +1,12 @@ -import TheMovieDb from '@server/api/themoviedb'; -import { - MediaRequestStatus, - MediaStatus, - MediaType, -} from '@server/constants/media'; +import { MediaRequestStatus, MediaStatus } 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 } from 'typeorm'; +import { EventSubscriber } from 'typeorm'; @EventSubscriber() export class MediaSubscriber implements EntitySubscriberInterface { - 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) { const requestRepository = getRepository(MediaRequest); @@ -192,52 +30,6 @@ export class MediaSubscriber implements EntitySubscriberInterface { 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 diff --git a/src/i18n/locale/en.json b/src/i18n/locale/en.json index 10165c9e1..2464a76d6 100644 --- a/src/i18n/locale/en.json +++ b/src/i18n/locale/en.json @@ -1190,9 +1190,11 @@ "i18n.canceling": "Canceling…", "i18n.close": "Close", "i18n.collection": "Collection", + "i18n.completed": "Completed", "i18n.decline": "Decline", "i18n.declined": "Declined", "i18n.delete": "Delete", + "i18n.deleted": "Deleted", "i18n.deleting": "Deleting…", "i18n.delimitedlist": "{a}, {b}", "i18n.edit": "Edit",