Skip to content

Commit

Permalink
New: Calendar filtering by tags
Browse files Browse the repository at this point in the history
Closes #3658
Closes #4211

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
  • Loading branch information
Qstick and markus101 committed Feb 12, 2024
1 parent c7faf7c commit a4af75b
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 15 deletions.
2 changes: 2 additions & 0 deletions frontend/src/App/State/AppState.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AlbumAppState from './AlbumAppState';
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
import CalendarAppState from './CalendarAppState';
import HistoryAppState from './HistoryAppState';
import QueueAppState from './QueueAppState';
import SettingsAppState from './SettingsAppState';
Expand Down Expand Up @@ -52,6 +53,7 @@ interface AppState {
app: AppSectionState;
artist: ArtistAppState;
artistIndex: ArtistIndexAppState;
calendar: CalendarAppState;
history: HistoryAppState;
queue: QueueAppState;
settings: SettingsAppState;
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/App/State/CalendarAppState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Album from 'Album/Album';
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';

interface CalendarAppState
extends AppSectionState<Album>,
AppSectionFilterState<Album> {}

export default CalendarAppState;
54 changes: 54 additions & 0 deletions frontend/src/Calendar/CalendarFilterModal.tsx
Original file line number Diff line number Diff line change
@@ -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 { 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 CalendarFilterModalProps {
isOpen: boolean;
}

export default function CalendarFilterModal(props: CalendarFilterModalProps) {
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}
/>
);
}
6 changes: 5 additions & 1 deletion frontend/src/Calendar/CalendarPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { align, icons } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import CalendarConnector from './CalendarConnector';
import CalendarFilterModal from './CalendarFilterModal';
import CalendarLinkModal from './iCal/CalendarLinkModal';
import LegendConnector from './Legend/LegendConnector';
import CalendarOptionsModal from './Options/CalendarOptionsModal';
Expand Down Expand Up @@ -78,6 +79,7 @@ class CalendarPage extends Component {
const {
selectedFilterKey,
filters,
customFilters,
hasArtist,
artistError,
artistIsFetching,
Expand Down Expand Up @@ -137,7 +139,8 @@ class CalendarPage extends Component {
isDisabled={!hasArtist}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
customFilters={customFilters}
filterModalConnectorComponent={CalendarFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
Expand Down Expand Up @@ -204,6 +207,7 @@ class CalendarPage extends Component {
CalendarPage.propTypes = {
selectedFilterKey: PropTypes.string.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
hasArtist: PropTypes.bool.isRequired,
artistError: PropTypes.object,
artistIsFetching: PropTypes.bool.isRequired,
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/Calendar/CalendarPageConnector.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 createArtistCountSelector from 'Store/Selectors/createArtistCountSelector';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
Expand Down Expand Up @@ -59,6 +60,7 @@ function createMapStateToProps() {
return createSelector(
(state) => state.calendar.selectedFilterKey,
(state) => state.calendar.filters,
createCustomFiltersSelector('calendar'),
createArtistCountSelector(),
createUISettingsSelector(),
createMissingAlbumIdsSelector(),
Expand All @@ -67,6 +69,7 @@ function createMapStateToProps() {
(
selectedFilterKey,
filters,
customFilters,
artistCount,
uiSettings,
missingAlbumIds,
Expand All @@ -76,6 +79,7 @@ function createMapStateToProps() {
return {
selectedFilterKey,
filters,
customFilters,
colorImpairedMode: uiSettings.enableColorImpairedMode,
hasArtist: !!artistCount.count,
artistError: artistCount.error,
Expand Down
59 changes: 47 additions & 12 deletions frontend/src/Store/Actions/calendarActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 translate from 'Utilities/String/translate';
import { set, update } from './baseActions';
import { executeCommandHelper } from './commandActions';
Expand Down Expand Up @@ -54,8 +55,8 @@ export const defaultState = {
label: () => translate('All'),
filters: [
{
key: 'monitored',
value: false,
key: 'unmonitored',
value: [true],
type: filterTypes.EQUAL
}
]
Expand All @@ -65,19 +66,35 @@ export const defaultState = {
label: () => translate('MonitoredOnly'),
filters: [
{
key: 'monitored',
value: true,
key: 'unmonitored',
value: [false],
type: filterTypes.EQUAL
}
]
}
],

filterBuilderProps: [
{
name: 'unmonitored',
label: () => translate('IncludeUnmonitored'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'tags',
label: () => translate('Tags'),
type: filterBuilderTypes.CONTAINS,
valueType: filterBuilderValueTypes.TAG
}
]
};

export const persistState = [
'calendar.view',
'calendar.selectedFilterKey',
'calendar.options'
'calendar.options',
'calendar.customFilters'
];

//
Expand Down Expand Up @@ -189,6 +206,10 @@ function isRangePopulated(start, end, state) {
return false;
}

function getCustomFilters(state, type) {
return state.customFilters.items.filter((customFilter) => customFilter.type === type);
}

//
// Action Creators

Expand All @@ -210,7 +231,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,
Expand All @@ -237,13 +259,26 @@ export const actionHandlers = handleThunks({

dispatch(set(attrs));

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: {
unmonitored,
start,
end
}
data: requestParams
}).request;

promise.done((data) => {
Expand Down
40 changes: 38 additions & 2 deletions src/Lidarr.Api.V1/Calendar/CalendarController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,72 @@
using Lidarr.Http;
using Lidarr.Http.Extensions;
using Microsoft.AspNetCore.Mvc;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.ArtistStats;
using NzbDrone.Core.DecisionEngine.Specifications;
using NzbDrone.Core.MediaCover;
using NzbDrone.Core.Music;
using NzbDrone.Core.Tags;
using NzbDrone.SignalR;

namespace Lidarr.Api.V1.Calendar
{
[V1ApiController]
public class CalendarController : AlbumControllerWithSignalR
{
private readonly IArtistService _artistService;
private readonly ITagService _tagService;

public CalendarController(IAlbumService albumService,
IArtistService artistService,
IArtistStatisticsService artistStatisticsService,
IMapCoversToLocal coverMapper,
IUpgradableSpecification upgradableSpecification,
ITagService tagService,
IBroadcastSignalRMessage signalRBroadcaster)
: base(albumService, artistStatisticsService, coverMapper, upgradableSpecification, signalRBroadcaster)
{
_artistService = artistService;
_tagService = tagService;
}

[HttpGet]
public List<AlbumResource> GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeArtist = false)
[Produces("application/json")]
public List<AlbumResource> GetCalendar(DateTime? start, DateTime? end, bool unmonitored = false, bool includeArtist = false, string tags = "")
{
// TODO: Add Album Image support to AlbumControllerWithSignalR
var includeAlbumImages = Request.GetBooleanQueryParameter("includeAlbumImages");

var startUse = start ?? DateTime.Today;
var endUse = end ?? DateTime.Today.AddDays(2);
var albums = _albumService.AlbumsBetweenDates(startUse, endUse, unmonitored);
var allArtists = _artistService.GetAllArtists();
var parsedTags = new List<int>();
var result = new List<Album>();

if (tags.IsNotNullOrWhiteSpace())
{
parsedTags.AddRange(tags.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
}

foreach (var album in albums)
{
var artist = allArtists.SingleOrDefault(s => s.Id == album.ArtistId);

if (artist == null)
{
continue;
}

if (parsedTags.Any() && parsedTags.None(artist.Tags.Contains))
{
continue;
}

result.Add(album);
}

var resources = MapToResource(_albumService.AlbumsBetweenDates(startUse, endUse, unmonitored), includeArtist);
var resources = MapToResource(result, includeArtist);

return resources.OrderBy(e => e.ReleaseDate).ToList();
}
Expand Down

0 comments on commit a4af75b

Please sign in to comment.