From e6e609200ca2b26bcdb684e1334c04d590c8c72f Mon Sep 17 00:00:00 2001 From: OwsleyJr Date: Tue, 8 Apr 2025 13:37:49 -0500 Subject: [PATCH] refactor: improve reliability of season request completion refactor: remove seasonrequest code --- server/entity/Media.ts | 79 +++++++++++++++- server/entity/Season.ts | 45 +-------- server/entity/SeasonRequest.ts | 24 ----- server/lib/availabilitySync.ts | 145 +++++++++++++++-------------- src/components/TvDetails/index.tsx | 2 - 5 files changed, 148 insertions(+), 147 deletions(-) diff --git a/server/entity/Media.ts b/server/entity/Media.ts index 510637e9c..2163564ae 100644 --- a/server/entity/Media.ts +++ b/server/entity/Media.ts @@ -6,6 +6,7 @@ import { 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'; @@ -318,12 +319,17 @@ 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 ( - this.status === MediaStatus.AVAILABLE || - this.status === MediaStatus.DELETED || - this.status4k === MediaStatus.AVAILABLE || - this.status4k === MediaStatus.DELETED + validStatuses.includes(this.status) || + validStatuses.includes(this.status4k) ) { const relatedRequests = await requestRepository.find({ relations: { @@ -338,16 +344,79 @@ class Media { // 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); } }); - requestRepository.save(relatedRequests); + + 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, + }) + ) + ); + } } } } diff --git a/server/entity/Season.ts b/server/entity/Season.ts index 80f442309..44a83d976 100644 --- a/server/entity/Season.ts +++ b/server/entity/Season.ts @@ -1,9 +1,5 @@ -import { MediaRequestStatus, MediaStatus } from '@server/constants/media'; -import { getRepository } from '@server/datasource'; -import SeasonRequest from '@server/entity/SeasonRequest'; +import { MediaStatus } from '@server/constants/media'; import { - AfterInsert, - AfterUpdate, Column, CreateDateColumn, Entity, @@ -39,45 +35,6 @@ class Season { constructor(init?: Partial) { Object.assign(this, init); } - - @AfterInsert() - @AfterUpdate() - public async updateSeasonRequests(): Promise { - const seasonRequestRepository = getRepository(SeasonRequest); - - if ( - this.status === MediaStatus.AVAILABLE || - this.status === MediaStatus.DELETED || - this.status4k === MediaStatus.AVAILABLE || - this.status4k === MediaStatus.DELETED - ) { - const relatedSeasonRequests = await seasonRequestRepository.find({ - relations: { - request: true, - }, - where: { - request: { media: { id: (await this.media).id } }, - seasonNumber: this.seasonNumber, - }, - }); - - // Check seasons when/if they become available or deleted, - // then set the related season request to completed - 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 f2416ded5..f9eeef501 100644 --- a/server/entity/SeasonRequest.ts +++ b/server/entity/SeasonRequest.ts @@ -1,7 +1,5 @@ import { MediaRequestStatus } from '@server/constants/media'; -import { getRepository } from '@server/datasource'; import { - AfterUpdate, Column, CreateDateColumn, Entity, @@ -36,28 +34,6 @@ class SeasonRequest { constructor(init?: Partial) { Object.assign(this, init); } - - @AfterUpdate() - public async updateMediaRequests(): Promise { - const requestRepository = getRepository(MediaRequest); - - const relatedRequest = await requestRepository.findOne({ - where: { id: this.request.id }, - }); - - // Check the parent of the season request and - // if every season request is complete - // set the parent request to complete as well - const isRequestComplete = relatedRequest?.seasons.every( - (seasonRequest) => seasonRequest.status === MediaRequestStatus.COMPLETED - ); - - if (isRequestComplete && relatedRequest) { - relatedRequest.status = MediaRequestStatus.COMPLETED; - - requestRepository.save(relatedRequest); - } - } } export default SeasonRequest; diff --git a/server/lib/availabilitySync.ts b/server/lib/availabilitySync.ts index f016a8529..7db24fec9 100644 --- a/server/lib/availabilitySync.ts +++ b/server/lib/availabilitySync.ts @@ -247,7 +247,7 @@ class AvailabilitySync { try { // If media type is tv, check if a season is processing - //to see if we need to keep the external metadata + // to see if we need to keep the external metadata let isMediaProcessing = false; if (media.mediaType === 'tv') { @@ -386,44 +386,44 @@ class AvailabilitySync { // Check for availability in all of the available radarr servers // If any find the media, we will assume the media exists - for (const server of this.radarrServers) { - if (server.is4k === is4k) { - const radarrAPI = new RadarrAPI({ - apiKey: server.apiKey, - url: RadarrAPI.buildUrl(server, '/api/v3'), - }); + for (const server of this.radarrServers.filter( + (server) => server.is4k === is4k + )) { + const radarrAPI = new RadarrAPI({ + apiKey: server.apiKey, + url: RadarrAPI.buildUrl(server, '/api/v3'), + }); - try { - let radarr: RadarrMovie | undefined; + try { + let radarr: RadarrMovie | undefined; - if (media.externalServiceId && !is4k) { - radarr = await radarrAPI.getMovie({ - id: media.externalServiceId, - }); - } + if (media.externalServiceId && !is4k) { + radarr = await radarrAPI.getMovie({ + id: media.externalServiceId, + }); + } - if (media.externalServiceId4k && is4k) { - radarr = await radarrAPI.getMovie({ - id: media.externalServiceId4k, - }); - } + if (media.externalServiceId4k && is4k) { + radarr = await radarrAPI.getMovie({ + id: media.externalServiceId4k, + }); + } - if (radarr && radarr.hasFile) { - existsInRadarr = true; - } - } catch (ex) { - if (!ex.message.includes('404')) { - existsInRadarr = true; - logger.debug( - `Failure retrieving the ${ - is4k ? '4K' : 'non-4K' - } movie [TMDB ID ${media.tmdbId}] from Radarr.`, - { - errorMessage: ex.message, - label: 'AvailabilitySync', - } - ); - } + if (radarr && radarr.hasFile) { + existsInRadarr = true; + } + } catch (ex) { + if (!ex.message.includes('404')) { + existsInRadarr = true; + logger.debug( + `Failure retrieving the ${is4k ? '4K' : 'non-4K'} movie [TMDB ID ${ + media.tmdbId + }] from Radarr.`, + { + errorMessage: ex.message, + label: 'AvailabilitySync', + } + ); } } } @@ -440,46 +440,45 @@ class AvailabilitySync { // Check for availability in all of the available sonarr servers // If any find the media, we will assume the media exists - for (const server of this.sonarrServers) { - if (server.is4k === is4k) { - const sonarrAPI = new SonarrAPI({ - apiKey: server.apiKey, - url: SonarrAPI.buildUrl(server, '/api/v3'), - }); + for (const server of this.sonarrServers.filter((server) => { + return server.is4k === is4k; + })) { + const sonarrAPI = new SonarrAPI({ + apiKey: server.apiKey, + url: SonarrAPI.buildUrl(server, '/api/v3'), + }); - try { - let sonarr: SonarrSeries | undefined; + try { + let sonarr: SonarrSeries | undefined; - if (media.externalServiceId && !is4k) { - sonarr = await sonarrAPI.getSeriesById(media.externalServiceId); - this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] = - sonarr.seasons; - } + if (media.externalServiceId && !is4k) { + sonarr = await sonarrAPI.getSeriesById(media.externalServiceId); + this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId}`] = + sonarr.seasons; + } - if (media.externalServiceId4k && is4k) { - sonarr = await sonarrAPI.getSeriesById(media.externalServiceId4k); - this.sonarrSeasonsCache[ - `${server.id}-${media.externalServiceId4k}` - ] = sonarr.seasons; - } + if (media.externalServiceId4k && is4k) { + sonarr = await sonarrAPI.getSeriesById(media.externalServiceId4k); + this.sonarrSeasonsCache[`${server.id}-${media.externalServiceId4k}`] = + sonarr.seasons; + } - if (sonarr && sonarr.statistics.episodeFileCount > 0) { - existsInSonarr = true; - } - } catch (ex) { - if (!ex.message.includes('404')) { - existsInSonarr = true; - preventSeasonSearch = true; - logger.debug( - `Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${ - media.tmdbId - }] from Sonarr.`, - { - errorMessage: ex.message, - label: 'AvailabilitySync', - } - ); - } + if (sonarr && sonarr.statistics.episodeFileCount > 0) { + existsInSonarr = true; + } + } catch (ex) { + if (!ex.message.includes('404')) { + existsInSonarr = true; + preventSeasonSearch = true; + logger.debug( + `Failure retrieving the ${is4k ? '4K' : 'non-4K'} show [TMDB ID ${ + media.tmdbId + }] from Sonarr.`, + { + errorMessage: ex.message, + label: 'AvailabilitySync', + } + ); } } } @@ -523,7 +522,9 @@ class AvailabilitySync { // Check each sonarr instance to see if the media still exists // If found, we will assume the media exists and prevent removal // We can use the cache we built when we fetched the series with mediaExistsInSonarr - for (const server of this.sonarrServers) { + for (const server of this.sonarrServers.filter( + (server) => server.is4k === is4k + )) { let sonarrSeasons: SonarrSeason[] | undefined; if (media.externalServiceId && !is4k) { diff --git a/src/components/TvDetails/index.tsx b/src/components/TvDetails/index.tsx index 6d07fa326..ad1b834fd 100644 --- a/src/components/TvDetails/index.tsx +++ b/src/components/TvDetails/index.tsx @@ -584,8 +584,6 @@ const TvDetails = ({ tv }: TvDetailsProps) => { return null; } - console.log({ request }); - return ( {({ open }) => (