fix(missing items filtering): adds RT audience as missing item filter

fix #246
This commit is contained in:
Tom Wheeler
2026-01-01 09:03:57 +13:00
parent 82a04d33fe
commit 424c8f2347
11 changed files with 280 additions and 18 deletions

View File

@@ -528,6 +528,14 @@ components:
minimum: 0
maximum: 100
nullable: true
minimumRottenTomatoesAudienceRating:
type: number
format: float
description: 'Only process movies/TV shows with Rotten Tomatoes audience score >= this value (0 = no limit)'
example: 75
minimum: 0
maximum: 100
nullable: true
excludedGenres:
type: array
items:
@@ -1083,6 +1091,14 @@ components:
minimum: 0
maximum: 100
nullable: true
minimumRottenTomatoesAudienceRating:
type: number
format: float
description: 'Only process movies/TV shows with Rotten Tomatoes audience score >= this value (0 = no limit)'
example: 75
minimum: 0
maximum: 100
nullable: true
excludedGenres:
type: array
items:
@@ -1791,6 +1807,14 @@ components:
minimum: 0
maximum: 100
nullable: true
minimumRottenTomatoesAudienceRating:
type: number
format: float
description: 'Only process movies/TV shows with Rotten Tomatoes audience score >= this value (0 = no limit)'
example: 75
minimum: 0
maximum: 100
nullable: true
excludedGenres:
type: array
items:

View File

@@ -14,14 +14,18 @@ export interface FilteredMissingItemsResult {
filteredItems: MissingItem[];
/** IMDb ratings map for filtered items (tmdbId -> rating) */
imdbRatingsMap: Map<number, number | null>;
/** Rotten Tomatoes ratings map for filtered items (tmdbId -> critics score) */
/** Rotten Tomatoes critics ratings map for filtered items (tmdbId -> critics score) */
rtRatingsMap: Map<number, number | null>;
/** Rotten Tomatoes audience ratings map for filtered items (tmdbId -> audience score) */
rtAudienceRatingsMap: Map<number, number | null>;
/** Items filtered by year */
yearFilteredItems: string[];
/** Items filtered by low IMDb rating */
lowRatedItems: string[];
/** Items filtered by low Rotten Tomatoes rating */
/** Items filtered by low Rotten Tomatoes critics rating */
lowRatedRTItems: string[];
/** Items filtered by low Rotten Tomatoes audience rating */
lowRatedRTAudienceItems: string[];
/** Items filtered by excluded genres */
excludedGenreItems: string[];
/** Items filtered by excluded countries */
@@ -69,6 +73,7 @@ export class MissingItemFilterService {
const yearFilteredItems: string[] = [];
const lowRatedItems: string[] = [];
const lowRatedRTItems: string[] = [];
const lowRatedRTAudienceItems: string[] = [];
const excludedGenreItems: string[] = [];
const excludedCountryItems: string[] = [];
const excludedLanguageItems: string[] = [];
@@ -136,13 +141,17 @@ export class MissingItemFilterService {
// Step 2.5: Fetch Rotten Tomatoes ratings if filter is enabled
const rtRatingsMap = new Map<number, number | null>(); // tmdbId -> critics score
const rtAudienceRatingsMap = new Map<number, number | null>(); // tmdbId -> audience score
if (
config.minimumRottenTomatoesRating &&
config.minimumRottenTomatoesRating > 0
(config.minimumRottenTomatoesRating &&
config.minimumRottenTomatoesRating > 0) ||
(config.minimumRottenTomatoesAudienceRating &&
config.minimumRottenTomatoesAudienceRating > 0)
) {
await this.fetchRTRatings(
yearFilteredMissingItems,
rtRatingsMap,
rtAudienceRatingsMap,
config,
serviceLabel
);
@@ -187,7 +196,7 @@ export class MissingItemFilterService {
// If not in map (no IMDb ID found), allow the item (continue processing)
}
// Check Rotten Tomatoes rating filter using cached ratings
// Check Rotten Tomatoes critics rating filter using cached ratings
if (
config.minimumRottenTomatoesRating &&
config.minimumRottenTomatoesRating > 0
@@ -198,7 +207,7 @@ export class MissingItemFilterService {
// If score is null or undefined (no rating found), allow the item
if (score === null || score === undefined) {
logger.debug(
`No Rotten Tomatoes rating found for ${item.title}, allowing item`,
`No Rotten Tomatoes critics rating found for ${item.title}, allowing item`,
{
label: serviceLabel,
tmdbId: item.tmdbId,
@@ -208,7 +217,7 @@ export class MissingItemFilterService {
} else if (score < config.minimumRottenTomatoesRating) {
// Score exists but below threshold
logger.debug(
`${item.title} RT score ${score} below minimum ${config.minimumRottenTomatoesRating}`,
`${item.title} RT critics score ${score} below minimum ${config.minimumRottenTomatoesRating}`,
{
label: serviceLabel,
tmdbId: item.tmdbId,
@@ -225,6 +234,44 @@ export class MissingItemFilterService {
// If not in map (no RT rating found), allow the item (continue processing)
}
// Check Rotten Tomatoes audience rating filter using cached ratings
if (
config.minimumRottenTomatoesAudienceRating &&
config.minimumRottenTomatoesAudienceRating > 0
) {
if (rtAudienceRatingsMap.has(item.tmdbId)) {
const score = rtAudienceRatingsMap.get(item.tmdbId);
// If score is null or undefined (no rating found), allow the item
if (score === null || score === undefined) {
logger.debug(
`No Rotten Tomatoes audience rating found for ${item.title}, allowing item`,
{
label: serviceLabel,
tmdbId: item.tmdbId,
title: item.title,
}
);
} else if (score < config.minimumRottenTomatoesAudienceRating) {
// Score exists but below threshold
logger.debug(
`${item.title} RT audience score ${score} below minimum ${config.minimumRottenTomatoesAudienceRating}`,
{
label: serviceLabel,
tmdbId: item.tmdbId,
title: item.title,
score,
minimumScore: config.minimumRottenTomatoesAudienceRating,
}
);
lowRatedRTAudienceItems.push(item.title);
continue;
}
// else: score >= minimum, allow the item (continue processing)
}
// If not in map (no RT rating found), allow the item (continue processing)
}
// Check genre filter (supports both include and exclude modes)
const genreFilter = this.getGenreFilter(config);
if (genreFilter && genreFilter.values.length > 0) {
@@ -305,9 +352,11 @@ export class MissingItemFilterService {
filteredItems: fullyFilteredItems,
imdbRatingsMap,
rtRatingsMap,
rtAudienceRatingsMap,
yearFilteredItems,
lowRatedItems,
lowRatedRTItems,
lowRatedRTAudienceItems,
excludedGenreItems,
excludedCountryItems,
excludedLanguageItems,
@@ -408,6 +457,7 @@ export class MissingItemFilterService {
private async fetchRTRatings(
items: MissingItem[],
ratingsMap: Map<number, number | null>,
audienceRatingsMap: Map<number, number | null>,
config: CollectionConfig,
serviceLabel: string
): Promise<void> {
@@ -426,6 +476,7 @@ export class MissingItemFilterService {
items.map(async (item) => {
try {
let rtRating = null;
let audienceScore = null;
if (item.mediaType === 'movie' && item.year) {
const rating = await this.rtAPI.getMovieRatings(
@@ -433,31 +484,48 @@ export class MissingItemFilterService {
item.year
);
rtRating = rating?.criticsScore ?? null;
audienceScore = rating?.audienceScore ?? null;
audienceRatingsMap.set(item.tmdbId, audienceScore);
} else if (item.mediaType === 'tv' && item.year) {
const rating = await this.rtAPI.getTVRatings(
item.title,
item.year
);
rtRating = rating?.criticsScore ?? null;
audienceScore = rating?.audienceScore ?? null;
audienceRatingsMap.set(item.tmdbId, audienceScore);
}
ratingsMap.set(item.tmdbId, rtRating);
if (rtRating !== null) {
logger.debug(
`Found RT rating ${rtRating} for ${item.title} (${item.year})`,
`Found RT critics score ${rtRating} for ${item.title} (${item.year})`,
{
label: serviceLabel,
tmdbId: item.tmdbId,
title: item.title,
year: item.year,
rating: rtRating,
criticsScore: rtRating,
}
);
}
if (audienceScore !== null) {
logger.debug(
`Found RT audience score ${audienceScore} for ${item.title} (${item.year})`,
{
label: serviceLabel,
tmdbId: item.tmdbId,
title: item.title,
year: item.year,
audienceScore: audienceScore,
}
);
}
} catch (error) {
logger.debug(
`Failed to get RT rating for ${item.title}, will allow item`,
`Failed to get RT ratings for ${item.title}, will allow item`,
{
label: serviceLabel,
tmdbId: item.tmdbId,
@@ -467,23 +535,34 @@ export class MissingItemFilterService {
);
// Set to null to indicate we tried but failed
ratingsMap.set(item.tmdbId, null);
audienceRatingsMap.set(item.tmdbId, null);
}
})
);
const ratingsFound = Array.from(ratingsMap.values()).filter(
const criticsRatingsFound = Array.from(ratingsMap.values()).filter(
(r) => r !== null
).length;
const audienceRatingsFound = Array.from(
audienceRatingsMap.values()
).filter((r) => r !== null).length;
logger.debug(
`Cached ${ratingsMap.size} RT ratings (${ratingsFound} found, ${
ratingsMap.size - ratingsFound
} not found)`,
`Cached ${
ratingsMap.size
} RT ratings - Critics: ${criticsRatingsFound} found, ${
ratingsMap.size - criticsRatingsFound
} not found | Audience: ${audienceRatingsFound} found, ${
audienceRatingsMap.size - audienceRatingsFound
} not found`,
{
label: serviceLabel,
collection: config.name,
totalCached: ratingsMap.size,
ratingsFound,
ratingsNotFound: ratingsMap.size - ratingsFound,
criticsRatingsFound,
criticsRatingsNotFound: ratingsMap.size - criticsRatingsFound,
audienceRatingsFound,
audienceRatingsNotFound:
audienceRatingsMap.size - audienceRatingsFound,
}
);
} catch (error) {
@@ -1000,10 +1079,10 @@ export class MissingItemFilterService {
);
}
// Log summary of items excluded by Rotten Tomatoes rating
// Log summary of items excluded by Rotten Tomatoes critics rating
if (result.lowRatedRTItems.length > 0) {
logger.info(
`Items skipped due to Rotten Tomatoes rating below ${config.minimumRottenTomatoesRating}`,
`Items skipped due to Rotten Tomatoes critics rating below ${config.minimumRottenTomatoesRating}`,
{
label: `${sourceLabel} Collections`,
collection: config.name,
@@ -1016,6 +1095,23 @@ export class MissingItemFilterService {
}
);
}
// Log summary of items excluded by Rotten Tomatoes audience rating
if (result.lowRatedRTAudienceItems.length > 0) {
logger.info(
`Items skipped due to Rotten Tomatoes audience rating below ${config.minimumRottenTomatoesAudienceRating}`,
{
label: `${sourceLabel} Collections`,
collection: config.name,
minimumRating: config.minimumRottenTomatoesAudienceRating,
count: result.lowRatedRTAudienceItems.length,
titles: result.lowRatedRTAudienceItems.slice(0, 10),
...(result.lowRatedRTAudienceItems.length > 10 && {
additionalCount: result.lowRatedRTAudienceItems.length - 10,
}),
}
);
}
}
}

View File

@@ -119,6 +119,7 @@ export interface CollectionConfig {
readonly minimumYear?: number; // Only process movies/TV shows released on or after this year (0 = no limit)
readonly minimumImdbRating?: number; // Only process movies/TV shows with IMDb rating >= this value (0 = no limit)
readonly minimumRottenTomatoesRating?: number; // Only process movies/TV shows with Rotten Tomatoes critics score >= this value (0 = no limit)
readonly minimumRottenTomatoesAudienceRating?: number; // Only process movies/TV shows with Rotten Tomatoes audience score >= this value (0 = no limit)
readonly excludedGenres?: number[]; // @deprecated Use filterSettings.genres - Exclude items with these TMDB genre IDs from missing items search
readonly excludedCountries?: string[]; // @deprecated Use filterSettings.countries - Exclude items with these ISO 3166-1 country codes from missing items search
readonly excludedLanguages?: string[]; // @deprecated Use filterSettings.languages - Exclude items with these ISO 639-1 language codes from missing items search

View File

@@ -58,6 +58,7 @@ const messages = defineMessages({
minimumYear: 'Minimum Year',
minimumImdbRating: 'Minimum IMDb Rating',
minimumRottenTomatoesRating: 'Minimum RT Rating',
minimumRottenTomatoesAudienceRating: 'Minimum RT Audience Rating',
showUnwatchedOnly: 'Unwatched Only',
createPlaceholders: 'Create Placeholders',
editValues: 'Edit Selected',
@@ -104,6 +105,7 @@ type UnifiedCollection = {
minimumYear?: number;
minimumImdbRating?: number;
minimumRottenTomatoesRating?: number;
minimumRottenTomatoesAudienceRating?: number;
showUnwatchedOnly?: boolean;
createPlaceholdersForMissing?: boolean;
// Original config for saving
@@ -154,6 +156,7 @@ const BulkEditModal: React.FC<BulkEditModalProps> = ({
minimumYear?: number | '';
minimumImdbRating?: number | '';
minimumRottenTomatoesRating?: number | '';
minimumRottenTomatoesAudienceRating?: number | '';
showUnwatchedOnly?: boolean;
createPlaceholdersForMissing?: boolean;
}>({});
@@ -190,6 +193,8 @@ const BulkEditModal: React.FC<BulkEditModalProps> = ({
minimumYear: config.minimumYear,
minimumImdbRating: config.minimumImdbRating,
minimumRottenTomatoesRating: config.minimumRottenTomatoesRating,
minimumRottenTomatoesAudienceRating:
config.minimumRottenTomatoesAudienceRating,
showUnwatchedOnly: config.showUnwatchedOnly,
createPlaceholdersForMissing: config.createPlaceholdersForMissing,
originalConfig: config,
@@ -343,6 +348,11 @@ const BulkEditModal: React.FC<BulkEditModalProps> = ({
(a.minimumRottenTomatoesRating || 0) -
(b.minimumRottenTomatoesRating || 0);
break;
case 'minimumRottenTomatoesAudienceRating':
comparison =
(a.minimumRottenTomatoesAudienceRating || 0) -
(b.minimumRottenTomatoesAudienceRating || 0);
break;
case 'showUnwatchedOnly':
comparison =
(a.showUnwatchedOnly ? 1 : 0) - (b.showUnwatchedOnly ? 1 : 0);
@@ -446,6 +456,7 @@ const BulkEditModal: React.FC<BulkEditModalProps> = ({
'minimumYear',
'minimumImdbRating',
'minimumRottenTomatoesRating',
'minimumRottenTomatoesAudienceRating',
'showUnwatchedOnly',
'createPlaceholdersForMissing',
];
@@ -632,6 +643,19 @@ const BulkEditModal: React.FC<BulkEditModalProps> = ({
: editValues.minimumRottenTomatoesRating;
}
if (
editValues.minimumRottenTomatoesAudienceRating !== undefined &&
isFieldApplicable(
'minimumRottenTomatoesAudienceRating',
collection.type
)
) {
updatedFields.minimumRottenTomatoesAudienceRating =
editValues.minimumRottenTomatoesAudienceRating === ''
? undefined
: editValues.minimumRottenTomatoesAudienceRating;
}
if (
editValues.showUnwatchedOnly !== undefined &&
isFieldApplicable('showUnwatchedOnly', collection.type)
@@ -959,6 +983,17 @@ const BulkEditModal: React.FC<BulkEditModalProps> = ({
{intl.formatMessage(messages.minimumRottenTomatoesRating)}
{renderSortIndicator('minimumRottenTomatoesRating')}
</th>
<th
className="w-24 cursor-pointer px-3 py-2 text-center text-xs font-medium text-gray-400 hover:text-gray-300"
onClick={() =>
handleColumnSort('minimumRottenTomatoesAudienceRating')
}
>
{intl.formatMessage(
messages.minimumRottenTomatoesAudienceRating
)}
{renderSortIndicator('minimumRottenTomatoesAudienceRating')}
</th>
<th
className="w-32 cursor-pointer px-3 py-2 text-center text-xs font-medium text-gray-400 hover:text-gray-300"
onClick={() => handleColumnSort('showUnwatchedOnly')}
@@ -1276,6 +1311,18 @@ const BulkEditModal: React.FC<BulkEditModalProps> = ({
>
{collection.minimumRottenTomatoesRating || '-'}
</td>
<td
className={`px-3 py-2 text-center text-sm ${
!isFieldApplicable(
'minimumRottenTomatoesAudienceRating',
collection.type
)
? 'text-gray-600 opacity-30'
: 'text-gray-300'
}`}
>
{collection.minimumRottenTomatoesAudienceRating || '-'}
</td>
<td
className={`px-3 py-2 text-center ${
!isFieldApplicable(
@@ -1733,6 +1780,27 @@ const BulkEditModal: React.FC<BulkEditModalProps> = ({
max={100}
/>
</td>
<td className="px-3 py-2">
<input
type="number"
value={
editValues.minimumRottenTomatoesAudienceRating || ''
}
onChange={(e) =>
setEditValues({
...editValues,
minimumRottenTomatoesAudienceRating:
e.target.value === ''
? ''
: Number(e.target.value),
})
}
placeholder="-"
className="w-full rounded border border-gray-600 bg-stone-700 px-2 py-1 text-xs text-white"
min={0}
max={100}
/>
</td>
<td className="px-3 py-2">
<select
value={

View File

@@ -46,6 +46,10 @@ const messages = defineMessages({
minimumRottenTomatoesRating: 'Minimum Rotten Tomatoes rating',
minimumRottenTomatoesRatingHelp:
'Only grab movies/TV shows with a Rotten Tomatoes critics score >= this value (0 = no limit). Items without ratings will be allowed.',
minimumRottenTomatoesAudienceRating:
'Minimum Rotten Tomatoes audience rating',
minimumRottenTomatoesAudienceRatingHelp:
'Only grab movies/TV shows with a Rotten Tomatoes audience score >= this value (0 = no limit). Items without ratings will be allowed.',
// Download method
downloadMethod: 'Download Method',
@@ -521,6 +525,34 @@ const AutoRequestSection = ({
</div>
</div>
{/* Minimum Rotten Tomatoes Audience Rating */}
<div className="mb-6">
<div className="mb-2 text-sm font-medium text-gray-200">
{intl.formatMessage(messages.minimumRottenTomatoesAudienceRating)}
</div>
<div className="form-input-field">
<Field
type="text"
inputMode="decimal"
id="minimumRottenTomatoesAudienceRating"
name="minimumRottenTomatoesAudienceRating"
placeholder="0"
className="short"
/>
</div>
{errors.minimumRottenTomatoesAudienceRating &&
touched.minimumRottenTomatoesAudienceRating && (
<div className="error">
{errors.minimumRottenTomatoesAudienceRating}
</div>
)}
<div className="label-tip mt-2">
{intl.formatMessage(
messages.minimumRottenTomatoesAudienceRatingHelp
)}
</div>
</div>
{/* Genre Filter with Include/Exclude Mode */}
<FilterWithMode
filterType="genres"

View File

@@ -1140,6 +1140,9 @@ const CollectionFormConfigForm = ({
(config as CollectionFormConfig).minimumImdbRating || 0,
minimumRottenTomatoesRating:
(config as CollectionFormConfig).minimumRottenTomatoesRating || 0,
minimumRottenTomatoesAudienceRating:
(config as CollectionFormConfig)
.minimumRottenTomatoesAudienceRating || 0,
excludedGenres: (config as CollectionFormConfig).excludedGenres || [],
excludedCountries:
(config as CollectionFormConfig).excludedCountries || [],
@@ -1593,6 +1596,13 @@ const CollectionFormConfigForm = ({
? parseFloat(values.minimumRottenTomatoesRating.toString())
: 0
: undefined,
minimumRottenTomatoesAudienceRating: values.enableGrabMissingItems
? values.minimumRottenTomatoesAudienceRating
? parseFloat(
values.minimumRottenTomatoesAudienceRating.toString()
)
: 0
: undefined,
// Unified person minimum items mapped to person collections
personMinimumItems: isPersonCollection
? optionalNumber(values.personMinimumItems) ??

View File

@@ -420,6 +420,10 @@ const CollectionSettings = ({
...(config.minimumRottenTomatoesRating !== undefined && {
minimumRottenTomatoesRating: config.minimumRottenTomatoesRating,
}),
...(config.minimumRottenTomatoesAudienceRating !== undefined && {
minimumRottenTomatoesAudienceRating:
config.minimumRottenTomatoesAudienceRating,
}),
...(config.excludedGenres !== undefined && {
excludedGenres: config.excludedGenres,
}),

View File

@@ -23,6 +23,7 @@
"components.Collections.BulkEditModal.maxSeasonsToRequest": "Max Seasons to Request",
"components.Collections.BulkEditModal.minimumImdbRating": "Minimum IMDb Rating",
"components.Collections.BulkEditModal.minimumRottenTomatoesRating": "Minimum RT Rating",
"components.Collections.BulkEditModal.minimumRottenTomatoesAudienceRating": "Minimum RT Audience Rating",
"components.Collections.BulkEditModal.minimumYear": "Minimum Year",
"components.Collections.BulkEditModal.noCollections": "No collections available",
"components.Collections.BulkEditModal.overseerrMode": "Overseerr",
@@ -157,6 +158,8 @@
"components.Collections.FormSections.minimumPlays": "Minimum Play Count",
"components.Collections.FormSections.minimumRottenTomatoesRating": "Minimum Rotten Tomatoes rating",
"components.Collections.FormSections.minimumRottenTomatoesRatingHelp": "Only grab movies/TV shows with a Rotten Tomatoes critics score >= this value (0 = no limit). Items without ratings will be allowed.",
"components.Collections.FormSections.minimumRottenTomatoesAudienceRating": "Minimum Rotten Tomatoes audience rating",
"components.Collections.FormSections.minimumRottenTomatoesAudienceRatingHelp": "Only grab movies/TV shows with a Rotten Tomatoes audience score >= this value (0 = no limit). Items without ratings will be allowed.",
"components.Collections.FormSections.minimumYear": "Minimum release year",
"components.Collections.FormSections.minimumYearHelp": "Only grab movies/TV shows released on or after this year (0 = no limit)",
"components.Collections.FormSections.mixedContentWarning": "Warning: Conflicting episodes/TV show lists detected across sources. Only \"Cycle Lists\" mode is available to prevent collection type conflicts.",

View File

@@ -276,6 +276,7 @@ export interface CollectionFormConfig {
readonly minimumYear?: number; // Only process movies/TV shows released on or after this year (0 = no limit)
readonly minimumImdbRating?: number; // Only process movies/TV shows with IMDb rating >= this value (0 = no limit)
readonly minimumRottenTomatoesRating?: number; // Only process movies/TV shows with Rotten Tomatoes critics score >= this value (0 = no limit)
readonly minimumRottenTomatoesAudienceRating?: number; // Only process movies/TV shows with Rotten Tomatoes audience score >= this value (0 = no limit)
readonly excludedGenres?: number[]; // @deprecated Use filterSettings.genres - Exclude items with these TMDB genre IDs from missing items search
readonly excludedCountries?: string[]; // @deprecated Use filterSettings.countries - Exclude items with these ISO 3166-1 country codes from missing items search
readonly excludedLanguages?: string[]; // @deprecated Use filterSettings.languages - Exclude items with these ISO 639-1 language codes from missing items search
@@ -454,6 +455,7 @@ export interface CollectionConfigCreateRequest {
readonly minimumYear?: number;
readonly minimumImdbRating?: number;
readonly minimumRottenTomatoesRating?: number;
readonly minimumRottenTomatoesAudienceRating?: number;
readonly excludedGenres?: number[];
readonly excludedCountries?: string[];
readonly excludedLanguages?: string[];
@@ -595,6 +597,8 @@ export const toCollectionCreateRequest = (
minimumYear: config.minimumYear,
minimumImdbRating: config.minimumImdbRating,
minimumRottenTomatoesRating: config.minimumRottenTomatoesRating,
minimumRottenTomatoesAudienceRating:
config.minimumRottenTomatoesAudienceRating,
excludedGenres: config.excludedGenres,
excludedCountries: config.excludedCountries,
excludedLanguages: config.excludedLanguages,

View File

@@ -294,6 +294,11 @@ export const saveIndividualConfigs = async (
minimumRottenTomatoesRating:
collectionConfig.minimumRottenTomatoesRating,
}),
...(collectionConfig.minimumRottenTomatoesAudienceRating !==
undefined && {
minimumRottenTomatoesAudienceRating:
collectionConfig.minimumRottenTomatoesAudienceRating,
}),
...(collectionConfig.excludedGenres !== undefined && {
excludedGenres: collectionConfig.excludedGenres,
}),

View File

@@ -248,6 +248,21 @@ const autoRequestValidations = {
return /^\d+(\.\d{1})?$/.test(value.toString());
}
),
minimumRottenTomatoesAudienceRating: Yup.number()
.min(
0,
'Minimum Rotten Tomatoes audience rating must be 0 or greater (0 = no limit)'
)
.max(100, 'Rotten Tomatoes audience ratings cannot exceed 100')
.test(
'decimal-places',
'Rotten Tomatoes audience rating can have at most 1 decimal place',
(value) => {
if (value === undefined || value === null) return true;
return /^\d+(\.\d{1})?$/.test(value.toString());
}
),
};
// Placeholder creation validation