From 82bf2aa4df74790c40befbe9622f6761e7cf3f4f Mon Sep 17 00:00:00 2001 From: Brandon Cohen Date: Fri, 19 May 2023 17:01:10 -0400 Subject: [PATCH] refactor: request completion handling moved to entity --- server/entity/Media.ts | 35 +++++- server/entity/Season.ts | 34 +++++- server/entity/SeasonRequest.ts | 21 ++-- server/lib/availabilitySync.ts | 5 +- server/lib/scanners/baseScanner.ts | 162 +-------------------------- server/subscriber/MediaSubscriber.ts | 57 ++++------ 6 files changed, 106 insertions(+), 208 deletions(-) diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 2d1691724..d5c788741 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -1,6 +1,10 @@ import RadarrAPI from '@server/api/servarr/radarr'; import SonarrAPI from '@server/api/servarr/sonarr'; -import { MediaStatus, MediaType } from '@server/constants/media'; +import { + MediaRequestStatus, + MediaStatus, + MediaType, +} from '@server/constants/media'; import { getRepository } from '@server/datasource'; import type { DownloadingItem } from '@server/lib/downloadtracker'; import downloadTracker from '@server/lib/downloadtracker'; @@ -8,6 +12,7 @@ import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { AfterLoad, + AfterUpdate, Column, CreateDateColumn, Entity, @@ -309,6 +314,34 @@ class Media { } } } + + @AfterUpdate() + public async updateRelatedMediaRequest(): Promise { + const requestRepository = getRepository(MediaRequest); + + const relatedRequests = await requestRepository.find({ + relations: { + media: true, + }, + where: { + media: { id: this.id }, + status: MediaRequestStatus.APPROVED, + }, + }); + + if (relatedRequests.length > 0) { + relatedRequests.forEach((request) => { + if ( + this[request.is4k ? 'status4k' : 'status'] === + MediaStatus.AVAILABLE || + this[request.is4k ? 'status4k' : 'status'] === MediaStatus.DELETED + ) { + request.status = MediaRequestStatus.COMPLETED; + } + }); + requestRepository.save(relatedRequests); + } + } } export default Media; diff --git a/server/entity/Season.ts b/server/entity/Season.ts index 44a83d976..993fcea10 100644 --- a/server/entity/Season.ts +++ b/server/entity/Season.ts @@ -1,5 +1,8 @@ -import { MediaStatus } from '@server/constants/media'; +import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; +import { getRepository } from '@server/datasource'; +import SeasonRequest from '@server/entity/SeasonRequest'; import { + AfterUpdate, Column, CreateDateColumn, Entity, @@ -35,6 +38,35 @@ class Season { constructor(init?: Partial) { Object.assign(this, init); } + + @AfterUpdate() + public async updateSeasonRequests(): Promise { + const seasonRequestRepository = getRepository(SeasonRequest); + + const relatedSeasonRequests = await seasonRequestRepository.find({ + relations: { + request: true, + }, + where: { + request: { media: { id: (await this.media).id } }, + seasonNumber: this.seasonNumber, + }, + }); + + relatedSeasonRequests.forEach((seasonRequest) => { + if ( + this.seasonNumber === seasonRequest.seasonNumber && + ((!seasonRequest.request.is4k && + (this.status === MediaStatus.AVAILABLE || + this.status === MediaStatus.DELETED)) || + (seasonRequest.request.is4k && + this.status4k === MediaStatus.AVAILABLE) || + this.status4k === MediaStatus.DELETED) + ) + seasonRequest.status = MediaRequestStatus.COMPLETED; + }); + seasonRequestRepository.save(relatedSeasonRequests); + } } export default Season; diff --git a/server/entity/SeasonRequest.ts b/server/entity/SeasonRequest.ts index 984c3d3bc..c71445723 100644 --- a/server/entity/SeasonRequest.ts +++ b/server/entity/SeasonRequest.ts @@ -38,22 +38,21 @@ class SeasonRequest { } @AfterUpdate() - public async handleRemoveParent(): Promise { - const mediaRequestRepository = getRepository(MediaRequest); - const requestToBeDeleted = await mediaRequestRepository.findOneOrFail({ + public async updateMediaRequests(): Promise { + const requestRepository = getRepository(MediaRequest); + + const relatedRequest = await requestRepository.findOne({ where: { id: this.request.id }, }); - const allSeasonsAreCompleted = requestToBeDeleted.seasons.filter( - (season) => { - return season.status === MediaRequestStatus.COMPLETED; - } + const isRequestComplete = relatedRequest?.seasons.every( + (seasonRequest) => seasonRequest.status === MediaRequestStatus.COMPLETED ); - if (requestToBeDeleted.seasons.length === allSeasonsAreCompleted.length) { - await mediaRequestRepository.update(this.request.id, { - status: MediaRequestStatus.COMPLETED, - }); + if (isRequestComplete && relatedRequest) { + relatedRequest.status = MediaRequestStatus.COMPLETED; + + requestRepository.save(relatedRequest); } } } diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts index 398701bc4..4b1913001 100644 --- a/server/lib/availabilitySync.ts +++ b/server/lib/availabilitySync.ts @@ -3,7 +3,7 @@ import PlexAPI from '@server/api/plexapi'; import RadarrAPI, { type RadarrMovie } from '@server/api/servarr/radarr'; import type { SonarrSeason, SonarrSeries } from '@server/api/servarr/sonarr'; import SonarrAPI from '@server/api/servarr/sonarr'; -import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; +import { MediaStatus } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import Media from '@server/entity/Media'; import MediaRequest from '@server/entity/MediaRequest'; @@ -203,6 +203,9 @@ class AvailabilitySync { await this.mediaUpdater(media, true); } } + if (!mediaExists || didDeleteSeasons) { + await mediaRepository.save(media); + } } } catch (ex) { logger.error('Failed to complete availability sync.', { diff --git a/server/lib/scanners/baseScanner.ts b/server/lib/scanners/baseScanner.ts index 25d3898cc..439174f7f 100644 --- a/server/lib/scanners/baseScanner.ts +++ b/server/lib/scanners/baseScanner.ts @@ -1,19 +1,12 @@ import TheMovieDb from '@server/api/themoviedb'; -import { - MediaRequestStatus, - MediaStatus, - MediaType, -} from '@server/constants/media'; +import { MediaStatus, MediaType } 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 SeasonRequest from '@server/entity/SeasonRequest'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import AsyncLock from '@server/utils/asyncLock'; import { randomUUID } from 'crypto'; -import { In } from 'typeorm'; // Default scan rates (can be overidden) const BUNDLE_SIZE = 20; @@ -96,61 +89,6 @@ class BaseScanner { return existing; } - private async requestUpdater(mediaId: number, is4k: boolean) { - const requestRepository = getRepository(MediaRequest); - - const request = await requestRepository.find({ - relations: { - media: true, - }, - where: { - media: { id: mediaId }, - is4k: is4k, - status: MediaRequestStatus.APPROVED, - }, - }); - - const requestIds = request.map((request) => request.id); - - if (requestIds.length > 0) { - await requestRepository.update( - { id: In(requestIds) }, - { status: MediaRequestStatus.COMPLETED } - ); - } - } - - private async seasonRequestUpdater( - mediaId: number | undefined, - seasonNumber: number, - is4k: boolean - ) { - const seasonRequestRepository = getRepository(SeasonRequest); - - const seasonToBeCompleted = await seasonRequestRepository.findOne({ - relations: { - request: { - media: true, - }, - }, - where: { - request: { - is4k: is4k, - media: { - id: mediaId, - }, - }, - seasonNumber: seasonNumber, - }, - }); - - if (seasonToBeCompleted) { - await seasonRequestRepository.update(seasonToBeCompleted?.id, { - status: MediaRequestStatus.COMPLETED, - }); - } - } - protected async processMovie( tmdbId: number, { @@ -224,14 +162,6 @@ class BaseScanner { } if (changedExisting) { - if (existing.status === MediaStatus.AVAILABLE) { - this.requestUpdater(existing.id, false); - } - - if (existing.status4k === MediaStatus.AVAILABLE) { - this.requestUpdater(existing.id, true); - } - await mediaRepository.save(existing); this.log( @@ -275,14 +205,6 @@ class BaseScanner { is4k && this.enable4kMovie ? ratingKey : undefined; } - if (newMedia['status'] === MediaStatus.AVAILABLE) { - this.requestUpdater(newMedia.id, false); - } - - if (newMedia['status4k'] === MediaStatus.AVAILABLE && existing) { - this.requestUpdater(newMedia.id, true); - } - await mediaRepository.save(newMedia); this.log(`Saved new media: ${title}`); } @@ -381,22 +303,6 @@ class BaseScanner { existingSeason.status4k !== MediaStatus.DELETED ? MediaStatus.PROCESSING : existingSeason.status4k; - - if ( - (season.totalEpisodes === season.episodes && season.episodes > 0) || - existingSeason.status === MediaStatus.AVAILABLE - ) { - this.seasonRequestUpdater(media?.id, season.seasonNumber, false); - } - - if ( - (this.enable4kShow && - season.episodes4k === season.totalEpisodes && - season.episodes4k > 0) || - existingSeason.status4k === MediaStatus.AVAILABLE - ) { - this.seasonRequestUpdater(media?.id, season.seasonNumber, true); - } } else { newSeasons.push( new Season({ @@ -421,18 +327,6 @@ class BaseScanner { : MediaStatus.UNKNOWN, }) ); - - if (season.totalEpisodes === season.episodes && season.episodes > 0) { - this.seasonRequestUpdater(media?.id, season.seasonNumber, false); - } - - if ( - this.enable4kShow && - season.totalEpisodes === season.episodes4k && - season.episodes4k > 0 - ) { - this.seasonRequestUpdater(media?.id, season.seasonNumber, true); - } } } @@ -561,53 +455,6 @@ class BaseScanner { ? MediaStatus.DELETED : MediaStatus.UNKNOWN; - const seasonsCompleted = - seasons.length && - seasons.filter( - (season) => - season.episodes === season.totalEpisodes && season.episodes > 0 - ).length; - - const seasonsCompleted4k = - seasons.length && - seasons.filter( - (season) => - season.episodes4k === season.totalEpisodes && - season.episodes4k > 0 - ).length; - - const seasonRequestRepository = getRepository(SeasonRequest); - - const seasonRequestsCompleted = await seasonRequestRepository.find({ - relations: { - request: { - media: true, - }, - }, - where: { - request: { - is4k: is4k, - media: { - id: media.id, - }, - }, - status: MediaRequestStatus.COMPLETED, - }, - }); - - if ( - seasonsCompleted === seasonRequestsCompleted.length && - seasonRequestsCompleted.length > 0 - ) { - this.requestUpdater(media.id, false); - } - - if ( - seasonsCompleted4k === seasonRequestsCompleted.length && - seasonRequestsCompleted.length > 0 - ) { - this.requestUpdater(media.id, true); - } await mediaRepository.save(media); this.log(`Updating existing title: ${title}`); @@ -670,13 +517,6 @@ class BaseScanner { : MediaStatus.UNKNOWN, }); - if (isAllStandardSeasons) { - this.requestUpdater(newMedia.id, false); - } - - if (isAll4kSeasons && this.enable4kShow) { - this.requestUpdater(newMedia.id, true); - } await mediaRepository.save(newMedia); this.log(`Saved ${title}`); diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts index cc6444ad5..3e523250d 100644 --- a/server/subscriber/MediaSubscriber.ts +++ b/server/subscriber/MediaSubscriber.ts @@ -12,7 +12,7 @@ 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, Not } from 'typeorm'; +import { EventSubscriber, In } from 'typeorm'; @EventSubscriber() export class MediaSubscriber implements EntitySubscriberInterface { @@ -27,47 +27,39 @@ export class MediaSubscriber implements EntitySubscriberInterface { ) { if (entity.mediaType === MediaType.MOVIE) { const requestRepository = getRepository(MediaRequest); - const relatedRequests = await requestRepository.find({ + const relatedRequest = await requestRepository.findOne({ where: { media: { id: entity.id, }, is4k, - status: Not( - MediaRequestStatus.DECLINED && MediaRequestStatus.COMPLETED - ), + status: MediaRequestStatus.COMPLETED, }, + order: { id: 'DESC' }, }); - if (relatedRequests.length > 0) { - const tmdb = new TheMovieDb(); + const tmdb = new TheMovieDb(); + if (relatedRequest) { 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, - notifySystem: true, - 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, - } - ); + 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)', { @@ -118,10 +110,9 @@ export class MediaSubscriber implements EntitySubscriberInterface { id: entity.id, }, is4k, - status: Not( - MediaRequestStatus.DECLINED && MediaRequestStatus.COMPLETED - ), + status: MediaRequestStatus.COMPLETED, }, + order: { id: 'DESC' }, }); const request = requests.find( (request) =>