From ba96dad8c7371cc193b3fea2ae3b12c606aa86fe Mon Sep 17 00:00:00 2001
From: Qstick <qstick@gmail.com>
Date: Tue, 28 Aug 2018 23:01:02 -0400
Subject: [PATCH] Fixed: UI and Command manager updates

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
---
 .../AddNewArtist/AddNewArtistSearchResult.js  |  13 +-
 .../src/Album/Details/AlbumDetailsMedium.js   |  13 +-
 frontend/src/Album/EpisodeStatus.js           |  15 +-
 frontend/src/AlbumStudio/AlbumStudioRow.js    |  11 +-
 frontend/src/Artist/Details/ArtistDetails.js  |   2 +
 .../src/Artist/Details/ArtistDetailsSeason.js |  13 +-
 .../Artist/Index/Table/ArtistStatusCell.js    |  22 ++-
 frontend/src/Calendar/Agenda/AgendaEvent.js   |   9 +-
 .../src/Calendar/Day/CalendarDayConnector.js  |   7 +-
 frontend/src/Calendar/Events/CalendarEvent.js |  11 +-
 frontend/src/Components/Icon.js               |  21 ++-
 .../Page/Sidebar/Messages/Message.js          |   3 +-
 .../InteractiveImportModalContent.css         |   1 -
 .../InteractiveImportModalContent.js          |  50 +++---
 .../Interactive/InteractiveImportRow.js       |   3 +
 .../Profiles/Quality/QualityProfileItem.js    |   3 +-
 .../Quality/QualityProfileItemGroup.js        |   3 +-
 frontend/src/Store/Actions/calendarActions.js |   5 +-
 frontend/src/Store/Actions/commandActions.js  |  17 +-
 frontend/src/System/Backup/BackupRow.js       |   9 +-
 frontend/src/System/Status/Health/Health.js   |  11 +-
 .../src/Wanted/CutoffUnmet/CutoffUnmet.js     |   5 +-
 .../CutoffUnmet/CutoffUnmetConnector.js       |   2 -
 frontend/src/Wanted/Missing/Missing.js        |   5 +-
 .../src/Wanted/Missing/MissingConnector.js    |   2 -
 src/Lidarr.Api.V1/Commands/CommandResource.cs |  33 +---
 .../Commands/CommandExecutorFixture.cs        |   4 +-
 .../Commands/CommandQueueManagerFixture.cs    |   7 +-
 .../CheckForFinishedDownloadCommand.cs        |   4 +-
 .../Commands/RenameArtistCommand.cs           |   1 +
 .../MediaFiles/Commands/RenameFilesCommand.cs |   5 +-
 .../TrackImport/Manual/ManualImportCommand.cs |   1 +
 .../Messaging/Commands/Command.cs             |   3 +-
 .../Messaging/Commands/CommandQueue.cs        | 157 ++++++++++++------
 .../Messaging/Commands/CommandQueueManager.cs |  59 +++----
 .../Music/Commands/BulkMoveArtistCommand.cs   |   1 +
 .../Music/Commands/MoveArtistCommand.cs       |   1 +
 src/NzbDrone.Core/Music/MoveArtistService.cs  |  16 +-
 .../Commands/ApplicationUpdateCommand.cs      |   1 +
 test.sh                                       |   1 +
 40 files changed, 298 insertions(+), 252 deletions(-)

diff --git a/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js b/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js
index 3496f64d8..46d04833c 100644
--- a/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js
+++ b/frontend/src/AddArtist/AddNewArtist/AddNewArtistSearchResult.js
@@ -116,13 +116,12 @@ class AddNewArtistSearchResult extends Component {
 
               {
                 isExistingArtist &&
-                  <span title="Already in your library">
-                    <Icon
-                      className={styles.alreadyExistsIcon}
-                      name={icons.CHECK_CIRCLE}
-                      size={36}
-                    />
-                  </span>
+                <Icon
+                  className={styles.alreadyExistsIcon}
+                  name={icons.CHECK_CIRCLE}
+                  size={36}
+                  title="Already in your library"
+                />
               }
             </div>
 
diff --git a/frontend/src/Album/Details/AlbumDetailsMedium.js b/frontend/src/Album/Details/AlbumDetailsMedium.js
index 13dba0642..675ac5954 100644
--- a/frontend/src/Album/Details/AlbumDetailsMedium.js
+++ b/frontend/src/Album/Details/AlbumDetailsMedium.js
@@ -134,13 +134,12 @@ class AlbumDetailsMedium extends Component {
             className={styles.expandButton}
             onPress={this.onExpandPress}
           >
-            <span title={isExpanded ? 'Hide tracks' : 'Show tracks'}>
-              <Icon
-                className={styles.expandButtonIcon}
-                name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
-                size={24}
-              />
-            </span>
+            <Icon
+              className={styles.expandButtonIcon}
+              name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
+              title={isExpanded ? 'Hide tracks' : 'Show tracks'}
+              size={24}
+            />
             {
               !isSmallScreen &&
                 <span>&nbsp;</span>
diff --git a/frontend/src/Album/EpisodeStatus.js b/frontend/src/Album/EpisodeStatus.js
index 8dc912f44..9cdbd1923 100644
--- a/frontend/src/Album/EpisodeStatus.js
+++ b/frontend/src/Album/EpisodeStatus.js
@@ -48,9 +48,10 @@ function EpisodeStatus(props) {
 
   if (grabbed) {
     return (
-      <div className={styles.center} title="Album is downloading">
+      <div className={styles.center}>
         <Icon
           name={icons.DOWNLOADING}
+          title="Album is downloading"
         />
       </div>
     );
@@ -74,9 +75,10 @@ function EpisodeStatus(props) {
 
   if (!airDateUtc) {
     return (
-      <div className={styles.center} title="TBA">
+      <div className={styles.center}>
         <Icon
           name={icons.TBA}
+          title="TBA"
         />
       </div>
     );
@@ -84,9 +86,10 @@ function EpisodeStatus(props) {
 
   if (!monitored) {
     return (
-      <div className={styles.center} title="Album is not monitored">
+      <div className={styles.center}>
         <Icon
           name={icons.UNMONITORED}
+          title="Album is not monitored"
         />
       </div>
     );
@@ -94,18 +97,20 @@ function EpisodeStatus(props) {
 
   if (hasAired) {
     return (
-      <div className={styles.center} title="Track missing from disk">
+      <div className={styles.center}>
         <Icon
           name={icons.MISSING}
+          title="Track missing from disk"
         />
       </div>
     );
   }
 
   return (
-    <div className={styles.center} title="Album has not aired">
+    <div className={styles.center}>
       <Icon
         name={icons.NOT_AIRED}
+        title="Album has not aired"
       />
     </div>
   );
diff --git a/frontend/src/AlbumStudio/AlbumStudioRow.js b/frontend/src/AlbumStudio/AlbumStudioRow.js
index 99d12dd4b..e2ed18f12 100644
--- a/frontend/src/AlbumStudio/AlbumStudioRow.js
+++ b/frontend/src/AlbumStudio/AlbumStudioRow.js
@@ -39,12 +39,11 @@ class AlbumStudioRow extends Component {
         />
 
         <TableRowCell className={styles.status}>
-          <span title={status === 'ended' ? 'Ended' : 'Continuing'}>
-            <Icon
-              className={styles.statusIcon}
-              name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
-            />
-          </span>
+          <Icon
+            className={styles.statusIcon}
+            name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
+            title={status === 'ended' ? 'Ended' : 'Continuing'}
+          />
         </TableRowCell>
 
         <TableRowCell className={styles.title}>
diff --git a/frontend/src/Artist/Details/ArtistDetails.js b/frontend/src/Artist/Details/ArtistDetails.js
index 0aef778e9..25341bb3b 100644
--- a/frontend/src/Artist/Details/ArtistDetails.js
+++ b/frontend/src/Artist/Details/ArtistDetails.js
@@ -580,7 +580,9 @@ class ArtistDetails extends Component {
           <InteractiveImportModal
             isOpen={isInteractiveImportModalOpen}
             folder={path}
+            allowArtistChange={false}
             showFilterExistingFiles={true}
+            showImportMode={false}
             onModalClose={this.onInteractiveImportModalClose}
           />
         </PageContentBodyConnector>
diff --git a/frontend/src/Artist/Details/ArtistDetailsSeason.js b/frontend/src/Artist/Details/ArtistDetailsSeason.js
index 4ec43447b..3df7fca77 100644
--- a/frontend/src/Artist/Details/ArtistDetailsSeason.js
+++ b/frontend/src/Artist/Details/ArtistDetailsSeason.js
@@ -150,13 +150,12 @@ class ArtistDetailsSeason extends Component {
 
             </div>
 
-            <span title={isExpanded ? 'Hide albums' : 'Show albums'}>
-              <Icon
-                className={styles.expandButtonIcon}
-                name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
-                size={24}
-              />
-            </span>
+            <Icon
+              className={styles.expandButtonIcon}
+              name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
+              title={isExpanded ? 'Hide albums' : 'Show albums'}
+              size={24}
+            />
 
             {
               !isSmallScreen &&
diff --git a/frontend/src/Artist/Index/Table/ArtistStatusCell.js b/frontend/src/Artist/Index/Table/ArtistStatusCell.js
index 7ead1d0cf..8b76dc38e 100644
--- a/frontend/src/Artist/Index/Table/ArtistStatusCell.js
+++ b/frontend/src/Artist/Index/Table/ArtistStatusCell.js
@@ -19,19 +19,17 @@ function ArtistStatusCell(props) {
       className={className}
       {...otherProps}
     >
-      <span title={monitored ? 'Artist is monitored' : 'Artist is unmonitored'}>
-        <Icon
-          className={styles.statusIcon}
-          name={monitored ? icons.MONITORED : icons.UNMONITORED}
-        />
-      </span>
+      <Icon
+        className={styles.statusIcon}
+        name={monitored ? icons.MONITORED : icons.UNMONITORED}
+        title={monitored ? 'Artist is monitored' : 'Artist is unmonitored'}
+      />
 
-      <span title={status === 'ended' ? 'Ended' : 'Continuing'}>
-        <Icon
-          className={styles.statusIcon}
-          name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
-        />
-      </span>
+      <Icon
+        className={styles.statusIcon}
+        name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
+        title={status === 'ended' ? 'Ended' : 'Continuing'}
+      />
     </Component>
   );
 }
diff --git a/frontend/src/Calendar/Agenda/AgendaEvent.js b/frontend/src/Calendar/Agenda/AgendaEvent.js
index b450b9382..c869b096c 100644
--- a/frontend/src/Calendar/Agenda/AgendaEvent.js
+++ b/frontend/src/Calendar/Agenda/AgendaEvent.js
@@ -107,11 +107,10 @@ class AgendaEvent extends Component {
 
           {
             !queueItem && grabbed &&
-              <span title="Album is downloading">
-                <Icon
-                  name={icons.DOWNLOADING}
-                />
-              </span>
+              <Icon
+                name={icons.DOWNLOADING}
+                title="Album is downloading"
+              />
           }
         </Link>
       </div>
diff --git a/frontend/src/Calendar/Day/CalendarDayConnector.js b/frontend/src/Calendar/Day/CalendarDayConnector.js
index 30ad0cb61..6206ef4c6 100644
--- a/frontend/src/Calendar/Day/CalendarDayConnector.js
+++ b/frontend/src/Calendar/Day/CalendarDayConnector.js
@@ -4,15 +4,14 @@ import PropTypes from 'prop-types';
 import React, { Component } from 'react';
 import { connect } from 'react-redux';
 import { createSelector } from 'reselect';
-import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
 import CalendarDay from './CalendarDay';
 
 function createCalendarEventsConnector() {
   return createSelector(
     (state, { date }) => date,
-    createClientSideCollectionSelector('calendar'),
-    (date, calendar) => {
-      const filtered = _.filter(calendar.items, (item) => {
+    (state) => state.calendar.items,
+    (date, items) => {
+      const filtered = _.filter(items, (item) => {
         return moment(date).isSame(moment(item.releaseDate), 'day');
       });
 
diff --git a/frontend/src/Calendar/Events/CalendarEvent.js b/frontend/src/Calendar/Events/CalendarEvent.js
index f664879ab..0189fce3d 100644
--- a/frontend/src/Calendar/Events/CalendarEvent.js
+++ b/frontend/src/Calendar/Events/CalendarEvent.js
@@ -91,12 +91,11 @@ class CalendarEvent extends Component {
 
             {
               !queueItem && grabbed &&
-                <span title="Album is downloading">
-                  <Icon
-                    className={styles.statusIcon}
-                    name={icons.DOWNLOADING}
-                  />
-                </span>
+              <Icon
+                className={styles.statusIcon}
+                name={icons.DOWNLOADING}
+                title="Album is downloading"
+              />
             }
           </div>
 
diff --git a/frontend/src/Components/Icon.js b/frontend/src/Components/Icon.js
index 13daccf5d..42a808e20 100644
--- a/frontend/src/Components/Icon.js
+++ b/frontend/src/Components/Icon.js
@@ -7,6 +7,7 @@ import styles from './Icon.css';
 
 function Icon(props) {
   const {
+    containerClassName,
     className,
     name,
     kind,
@@ -16,11 +17,7 @@ function Icon(props) {
     ...otherProps
   } = props;
 
-  if (title && !window.Lidarr.isProduction) {
-    console.error('Icons cannot have a title');
-  }
-
-  return (
+  const icon = (
     <FontAwesomeIcon
       className={classNames(
         className,
@@ -34,9 +31,23 @@ function Icon(props) {
       {...otherProps}
     />
   );
+
+  if (title) {
+    return (
+      <span
+        className={containerClassName}
+        title={title}
+      >
+        {icon}
+      </span>
+    );
+  }
+
+  return icon;
 }
 
 Icon.propTypes = {
+  containerClassName: PropTypes.string,
   className: PropTypes.string,
   name: PropTypes.object.isRequired,
   kind: PropTypes.string.isRequired,
diff --git a/frontend/src/Components/Page/Sidebar/Messages/Message.js b/frontend/src/Components/Page/Sidebar/Messages/Message.js
index b905ce658..bb7a027fa 100644
--- a/frontend/src/Components/Page/Sidebar/Messages/Message.js
+++ b/frontend/src/Components/Page/Sidebar/Messages/Message.js
@@ -45,9 +45,10 @@ function Message(props) {
       styles[type]
     )}
     >
-      <div className={styles.iconContainer} title={name}>
+      <div className={styles.iconContainer}>
         <Icon
           name={getIconName(name)}
+          title={name}
         />
       </div>
 
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css
index 88b4e6178..5bad6c050 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.css
@@ -6,7 +6,6 @@
 
 .filterText {
   margin-left: 5px;
-  font-size: $largeFontSize;
 }
 
 .footer {
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js
index 3a2a9f862..2ec60c549 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportModalContent.js
@@ -169,7 +169,9 @@ class InteractiveImportModalContent extends Component {
   render() {
     const {
       downloadId,
+      allowArtistChange,
       showFilterExistingFiles,
+      showImportMode,
       filterExistingFiles,
       title,
       folder,
@@ -211,17 +213,7 @@ class InteractiveImportModalContent extends Component {
 
         <ModalBody>
           {
-            isFetching &&
-              <LoadingIndicator />
-          }
-
-          {
-            error &&
-              <div>{errorMessage}</div>
-          }
-
-          {
-            isPopulated && showFilterExistingFiles && !isFetching &&
+            showFilterExistingFiles &&
             <div className={styles.filterContainer}>
               <Menu alignMenu={align.RIGHT}>
                 <MenuButton>
@@ -258,6 +250,16 @@ class InteractiveImportModalContent extends Component {
             </div>
           }
 
+          {
+            isFetching &&
+              <LoadingIndicator />
+          }
+
+          {
+            error &&
+              <div>{errorMessage}</div>
+          }
+
           {
             isPopulated && !!items.length && !isFetching && !isFetching &&
               <Table
@@ -278,6 +280,7 @@ class InteractiveImportModalContent extends Component {
                           key={item.id}
                           isSelected={selectedState[item.id]}
                           {...item}
+                          allowArtistChange={allowArtistChange}
                           onSelectedChange={this.onSelectedChange}
                           onValidRowChange={this.onValidRowChange}
                         />
@@ -295,9 +298,9 @@ class InteractiveImportModalContent extends Component {
         </ModalBody>
 
         <ModalFooter className={styles.footer}>
-          {
-            !downloadId &&
-              <div className={styles.leftButtons}>
+          <div className={styles.leftButtons}>
+            {
+              !downloadId && showImportMode &&
                 <SelectInput
                   className={styles.importMode}
                   name="importMode"
@@ -305,13 +308,16 @@ class InteractiveImportModalContent extends Component {
                   values={importModeOptions}
                   onChange={this.onImportModeChange}
                 />
-              </div>
-          }
+            }
+          </div>
 
-          <div className={downloadId ? styles.leftButtons : styles.centerButtons}>
-            <Button onPress={this.onSelectArtistPress}>
-              Select Artist
-            </Button>
+          <div className={styles.centerButtons}>
+            {
+              allowArtistChange &&
+                <Button onPress={this.onSelectArtistPress}>
+                  Select Artist
+                </Button>
+            }
 
             <Button onPress={this.onSelectAlbumPress}>
               Select Album
@@ -357,6 +363,8 @@ class InteractiveImportModalContent extends Component {
 
 InteractiveImportModalContent.propTypes = {
   downloadId: PropTypes.string,
+  allowArtistChange: PropTypes.bool.isRequired,
+  showImportMode: PropTypes.bool.isRequired,
   showFilterExistingFiles: PropTypes.bool.isRequired,
   filterExistingFiles: PropTypes.bool.isRequired,
   importMode: PropTypes.string.isRequired,
@@ -377,7 +385,9 @@ InteractiveImportModalContent.propTypes = {
 };
 
 InteractiveImportModalContent.defaultProps = {
+  allowArtistChange: true,
   showFilterExistingFiles: false,
+  showImportMode: true,
   importMode: 'move'
 };
 
diff --git a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
index 7a463ec61..ae8a76391 100644
--- a/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
+++ b/frontend/src/InteractiveImport/Interactive/InteractiveImportRow.js
@@ -163,6 +163,7 @@ class InteractiveImportRow extends Component {
   render() {
     const {
       id,
+      allowArtistChange,
       relativePath,
       artist,
       album,
@@ -210,6 +211,7 @@ class InteractiveImportRow extends Component {
         </TableRowCell>
 
         <TableRowCellButton
+          isDisabled={!allowArtistChange}
           onPress={this.onSelectArtistPress}
         >
           {
@@ -348,6 +350,7 @@ class InteractiveImportRow extends Component {
 
 InteractiveImportRow.propTypes = {
   id: PropTypes.number.isRequired,
+  allowArtistChange: PropTypes.bool.isRequired,
   relativePath: PropTypes.string.isRequired,
   artist: PropTypes.object,
   album: PropTypes.object,
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js
index 5950b4d52..8161e7061 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItem.js
@@ -92,9 +92,10 @@ class QualityProfileItem extends Component {
 
         {
           connectDragSource(
-            <div className={styles.dragHandle} title="Create group">
+            <div className={styles.dragHandle}>
               <Icon
                 className={styles.dragIcon}
+                title="Create group"
                 name={icons.REORDER}
               />
             </div>
diff --git a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js
index b59df95bb..34008b1ec 100644
--- a/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js
+++ b/frontend/src/Settings/Profiles/Quality/QualityProfileItemGroup.js
@@ -129,10 +129,11 @@ class QualityProfileItemGroup extends Component {
 
           {
             connectDragSource(
-              <div className={styles.dragHandle} title="Reorder">
+              <div className={styles.dragHandle}>
                 <Icon
                   className={styles.dragIcon}
                   name={icons.REORDER}
+                  title="Reorder"
                 />
               </div>
             )
diff --git a/frontend/src/Store/Actions/calendarActions.js b/frontend/src/Store/Actions/calendarActions.js
index 3d4337b60..c9fdedf08 100644
--- a/frontend/src/Store/Actions/calendarActions.js
+++ b/frontend/src/Store/Actions/calendarActions.js
@@ -45,7 +45,7 @@ export const defaultState = {
       filters: [
         {
           key: 'monitored',
-          value: false || true,
+          value: false,
           type: filterTypes.EQUAL
         }
       ]
@@ -66,7 +66,8 @@ export const defaultState = {
 
 export const persistState = [
   'calendar.view',
-  'calendar.selectedFilterKey'
+  'calendar.selectedFilterKey',
+  'calendar.showUpcoming'
 ];
 
 //
diff --git a/frontend/src/Store/Actions/commandActions.js b/frontend/src/Store/Actions/commandActions.js
index f5c08270f..1c399c88e 100644
--- a/frontend/src/Store/Actions/commandActions.js
+++ b/frontend/src/Store/Actions/commandActions.js
@@ -57,7 +57,7 @@ function showCommandMessage(payload, dispatch) {
   const {
     id,
     name,
-    manual,
+    trigger,
     message,
     body = {},
     state
@@ -80,7 +80,7 @@ function showCommandMessage(payload, dispatch) {
     hideAfter = 4;
   } else if (state === 'failed') {
     type = messageTypes.ERROR;
-    hideAfter = manual ? 10 : 4;
+    hideAfter = trigger === 'manual' ? 10 : 4;
   }
 
   dispatch(showMessage({
@@ -95,10 +95,11 @@ function showCommandMessage(payload, dispatch) {
 function scheduleRemoveCommand(command, dispatch) {
   const {
     id,
-    state
+    status,
+    body
   } = command;
 
-  if (state === 'queued') {
+  if (status === 'queued') {
     return;
   }
 
@@ -108,6 +109,12 @@ 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 }),
@@ -115,7 +122,7 @@ function scheduleRemoveCommand(command, dispatch) {
     ]));
 
     delete removeCommandTimeoutIds[id];
-  }, 30000);
+  }, timeout);
 }
 
 //
diff --git a/frontend/src/System/Backup/BackupRow.js b/frontend/src/System/Backup/BackupRow.js
index cbc53812e..e32145352 100644
--- a/frontend/src/System/Backup/BackupRow.js
+++ b/frontend/src/System/Backup/BackupRow.js
@@ -87,11 +87,10 @@ class BackupRow extends Component {
       <TableRow key={id}>
         <TableRowCell className={styles.type}>
           {
-            <span title={iconTooltip}>
-              <Icon
-                name={iconClassName}
-              />
-            </span>
+            <Icon
+              name={iconClassName}
+              title={iconTooltip}
+            />
           }
         </TableRowCell>
 
diff --git a/frontend/src/System/Status/Health/Health.js b/frontend/src/System/Status/Health/Health.js
index 869354ae1..ff2165048 100644
--- a/frontend/src/System/Status/Health/Health.js
+++ b/frontend/src/System/Status/Health/Health.js
@@ -125,12 +125,11 @@ class Health extends Component {
                     return (
                       <TableRow key={`health${item.message}`}>
                         <TableRowCell>
-                          <span title={titleCase(item.type)}>
-                            <Icon
-                              name={icons.DANGER}
-                              kind={item.type.toLowerCase() === 'error' ? kinds.DANGER : kinds.WARNING}
-                            />
-                          </span>
+                          <Icon
+                            name={icons.DANGER}
+                            kind={item.type.toLowerCase() === 'error' ? kinds.DANGER : kinds.WARNING}
+                            title={titleCase(item.type)}
+                          />
                         </TableRowCell>
 
                         <TableRowCell>{item.message}</TableRowCell>
diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js
index 237ac3293..f328b73ac 100644
--- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js
+++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmet.js
@@ -105,7 +105,6 @@ class CutoffUnmet extends Component {
       filters,
       columns,
       totalRecords,
-      isSearchingForAlbums,
       isSearchingForCutoffUnmetAlbums,
       isSaving,
       onFilterSelect,
@@ -129,8 +128,7 @@ class CutoffUnmet extends Component {
             <PageToolbarButton
               label="Search Selected"
               iconName={icons.SEARCH}
-              isDisabled={!itemsSelected}
-              isSpinning={isSearchingForAlbums}
+              isDisabled={!itemsSelected || isSearchingForCutoffUnmetAlbums}
               onPress={this.onSearchSelectedPress}
             />
 
@@ -255,7 +253,6 @@ CutoffUnmet.propTypes = {
   filters: PropTypes.arrayOf(PropTypes.object).isRequired,
   columns: PropTypes.arrayOf(PropTypes.object).isRequired,
   totalRecords: PropTypes.number,
-  isSearchingForAlbums: PropTypes.bool.isRequired,
   isSearchingForCutoffUnmetAlbums: PropTypes.bool.isRequired,
   isSaving: PropTypes.bool.isRequired,
   onFilterSelect: PropTypes.func.isRequired,
diff --git a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js
index bc93388a3..6e7149b0c 100644
--- a/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js
+++ b/frontend/src/Wanted/CutoffUnmet/CutoffUnmetConnector.js
@@ -20,11 +20,9 @@ function createMapStateToProps() {
     (state) => state.wanted.cutoffUnmet,
     createCommandsSelector(),
     (cutoffUnmet, commands) => {
-      const isSearchingForAlbums = _.some(commands, { name: commandNames.ALBUM_SEARCH });
       const isSearchingForCutoffUnmetAlbums = _.some(commands, { name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH });
 
       return {
-        isSearchingForAlbums,
         isSearchingForCutoffUnmetAlbums,
         isSaving: _.some(cutoffUnmet.items, { isSaving: true }),
         ...cutoffUnmet
diff --git a/frontend/src/Wanted/Missing/Missing.js b/frontend/src/Wanted/Missing/Missing.js
index c613f101f..3eda2ed60 100644
--- a/frontend/src/Wanted/Missing/Missing.js
+++ b/frontend/src/Wanted/Missing/Missing.js
@@ -114,7 +114,6 @@ class Missing extends Component {
       filters,
       columns,
       totalRecords,
-      isSearchingForAlbums,
       isSearchingForMissingAlbums,
       isSaving,
       onFilterSelect,
@@ -139,8 +138,7 @@ class Missing extends Component {
             <PageToolbarButton
               label="Search Selected"
               iconName={icons.SEARCH}
-              isDisabled={!itemsSelected}
-              isSpinning={isSearchingForAlbums}
+              isDisabled={!itemsSelected || isSearchingForMissingAlbums}
               onPress={this.onSearchSelectedPress}
             />
 
@@ -277,7 +275,6 @@ Missing.propTypes = {
   filters: PropTypes.arrayOf(PropTypes.object).isRequired,
   columns: PropTypes.arrayOf(PropTypes.object).isRequired,
   totalRecords: PropTypes.number,
-  isSearchingForAlbums: PropTypes.bool.isRequired,
   isSearchingForMissingAlbums: PropTypes.bool.isRequired,
   isSaving: PropTypes.bool.isRequired,
   onFilterSelect: PropTypes.func.isRequired,
diff --git a/frontend/src/Wanted/Missing/MissingConnector.js b/frontend/src/Wanted/Missing/MissingConnector.js
index c7d7e8328..05f6128c0 100644
--- a/frontend/src/Wanted/Missing/MissingConnector.js
+++ b/frontend/src/Wanted/Missing/MissingConnector.js
@@ -19,11 +19,9 @@ function createMapStateToProps() {
     (state) => state.wanted.missing,
     createCommandsSelector(),
     (missing, commands) => {
-      const isSearchingForAlbums = _.some(commands, { name: commandNames.ALBUM_SEARCH });
       const isSearchingForMissingAlbums = _.some(commands, { name: commandNames.MISSING_ALBUM_SEARCH });
 
       return {
-        isSearchingForAlbums,
         isSearchingForMissingAlbums,
         isSaving: _.some(missing.items, { isSaving: true }),
         ...missing
diff --git a/src/Lidarr.Api.V1/Commands/CommandResource.cs b/src/Lidarr.Api.V1/Commands/CommandResource.cs
index 6e0f8a907..77d2cc295 100644
--- a/src/Lidarr.Api.V1/Commands/CommandResource.cs
+++ b/src/Lidarr.Api.V1/Commands/CommandResource.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
 using System.Collections.Generic;
 using System.Linq;
 using Newtonsoft.Json;
@@ -24,37 +24,6 @@ namespace Lidarr.Api.V1.Commands
         [JsonIgnore]
         public string CompletionMessage { get; set; }
 
-        //Legacy
-        public CommandStatus State
-        {
-            get
-            {
-                return Status;
-            }
-
-            set { }
-        }
-
-        public bool Manual
-        {
-            get
-            {
-                return Trigger == CommandTrigger.Manual;
-            }
-
-            set { }
-        }
-
-        public DateTime StartedOn
-        {
-            get
-            {
-                return Queued;
-            }
-
-            set { }
-        }
-
         public DateTime? StateChangeTime
         {
             get
diff --git a/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs b/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs
index e1a7853f8..8b57f70c1 100644
--- a/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs
+++ b/src/NzbDrone.Core.Test/Messaging/Commands/CommandExecutorFixture.cs
@@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Messaging.Commands
     [TestFixture]
     public class CommandExecutorFixture : TestBase<CommandExecutor>
     {
-        private BlockingCollection<CommandModel> _commandQueue;
+        private CommandQueue _commandQueue;
         private Mock<IExecute<CommandA>> _executorA;
         private Mock<IExecute<CommandB>> _executorB;
         private bool _commandExecuted = false;
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.Messaging.Commands
 
         private void GivenCommandQueue()
         {
-            _commandQueue = new BlockingCollection<CommandModel>(new CommandQueue());
+            _commandQueue = new CommandQueue();
 
             Mocker.GetMock<IManageCommandQueue>()
                   .Setup(s => s.Queue(It.IsAny<CancellationToken>()))
diff --git a/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs b/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs
index 16178a9cc..68ec47951 100644
--- a/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs
+++ b/src/NzbDrone.Core.Test/Messaging/Commands/CommandQueueManagerFixture.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using System.Linq;
 using FluentAssertions;
 using Moq;
@@ -42,6 +43,10 @@ namespace NzbDrone.Core.Test.Messaging.Commands
         {
             var command = Subject.Push<CheckForFinishedDownloadCommand>(new CheckForFinishedDownloadCommand());
 
+            // Start the command to mimic CommandQueue's behaviour
+            command.StartedAt = DateTime.Now;
+            command.Status = CommandStatus.Started;
+
             Subject.Start(command);
             Subject.Complete(command, "All done");
             Subject.CleanCommands();
diff --git a/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs b/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs
index 7dc987d84..71f7f3d5e 100644
--- a/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs
+++ b/src/NzbDrone.Core/Download/CheckForFinishedDownloadCommand.cs
@@ -1,9 +1,9 @@
-using NzbDrone.Core.Messaging.Commands;
+using NzbDrone.Core.Messaging.Commands;
 
 namespace NzbDrone.Core.Download
 {
     public class CheckForFinishedDownloadCommand : Command
     {
-
+        public override bool RequiresDiskAccess => true;
     }
 }
diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs
index 26b1077be..1e027f570 100644
--- a/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs
+++ b/src/NzbDrone.Core/MediaFiles/Commands/RenameArtistCommand.cs
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.MediaFiles.Commands
         public List<int> ArtistIds { get; set; }
 
         public override bool SendUpdatesToClient => true;
+        public override bool RequiresDiskAccess => true;
 
         public RenameArtistCommand()
         {
diff --git a/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs b/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs
index 4154116a9..e7464a2ad 100644
--- a/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs
+++ b/src/NzbDrone.Core/MediaFiles/Commands/RenameFilesCommand.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
 using NzbDrone.Core.Messaging.Commands;
 
 namespace NzbDrone.Core.MediaFiles.Commands
@@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles.Commands
         public List<int> Files { get; set; }
 
         public override bool SendUpdatesToClient => true;
+        public override bool RequiresDiskAccess => true;
 
         public RenameFilesCommand()
         {
@@ -20,4 +21,4 @@ namespace NzbDrone.Core.MediaFiles.Commands
             Files = files;
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportCommand.cs b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportCommand.cs
index bf7851268..2c03d3f91 100644
--- a/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportCommand.cs
+++ b/src/NzbDrone.Core/MediaFiles/TrackImport/Manual/ManualImportCommand.cs
@@ -8,6 +8,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
         public List<ManualImportFile> Files { get; set; }
 
         public override bool SendUpdatesToClient => true;
+        public override bool RequiresDiskAccess => true;
 
         public ImportMode ImportMode { get; set; }
     }
diff --git a/src/NzbDrone.Core/Messaging/Commands/Command.cs b/src/NzbDrone.Core/Messaging/Commands/Command.cs
index 2eb164c03..db80322f5 100644
--- a/src/NzbDrone.Core/Messaging/Commands/Command.cs
+++ b/src/NzbDrone.Core/Messaging/Commands/Command.cs
@@ -20,8 +20,9 @@ namespace NzbDrone.Core.Messaging.Commands
         }
 
         public virtual bool UpdateScheduledTask => true;
-
         public virtual string CompletionMessage => "Completed";
+        public virtual bool RequiresDiskAccess => false;
+        public virtual bool IsExclusive => false;
 
         public string Name { get; private set; }
         public DateTime? LastExecutionTime { get; set; }
diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandQueue.cs b/src/NzbDrone.Core/Messaging/Commands/CommandQueue.cs
index ad555fe6c..d4e585f02 100644
--- a/src/NzbDrone.Core/Messaging/Commands/CommandQueue.cs
+++ b/src/NzbDrone.Core/Messaging/Commands/CommandQueue.cs
@@ -1,27 +1,36 @@
-using System;
+using System;
 using System.Collections;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
+using System.Threading;
 
 namespace NzbDrone.Core.Messaging.Commands
 {
-    public class CommandQueue : IProducerConsumerCollection<CommandModel>
+    public class CommandQueue : IEnumerable
     {
-        private object Mutex = new object();
-
-        private List<CommandModel> _items;
+        private readonly object _mutex = new object();
+        private readonly List<CommandModel> _items;
 
         public CommandQueue()
         {
             _items = new List<CommandModel>();
         }
 
+        public int Count => _items.Count;
+
+        public void Add(CommandModel item)
+        {
+            lock (_mutex)
+            {
+                _items.Add(item);
+            }
+        }
+
         public IEnumerator<CommandModel> GetEnumerator()
         {
             List<CommandModel> copy = null;
 
-            lock (Mutex)
+            lock (_mutex)
             {
                 copy = new List<CommandModel>(_items);
             }
@@ -34,77 +43,123 @@ namespace NzbDrone.Core.Messaging.Commands
             return GetEnumerator();
         }
 
-        public void CopyTo(Array array, int index)
+        public List<CommandModel> All()
         {
-            lock (Mutex)
+            List<CommandModel> rval = null;
+
+            lock (_mutex)
             {
-                ((ICollection)_items).CopyTo(array, index);
+                rval = _items;
             }
-        }
 
-        public int Count => _items.Count;
-
-        public object SyncRoot => Mutex;
+            return rval;
+        }
 
-        public bool IsSynchronized => true;
+        public CommandModel Find(int id)
+        {
+            return All().FirstOrDefault(q => q.Id == id);
+        }
 
-        public void CopyTo(CommandModel[] array, int index)
+        public void RemoveMany(IEnumerable<CommandModel> commands)
         {
-            lock (Mutex)
+            lock (_mutex)
             {
-                _items.CopyTo(array, index);
+                foreach (var command in commands)
+                {
+                    _items.Remove(command);
+                }
             }
         }
 
-        public bool TryAdd(CommandModel item)
+        public List<CommandModel> QueuedOrStarted()
+        {
+            return All().Where(q => q.Status == CommandStatus.Queued || q.Status == CommandStatus.Started)
+                .ToList();
+        }
+
+        public IEnumerable<CommandModel> GetConsumingEnumerable()
         {
-            Add(item);
-            return true;
+            return GetConsumingEnumerable(CancellationToken.None);
         }
 
-        public bool TryTake(out CommandModel item)
+        public IEnumerable<CommandModel> GetConsumingEnumerable(CancellationToken cancellationToken)
         {
-            bool rval = true;
-            lock (Mutex)
+            while (!cancellationToken.IsCancellationRequested)
             {
-                if (_items.Count == 0)
+                if (TryGet(out var command))
                 {
-                    item = default(CommandModel);
-                    rval = false;
+                    yield return command;
                 }
 
-                else
-                {
-                    item = _items.Where(c => c.Status == CommandStatus.Queued)
-                                 .OrderByDescending(c => c.Priority)
-                                 .ThenBy(c => c.QueuedAt)
-                                 .First();
-
-                    _items.Remove(item);
-                }
+                Thread.Sleep(10);
             }
-
-            return rval;
         }
 
-        public CommandModel[] ToArray()
+        public bool TryGet(out CommandModel item)
         {
-            CommandModel[] rval = null;
+            var rval = true;
+            item = default(CommandModel);
 
-            lock (Mutex)
+            lock (_mutex)
             {
-                rval = _items.ToArray();
-            }
+                if (_items.Count == 0)
+                {
+                    rval = false;
+                }
 
-            return rval;
-        }
+                else
+                {
+                    var startedCommands = _items.Where(c => c.Status == CommandStatus.Started)
+                        .ToList();
+
+                        var localItem = _items.Where(c =>
+                        {
+                            // If an executing command requires disk access don't return a command that
+                            // requires disk access. A lower priority or later queued task could be returned
+                            // instead, but that will allow other tasks to execute whiule waiting for disk access.
+                            if (startedCommands.Any(x => x.Body.RequiresDiskAccess))
+                            {
+                                return c.Status == CommandStatus.Queued &&
+                                       !c.Body.RequiresDiskAccess;
+                            }
+
+                            return c.Status == CommandStatus.Queued;
+                        })
+                                              .OrderByDescending(c => c.Priority)
+                                              .ThenBy(c => c.QueuedAt)
+                                              .FirstOrDefault();
+
+                        // Nothing queued that meets the requirements
+                        if (localItem == null)
+                        {
+                            rval = false;
+                        }
+
+                        // If any executing command is exclusive don't want return another command until it completes.
+                        else if (startedCommands.Any(c => c.Body.IsExclusive))
+                        {
+                            rval = false;
+                        }
+
+                        // If the next command to execute is exclusive wait for executing commands to complete.
+                        // This will prevent other tasks from starting so the exclusive task executes in the order it should.
+                        else if (localItem.Body.IsExclusive && startedCommands.Any())
+                        {
+                            rval = false;
+                        }
+
+                        // A command ready to execute
+                        else
+                        {
+                            localItem.StartedAt = DateTime.Now;
+                            localItem.Status = CommandStatus.Started;
+
+                            item = localItem;
+                        }
+                    }
+                }
 
-        public void Add(CommandModel item)
-        {
-            lock (Mutex)
-            {
-                _items.Add(item);
+            return rval;
             }
         }
-    }
 }
diff --git a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs
index 5fb2eb02a..dc0e03462 100644
--- a/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs
+++ b/src/NzbDrone.Core/Messaging/Commands/CommandQueueManager.cs
@@ -1,11 +1,9 @@
 using System;
-using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 using System.Threading;
 using NLog;
 using NzbDrone.Common;
-using NzbDrone.Common.Cache;
 using NzbDrone.Common.EnsureThat;
 using NzbDrone.Common.Serializer;
 using NzbDrone.Core.Lifecycle;
@@ -35,20 +33,17 @@ namespace NzbDrone.Core.Messaging.Commands
         private readonly IServiceFactory _serviceFactory;
         private readonly Logger _logger;
 
-        private readonly ICached<CommandModel> _commandCache;
-        private readonly BlockingCollection<CommandModel> _commandQueue;
+        private readonly CommandQueue _commandQueue;
 
         public CommandQueueManager(ICommandRepository repo,
                                    IServiceFactory serviceFactory,
-                                   ICacheManager cacheManager,
                                    Logger logger)
         {
             _repo = repo;
             _serviceFactory = serviceFactory;
             _logger = logger;
 
-            _commandCache = cacheManager.GetCache<CommandModel>(GetType());
-            _commandQueue = new BlockingCollection<CommandModel>(new CommandQueue());
+            _commandQueue = new CommandQueue();
         }
 
         public List<CommandModel> PushMany<TCommand>(List<TCommand> commands) where TCommand : Command
@@ -56,8 +51,7 @@ namespace NzbDrone.Core.Messaging.Commands
             _logger.Trace("Publishing {0} commands", commands.Count);
 
             var commandModels = new List<CommandModel>();
-            var existingCommands = _commandCache.Values.Where(q => q.Status == CommandStatus.Queued ||
-                                                              q.Status == CommandStatus.Started).ToList();
+            var existingCommands = _commandQueue.QueuedOrStarted();
 
             foreach (var command in commands)
             {
@@ -86,7 +80,6 @@ namespace NzbDrone.Core.Messaging.Commands
 
             foreach (var commandModel in commandModels)
             {
-                _commandCache.Set(commandModel.Id.ToString(), commandModel);
                 _commandQueue.Add(commandModel);
             }
 
@@ -124,7 +117,6 @@ namespace NzbDrone.Core.Messaging.Commands
             _logger.Trace("Inserting new command: {0}", commandModel.Name);
 
             _repo.Insert(commandModel);
-            _commandCache.Set(commandModel.Id.ToString(), commandModel);
             _commandQueue.Add(commandModel);
 
             return commandModel;
@@ -146,28 +138,31 @@ namespace NzbDrone.Core.Messaging.Commands
 
         public CommandModel Get(int id)
         {
-            return _commandCache.Get(id.ToString(), () => FindCommand(_repo.Get(id)));
+            var command = _commandQueue.Find(id);
+
+            if (command == null)
+            {
+                command = _repo.Get(id);
+            }
+
+            return command;
         }
 
         public List<CommandModel> GetStarted()
         {
             _logger.Trace("Getting started commands");
-            return _commandCache.Values.Where(c => c.Status == CommandStatus.Started).ToList();
+            return _commandQueue.All().Where(c => c.Status == CommandStatus.Started).ToList();
         }
 
         public void SetMessage(CommandModel command, string message)
         {
             command.Message = message;
-            _commandCache.Set(command.Id.ToString(), command);
         }
 
         public void Start(CommandModel command)
         {
-            command.StartedAt = DateTime.UtcNow;
-            command.Status = CommandStatus.Started;
-
+            // Marks the command as started in the DB, the queue takes care of marking it as started on it's own
             _logger.Trace("Marking command as started: {0}", command.Name);
-            _commandCache.Set(command.Id.ToString(), command);
             _repo.Start(command);
         }
 
@@ -195,12 +190,11 @@ namespace NzbDrone.Core.Messaging.Commands
         {
             _logger.Trace("Cleaning up old commands");
 
-            var old = _commandCache.Values.Where(c => c.EndedAt < DateTime.UtcNow.AddMinutes(-5));
+            var commands = _commandQueue.All()
+                .Where(c => c.EndedAt < DateTime.UtcNow.AddMinutes(-5))
+                .ToList();
 
-            foreach (var command in old)
-            {
-                _commandCache.Remove(command.Id.ToString());
-            }
+            _commandQueue.RemoveMany(commands);
 
             _repo.Trim();
         }
@@ -215,18 +209,6 @@ namespace NzbDrone.Core.Messaging.Commands
             return Json.Deserialize("{}", commandType);
         }
 
-        private CommandModel FindCommand(CommandModel command)
-        {
-            var cachedCommand = _commandCache.Find(command.Id.ToString());
-
-            if (cachedCommand != null)
-            {
-                command.Message = cachedCommand.Message;
-            }
-
-            return command;
-        }
-
         private void Update(CommandModel command, CommandStatus status, string message)
         {
             SetMessage(command, message);
@@ -236,15 +218,14 @@ namespace NzbDrone.Core.Messaging.Commands
             command.Status = status;
 
             _logger.Trace("Updating command status");
-            _commandCache.Set(command.Id.ToString(), command);
             _repo.End(command);
         }
 
         private List<CommandModel> QueuedOrStarted(string name)
         {
-            return _commandCache.Values.Where(q => q.Name == name &&
-                                                   (q.Status == CommandStatus.Queued ||
-                                                    q.Status == CommandStatus.Started)).ToList();
+            return _commandQueue.QueuedOrStarted()
+                .Where(q => q.Name == name)
+                .ToList();
         }
 
         public void Handle(ApplicationStartedEvent message)
diff --git a/src/NzbDrone.Core/Music/Commands/BulkMoveArtistCommand.cs b/src/NzbDrone.Core/Music/Commands/BulkMoveArtistCommand.cs
index 1a2fed394..8f035792b 100644
--- a/src/NzbDrone.Core/Music/Commands/BulkMoveArtistCommand.cs
+++ b/src/NzbDrone.Core/Music/Commands/BulkMoveArtistCommand.cs
@@ -10,6 +10,7 @@ namespace NzbDrone.Core.Music.Commands
         public string DestinationRootFolder { get; set; }
 
         public override bool SendUpdatesToClient => true;
+        public override bool RequiresDiskAccess => true;
     }
 
     public class BulkMoveArtist : IEquatable<BulkMoveArtist>
diff --git a/src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs b/src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs
index 4ece88c3b..c120eddd4 100644
--- a/src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs
+++ b/src/NzbDrone.Core/Music/Commands/MoveArtistCommand.cs
@@ -9,5 +9,6 @@ namespace NzbDrone.Core.Music.Commands
         public string DestinationPath { get; set; }
 
         public override bool SendUpdatesToClient => true;
+        public override bool RequiresDiskAccess => true;
     }
 }
diff --git a/src/NzbDrone.Core/Music/MoveArtistService.cs b/src/NzbDrone.Core/Music/MoveArtistService.cs
index d8f792fbc..312d22c89 100644
--- a/src/NzbDrone.Core/Music/MoveArtistService.cs
+++ b/src/NzbDrone.Core/Music/MoveArtistService.cs
@@ -34,7 +34,7 @@ namespace NzbDrone.Core.Music
             _logger = logger;
         }
 
-        private void MoveSingleArtist(Artist artist, string sourcePath, string destinationPath)
+        private void MoveSingleArtist(Artist artist, string sourcePath, string destinationPath, int? index = null, int? total = null)
         {
             if (!_diskProvider.FolderExists(sourcePath))
             {
@@ -42,7 +42,14 @@ namespace NzbDrone.Core.Music
                 return;
             }
 
-            _logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", artist.Name, sourcePath, destinationPath);
+            if (index != null && total != null)
+            {
+                _logger.ProgressInfo("Moving {0} from '{1}' to '{2}' ({3}/{4})", artist.Name, sourcePath, destinationPath, index + 1, total);
+            }
+            else
+            {
+                _logger.ProgressInfo("Moving {0} from '{1}' to '{2}'", artist.Name, sourcePath, destinationPath);
+            }
 
             try
             {
@@ -81,12 +88,13 @@ namespace NzbDrone.Core.Music
 
             _logger.ProgressInfo("Moving {0} artist to '{1}'", artistToMove.Count, destinationRootFolder);
 
-            foreach (var s in artistToMove)
+            for (var index = 0; index < artistToMove.Count; index++)
             {
+                var s = artistToMove[index];
                 var artist = _artistService.GetArtist(s.ArtistId);
                 var destinationPath = Path.Combine(destinationRootFolder, _filenameBuilder.GetArtistFolder(artist));
 
-                MoveSingleArtist(artist, s.SourcePath, destinationPath);
+                MoveSingleArtist(artist, s.SourcePath, destinationPath, index, artistToMove.Count);
             }
 
             _logger.ProgressInfo("Finished moving {0} artist to '{1}'", artistToMove.Count, destinationRootFolder);
diff --git a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs
index f3b920b08..0ca1d8074 100644
--- a/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs
+++ b/src/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs
@@ -5,6 +5,7 @@ namespace NzbDrone.Core.Update.Commands
     public class ApplicationUpdateCommand : Command
     {
         public override bool SendUpdatesToClient => true;
+        public override bool IsExclusive => true;
 
         public override string CompletionMessage => null;
     }
diff --git a/test.sh b/test.sh
index f0f242a95..e4a1307f4 100644
--- a/test.sh
+++ b/test.sh
@@ -1,3 +1,4 @@
+#! /bin/bash
 PLATFORM=$1
 TYPE=$2
 WHERE="cat != ManualTest"