diff --git a/frontend/src/Activity/History/History.js b/frontend/src/Activity/History/History.js
index 5c830ee1a..d144a5402 100644
--- a/frontend/src/Activity/History/History.js
+++ b/frontend/src/Activity/History/History.js
@@ -15,6 +15,7 @@ import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
+import HistoryFilterModal from './HistoryFilterModal';
import HistoryRowConnector from './HistoryRowConnector';
class History extends Component {
@@ -52,6 +53,7 @@ class History extends Component {
columns,
selectedFilterKey,
filters,
+ customFilters,
totalRecords,
isArtistFetching,
isArtistPopulated,
@@ -94,7 +96,8 @@ class History extends Component {
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
- customFilters={[]}
+ customFilters={customFilters}
+ filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={onFilterSelect}
/>
@@ -165,8 +168,9 @@ History.propTypes = {
error: PropTypes.object,
items: PropTypes.arrayOf(PropTypes.object).isRequired,
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
- selectedFilterKey: PropTypes.string.isRequired,
+ selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
+ customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
totalRecords: PropTypes.number,
isArtistFetching: PropTypes.bool.isRequired,
isArtistPopulated: PropTypes.bool.isRequired,
diff --git a/frontend/src/Activity/History/HistoryConnector.js b/frontend/src/Activity/History/HistoryConnector.js
index 801aaf0e0..2b3354bc5 100644
--- a/frontend/src/Activity/History/HistoryConnector.js
+++ b/frontend/src/Activity/History/HistoryConnector.js
@@ -6,6 +6,7 @@ import withCurrentPage from 'Components/withCurrentPage';
import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions';
import * as historyActions from 'Store/Actions/historyActions';
import { clearTracks, fetchTracks } from 'Store/Actions/trackActions';
+import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
@@ -17,7 +18,8 @@ function createMapStateToProps() {
(state) => state.artist,
(state) => state.albums,
(state) => state.tracks,
- (history, artist, albums, tracks) => {
+ createCustomFiltersSelector('history'),
+ (history, artist, albums, tracks, customFilters) => {
return {
isArtistFetching: artist.isFetching,
isArtistPopulated: artist.isPopulated,
@@ -27,6 +29,7 @@ function createMapStateToProps() {
isTracksFetching: tracks.isFetching,
isTracksPopulated: tracks.isPopulated,
tracksError: tracks.error,
+ customFilters,
...history
};
}
diff --git a/frontend/src/Activity/History/HistoryFilterModal.tsx b/frontend/src/Activity/History/HistoryFilterModal.tsx
new file mode 100644
index 000000000..f4ad2e57c
--- /dev/null
+++ b/frontend/src/Activity/History/HistoryFilterModal.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 { setHistoryFilter } from 'Store/Actions/historyActions';
+
+function createHistorySelector() {
+ return createSelector(
+ (state: AppState) => state.history.items,
+ (queueItems) => {
+ return queueItems;
+ }
+ );
+}
+
+function createFilterBuilderPropsSelector() {
+ return createSelector(
+ (state: AppState) => state.history.filterBuilderProps,
+ (filterBuilderProps) => {
+ return filterBuilderProps;
+ }
+ );
+}
+
+interface HistoryFilterModalProps {
+ isOpen: boolean;
+}
+
+export default function HistoryFilterModal(props: HistoryFilterModalProps) {
+ const sectionItems = useSelector(createHistorySelector());
+ const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
+ const customFilterType = 'history';
+
+ const dispatch = useDispatch();
+
+ const dispatchSetFilter = useCallback(
+ (payload: unknown) => {
+ dispatch(setHistoryFilter(payload));
+ },
+ [dispatch]
+ );
+
+ return (
+
+ );
+}
diff --git a/frontend/src/App/State/AppState.ts b/frontend/src/App/State/AppState.ts
index 5b7b01252..5add86a4e 100644
--- a/frontend/src/App/State/AppState.ts
+++ b/frontend/src/App/State/AppState.ts
@@ -1,5 +1,6 @@
import AlbumAppState from './AlbumAppState';
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
+import HistoryAppState from './HistoryAppState';
import QueueAppState from './QueueAppState';
import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState';
@@ -42,6 +43,7 @@ interface AppState {
albums: AlbumAppState;
artist: ArtistAppState;
artistIndex: ArtistIndexAppState;
+ history: HistoryAppState;
queue: QueueAppState;
settings: SettingsAppState;
tags: TagsAppState;
diff --git a/frontend/src/App/State/HistoryAppState.ts b/frontend/src/App/State/HistoryAppState.ts
new file mode 100644
index 000000000..e368ff86e
--- /dev/null
+++ b/frontend/src/App/State/HistoryAppState.ts
@@ -0,0 +1,10 @@
+import AppSectionState, {
+ AppSectionFilterState,
+} from 'App/State/AppSectionState';
+import History from 'typings/History';
+
+interface HistoryAppState
+ extends AppSectionState,
+ AppSectionFilterState {}
+
+export default HistoryAppState;
diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js
index 91b9ccc64..f613d20bc 100644
--- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js
+++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js
@@ -8,6 +8,7 @@ import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowVal
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
+import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
@@ -58,6 +59,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
case filterBuilderValueTypes.DATE:
return DateFilterBuilderRowValue;
+ case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
+ return HistoryEventTypeFilterBuilderRowValue;
+
case filterBuilderValueTypes.INDEXER:
return IndexerFilterBuilderRowValueConnector;
diff --git a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx
new file mode 100644
index 000000000..45e6fc756
--- /dev/null
+++ b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx
@@ -0,0 +1,69 @@
+import React from 'react';
+import translate from 'Utilities/String/translate';
+import FilterBuilderRowValue from './FilterBuilderRowValue';
+import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
+
+const EVENT_TYPE_OPTIONS = [
+ {
+ id: 1,
+ get name() {
+ return translate('Grabbed');
+ },
+ },
+ {
+ id: 3,
+ get name() {
+ return translate('TrackImported');
+ },
+ },
+ {
+ id: 4,
+ get name() {
+ return translate('DownloadFailed');
+ },
+ },
+ {
+ id: 7,
+ get name() {
+ return translate('ImportFailed');
+ },
+ },
+ {
+ id: 8,
+ get name() {
+ return translate('DownloadImported');
+ },
+ },
+ {
+ id: 5,
+ get name() {
+ return translate('Deleted');
+ },
+ },
+ {
+ id: 6,
+ get name() {
+ return translate('Renamed');
+ },
+ },
+ {
+ id: 9,
+ get name() {
+ return translate('Retagged');
+ },
+ },
+ {
+ id: 7,
+ get name() {
+ return translate('Ignored');
+ },
+ },
+];
+
+function HistoryEventTypeFilterBuilderRowValue(
+ props: FilterBuilderRowValueProps
+) {
+ return ;
+}
+
+export default HistoryEventTypeFilterBuilderRowValue;
diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js
index a2f982404..b19d16c8a 100644
--- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js
+++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js
@@ -2,6 +2,7 @@ export const BOOL = 'bool';
export const BYTES = 'bytes';
export const DATE = 'date';
export const DEFAULT = 'default';
+export const HISTORY_EVENT_TYPE = 'historyEventType';
export const INDEXER = 'indexer';
export const METADATA_PROFILE = 'metadataProfile';
export const PROTOCOL = 'protocol';
diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js
index 9dc212645..2fddabbda 100644
--- a/frontend/src/Store/Actions/historyActions.js
+++ b/frontend/src/Store/Actions/historyActions.js
@@ -1,7 +1,7 @@
import React from 'react';
import { createAction } from 'redux-actions';
import Icon from 'Components/Icon';
-import { filterTypes, icons, sortDirections } from 'Helpers/Props';
+import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
@@ -219,6 +219,27 @@ export const defaultState = {
}
]
}
+ ],
+
+ filterBuilderProps: [
+ {
+ name: 'eventType',
+ label: () => translate('EventType'),
+ type: filterBuilderTypes.EQUAL,
+ valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE
+ },
+ {
+ name: 'artistIds',
+ label: () => translate('Artist'),
+ type: filterBuilderTypes.EQUAL,
+ valueType: filterBuilderValueTypes.ARTIST
+ },
+ {
+ name: 'quality',
+ label: () => translate('Quality'),
+ type: filterBuilderTypes.EQUAL,
+ valueType: filterBuilderValueTypes.QUALITY
+ }
]
};
diff --git a/frontend/src/typings/History.ts b/frontend/src/typings/History.ts
new file mode 100644
index 000000000..55e004742
--- /dev/null
+++ b/frontend/src/typings/History.ts
@@ -0,0 +1,29 @@
+import { QualityModel } from 'Quality/Quality';
+import CustomFormat from './CustomFormat';
+
+export type HistoryEventType =
+ | 'grabbed'
+ | 'artistFolderImported'
+ | 'trackFileImported'
+ | 'downloadFailed'
+ | 'trackFileDeleted'
+ | 'trackFileRenamed'
+ | 'albumImportIncomplete'
+ | 'downloadImported'
+ | 'trackFileRetagged'
+ | 'downloadIgnored';
+
+export default interface History {
+ episodeId: number;
+ seriesId: number;
+ sourceTitle: string;
+ quality: QualityModel;
+ customFormats: CustomFormat[];
+ customFormatScore: number;
+ qualityCutoffNotMet: boolean;
+ date: string;
+ downloadId: string;
+ eventType: HistoryEventType;
+ data: unknown;
+ id: number;
+}
diff --git a/src/Lidarr.Api.V1/History/HistoryController.cs b/src/Lidarr.Api.V1/History/HistoryController.cs
index 7c8197209..150ef3b08 100644
--- a/src/Lidarr.Api.V1/History/HistoryController.cs
+++ b/src/Lidarr.Api.V1/History/HistoryController.cs
@@ -68,7 +68,7 @@ namespace Lidarr.Api.V1.History
[HttpGet]
[Produces("application/json")]
- public PagingResource GetHistory([FromQuery] PagingRequestResource paging, bool includeArtist, bool includeAlbum, bool includeTrack, int? eventType, int? albumId, string downloadId)
+ public PagingResource GetHistory([FromQuery] PagingRequestResource paging, bool includeArtist, bool includeAlbum, bool includeTrack, int? eventType, int? albumId, string downloadId, [FromQuery] int[] artistIds = null, [FromQuery] int[] quality = null)
{
var pagingResource = new PagingResource(paging);
var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending);
@@ -89,7 +89,12 @@ namespace Lidarr.Api.V1.History
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
}
- return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h, includeArtist, includeAlbum, includeTrack));
+ if (artistIds != null && artistIds.Any())
+ {
+ pagingSpec.FilterExpressions.Add(h => artistIds.Contains(h.ArtistId));
+ }
+
+ return pagingSpec.ApplyToPage(h => _historyService.Paged(pagingSpec, quality), h => MapToResource(h, includeArtist, includeAlbum, includeTrack));
}
[HttpGet("since")]
diff --git a/src/NzbDrone.Core/Analytics/AnalyticsService.cs b/src/NzbDrone.Core/Analytics/AnalyticsService.cs
index f22d3d908..bca32969c 100644
--- a/src/NzbDrone.Core/Analytics/AnalyticsService.cs
+++ b/src/NzbDrone.Core/Analytics/AnalyticsService.cs
@@ -30,7 +30,7 @@ namespace NzbDrone.Core.Analytics
{
get
{
- var lastRecord = _historyService.Paged(new PagingSpec() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending });
+ var lastRecord = _historyService.Paged(new PagingSpec() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending }, null);
var monthAgo = DateTime.UtcNow.AddMonths(-1);
return lastRecord.Records.Any(v => v.Date > monthAgo);
diff --git a/src/NzbDrone.Core/Datastore/BasicRepository.cs b/src/NzbDrone.Core/Datastore/BasicRepository.cs
index 009b6614e..49013dd8a 100644
--- a/src/NzbDrone.Core/Datastore/BasicRepository.cs
+++ b/src/NzbDrone.Core/Datastore/BasicRepository.cs
@@ -407,7 +407,7 @@ namespace NzbDrone.Core.Datastore
return pagingSpec;
}
- private void AddFilters(SqlBuilder builder, PagingSpec pagingSpec)
+ protected void AddFilters(SqlBuilder builder, PagingSpec pagingSpec)
{
var filters = pagingSpec.FilterExpressions;
diff --git a/src/NzbDrone.Core/History/EntityHistoryRepository.cs b/src/NzbDrone.Core/History/EntityHistoryRepository.cs
index 4491bed11..e5f427bc0 100644
--- a/src/NzbDrone.Core/History/EntityHistoryRepository.cs
+++ b/src/NzbDrone.Core/History/EntityHistoryRepository.cs
@@ -18,6 +18,7 @@ namespace NzbDrone.Core.History
List FindDownloadHistory(int idArtistId, QualityModel quality);
void DeleteForArtists(List artistIds);
List Since(DateTime date, EntityHistoryEventType? eventType);
+ PagingSpec GetPaged(PagingSpec pagingSpec, int[] qualities);
}
public class EntityHistoryRepository : BasicRepository, IHistoryRepository
@@ -98,19 +99,6 @@ namespace NzbDrone.Core.History
Delete(c => artistIds.Contains(c.ArtistId));
}
- protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
- .Join((h, a) => h.ArtistId == a.Id)
- .Join((h, a) => h.AlbumId == a.Id)
- .LeftJoin((h, t) => h.TrackId == t.Id);
- protected override IEnumerable PagedQuery(SqlBuilder builder) =>
- _database.QueryJoined(builder, (history, artist, album, track) =>
- {
- history.Artist = artist;
- history.Album = album;
- history.Track = track;
- return history;
- });
-
public List Since(DateTime date, EntityHistoryEventType? eventType)
{
var builder = Builder()
@@ -130,5 +118,53 @@ namespace NzbDrone.Core.History
return history;
}).OrderBy(h => h.Date).ToList();
}
+
+ public PagingSpec GetPaged(PagingSpec pagingSpec, int[] qualities)
+ {
+ pagingSpec.Records = GetPagedRecords(PagedBuilder(pagingSpec, qualities), pagingSpec, PagedQuery);
+
+ var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(EntityHistory))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
+ pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder(pagingSpec, qualities).Select(typeof(EntityHistory)), pagingSpec, countTemplate);
+
+ return pagingSpec;
+ }
+
+ private SqlBuilder PagedBuilder(PagingSpec pagingSpec, int[] qualities)
+ {
+ var builder = Builder()
+ .Join((h, a) => h.ArtistId == a.Id)
+ .Join((h, a) => h.AlbumId == a.Id)
+ .LeftJoin((h, t) => h.TrackId == t.Id);
+
+ AddFilters(builder, pagingSpec);
+
+ if (qualities is { Length: > 0 })
+ {
+ builder.Where($"({BuildQualityWhereClause(qualities)})");
+ }
+
+ return builder;
+ }
+
+ protected override IEnumerable PagedQuery(SqlBuilder builder) =>
+ _database.QueryJoined(builder, (history, artist, album, track) =>
+ {
+ history.Artist = artist;
+ history.Album = album;
+ history.Track = track;
+ return history;
+ });
+
+ private string BuildQualityWhereClause(int[] qualities)
+ {
+ var clauses = new List();
+
+ foreach (var quality in qualities)
+ {
+ clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EntityHistory))}\".\"Quality\" LIKE '%_quality_: {quality},%'");
+ }
+
+ return $"({string.Join(" OR ", clauses)})";
+ }
}
}
diff --git a/src/NzbDrone.Core/History/EntityHistoryService.cs b/src/NzbDrone.Core/History/EntityHistoryService.cs
index b1a028af4..061b4b267 100644
--- a/src/NzbDrone.Core/History/EntityHistoryService.cs
+++ b/src/NzbDrone.Core/History/EntityHistoryService.cs
@@ -18,7 +18,7 @@ namespace NzbDrone.Core.History
{
public interface IHistoryService
{
- PagingSpec Paged(PagingSpec pagingSpec);
+ PagingSpec Paged(PagingSpec pagingSpec, int[] qualities);
EntityHistory MostRecentForAlbum(int albumId);
EntityHistory MostRecentForDownloadId(string downloadId);
EntityHistory Get(int historyId);
@@ -52,9 +52,9 @@ namespace NzbDrone.Core.History
_logger = logger;
}
- public PagingSpec Paged(PagingSpec pagingSpec)
+ public PagingSpec Paged(PagingSpec pagingSpec, int[] qualities)
{
- return _historyRepository.GetPaged(pagingSpec);
+ return _historyRepository.GetPaged(pagingSpec, qualities);
}
public EntityHistory MostRecentForAlbum(int albumId)