import { truncate } from 'lodash'; import type { EntitySubscriberInterface, UpdateEvent } from 'typeorm'; import { EventSubscriber, In, Not } from 'typeorm'; import TheMovieDb from '../api/themoviedb'; import { MediaRequestStatus, MediaStatus, MediaType } from '../constants/media'; import { getRepository } from '../datasource'; import Media from '../entity/Media'; import { MediaRequest } from '../entity/MediaRequest'; import Season from '../entity/Season'; import notificationManager, { Notification } from '../lib/notifications'; import logger from '../logger'; @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 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: entity.tmdbId }); relatedRequests.forEach((request) => { notificationManager.sendNotification( Notification.MEDIA_AVAILABLE, { event: `${is4k ? '4K ' : ''}Movie Request Now Available`, notifyAdmin: false, 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: 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, 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): 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; } }