mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-19 17:32:38 +01:00
New: History custom filters
(cherry picked from commit 2fe8f3084c90688e6dd01d600796396e74f43ff9) Closes #9298
This commit is contained in:
parent
299d50d56c
commit
0591d05c3b
@ -14,6 +14,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
|||||||
import TablePager from 'Components/Table/TablePager';
|
import TablePager from 'Components/Table/TablePager';
|
||||||
import { align, icons, kinds } from 'Helpers/Props';
|
import { align, icons, kinds } from 'Helpers/Props';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import HistoryFilterModal from './HistoryFilterModal';
|
||||||
import HistoryRowConnector from './HistoryRowConnector';
|
import HistoryRowConnector from './HistoryRowConnector';
|
||||||
|
|
||||||
class History extends Component {
|
class History extends Component {
|
||||||
@ -33,6 +34,7 @@ class History extends Component {
|
|||||||
columns,
|
columns,
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
|
customFilters,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
onFirstPagePress,
|
onFirstPagePress,
|
||||||
@ -70,7 +72,8 @@ class History extends Component {
|
|||||||
alignMenu={align.RIGHT}
|
alignMenu={align.RIGHT}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={[]}
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={HistoryFilterModal}
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
@ -144,8 +147,9 @@ History.propTypes = {
|
|||||||
moviesError: PropTypes.object,
|
moviesError: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: 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,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
onFirstPagePress: PropTypes.func.isRequired
|
onFirstPagePress: PropTypes.func.isRequired
|
||||||
|
@ -4,6 +4,7 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import * as historyActions from 'Store/Actions/historyActions';
|
import * as historyActions from 'Store/Actions/historyActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import History from './History';
|
import History from './History';
|
||||||
|
|
||||||
@ -11,11 +12,13 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.history,
|
(state) => state.history,
|
||||||
(state) => state.movies,
|
(state) => state.movies,
|
||||||
(history, movies) => {
|
createCustomFiltersSelector('history'),
|
||||||
|
(history, movies, customFilters) => {
|
||||||
return {
|
return {
|
||||||
isMoviesFetching: movies.isFetching,
|
isMoviesFetching: movies.isFetching,
|
||||||
isMoviesPopulated: movies.isPopulated,
|
isMoviesPopulated: movies.isPopulated,
|
||||||
moviesError: movies.error,
|
moviesError: movies.error,
|
||||||
|
customFilters,
|
||||||
...history
|
...history
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
54
frontend/src/Activity/History/HistoryFilterModal.tsx
Normal file
54
frontend/src/Activity/History/HistoryFilterModal.tsx
Normal file
@ -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 (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,6 +1,7 @@
|
|||||||
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
import InteractiveImportAppState from 'App/State/InteractiveImportAppState';
|
||||||
import CalendarAppState from './CalendarAppState';
|
import CalendarAppState from './CalendarAppState';
|
||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
|
import HistoryAppState from './HistoryAppState';
|
||||||
import MovieCollectionAppState from './MovieCollectionAppState';
|
import MovieCollectionAppState from './MovieCollectionAppState';
|
||||||
import MovieFilesAppState from './MovieFilesAppState';
|
import MovieFilesAppState from './MovieFilesAppState';
|
||||||
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
import MoviesAppState, { MovieIndexAppState } from './MoviesAppState';
|
||||||
@ -46,6 +47,7 @@ export interface CustomFilter {
|
|||||||
interface AppState {
|
interface AppState {
|
||||||
calendar: CalendarAppState;
|
calendar: CalendarAppState;
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
|
history: HistoryAppState;
|
||||||
interactiveImport: InteractiveImportAppState;
|
interactiveImport: InteractiveImportAppState;
|
||||||
movieCollections: MovieCollectionAppState;
|
movieCollections: MovieCollectionAppState;
|
||||||
movieFiles: MovieFilesAppState;
|
movieFiles: MovieFilesAppState;
|
||||||
|
10
frontend/src/App/State/HistoryAppState.ts
Normal file
10
frontend/src/App/State/HistoryAppState.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
|
import History from 'typings/History';
|
||||||
|
|
||||||
|
interface HistoryAppState
|
||||||
|
extends AppSectionState<History>,
|
||||||
|
AppSectionFilterState<History> {}
|
||||||
|
|
||||||
|
export default HistoryAppState;
|
@ -6,6 +6,7 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
|
|||||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
|
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||||
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
|
import ImportListFilterBuilderRowValueConnector from './ImportListFilterBuilderRowValueConnector';
|
||||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||||
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
||||||
@ -60,6 +61,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||||||
case filterBuilderValueTypes.DATE:
|
case filterBuilderValueTypes.DATE:
|
||||||
return DateFilterBuilderRowValue;
|
return DateFilterBuilderRowValue;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||||
|
return HistoryEventTypeFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.INDEXER:
|
case filterBuilderValueTypes.INDEXER:
|
||||||
return IndexerFilterBuilderRowValueConnector;
|
return IndexerFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
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('Imported');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
get name() {
|
||||||
|
return translate('Failed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
get name() {
|
||||||
|
return translate('Deleted');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6,
|
||||||
|
get name() {
|
||||||
|
return translate('Renamed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 7,
|
||||||
|
get name() {
|
||||||
|
return translate('Ignored');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function HistoryEventTypeFilterBuilderRowValue(
|
||||||
|
props: FilterBuilderRowValueProps
|
||||||
|
) {
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryEventTypeFilterBuilderRowValue;
|
@ -2,6 +2,7 @@ export const BOOL = 'bool';
|
|||||||
export const BYTES = 'bytes';
|
export const BYTES = 'bytes';
|
||||||
export const DATE = 'date';
|
export const DATE = 'date';
|
||||||
export const DEFAULT = 'default';
|
export const DEFAULT = 'default';
|
||||||
|
export const HISTORY_EVENT_TYPE = 'historyEventType';
|
||||||
export const INDEXER = 'indexer';
|
export const INDEXER = 'indexer';
|
||||||
export const LANGUAGE = 'language';
|
export const LANGUAGE = 'language';
|
||||||
export const PROTOCOL = 'protocol';
|
export const PROTOCOL = 'protocol';
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import Icon from 'Components/Icon';
|
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 { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||||
@ -177,6 +177,33 @@ export const defaultState = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
filterBuilderProps: [
|
||||||
|
{
|
||||||
|
name: 'eventType',
|
||||||
|
label: () => translate('EventType'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'movieIds',
|
||||||
|
label: () => translate('Movie'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.MOVIE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'quality',
|
||||||
|
label: () => translate('Quality'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.QUALITY
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'languages',
|
||||||
|
label: () => translate('Languages'),
|
||||||
|
type: filterBuilderTypes.CONTAINS,
|
||||||
|
valueType: filterBuilderValueTypes.LANGUAGE
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
};
|
};
|
||||||
|
27
frontend/src/typings/History.ts
Normal file
27
frontend/src/typings/History.ts
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import CustomFormat from './CustomFormat';
|
||||||
|
|
||||||
|
export type HistoryEventType =
|
||||||
|
| 'grabbed'
|
||||||
|
| 'downloadFolderImported'
|
||||||
|
| 'downloadFailed'
|
||||||
|
| 'movieFileDeleted'
|
||||||
|
| 'movieFolderImported'
|
||||||
|
| 'movieFileRenamed'
|
||||||
|
| 'downloadIgnored';
|
||||||
|
|
||||||
|
export default interface History {
|
||||||
|
movieId: number;
|
||||||
|
sourceTitle: string;
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
customFormats: CustomFormat[];
|
||||||
|
customFormatScore: number;
|
||||||
|
qualityCutoffNotMet: boolean;
|
||||||
|
date: string;
|
||||||
|
downloadId: string;
|
||||||
|
eventType: HistoryEventType;
|
||||||
|
data: unknown;
|
||||||
|
id: number;
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using NzbDrone.Common.EnvironmentInfo;
|
using NzbDrone.Common.EnvironmentInfo;
|
||||||
using NzbDrone.Core.Configuration;
|
using NzbDrone.Core.Configuration;
|
||||||
@ -30,7 +30,7 @@ public bool InstallIsActive
|
|||||||
{
|
{
|
||||||
get
|
get
|
||||||
{
|
{
|
||||||
var lastRecord = _historyService.Paged(new PagingSpec<MovieHistory>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending });
|
var lastRecord = _historyService.Paged(new PagingSpec<MovieHistory>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending }, null, null);
|
||||||
var monthAgo = DateTime.UtcNow.AddMonths(-1);
|
var monthAgo = DateTime.UtcNow.AddMonths(-1);
|
||||||
|
|
||||||
return lastRecord.Records.Any(v => v.Date > monthAgo);
|
return lastRecord.Records.Any(v => v.Date > monthAgo);
|
||||||
|
@ -407,7 +407,7 @@ public virtual PagingSpec<TModel> GetPaged(PagingSpec<TModel> pagingSpec)
|
|||||||
return pagingSpec;
|
return pagingSpec;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddFilters(SqlBuilder builder, PagingSpec<TModel> pagingSpec)
|
protected void AddFilters(SqlBuilder builder, PagingSpec<TModel> pagingSpec)
|
||||||
{
|
{
|
||||||
var filters = pagingSpec.FilterExpressions;
|
var filters = pagingSpec.FilterExpressions;
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ public interface IHistoryRepository : IBasicRepository<MovieHistory>
|
|||||||
void DeleteForMovies(List<int> movieIds);
|
void DeleteForMovies(List<int> movieIds);
|
||||||
MovieHistory MostRecentForMovie(int movieId);
|
MovieHistory MostRecentForMovie(int movieId);
|
||||||
List<MovieHistory> Since(DateTime date, MovieHistoryEventType? eventType);
|
List<MovieHistory> Since(DateTime date, MovieHistoryEventType? eventType);
|
||||||
|
PagingSpec<MovieHistory> GetPaged(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class HistoryRepository : BasicRepository<MovieHistory>, IHistoryRepository
|
public class HistoryRepository : BasicRepository<MovieHistory>, IHistoryRepository
|
||||||
@ -74,19 +75,6 @@ public void DeleteForMovies(List<int> movieIds)
|
|||||||
Delete(c => movieIds.Contains(c.MovieId));
|
Delete(c => movieIds.Contains(c.MovieId));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
|
|
||||||
.Join<MovieHistory, Movie>((h, m) => h.MovieId == m.Id)
|
|
||||||
.Join<Movie, QualityProfile>((m, p) => m.QualityProfileId == p.Id)
|
|
||||||
.LeftJoin<Movie, MovieMetadata>((m, mm) => m.MovieMetadataId == mm.Id);
|
|
||||||
|
|
||||||
protected override IEnumerable<MovieHistory> PagedQuery(SqlBuilder sql) =>
|
|
||||||
_database.QueryJoined<MovieHistory, Movie, QualityProfile>(sql, (hist, movie, profile) =>
|
|
||||||
{
|
|
||||||
hist.Movie = movie;
|
|
||||||
hist.Movie.QualityProfile = profile;
|
|
||||||
return hist;
|
|
||||||
});
|
|
||||||
|
|
||||||
public MovieHistory MostRecentForMovie(int movieId)
|
public MovieHistory MostRecentForMovie(int movieId)
|
||||||
{
|
{
|
||||||
return Query(x => x.MovieId == movieId).MaxBy(h => h.Date);
|
return Query(x => x.MovieId == movieId).MaxBy(h => h.Date);
|
||||||
@ -106,5 +94,77 @@ public List<MovieHistory> Since(DateTime date, MovieHistoryEventType? eventType)
|
|||||||
|
|
||||||
return PagedQuery(builder).OrderBy(h => h.Date).ToList();
|
return PagedQuery(builder).OrderBy(h => h.Date).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PagingSpec<MovieHistory> GetPaged(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities)
|
||||||
|
{
|
||||||
|
pagingSpec.Records = GetPagedRecords(PagedBuilder(pagingSpec, languages, qualities), pagingSpec, PagedQuery);
|
||||||
|
|
||||||
|
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
|
||||||
|
pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder(pagingSpec, languages, qualities).Select(typeof(MovieHistory)), pagingSpec, countTemplate);
|
||||||
|
|
||||||
|
return pagingSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SqlBuilder PagedBuilder(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities)
|
||||||
|
{
|
||||||
|
var builder = Builder()
|
||||||
|
.Join<MovieHistory, Movie>((h, m) => h.MovieId == m.Id)
|
||||||
|
.Join<Movie, QualityProfile>((m, p) => m.QualityProfileId == p.Id)
|
||||||
|
.LeftJoin<Movie, MovieMetadata>((m, mm) => m.MovieMetadataId == mm.Id);
|
||||||
|
|
||||||
|
AddFilters(builder, pagingSpec);
|
||||||
|
|
||||||
|
if (languages is { Length: > 0 })
|
||||||
|
{
|
||||||
|
builder.Where($"({BuildLanguageWhereClause(languages)})");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (qualities is { Length: > 0 })
|
||||||
|
{
|
||||||
|
builder.Where($"({BuildQualityWhereClause(qualities)})");
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IEnumerable<MovieHistory> PagedQuery(SqlBuilder builder) =>
|
||||||
|
_database.QueryJoined<MovieHistory, Movie, QualityProfile>(builder, (hist, movie, profile) =>
|
||||||
|
{
|
||||||
|
hist.Movie = movie;
|
||||||
|
hist.Movie.QualityProfile = profile;
|
||||||
|
return hist;
|
||||||
|
});
|
||||||
|
|
||||||
|
private string BuildLanguageWhereClause(int[] languages)
|
||||||
|
{
|
||||||
|
var clauses = new List<string>();
|
||||||
|
|
||||||
|
foreach (var language in languages)
|
||||||
|
{
|
||||||
|
// There are 4 different types of values we should see:
|
||||||
|
// - Not the last value in the array
|
||||||
|
// - When it's the last value in the array and on different OSes
|
||||||
|
// - When it was converted from a single language
|
||||||
|
|
||||||
|
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[% {language},%]'");
|
||||||
|
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[% {language}' || CHAR(13) || '%]'");
|
||||||
|
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[% {language}' || CHAR(10) || '%]'");
|
||||||
|
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Languages\" LIKE '[{language}]'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"({string.Join(" OR ", clauses)})";
|
||||||
|
}
|
||||||
|
|
||||||
|
private string BuildQualityWhereClause(int[] qualities)
|
||||||
|
{
|
||||||
|
var clauses = new List<string>();
|
||||||
|
|
||||||
|
foreach (var quality in qualities)
|
||||||
|
{
|
||||||
|
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(MovieHistory))}\".\"Quality\" LIKE '%_quality_: {quality},%'");
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"({string.Join(" OR ", clauses)})";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ namespace NzbDrone.Core.History
|
|||||||
public interface IHistoryService
|
public interface IHistoryService
|
||||||
{
|
{
|
||||||
QualityModel GetBestQualityInHistory(QualityProfile profile, int movieId);
|
QualityModel GetBestQualityInHistory(QualityProfile profile, int movieId);
|
||||||
PagingSpec<MovieHistory> Paged(PagingSpec<MovieHistory> pagingSpec);
|
PagingSpec<MovieHistory> Paged(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities);
|
||||||
MovieHistory MostRecentForMovie(int movieId);
|
MovieHistory MostRecentForMovie(int movieId);
|
||||||
MovieHistory MostRecentForDownloadId(string downloadId);
|
MovieHistory MostRecentForDownloadId(string downloadId);
|
||||||
MovieHistory Get(int historyId);
|
MovieHistory Get(int historyId);
|
||||||
@ -49,9 +49,9 @@ public HistoryService(IHistoryRepository historyRepository, Logger logger)
|
|||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public PagingSpec<MovieHistory> Paged(PagingSpec<MovieHistory> pagingSpec)
|
public PagingSpec<MovieHistory> Paged(PagingSpec<MovieHistory> pagingSpec, int[] languages, int[] qualities)
|
||||||
{
|
{
|
||||||
return _historyRepository.GetPaged(pagingSpec);
|
return _historyRepository.GetPaged(pagingSpec, languages, qualities);
|
||||||
}
|
}
|
||||||
|
|
||||||
public MovieHistory MostRecentForMovie(int movieId)
|
public MovieHistory MostRecentForMovie(int movieId)
|
||||||
|
@ -61,7 +61,7 @@ protected HistoryResource MapToResource(MovieHistory model, bool includeMovie)
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, bool includeMovie, int? eventType, string downloadId)
|
public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, bool includeMovie, int? eventType, string downloadId, [FromQuery] int[] movieIds = null, [FromQuery] int[] languages = null, [FromQuery] int[] quality = null)
|
||||||
{
|
{
|
||||||
var pagingResource = new PagingResource<HistoryResource>(paging);
|
var pagingResource = new PagingResource<HistoryResource>(paging);
|
||||||
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, MovieHistory>("date", SortDirection.Descending);
|
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, MovieHistory>("date", SortDirection.Descending);
|
||||||
@ -77,16 +77,23 @@ public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResou
|
|||||||
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
|
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h, includeMovie));
|
if (movieIds != null && movieIds.Any())
|
||||||
|
{
|
||||||
|
pagingSpec.FilterExpressions.Add(h => movieIds.Contains(h.MovieId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pagingSpec.ApplyToPage(h => _historyService.Paged(pagingSpec, languages, quality), h => MapToResource(h, includeMovie));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("since")]
|
[HttpGet("since")]
|
||||||
|
[Produces("application/json")]
|
||||||
public List<HistoryResource> GetHistorySince(DateTime date, MovieHistoryEventType? eventType = null, bool includeMovie = false)
|
public List<HistoryResource> GetHistorySince(DateTime date, MovieHistoryEventType? eventType = null, bool includeMovie = false)
|
||||||
{
|
{
|
||||||
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeMovie)).ToList();
|
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeMovie)).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("movie")]
|
[HttpGet("movie")]
|
||||||
|
[Produces("application/json")]
|
||||||
public List<HistoryResource> GetMovieHistory(int movieId, MovieHistoryEventType? eventType = null, bool includeMovie = false)
|
public List<HistoryResource> GetMovieHistory(int movieId, MovieHistoryEventType? eventType = null, bool includeMovie = false)
|
||||||
{
|
{
|
||||||
return _historyService.GetByMovieId(movieId, eventType).Select(h => MapToResource(h, includeMovie)).ToList();
|
return _historyService.GetByMovieId(movieId, eventType).Select(h => MapToResource(h, includeMovie)).ToList();
|
||||||
|
Loading…
Reference in New Issue
Block a user