From e357d17b187378b92377f8acb077b12c1e7ea527 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 21 May 2023 17:51:36 -0700 Subject: [PATCH] New: Queue custom filters --- frontend/src/Activity/Queue/Queue.js | 29 +++++++++- frontend/src/Activity/Queue/QueueConnector.js | 13 ++++- .../src/Activity/Queue/QueueFilterModal.tsx | 54 +++++++++++++++++++ frontend/src/App/State/AppSectionState.ts | 5 ++ frontend/src/App/State/QueueAppState.ts | 10 +++- .../Filter/Builder/FilterBuilderRow.js | 8 +++ .../Builder/FilterBuilderRowValueProps.ts | 16 ++++++ .../Builder/LanguageFilterBuilderRowValue.tsx | 13 +++++ .../Builder/SeriesFilterBuilderRowValue.tsx | 21 ++++++++ .../Helpers/Props/filterBuilderValueTypes.js | 2 + .../createFetchServerSideCollectionHandler.js | 12 +++-- frontend/src/Store/Actions/calendarActions.js | 2 - frontend/src/Store/Actions/queueActions.js | 47 ++++++++++++++-- src/NzbDrone.Core/Localization/Core/en.json | 1 + src/Sonarr.Api.V3/Queue/QueueController.cs | 38 +++++++++++-- 15 files changed, 254 insertions(+), 17 deletions(-) create mode 100644 frontend/src/Activity/Queue/QueueFilterModal.tsx create mode 100644 frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts create mode 100644 frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx create mode 100644 frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx diff --git a/frontend/src/Activity/Queue/Queue.js b/frontend/src/Activity/Queue/Queue.js index 0b5827ea9..638fb1f52 100644 --- a/frontend/src/Activity/Queue/Queue.js +++ b/frontend/src/Activity/Queue/Queue.js @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import Alert from 'Components/Alert'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; +import FilterMenu from 'Components/Menu/FilterMenu'; import PageContent from 'Components/Page/PageContent'; import PageContentBody from 'Components/Page/PageContentBody'; import PageToolbar from 'Components/Page/Toolbar/PageToolbar'; @@ -21,6 +22,7 @@ import getSelectedIds from 'Utilities/Table/getSelectedIds'; import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState'; import selectAll from 'Utilities/Table/selectAll'; import toggleSelected from 'Utilities/Table/toggleSelected'; +import QueueFilterModal from './QueueFilterModal'; import QueueOptionsConnector from './QueueOptionsConnector'; import QueueRowConnector from './QueueRowConnector'; import RemoveQueueItemsModal from './RemoveQueueItemsModal'; @@ -151,11 +153,16 @@ class Queue extends Component { isEpisodesPopulated, episodesError, columns, + selectedFilterKey, + filters, + customFilters, + count, totalRecords, isGrabbing, isRemoving, isRefreshMonitoredDownloadsExecuting, onRefreshPress, + onFilterSelect, ...otherProps } = this.props; @@ -218,6 +225,15 @@ class Queue extends Component { iconName={icons.TABLE} /> + + @@ -239,7 +255,11 @@ class Queue extends Component { { isAllPopulated && !hasError && !items.length ? - {translate('QueueIsEmpty')} + { + selectedFilterKey !== 'all' && count > 0 ? + translate('QueueFilterHasNoItems') : + translate('QueueIsEmpty') + } : null } @@ -323,13 +343,18 @@ Queue.propTypes = { isEpisodesPopulated: PropTypes.bool.isRequired, episodesError: PropTypes.object, columns: PropTypes.arrayOf(PropTypes.object).isRequired, + selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + filters: PropTypes.arrayOf(PropTypes.object).isRequired, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, + count: PropTypes.number.isRequired, totalRecords: PropTypes.number, isGrabbing: PropTypes.bool.isRequired, isRemoving: PropTypes.bool.isRequired, isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired, onRefreshPress: PropTypes.func.isRequired, onGrabSelectedPress: PropTypes.func.isRequired, - onRemoveSelectedPress: PropTypes.func.isRequired + onRemoveSelectedPress: PropTypes.func.isRequired, + onFilterSelect: PropTypes.func.isRequired }; export default Queue; diff --git a/frontend/src/Activity/Queue/QueueConnector.js b/frontend/src/Activity/Queue/QueueConnector.js index f7d5d5152..178cb8e5f 100644 --- a/frontend/src/Activity/Queue/QueueConnector.js +++ b/frontend/src/Activity/Queue/QueueConnector.js @@ -7,6 +7,7 @@ import withCurrentPage from 'Components/withCurrentPage'; import { executeCommand } from 'Store/Actions/commandActions'; import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions'; import * as queueActions from 'Store/Actions/queueActions'; +import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import hasDifferentItems from 'Utilities/Object/hasDifferentItems'; import selectUniqueIds from 'Utilities/Object/selectUniqueIds'; @@ -18,12 +19,16 @@ function createMapStateToProps() { (state) => state.episodes, (state) => state.queue.options, (state) => state.queue.paged, + (state) => state.queue.status.item, + createCustomFiltersSelector('queue'), createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS), - (episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => { + (episodes, options, queue, status, customFilters, isRefreshMonitoredDownloadsExecuting) => { return { + count: options.includeUnknownSeriesItems ? status.totalCount : status.count, isEpisodesFetching: episodes.isFetching, isEpisodesPopulated: episodes.isPopulated, episodesError: episodes.error, + customFilters, isRefreshMonitoredDownloadsExecuting, ...options, ...queue @@ -122,6 +127,10 @@ class QueueConnector extends Component { this.props.setQueueSort({ sortKey }); }; + onFilterSelect = (selectedFilterKey) => { + this.props.setQueueFilter({ selectedFilterKey }); + }; + onTableOptionChange = (payload) => { this.props.setQueueTableOption(payload); @@ -156,6 +165,7 @@ class QueueConnector extends Component { onLastPagePress={this.onLastPagePress} onPageSelect={this.onPageSelect} onSortPress={this.onSortPress} + onFilterSelect={this.onFilterSelect} onTableOptionChange={this.onTableOptionChange} onRefreshPress={this.onRefreshPress} onGrabSelectedPress={this.onGrabSelectedPress} @@ -178,6 +188,7 @@ QueueConnector.propTypes = { gotoQueueLastPage: PropTypes.func.isRequired, gotoQueuePage: PropTypes.func.isRequired, setQueueSort: PropTypes.func.isRequired, + setQueueFilter: PropTypes.func.isRequired, setQueueTableOption: PropTypes.func.isRequired, clearQueue: PropTypes.func.isRequired, grabQueueItems: PropTypes.func.isRequired, diff --git a/frontend/src/Activity/Queue/QueueFilterModal.tsx b/frontend/src/Activity/Queue/QueueFilterModal.tsx new file mode 100644 index 000000000..3fce6c166 --- /dev/null +++ b/frontend/src/Activity/Queue/QueueFilterModal.tsx @@ -0,0 +1,54 @@ +import React, { useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { createSelector } from 'reselect'; +import AppState from 'App/State/AppState'; +import FilterModal from 'Components/Filter/FilterModal'; +import { setQueueFilter } from 'Store/Actions/queueActions'; + +function createQueueSelector() { + return createSelector( + (state: AppState) => state.queue.paged.items, + (queueItems) => { + return queueItems; + } + ); +} + +function createFilterBuilderPropsSelector() { + return createSelector( + (state: AppState) => state.queue.paged.filterBuilderProps, + (filterBuilderProps) => { + return filterBuilderProps; + } + ); +} + +interface QueueFilterModalProps { + isOpen: boolean; +} + +export default function QueueFilterModal(props: QueueFilterModalProps) { + const sectionItems = useSelector(createQueueSelector()); + const filterBuilderProps = useSelector(createFilterBuilderPropsSelector()); + const customFilterType = 'queue'; + + const dispatch = useDispatch(); + + const dispatchSetFilter = useCallback( + (payload: unknown) => { + dispatch(setQueueFilter(payload)); + }, + [dispatch] + ); + + return ( + + ); +} diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index d511963fc..cabc39b1c 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,4 +1,5 @@ import SortDirection from 'Helpers/Props/SortDirection'; +import { FilterBuilderProp } from './AppState'; export interface Error { responseJSON: { @@ -20,6 +21,10 @@ export interface PagedAppSectionState { pageSize: number; } +export interface AppSectionFilterState { + filterBuilderProps: FilterBuilderProp[]; +} + export interface AppSectionSchemaState { isSchemaFetching: boolean; isSchemaPopulated: boolean; diff --git a/frontend/src/App/State/QueueAppState.ts b/frontend/src/App/State/QueueAppState.ts index 05fc5a59a..a2936109d 100644 --- a/frontend/src/App/State/QueueAppState.ts +++ b/frontend/src/App/State/QueueAppState.ts @@ -2,7 +2,11 @@ import ModelBase from 'App/ModelBase'; import Language from 'Language/Language'; import { QualityModel } from 'Quality/Quality'; import CustomFormat from 'typings/CustomFormat'; -import AppSectionState, { AppSectionItemState, Error } from './AppSectionState'; +import AppSectionState, { + AppSectionFilterState, + AppSectionItemState, + Error, +} from './AppSectionState'; export interface StatusMessage { title: string; @@ -37,7 +41,9 @@ export interface QueueDetailsAppState extends AppSectionState { params: unknown; } -export interface QueuePagedAppState extends AppSectionState { +export interface QueuePagedAppState + extends AppSectionState, + AppSectionFilterState { isGrabbing: boolean; grabError: Error; isRemoving: boolean; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index ee92b395d..6591bc4b7 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -7,9 +7,11 @@ import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; +import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; import QualityFilterBuilderRowValueConnector from './QualityFilterBuilderRowValueConnector'; import QualityProfileFilterBuilderRowValueConnector from './QualityProfileFilterBuilderRowValueConnector'; +import SeriesFilterBuilderRowValue from './SeriesFilterBuilderRowValue'; import SeriesStatusFilterBuilderRowValue from './SeriesStatusFilterBuilderRowValue'; import SeriesTypeFilterBuilderRowValue from './SeriesTypeFilterBuilderRowValue'; import TagFilterBuilderRowValueConnector from './TagFilterBuilderRowValueConnector'; @@ -60,6 +62,9 @@ function getRowValueConnector(selectedFilterBuilderProp) { case filterBuilderValueTypes.INDEXER: return IndexerFilterBuilderRowValueConnector; + case filterBuilderValueTypes.LANGUAGE: + return LanguageFilterBuilderRowValue; + case filterBuilderValueTypes.PROTOCOL: return ProtocolFilterBuilderRowValue; @@ -69,6 +74,9 @@ function getRowValueConnector(selectedFilterBuilderProp) { case filterBuilderValueTypes.QUALITY_PROFILE: return QualityProfileFilterBuilderRowValueConnector; + case filterBuilderValueTypes.SERIES: + return SeriesFilterBuilderRowValue; + case filterBuilderValueTypes.SERIES_STATUS: return SeriesStatusFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts new file mode 100644 index 000000000..5bf9e5785 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts @@ -0,0 +1,16 @@ +import { FilterBuilderProp } from 'App/State/AppState'; + +interface FilterBuilderRowOnChangeProps { + name: string; + value: unknown[]; +} + +interface FilterBuilderRowValueProps { + filterType?: string; + filterValue: string | number | object | string[] | number[] | object[]; + selectedFilterBuilderProp: FilterBuilderProp; + sectionItem: unknown[]; + onChange: (payload: FilterBuilderRowOnChangeProps) => void; +} + +export default FilterBuilderRowValueProps; diff --git a/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx new file mode 100644 index 000000000..e828fd848 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/LanguageFilterBuilderRowValue.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import createLanguagesSelector from 'Store/Selectors/createLanguagesSelector'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; + +function LanguageFilterBuilderRowValue(props: FilterBuilderRowValueProps) { + const { items } = useSelector(createLanguagesSelector()); + + return ; +} + +export default LanguageFilterBuilderRowValue; diff --git a/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx new file mode 100644 index 000000000..418874fc7 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/SeriesFilterBuilderRowValue.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { useSelector } from 'react-redux'; +import Series from 'Series/Series'; +import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector'; +import FilterBuilderRowValue from './FilterBuilderRowValue'; +import FilterBuilderRowValueProps from './FilterBuilderRowValueProps'; + +function SeriesFilterBuilderRowValue(props: FilterBuilderRowValueProps) { + const allSeries: Series[] = useSelector(createAllSeriesSelector()); + + const tagList = allSeries.map((series) => { + return { + id: series.id, + name: series.title, + }; + }); + + return ; +} + +export default SeriesFilterBuilderRowValue; diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js index a1f8f499d..49b6fcbb6 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js @@ -3,9 +3,11 @@ export const BYTES = 'bytes'; export const DATE = 'date'; export const DEFAULT = 'default'; export const INDEXER = 'indexer'; +export const LANGUAGE = 'language'; export const PROTOCOL = 'protocol'; export const QUALITY = 'quality'; export const QUALITY_PROFILE = 'qualityProfile'; +export const SERIES = 'series'; export const SERIES_STATUS = 'seriesStatus'; export const SERIES_TYPES = 'seriesType'; export const TAG = 'tag'; diff --git a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js index a80ee1e45..f5ef10a4d 100644 --- a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js +++ b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js @@ -6,6 +6,8 @@ import getSectionState from 'Utilities/State/getSectionState'; import { set, updateServerSideCollection } from '../baseActions'; function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) { + const [baseSection] = section.split('.'); + return function(getState, payload, dispatch) { dispatch(set({ section, isFetching: true })); @@ -25,10 +27,13 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const { selectedFilterKey, - filters, - customFilters + filters } = sectionState; + const customFilters = getState().customFilters.items.filter((customFilter) => { + return customFilter.type === section || customFilter.type === baseSection; + }); + const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters); selectedFilters.forEach((filter) => { @@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const promise = createAjaxRequest({ url, - data + data, + traditional: true }).request; promise.done((response) => { diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js index 0e0febea6..4fece9523 100644 --- a/frontend/src/Store/Actions/calendarActions.js +++ b/frontend/src/Store/Actions/calendarActions.js @@ -52,8 +52,6 @@ export const defaultState = { selectedFilterKey: 'monitored', - customFilters: [], - filters: [ { key: 'all', diff --git a/frontend/src/Store/Actions/queueActions.js b/frontend/src/Store/Actions/queueActions.js index 6c4ee38cc..15ea35561 100644 --- a/frontend/src/Store/Actions/queueActions.js +++ b/frontend/src/Store/Actions/queueActions.js @@ -3,7 +3,7 @@ import React from 'react'; import { createAction } from 'redux-actions'; import { batchActions } from 'redux-batched-actions'; import Icon from 'Components/Icon'; -import { icons, sortDirections } from 'Helpers/Props'; +import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props'; import { createThunk, handleThunks } from 'Store/thunks'; import createAjaxRequest from 'Utilities/createAjaxRequest'; import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers'; @@ -170,6 +170,43 @@ export const defaultState = { isVisible: true, isModifiable: false } + ], + + selectedFilterKey: 'all', + + filters: [ + { + key: 'all', + label: 'All', + filters: [] + } + ], + + filterBuilderProps: [ + { + name: 'seriesIds', + label: 'Series', + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.SERIES + }, + { + name: 'quality', + label: 'Quality', + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.QUALITY + }, + { + name: 'languages', + label: 'Languages', + type: filterBuilderTypes.CONTAINS, + valueType: filterBuilderValueTypes.LANGUAGE + }, + { + name: 'protocol', + label: 'Protocol', + type: filterBuilderTypes.EQUAL, + valueType: filterBuilderValueTypes.PROTOCOL + } ] } }; @@ -179,7 +216,8 @@ export const persistState = [ 'queue.paged.pageSize', 'queue.paged.sortKey', 'queue.paged.sortDirection', - 'queue.paged.columns' + 'queue.paged.columns', + 'queue.paged.selectedFilterKey' ]; // @@ -204,6 +242,7 @@ export const GOTO_NEXT_QUEUE_PAGE = 'queue/gotoQueueNextPage'; export const GOTO_LAST_QUEUE_PAGE = 'queue/gotoQueueLastPage'; export const GOTO_QUEUE_PAGE = 'queue/gotoQueuePage'; export const SET_QUEUE_SORT = 'queue/setQueueSort'; +export const SET_QUEUE_FILTER = 'queue/setQueueFilter'; export const SET_QUEUE_TABLE_OPTION = 'queue/setQueueTableOption'; export const SET_QUEUE_OPTION = 'queue/setQueueOption'; export const CLEAR_QUEUE = 'queue/clearQueue'; @@ -228,6 +267,7 @@ export const gotoQueueNextPage = createThunk(GOTO_NEXT_QUEUE_PAGE); export const gotoQueueLastPage = createThunk(GOTO_LAST_QUEUE_PAGE); export const gotoQueuePage = createThunk(GOTO_QUEUE_PAGE); export const setQueueSort = createThunk(SET_QUEUE_SORT); +export const setQueueFilter = createThunk(SET_QUEUE_FILTER); export const setQueueTableOption = createAction(SET_QUEUE_TABLE_OPTION); export const setQueueOption = createAction(SET_QUEUE_OPTION); export const clearQueue = createAction(CLEAR_QUEUE); @@ -279,7 +319,8 @@ export const actionHandlers = handleThunks({ [serverSideCollectionHandlers.NEXT_PAGE]: GOTO_NEXT_QUEUE_PAGE, [serverSideCollectionHandlers.LAST_PAGE]: GOTO_LAST_QUEUE_PAGE, [serverSideCollectionHandlers.EXACT_PAGE]: GOTO_QUEUE_PAGE, - [serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT + [serverSideCollectionHandlers.SORT]: SET_QUEUE_SORT, + [serverSideCollectionHandlers.FILTER]: SET_QUEUE_FILTER }, fetchDataAugmenter ), diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json index f772d5047..47e36054a 100644 --- a/src/NzbDrone.Core/Localization/Core/en.json +++ b/src/NzbDrone.Core/Localization/Core/en.json @@ -1008,6 +1008,7 @@ "QualitySettings": "Quality Settings", "QualitySettingsSummary": "Quality sizes and naming", "Queue": "Queue", + "QueueFilterHasNoItems": "Selected queue filter has no items", "QueueIsEmpty": "Queue is empty", "QueueLoadError": "Failed to load Queue", "Queued": "Queued", diff --git a/src/Sonarr.Api.V3/Queue/QueueController.cs b/src/Sonarr.Api.V3/Queue/QueueController.cs index edb631ee4..c3e4c1d52 100644 --- a/src/Sonarr.Api.V3/Queue/QueueController.cs +++ b/src/Sonarr.Api.V3/Queue/QueueController.cs @@ -9,6 +9,7 @@ using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.Download; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Download.TrackedDownloads; +using NzbDrone.Core.Indexers; using NzbDrone.Core.Languages; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Profiles.Qualities; @@ -135,15 +136,15 @@ namespace Sonarr.Api.V3.Queue [HttpGet] [Produces("application/json")] - public PagingResource GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false) + public PagingResource GetQueue([FromQuery] PagingRequestResource paging, bool includeUnknownSeriesItems = false, bool includeSeries = false, bool includeEpisode = false, [FromQuery] int[] seriesIds = null, DownloadProtocol? protocol = null, [FromQuery] int[] languages = null, int? quality = null) { var pagingResource = new PagingResource(paging); var pagingSpec = pagingResource.MapToPagingSpec("timeleft", SortDirection.Ascending); - return pagingSpec.ApplyToPage((spec) => GetQueue(spec, includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); + return pagingSpec.ApplyToPage((spec) => GetQueue(spec, seriesIds?.ToHashSet(), protocol, languages?.ToHashSet(), quality, includeUnknownSeriesItems), (q) => MapToResource(q, includeSeries, includeEpisode)); } - private PagingSpec GetQueue(PagingSpec pagingSpec, bool includeUnknownSeriesItems) + private PagingSpec GetQueue(PagingSpec pagingSpec, HashSet seriesIds, DownloadProtocol? protocol, HashSet languages, int? quality, bool includeUnknownSeriesItems) { var ascending = pagingSpec.SortDirection == SortDirection.Ascending; var orderByFunc = GetOrderByFunc(pagingSpec); @@ -151,7 +152,36 @@ namespace Sonarr.Api.V3.Queue var queue = _queueService.GetQueue(); var filteredQueue = includeUnknownSeriesItems ? queue : queue.Where(q => q.Series != null); var pending = _pendingReleaseService.GetPendingQueue(); - var fullQueue = filteredQueue.Concat(pending).ToList(); + + var hasSeriesIdFilter = seriesIds.Any(); + var hasLanguageFilter = languages.Any(); + var fullQueue = filteredQueue.Concat(pending).Where(q => + { + var include = true; + + if (hasSeriesIdFilter) + { + include &= q.Series != null && seriesIds.Contains(q.Series.Id); + } + + if (include && protocol.HasValue) + { + include &= q.Protocol == protocol.Value; + } + + if (include && hasLanguageFilter) + { + include &= q.Languages.Any(l => languages.Contains(l.Id)); + } + + if (include && quality.HasValue) + { + include &= q.Quality.Quality.Id == quality.Value; + } + + return include; + }).ToList(); + IOrderedEnumerable ordered; if (pagingSpec.SortKey == "timeleft")