From 7ec5123cd0e3143145a08762ce07c7070e299be5 Mon Sep 17 00:00:00 2001 From: Gauthier Date: Fri, 21 Nov 2025 22:15:17 +0100 Subject: [PATCH] feat(overriderules): apply override rules to advanced requests This PR apply the override rules to the Advanced Request Modal --- seerr-api.yml | 26 ++++ server/entity/MediaRequest.ts | 131 ++---------------- server/routes/overrideRule.ts | 40 ++++++ .../RequestModal/AdvancedRequester/index.tsx | 33 +++++ .../RequestModal/MovieRequestModal.tsx | 2 + .../RequestModal/TvRequestModal.tsx | 1 + 6 files changed, 116 insertions(+), 117 deletions(-) diff --git a/seerr-api.yml b/seerr-api.yml index bf9d88271..4c287966b 100644 --- a/seerr-api.yml +++ b/seerr-api.yml @@ -7755,6 +7755,32 @@ paths: application/json: schema: $ref: '#/components/schemas/OverrideRule' + /overrideRule/advancedRequest: + post: + summary: Advanced override rule request + description: Processes an advanced override rule request. + tags: + - overriderule + responses: + '200': + description: Advanced override rule request processed + content: + application/json: + schema: + type: object + properties: + rootFolder: + type: string + nullable: true + profileId: + type: number + nullable: true + tags: + type: array + items: + type: number + nullable: true + security: - cookieAuth: [] - apiKey: [] diff --git a/server/entity/MediaRequest.ts b/server/entity/MediaRequest.ts index cdfa17c3a..6e9ead33e 100644 --- a/server/entity/MediaRequest.ts +++ b/server/entity/MediaRequest.ts @@ -1,15 +1,13 @@ import TheMovieDb from '@server/api/themoviedb'; -import { ANIME_KEYWORD_ID } from '@server/api/themoviedb/constants'; -import type { TmdbKeyword } from '@server/api/themoviedb/interfaces'; import { MediaRequestStatus, MediaStatus, MediaType, } from '@server/constants/media'; import { getRepository } from '@server/datasource'; -import OverrideRule from '@server/entity/OverrideRule'; import type { MediaRequestBody } from '@server/interfaces/api/requestInterfaces'; import notificationManager, { Notification } from '@server/lib/notifications'; +import overrideRules from '@server/lib/overrideRules'; import { Permission } from '@server/lib/permissions'; import { getSettings } from '@server/lib/settings'; import logger from '@server/logger'; @@ -211,121 +209,20 @@ export class MediaRequest { let tags = requestBody.tags; if (useOverrides) { - const defaultRadarrId = requestBody.is4k - ? settings.radarr.findIndex((r) => r.is4k && r.isDefault) - : settings.radarr.findIndex((r) => !r.is4k && r.isDefault); - const defaultSonarrId = requestBody.is4k - ? settings.sonarr.findIndex((s) => s.is4k && s.isDefault) - : settings.sonarr.findIndex((s) => !s.is4k && s.isDefault); - - const overrideRuleRepository = getRepository(OverrideRule); - const overrideRules = await overrideRuleRepository.find({ - where: - requestBody.mediaType === MediaType.MOVIE - ? { radarrServiceId: defaultRadarrId } - : { sonarrServiceId: defaultSonarrId }, + const overrideRulesResult = await overrideRules({ + mediaType: requestBody.mediaType, + is4k: requestBody.is4k || false, + tmdbMedia, + requestUser, }); - - const appliedOverrideRules = overrideRules.filter((rule) => { - const hasAnimeKeyword = - 'results' in tmdbMedia.keywords && - tmdbMedia.keywords.results.some( - (keyword: TmdbKeyword) => keyword.id === ANIME_KEYWORD_ID - ); - - // Skip override rules if the media is an anime TV show as anime TV - // is handled by default and override rules do not explicitly include - // the anime keyword - if ( - requestBody.mediaType === MediaType.TV && - hasAnimeKeyword && - (!rule.keywords || - !rule.keywords.split(',').map(Number).includes(ANIME_KEYWORD_ID)) - ) { - return false; - } - - if ( - rule.users && - !rule.users - .split(',') - .some((userId) => Number(userId) === requestUser.id) - ) { - return false; - } - if ( - rule.genre && - !rule.genre - .split(',') - .some((genreId) => - tmdbMedia.genres - .map((genre) => genre.id) - .includes(Number(genreId)) - ) - ) { - return false; - } - if ( - rule.language && - !rule.language - .split('|') - .some((languageId) => languageId === tmdbMedia.original_language) - ) { - return false; - } - if ( - rule.keywords && - !rule.keywords.split(',').some((keywordId) => { - let keywordList: TmdbKeyword[] = []; - - if ('keywords' in tmdbMedia.keywords) { - keywordList = tmdbMedia.keywords.keywords; - } else if ('results' in tmdbMedia.keywords) { - keywordList = tmdbMedia.keywords.results; - } - - return keywordList - .map((keyword: TmdbKeyword) => keyword.id) - .includes(Number(keywordId)); - }) - ) { - return false; - } - return true; - }); - - // hacky way to prioritize rules - // TODO: make this better - const prioritizedRule = appliedOverrideRules.sort((a, b) => { - const keys: (keyof OverrideRule)[] = ['genre', 'language', 'keywords']; - - const aSpecificity = keys.filter((key) => a[key] !== null).length; - const bSpecificity = keys.filter((key) => b[key] !== null).length; - - // Take the rule with the most specific condition first - return bSpecificity - aSpecificity; - })[0]; - - if (prioritizedRule) { - if (prioritizedRule.rootFolder) { - rootFolder = prioritizedRule.rootFolder; - } - if (prioritizedRule.profileId) { - profileId = prioritizedRule.profileId; - } - if (prioritizedRule.tags) { - tags = [ - ...new Set([ - ...(tags || []), - ...prioritizedRule.tags.split(',').map((tag) => Number(tag)), - ]), - ]; - } - - logger.debug('Override rule applied.', { - label: 'Media Request', - overrides: prioritizedRule, - }); + if (overrideRulesResult.rootFolder) { + rootFolder = overrideRulesResult.rootFolder; + } + if (overrideRulesResult.profileId) { + profileId = overrideRulesResult.profileId; + } + if (overrideRulesResult.tags) { + tags = overrideRulesResult.tags; } } diff --git a/server/routes/overrideRule.ts b/server/routes/overrideRule.ts index 912a68aae..8f8f10284 100644 --- a/server/routes/overrideRule.ts +++ b/server/routes/overrideRule.ts @@ -1,6 +1,12 @@ +import TheMovieDb from '@server/api/themoviedb'; +import { MediaType } from '@server/constants/media'; import { getRepository } from '@server/datasource'; import OverrideRule from '@server/entity/OverrideRule'; +import { User } from '@server/entity/User'; import type { OverrideRuleResultsResponse } from '@server/interfaces/api/overrideRuleInterfaces'; +import overrideRules, { + type OverrideRulesResult, +} from '@server/lib/overrideRules'; import { Permission } from '@server/lib/permissions'; import { isAuthenticated } from '@server/middleware/auth'; import { Router } from 'express'; @@ -61,6 +67,40 @@ overrideRuleRoutes.post< } }); +overrideRuleRoutes.post( + '/advancedRequest', + isAuthenticated(Permission.REQUEST_ADVANCED), + async (req, res, next) => { + try { + const tmdb = new TheMovieDb(); + const tmdbMedia = + req.body.mediaType === MediaType.MOVIE + ? await tmdb.getMovie({ movieId: req.body.tmdbId }) + : await tmdb.getTvShow({ tvId: req.body.tmdbId }); + + const userRepository = getRepository(User); + const user = await userRepository.findOne({ + where: { id: req.body.requestUser }, + relations: { requests: true }, + }); + if (!user) { + return next({ status: 404, message: 'User not found.' }); + } + + const overrideRulesResult: OverrideRulesResult = await overrideRules({ + mediaType: req.body.mediaType, + is4k: req.body.is4k, + tmdbMedia, + requestUser: user, + }); + + res.status(200).json(overrideRulesResult); + } catch { + next({ status: 404, message: 'Media not found' }); + } + } +); + overrideRuleRoutes.put< { ruleId: string }, OverrideRule, diff --git a/src/components/RequestModal/AdvancedRequester/index.tsx b/src/components/RequestModal/AdvancedRequester/index.tsx index ad11db82e..7f5763af3 100644 --- a/src/components/RequestModal/AdvancedRequester/index.tsx +++ b/src/components/RequestModal/AdvancedRequester/index.tsx @@ -13,7 +13,9 @@ import type { ServiceCommonServerWithDetails, } from '@server/interfaces/api/serviceInterfaces'; import type { UserResultsResponse } from '@server/interfaces/api/userInterfaces'; +import type { OverrideRulesResult } from '@server/lib/overrideRules'; import { hasPermission } from '@server/lib/permissions'; +import axios from 'axios'; import { isEqual } from 'lodash'; import { useEffect, useMemo, useState } from 'react'; import { useIntl } from 'react-intl'; @@ -51,6 +53,7 @@ export type RequestOverrides = { interface AdvancedRequesterProps { type: 'movie' | 'tv'; + tmdbId?: number; is4k: boolean; isAnime?: boolean; defaultOverrides?: RequestOverrides; @@ -60,6 +63,7 @@ interface AdvancedRequesterProps { const AdvancedRequester = ({ type, + tmdbId, is4k = false, isAnime = false, defaultOverrides, @@ -284,6 +288,35 @@ const AdvancedRequester = ({ selectedTags, ]); + useEffect(() => { + (async () => { + if (tmdbId) { + try { + const { data: override } = await axios.post( + '/api/v1/overrideRule/advancedRequest', + { + mediaType: type, + is4k, + requestUser: requestUser?.id ?? currentUser?.id, + tmdbId, + } + ); + if (override.rootFolder) { + setSelectedFolder(override.rootFolder); + } + if (override.profileId) { + setSelectedProfile(override.profileId); + } + if (override.tags) { + setSelectedTags(override.tags); + } + } catch { + /* empty */ + } + } + })(); + }, [serverData, is4k, requestUser]); + if (!data && !error) { return (
diff --git a/src/components/RequestModal/MovieRequestModal.tsx b/src/components/RequestModal/MovieRequestModal.tsx index 134a937f6..969aa33ca 100644 --- a/src/components/RequestModal/MovieRequestModal.tsx +++ b/src/components/RequestModal/MovieRequestModal.tsx @@ -288,6 +288,7 @@ const MovieRequestModal = ({ hasPermission(Permission.MANAGE_REQUESTS)) && ( { diff --git a/src/components/RequestModal/TvRequestModal.tsx b/src/components/RequestModal/TvRequestModal.tsx index 5d2249de8..bc09c6f27 100644 --- a/src/components/RequestModal/TvRequestModal.tsx +++ b/src/components/RequestModal/TvRequestModal.tsx @@ -722,6 +722,7 @@ const TvRequestModal = ({ hasPermission(Permission.MANAGE_REQUESTS)) && ( keyword.id === ANIME_KEYWORD_ID