mirror of https://github.com/lidarr/Lidarr
New: Artist History Modal in Artist Details Page
This commit is contained in:
parent
0981260887
commit
7e4a8c8ff7
|
@ -1,3 +0,0 @@
|
|||
.absoluteEpisodeNumber {
|
||||
margin-left: 5px;
|
||||
}
|
|
@ -1,15 +1,12 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
import styles from './SeasonEpisodeNumber.css';
|
||||
import EpisodeNumber from './EpisodeNumber';
|
||||
|
||||
function SeasonEpisodeNumber(props) {
|
||||
const {
|
||||
seasonNumber,
|
||||
episodeNumber,
|
||||
absoluteEpisodeNumber,
|
||||
airDate,
|
||||
artistType
|
||||
artistType,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
if (artistType === 'daily' && airDate) {
|
||||
|
@ -18,32 +15,16 @@ function SeasonEpisodeNumber(props) {
|
|||
);
|
||||
}
|
||||
|
||||
if (artistType === 'anime') {
|
||||
return (
|
||||
<span>
|
||||
{seasonNumber}x{padNumber(episodeNumber, 2)}
|
||||
|
||||
{
|
||||
absoluteEpisodeNumber &&
|
||||
<span className={styles.absoluteEpisodeNumber}>
|
||||
({absoluteEpisodeNumber})
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{seasonNumber}x{padNumber(episodeNumber, 2)}
|
||||
</span>
|
||||
<EpisodeNumber
|
||||
seriesType={artistType}
|
||||
showSeasonNumber={true}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
SeasonEpisodeNumber.propTypes = {
|
||||
seasonNumber: PropTypes.number.isRequired,
|
||||
episodeNumber: PropTypes.number.isRequired,
|
||||
absoluteEpisodeNumber: PropTypes.number,
|
||||
airDate: PropTypes.string,
|
||||
artistType: PropTypes.string
|
||||
};
|
||||
|
|
|
@ -26,6 +26,7 @@ import QualityProfileNameConnector from 'Settings/Profiles/Quality/QualityProfil
|
|||
import ArtistPoster from 'Artist/ArtistPoster';
|
||||
import EditArtistModalConnector from 'Artist/Edit/EditArtistModalConnector';
|
||||
import DeleteArtistModal from 'Artist/Delete/DeleteArtistModal';
|
||||
import ArtistHistoryModal from 'Artist/History/ArtistHistoryModal';
|
||||
import ArtistAlternateTitles from './ArtistAlternateTitles';
|
||||
import ArtistDetailsSeasonConnector from './ArtistDetailsSeasonConnector';
|
||||
import ArtistTagsConnector from './ArtistTagsConnector';
|
||||
|
@ -92,6 +93,7 @@ class ArtistDetails extends Component {
|
|||
isManageEpisodesOpen: false,
|
||||
isEditArtistModalOpen: false,
|
||||
isDeleteArtistModalOpen: false,
|
||||
isArtistHistoryModalOpen: false,
|
||||
allExpanded: false,
|
||||
allCollapsed: false,
|
||||
expandedState: {}
|
||||
|
@ -136,6 +138,14 @@ class ArtistDetails extends Component {
|
|||
this.setState({ isDeleteArtistModalOpen: false });
|
||||
}
|
||||
|
||||
onArtistHistoryPress = () => {
|
||||
this.setState({ isArtistHistoryModalOpen: true });
|
||||
}
|
||||
|
||||
onArtistHistoryModalClose = () => {
|
||||
this.setState({ isArtistHistoryModalOpen: false });
|
||||
}
|
||||
|
||||
onExpandAllPress = () => {
|
||||
const {
|
||||
allExpanded,
|
||||
|
@ -197,6 +207,7 @@ class ArtistDetails extends Component {
|
|||
isManageEpisodesOpen,
|
||||
isEditArtistModalOpen,
|
||||
isDeleteArtistModalOpen,
|
||||
isArtistHistoryModalOpen,
|
||||
allExpanded,
|
||||
allCollapsed,
|
||||
expandedState
|
||||
|
@ -254,6 +265,12 @@ class ArtistDetails extends Component {
|
|||
onPress={this.onManageEpisodesPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="History"
|
||||
iconName={icons.HISTORY}
|
||||
onPress={this.onArtistHistoryPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
|
@ -540,6 +557,12 @@ class ArtistDetails extends Component {
|
|||
onModalClose={this.onManageEpisodesModalClose}
|
||||
/>
|
||||
|
||||
<ArtistHistoryModal
|
||||
isOpen={isArtistHistoryModalOpen}
|
||||
artistId={id}
|
||||
onModalClose={this.onArtistHistoryModalClose}
|
||||
/>
|
||||
|
||||
<EditArtistModalConnector
|
||||
isOpen={isEditArtistModalOpen}
|
||||
artistId={id}
|
||||
|
|
|
@ -171,71 +171,6 @@ class ArtistDetailsSeason extends Component {
|
|||
}
|
||||
</Link>
|
||||
|
||||
{
|
||||
isSmallScreen ?
|
||||
<Menu
|
||||
className={styles.actionsMenu}
|
||||
alignMenu={align.RIGHT}
|
||||
enforceMaxHeight={false}
|
||||
>
|
||||
<MenuButton>
|
||||
<Icon
|
||||
name={icons.ACTIONS}
|
||||
size={22}
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
<MenuContent className={styles.actionsMenuContent}>
|
||||
<MenuItem
|
||||
isDisabled={isSearching}
|
||||
onPress={onSearchPress}
|
||||
>
|
||||
<SpinnerIcon
|
||||
className={styles.actionMenuIcon}
|
||||
name={icons.SEARCH}
|
||||
isSpinning={isSearching}
|
||||
/>
|
||||
|
||||
Search
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onPress={this.onOrganizePress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.actionMenuIcon}
|
||||
name={icons.ORGANIZE}
|
||||
/>
|
||||
|
||||
Preview Rename
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onPress={this.onManageEpisodesPress}
|
||||
>
|
||||
<Icon
|
||||
className={styles.actionMenuIcon}
|
||||
name={icons.EPISODE_FILE}
|
||||
/>
|
||||
|
||||
Manage Tracks
|
||||
</MenuItem>
|
||||
</MenuContent>
|
||||
</Menu> :
|
||||
|
||||
<div className={styles.actions}>
|
||||
<SpinnerIconButton
|
||||
className={styles.actionButton}
|
||||
name={icons.SEARCH}
|
||||
title="Search for album"
|
||||
size={24}
|
||||
isSpinning={isSearching}
|
||||
onPress={onSearchPress}
|
||||
/>
|
||||
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import ArtistHistoryModalContentConnector from './ArtistHistoryModalContentConnector';
|
||||
|
||||
function ArtistHistoryModal(props) {
|
||||
const {
|
||||
isOpen,
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ArtistHistoryModalContentConnector
|
||||
{...otherProps}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
ArtistHistoryModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistHistoryModal;
|
|
@ -0,0 +1,132 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import ArtistHistoryRowConnector from './ArtistHistoryRowConnector';
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: 'eventType',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'album',
|
||||
label: 'Album',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'sourceTitle',
|
||||
label: 'Source Title',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'date',
|
||||
label: 'Date',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'details',
|
||||
label: 'Details',
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: 'Actions',
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
class ArtistHistoryModalContent extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
albumId,
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
onMarkAsFailedPress,
|
||||
onModalClose
|
||||
} = this.props;
|
||||
|
||||
const fullArtist = albumId == null;
|
||||
const hasItems = !!items.length;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
History
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
isFetching &&
|
||||
<LoadingIndicator />
|
||||
}
|
||||
|
||||
{
|
||||
!isFetching && !!error &&
|
||||
<div>Unable to load history.</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && !hasItems && !error &&
|
||||
<div>No history.</div>
|
||||
}
|
||||
|
||||
{
|
||||
isPopulated && hasItems && !error &&
|
||||
<Table columns={columns}>
|
||||
<TableBody>
|
||||
{
|
||||
items.map((item) => {
|
||||
return (
|
||||
<ArtistHistoryRowConnector
|
||||
key={item.id}
|
||||
fullArtist={fullArtist}
|
||||
{...item}
|
||||
onMarkAsFailedPress={onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
</TableBody>
|
||||
</Table>
|
||||
}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistHistoryModalContent.propTypes = {
|
||||
albumId: PropTypes.number,
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
error: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistHistoryModalContent;
|
|
@ -0,0 +1,81 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchArtistHistory, clearArtistHistory, artistHistoryMarkAsFailed } from 'Store/Actions/artistHistoryActions';
|
||||
import ArtistHistoryModalContent from './ArtistHistoryModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artistHistory,
|
||||
(artistHistory) => {
|
||||
return artistHistory;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchArtistHistory,
|
||||
clearArtistHistory,
|
||||
artistHistoryMarkAsFailed
|
||||
};
|
||||
|
||||
class ArtistHistoryModalContentConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
artistId,
|
||||
albumId
|
||||
} = this.props;
|
||||
|
||||
this.props.fetchArtistHistory({
|
||||
artistId,
|
||||
albumId
|
||||
});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.clearArtistHistory();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMarkAsFailedPress = (historyId) => {
|
||||
const {
|
||||
artistId,
|
||||
albumId
|
||||
} = this.props;
|
||||
|
||||
this.props.artistHistoryMarkAsFailed({
|
||||
historyId,
|
||||
artistId,
|
||||
albumId
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ArtistHistoryModalContent
|
||||
{...this.props}
|
||||
onMarkAsFailedPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistHistoryModalContentConnector.propTypes = {
|
||||
artistId: PropTypes.number.isRequired,
|
||||
albumId: PropTypes.number,
|
||||
fetchArtistHistory: PropTypes.func.isRequired,
|
||||
clearArtistHistory: PropTypes.func.isRequired,
|
||||
artistHistoryMarkAsFailed: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistHistoryModalContentConnector);
|
|
@ -0,0 +1,6 @@
|
|||
.details,
|
||||
.actions {
|
||||
composes: cell from 'Components/Table/Cells/TableRowCell.css';
|
||||
|
||||
width: 65px;
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import IconButton from 'Components/Link/IconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import EpisodeQuality from 'Album/EpisodeQuality';
|
||||
import HistoryDetailsConnector from 'Activity/History/Details/HistoryDetailsConnector';
|
||||
import HistoryEventTypeCell from 'Activity/History/HistoryEventTypeCell';
|
||||
import styles from './ArtistHistoryRow.css';
|
||||
|
||||
function getTitle(eventType) {
|
||||
switch (eventType) {
|
||||
case 'grabbed': return 'Grabbed';
|
||||
case 'artistFolderImported': return 'Artist Folder Imported';
|
||||
case 'downloadFolderImported': return 'Download Folder Imported';
|
||||
case 'downloadFailed': return 'Download Failed';
|
||||
case 'trackFileDeleted': return 'Track File Deleted';
|
||||
case 'trackFileRenamed': return 'Track File Renamed';
|
||||
default: return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
class ArtistHistoryRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isMarkAsFailedModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMarkAsFailedPress = () => {
|
||||
this.setState({ isMarkAsFailedModalOpen: true });
|
||||
}
|
||||
|
||||
onConfirmMarkAsFailed = () => {
|
||||
this.props.onMarkAsFailedPress(this.props.id);
|
||||
this.setState({ isMarkAsFailedModalOpen: false });
|
||||
}
|
||||
|
||||
onMarkAsFailedModalClose = () => {
|
||||
this.setState({ isMarkAsFailedModalOpen: false });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
eventType,
|
||||
sourceTitle,
|
||||
quality,
|
||||
qualityCutoffNotMet,
|
||||
date,
|
||||
data,
|
||||
fullArtist,
|
||||
artist,
|
||||
album
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
isMarkAsFailedModalOpen
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<HistoryEventTypeCell
|
||||
eventType={eventType}
|
||||
data={data}
|
||||
/>
|
||||
|
||||
<TableRowCell key={name}>
|
||||
{album.title}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{sourceTitle}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
isCutoffNotMet={qualityCutoffNotMet}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<RelativeDateCellConnector
|
||||
date={date}
|
||||
/>
|
||||
|
||||
<TableRowCell className={styles.details}>
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.INFO}
|
||||
/>
|
||||
}
|
||||
title={getTitle(eventType)}
|
||||
body={
|
||||
<HistoryDetailsConnector
|
||||
eventType={eventType}
|
||||
sourceTitle={sourceTitle}
|
||||
data={data}
|
||||
/>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.actions}>
|
||||
{
|
||||
eventType === 'grabbed' &&
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
onPress={this.onMarkAsFailedPress}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isMarkAsFailedModalOpen}
|
||||
kind={kinds.DANGER}
|
||||
title="Mark as Failed"
|
||||
message={`Are you sure you want to mark '${sourceTitle}' as failed?`}
|
||||
confirmLabel="Mark as Failed"
|
||||
onConfirm={this.onConfirmMarkAsFailed}
|
||||
onCancel={this.onMarkAsFailedModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArtistHistoryRow.propTypes = {
|
||||
id: PropTypes.number.isRequired,
|
||||
eventType: PropTypes.string.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
quality: PropTypes.object.isRequired,
|
||||
qualityCutoffNotMet: PropTypes.bool.isRequired,
|
||||
date: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
fullArtist: PropTypes.bool.isRequired,
|
||||
artist: PropTypes.object.isRequired,
|
||||
album: PropTypes.object.isRequired,
|
||||
onMarkAsFailedPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ArtistHistoryRow;
|
|
@ -0,0 +1,26 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchHistory, markAsFailed } from 'Store/Actions/historyActions';
|
||||
import createArtistSelector from 'Store/Selectors/createArtistSelector';
|
||||
import createEpisodeSelector from 'Store/Selectors/createEpisodeSelector';
|
||||
import ArtistHistoryRow from './ArtistHistoryRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createArtistSelector(),
|
||||
createEpisodeSelector(),
|
||||
(artist, album) => {
|
||||
return {
|
||||
artist,
|
||||
album
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchHistory,
|
||||
markAsFailed
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ArtistHistoryRow);
|
|
@ -39,6 +39,7 @@ export const FOLDER_OPEN = 'fa fa-folder-open';
|
|||
export const GROUP = 'fa fa-object-group';
|
||||
export const HEALTH = 'fa fa-medkit';
|
||||
export const HEART = 'fa fa-heart';
|
||||
export const HISTORY = 'fa fa-history';
|
||||
export const HOUSEKEEPING = 'fa fa-home';
|
||||
export const INFO = 'fa fa-info-circle';
|
||||
export const INTERACTIVE = 'fa fa-user';
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import $ from 'jquery';
|
||||
import { createAction } from 'redux-actions';
|
||||
import { batchActions } from 'redux-batched-actions';
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
import { set, update } from './baseActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'artistHistory';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
items: []
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_ARTIST_HISTORY = 'artistHistory/fetchArtistHistory';
|
||||
export const CLEAR_ARTIST_HISTORY = 'artistHistory/clearArtistHistory';
|
||||
export const ARTIST_HISTORY_MARK_AS_FAILED = 'artistHistory/artistHistoryMarkAsFailed';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchArtistHistory = createThunk(FETCH_ARTIST_HISTORY);
|
||||
export const clearArtistHistory = createAction(CLEAR_ARTIST_HISTORY);
|
||||
export const artistHistoryMarkAsFailed = createThunk(ARTIST_HISTORY_MARK_AS_FAILED);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
|
||||
[FETCH_ARTIST_HISTORY]: function(getState, payload, dispatch) {
|
||||
dispatch(set({ section, isFetching: true }));
|
||||
|
||||
const promise = $.ajax({
|
||||
url: '/history/artist',
|
||||
data: payload
|
||||
});
|
||||
|
||||
promise.done((data) => {
|
||||
dispatch(batchActions([
|
||||
update({ section, data }),
|
||||
|
||||
set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: true,
|
||||
error: null
|
||||
})
|
||||
]));
|
||||
});
|
||||
|
||||
promise.fail((xhr) => {
|
||||
dispatch(set({
|
||||
section,
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: xhr
|
||||
}));
|
||||
});
|
||||
},
|
||||
|
||||
[ARTIST_HISTORY_MARK_AS_FAILED]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
historyId,
|
||||
artistId,
|
||||
albumId
|
||||
} = payload;
|
||||
|
||||
const promise = $.ajax({
|
||||
url: '/history/failed',
|
||||
method: 'POST',
|
||||
data: {
|
||||
id: historyId
|
||||
}
|
||||
});
|
||||
|
||||
promise.done(() => {
|
||||
dispatch(fetchArtistHistory({ artistId, albumId }));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
|
||||
export const reducers = createHandleActions({
|
||||
|
||||
[CLEAR_ARTIST_HISTORY]: (state) => {
|
||||
return Object.assign({}, state, defaultState);
|
||||
}
|
||||
|
||||
}, defaultState, section);
|
||||
|
|
@ -19,6 +19,7 @@ import * as rootFolders from './rootFolderActions';
|
|||
import * as albumStudio from './albumStudioActions';
|
||||
import * as artist from './artistActions';
|
||||
import * as artistEditor from './artistEditorActions';
|
||||
import * as artistHistory from './artistHistoryActions';
|
||||
import * as artistIndex from './artistIndexActions';
|
||||
import * as settings from './settingsActions';
|
||||
import * as system from './systemActions';
|
||||
|
@ -48,6 +49,7 @@ export default [
|
|||
albumStudio,
|
||||
artist,
|
||||
artistEditor,
|
||||
artistHistory,
|
||||
artistIndex,
|
||||
settings,
|
||||
system,
|
||||
|
|
|
@ -31,6 +31,12 @@ export const defaultState = {
|
|||
recentFolders: [],
|
||||
importMode: 'move',
|
||||
sortPredicates: {
|
||||
relativePath: function(item, direction) {
|
||||
const relativePath = item.relativePath;
|
||||
|
||||
return relativePath.toLowerCase();
|
||||
},
|
||||
|
||||
artist: function(item, direction) {
|
||||
const artist = item.artist;
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ namespace Lidarr.Api.V1.History
|
|||
GetResourcePaged = GetHistory;
|
||||
|
||||
Get["/since"] = x => GetHistorySince();
|
||||
Get["/artist"] = x => GetArtistHistory();
|
||||
Post["/failed"] = x => MarkAsFailed();
|
||||
}
|
||||
|
||||
|
@ -109,6 +110,38 @@ namespace Lidarr.Api.V1.History
|
|||
return _historyService.Since(date, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
|
||||
}
|
||||
|
||||
private List<HistoryResource> GetArtistHistory()
|
||||
{
|
||||
var queryArtistId = Request.Query.ArtistId;
|
||||
var queryAlbumId = Request.Query.AlbumId;
|
||||
var queryEventType = Request.Query.EventType;
|
||||
|
||||
if (!queryArtistId.HasValue)
|
||||
{
|
||||
throw new BadRequestException("artistId is missing");
|
||||
}
|
||||
|
||||
int artistId = Convert.ToInt32(queryArtistId.Value);
|
||||
HistoryEventType? eventType = null;
|
||||
var includeArtist = Request.GetBooleanQueryParameter("includeArtist");
|
||||
var includeAlbum = Request.GetBooleanQueryParameter("includeAlbum");
|
||||
var includeTrack = Request.GetBooleanQueryParameter("includeTrack");
|
||||
|
||||
if (queryEventType.HasValue)
|
||||
{
|
||||
eventType = (HistoryEventType)Convert.ToInt32(queryEventType.Value);
|
||||
}
|
||||
|
||||
if (queryAlbumId.HasValue)
|
||||
{
|
||||
int albumId = Convert.ToInt32(queryAlbumId.Value);
|
||||
|
||||
return _historyService.GetByAlbum(artistId, albumId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
|
||||
}
|
||||
|
||||
return _historyService.GetByArtist(artistId, eventType).Select(h => MapToResource(h, includeArtist, includeAlbum, includeTrack)).ToList();
|
||||
}
|
||||
|
||||
private Response MarkAsFailed()
|
||||
{
|
||||
var id = (int)Request.Form.Id;
|
||||
|
|
|
@ -14,6 +14,8 @@ namespace NzbDrone.Core.History
|
|||
History MostRecentForAlbum(int albumId);
|
||||
History MostRecentForDownloadId(string downloadId);
|
||||
List<History> FindByDownloadId(string downloadId);
|
||||
List<History> GetByArtist(int artistId, HistoryEventType? eventType);
|
||||
List<History> GetByAlbum(int artistId, int albumId, HistoryEventType? eventType);
|
||||
List<History> FindDownloadHistory(int idArtistId, QualityModel quality);
|
||||
void DeleteForArtist(int artistId);
|
||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||
|
@ -48,6 +50,36 @@ namespace NzbDrone.Core.History
|
|||
return Query.Where(h => h.DownloadId == downloadId);
|
||||
}
|
||||
|
||||
public List<History> GetByArtist(int artistId, HistoryEventType? eventType)
|
||||
{
|
||||
var query = Query.Where(h => h.ArtistId == artistId);
|
||||
|
||||
if (eventType.HasValue)
|
||||
{
|
||||
query.AndWhere(h => h.EventType == eventType);
|
||||
}
|
||||
|
||||
query.OrderByDescending(h => h.Date);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public List<History> GetByAlbum(int artistId, int albumId, HistoryEventType? eventType)
|
||||
{
|
||||
var query = Query.Join<History, Album>(JoinType.Inner, h => h.Album, (h, e) => h.AlbumId == e.Id)
|
||||
.Where(h => h.ArtistId == artistId)
|
||||
.AndWhere(h => h.AlbumId == albumId);
|
||||
|
||||
if (eventType.HasValue)
|
||||
{
|
||||
query.AndWhere(h => h.EventType == eventType);
|
||||
}
|
||||
|
||||
query.OrderByDescending(h => h.Date);
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
public List<History> FindDownloadHistory(int idArtistId, QualityModel quality)
|
||||
{
|
||||
return Query.Where(h =>
|
||||
|
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Marr.Data.QGen;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
@ -13,7 +14,7 @@ using NzbDrone.Core.Parser.Model;
|
|||
using NzbDrone.Core.Profiles.Qualities;
|
||||
using NzbDrone.Core.Profiles.Languages;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Qualities;
|
||||
using NzbDrone.Core.Music;
|
||||
using NzbDrone.Core.Music.Events;
|
||||
|
||||
namespace NzbDrone.Core.History
|
||||
|
@ -24,6 +25,8 @@ namespace NzbDrone.Core.History
|
|||
History MostRecentForAlbum(int episodeId);
|
||||
History MostRecentForDownloadId(string downloadId);
|
||||
History Get(int historyId);
|
||||
List<History> GetByArtist(int artistId, HistoryEventType? eventType);
|
||||
List<History> GetByAlbum(int artistId, int albumId, HistoryEventType? eventType);
|
||||
List<History> Find(string downloadId, HistoryEventType eventType);
|
||||
List<History> FindByDownloadId(string downloadId);
|
||||
List<History> Since(DateTime date, HistoryEventType? eventType);
|
||||
|
@ -66,6 +69,16 @@ namespace NzbDrone.Core.History
|
|||
return _historyRepository.Get(historyId);
|
||||
}
|
||||
|
||||
public List<History> GetByArtist(int artistId, HistoryEventType? eventType)
|
||||
{
|
||||
return _historyRepository.GetByArtist(artistId, eventType);
|
||||
}
|
||||
|
||||
public List<History> GetByAlbum(int artistId, int albumId, HistoryEventType? eventType)
|
||||
{
|
||||
return _historyRepository.GetByAlbum(artistId, albumId, eventType);
|
||||
}
|
||||
|
||||
public List<History> Find(string downloadId, HistoryEventType eventType)
|
||||
{
|
||||
return _historyRepository.FindByDownloadId(downloadId).Where(c => c.EventType == eventType).ToList();
|
||||
|
|
Loading…
Reference in New Issue