diff --git a/frontend/src/Wanted/Missing/Missing.tsx b/frontend/src/Wanted/Missing/Missing.tsx
index 0e7996601..fdf5d4aa4 100644
--- a/frontend/src/Wanted/Missing/Missing.tsx
+++ b/frontend/src/Wanted/Missing/Missing.tsx
@@ -20,6 +20,7 @@ import TablePager from 'Components/Table/TablePager';
import Episode from 'Episode/Episode';
import { useToggleEpisodesMonitored } from 'Episode/useEpisode';
import { Filter } from 'Filters/Filter';
+import { useCustomFiltersList } from 'Filters/useCustomFilters';
import { align, icons, kinds } from 'Helpers/Props';
import { SortDirection } from 'Helpers/Props/sortDirections';
import InteractiveImportModal from 'InteractiveImport/InteractiveImportModal';
@@ -32,6 +33,7 @@ import {
unregisterPagePopulator,
} from 'Utilities/pagePopulator';
import translate from 'Utilities/String/translate';
+import MissingFilterModal from './MissingFilterModal';
import {
setMissingOption,
setMissingOptions,
@@ -39,7 +41,7 @@ import {
useMissingOptions,
} from './missingOptionsStore';
import MissingRow from './MissingRow';
-import useMissing, { FILTERS } from './useMissing';
+import useMissing, { FILTERS, useFilters } from './useMissing';
function getMonitoredValue(
filters: Filter[],
@@ -66,6 +68,9 @@ function MissingContent() {
const { columns, pageSize, sortKey, sortDirection, selectedFilterKey } =
useMissingOptions();
+ const filters = useFilters();
+ const customFilters = useCustomFiltersList('wanted.missing');
+
const isSearchingForAllEpisodes = useCommandExecuting(
CommandNames.MissingEpisodeSearch
);
@@ -257,8 +262,9 @@ function MissingContent() {
diff --git a/frontend/src/Wanted/Missing/MissingFilterModal.tsx b/frontend/src/Wanted/Missing/MissingFilterModal.tsx
new file mode 100644
index 000000000..f8546bcca
--- /dev/null
+++ b/frontend/src/Wanted/Missing/MissingFilterModal.tsx
@@ -0,0 +1,26 @@
+import React, { useCallback } from 'react';
+import { SetFilter } from 'Components/Filter/Filter';
+import FilterModal, { FilterModalProps } from 'Components/Filter/FilterModal';
+import Episode from 'Episode/Episode';
+import { setMissingOption } from './missingOptionsStore';
+import useMissing, { FILTER_BUILDER } from './useMissing';
+
+type MissingFilterModalProps = FilterModalProps;
+
+export default function MissingFilterModal(props: MissingFilterModalProps) {
+ const { records } = useMissing();
+
+ const dispatchSetFilter = useCallback(({ selectedFilterKey }: SetFilter) => {
+ setMissingOption('selectedFilterKey', selectedFilterKey);
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/frontend/src/Wanted/Missing/useMissing.tsx b/frontend/src/Wanted/Missing/useMissing.tsx
index 1a4b2deae..d98bf6a30 100644
--- a/frontend/src/Wanted/Missing/useMissing.tsx
+++ b/frontend/src/Wanted/Missing/useMissing.tsx
@@ -1,10 +1,13 @@
import { keepPreviousData } from '@tanstack/react-query';
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';
import Episode from 'Episode/Episode';
import { setEpisodeQueryKey } from 'Episode/useEpisode';
-import { Filter } from 'Filters/Filter';
+import { Filter, FilterBuilderProp } from 'Filters/Filter';
+import { useCustomFiltersList } from 'Filters/useCustomFilters';
import usePage from 'Helpers/Hooks/usePage';
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
+import { filterBuilderValueTypes } from 'Helpers/Props';
+import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
import translate from 'Utilities/String/translate';
import { useMissingOptions } from './missingOptionsStore';
@@ -31,20 +34,49 @@ export const FILTERS: Filter[] = [
},
],
},
+ {
+ key: 'excludeSpecials',
+ label: () => translate('ExcludeSpecials'),
+ filters: [
+ {
+ key: 'includeSpecials',
+ value: [false],
+ type: 'equal',
+ },
+ ],
+ },
+];
+
+export const FILTER_BUILDER: FilterBuilderProp[] = [
+ {
+ name: 'monitored',
+ label: () => translate('Monitored'),
+ type: 'exact',
+ valueType: filterBuilderValueTypes.BOOL,
+ },
+ {
+ name: 'includeSpecials',
+ label: () => translate('IncludeSpecials'),
+ type: 'equal',
+ valueType: filterBuilderValueTypes.BOOL,
+ },
];
const useMissing = () => {
const { page, goToPage } = usePage('missing');
const { pageSize, selectedFilterKey, sortKey, sortDirection } =
useMissingOptions();
+ const customFilters = useCustomFiltersList('wanted.missing');
+
+ const filters = useMemo(() => {
+ return findSelectedFilters(selectedFilterKey, FILTERS, customFilters);
+ }, [selectedFilterKey, customFilters]);
const { isPlaceholderData, queryKey, ...query } = usePagedApiQuery({
path: '/wanted/missing',
page,
pageSize,
- queryParams: {
- monitored: selectedFilterKey === 'monitored',
- },
+ filters,
sortKey,
sortDirection,
queryOptions: {
@@ -67,3 +99,7 @@ const useMissing = () => {
};
export default useMissing;
+
+export const useFilters = () => {
+ return FILTERS;
+};
diff --git a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs
index c4931b53c..0a7da938c 100644
--- a/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs
+++ b/src/NzbDrone.Core/IndexerSearch/EpisodeSearchService.cs
@@ -151,7 +151,7 @@ public void Execute(MissingEpisodeSearchCommand message)
pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false);
}
- episodes = _episodeService.EpisodesWithoutFiles(pagingSpec).Records.ToList();
+ episodes = _episodeService.EpisodesWithoutFiles(pagingSpec, true).Records.ToList();
}
var queue = GetQueuedEpisodeIds();
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 5d0d58a94..0c58bee40 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -708,6 +708,7 @@
"Events": "Events",
"Example": "Example",
"Exception": "Exception",
+ "ExcludeSpecials": "Exclude Specials",
"ExcludeUnknownSeriesItems": "Exclude Unknown Series Items",
"ExcludedReleaseProfile": "Excluded Release Profile",
"ExcludedReleaseProfiles": "Excluded Release Profiles",
diff --git a/src/NzbDrone.Core/Tv/EpisodeService.cs b/src/NzbDrone.Core/Tv/EpisodeService.cs
index b28058150..79b4f9c85 100644
--- a/src/NzbDrone.Core/Tv/EpisodeService.cs
+++ b/src/NzbDrone.Core/Tv/EpisodeService.cs
@@ -27,7 +27,7 @@ public interface IEpisodeService
List GetEpisodesBySeason(int seriesId, int seasonNumber);
List GetEpisodesBySceneSeason(int seriesId, int sceneSeasonNumber);
List EpisodesWithFiles(int seriesId);
- PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec);
+ PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec, bool includeSpecials);
List GetEpisodesByFileId(int episodeFileId);
void UpdateEpisode(Episode episode);
void SetEpisodeMonitored(int episodeId, bool monitored);
@@ -158,11 +158,9 @@ public List EpisodesWithFiles(int seriesId)
return _episodeRepository.EpisodesWithFiles(seriesId);
}
- public PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec)
+ public PagingSpec EpisodesWithoutFiles(PagingSpec pagingSpec, bool includeSpecials)
{
- var episodeResult = _episodeRepository.EpisodesWithoutFiles(pagingSpec, true);
-
- return episodeResult;
+ return _episodeRepository.EpisodesWithoutFiles(pagingSpec, includeSpecials);
}
public List GetEpisodesByFileId(int episodeFileId)
diff --git a/src/Sonarr.Api.V3/Wanted/MissingController.cs b/src/Sonarr.Api.V3/Wanted/MissingController.cs
index bbfde535f..ab867459d 100644
--- a/src/Sonarr.Api.V3/Wanted/MissingController.cs
+++ b/src/Sonarr.Api.V3/Wanted/MissingController.cs
@@ -48,7 +48,7 @@ public PagingResource GetMissingEpisodes([FromQuery] PagingRequ
pagingSpec.FilterExpressions.Add(v => v.Monitored == false || v.Series.Monitored == false);
}
- var resource = pagingSpec.ApplyToPage(_episodeService.EpisodesWithoutFiles, v => MapToResource(v, includeSeries, false, includeImages));
+ var resource = pagingSpec.ApplyToPage(spec => _episodeService.EpisodesWithoutFiles(spec, true), v => MapToResource(v, includeSeries, false, includeImages));
return resource;
}
diff --git a/src/Sonarr.Api.V5/Wanted/MissingController.cs b/src/Sonarr.Api.V5/Wanted/MissingController.cs
index d9cb6aee2..e63d0bef5 100644
--- a/src/Sonarr.Api.V5/Wanted/MissingController.cs
+++ b/src/Sonarr.Api.V5/Wanted/MissingController.cs
@@ -24,7 +24,7 @@ public MissingController(IEpisodeService episodeService,
[HttpGet]
[Produces("application/json")]
- public PagingResource GetMissingEpisodes([FromQuery] PagingRequestResource paging, bool monitored = true, [FromQuery] MissingSubresource[]? includeSubresources = null)
+ public PagingResource GetMissingEpisodes([FromQuery] PagingRequestResource paging, bool monitored = true, bool includeSpecials = true, [FromQuery] MissingSubresource[]? includeSubresources = null)
{
var pagingResource = new PagingResource(paging);
var pagingSpec = pagingResource.MapToPagingSpec(
@@ -49,7 +49,7 @@ public PagingResource GetMissingEpisodes([FromQuery] PagingRequ
var includeSeries = includeSubresources.Contains(MissingSubresource.Series);
var includeImages = includeSubresources.Contains(MissingSubresource.Images);
- var resource = pagingSpec.ApplyToPage(_episodeService.EpisodesWithoutFiles, v => MapToResource(v, includeSeries, false, includeImages));
+ var resource = pagingSpec.ApplyToPage(spec => _episodeService.EpisodesWithoutFiles(spec, includeSpecials), v => MapToResource(v, includeSeries, false, includeImages));
return resource;
}