mirror of
https://github.com/agregarr/agregarr.git
synced 2026-02-16 14:59:29 -06:00
fix(missing items filtering): adds RT audience as missing item filter
fix #246
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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) ??
|
||||
|
||||
@@ -420,6 +420,10 @@ const CollectionSettings = ({
|
||||
...(config.minimumRottenTomatoesRating !== undefined && {
|
||||
minimumRottenTomatoesRating: config.minimumRottenTomatoesRating,
|
||||
}),
|
||||
...(config.minimumRottenTomatoesAudienceRating !== undefined && {
|
||||
minimumRottenTomatoesAudienceRating:
|
||||
config.minimumRottenTomatoesAudienceRating,
|
||||
}),
|
||||
...(config.excludedGenres !== undefined && {
|
||||
excludedGenres: config.excludedGenres,
|
||||
}),
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -294,6 +294,11 @@ export const saveIndividualConfigs = async (
|
||||
minimumRottenTomatoesRating:
|
||||
collectionConfig.minimumRottenTomatoesRating,
|
||||
}),
|
||||
...(collectionConfig.minimumRottenTomatoesAudienceRating !==
|
||||
undefined && {
|
||||
minimumRottenTomatoesAudienceRating:
|
||||
collectionConfig.minimumRottenTomatoesAudienceRating,
|
||||
}),
|
||||
...(collectionConfig.excludedGenres !== undefined && {
|
||||
excludedGenres: collectionConfig.excludedGenres,
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user