diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 2163564ae..2d1691724 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -1,19 +1,13 @@ import RadarrAPI from '@server/api/servarr/radarr'; import SonarrAPI from '@server/api/servarr/sonarr'; -import { - MediaRequestStatus, - MediaStatus, - MediaType, -} from '@server/constants/media'; +import { MediaStatus, MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; -import SeasonRequest from '@server/entity/SeasonRequest'; import type { DownloadingItem } from '@server/lib/downloadtracker'; import downloadTracker from '@server/lib/downloadtracker'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; import { AfterLoad, - AfterUpdate, Column, CreateDateColumn, Entity, @@ -315,111 +309,6 @@ class Media { } } } - - @AfterUpdate() - public async updateRelatedMediaRequest(): Promise { - const requestRepository = getRepository(MediaRequest); - const seasonRequestRepository = getRepository(SeasonRequest); - - const validStatuses = [ - MediaStatus.PARTIALLY_AVAILABLE, - MediaStatus.AVAILABLE, - MediaStatus.DELETED, - ]; - - if ( - validStatuses.includes(this.status) || - validStatuses.includes(this.status4k) - ) { - const relatedRequests = await requestRepository.find({ - relations: { - media: true, - }, - where: { - media: { id: this.id }, - status: MediaRequestStatus.APPROVED, - }, - }); - - // Check the media entity status and if available - // or deleted, set the related request to completed - if (relatedRequests.length > 0) { - const completedRequests: MediaRequest[] = []; - - relatedRequests.forEach((request) => { - let shouldComplete = false; - - if ( - this[request.is4k ? 'status4k' : 'status'] === - MediaStatus.AVAILABLE || - this[request.is4k ? 'status4k' : 'status'] === MediaStatus.DELETED - ) { - shouldComplete = true; - } else if (this.mediaType === 'tv') { - // For TV, check if all requested seasons are available or deleted - const allSeasonsReady = request.seasons.every((requestSeason) => { - const matchingSeason = this.seasons.find( - (mediaSeason) => - mediaSeason.seasonNumber === requestSeason.seasonNumber - ); - - if (!matchingSeason) { - return false; - } - - return ( - matchingSeason[request.is4k ? 'status4k' : 'status'] === - MediaStatus.AVAILABLE || - matchingSeason[request.is4k ? 'status4k' : 'status'] === - MediaStatus.DELETED - ); - }); - - shouldComplete = allSeasonsReady; - } - - if (shouldComplete) { - request.status = MediaRequestStatus.COMPLETED; - completedRequests.push(request); - } - }); - - await requestRepository.save(completedRequests); - - // Handle season requests and mark them completed when - // that specific season becomes available - if (this.mediaType === 'tv') { - const seasonsToUpdate = relatedRequests.flatMap((request) => { - return request.seasons.filter((requestSeason) => { - const matchingSeason = this.seasons.find( - (mediaSeason) => - mediaSeason.seasonNumber === requestSeason.seasonNumber - ); - - if (!matchingSeason) { - return false; - } - - return ( - matchingSeason[request.is4k ? 'status4k' : 'status'] === - MediaStatus.AVAILABLE || - matchingSeason[request.is4k ? 'status4k' : 'status'] === - MediaStatus.DELETED - ); - }); - }); - - await Promise.all( - seasonsToUpdate.map((season) => - seasonRequestRepository.update(season.id, { - status: MediaRequestStatus.COMPLETED, - }) - ) - ); - } - } - } - } } export default Media; diff --git a/server/subscriber/MediaSubscriber.ts b/server/subscriber/MediaSubscriber.ts index b73e4ecbd..9ec002fe2 100644 --- a/server/subscriber/MediaSubscriber.ts +++ b/server/subscriber/MediaSubscriber.ts @@ -1,7 +1,13 @@ -import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; +import { + MediaRequestStatus, + 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 type { EntitySubscriberInterface, UpdateEvent } from 'typeorm'; import { EventSubscriber } from 'typeorm'; @@ -25,7 +31,101 @@ export class MediaSubscriber implements EntitySubscriberInterface { } } - public beforeUpdate(event: UpdateEvent): void { + private async updateRelatedMediaRequest(event: Media, is4k: boolean) { + const requestRepository = getRepository(MediaRequest); + const seasonRequestRepository = getRepository(SeasonRequest); + + const relatedRequests = await requestRepository.find({ + relations: { + media: true, + }, + where: { + media: { id: event.id }, + status: MediaRequestStatus.APPROVED, + is4k, + }, + }); + + // Check the media entity status and if available + // or deleted, set the related request to completed + if (relatedRequests.length > 0) { + const completedRequests: MediaRequest[] = []; + + relatedRequests.forEach((request) => { + let shouldComplete = false; + + if ( + event[request.is4k ? 'status4k' : 'status'] === + MediaStatus.AVAILABLE || + event[request.is4k ? 'status4k' : 'status'] === MediaStatus.DELETED + ) { + shouldComplete = true; + } else if (event.mediaType === 'tv') { + // For TV, check if all requested seasons are available or deleted + const allSeasonsReady = request.seasons.every((requestSeason) => { + const matchingSeason = event.seasons.find( + (mediaSeason) => + mediaSeason.seasonNumber === requestSeason.seasonNumber + ); + + if (!matchingSeason) { + return false; + } + + return ( + matchingSeason[request.is4k ? 'status4k' : 'status'] === + MediaStatus.AVAILABLE || + matchingSeason[request.is4k ? 'status4k' : 'status'] === + MediaStatus.DELETED + ); + }); + + shouldComplete = allSeasonsReady; + } + + if (shouldComplete) { + request.status = MediaRequestStatus.COMPLETED; + completedRequests.push(request); + } + }); + + await requestRepository.save(completedRequests); + + // Handle season requests and mark them completed when + // that specific season becomes available + if (event.mediaType === 'tv') { + const seasonsToUpdate = relatedRequests.flatMap((request) => { + return request.seasons.filter((requestSeason) => { + const matchingSeason = event.seasons.find( + (mediaSeason) => + mediaSeason.seasonNumber === requestSeason.seasonNumber + ); + + if (!matchingSeason) { + return false; + } + + return ( + matchingSeason[request.is4k ? 'status4k' : 'status'] === + MediaStatus.AVAILABLE || + matchingSeason[request.is4k ? 'status4k' : 'status'] === + MediaStatus.DELETED + ); + }); + }); + + await Promise.all( + seasonsToUpdate.map((season) => + seasonRequestRepository.update(season.id, { + status: MediaRequestStatus.COMPLETED, + }) + ) + ); + } + } + } + + public async beforeUpdate(event: UpdateEvent): Promise { if (!event.entity) { return; } @@ -43,6 +143,57 @@ export class MediaSubscriber implements EntitySubscriberInterface { ) { this.updateChildRequestStatus(event.entity as Media, true); } + + // Manually load related seasons into databaseEntity + // for seasonStatusCheck in afterUpdate + const seasons = await event.manager + .getRepository(Season) + .createQueryBuilder('season') + .leftJoin('season.media', 'media') + .where('media.id = :id', { id: event.databaseEntity.id }) + .getMany(); + + event.databaseEntity.seasons = seasons; + } + + public async afterUpdate(event: UpdateEvent): Promise { + if (!event.entity) { + return; + } + + const validStatuses = [ + MediaStatus.PARTIALLY_AVAILABLE, + MediaStatus.AVAILABLE, + MediaStatus.DELETED, + ]; + + const seasonStatusCheck = (is4k: boolean) => { + return event.entity?.seasons?.some((season: Season, index: number) => { + const previousSeason = event.databaseEntity.seasons[index]; + + return ( + season[is4k ? 'status4k' : 'status'] !== + previousSeason[is4k ? 'status4k' : 'status'] + ); + }); + }; + + if ( + (event.entity.status !== event.databaseEntity?.status || + (event.entity.mediaType === MediaType.TV && + seasonStatusCheck(false))) && + validStatuses.includes(event.entity.status) + ) { + this.updateRelatedMediaRequest(event.entity as Media, false); + } + + if ( + (event.entity.status4k !== event.databaseEntity?.status4k || + (event.entity.mediaType === MediaType.TV && seasonStatusCheck(true))) && + validStatuses.includes(event.entity.status4k) + ) { + this.updateRelatedMediaRequest(event.entity as Media, true); + } } public listenTo(): typeof Media {