From 67146c33ef7f28d520ba2c50b32673d43f4525c8 Mon Sep 17 00:00:00 2001 From: sct Date: Thu, 17 Dec 2020 06:28:03 +0000 Subject: [PATCH] fix(plex-sync): bundle duplicate ratingKeys to speed up recently added sync This includes a rewrite to move movie/series availability notifications into a subscriber to prevent duplicate notifications for series fix #360 --- ormconfig.js | 2 + server/entity/Media.ts | 29 ------- server/entity/Season.ts | 60 -------------- server/job/plexsync/index.ts | 21 ++++- server/subscriber/MediaSubscriber.ts | 112 +++++++++++++++++++++++++++ 5 files changed, 134 insertions(+), 90 deletions(-) create mode 100644 server/subscriber/MediaSubscriber.ts diff --git a/ormconfig.js b/ormconfig.js index 93da376bc..2c0afb735 100644 --- a/ormconfig.js +++ b/ormconfig.js @@ -5,6 +5,7 @@ const devConfig = { logging: false, entities: ['server/entity/**/*.ts'], migrations: ['server/migration/**/*.ts'], + subscribers: ['server/subscriber/**/*.ts'], cli: { entitiesDir: 'server/entity', migrationsDir: 'server/migration', @@ -19,6 +20,7 @@ const prodConfig = { entities: ['dist/entity/**/*.js'], migrations: ['dist/migration/**/*.js'], migrationsRun: true, + subscribers: ['dist/subscriber/**/*.js'], cli: { entitiesDir: 'dist/entity', migrationsDir: 'dist/migration', diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 8f2f8ff6d..0222e1043 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -8,14 +8,11 @@ import { UpdateDateColumn, getRepository, In, - AfterUpdate, } from 'typeorm'; import { MediaRequest } from './MediaRequest'; import { MediaStatus, MediaType } from '../constants/media'; import logger from '../logger'; import Season from './Season'; -import notificationManager, { Notification } from '../lib/notifications'; -import TheMovieDb from '../api/themoviedb'; @Entity() class Media { @@ -98,32 +95,6 @@ class Media { constructor(init?: Partial) { Object.assign(this, init); } - - @AfterUpdate() - private async _notifyAvailable() { - if (this.status === MediaStatus.AVAILABLE) { - if (this.mediaType === MediaType.MOVIE) { - const requestRepository = getRepository(MediaRequest); - const relatedRequests = await requestRepository.find({ - where: { media: this }, - }); - - if (relatedRequests.length > 0) { - const tmdb = new TheMovieDb(); - const movie = await tmdb.getMovie({ movieId: this.tmdbId }); - - relatedRequests.forEach((request) => { - notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { - notifyUser: request.requestedBy, - subject: movie.title, - message: movie.overview, - image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, - }); - }); - } - } - } - } } export default Media; diff --git a/server/entity/Season.ts b/server/entity/Season.ts index a591c3ca7..d66805cbd 100644 --- a/server/entity/Season.ts +++ b/server/entity/Season.ts @@ -5,15 +5,9 @@ import { ManyToOne, CreateDateColumn, UpdateDateColumn, - AfterInsert, - AfterUpdate, - getRepository, } from 'typeorm'; import { MediaStatus } from '../constants/media'; import Media from './Media'; -import logger from '../logger'; -import TheMovieDb from '../api/themoviedb'; -import notificationManager, { Notification } from '../lib/notifications'; @Entity() class Season { @@ -38,60 +32,6 @@ class Season { constructor(init?: Partial) { Object.assign(this, init); } - - @AfterInsert() - @AfterUpdate() - private async _sendSeasonAvailableNotification() { - if (this.status === MediaStatus.AVAILABLE) { - try { - const lazyMedia = await this.media; - const tmdb = new TheMovieDb(); - const mediaRepository = getRepository(Media); - const media = await mediaRepository.findOneOrFail({ - where: { id: lazyMedia.id }, - relations: ['requests'], - }); - - const availableSeasons = media.seasons.map( - (season) => season.seasonNumber - ); - - const request = media.requests.find( - (request) => - // Check if the season is complete AND it contains the current season that was just marked available - request.seasons.every((season) => - availableSeasons.includes(season.seasonNumber) - ) && - request.seasons.some( - (season) => season.seasonNumber === this.seasonNumber - ) - ); - - if (request) { - const tv = await tmdb.getTvShow({ tvId: media.tmdbId }); - notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { - subject: tv.name, - message: tv.overview, - notifyUser: request.requestedBy, - image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, - extra: [ - { - name: 'Seasons', - value: request.seasons - .map((season) => season.seasonNumber) - .join(', '), - }, - ], - }); - } - } catch (e) { - logger.error('Something went wrong sending season available notice', { - label: 'Notifications', - message: e.message, - }); - } - } - } } export default Season; diff --git a/server/job/plexsync/index.ts b/server/job/plexsync/index.ts index 87e078d40..c38197ffa 100644 --- a/server/job/plexsync/index.ts +++ b/server/job/plexsync/index.ts @@ -7,6 +7,7 @@ import { MediaStatus, MediaType } from '../../constants/media'; import logger from '../../logger'; import { getSettings, Library } from '../../lib/settings'; import Season from '../../entity/Season'; +import { uniqWith } from 'lodash'; const BUNDLE_SIZE = 20; const UPDATE_RATE = 4 * 1000; @@ -326,7 +327,25 @@ class JobPlexSync { `Beginning to process recently added for library: ${library.name}`, 'info' ); - this.items = await this.plexClient.getRecentlyAdded(library.id); + const libraryItems = await this.plexClient.getRecentlyAdded( + library.id + ); + + // Bundle items up by rating keys + this.items = uniqWith(libraryItems, (mediaA, mediaB) => { + if (mediaA.grandparentRatingKey && mediaB.grandparentRatingKey) { + return ( + mediaA.grandparentRatingKey === mediaB.grandparentRatingKey + ); + } + + if (mediaA.parentRatingKey && mediaB.parentRatingKey) { + return mediaA.parentRatingKey === mediaB.parentRatingKey; + } + + return mediaA.ratingKey === mediaB.ratingKey; + }); + await this.loop(); } } else { diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts new file mode 100644 index 000000000..f63b14f64 --- /dev/null +++ b/server/subscriber/MediaSubscriber.ts @@ -0,0 +1,112 @@ +import { + EntitySubscriberInterface, + EventSubscriber, + getRepository, + UpdateEvent, +} from 'typeorm'; +import TheMovieDb from '../api/themoviedb'; +import { MediaStatus, MediaType } from '../constants/media'; +import Media from '../entity/Media'; +import { MediaRequest } from '../entity/MediaRequest'; +import notificationManager, { Notification } from '../lib/notifications'; + +@EventSubscriber() +export class MediaSubscriber implements EntitySubscriberInterface { + private async notifyAvailableMovie(entity: Media) { + if (entity.status === MediaStatus.AVAILABLE) { + if (entity.mediaType === MediaType.MOVIE) { + const requestRepository = getRepository(MediaRequest); + const relatedRequests = await requestRepository.find({ + where: { media: entity }, + }); + + if (relatedRequests.length > 0) { + const tmdb = new TheMovieDb(); + const movie = await tmdb.getMovie({ movieId: entity.tmdbId }); + + relatedRequests.forEach((request) => { + notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { + notifyUser: request.requestedBy, + subject: movie.title, + message: movie.overview, + image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${movie.poster_path}`, + }); + }); + } + } + } + } + + private async notifyAvailableSeries(entity: Media, dbEntity: Media) { + const newAvailableSeasons = entity.seasons + .filter((season) => season.status === MediaStatus.AVAILABLE) + .map((season) => season.seasonNumber); + const oldAvailableSeasons = dbEntity.seasons + .filter((season) => season.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: entity }, + }); + 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) + ); + const tv = await tmdb.getTvShow({ tvId: entity.tmdbId }); + notificationManager.sendNotification(Notification.MEDIA_AVAILABLE, { + subject: tv.name, + message: tv.overview, + notifyUser: request.requestedBy, + image: `https://image.tmdb.org/t/p/w600_and_h900_bestv2${tv.poster_path}`, + extra: [ + { + name: 'Seasons', + value: request.seasons + .map((season) => season.seasonNumber) + .join(', '), + }, + ], + }); + } + } + } + } + + public beforeUpdate(event: UpdateEvent): void { + if ( + event.entity.mediaType === MediaType.MOVIE && + event.entity.status === MediaStatus.AVAILABLE + ) { + this.notifyAvailableMovie(event.entity); + } + + if ( + event.entity.mediaType === MediaType.TV && + (event.entity.status === MediaStatus.AVAILABLE || + event.entity.status === MediaStatus.PARTIALLY_AVAILABLE) + ) { + this.notifyAvailableSeries(event.entity, event.databaseEntity); + } + } +}