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
pull/376/head
sct 4 years ago
parent d5eb4d8d43
commit 67146c33ef

@ -5,6 +5,7 @@ const devConfig = {
logging: false, logging: false,
entities: ['server/entity/**/*.ts'], entities: ['server/entity/**/*.ts'],
migrations: ['server/migration/**/*.ts'], migrations: ['server/migration/**/*.ts'],
subscribers: ['server/subscriber/**/*.ts'],
cli: { cli: {
entitiesDir: 'server/entity', entitiesDir: 'server/entity',
migrationsDir: 'server/migration', migrationsDir: 'server/migration',
@ -19,6 +20,7 @@ const prodConfig = {
entities: ['dist/entity/**/*.js'], entities: ['dist/entity/**/*.js'],
migrations: ['dist/migration/**/*.js'], migrations: ['dist/migration/**/*.js'],
migrationsRun: true, migrationsRun: true,
subscribers: ['dist/subscriber/**/*.js'],
cli: { cli: {
entitiesDir: 'dist/entity', entitiesDir: 'dist/entity',
migrationsDir: 'dist/migration', migrationsDir: 'dist/migration',

@ -8,14 +8,11 @@ import {
UpdateDateColumn, UpdateDateColumn,
getRepository, getRepository,
In, In,
AfterUpdate,
} from 'typeorm'; } from 'typeorm';
import { MediaRequest } from './MediaRequest'; import { MediaRequest } from './MediaRequest';
import { MediaStatus, MediaType } from '../constants/media'; import { MediaStatus, MediaType } from '../constants/media';
import logger from '../logger'; import logger from '../logger';
import Season from './Season'; import Season from './Season';
import notificationManager, { Notification } from '../lib/notifications';
import TheMovieDb from '../api/themoviedb';
@Entity() @Entity()
class Media { class Media {
@ -98,32 +95,6 @@ class Media {
constructor(init?: Partial<Media>) { constructor(init?: Partial<Media>) {
Object.assign(this, init); 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; export default Media;

@ -5,15 +5,9 @@ import {
ManyToOne, ManyToOne,
CreateDateColumn, CreateDateColumn,
UpdateDateColumn, UpdateDateColumn,
AfterInsert,
AfterUpdate,
getRepository,
} from 'typeorm'; } from 'typeorm';
import { MediaStatus } from '../constants/media'; import { MediaStatus } from '../constants/media';
import Media from './Media'; import Media from './Media';
import logger from '../logger';
import TheMovieDb from '../api/themoviedb';
import notificationManager, { Notification } from '../lib/notifications';
@Entity() @Entity()
class Season { class Season {
@ -38,60 +32,6 @@ class Season {
constructor(init?: Partial<Season>) { constructor(init?: Partial<Season>) {
Object.assign(this, init); 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; export default Season;

@ -7,6 +7,7 @@ import { MediaStatus, MediaType } from '../../constants/media';
import logger from '../../logger'; import logger from '../../logger';
import { getSettings, Library } from '../../lib/settings'; import { getSettings, Library } from '../../lib/settings';
import Season from '../../entity/Season'; import Season from '../../entity/Season';
import { uniqWith } from 'lodash';
const BUNDLE_SIZE = 20; const BUNDLE_SIZE = 20;
const UPDATE_RATE = 4 * 1000; const UPDATE_RATE = 4 * 1000;
@ -326,7 +327,25 @@ class JobPlexSync {
`Beginning to process recently added for library: ${library.name}`, `Beginning to process recently added for library: ${library.name}`,
'info' '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(); await this.loop();
} }
} else { } else {

@ -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<Media>): 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);
}
}
}
Loading…
Cancel
Save