diff --git a/apps/server/src/modules/collections/collection-handler.spec.ts b/apps/server/src/modules/collections/collection-handler.spec.ts index cdbb1f0cc..768ad85e1 100644 --- a/apps/server/src/modules/collections/collection-handler.spec.ts +++ b/apps/server/src/modules/collections/collection-handler.spec.ts @@ -241,7 +241,7 @@ describe('CollectionHandler', () => { expect(seerrApi.removeSeasonRequest).toHaveBeenCalledTimes(1); }); - it('should call removeSeasonRequest for episodes', async () => { + it('does not mutate Seerr requests for episodes (no per-episode request granularity)', async () => { const collection = createCollection({ arrAction: ServarrAction.DELETE, forceSeerr: true, @@ -263,11 +263,10 @@ describe('CollectionHandler', () => { collectionHandler.handleMedia(collection, collectionMedia), ).resolves.toBe(true); - expect(seerrApi.removeSeasonRequest).toHaveBeenCalledWith( - collectionMedia.tmdbId, - collectionMedia.mediaData.parentIndex, - ); - expect(seerrApi.removeSeasonRequest).toHaveBeenCalledTimes(1); + // Removing one episode must not delete the whole season's request (which + // covers the still-present episodes); rely on Seerr's availability sync. + expect(seerrApi.removeSeasonRequest).not.toHaveBeenCalled(); + expect(seerrApi.removeMediaByTmdbId).not.toHaveBeenCalled(); }); it('should not mutate Seerr requests for DELETE_SHOW_IF_EMPTY season actions', async () => { diff --git a/apps/server/src/modules/collections/collection-handler.ts b/apps/server/src/modules/collections/collection-handler.ts index 54f8c0ad0..e1c5ae936 100644 --- a/apps/server/src/modules/collections/collection-handler.ts +++ b/apps/server/src/modules/collections/collection-handler.ts @@ -144,20 +144,16 @@ export class CollectionHandler { break; } case 'episode': { - const mediaDataEpisode = await mediaServer.getMetadata( - media.mediaServerId, + // Seerr tracks requests per season, not per episode, so there is + // no per-episode request to remove — deleting the season request + // would drop the request for every other (still-present) episode + // in that season. Skip the force-removal and let Seerr's + // availability sync reconcile, as it does when Force Seerr is off. + // The UI hides the toggle for episode rules; this also guards + // existing collections that still have it set. + this.logger.debug( + `[Seerr] Skipping request removal for episode-level collection '${collection.title}' (TMDB ID '${tmdbId}'): Seerr has no per-episode request granularity. Relying on availability sync.`, ); - - if (mediaDataEpisode?.parentIndex !== undefined) { - await this.seerrApi.removeSeasonRequest( - tmdbId, - mediaDataEpisode.parentIndex, - ); - - this.logger.log( - `[Seerr] Removed request of season ${mediaDataEpisode.parentIndex} from show with TMDB ID '${tmdbId}'. Because episode ${mediaDataEpisode.index} was removed.`, - ); - } break; } default: diff --git a/apps/server/src/modules/rules/rules.service.ts b/apps/server/src/modules/rules/rules.service.ts index fdd2afe3c..d3303c6ea 100644 --- a/apps/server/src/modules/rules/rules.service.ts +++ b/apps/server/src/modules/rules/rules.service.ts @@ -310,6 +310,19 @@ export class RulesService { } } + // Resolve the collection's media type: a movie library is always 'movie'; + // a TV library uses the rule group's selected dataType (show/season/episode), + // defaulting to 'show'. + private resolveCollectionType( + libType: MediaItemType, + params: RulesDto, + ): MediaItemType { + if (libType === 'movie') { + return 'movie'; + } + return params.dataType !== undefined ? params.dataType : 'show'; + } + async setRules(params: RulesDto) { try { let state: ReturnStatus = this.createReturnStatus(true, 'Success'); @@ -344,21 +357,22 @@ export class RulesService { const lib = (await mediaServer.getLibraries()).find( (el) => el.id === params.libraryId, ); + const collectionType = this.resolveCollectionType(lib.type, params); const collection = ( await this.collectionService.createCollection({ libraryId: params.libraryId, - type: - lib.type === 'movie' - ? 'movie' - : params.dataType !== undefined - ? params.dataType - : 'show', + type: collectionType, title: params.name, description: params.description, arrAction: params.arrAction ? params.arrAction : 0, isActive: params.isActive, listExclusions: params.listExclusions ? params.listExclusions : false, - forceSeerr: params.forceSeerr ? params.forceSeerr : false, + // Force Seerr is unsupported for episode rules (Seerr has no + // per-episode request granularity), so never persist it enabled. The + // UI hides the toggle; this also clears the flag on re-save for rules + // created before it was hidden. + forceSeerr: + collectionType !== 'episode' && params.forceSeerr ? true : false, tautulliWatchedPercentOverride: params.tautulliWatchedPercentOverride ?? null, radarrSettingsId: params.radarrSettingsId ?? null, @@ -518,20 +532,21 @@ export class RulesService { (el) => el.id === params.libraryId, ); + const collectionType = this.resolveCollectionType(lib.type, params); const collectionData = { libraryId: params.libraryId, - type: - lib.type === 'movie' - ? 'movie' - : params.dataType !== undefined - ? params.dataType - : 'show', + type: collectionType, title: params.name, description: params.description, arrAction: params.arrAction ? params.arrAction : 0, isActive: params.isActive, listExclusions: params.listExclusions ? params.listExclusions : false, - forceSeerr: params.forceSeerr ? params.forceSeerr : false, + // Force Seerr is unsupported for episode rules (Seerr has no + // per-episode request granularity), so never persist it enabled. The + // UI hides the toggle; this also clears the flag on re-save for rules + // created before it was hidden. + forceSeerr: + collectionType !== 'episode' && params.forceSeerr ? true : false, tautulliWatchedPercentOverride: params.tautulliWatchedPercentOverride ?? null, radarrSettingsId: params.radarrSettingsId ?? null, diff --git a/apps/ui/src/components/Rules/RuleGroup/AddModal/index.tsx b/apps/ui/src/components/Rules/RuleGroup/AddModal/index.tsx index fd99d13af..c3c9d7079 100644 --- a/apps/ui/src/components/Rules/RuleGroup/AddModal/index.tsx +++ b/apps/ui/src/components/Rules/RuleGroup/AddModal/index.tsx @@ -1350,7 +1350,7 @@ const AddModal = (props: AddModal) => { )} - {seerrEnabled && ( + {seerrEnabled && selectedType !== 'episode' && (