diff --git a/frontend/src/App/AppRoutes.js b/frontend/src/App/AppRoutes.js
index 7a75d774e..9e15ece36 100644
--- a/frontend/src/App/AppRoutes.js
+++ b/frontend/src/App/AppRoutes.js
@@ -30,7 +30,7 @@ import TagSettings from 'Settings/Tags/TagSettings';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
import UISettingsConnector from 'Settings/UI/UISettingsConnector';
import Status from 'System/Status/Status';
-import TasksConnector from 'System/Tasks/TasksConnector';
+import Tasks from 'System/Tasks/Tasks';
import BackupsConnector from 'System/Backup/BackupsConnector';
import UpdatesConnector from 'System/Updates/UpdatesConnector';
import LogsTableConnector from 'System/Events/LogsTableConnector';
@@ -218,7 +218,7 @@ function AppRoutes(props) {
command.name === commandNames.REFRESH_ARTIST && !command.body.artistId);
+ const isArtistRefreshing = isCommandExecuting(findCommand(commands, { name: commandNames.REFRESH_ARTIST, artistId: artist.id }));
+ const artistRefreshingCommand = findCommand(commands, { name: commandNames.REFRESH_ARTIST });
+ const allArtistRefreshing = (
+ isCommandExecuting(artistRefreshingCommand) &&
+ !artistRefreshingCommand.body.artistId
+ );
const isRefreshing = isArtistRefreshing || allArtistRefreshing;
- const isSearching = !!findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id });
- const isRenamingFiles = !!findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id });
+ const isSearching = isCommandExecuting(findCommand(commands, { name: commandNames.ARTIST_SEARCH, artistId: artist.id }));
+ const isRenamingFiles = isCommandExecuting(findCommand(commands, { name: commandNames.RENAME_FILES, artistId: artist.id }));
+
const isRenamingArtistCommand = findCommand(commands, { name: commandNames.RENAME_ARTIST });
- const isRenamingArtist = !!(isRenamingArtistCommand && isRenamingArtistCommand.body.artistId.indexOf(artist.id) > -1);
+ const isRenamingArtist = (
+ isCommandExecuting(isRenamingArtistCommand) &&
+ isRenamingArtistCommand.body.artistId.indexOf(artist.id) > -1
+ );
const isFetching = albums.isFetching || trackFiles.isFetching;
const isPopulated = albums.isPopulated && trackFiles.isPopulated;
diff --git a/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js b/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js
index e9eeeab3a..11202c7ac 100644
--- a/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js
+++ b/frontend/src/Artist/Details/ArtistDetailsSeasonConnector.js
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import { findCommand } from 'Utilities/Command';
+import { findCommand, isCommandExecuting } from 'Utilities/Command';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import createArtistSelector from 'Store/Selectors/createArtistSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
diff --git a/frontend/src/Artist/Editor/ArtistEditorConnector.js b/frontend/src/Artist/Editor/ArtistEditorConnector.js
index 3190fbc83..d029bbec2 100644
--- a/frontend/src/Artist/Editor/ArtistEditorConnector.js
+++ b/frontend/src/Artist/Editor/ArtistEditorConnector.js
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
-import createCommandSelector from 'Store/Selectors/createCommandSelector';
+import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import { setArtistEditorSort, setArtistEditorFilter, saveArtistEditor } from 'Store/Actions/artistEditorActions';
import { fetchRootFolders } from 'Store/Actions/rootFolderActions';
import { executeCommand } from 'Store/Actions/commandActions';
@@ -15,7 +15,7 @@ function createMapStateToProps() {
(state) => state.settings.languageProfiles,
(state) => state.settings.metadataProfiles,
createClientSideCollectionSelector('artist', 'artistEditor'),
- createCommandSelector(commandNames.RENAME_ARTIST),
+ createCommandExecutingSelector(commandNames.RENAME_ARTIST),
(languageProfiles, metadataProfiles, artist, isOrganizingArtist) => {
return {
isOrganizingArtist,
diff --git a/frontend/src/Artist/Index/ArtistIndexConnector.js b/frontend/src/Artist/Index/ArtistIndexConnector.js
index 3140861e1..0c22d2556 100644
--- a/frontend/src/Artist/Index/ArtistIndexConnector.js
+++ b/frontend/src/Artist/Index/ArtistIndexConnector.js
@@ -5,7 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
import dimensions from 'Styles/Variables/dimensions';
-import createCommandSelector from 'Store/Selectors/createCommandSelector';
+import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
import { fetchArtist } from 'Store/Actions/artistActions';
import scrollPositions from 'Store/scrollPositions';
@@ -47,8 +47,8 @@ function getScrollTop(view, scrollTop, isSmallScreen) {
function createMapStateToProps() {
return createSelector(
createClientSideCollectionSelector('artist', 'artistIndex'),
- createCommandSelector(commandNames.REFRESH_ARTIST),
- createCommandSelector(commandNames.RSS_SYNC),
+ createCommandExecutingSelector(commandNames.REFRESH_ARTIST),
+ createCommandExecutingSelector(commandNames.RSS_SYNC),
createDimensionsSelector(),
(
artist,
diff --git a/frontend/src/Components/SignalRConnector.js b/frontend/src/Components/SignalRConnector.js
index 1c7439aa6..e1f689035 100644
--- a/frontend/src/Components/SignalRConnector.js
+++ b/frontend/src/Components/SignalRConnector.js
@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { repopulatePage } from 'Utilities/pagePopulator';
import titleCase from 'Utilities/String/titleCase';
-import { updateCommand, finishCommand } from 'Store/Actions/commandActions';
+import { fetchCommands, updateCommand, finishCommand } from 'Store/Actions/commandActions';
import { setAppValue, setVersion } from 'Store/Actions/appActions';
import { update, updateItem, removeItem } from 'Store/Actions/baseActions';
import { fetchHealth } from 'Store/Actions/systemActions';
@@ -58,16 +58,17 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
- updateCommand,
- finishCommand,
- setAppValue,
- setVersion,
- update,
- updateItem,
- removeItem,
- fetchHealth,
- fetchQueue,
- fetchQueueDetails
+ dispatchFetchCommands: fetchCommands,
+ dispatchUpdateCommand: updateCommand,
+ dispatchFinishCommand: finishCommand,
+ dispatchSetAppValue: setAppValue,
+ dispatchSetVersion: setVersion,
+ dispatchUpdate: update,
+ dispatchUpdateItem: updateItem,
+ dispatchRemoveItem: removeItem,
+ dispatchFetchHealth: fetchHealth,
+ dispatchFetchQueue: fetchQueue,
+ dispatchFetchQueueDetails: fetchQueueDetails
};
class SignalRConnector extends Component {
@@ -146,7 +147,7 @@ class SignalRConnector extends Component {
handleCalendar = (body) => {
if (body.action === 'updated') {
- this.props.updateItem({
+ this.props.dispatchUpdateItem({
section: 'calendar',
updateOnly: true,
...body.resource
@@ -155,22 +156,27 @@ class SignalRConnector extends Component {
}
handleCommand = (body) => {
+ if (body.action === 'sync') {
+ this.props.dispatchFetchCommands();
+ return;
+ }
+
const resource = body.resource;
- const state = resource.state;
+ const status = resource.status;
// Both sucessful and failed commands need to be
// completed, otherwise they spin until they timeout.
- if (state === 'completed' || state === 'failed') {
- this.props.finishCommand(resource);
+ if (status === 'completed' || status === 'failed') {
+ this.props.dispatchFinishCommand(resource);
} else {
- this.props.updateCommand(resource);
+ this.props.dispatchUpdateCommand(resource);
}
}
handleAlbum = (body) => {
if (body.action === 'updated') {
- this.props.updateItem({
+ this.props.dispatchUpdateItem({
section: 'albums',
updateOnly: true,
...body.resource
@@ -180,7 +186,7 @@ class SignalRConnector extends Component {
handleTrack = (body) => {
if (body.action === 'updated') {
- this.props.updateItem({
+ this.props.dispatchUpdateItem({
section: 'tracks',
updateOnly: true,
...body.resource
@@ -192,14 +198,14 @@ class SignalRConnector extends Component {
const section = 'trackFiles';
if (body.action === 'updated') {
- this.props.updateItem({ section, ...body.resource });
+ this.props.dispatchUpdateItem({ section, ...body.resource });
} else if (body.action === 'deleted') {
- this.props.removeItem({ section, id: body.resource.id });
+ this.props.dispatchRemoveItem({ section, id: body.resource.id });
}
}
handleHealth = () => {
- this.props.fetchHealth();
+ this.props.dispatchFetchHealth();
}
handleArtist = (body) => {
@@ -207,35 +213,35 @@ class SignalRConnector extends Component {
const section = 'artist';
if (action === 'updated') {
- this.props.updateItem({ section, ...body.resource });
+ this.props.dispatchUpdateItem({ section, ...body.resource });
} else if (action === 'deleted') {
- this.props.removeItem({ section, id: body.resource.id });
+ this.props.dispatchRemoveItem({ section, id: body.resource.id });
}
}
handleQueue = () => {
if (this.props.isQueuePopulated) {
- this.props.fetchQueue();
+ this.props.dispatchFetchQueue();
}
}
handleQueueDetails = () => {
- this.props.fetchQueueDetails();
+ this.props.dispatchFetchQueueDetails();
}
handleQueueStatus = (body) => {
- this.props.update({ section: 'queue.status', data: body.resource });
+ this.props.dispatchUpdate({ section: 'queue.status', data: body.resource });
}
handleVersion = (body) => {
const version = body.Version;
- this.props.setVersion({ version });
+ this.props.dispatchSetVersion({ version });
}
handleWantedCutoff = (body) => {
if (body.action === 'updated') {
- this.props.updateItem({
+ this.props.dispatchUpdateItem({
section: 'cutoffUnmet',
updateOnly: true,
...body.resource
@@ -245,7 +251,7 @@ class SignalRConnector extends Component {
handleWantedMissing = (body) => {
if (body.action === 'updated') {
- this.props.updateItem({
+ this.props.dispatchUpdateItem({
section: 'missing',
updateOnly: true,
...body.resource
@@ -268,14 +274,20 @@ class SignalRConnector extends Component {
// Clear disconnected time
this.disconnectedTime = null;
+ const {
+ dispatchFetchCommands,
+ dispatchSetAppValue
+ } = this.props;
+
// Repopulate the page (if a repopulator is set) to ensure things
// are in sync after reconnecting.
if (this.props.isReconnecting || this.props.isDisconnected) {
+ dispatchFetchCommands();
repopulatePage();
}
- this.props.setAppValue({
+ dispatchSetAppValue({
isConnected: true,
isReconnecting: false,
isDisconnected: false,
@@ -305,7 +317,7 @@ class SignalRConnector extends Component {
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
}
- this.props.setAppValue({
+ this.props.dispatchSetAppValue({
isReconnecting: true
});
}
@@ -319,7 +331,7 @@ class SignalRConnector extends Component {
this.disconnectedTime = Math.floor(new Date().getTime() / 1000);
}
- this.props.setAppValue({
+ this.props.dispatchSetAppValue({
isConnected: false,
isReconnecting: true,
isDisconnected: isAppDisconnected(this.disconnectedTime)
@@ -340,16 +352,17 @@ SignalRConnector.propTypes = {
isReconnecting: PropTypes.bool.isRequired,
isDisconnected: PropTypes.bool.isRequired,
isQueuePopulated: PropTypes.bool.isRequired,
- updateCommand: PropTypes.func.isRequired,
- finishCommand: PropTypes.func.isRequired,
- setAppValue: PropTypes.func.isRequired,
- setVersion: PropTypes.func.isRequired,
- update: PropTypes.func.isRequired,
- updateItem: PropTypes.func.isRequired,
- removeItem: PropTypes.func.isRequired,
- fetchHealth: PropTypes.func.isRequired,
- fetchQueue: PropTypes.func.isRequired,
- fetchQueueDetails: PropTypes.func.isRequired
+ dispatchFetchCommands: PropTypes.func.isRequired,
+ dispatchUpdateCommand: PropTypes.func.isRequired,
+ dispatchFinishCommand: PropTypes.func.isRequired,
+ dispatchSetAppValue: PropTypes.func.isRequired,
+ dispatchSetVersion: PropTypes.func.isRequired,
+ dispatchUpdate: PropTypes.func.isRequired,
+ dispatchUpdateItem: PropTypes.func.isRequired,
+ dispatchRemoveItem: PropTypes.func.isRequired,
+ dispatchFetchHealth: PropTypes.func.isRequired,
+ dispatchFetchQueue: PropTypes.func.isRequired,
+ dispatchFetchQueueDetails: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(SignalRConnector);
diff --git a/frontend/src/Store/Actions/commandActions.js b/frontend/src/Store/Actions/commandActions.js
index 1c399c88e..e0dd81d3d 100644
--- a/frontend/src/Store/Actions/commandActions.js
+++ b/frontend/src/Store/Actions/commandActions.js
@@ -7,6 +7,7 @@ import { messageTypes } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions';
+import createRemoveItemHandler from './Creators/createRemoveItemHandler';
import { showMessage, hideMessage } from './appActions';
import { updateItem } from './baseActions';
@@ -35,6 +36,7 @@ export const defaultState = {
export const FETCH_COMMANDS = 'commands/fetchCommands';
export const EXECUTE_COMMAND = 'commands/executeCommand';
+export const CANCEL_COMMAND = 'commands/cancelCommand';
export const ADD_COMMAND = 'commands/updateCommand';
export const UPDATE_COMMAND = 'commands/finishCommand';
export const FINISH_COMMAND = 'commands/addCommand';
@@ -45,6 +47,7 @@ export const REMOVE_COMMAND = 'commands/removeCommand';
export const fetchCommands = createThunk(FETCH_COMMANDS);
export const executeCommand = createThunk(EXECUTE_COMMAND);
+export const cancelCommand = createThunk(CANCEL_COMMAND);
export const updateCommand = createThunk(UPDATE_COMMAND);
export const finishCommand = createThunk(FINISH_COMMAND);
export const addCommand = createAction(ADD_COMMAND);
@@ -60,7 +63,7 @@ function showCommandMessage(payload, dispatch) {
trigger,
message,
body = {},
- state
+ status
} = payload;
const {
@@ -75,10 +78,10 @@ function showCommandMessage(payload, dispatch) {
let type = messageTypes.INFO;
let hideAfter = 0;
- if (state === 'completed') {
+ if (status === 'completed') {
type = messageTypes.SUCCESS;
hideAfter = 4;
- } else if (state === 'failed') {
+ } else if (status === 'failed') {
type = messageTypes.ERROR;
hideAfter = trigger === 'manual' ? 10 : 4;
}
@@ -95,8 +98,7 @@ function showCommandMessage(payload, dispatch) {
function scheduleRemoveCommand(command, dispatch) {
const {
id,
- status,
- body
+ status
} = command;
if (status === 'queued') {
@@ -109,12 +111,6 @@ function scheduleRemoveCommand(command, dispatch) {
clearTimeout(timeoutId);
}
- // 5 minute timeout for executing disk access commands and
- // 30 seconds for all other commands.
- const timeout = body.requiresDiskAccess && status === 'started' ?
- 60000 * 5 :
- 30000;
-
removeCommandTimeoutIds[id] = setTimeout(() => {
dispatch(batchActions([
removeCommand({ section: 'commands', id }),
@@ -122,7 +118,7 @@ function scheduleRemoveCommand(command, dispatch) {
]));
delete removeCommandTimeoutIds[id];
- }, timeout);
+ }, 60000 * 5);
}
//
@@ -159,6 +155,8 @@ export const actionHandlers = handleThunks({
});
},
+ [CANCEL_COMMAND]: createRemoveItemHandler(section, '/command'),
+
[UPDATE_COMMAND]: function(getState, payload, dispatch) {
dispatch(updateItem({ section: 'commands', ...payload }));
@@ -178,7 +176,7 @@ export const actionHandlers = handleThunks({
}
});
- dispatch(removeCommand({ section: 'commands', ...payload }));
+ scheduleRemoveCommand(payload, dispatch);
showCommandMessage(payload, dispatch);
}
diff --git a/frontend/src/Store/Selectors/createCommandExecutingSelector.js b/frontend/src/Store/Selectors/createCommandExecutingSelector.js
index ac79194ad..337b31f6a 100644
--- a/frontend/src/Store/Selectors/createCommandExecutingSelector.js
+++ b/frontend/src/Store/Selectors/createCommandExecutingSelector.js
@@ -1,12 +1,12 @@
-import _ from 'lodash';
import { createSelector } from 'reselect';
+import { findCommand, isCommandExecuting } from 'Utilities/Command';
import createCommandsSelector from './createCommandsSelector';
-function createCommandExecutingSelector(name) {
+function createCommandExecutingSelector(name, contraints = {}) {
return createSelector(
createCommandsSelector(),
(commands) => {
- return _.some(commands, { name });
+ return isCommandExecuting(findCommand(commands, { name, ...contraints }));
}
);
}
diff --git a/frontend/src/Store/Selectors/createCommandSelector.js b/frontend/src/Store/Selectors/createCommandSelector.js
index ff5bfe50a..5f69ab005 100644
--- a/frontend/src/Store/Selectors/createCommandSelector.js
+++ b/frontend/src/Store/Selectors/createCommandSelector.js
@@ -1,12 +1,12 @@
-import _ from 'lodash';
import { createSelector } from 'reselect';
+import { findCommand } from 'Utilities/Command';
import createCommandsSelector from './createCommandsSelector';
function createCommandSelector(name, contraints = {}) {
return createSelector(
createCommandsSelector(),
(commands) => {
- return _.some(commands, { name, ...contraints });
+ return !!findCommand(commands, { name, ...contraints });
}
);
}
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.css b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css
new file mode 100644
index 000000000..30f86efff
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.css
@@ -0,0 +1,31 @@
+.trigger {
+ composes: cell from 'Components/Table/Cells/TableRowCell.css';
+
+ width: 50px;
+}
+
+.triggerContent {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+}
+
+.queued,
+.started,
+.ended {
+ composes: cell from 'Components/Table/Cells/TableRowCell.css';
+
+ width: 180px;
+}
+
+.duration {
+ composes: cell from 'Components/Table/Cells/TableRowCell.css';
+
+ width: 100px;
+}
+
+.actions {
+ composes: cell from 'Components/Table/Cells/TableRowCell.css';
+
+ width: 20px;
+}
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRow.js b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js
new file mode 100644
index 000000000..8353a5325
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRow.js
@@ -0,0 +1,264 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import titleCase from 'Utilities/String/titleCase';
+import formatDate from 'Utilities/Date/formatDate';
+import formatDateTime from 'Utilities/Date/formatDateTime';
+import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
+import { icons, kinds } from 'Helpers/Props';
+import Icon from 'Components/Icon';
+import IconButton from 'Components/Link/IconButton';
+import ConfirmModal from 'Components/Modal/ConfirmModal';
+import TableRow from 'Components/Table/TableRow';
+import TableRowCell from 'Components/Table/Cells/TableRowCell';
+import styles from './QueuedTaskRow.css';
+
+function getStatusIconProps(status, message) {
+ const title = titleCase(status);
+
+ switch (status) {
+ case 'queued':
+ return {
+ name: icons.PENDING,
+ title
+ };
+
+ case 'started':
+ return {
+ name: icons.REFRESH,
+ isSpinning: true,
+ title
+ };
+
+ case 'completed':
+ return {
+ name: icons.CHECK,
+ kind: kinds.SUCCESS,
+ title: message === 'Completed' ? title : `${title}: ${message}`
+ };
+
+ case 'failed':
+ return {
+ name: icons.FATAL,
+ kind: kinds.ERROR,
+ title: `${title}: ${message}`
+ };
+
+ default:
+ return {
+ name: icons.UNKNOWN,
+ title
+ };
+ }
+}
+
+function getFormattedDates(props) {
+ const {
+ queued,
+ started,
+ ended,
+ showRelativeDates,
+ shortDateFormat
+ } = props;
+
+ if (showRelativeDates) {
+ return {
+ queuedAt: moment(queued).fromNow(),
+ startedAt: started ? moment(started).fromNow() : '-',
+ endedAt: ended ? moment(ended).fromNow() : '-'
+ };
+ }
+
+ return {
+ queuedAt: formatDate(queued, shortDateFormat),
+ startedAt: started ? formatDate(started, shortDateFormat) : '-',
+ endedAt: ended ? formatDate(ended, shortDateFormat) : '-'
+ };
+}
+
+class QueuedTaskRow extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = {
+ ...getFormattedDates(props),
+ isCancelConfirmModalOpen: false
+ };
+
+ this._updateTimeoutId = null;
+ }
+
+ componentDidMount() {
+ this.setUpdateTimer();
+ }
+
+ componentDidUpdate(prevProps) {
+ const {
+ queued,
+ started,
+ ended
+ } = this.props;
+
+ if (
+ queued !== prevProps.queued ||
+ started !== prevProps.started ||
+ ended !== prevProps.ended
+ ) {
+ this.setState(getFormattedDates(this.props));
+ }
+ }
+
+ componentWillUnmount() {
+ if (this._updateTimeoutId) {
+ this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
+ }
+ }
+
+ //
+ // Control
+
+ setUpdateTimer() {
+ this._updateTimeoutId = setTimeout(() => {
+ this.setState(getFormattedDates(this.props));
+ this.setUpdateTimer();
+ }, 30000);
+ }
+
+ //
+ // Listeners
+
+ onCancelPress = () => {
+ this.setState({
+ isCancelConfirmModalOpen: true
+ });
+ }
+
+ onAbortCancel = () => {
+ this.setState({
+ isCancelConfirmModalOpen: false
+ });
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ trigger,
+ commandName,
+ queued,
+ started,
+ ended,
+ status,
+ duration,
+ message,
+ longDateFormat,
+ timeFormat,
+ onCancelPress
+ } = this.props;
+
+ const {
+ queuedAt,
+ startedAt,
+ endedAt,
+ isCancelConfirmModalOpen
+ } = this.state;
+
+ let triggerIcon = icons.UNKNOWN;
+
+ if (trigger === 'manual') {
+ triggerIcon = icons.INTERACTIVE;
+ } else if (trigger === 'scheduled') {
+ triggerIcon = icons.SCHEDULED;
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+ {commandName}
+
+
+ {queuedAt}
+
+
+
+ {startedAt}
+
+
+
+ {endedAt}
+
+
+
+ {formatTimeSpan(duration)}
+
+
+
+ {
+ status === 'queued' &&
+
+ }
+
+
+
+
+ );
+ }
+}
+
+QueuedTaskRow.propTypes = {
+ trigger: PropTypes.string.isRequired,
+ commandName: PropTypes.string.isRequired,
+ queued: PropTypes.string.isRequired,
+ started: PropTypes.string,
+ ended: PropTypes.string,
+ status: PropTypes.string.isRequired,
+ duration: PropTypes.string,
+ message: PropTypes.string,
+ showRelativeDates: PropTypes.bool.isRequired,
+ shortDateFormat: PropTypes.string.isRequired,
+ longDateFormat: PropTypes.string.isRequired,
+ timeFormat: PropTypes.string.isRequired,
+ onCancelPress: PropTypes.func.isRequired
+};
+
+export default QueuedTaskRow;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js
new file mode 100644
index 000000000..f55ab985a
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTaskRowConnector.js
@@ -0,0 +1,31 @@
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { cancelCommand } from 'Store/Actions/commandActions';
+import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
+import QueuedTaskRow from './QueuedTaskRow';
+
+function createMapStateToProps() {
+ return createSelector(
+ createUISettingsSelector(),
+ (uiSettings) => {
+ return {
+ showRelativeDates: uiSettings.showRelativeDates,
+ shortDateFormat: uiSettings.shortDateFormat,
+ longDateFormat: uiSettings.longDateFormat,
+ timeFormat: uiSettings.timeFormat
+ };
+ }
+ );
+}
+
+function createMapDispatchToProps(dispatch, props) {
+ return {
+ onCancelPress() {
+ dispatch(cancelCommand({
+ id: props.id
+ }));
+ }
+ };
+}
+
+export default connect(createMapStateToProps, createMapDispatchToProps)(QueuedTaskRow);
diff --git a/frontend/src/System/Tasks/Queued/QueuedTasks.js b/frontend/src/System/Tasks/Queued/QueuedTasks.js
new file mode 100644
index 000000000..a2fd526fa
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTasks.js
@@ -0,0 +1,89 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import FieldSet from 'Components/FieldSet';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import Table from 'Components/Table/Table';
+import TableBody from 'Components/Table/TableBody';
+import QueuedTaskRowConnector from './QueuedTaskRowConnector';
+
+const columns = [
+ {
+ name: 'trigger',
+ label: '',
+ isVisible: true
+ },
+ {
+ name: 'commandName',
+ label: 'Name',
+ isVisible: true
+ },
+ {
+ name: 'queued',
+ label: 'Queued',
+ isVisible: true
+ },
+ {
+ name: 'started',
+ label: 'Started',
+ isVisible: true
+ },
+ {
+ name: 'ended',
+ label: 'Ended',
+ isVisible: true
+ },
+ {
+ name: 'duration',
+ label: 'Duration',
+ isVisible: true
+ },
+ {
+ name: 'actions',
+ isVisible: true
+ }
+];
+
+function QueuedTasks(props) {
+ const {
+ isFetching,
+ isPopulated,
+ items
+ } = props;
+
+ return (
+
+ );
+}
+
+QueuedTasks.propTypes = {
+ isFetching: PropTypes.bool.isRequired,
+ isPopulated: PropTypes.bool.isRequired,
+ items: PropTypes.array.isRequired
+};
+
+export default QueuedTasks;
diff --git a/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js
new file mode 100644
index 000000000..5fa4d9ead
--- /dev/null
+++ b/frontend/src/System/Tasks/Queued/QueuedTasksConnector.js
@@ -0,0 +1,46 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+import { createSelector } from 'reselect';
+import { fetchCommands } from 'Store/Actions/commandActions';
+import QueuedTasks from './QueuedTasks';
+
+function createMapStateToProps() {
+ return createSelector(
+ (state) => state.commands,
+ (commands) => {
+ return commands;
+ }
+ );
+}
+
+const mapDispatchToProps = {
+ dispatchFetchCommands: fetchCommands
+};
+
+class QueuedTasksConnector extends Component {
+
+ //
+ // Lifecycle
+
+ componentDidMount() {
+ this.props.dispatchFetchCommands();
+ }
+
+ //
+ // Render
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+QueuedTasksConnector.propTypes = {
+ dispatchFetchCommands: PropTypes.func.isRequired
+};
+
+export default connect(createMapStateToProps, mapDispatchToProps)(QueuedTasksConnector);
diff --git a/frontend/src/System/Tasks/TaskRow.css b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.css
similarity index 100%
rename from frontend/src/System/Tasks/TaskRow.css
rename to frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.css
diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js
new file mode 100644
index 000000000..82cedc720
--- /dev/null
+++ b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRow.js
@@ -0,0 +1,182 @@
+import moment from 'moment';
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import formatDate from 'Utilities/Date/formatDate';
+import formatDateTime from 'Utilities/Date/formatDateTime';
+import { icons } from 'Helpers/Props';
+import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
+import TableRow from 'Components/Table/TableRow';
+import TableRowCell from 'Components/Table/Cells/TableRowCell';
+import styles from './ScheduledTaskRow.css';
+
+function getFormattedDates(props) {
+ const {
+ lastExecution,
+ nextExecution,
+ interval,
+ showRelativeDates,
+ shortDateFormat
+ } = props;
+
+ const isDisabled = interval === 0;
+
+ if (showRelativeDates) {
+ return {
+ lastExecutionTime: moment(lastExecution).fromNow(),
+ nextExecutionTime: isDisabled ? '-' : moment(nextExecution).fromNow()
+ };
+ }
+
+ return {
+ lastExecutionTime: formatDate(lastExecution, shortDateFormat),
+ nextExecutionTime: isDisabled ? '-' : formatDate(nextExecution, shortDateFormat)
+ };
+}
+
+class ScheduledTaskRow extends Component {
+
+ //
+ // Lifecycle
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = getFormattedDates(props);
+
+ this._updateTimeoutId = null;
+ }
+
+ componentDidMount() {
+ this.setUpdateTimer();
+ }
+
+ componentDidUpdate(prevProps) {
+ const {
+ lastExecution,
+ nextExecution
+ } = this.props;
+
+ if (
+ lastExecution !== prevProps.lastExecution ||
+ nextExecution !== prevProps.nextExecution
+ ) {
+ this.setState(getFormattedDates(this.props));
+ }
+ }
+
+ componentWillUnmount() {
+ if (this._updateTimeoutId) {
+ this._updateTimeoutId = clearTimeout(this._updateTimeoutId);
+ }
+ }
+
+ //
+ // Listeners
+
+ setUpdateTimer() {
+ const { interval } = this.props;
+ const timeout = interval < 60 ? 10000 : 60000;
+
+ this._updateTimeoutId = setTimeout(() => {
+ this.setState(getFormattedDates(this.props));
+ this.setUpdateTimer();
+ }, timeout);
+ }
+
+ //
+ // Render
+
+ render() {
+ const {
+ name,
+ interval,
+ lastExecution,
+ nextExecution,
+ isQueued,
+ isExecuting,
+ longDateFormat,
+ timeFormat,
+ onExecutePress
+ } = this.props;
+
+ const {
+ lastExecutionTime,
+ nextExecutionTime
+ } = this.state;
+
+ const isDisabled = interval === 0;
+ const executeNow = !isDisabled && moment().isAfter(nextExecution);
+ const hasNextExecutionTime = !isDisabled && !executeNow;
+ const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1');
+
+ return (
+
+ {name}
+
+ {isDisabled ? 'disabled' : duration}
+
+
+
+ {lastExecutionTime}
+
+
+ {
+ isDisabled &&
+ -
+ }
+
+ {
+ executeNow && isQueued &&
+ queued
+ }
+
+ {
+ executeNow && !isQueued &&
+ now
+ }
+
+ {
+ hasNextExecutionTime &&
+
+ {nextExecutionTime}
+
+ }
+
+
+
+
+
+ );
+ }
+}
+
+ScheduledTaskRow.propTypes = {
+ name: PropTypes.string.isRequired,
+ interval: PropTypes.number.isRequired,
+ lastExecution: PropTypes.string.isRequired,
+ nextExecution: PropTypes.string.isRequired,
+ isQueued: PropTypes.bool.isRequired,
+ isExecuting: PropTypes.bool.isRequired,
+ showRelativeDates: PropTypes.bool.isRequired,
+ shortDateFormat: PropTypes.string.isRequired,
+ longDateFormat: PropTypes.string.isRequired,
+ timeFormat: PropTypes.string.isRequired,
+ onExecutePress: PropTypes.func.isRequired
+};
+
+export default ScheduledTaskRow;
diff --git a/frontend/src/System/Tasks/TaskRowConnector.js b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js
similarity index 80%
rename from frontend/src/System/Tasks/TaskRowConnector.js
rename to frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js
index 035364034..79a0c6c87 100644
--- a/frontend/src/System/Tasks/TaskRowConnector.js
+++ b/frontend/src/System/Tasks/Scheduled/ScheduledTaskRowConnector.js
@@ -2,12 +2,12 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
-import { findCommand } from 'Utilities/Command';
+import { findCommand, isCommandExecuting } from 'Utilities/Command';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchTask } from 'Store/Actions/systemActions';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
-import TaskRow from './TaskRow';
+import ScheduledTaskRow from './ScheduledTaskRow';
function createMapStateToProps() {
return createSelector(
@@ -15,10 +15,11 @@ function createMapStateToProps() {
createCommandsSelector(),
createUISettingsSelector(),
(taskName, commands, uiSettings) => {
- const isExecuting = !!findCommand(commands, { name: taskName });
+ const command = findCommand(commands, { name: taskName });
return {
- isExecuting,
+ isQueued: !!(command && command.state === 'queued'),
+ isExecuting: isCommandExecuting(command),
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
@@ -46,7 +47,7 @@ function createMapDispatchToProps(dispatch, props) {
};
}
-class TaskRowConnector extends Component {
+class ScheduledTaskRowConnector extends Component {
//
// Lifecycle
@@ -75,17 +76,17 @@ class TaskRowConnector extends Component {
} = this.props;
return (
-
);
}
}
-TaskRowConnector.propTypes = {
+ScheduledTaskRowConnector.propTypes = {
id: PropTypes.number.isRequired,
isExecuting: PropTypes.bool.isRequired,
dispatchFetchTask: PropTypes.func.isRequired
};
-export default connect(createMapStateToProps, createMapDispatchToProps)(TaskRowConnector);
+export default connect(createMapStateToProps, createMapDispatchToProps)(ScheduledTaskRowConnector);
diff --git a/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js
new file mode 100644
index 000000000..7c6fe8a32
--- /dev/null
+++ b/frontend/src/System/Tasks/Scheduled/ScheduledTasks.js
@@ -0,0 +1,79 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import FieldSet from 'Components/FieldSet';
+import LoadingIndicator from 'Components/Loading/LoadingIndicator';
+import Table from 'Components/Table/Table';
+import TableBody from 'Components/Table/TableBody';
+import ScheduledTaskRowConnector from './ScheduledTaskRowConnector';
+
+const columns = [
+ {
+ name: 'name',
+ label: 'Name',
+ isVisible: true
+ },
+ {
+ name: 'interval',
+ label: 'Interval',
+ isVisible: true
+ },
+ {
+ name: 'lastExecution',
+ label: 'Last Execution',
+ isVisible: true
+ },
+ {
+ name: 'nextExecution',
+ label: 'Next Execution',
+ isVisible: true
+ },
+ {
+ name: 'actions',
+ isVisible: true
+ }
+];
+
+function ScheduledTasks(props) {
+ const {
+ isFetching,
+ isPopulated,
+ items
+ } = props;
+
+ return (
+
+ );
+}
+
+ScheduledTasks.propTypes = {
+ isFetching: PropTypes.bool.isRequired,
+ isPopulated: PropTypes.bool.isRequired,
+ items: PropTypes.array.isRequired
+};
+
+export default ScheduledTasks;
diff --git a/frontend/src/System/Tasks/TasksConnector.js b/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js
similarity index 66%
rename from frontend/src/System/Tasks/TasksConnector.js
rename to frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js
index 492040674..8f418d3bb 100644
--- a/frontend/src/System/Tasks/TasksConnector.js
+++ b/frontend/src/System/Tasks/Scheduled/ScheduledTasksConnector.js
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchTasks } from 'Store/Actions/systemActions';
-import Tasks from './Tasks';
+import ScheduledTasks from './ScheduledTasks';
function createMapStateToProps() {
return createSelector(
@@ -15,16 +15,16 @@ function createMapStateToProps() {
}
const mapDispatchToProps = {
- fetchTasks
+ dispatchFetchTasks: fetchTasks
};
-class TasksConnector extends Component {
+class ScheduledTasksConnector extends Component {
//
// Lifecycle
componentDidMount() {
- this.props.fetchTasks();
+ this.props.dispatchFetchTasks();
}
//
@@ -32,15 +32,15 @@ class TasksConnector extends Component {
render() {
return (
-
);
}
}
-TasksConnector.propTypes = {
- fetchTasks: PropTypes.func.isRequired
+ScheduledTasksConnector.propTypes = {
+ dispatchFetchTasks: PropTypes.func.isRequired
};
-export default connect(createMapStateToProps, mapDispatchToProps)(TasksConnector);
+export default connect(createMapStateToProps, mapDispatchToProps)(ScheduledTasksConnector);
diff --git a/frontend/src/System/Tasks/TaskRow.js b/frontend/src/System/Tasks/TaskRow.js
deleted file mode 100644
index f118842a7..000000000
--- a/frontend/src/System/Tasks/TaskRow.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import moment from 'moment';
-import PropTypes from 'prop-types';
-import React from 'react';
-import formatDate from 'Utilities/Date/formatDate';
-import formatDateTime from 'Utilities/Date/formatDateTime';
-import { icons } from 'Helpers/Props';
-import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
-import TableRow from 'Components/Table/TableRow';
-import TableRowCell from 'Components/Table/Cells/TableRowCell';
-import styles from './TaskRow.css';
-
-function TaskRow(props) {
- const {
- name,
- interval,
- lastExecution,
- nextExecution,
- isExecuting,
- showRelativeDates,
- shortDateFormat,
- longDateFormat,
- timeFormat,
- onExecutePress
- } = props;
-
- const disabled = interval === 0;
- const executeNow = !disabled && moment().isAfter(nextExecution);
- const hasNextExecutionTime = !disabled && !executeNow;
- const duration = moment.duration(interval, 'minutes').humanize().replace(/an?(?=\s)/, '1');
-
- return (
-
- {name}
-
- {disabled ? 'disabled' : duration}
-
-
-
- {showRelativeDates ? moment(lastExecution).fromNow() : formatDate(lastExecution, shortDateFormat)}
-
-
- {
- disabled &&
- -
- }
-
- {
- executeNow &&
- now
- }
-
- {
- hasNextExecutionTime &&
-
- {showRelativeDates ? moment(nextExecution).fromNow() : formatDate(nextExecution, shortDateFormat)}
-
- }
-
-
-
-
-
- );
-}
-
-TaskRow.propTypes = {
- name: PropTypes.string.isRequired,
- interval: PropTypes.number.isRequired,
- lastExecution: PropTypes.string.isRequired,
- nextExecution: PropTypes.string.isRequired,
- isExecuting: PropTypes.bool.isRequired,
- showRelativeDates: PropTypes.bool.isRequired,
- shortDateFormat: PropTypes.string.isRequired,
- longDateFormat: PropTypes.string.isRequired,
- timeFormat: PropTypes.string.isRequired,
- onExecutePress: PropTypes.func.isRequired
-};
-
-export default TaskRow;
diff --git a/frontend/src/System/Tasks/Tasks.js b/frontend/src/System/Tasks/Tasks.js
index ae2d75dbb..dbbb4d1bf 100644
--- a/frontend/src/System/Tasks/Tasks.js
+++ b/frontend/src/System/Tasks/Tasks.js
@@ -1,89 +1,18 @@
-import PropTypes from 'prop-types';
-import React, { Component } from 'react';
+import React from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
-import LoadingIndicator from 'Components/Loading/LoadingIndicator';
-import Table from 'Components/Table/Table';
-import TableBody from 'Components/Table/TableBody';
-import TaskRowConnector from './TaskRowConnector';
-
-const columns = [
- {
- name: 'name',
- label: 'Name',
- isVisible: true
- },
- {
- name: 'interval',
- label: 'Interval',
- isVisible: true
- },
- {
- name: 'lastExecution',
- label: 'Last Execution',
- isVisible: true
- },
- {
- name: 'nextExecution',
- label: 'Next Execution',
- isVisible: true
- },
- {
- name: 'actions',
- isVisible: true
- }
-];
-
-class Tasks extends Component {
-
- //
- // Render
-
- render() {
- const {
- isFetching,
- isPopulated,
- items
- } = this.props;
-
- return (
-
-
- {
- isFetching && !isPopulated &&
-
- }
-
- {
- isPopulated &&
-
-
- {
- items.map((item) => {
- return (
-
- );
- })
- }
-
-
- }
-
-
- );
- }
+import ScheduledTasksConnector from './Scheduled/ScheduledTasksConnector';
+import QueuedTasksConnector from './Queued/QueuedTasksConnector';
+function Tasks() {
+ return (
+
+
+
+
+
+
+ );
}
-Tasks.propTypes = {
- isFetching: PropTypes.bool.isRequired,
- isPopulated: PropTypes.bool.isRequired,
- items: PropTypes.array.isRequired
-};
-
export default Tasks;
diff --git a/frontend/src/Utilities/Command/isCommandComplete.js b/frontend/src/Utilities/Command/isCommandComplete.js
index e64737188..558ab801b 100644
--- a/frontend/src/Utilities/Command/isCommandComplete.js
+++ b/frontend/src/Utilities/Command/isCommandComplete.js
@@ -3,7 +3,7 @@ function isCommandComplete(command) {
return false;
}
- return command.state === 'complete';
+ return command.status === 'complete';
}
export default isCommandComplete;
diff --git a/frontend/src/Utilities/Command/isCommandExecuting.js b/frontend/src/Utilities/Command/isCommandExecuting.js
index 4e2e6d8c4..8e637704e 100644
--- a/frontend/src/Utilities/Command/isCommandExecuting.js
+++ b/frontend/src/Utilities/Command/isCommandExecuting.js
@@ -3,7 +3,7 @@ function isCommandExecuting(command) {
return false;
}
- return command.state === 'queued' || command.state === 'started';
+ return command.status === 'queued' || command.status === 'started';
}
export default isCommandExecuting;
diff --git a/frontend/src/Utilities/Command/isCommandFailed.js b/frontend/src/Utilities/Command/isCommandFailed.js
index f48d790e3..00e5ccdf2 100644
--- a/frontend/src/Utilities/Command/isCommandFailed.js
+++ b/frontend/src/Utilities/Command/isCommandFailed.js
@@ -3,10 +3,10 @@ function isCommandFailed(command) {
return false;
}
- return command.state === 'failed' ||
- command.state === 'aborted' ||
- command.state === 'cancelled' ||
- command.state === 'orphaned';
+ return command.status === 'failed' ||
+ command.status === 'aborted' ||
+ command.status === 'cancelled' ||
+ command.status === 'orphaned';
}
export default isCommandFailed;
diff --git a/src/Lidarr.Api.V1/Commands/CommandModule.cs b/src/Lidarr.Api.V1/Commands/CommandModule.cs
index 59668931d..088a52698 100644
--- a/src/Lidarr.Api.V1/Commands/CommandModule.cs
+++ b/src/Lidarr.Api.V1/Commands/CommandModule.cs
@@ -32,6 +32,7 @@ namespace Lidarr.Api.V1.Commands
GetResourceById = GetCommand;
CreateResource = StartCommand;
GetResourceAll = GetStartedCommands;
+ DeleteResource = CancelCommand;
PostValidator.RuleFor(c => c.Name).NotBlank();
@@ -62,7 +63,13 @@ namespace Lidarr.Api.V1.Commands
private List GetStartedCommands()
{
- return _commandQueueManager.GetStarted().ToResource();
+ return _commandQueueManager.All().ToResource();
+ }
+
+ private void CancelCommand(int id)
+ {
+ // TODO: Cancel the existing command
+ // Executing tasks should preferably exit gracefully
}
public void Handle(CommandUpdatedEvent message)
@@ -75,6 +82,13 @@ namespace Lidarr.Api.V1.Commands
}
_debouncer.Execute();
}
+
+ if (message.Command.Name == typeof(MessagingCleanupCommand).Name.Replace("Command", "") &&
+ message.Command.Status == CommandStatus.Completed)
+ {
+ BroadcastResourceChange(ModelAction.Sync);
+ }
+
}
private void SendUpdates()
diff --git a/src/Lidarr.Api.V1/Commands/CommandResource.cs b/src/Lidarr.Api.V1/Commands/CommandResource.cs
index 77d2cc295..dff2bf782 100644
--- a/src/Lidarr.Api.V1/Commands/CommandResource.cs
+++ b/src/Lidarr.Api.V1/Commands/CommandResource.cs
@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
+using NzbDrone.Common.Extensions;
using NzbDrone.Core.Messaging.Commands;
using Lidarr.Http.REST;
@@ -10,6 +11,7 @@ namespace Lidarr.Api.V1.Commands
public class CommandResource : RestResource
{
public string Name { get; set; }
+ public string CommandName { get; set; }
public string Message { get; set; }
public Command Body { get; set; }
public CommandPriority Priority { get; set; }
@@ -75,6 +77,7 @@ namespace Lidarr.Api.V1.Commands
Id = model.Id,
Name = model.Name,
+ CommandName = model.Name.SplitCamelCase(),
Message = model.Message,
Body = model.Body,
Priority = model.Priority,
diff --git a/src/Lidarr.Api.V1/System/Tasks/TaskModule.cs b/src/Lidarr.Api.V1/System/Tasks/TaskModule.cs
index cf1a38fb1..819d341fc 100644
--- a/src/Lidarr.Api.V1/System/Tasks/TaskModule.cs
+++ b/src/Lidarr.Api.V1/System/Tasks/TaskModule.cs
@@ -1,6 +1,7 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
+using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Events;
using NzbDrone.Core.Jobs;
using NzbDrone.Core.Messaging.Events;
@@ -13,8 +14,6 @@ namespace Lidarr.Api.V1.System.Tasks
{
private readonly ITaskManager _taskManager;
- private static readonly Regex NameRegex = new Regex("(? " " + match.Value),
+ Name = taskName.SplitCamelCase(),
TaskName = taskName,
Interval = scheduledTask.Interval,
LastExecution = scheduledTask.LastExecution,
diff --git a/src/Lidarr.Http/LidarrRestModuleWithSignalR.cs b/src/Lidarr.Http/LidarrRestModuleWithSignalR.cs
index ea095fb0b..6ac1a41e5 100644
--- a/src/Lidarr.Http/LidarrRestModuleWithSignalR.cs
+++ b/src/Lidarr.Http/LidarrRestModuleWithSignalR.cs
@@ -46,20 +46,22 @@ namespace Lidarr.Http
}
}
-
protected void BroadcastResourceChange(ModelAction action, TResource resource)
{
- var signalRMessage = new SignalRMessage
+ if (GetType().Namespace.Contains("V1"))
{
- Name = Resource,
- Body = new ResourceChangeMessage(resource, action),
- Action = action
- };
+ var signalRMessage = new SignalRMessage
+ {
+ Name = Resource,
+ Body = new ResourceChangeMessage(resource, action),
+ Action = action
+ };
- _signalRBroadcaster.BroadcastMessage(signalRMessage);
+ _signalRBroadcaster.BroadcastMessage(signalRMessage);
+ }
}
-
+
protected void BroadcastResourceChange(ModelAction action)
{
if (GetType().Namespace.Contains("V1"))
diff --git a/src/NzbDrone.Common/Extensions/StringExtensions.cs b/src/NzbDrone.Common/Extensions/StringExtensions.cs
index c75c8ab2b..5eeed958d 100644
--- a/src/NzbDrone.Common/Extensions/StringExtensions.cs
+++ b/src/NzbDrone.Common/Extensions/StringExtensions.cs
@@ -9,6 +9,8 @@ namespace NzbDrone.Common.Extensions
{
public static class StringExtensions
{
+ private static readonly Regex CamelCaseRegex = new Regex("(? " " + match.Value);
+ }
+
}
}
diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandQueue.cs b/src/NzbDrone.Core/Messaging/Commands/CommandQueue.cs
index d4e585f02..793f1eb3a 100644
--- a/src/NzbDrone.Core/Messaging/Commands/CommandQueue.cs
+++ b/src/NzbDrone.Core/Messaging/Commands/CommandQueue.cs
@@ -151,7 +151,7 @@ namespace NzbDrone.Core.Messaging.Commands
// A command ready to execute
else
{
- localItem.StartedAt = DateTime.Now;
+ localItem.StartedAt = DateTime.UtcNow;
localItem.Status = CommandStatus.Started;
item = localItem;
diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs
index dc0e03462..590196dfa 100644
--- a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs
+++ b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs
@@ -17,6 +17,7 @@ namespace NzbDrone.Core.Messaging.Commands
CommandModel Push(TCommand command, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified) where TCommand : Command;
CommandModel Push(string commandName, DateTime? lastExecutionTime, CommandPriority priority = CommandPriority.Normal, CommandTrigger trigger = CommandTrigger.Unspecified);
IEnumerable Queue(CancellationToken cancellationToken);
+ List All();
CommandModel Get(int id);
List GetStarted();
void SetMessage(CommandModel command, string message);
@@ -136,6 +137,12 @@ namespace NzbDrone.Core.Messaging.Commands
return _commandQueue.GetConsumingEnumerable(cancellationToken);
}
+ public List All()
+ {
+ _logger.Trace("Getting all commands");
+ return _commandQueue.All();
+ }
+
public CommandModel Get(int id)
{
var command = _commandQueue.Find(id);