New Custom Filters based on Availability

https://github.com/Radarr/Radarr/issues/2074

make the UI take into account AvailabilityDelay
to make it consistent with RSS Decision Engine

add custom filter options for minimumAvailabilityDate
with and without Delay

Simplify the filter names.
ConsideredAvailable ---> DateConsideredAvailable  (these always take into account the delay)
MinimumAvailability ---> MinimumAvailabilityDate  (these never take into account delay)

if delay = 0 .. the 2 dates above are identical.
if delay = 0, MimimumAvailabilityDate will be 1 of InCinemas, Announced or Physical/Digital (Released) dates

Add ability to make the custom filter dates relative to
currentdate

DateConsideredAvailable tooltip on NotAvailable
This commit is contained in:
geogolem 2024-08-30 14:24:06 -04:00
parent f9562b9b76
commit 762cc0903b
15 changed files with 87 additions and 53 deletions

View file

@ -3,7 +3,7 @@ import React, { Component } from 'react';
import NumberInput from 'Components/Form/NumberInput';
import SelectInput from 'Components/Form/SelectInput';
import TextInput from 'Components/Form/TextInput';
import { IN_LAST, IN_NEXT, NOT_IN_LAST, NOT_IN_NEXT } from 'Helpers/Props/filterTypes';
import { IN_LAST, IN_NEXT, IN_PAST, NOT_IN_LAST, NOT_IN_NEXT } from 'Helpers/Props/filterTypes';
import isString from 'Utilities/String/isString';
import translate from 'Utilities/String/translate';
import { NAME } from './FilterBuilderRowValue';
@ -57,6 +57,12 @@ function isInFilter(filterType) {
);
}
function isPastFutureFilter(filterType) {
return (
filterType === IN_PAST
);
}
class DateFilterBuilderRowValue extends Component {
//
@ -187,6 +193,10 @@ class DateFilterBuilderRowValue extends Component {
);
}
if (isPastFutureFilter(filterType)) {
return null;
}
return (
<TextInput
name={NAME}

View file

@ -62,7 +62,8 @@ export const possibleFilterTypes = {
{
key: filterTypes.NOT_IN_NEXT,
value: () => translate('FilterNotInNext')
}
},
{ key: filterTypes.IN_PAST, value: 'before today' }
],
[EQUAL]: [

View file

@ -8,6 +8,7 @@ export const IN_NEXT = 'inNext';
export const NOT_IN_NEXT = 'notInNext';
export const LESS_THAN = 'lessThan';
export const LESS_THAN_OR_EQUAL = 'lessThanOrEqual';
export const IN_PAST = 'inPast';
export const NOT_CONTAINS = 'notContains';
export const NOT_EQUAL = 'notEqual';
export const STARTS_WITH = 'startsWith';
@ -22,6 +23,7 @@ export const all = [
GREATER_THAN_OR_EQUAL,
LESS_THAN,
LESS_THAN_OR_EQUAL,
IN_PAST,
NOT_CONTAINS,
NOT_EQUAL,
IN_LAST,

View file

@ -73,6 +73,7 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
tags,
hasFile,
isAvailable,
dateConsideredAvailable,
tmdbId,
imdbId,
studio,
@ -165,6 +166,7 @@ function MovieIndexOverview(props: MovieIndexOverviewProps) {
monitored={monitored}
hasFile={hasFile}
isAvailable={isAvailable}
dateConsideredAvailable={dateConsideredAvailable}
status={status}
width={posterWidth}
detailedProgressBar={overviewOptions.detailedProgressBar}

View file

@ -77,6 +77,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
studio,
added,
year,
dateConsideredAvailable,
inCinemas,
physicalRelease,
digitalRelease,
@ -219,6 +220,7 @@ function MovieIndexPoster(props: MovieIndexPosterProps) {
monitored={monitored}
hasFile={hasFile}
isAvailable={isAvailable}
dateConsideredAvailable={dateConsideredAvailable}
status={status}
width={posterWidth}
detailedProgressBar={detailedProgressBar}

View file

@ -18,6 +18,7 @@ interface MovieIndexProgressBarProps {
status: MovieStatus;
hasFile: boolean;
isAvailable: boolean;
dateConsideredAvailable: string;
width: number;
detailedProgressBar: boolean;
bottomRadius?: boolean;
@ -31,6 +32,7 @@ function MovieIndexProgressBar({
status,
hasFile,
isAvailable,
dateConsideredAvailable,
width,
detailedProgressBar,
bottomRadius,
@ -74,6 +76,7 @@ function MovieIndexProgressBar({
showText={detailedProgressBar}
width={width}
text={queueStatusText ? queueStatusText : movieStatus}
title={movieStatus === 'NotAvailable' ? dateConsideredAvailable : ''}
/>
);
}

View file

@ -80,6 +80,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
tmdbId,
imdbId,
isAvailable,
dateConsideredAvailable,
hasFile,
movieFile,
youTubeTrailerId,
@ -363,6 +364,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
monitored={monitored}
hasFile={hasFile}
isAvailable={isAvailable}
dateConsideredAvailable={dateConsideredAvailable}
status={status}
width={125}
detailedProgressBar={true}

View file

@ -95,6 +95,7 @@ interface Movie extends ModelBase {
grabbed?: boolean;
lastSearchTime?: string;
isAvailable: boolean;
dateConsideredAvailable: string;
isSaving?: boolean;
addOptions: MovieAddOptions;
}

View file

@ -467,6 +467,12 @@ export const defaultState = {
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'dateConsideredAvailable',
label: translate('DateConsideredAvailable'),
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
{
name: 'minimumAvailability',
label: () => translate('MinimumAvailability'),
@ -508,6 +514,12 @@ export const defaultState = {
label: () => translate('Popularity'),
type: filterBuilderTypes.NUMBER
},
{
name: 'minimumAvailabilityDate',
label: translate('MinimumAvailabilityDate'),
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
{
name: 'certification',
label: () => translate('Certification'),

View file

@ -152,6 +152,14 @@ export const filterPredicates = {
return dateFilterPredicate(item.physicalRelease, filterValue, type);
},
dateConsideredAvailable: function(item, filterValue, type) {
return dateFilterPredicate(item.dateConsideredAvailable, filterValue, type);
},
minimumAvailabilityDate: function(item, filterValue, type) {
return dateFilterPredicate(item.minimumAvailabilityDate, filterValue, type);
},
digitalRelease: function(item, filterValue, type) {
return dateFilterPredicate(item.digitalRelease, filterValue, type);
},

View file

@ -316,12 +316,24 @@ export const defaultState = {
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'dateConsideredAvailable',
label: translate('DateConsideredAvailable'),
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
{
name: 'minimumAvailability',
label: () => translate('MinimumAvailability'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.MINIMUM_AVAILABILITY
},
{
name: 'minimumAvailabilityDate',
label: translate('MinimumAvailabilityDate'),
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
{
name: 'title',
label: () => translate('Title'),

View file

@ -37,6 +37,9 @@ export default function(itemValue, filterValue, type) {
isAfter(itemValue, { [filterValue.time]: filterValue.value })
);
case filterTypes.IN_PAST:
return moment(itemValue).isBefore(new Date());
default:
return false;
}

View file

@ -305,6 +305,7 @@
"Database": "Database",
"DatabaseMigration": "Database Migration",
"Date": "Date",
"DateConsideredAvailable": "Date Considered Available",
"Dates": "Dates",
"Day": "Day",
"DayOfWeekAt": "{day} at {time}",
@ -1075,6 +1076,7 @@
"MinimumAge": "Minimum Age",
"MinimumAgeHelpText": "Usenet only: Minimum age in minutes of NZBs before they are grabbed. Use this to give new releases time to propagate to your usenet provider.",
"MinimumAvailability": "Minimum Availability",
"MinimumAvailabilityDate": "Minimum Availability Date",
"MinimumCustomFormatScore": "Minimum Custom Format Score",
"MinimumCustomFormatScoreHelpText": "Minimum custom format score allowed to download",
"MinimumCustomFormatScoreIncrement": "Minimum Custom Format Score Increment",

View file

@ -76,70 +76,40 @@ public string FolderName()
public bool IsAvailable(int delay = 0)
{
// the below line is what was used before delay was implemented, could still be used for cases when delay==0
// return (Status >= MinimumAvailability || (MinimumAvailability == MovieStatusType.PreDB && Status >= MovieStatusType.Released));
// This more complex sequence handles the delay
DateTime minimumAvailabilityDate;
if (MinimumAvailability is MovieStatusType.TBA or MovieStatusType.Announced)
{
minimumAvailabilityDate = DateTime.MinValue;
}
else if (MinimumAvailability == MovieStatusType.InCinemas && MovieMetadata.Value.InCinemas.HasValue)
{
minimumAvailabilityDate = MovieMetadata.Value.InCinemas.Value;
}
else
{
if (MovieMetadata.Value.PhysicalRelease.HasValue && MovieMetadata.Value.DigitalRelease.HasValue)
{
minimumAvailabilityDate = new DateTime(Math.Min(MovieMetadata.Value.PhysicalRelease.Value.Ticks, MovieMetadata.Value.DigitalRelease.Value.Ticks));
}
else if (MovieMetadata.Value.PhysicalRelease.HasValue)
{
minimumAvailabilityDate = MovieMetadata.Value.PhysicalRelease.Value;
}
else if (MovieMetadata.Value.DigitalRelease.HasValue)
{
minimumAvailabilityDate = MovieMetadata.Value.DigitalRelease.Value;
}
else
{
minimumAvailabilityDate = MovieMetadata.Value.InCinemas?.AddDays(90) ?? DateTime.MaxValue;
}
}
if (minimumAvailabilityDate == DateTime.MinValue || minimumAvailabilityDate == DateTime.MaxValue)
{
return DateTime.UtcNow >= minimumAvailabilityDate;
}
return DateTime.UtcNow >= minimumAvailabilityDate.AddDays(delay);
return DateTime.UtcNow >= GetReleaseDate(delay, true);
}
public DateTime? GetReleaseDate()
public DateTime? GetReleaseDate(int delay = 0, bool isAvailabilityCheck = false)
{
if (MinimumAvailability is MovieStatusType.TBA or MovieStatusType.Announced)
{
return new[] { MovieMetadata.Value.InCinemas, MovieMetadata.Value.DigitalRelease, MovieMetadata.Value.PhysicalRelease }
.Where(x => x.HasValue)
.Min();
if (isAvailabilityCheck)
{
return DateTime.MinValue;
}
else if (MovieMetadata.Value.InCinemas.HasValue || MovieMetadata.Value.DigitalRelease.HasValue || MovieMetadata.Value.PhysicalRelease.HasValue)
{
return new[] { MovieMetadata.Value.InCinemas, MovieMetadata.Value.DigitalRelease, MovieMetadata.Value.PhysicalRelease }
.Where(x => x.HasValue)
.Min()?.AddDays(delay);
}
}
if (MinimumAvailability == MovieStatusType.InCinemas && MovieMetadata.Value.InCinemas.HasValue)
else if (MinimumAvailability == MovieStatusType.InCinemas && MovieMetadata.Value.InCinemas.HasValue)
{
return MovieMetadata.Value.InCinemas.Value;
return MovieMetadata.Value.InCinemas.Value.AddDays(delay);
}
if (MovieMetadata.Value.DigitalRelease.HasValue || MovieMetadata.Value.PhysicalRelease.HasValue)
else if (MovieMetadata.Value.DigitalRelease.HasValue || MovieMetadata.Value.PhysicalRelease.HasValue)
{
return new[] { MovieMetadata.Value.DigitalRelease, MovieMetadata.Value.PhysicalRelease }
.Where(x => x.HasValue)
.Min();
.Min()?.AddDays(delay);
}
else if (!MovieMetadata.Value.InCinemas.HasValue && isAvailabilityCheck)
{
return DateTime.MaxValue;
}
return MovieMetadata.Value.InCinemas?.AddDays(90);
return MovieMetadata.Value.InCinemas?.AddDays(90 + delay);
}
public override string ToString()

View file

@ -63,7 +63,9 @@ public MovieResource()
// Editing Only
public bool Monitored { get; set; }
public MovieStatusType MinimumAvailability { get; set; }
public DateTime? MinimumAvailabilityDate { get; set; }
public bool IsAvailable { get; set; }
public DateTime? DateConsideredAvailable { get; set; }
public string FolderName { get; set; }
public int Runtime { get; set; }
@ -143,6 +145,8 @@ public static MovieResource ToResource(this Movie model, int availDelay, MovieTr
MinimumAvailability = model.MinimumAvailability,
IsAvailable = model.IsAvailable(availDelay),
DateConsideredAvailable = model.GetReleaseDate(availDelay),
MinimumAvailabilityDate = model.GetReleaseDate(),
FolderName = model.FolderName(),
Runtime = model.MovieMetadata.Value.Runtime,