New: Add "Download Status" filter option for custom filters

This commit is contained in:
Will Burland 2026-01-08 19:45:33 +00:00
parent 89110c2cc8
commit e7f2f44dce
11 changed files with 159 additions and 11 deletions

View file

@ -0,0 +1,53 @@
import React from 'react';
import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const protocols = [
{
id: 'downloaded',
get name() {
return translate('DownloadedAndMonitored');
}
},
{
id: 'unmonitored',
get name() {
return translate('DownloadedButNotMonitored');
}
},
{
id: 'missingMonitored',
get name() {
return translate('MissingMonitoredAndConsideredAvailable');
}
},
{
id: 'missingUnmonitored',
get name() {
return translate('MissingNotMonitored');
}
},
{
id: 'queue',
get name() {
return translate('Queued');
}
},
{
id: 'continuing',
get name() {
return translate('Unreleased');
}
},
];
function DownloadStatusFilterBuilderRowValue(props) {
return (
<FilterBuilderRowValue
tagList={protocols}
{...props}
/>
);
}
export default DownloadStatusFilterBuilderRowValue;

View file

@ -13,6 +13,7 @@ import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValu
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
import MinimumAvailabilityFilterBuilderRowValue from './MinimumAvailabilityFilterBuilderRowValue';
import MovieFilterBuilderRowValue from './MovieFilterBuilderRowValue';
import DownloadStatusFilterBuilderRowValue from './DownloadStatusFilterBuilderRowValue';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector';
import QualityProfileFilterBuilderRowValue from './QualityProfileFilterBuilderRowValue';
@ -87,6 +88,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.MOVIE:
return MovieFilterBuilderRowValue;
case filterBuilderValueTypes.DOWNLOAD_STATUS:
return DownloadStatusFilterBuilderRowValue;
case filterBuilderValueTypes.RELEASE_STATUS:
return ReleaseStatusFilterBuilderRowValue;

View file

@ -10,6 +10,7 @@ export const QUALITY = 'quality';
export const QUALITY_PROFILE = 'qualityProfile';
export const QUEUE_STATUS = 'queueStatus';
export const MOVIE = 'movie';
export const DOWNLOAD_STATUS = 'downloadStatus';
export const RELEASE_STATUS = 'releaseStatus';
export const MINIMUM_AVAILABILITY = 'minimumAvailability';
export const TAG = 'tag';

View file

@ -70,6 +70,7 @@ function MovieIndexRow(props: MovieIndexRowProps) {
releaseDate,
runtime,
minimumAvailability,
downloadStatus,
path,
genres = [],
keywords = [],
@ -314,6 +315,14 @@ function MovieIndexRow(props: MovieIndexRowProps) {
);
}
if (name === 'downloadStatus') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>
{translate(firstCharToUpper(downloadStatus))}
</VirtualTableRowCell>
);
}
if (name === 'path') {
return (
<VirtualTableRowCell key={name} className={styles[name]}>

View file

@ -13,6 +13,8 @@ export type MovieStatus =
export type MovieAvailability = 'announced' | 'inCinemas' | 'released';
export type DownloadStatus = 'downloaded' | 'unmonitored' | 'missingMonitored' | 'missingUnmonitored' | 'queue' | 'continuing';
export type CoverType = 'poster' | 'fanart' | 'headshot';
export interface Image {
@ -80,6 +82,7 @@ interface Movie extends ModelBase {
rootFolderPath: string;
runtime: number;
minimumAvailability: MovieAvailability;
downloadStatus: DownloadStatus;
path: string;
genres: string[];
keywords: string[];

View file

@ -440,6 +440,12 @@ export const defaultState = {
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
{
name: 'downloadStatus',
label: () => translate('DownloadStatus'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.DOWNLOAD_STATUS
},
{
name: 'physicalRelease',
label: () => translate('PhysicalRelease'),

View file

@ -576,6 +576,7 @@
"DownloadPropersAndRepacksHelpTextCustomFormat": "Use 'Do not Prefer' to sort by custom format score over Propers/Repacks",
"DownloadPropersAndRepacksHelpTextWarning": "Use custom formats for automatic upgrades to Propers/Repacks",
"DownloadStationStatusExtracting": "Extracting: {progress}%",
"DownloadStatus": "Download Status",
"DownloadWarning": "Download warning: {warningMessage}",
"Downloaded": "Downloaded",
"DownloadedAndMonitored": "Downloaded (Monitored)",

View file

@ -23,6 +23,7 @@
using NzbDrone.Core.Validation;
using NzbDrone.Core.Validation.Paths;
using NzbDrone.SignalR;
using NzbDrone.Core.Queue;
using Radarr.Http;
using Radarr.Http.REST;
using Radarr.Http.REST.Attributes;
@ -47,6 +48,7 @@ public class MovieController : RestControllerWithSignalR<MovieResource, Movie>,
private readonly IRootFolderService _rootFolderService;
private readonly IUpgradableSpecification _qualityUpgradableSpecification;
private readonly IConfigService _configService;
private readonly IQueueService _queueService;
public MovieController(IBroadcastSignalRMessage signalRBroadcaster,
IMovieService moviesService,
@ -58,6 +60,7 @@ public MovieController(IBroadcastSignalRMessage signalRBroadcaster,
IRootFolderService rootFolderService,
IUpgradableSpecification qualityUpgradableSpecification,
IConfigService configService,
IQueueService queueService,
RootFolderValidator rootFolderValidator,
MappedNetworkDriveValidator mappedNetworkDriveValidator,
MoviePathValidator moviesPathValidator,
@ -78,6 +81,7 @@ public MovieController(IBroadcastSignalRMessage signalRBroadcaster,
_configService = configService;
_coverMapper = coverMapper;
_commandQueueManager = commandQueueManager;
_queueService = queueService;
_rootFolderService = rootFolderService;
SharedValidator.RuleFor(s => s.Path).Cascade(CascadeMode.Stop)
@ -150,8 +154,8 @@ public List<MovieResource> AllMovie(int? tmdbId, bool excludeLocalCovers = false
foreach (var movie in movies)
{
var translation = GetTranslationFromDict(tdict, movie.MovieMetadata, translationLanguage);
moviesResources.Add(movie.ToResource(availDelay, translation, _qualityUpgradableSpecification));
var translation = GetTranslationFromDict(tdict, movie.MovieMetadata, translationLanguage);
moviesResources.Add(movie.ToResource(availDelay, translation, _qualityUpgradableSpecification, null, _queueService));
}
if (!excludeLocalCovers)
@ -191,7 +195,7 @@ protected MovieResource MapToResource(Movie movie, Language translationLanguage
var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.MovieMetadataId);
var translation = GetMovieTranslation(translations, movie.MovieMetadata, translationLanguage);
var resource = movie.ToResource(availDelay, translation, _qualityUpgradableSpecification);
var resource = movie.ToResource(availDelay, translation, _qualityUpgradableSpecification, null, _queueService);
MapCoversToLocal(resource);
FetchAndLinkMovieStatistics(resource);

View file

@ -6,6 +6,7 @@
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.Download;
using NzbDrone.Core.Queue;
using NzbDrone.Core.Languages;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.MediaFiles.Events;
@ -30,6 +31,7 @@ public abstract class MovieControllerWithSignalR : RestControllerWithSignalR<Mov
protected readonly ICustomFormatCalculationService _formatCalculator;
protected readonly IConfigService _configService;
protected readonly IMapCoversToLocal _coverMapper;
protected readonly IQueueService _queueService;
protected MovieControllerWithSignalR(IMovieService movieService,
IMovieTranslationService movieTranslationService,
@ -38,7 +40,8 @@ protected MovieControllerWithSignalR(IMovieService movieService,
ICustomFormatCalculationService formatCalculator,
IConfigService configService,
IMapCoversToLocal coverMapper,
IBroadcastSignalRMessage signalRBroadcaster)
IBroadcastSignalRMessage signalRBroadcaster,
IQueueService queueService)
: base(signalRBroadcaster)
{
_movieService = movieService;
@ -48,18 +51,44 @@ protected MovieControllerWithSignalR(IMovieService movieService,
_formatCalculator = formatCalculator;
_configService = configService;
_coverMapper = coverMapper;
_queueService = queueService;
}
// Back-compat constructor (no queue service)
protected MovieControllerWithSignalR(IMovieService movieService,
IMovieTranslationService movieTranslationService,
IMovieStatisticsService movieStatisticsService,
IUpgradableSpecification upgradableSpecification,
ICustomFormatCalculationService formatCalculator,
IConfigService configService,
IMapCoversToLocal coverMapper,
IBroadcastSignalRMessage signalRBroadcaster)
: this(movieService, movieTranslationService, movieStatisticsService, upgradableSpecification, formatCalculator, configService, coverMapper, signalRBroadcaster, null)
{
}
protected MovieControllerWithSignalR(IMovieService movieService,
IUpgradableSpecification upgradableSpecification,
ICustomFormatCalculationService formatCalculator,
IBroadcastSignalRMessage signalRBroadcaster,
IQueueService queueService,
string resource)
: base(signalRBroadcaster)
{
_movieService = movieService;
_upgradableSpecification = upgradableSpecification;
_formatCalculator = formatCalculator;
_queueService = queueService;
}
// Back-compat constructor (no queue service)
protected MovieControllerWithSignalR(IMovieService movieService,
IUpgradableSpecification upgradableSpecification,
ICustomFormatCalculationService formatCalculator,
IBroadcastSignalRMessage signalRBroadcaster,
string resource)
: this(movieService, upgradableSpecification, formatCalculator, signalRBroadcaster, null, resource)
{
}
protected override MovieResource GetResourceById(int id)
@ -82,7 +111,7 @@ protected MovieResource MapToResource(Movie movie)
var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.MovieMetadataId);
var translation = GetMovieTranslation(translations, movie.MovieMetadata, language);
var resource = movie.ToResource(availDelay, translation, _upgradableSpecification, _formatCalculator);
var resource = movie.ToResource(availDelay, translation, _upgradableSpecification, _formatCalculator, _queueService);
FetchAndLinkMovieStatistics(resource);
_coverMapper.ConvertToLocalUrls(resource.Id, resource.Images);
@ -106,7 +135,7 @@ protected List<MovieResource> MapToResource(List<Movie> movies)
var translations = _movieTranslationService.GetAllTranslationsForMovieMetadata(movie.MovieMetadataId);
var translation = GetMovieTranslation(translations, movie.MovieMetadata, language);
var resource = movie.ToResource(availDelay, translation, _upgradableSpecification, _formatCalculator);
var resource = movie.ToResource(availDelay, translation, _upgradableSpecification, _formatCalculator, _queueService);
FetchAndLinkMovieStatistics(resource);
resources.Add(resource);

View file

@ -11,6 +11,7 @@
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Commands;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Queue;
using Radarr.Http;
namespace Radarr.Api.V3.Movies
@ -25,6 +26,7 @@ public class MovieEditorController : Controller
private readonly IManageCommandQueue _commandQueueManager;
private readonly MovieEditorValidator _movieEditorValidator;
private readonly IUpgradableSpecification _upgradableSpecification;
private readonly IQueueService _queueService;
public MovieEditorController(IMovieService movieService,
IMovieTranslationService movieTranslationService,
@ -32,7 +34,8 @@ public MovieEditorController(IMovieService movieService,
IConfigService configService,
IManageCommandQueue commandQueueManager,
MovieEditorValidator movieEditorValidator,
IUpgradableSpecification upgradableSpecification)
IUpgradableSpecification upgradableSpecification,
IQueueService queueService)
{
_movieService = movieService;
_movieTranslationService = movieTranslationService;
@ -41,6 +44,7 @@ public MovieEditorController(IMovieService movieService,
_commandQueueManager = commandQueueManager;
_movieEditorValidator = movieEditorValidator;
_upgradableSpecification = upgradableSpecification;
_queueService = queueService;
}
[HttpPut]
@ -125,7 +129,7 @@ public IActionResult SaveAll([FromBody] MovieEditorResource resource)
foreach (var movie in updatedMovies)
{
var translation = GetTranslationFromDict(tdict, movie.MovieMetadata, configLanguage);
var movieResource = movie.ToResource(availabilityDelay, translation, _upgradableSpecification);
var movieResource = movie.ToResource(availabilityDelay, translation, _upgradableSpecification, null, _queueService);
MapCoversToLocal(movieResource);

View file

@ -9,6 +9,7 @@
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Movies;
using NzbDrone.Core.Movies.Translations;
using NzbDrone.Core.Queue;
using Radarr.Api.V3.MovieFiles;
using Radarr.Http.REST;
using Swashbuckle.AspNetCore.Annotations;
@ -60,6 +61,9 @@ public MovieResource()
public bool? HasFile { get; set; }
public int MovieFileId { get; set; }
// Computed for frontend display/filtering
public string DownloadStatus { get; set; }
// Editing Only
public bool Monitored { get; set; }
public MovieStatusType MinimumAvailability { get; set; }
@ -99,7 +103,7 @@ public MovieResource()
public static class MovieResourceMapper
{
public static MovieResource ToResource(this Movie model, int availDelay, MovieTranslation movieTranslation = null, IUpgradableSpecification upgradableSpecification = null, ICustomFormatCalculationService formatCalculationService = null)
public static MovieResource ToResource(this Movie model, int availDelay, MovieTranslation movieTranslation = null, IUpgradableSpecification upgradableSpecification = null, ICustomFormatCalculationService formatCalculationService = null, IQueueService queueService = null)
{
if (model == null)
{
@ -113,6 +117,35 @@ public static MovieResource ToResource(this Movie model, int availDelay, MovieTr
var collection = model.MovieMetadata.Value.CollectionTmdbId > 0 ? new MovieCollectionResource { Title = model.MovieMetadata.Value.CollectionTitle, TmdbId = model.MovieMetadata.Value.CollectionTmdbId } : null;
var hasMovieFile = movieFile != null;
var isAvailable = model.IsAvailable(availDelay);
// detect if this movie has an active queue entry
var isQueued = queueService != null && queueService.GetQueue().Any(q => q.Movie != null && q.Movie.Id == model.Id);
string downloadStatus;
if (isQueued)
{
downloadStatus = "queue";
}
else if (hasMovieFile)
{
downloadStatus = model.Monitored ? "downloaded" : "unmonitored";
}
else if (isAvailable && !model.Monitored)
{
downloadStatus = "missingUnmonitored";
}
else if (isAvailable)
{
downloadStatus = "missingMonitored";
}
else
{
downloadStatus = "continuing";
}
return new MovieResource
{
Id = model.Id,
@ -160,6 +193,7 @@ public static MovieResource ToResource(this Movie model, int availDelay, MovieTr
AlternateTitles = model.MovieMetadata.Value.AlternativeTitles.ToResource(),
Ratings = model.MovieMetadata.Value.Ratings,
MovieFile = movieFile,
DownloadStatus = downloadStatus,
YouTubeTrailerId = model.MovieMetadata.Value.YouTubeTrailerId,
Studio = model.MovieMetadata.Value.Studio,
Collection = collection,
@ -225,9 +259,9 @@ public static Movie ToModel(this MovieResource resource, Movie movie)
return movie;
}
public static List<MovieResource> ToResource(this IEnumerable<Movie> movies, int availDelay, IUpgradableSpecification upgradableSpecification = null, ICustomFormatCalculationService formatCalculationService = null)
public static List<MovieResource> ToResource(this IEnumerable<Movie> movies, int availDelay, IUpgradableSpecification upgradableSpecification = null, ICustomFormatCalculationService formatCalculationService = null, IQueueService queueService = null)
{
return movies.Select(x => ToResource(x, availDelay, null, upgradableSpecification, formatCalculationService)).ToList();
return movies.Select(x => ToResource(x, availDelay, null, upgradableSpecification, formatCalculationService, queueService)).ToList();
}
public static List<Movie> ToModel(this IEnumerable<MovieResource> resources)