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> </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"