From 762cc0903b1a3b732f0ff2c279019283dfe61ba9 Mon Sep 17 00:00:00 2001 From: geogolem Date: Fri, 30 Aug 2024 14:24:06 -0400 Subject: [PATCH] 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 --- .../Builder/DateFilterBuilderRowValue.js | 12 +++- .../src/Helpers/Props/filterBuilderTypes.js | 3 +- frontend/src/Helpers/Props/filterTypes.js | 2 + .../Index/Overview/MovieIndexOverview.tsx | 2 + .../Movie/Index/Posters/MovieIndexPoster.tsx | 2 + .../ProgressBar/MovieIndexProgressBar.tsx | 3 + .../src/Movie/Index/Table/MovieIndexRow.tsx | 2 + frontend/src/Movie/Movie.ts | 1 + .../src/Store/Actions/discoverMovieActions.js | 12 ++++ frontend/src/Store/Actions/movieActions.js | 8 +++ .../src/Store/Actions/movieIndexActions.js | 12 ++++ .../src/Utilities/Date/dateFilterPredicate.js | 3 + src/NzbDrone.Core/Localization/Core/en.json | 2 + src/NzbDrone.Core/Movies/Movie.cs | 72 ++++++------------- src/Radarr.Api.V3/Movies/MovieResource.cs | 4 ++ 15 files changed, 87 insertions(+), 53 deletions(-) diff --git a/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.js b/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.js index 9309a80eee..19718d4756 100644 --- a/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.js +++ b/frontend/src/Components/Filter/Builder/DateFilterBuilderRowValue.js @@ -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 ( translate('FilterNotInNext') - } + }, + { key: filterTypes.IN_PAST, value: 'before today' } ], [EQUAL]: [ diff --git a/frontend/src/Helpers/Props/filterTypes.js b/frontend/src/Helpers/Props/filterTypes.js index 239a4e7e94..7fa6c7b8ae 100644 --- a/frontend/src/Helpers/Props/filterTypes.js +++ b/frontend/src/Helpers/Props/filterTypes.js @@ -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, diff --git a/frontend/src/Movie/Index/Overview/MovieIndexOverview.tsx b/frontend/src/Movie/Index/Overview/MovieIndexOverview.tsx index 4e9522a2f5..9aa7fe170b 100644 --- a/frontend/src/Movie/Index/Overview/MovieIndexOverview.tsx +++ b/frontend/src/Movie/Index/Overview/MovieIndexOverview.tsx @@ -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} diff --git a/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx b/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx index c6b27aa6f8..a78845218d 100644 --- a/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx +++ b/frontend/src/Movie/Index/Posters/MovieIndexPoster.tsx @@ -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} diff --git a/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.tsx b/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.tsx index aef82e2b8d..e41a867ebf 100644 --- a/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.tsx +++ b/frontend/src/Movie/Index/ProgressBar/MovieIndexProgressBar.tsx @@ -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 : ''} /> ); } diff --git a/frontend/src/Movie/Index/Table/MovieIndexRow.tsx b/frontend/src/Movie/Index/Table/MovieIndexRow.tsx index d546a8c511..71db6b6cf4 100644 --- a/frontend/src/Movie/Index/Table/MovieIndexRow.tsx +++ b/frontend/src/Movie/Index/Table/MovieIndexRow.tsx @@ -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} diff --git a/frontend/src/Movie/Movie.ts b/frontend/src/Movie/Movie.ts index b7b4ee6b28..cb7f36a1ec 100644 --- a/frontend/src/Movie/Movie.ts +++ b/frontend/src/Movie/Movie.ts @@ -95,6 +95,7 @@ interface Movie extends ModelBase { grabbed?: boolean; lastSearchTime?: string; isAvailable: boolean; + dateConsideredAvailable: string; isSaving?: boolean; addOptions: MovieAddOptions; } diff --git a/frontend/src/Store/Actions/discoverMovieActions.js b/frontend/src/Store/Actions/discoverMovieActions.js index c658b7ed5f..a821178771 100644 --- a/frontend/src/Store/Actions/discoverMovieActions.js +++ b/frontend/src/Store/Actions/discoverMovieActions.js @@ -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'), diff --git a/frontend/src/Store/Actions/movieActions.js b/frontend/src/Store/Actions/movieActions.js index cd56135894..8cc215d9e9 100644 --- a/frontend/src/Store/Actions/movieActions.js +++ b/frontend/src/Store/Actions/movieActions.js @@ -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); }, diff --git a/frontend/src/Store/Actions/movieIndexActions.js b/frontend/src/Store/Actions/movieIndexActions.js index e36bee132a..d3b79fa6b8 100644 --- a/frontend/src/Store/Actions/movieIndexActions.js +++ b/frontend/src/Store/Actions/movieIndexActions.js @@ -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'), diff --git a/frontend/src/Utilities/Date/dateFilterPredicate.js b/frontend/src/Utilities/Date/dateFilterPredicate.js index 59407e3ba9..4a55292a71 100644 --- a/frontend/src/Utilities/Date/dateFilterPredicate.js +++ b/frontend/src/Utilities/Date/dateFilterPredicate.js @@ -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; } diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index 47d10fa767..61341dbe50 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -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", diff --git a/src/NzbDrone.Core/Movies/Movie.cs b/src/NzbDrone.Core/Movies/Movie.cs index 1ef429fa2c..a34b0b29bc 100644 --- a/src/NzbDrone.Core/Movies/Movie.cs +++ b/src/NzbDrone.Core/Movies/Movie.cs @@ -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() diff --git a/src/Radarr.Api.V3/Movies/MovieResource.cs b/src/Radarr.Api.V3/Movies/MovieResource.cs index 9a9829eadd..1ea969d69c 100644 --- a/src/Radarr.Api.V3/Movies/MovieResource.cs +++ b/src/Radarr.Api.V3/Movies/MovieResource.cs @@ -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,