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; }