parent
7c0d344437
commit
62b948b24c
|
@ -1,4 +1,5 @@
|
|||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||
import CalendarAppState from './CalendarAppState';
|
||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||
import EpisodesAppState from './EpisodesAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
|
@ -39,6 +40,7 @@ export interface CustomFilter {
|
|||
}
|
||||
|
||||
interface AppState {
|
||||
calendar: CalendarAppState;
|
||||
episodesSelection: EpisodesAppState;
|
||||
episodeFiles: EpisodeFilesAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import AppSectionState from 'App/State/AppSectionState';
|
||||
import Episode from 'Episode/Episode';
|
||||
import { FilterBuilderProp } from './AppState';
|
||||
|
||||
interface CalendarAppState extends AppSectionState<Episode> {
|
||||
filterBuilderProps: FilterBuilderProp<Episode>[];
|
||||
}
|
||||
|
||||
export default CalendarAppState;
|
|
@ -0,0 +1,56 @@
|
|||
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 { setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||
|
||||
function createCalendarSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.items,
|
||||
(calendar) => {
|
||||
return calendar;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.calendar.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface SeriesIndexFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function CalendarFilterModal(
|
||||
props: SeriesIndexFilterModalProps
|
||||
) {
|
||||
const sectionItems = useSelector(createCalendarSelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'calendar';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setCalendarFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -11,6 +11,7 @@ import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
|||
import { align, icons } from 'Helpers/Props';
|
||||
import NoSeries from 'Series/NoSeries';
|
||||
import CalendarConnector from './CalendarConnector';
|
||||
import CalendarFilterModal from './CalendarFilterModal';
|
||||
import CalendarLinkModal from './iCal/CalendarLinkModal';
|
||||
import LegendConnector from './Legend/LegendConnector';
|
||||
import CalendarOptionsModal from './Options/CalendarOptionsModal';
|
||||
|
@ -75,6 +76,7 @@ class CalendarPage extends Component {
|
|||
const {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
hasSeries,
|
||||
missingEpisodeIds,
|
||||
isRssSyncExecuting,
|
||||
|
@ -132,7 +134,8 @@ class CalendarPage extends Component {
|
|||
isDisabled={!hasSeries}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={CalendarFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
@ -178,6 +181,7 @@ class CalendarPage extends Component {
|
|||
CalendarPage.propTypes = {
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasSeries: PropTypes.bool.isRequired,
|
||||
missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -5,6 +5,7 @@ import * as commandNames from 'Commands/commandNames';
|
|||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
|
||||
|
@ -59,6 +60,7 @@ function createMapStateToProps() {
|
|||
return createSelector(
|
||||
(state) => state.calendar.selectedFilterKey,
|
||||
(state) => state.calendar.filters,
|
||||
createCustomFiltersSelector('calendar'),
|
||||
createSeriesCountSelector(),
|
||||
createUISettingsSelector(),
|
||||
createMissingEpisodeIdsSelector(),
|
||||
|
@ -67,6 +69,7 @@ function createMapStateToProps() {
|
|||
(
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
seriesCount,
|
||||
uiSettings,
|
||||
missingEpisodeIds,
|
||||
|
@ -76,6 +79,7 @@ function createMapStateToProps() {
|
|||
return {
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||
hasSeries: !!seriesCount,
|
||||
missingEpisodeIds,
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import * as filterTypes from './filterTypes';
|
||||
|
||||
export const ARRAY = 'array';
|
||||
export const CONTAINS = 'contains';
|
||||
export const DATE = 'date';
|
||||
export const EQUAL = 'equal';
|
||||
export const EXACT = 'exact';
|
||||
export const NUMBER = 'number';
|
||||
export const STRING = 'string';
|
||||
|
||||
export const all = [
|
||||
ARRAY,
|
||||
CONTAINS,
|
||||
DATE,
|
||||
EQUAL,
|
||||
EXACT,
|
||||
NUMBER,
|
||||
STRING
|
||||
|
@ -20,6 +24,10 @@ export const possibleFilterTypes = {
|
|||
{ key: filterTypes.NOT_CONTAINS, value: 'does not contain' }
|
||||
],
|
||||
|
||||
[CONTAINS]: [
|
||||
{ key: filterTypes.CONTAINS, value: 'contains' }
|
||||
],
|
||||
|
||||
[DATE]: [
|
||||
{ key: filterTypes.LESS_THAN, value: 'is before' },
|
||||
{ key: filterTypes.GREATER_THAN, value: 'is after' },
|
||||
|
@ -29,6 +37,10 @@ export const possibleFilterTypes = {
|
|||
{ key: filterTypes.NOT_IN_NEXT, value: 'not in the next' }
|
||||
],
|
||||
|
||||
[EQUAL]: [
|
||||
{ key: filterTypes.EQUAL, value: 'is' }
|
||||
],
|
||||
|
||||
[EXACT]: [
|
||||
{ key: filterTypes.EQUAL, value: 'is' },
|
||||
{ key: filterTypes.NOT_EQUAL, value: 'is not' }
|
||||
|
|
|
@ -4,9 +4,10 @@ import { createAction } from 'redux-actions';
|
|||
import { batchActions } from 'redux-batched-actions';
|
||||
import * as calendarViews from 'Calendar/calendarViews';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import { filterTypes } from 'Helpers/Props';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes } from 'Helpers/Props';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
|
||||
import { set, update } from './baseActions';
|
||||
import { executeCommandHelper } from './commandActions';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
@ -50,14 +51,16 @@ export const defaultState = {
|
|||
|
||||
selectedFilterKey: 'monitored',
|
||||
|
||||
customFilters: [],
|
||||
|
||||
filters: [
|
||||
{
|
||||
key: 'all',
|
||||
label: 'All',
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: false,
|
||||
key: 'unmonitored',
|
||||
value: [true],
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
|
@ -67,20 +70,35 @@ export const defaultState = {
|
|||
label: 'Monitored Only',
|
||||
filters: [
|
||||
{
|
||||
key: 'monitored',
|
||||
value: true,
|
||||
key: 'unmonitored',
|
||||
value: [false],
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'unmonitored',
|
||||
label: 'Include Unmonitored',
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.BOOL
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
label: 'Tags',
|
||||
type: filterBuilderTypes.CONTAINS,
|
||||
valueType: filterBuilderValueTypes.TAG
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export const persistState = [
|
||||
'calendar.view',
|
||||
'calendar.selectedFilterKey',
|
||||
'calendar.options'
|
||||
'calendar.options',
|
||||
'seriesIndex.customFilters'
|
||||
];
|
||||
|
||||
//
|
||||
|
@ -192,6 +210,10 @@ function isRangePopulated(start, end, state) {
|
|||
return false;
|
||||
}
|
||||
|
||||
function getCustomFilters(state, type) {
|
||||
return state.customFilters.items.filter((customFilter) => customFilter.type === type);
|
||||
}
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
|
@ -213,7 +235,8 @@ export const actionHandlers = handleThunks({
|
|||
[FETCH_CALENDAR]: function(getState, payload, dispatch) {
|
||||
const state = getState();
|
||||
const calendar = state.calendar;
|
||||
const unmonitored = calendar.selectedFilterKey === 'all';
|
||||
const customFilters = getCustomFilters(state, section);
|
||||
const selectedFilters = findSelectedFilters(calendar.selectedFilterKey, calendar.filters, customFilters);
|
||||
|
||||
const {
|
||||
time = calendar.time,
|
||||
|
@ -240,13 +263,26 @@ export const actionHandlers = handleThunks({
|
|||
|
||||
dispatch(set(attrs));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/calendar',
|
||||
data: {
|
||||
unmonitored,
|
||||
const requestParams = {
|
||||
start,
|
||||
end
|
||||
};
|
||||
|
||||
selectedFilters.forEach((selectedFilter) => {
|
||||
if (selectedFilter.key === 'unmonitored') {
|
||||
requestParams.unmonitored = selectedFilter.value.includes(true);
|
||||
}
|
||||
|
||||
if (selectedFilter.key === 'tags') {
|
||||
requestParams.tags = selectedFilter.value.join(',');
|
||||
}
|
||||
});
|
||||
|
||||
requestParams.unmonitored = requestParams.unmonitored ?? false;
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: '/calendar',
|
||||
data: requestParams
|
||||
}).request;
|
||||
|
||||
promise.done((data) => {
|
||||
|
|
|
@ -108,7 +108,7 @@ function sort(items, state) {
|
|||
return _.orderBy(items, clauses, orders);
|
||||
}
|
||||
|
||||
function createCustomFiltersSelector(type, alternateType) {
|
||||
export function createCustomFiltersSelector(type, alternateType) {
|
||||
return createSelector(
|
||||
(state) => state.customFilters.items,
|
||||
(customFilters) => {
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
const thunks = {};
|
||||
|
||||
function identity(payload) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function createThunk(type, identityFunction = identity) {
|
||||
return function(payload = {}) {
|
||||
return function(dispatch, getState) {
|
||||
const thunk = thunks[type];
|
||||
|
||||
if (thunk) {
|
||||
return thunk(getState, identityFunction(payload), dispatch);
|
||||
}
|
||||
|
||||
throw Error(`Thunk handler has not been registered for ${type}`);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleThunks(handlers) {
|
||||
const types = Object.keys(handlers);
|
||||
|
||||
types.forEach((type) => {
|
||||
thunks[type] = handlers[type];
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import { Dispatch } from 'redux';
|
||||
import AppState from 'App/State/AppState';
|
||||
|
||||
type GetState = () => AppState;
|
||||
type Thunk = (
|
||||
getState: GetState,
|
||||
identity: unknown,
|
||||
dispatch: Dispatch
|
||||
) => unknown;
|
||||
|
||||
const thunks: Record<string, Thunk> = {};
|
||||
|
||||
function identity(payload: unknown) {
|
||||
return payload;
|
||||
}
|
||||
|
||||
export function createThunk(type: string, identityFunction = identity) {
|
||||
return function (payload: unknown = {}) {
|
||||
return function (dispatch: Dispatch, getState: GetState) {
|
||||
const thunk = thunks[type];
|
||||
|
||||
if (thunk) {
|
||||
return thunk(getState, identityFunction(payload), dispatch);
|
||||
}
|
||||
|
||||
throw Error(`Thunk handler has not been registered for ${type}`);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export function handleThunks(handlers: Record<string, Thunk>) {
|
||||
const types = Object.keys(handlers);
|
||||
|
||||
types.forEach((type) => {
|
||||
thunks[type] = handlers[type];
|
||||
});
|
||||
}
|
|
@ -2,8 +2,10 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Tags;
|
||||
using NzbDrone.Core.Tv;
|
||||
using NzbDrone.SignalR;
|
||||
using Sonarr.Api.V3.Episodes;
|
||||
|
@ -14,23 +16,53 @@ namespace Sonarr.Api.V3.Calendar
|
|||
[V3ApiController]
|
||||
public class CalendarController : EpisodeControllerWithSignalR
|
||||
{
|
||||
private readonly ITagService _tagService;
|
||||
|
||||
public CalendarController(IBroadcastSignalRMessage signalR,
|
||||
IEpisodeService episodeService,
|
||||
ISeriesService seriesService,
|
||||
IUpgradableSpecification qualityUpgradableSpecification,
|
||||
ITagService tagService,
|
||||
ICustomFormatCalculationService formatCalculator)
|
||||
: base(episodeService, seriesService, qualityUpgradableSpecification, formatCalculator, signalR)
|
||||
{
|
||||
_tagService = tagService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public List<EpisodeResource> GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeSeries = false, bool includeEpisodeFile = false, bool includeEpisodeImages = false)
|
||||
public List<EpisodeResource> GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeSeries = false, bool includeEpisodeFile = false, bool includeEpisodeImages = false, string tags = "")
|
||||
{
|
||||
var startUse = start ?? DateTime.Today;
|
||||
var endUse = end ?? DateTime.Today.AddDays(2);
|
||||
var episodes = _episodeService.EpisodesBetweenDates(startUse, endUse, unmonitored);
|
||||
var allSeries = _seriesService.GetAllSeries();
|
||||
var parsedTags = new List<int>();
|
||||
var result = new List<Episode>();
|
||||
|
||||
var resources = MapToResource(_episodeService.EpisodesBetweenDates(startUse, endUse, unmonitored), includeSeries, includeEpisodeFile, includeEpisodeImages);
|
||||
if (tags.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
parsedTags.AddRange(tags.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
|
||||
}
|
||||
|
||||
foreach (var episode in episodes)
|
||||
{
|
||||
var series = allSeries.SingleOrDefault(s => s.Id == episode.SeriesId);
|
||||
|
||||
if (series == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (parsedTags.Any() && parsedTags.None(series.Tags.Contains))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(episode);
|
||||
}
|
||||
|
||||
var resources = MapToResource(result, includeSeries, includeEpisodeFile, includeEpisodeImages);
|
||||
|
||||
return resources.OrderBy(e => e.AirDateUtc).ToList();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue