mirror of
https://github.com/lidarr/lidarr.git
synced 2025-07-06 04:52:21 -07:00
Fixed: UI and Command manager updates
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
parent
d9a51a1d02
commit
ba96dad8c7
40 changed files with 301 additions and 255 deletions
|
@ -116,13 +116,12 @@ class AddNewArtistSearchResult extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
isExistingArtist &&
|
isExistingArtist &&
|
||||||
<span title="Already in your library">
|
<Icon
|
||||||
<Icon
|
className={styles.alreadyExistsIcon}
|
||||||
className={styles.alreadyExistsIcon}
|
name={icons.CHECK_CIRCLE}
|
||||||
name={icons.CHECK_CIRCLE}
|
size={36}
|
||||||
size={36}
|
title="Already in your library"
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -134,13 +134,12 @@ class AlbumDetailsMedium extends Component {
|
||||||
className={styles.expandButton}
|
className={styles.expandButton}
|
||||||
onPress={this.onExpandPress}
|
onPress={this.onExpandPress}
|
||||||
>
|
>
|
||||||
<span title={isExpanded ? 'Hide tracks' : 'Show tracks'}>
|
<Icon
|
||||||
<Icon
|
className={styles.expandButtonIcon}
|
||||||
className={styles.expandButtonIcon}
|
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
|
||||||
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
|
title={isExpanded ? 'Hide tracks' : 'Show tracks'}
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
{
|
{
|
||||||
!isSmallScreen &&
|
!isSmallScreen &&
|
||||||
<span> </span>
|
<span> </span>
|
||||||
|
|
|
@ -48,9 +48,10 @@ function EpisodeStatus(props) {
|
||||||
|
|
||||||
if (grabbed) {
|
if (grabbed) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.center} title="Album is downloading">
|
<div className={styles.center}>
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.DOWNLOADING}
|
name={icons.DOWNLOADING}
|
||||||
|
title="Album is downloading"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -74,9 +75,10 @@ function EpisodeStatus(props) {
|
||||||
|
|
||||||
if (!airDateUtc) {
|
if (!airDateUtc) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.center} title="TBA">
|
<div className={styles.center}>
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.TBA}
|
name={icons.TBA}
|
||||||
|
title="TBA"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -84,9 +86,10 @@ function EpisodeStatus(props) {
|
||||||
|
|
||||||
if (!monitored) {
|
if (!monitored) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.center} title="Album is not monitored">
|
<div className={styles.center}>
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.UNMONITORED}
|
name={icons.UNMONITORED}
|
||||||
|
title="Album is not monitored"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -94,18 +97,20 @@ function EpisodeStatus(props) {
|
||||||
|
|
||||||
if (hasAired) {
|
if (hasAired) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.center} title="Track missing from disk">
|
<div className={styles.center}>
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.MISSING}
|
name={icons.MISSING}
|
||||||
|
title="Track missing from disk"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.center} title="Album has not aired">
|
<div className={styles.center}>
|
||||||
<Icon
|
<Icon
|
||||||
name={icons.NOT_AIRED}
|
name={icons.NOT_AIRED}
|
||||||
|
title="Album has not aired"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -39,12 +39,11 @@ class AlbumStudioRow extends Component {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TableRowCell className={styles.status}>
|
<TableRowCell className={styles.status}>
|
||||||
<span title={status === 'ended' ? 'Ended' : 'Continuing'}>
|
<Icon
|
||||||
<Icon
|
className={styles.statusIcon}
|
||||||
className={styles.statusIcon}
|
name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
|
||||||
name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
|
title={status === 'ended' ? 'Ended' : 'Continuing'}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.title}>
|
<TableRowCell className={styles.title}>
|
||||||
|
|
|
@ -580,7 +580,9 @@ class ArtistDetails extends Component {
|
||||||
<InteractiveImportModal
|
<InteractiveImportModal
|
||||||
isOpen={isInteractiveImportModalOpen}
|
isOpen={isInteractiveImportModalOpen}
|
||||||
folder={path}
|
folder={path}
|
||||||
|
allowArtistChange={false}
|
||||||
showFilterExistingFiles={true}
|
showFilterExistingFiles={true}
|
||||||
|
showImportMode={false}
|
||||||
onModalClose={this.onInteractiveImportModalClose}
|
onModalClose={this.onInteractiveImportModalClose}
|
||||||
/>
|
/>
|
||||||
</PageContentBodyConnector>
|
</PageContentBodyConnector>
|
||||||
|
|
|
@ -150,13 +150,12 @@ class ArtistDetailsSeason extends Component {
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span title={isExpanded ? 'Hide albums' : 'Show albums'}>
|
<Icon
|
||||||
<Icon
|
className={styles.expandButtonIcon}
|
||||||
className={styles.expandButtonIcon}
|
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
|
||||||
name={isExpanded ? icons.COLLAPSE : icons.EXPAND}
|
title={isExpanded ? 'Hide albums' : 'Show albums'}
|
||||||
size={24}
|
size={24}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
|
|
||||||
{
|
{
|
||||||
!isSmallScreen &&
|
!isSmallScreen &&
|
||||||
|
|
|
@ -19,19 +19,17 @@ function ArtistStatusCell(props) {
|
||||||
className={className}
|
className={className}
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
>
|
>
|
||||||
<span title={monitored ? 'Artist is monitored' : 'Artist is unmonitored'}>
|
<Icon
|
||||||
<Icon
|
className={styles.statusIcon}
|
||||||
className={styles.statusIcon}
|
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
||||||
name={monitored ? icons.MONITORED : icons.UNMONITORED}
|
title={monitored ? 'Artist is monitored' : 'Artist is unmonitored'}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
|
|
||||||
<span title={status === 'ended' ? 'Ended' : 'Continuing'}>
|
<Icon
|
||||||
<Icon
|
className={styles.statusIcon}
|
||||||
className={styles.statusIcon}
|
name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
|
||||||
name={status === 'ended' ? icons.ARTIST_ENDED : icons.ARTIST_CONTINUING}
|
title={status === 'ended' ? 'Ended' : 'Continuing'}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
</Component>
|
</Component>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -107,11 +107,10 @@ class AgendaEvent extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
!queueItem && grabbed &&
|
!queueItem && grabbed &&
|
||||||
<span title="Album is downloading">
|
<Icon
|
||||||
<Icon
|
name={icons.DOWNLOADING}
|
||||||
name={icons.DOWNLOADING}
|
title="Album is downloading"
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,15 +4,14 @@ import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
|
||||||
import CalendarDay from './CalendarDay';
|
import CalendarDay from './CalendarDay';
|
||||||
|
|
||||||
function createCalendarEventsConnector() {
|
function createCalendarEventsConnector() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { date }) => date,
|
(state, { date }) => date,
|
||||||
createClientSideCollectionSelector('calendar'),
|
(state) => state.calendar.items,
|
||||||
(date, calendar) => {
|
(date, items) => {
|
||||||
const filtered = _.filter(calendar.items, (item) => {
|
const filtered = _.filter(items, (item) => {
|
||||||
return moment(date).isSame(moment(item.releaseDate), 'day');
|
return moment(date).isSame(moment(item.releaseDate), 'day');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -91,12 +91,11 @@ class CalendarEvent extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
!queueItem && grabbed &&
|
!queueItem && grabbed &&
|
||||||
<span title="Album is downloading">
|
<Icon
|
||||||
<Icon
|
className={styles.statusIcon}
|
||||||
className={styles.statusIcon}
|
name={icons.DOWNLOADING}
|
||||||
name={icons.DOWNLOADING}
|
title="Album is downloading"
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ import styles from './Icon.css';
|
||||||
|
|
||||||
function Icon(props) {
|
function Icon(props) {
|
||||||
const {
|
const {
|
||||||
|
containerClassName,
|
||||||
className,
|
className,
|
||||||
name,
|
name,
|
||||||
kind,
|
kind,
|
||||||
|
@ -16,11 +17,7 @@ function Icon(props) {
|
||||||
...otherProps
|
...otherProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
if (title && !window.Lidarr.isProduction) {
|
const icon = (
|
||||||
console.error('Icons cannot have a title');
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
|
@ -34,9 +31,23 @@ function Icon(props) {
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={containerClassName}
|
||||||
|
title={title}
|
||||||
|
>
|
||||||
|
{icon}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
Icon.propTypes = {
|
Icon.propTypes = {
|
||||||
|
containerClassName: PropTypes.string,
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
name: PropTypes.object.isRequired,
|
name: PropTypes.object.isRequired,
|
||||||
kind: PropTypes.string.isRequired,
|
kind: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -45,9 +45,10 @@ function Message(props) {
|
||||||
styles[type]
|
styles[type]
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className={styles.iconContainer} title={name}>
|
<div className={styles.iconContainer}>
|
||||||
<Icon
|
<Icon
|
||||||
name={getIconName(name)}
|
name={getIconName(name)}
|
||||||
|
title={name}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@
|
||||||
|
|
||||||
.filterText {
|
.filterText {
|
||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
font-size: $largeFontSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
|
|
@ -169,7 +169,9 @@ class InteractiveImportModalContent extends Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
downloadId,
|
downloadId,
|
||||||
|
allowArtistChange,
|
||||||
showFilterExistingFiles,
|
showFilterExistingFiles,
|
||||||
|
showImportMode,
|
||||||
filterExistingFiles,
|
filterExistingFiles,
|
||||||
title,
|
title,
|
||||||
folder,
|
folder,
|
||||||
|
@ -211,17 +213,7 @@ class InteractiveImportModalContent extends Component {
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
{
|
{
|
||||||
isFetching &&
|
showFilterExistingFiles &&
|
||||||
<LoadingIndicator />
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
error &&
|
|
||||||
<div>{errorMessage}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
isPopulated && showFilterExistingFiles && !isFetching &&
|
|
||||||
<div className={styles.filterContainer}>
|
<div className={styles.filterContainer}>
|
||||||
<Menu alignMenu={align.RIGHT}>
|
<Menu alignMenu={align.RIGHT}>
|
||||||
<MenuButton>
|
<MenuButton>
|
||||||
|
@ -258,6 +250,16 @@ class InteractiveImportModalContent extends Component {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
isFetching &&
|
||||||
|
<LoadingIndicator />
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
error &&
|
||||||
|
<div>{errorMessage}</div>
|
||||||
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
isPopulated && !!items.length && !isFetching && !isFetching &&
|
isPopulated && !!items.length && !isFetching && !isFetching &&
|
||||||
<Table
|
<Table
|
||||||
|
@ -278,6 +280,7 @@ class InteractiveImportModalContent extends Component {
|
||||||
key={item.id}
|
key={item.id}
|
||||||
isSelected={selectedState[item.id]}
|
isSelected={selectedState[item.id]}
|
||||||
{...item}
|
{...item}
|
||||||
|
allowArtistChange={allowArtistChange}
|
||||||
onSelectedChange={this.onSelectedChange}
|
onSelectedChange={this.onSelectedChange}
|
||||||
onValidRowChange={this.onValidRowChange}
|
onValidRowChange={this.onValidRowChange}
|
||||||
/>
|
/>
|
||||||
|
@ -295,9 +298,9 @@ class InteractiveImportModalContent extends Component {
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter className={styles.footer}>
|
<ModalFooter className={styles.footer}>
|
||||||
{
|
<div className={styles.leftButtons}>
|
||||||
!downloadId &&
|
{
|
||||||
<div className={styles.leftButtons}>
|
!downloadId && showImportMode &&
|
||||||
<SelectInput
|
<SelectInput
|
||||||
className={styles.importMode}
|
className={styles.importMode}
|
||||||
name="importMode"
|
name="importMode"
|
||||||
|
@ -305,13 +308,16 @@ class InteractiveImportModalContent extends Component {
|
||||||
values={importModeOptions}
|
values={importModeOptions}
|
||||||
onChange={this.onImportModeChange}
|
onChange={this.onImportModeChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
}
|
</div>
|
||||||
|
|
||||||
<div className={downloadId ? styles.leftButtons : styles.centerButtons}>
|
<div className={styles.centerButtons}>
|
||||||
<Button onPress={this.onSelectArtistPress}>
|
{
|
||||||
Select Artist
|
allowArtistChange &&
|
||||||
</Button>
|
<Button onPress={this.onSelectArtistPress}>
|
||||||
|
Select Artist
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
|
||||||
<Button onPress={this.onSelectAlbumPress}>
|
<Button onPress={this.onSelectAlbumPress}>
|
||||||
Select Album
|
Select Album
|
||||||
|
@ -357,6 +363,8 @@ class InteractiveImportModalContent extends Component {
|
||||||
|
|
||||||
InteractiveImportModalContent.propTypes = {
|
InteractiveImportModalContent.propTypes = {
|
||||||
downloadId: PropTypes.string,
|
downloadId: PropTypes.string,
|
||||||
|
allowArtistChange: PropTypes.bool.isRequired,
|
||||||
|
showImportMode: PropTypes.bool.isRequired,
|
||||||
showFilterExistingFiles: PropTypes.bool.isRequired,
|
showFilterExistingFiles: PropTypes.bool.isRequired,
|
||||||
filterExistingFiles: PropTypes.bool.isRequired,
|
filterExistingFiles: PropTypes.bool.isRequired,
|
||||||
importMode: PropTypes.string.isRequired,
|
importMode: PropTypes.string.isRequired,
|
||||||
|
@ -377,7 +385,9 @@ InteractiveImportModalContent.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
InteractiveImportModalContent.defaultProps = {
|
InteractiveImportModalContent.defaultProps = {
|
||||||
|
allowArtistChange: true,
|
||||||
showFilterExistingFiles: false,
|
showFilterExistingFiles: false,
|
||||||
|
showImportMode: true,
|
||||||
importMode: 'move'
|
importMode: 'move'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -163,6 +163,7 @@ class InteractiveImportRow extends Component {
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
|
allowArtistChange,
|
||||||
relativePath,
|
relativePath,
|
||||||
artist,
|
artist,
|
||||||
album,
|
album,
|
||||||
|
@ -210,6 +211,7 @@ class InteractiveImportRow extends Component {
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCellButton
|
<TableRowCellButton
|
||||||
|
isDisabled={!allowArtistChange}
|
||||||
onPress={this.onSelectArtistPress}
|
onPress={this.onSelectArtistPress}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
|
@ -348,6 +350,7 @@ class InteractiveImportRow extends Component {
|
||||||
|
|
||||||
InteractiveImportRow.propTypes = {
|
InteractiveImportRow.propTypes = {
|
||||||
id: PropTypes.number.isRequired,
|
id: PropTypes.number.isRequired,
|
||||||
|
allowArtistChange: PropTypes.bool.isRequired,
|
||||||
relativePath: PropTypes.string.isRequired,
|
relativePath: PropTypes.string.isRequired,
|
||||||
artist: PropTypes.object,
|
artist: PropTypes.object,
|
||||||
album: PropTypes.object,
|
album: PropTypes.object,
|
||||||
|
|
|
@ -92,9 +92,10 @@ class QualityProfileItem extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
connectDragSource(
|
connectDragSource(
|
||||||
<div className={styles.dragHandle} title="Create group">
|
<div className={styles.dragHandle}>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.dragIcon}
|
className={styles.dragIcon}
|
||||||
|
title="Create group"
|
||||||
name={icons.REORDER}
|
name={icons.REORDER}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -129,10 +129,11 @@ class QualityProfileItemGroup extends Component {
|
||||||
|
|
||||||
{
|
{
|
||||||
connectDragSource(
|
connectDragSource(
|
||||||
<div className={styles.dragHandle} title="Reorder">
|
<div className={styles.dragHandle}>
|
||||||
<Icon
|
<Icon
|
||||||
className={styles.dragIcon}
|
className={styles.dragIcon}
|
||||||
name={icons.REORDER}
|
name={icons.REORDER}
|
||||||
|
title="Reorder"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
@ -45,7 +45,7 @@ export const defaultState = {
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
key: 'monitored',
|
key: 'monitored',
|
||||||
value: false || true,
|
value: false,
|
||||||
type: filterTypes.EQUAL
|
type: filterTypes.EQUAL
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -66,7 +66,8 @@ export const defaultState = {
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
'calendar.view',
|
'calendar.view',
|
||||||
'calendar.selectedFilterKey'
|
'calendar.selectedFilterKey',
|
||||||
|
'calendar.showUpcoming'
|
||||||
];
|
];
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -57,7 +57,7 @@ function showCommandMessage(payload, dispatch) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
manual,
|
trigger,
|
||||||
message,
|
message,
|
||||||
body = {},
|
body = {},
|
||||||
state
|
state
|
||||||
|
@ -80,7 +80,7 @@ function showCommandMessage(payload, dispatch) {
|
||||||
hideAfter = 4;
|
hideAfter = 4;
|
||||||
} else if (state === 'failed') {
|
} else if (state === 'failed') {
|
||||||
type = messageTypes.ERROR;
|
type = messageTypes.ERROR;
|
||||||
hideAfter = manual ? 10 : 4;
|
hideAfter = trigger === 'manual' ? 10 : 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(showMessage({
|
dispatch(showMessage({
|
||||||
|
@ -95,10 +95,11 @@ function showCommandMessage(payload, dispatch) {
|
||||||
function scheduleRemoveCommand(command, dispatch) {
|
function scheduleRemoveCommand(command, dispatch) {
|
||||||
const {
|
const {
|
||||||
id,
|
id,
|
||||||
state
|
status,
|
||||||
|
body
|
||||||
} = command;
|
} = command;
|
||||||
|
|
||||||
if (state === 'queued') {
|
if (status === 'queued') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +109,12 @@ function scheduleRemoveCommand(command, dispatch) {
|
||||||
clearTimeout(timeoutId);
|
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(() => {
|
removeCommandTimeoutIds[id] = setTimeout(() => {
|
||||||
dispatch(batchActions([
|
dispatch(batchActions([
|
||||||
removeCommand({ section: 'commands', id }),
|
removeCommand({ section: 'commands', id }),
|
||||||
|
@ -115,7 +122,7 @@ function scheduleRemoveCommand(command, dispatch) {
|
||||||
]));
|
]));
|
||||||
|
|
||||||
delete removeCommandTimeoutIds[id];
|
delete removeCommandTimeoutIds[id];
|
||||||
}, 30000);
|
}, timeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
@ -87,11 +87,10 @@ class BackupRow extends Component {
|
||||||
<TableRow key={id}>
|
<TableRow key={id}>
|
||||||
<TableRowCell className={styles.type}>
|
<TableRowCell className={styles.type}>
|
||||||
{
|
{
|
||||||
<span title={iconTooltip}>
|
<Icon
|
||||||
<Icon
|
name={iconClassName}
|
||||||
name={iconClassName}
|
title={iconTooltip}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
}
|
}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
|
|
|
@ -125,12 +125,11 @@ class Health extends Component {
|
||||||
return (
|
return (
|
||||||
<TableRow key={`health${item.message}`}>
|
<TableRow key={`health${item.message}`}>
|
||||||
<TableRowCell>
|
<TableRowCell>
|
||||||
<span title={titleCase(item.type)}>
|
<Icon
|
||||||
<Icon
|
name={icons.DANGER}
|
||||||
name={icons.DANGER}
|
kind={item.type.toLowerCase() === 'error' ? kinds.DANGER : kinds.WARNING}
|
||||||
kind={item.type.toLowerCase() === 'error' ? kinds.DANGER : kinds.WARNING}
|
title={titleCase(item.type)}
|
||||||
/>
|
/>
|
||||||
</span>
|
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell>{item.message}</TableRowCell>
|
<TableRowCell>{item.message}</TableRowCell>
|
||||||
|
|
|
@ -105,7 +105,6 @@ class CutoffUnmet extends Component {
|
||||||
filters,
|
filters,
|
||||||
columns,
|
columns,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isSearchingForAlbums,
|
|
||||||
isSearchingForCutoffUnmetAlbums,
|
isSearchingForCutoffUnmetAlbums,
|
||||||
isSaving,
|
isSaving,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
|
@ -129,8 +128,7 @@ class CutoffUnmet extends Component {
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Search Selected"
|
label="Search Selected"
|
||||||
iconName={icons.SEARCH}
|
iconName={icons.SEARCH}
|
||||||
isDisabled={!itemsSelected}
|
isDisabled={!itemsSelected || isSearchingForCutoffUnmetAlbums}
|
||||||
isSpinning={isSearchingForAlbums}
|
|
||||||
onPress={this.onSearchSelectedPress}
|
onPress={this.onSearchSelectedPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -255,7 +253,6 @@ CutoffUnmet.propTypes = {
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
isSearchingForAlbums: PropTypes.bool.isRequired,
|
|
||||||
isSearchingForCutoffUnmetAlbums: PropTypes.bool.isRequired,
|
isSearchingForCutoffUnmetAlbums: PropTypes.bool.isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -20,11 +20,9 @@ function createMapStateToProps() {
|
||||||
(state) => state.wanted.cutoffUnmet,
|
(state) => state.wanted.cutoffUnmet,
|
||||||
createCommandsSelector(),
|
createCommandsSelector(),
|
||||||
(cutoffUnmet, commands) => {
|
(cutoffUnmet, commands) => {
|
||||||
const isSearchingForAlbums = _.some(commands, { name: commandNames.ALBUM_SEARCH });
|
|
||||||
const isSearchingForCutoffUnmetAlbums = _.some(commands, { name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH });
|
const isSearchingForCutoffUnmetAlbums = _.some(commands, { name: commandNames.CUTOFF_UNMET_ALBUM_SEARCH });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSearchingForAlbums,
|
|
||||||
isSearchingForCutoffUnmetAlbums,
|
isSearchingForCutoffUnmetAlbums,
|
||||||
isSaving: _.some(cutoffUnmet.items, { isSaving: true }),
|
isSaving: _.some(cutoffUnmet.items, { isSaving: true }),
|
||||||
...cutoffUnmet
|
...cutoffUnmet
|
||||||
|
|
|
@ -114,7 +114,6 @@ class Missing extends Component {
|
||||||
filters,
|
filters,
|
||||||
columns,
|
columns,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
isSearchingForAlbums,
|
|
||||||
isSearchingForMissingAlbums,
|
isSearchingForMissingAlbums,
|
||||||
isSaving,
|
isSaving,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
|
@ -139,8 +138,7 @@ class Missing extends Component {
|
||||||
<PageToolbarButton
|
<PageToolbarButton
|
||||||
label="Search Selected"
|
label="Search Selected"
|
||||||
iconName={icons.SEARCH}
|
iconName={icons.SEARCH}
|
||||||
isDisabled={!itemsSelected}
|
isDisabled={!itemsSelected || isSearchingForMissingAlbums}
|
||||||
isSpinning={isSearchingForAlbums}
|
|
||||||
onPress={this.onSearchSelectedPress}
|
onPress={this.onSearchSelectedPress}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -277,7 +275,6 @@ Missing.propTypes = {
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
isSearchingForAlbums: PropTypes.bool.isRequired,
|
|
||||||
isSearchingForMissingAlbums: PropTypes.bool.isRequired,
|
isSearchingForMissingAlbums: PropTypes.bool.isRequired,
|
||||||
isSaving: PropTypes.bool.isRequired,
|
isSaving: PropTypes.bool.isRequired,
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
|
|
|
@ -19,11 +19,9 @@ function createMapStateToProps() {
|
||||||
(state) => state.wanted.missing,
|
(state) => state.wanted.missing,
|
||||||
createCommandsSelector(),
|
createCommandsSelector(),
|
||||||
(missing, commands) => {
|
(missing, commands) => {
|
||||||
const isSearchingForAlbums = _.some(commands, { name: commandNames.ALBUM_SEARCH });
|
|
||||||
const isSearchingForMissingAlbums = _.some(commands, { name: commandNames.MISSING_ALBUM_SEARCH });
|
const isSearchingForMissingAlbums = _.some(commands, { name: commandNames.MISSING_ALBUM_SEARCH });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isSearchingForAlbums,
|
|
||||||
isSearchingForMissingAlbums,
|
isSearchingForMissingAlbums,
|
||||||
isSaving: _.some(missing.items, { isSaving: true }),
|
isSaving: _.some(missing.items, { isSaving: true }),
|
||||||
...missing
|
...missing
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
@ -24,37 +24,6 @@ namespace Lidarr.Api.V1.Commands
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public string CompletionMessage { get; set; }
|
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
|
public DateTime? StateChangeTime
|
||||||
{
|
{
|
||||||
get
|
get
|
||||||
|
|
|
@ -15,7 +15,7 @@ namespace NzbDrone.Core.Test.Messaging.Commands
|
||||||
[TestFixture]
|
[TestFixture]
|
||||||
public class CommandExecutorFixture : TestBase<CommandExecutor>
|
public class CommandExecutorFixture : TestBase<CommandExecutor>
|
||||||
{
|
{
|
||||||
private BlockingCollection<CommandModel> _commandQueue;
|
private CommandQueue _commandQueue;
|
||||||
private Mock<IExecute<CommandA>> _executorA;
|
private Mock<IExecute<CommandA>> _executorA;
|
||||||
private Mock<IExecute<CommandB>> _executorB;
|
private Mock<IExecute<CommandB>> _executorB;
|
||||||
private bool _commandExecuted = false;
|
private bool _commandExecuted = false;
|
||||||
|
@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.Messaging.Commands
|
||||||
|
|
||||||
private void GivenCommandQueue()
|
private void GivenCommandQueue()
|
||||||
{
|
{
|
||||||
_commandQueue = new BlockingCollection<CommandModel>(new CommandQueue());
|
_commandQueue = new CommandQueue();
|
||||||
|
|
||||||
Mocker.GetMock<IManageCommandQueue>()
|
Mocker.GetMock<IManageCommandQueue>()
|
||||||
.Setup(s => s.Queue(It.IsAny<CancellationToken>()))
|
.Setup(s => s.Queue(It.IsAny<CancellationToken>()))
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Collections.Generic;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using FluentAssertions;
|
using FluentAssertions;
|
||||||
using Moq;
|
using Moq;
|
||||||
|
@ -42,6 +43,10 @@ namespace NzbDrone.Core.Test.Messaging.Commands
|
||||||
{
|
{
|
||||||
var command = Subject.Push<CheckForFinishedDownloadCommand>(new CheckForFinishedDownloadCommand());
|
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.Start(command);
|
||||||
Subject.Complete(command, "All done");
|
Subject.Complete(command, "All done");
|
||||||
Subject.CleanCommands();
|
Subject.CleanCommands();
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Download
|
namespace NzbDrone.Core.Download
|
||||||
{
|
{
|
||||||
public class CheckForFinishedDownloadCommand : Command
|
public class CheckForFinishedDownloadCommand : Command
|
||||||
{
|
{
|
||||||
|
public override bool RequiresDiskAccess => true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ namespace NzbDrone.Core.MediaFiles.Commands
|
||||||
public List<int> ArtistIds { get; set; }
|
public List<int> ArtistIds { get; set; }
|
||||||
|
|
||||||
public override bool SendUpdatesToClient => true;
|
public override bool SendUpdatesToClient => true;
|
||||||
|
public override bool RequiresDiskAccess => true;
|
||||||
|
|
||||||
public RenameArtistCommand()
|
public RenameArtistCommand()
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using NzbDrone.Core.Messaging.Commands;
|
using NzbDrone.Core.Messaging.Commands;
|
||||||
|
|
||||||
namespace NzbDrone.Core.MediaFiles.Commands
|
namespace NzbDrone.Core.MediaFiles.Commands
|
||||||
|
@ -9,6 +9,7 @@ namespace NzbDrone.Core.MediaFiles.Commands
|
||||||
public List<int> Files { get; set; }
|
public List<int> Files { get; set; }
|
||||||
|
|
||||||
public override bool SendUpdatesToClient => true;
|
public override bool SendUpdatesToClient => true;
|
||||||
|
public override bool RequiresDiskAccess => true;
|
||||||
|
|
||||||
public RenameFilesCommand()
|
public RenameFilesCommand()
|
||||||
{
|
{
|
||||||
|
@ -20,4 +21,4 @@ namespace NzbDrone.Core.MediaFiles.Commands
|
||||||
Files = files;
|
Files = files;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ namespace NzbDrone.Core.MediaFiles.TrackImport.Manual
|
||||||
public List<ManualImportFile> Files { get; set; }
|
public List<ManualImportFile> Files { get; set; }
|
||||||
|
|
||||||
public override bool SendUpdatesToClient => true;
|
public override bool SendUpdatesToClient => true;
|
||||||
|
public override bool RequiresDiskAccess => true;
|
||||||
|
|
||||||
public ImportMode ImportMode { get; set; }
|
public ImportMode ImportMode { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,8 +20,9 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual bool UpdateScheduledTask => true;
|
public virtual bool UpdateScheduledTask => true;
|
||||||
|
|
||||||
public virtual string CompletionMessage => "Completed";
|
public virtual string CompletionMessage => "Completed";
|
||||||
|
public virtual bool RequiresDiskAccess => false;
|
||||||
|
public virtual bool IsExclusive => false;
|
||||||
|
|
||||||
public string Name { get; private set; }
|
public string Name { get; private set; }
|
||||||
public DateTime? LastExecutionTime { get; set; }
|
public DateTime? LastExecutionTime { get; set; }
|
||||||
|
|
|
@ -1,27 +1,36 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections;
|
using System.Collections;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace NzbDrone.Core.Messaging.Commands
|
namespace NzbDrone.Core.Messaging.Commands
|
||||||
{
|
{
|
||||||
public class CommandQueue : IProducerConsumerCollection<CommandModel>
|
public class CommandQueue : IEnumerable
|
||||||
{
|
{
|
||||||
private object Mutex = new object();
|
private readonly object _mutex = new object();
|
||||||
|
private readonly List<CommandModel> _items;
|
||||||
private List<CommandModel> _items;
|
|
||||||
|
|
||||||
public CommandQueue()
|
public CommandQueue()
|
||||||
{
|
{
|
||||||
_items = new List<CommandModel>();
|
_items = new List<CommandModel>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int Count => _items.Count;
|
||||||
|
|
||||||
|
public void Add(CommandModel item)
|
||||||
|
{
|
||||||
|
lock (_mutex)
|
||||||
|
{
|
||||||
|
_items.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerator<CommandModel> GetEnumerator()
|
public IEnumerator<CommandModel> GetEnumerator()
|
||||||
{
|
{
|
||||||
List<CommandModel> copy = null;
|
List<CommandModel> copy = null;
|
||||||
|
|
||||||
lock (Mutex)
|
lock (_mutex)
|
||||||
{
|
{
|
||||||
copy = new List<CommandModel>(_items);
|
copy = new List<CommandModel>(_items);
|
||||||
}
|
}
|
||||||
|
@ -34,77 +43,123 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
return GetEnumerator();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rval;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CommandModel Find(int id)
|
||||||
|
{
|
||||||
|
return All().FirstOrDefault(q => q.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveMany(IEnumerable<CommandModel> commands)
|
||||||
|
{
|
||||||
|
lock (_mutex)
|
||||||
|
{
|
||||||
|
foreach (var command in commands)
|
||||||
|
{
|
||||||
|
_items.Remove(command);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Count => _items.Count;
|
public List<CommandModel> QueuedOrStarted()
|
||||||
|
|
||||||
public object SyncRoot => Mutex;
|
|
||||||
|
|
||||||
public bool IsSynchronized => true;
|
|
||||||
|
|
||||||
public void CopyTo(CommandModel[] array, int index)
|
|
||||||
{
|
{
|
||||||
lock (Mutex)
|
return All().Where(q => q.Status == CommandStatus.Queued || q.Status == CommandStatus.Started)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<CommandModel> GetConsumingEnumerable()
|
||||||
|
{
|
||||||
|
return GetConsumingEnumerable(CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<CommandModel> GetConsumingEnumerable(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_items.CopyTo(array, index);
|
if (TryGet(out var command))
|
||||||
|
{
|
||||||
|
yield return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryAdd(CommandModel item)
|
public bool TryGet(out CommandModel item)
|
||||||
{
|
{
|
||||||
Add(item);
|
var rval = true;
|
||||||
return true;
|
item = default(CommandModel);
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryTake(out CommandModel item)
|
lock (_mutex)
|
||||||
{
|
|
||||||
bool rval = true;
|
|
||||||
lock (Mutex)
|
|
||||||
{
|
{
|
||||||
if (_items.Count == 0)
|
if (_items.Count == 0)
|
||||||
{
|
{
|
||||||
item = default(CommandModel);
|
|
||||||
rval = false;
|
rval = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
item = _items.Where(c => c.Status == CommandStatus.Queued)
|
var startedCommands = _items.Where(c => c.Status == CommandStatus.Started)
|
||||||
.OrderByDescending(c => c.Priority)
|
.ToList();
|
||||||
.ThenBy(c => c.QueuedAt)
|
|
||||||
.First();
|
|
||||||
|
|
||||||
_items.Remove(item);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return rval;
|
return rval;
|
||||||
}
|
|
||||||
|
|
||||||
public CommandModel[] ToArray()
|
|
||||||
{
|
|
||||||
CommandModel[] rval = null;
|
|
||||||
|
|
||||||
lock (Mutex)
|
|
||||||
{
|
|
||||||
rval = _items.ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
return rval;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Add(CommandModel item)
|
|
||||||
{
|
|
||||||
lock (Mutex)
|
|
||||||
{
|
|
||||||
_items.Add(item);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using NLog;
|
using NLog;
|
||||||
using NzbDrone.Common;
|
using NzbDrone.Common;
|
||||||
using NzbDrone.Common.Cache;
|
|
||||||
using NzbDrone.Common.EnsureThat;
|
using NzbDrone.Common.EnsureThat;
|
||||||
using NzbDrone.Common.Serializer;
|
using NzbDrone.Common.Serializer;
|
||||||
using NzbDrone.Core.Lifecycle;
|
using NzbDrone.Core.Lifecycle;
|
||||||
|
@ -35,20 +33,17 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
private readonly IServiceFactory _serviceFactory;
|
private readonly IServiceFactory _serviceFactory;
|
||||||
private readonly Logger _logger;
|
private readonly Logger _logger;
|
||||||
|
|
||||||
private readonly ICached<CommandModel> _commandCache;
|
private readonly CommandQueue _commandQueue;
|
||||||
private readonly BlockingCollection<CommandModel> _commandQueue;
|
|
||||||
|
|
||||||
public CommandQueueManager(ICommandRepository repo,
|
public CommandQueueManager(ICommandRepository repo,
|
||||||
IServiceFactory serviceFactory,
|
IServiceFactory serviceFactory,
|
||||||
ICacheManager cacheManager,
|
|
||||||
Logger logger)
|
Logger logger)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_serviceFactory = serviceFactory;
|
_serviceFactory = serviceFactory;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
_commandCache = cacheManager.GetCache<CommandModel>(GetType());
|
_commandQueue = new CommandQueue();
|
||||||
_commandQueue = new BlockingCollection<CommandModel>(new CommandQueue());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<CommandModel> PushMany<TCommand>(List<TCommand> commands) where TCommand : Command
|
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);
|
_logger.Trace("Publishing {0} commands", commands.Count);
|
||||||
|
|
||||||
var commandModels = new List<CommandModel>();
|
var commandModels = new List<CommandModel>();
|
||||||
var existingCommands = _commandCache.Values.Where(q => q.Status == CommandStatus.Queued ||
|
var existingCommands = _commandQueue.QueuedOrStarted();
|
||||||
q.Status == CommandStatus.Started).ToList();
|
|
||||||
|
|
||||||
foreach (var command in commands)
|
foreach (var command in commands)
|
||||||
{
|
{
|
||||||
|
@ -86,7 +80,6 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
|
|
||||||
foreach (var commandModel in commandModels)
|
foreach (var commandModel in commandModels)
|
||||||
{
|
{
|
||||||
_commandCache.Set(commandModel.Id.ToString(), commandModel);
|
|
||||||
_commandQueue.Add(commandModel);
|
_commandQueue.Add(commandModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,7 +117,6 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
_logger.Trace("Inserting new command: {0}", commandModel.Name);
|
_logger.Trace("Inserting new command: {0}", commandModel.Name);
|
||||||
|
|
||||||
_repo.Insert(commandModel);
|
_repo.Insert(commandModel);
|
||||||
_commandCache.Set(commandModel.Id.ToString(), commandModel);
|
|
||||||
_commandQueue.Add(commandModel);
|
_commandQueue.Add(commandModel);
|
||||||
|
|
||||||
return commandModel;
|
return commandModel;
|
||||||
|
@ -146,28 +138,31 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
|
|
||||||
public CommandModel Get(int id)
|
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()
|
public List<CommandModel> GetStarted()
|
||||||
{
|
{
|
||||||
_logger.Trace("Getting started commands");
|
_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)
|
public void SetMessage(CommandModel command, string message)
|
||||||
{
|
{
|
||||||
command.Message = message;
|
command.Message = message;
|
||||||
_commandCache.Set(command.Id.ToString(), command);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start(CommandModel command)
|
public void Start(CommandModel command)
|
||||||
{
|
{
|
||||||
command.StartedAt = DateTime.UtcNow;
|
// Marks the command as started in the DB, the queue takes care of marking it as started on it's own
|
||||||
command.Status = CommandStatus.Started;
|
|
||||||
|
|
||||||
_logger.Trace("Marking command as started: {0}", command.Name);
|
_logger.Trace("Marking command as started: {0}", command.Name);
|
||||||
_commandCache.Set(command.Id.ToString(), command);
|
|
||||||
_repo.Start(command);
|
_repo.Start(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,12 +190,11 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
{
|
{
|
||||||
_logger.Trace("Cleaning up old 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)
|
_commandQueue.RemoveMany(commands);
|
||||||
{
|
|
||||||
_commandCache.Remove(command.Id.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
_repo.Trim();
|
_repo.Trim();
|
||||||
}
|
}
|
||||||
|
@ -215,18 +209,6 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
return Json.Deserialize("{}", commandType);
|
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)
|
private void Update(CommandModel command, CommandStatus status, string message)
|
||||||
{
|
{
|
||||||
SetMessage(command, message);
|
SetMessage(command, message);
|
||||||
|
@ -236,15 +218,14 @@ namespace NzbDrone.Core.Messaging.Commands
|
||||||
command.Status = status;
|
command.Status = status;
|
||||||
|
|
||||||
_logger.Trace("Updating command status");
|
_logger.Trace("Updating command status");
|
||||||
_commandCache.Set(command.Id.ToString(), command);
|
|
||||||
_repo.End(command);
|
_repo.End(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<CommandModel> QueuedOrStarted(string name)
|
private List<CommandModel> QueuedOrStarted(string name)
|
||||||
{
|
{
|
||||||
return _commandCache.Values.Where(q => q.Name == name &&
|
return _commandQueue.QueuedOrStarted()
|
||||||
(q.Status == CommandStatus.Queued ||
|
.Where(q => q.Name == name)
|
||||||
q.Status == CommandStatus.Started)).ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Handle(ApplicationStartedEvent message)
|
public void Handle(ApplicationStartedEvent message)
|
||||||
|
|
|
@ -10,6 +10,7 @@ namespace NzbDrone.Core.Music.Commands
|
||||||
public string DestinationRootFolder { get; set; }
|
public string DestinationRootFolder { get; set; }
|
||||||
|
|
||||||
public override bool SendUpdatesToClient => true;
|
public override bool SendUpdatesToClient => true;
|
||||||
|
public override bool RequiresDiskAccess => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class BulkMoveArtist : IEquatable<BulkMoveArtist>
|
public class BulkMoveArtist : IEquatable<BulkMoveArtist>
|
||||||
|
|
|
@ -9,5 +9,6 @@ namespace NzbDrone.Core.Music.Commands
|
||||||
public string DestinationPath { get; set; }
|
public string DestinationPath { get; set; }
|
||||||
|
|
||||||
public override bool SendUpdatesToClient => true;
|
public override bool SendUpdatesToClient => true;
|
||||||
|
public override bool RequiresDiskAccess => true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ namespace NzbDrone.Core.Music
|
||||||
_logger = logger;
|
_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))
|
if (!_diskProvider.FolderExists(sourcePath))
|
||||||
{
|
{
|
||||||
|
@ -42,7 +42,14 @@ namespace NzbDrone.Core.Music
|
||||||
return;
|
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
|
try
|
||||||
{
|
{
|
||||||
|
@ -81,12 +88,13 @@ namespace NzbDrone.Core.Music
|
||||||
|
|
||||||
_logger.ProgressInfo("Moving {0} artist to '{1}'", artistToMove.Count, destinationRootFolder);
|
_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 artist = _artistService.GetArtist(s.ArtistId);
|
||||||
var destinationPath = Path.Combine(destinationRootFolder, _filenameBuilder.GetArtistFolder(artist));
|
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);
|
_logger.ProgressInfo("Finished moving {0} artist to '{1}'", artistToMove.Count, destinationRootFolder);
|
||||||
|
|
|
@ -5,6 +5,7 @@ namespace NzbDrone.Core.Update.Commands
|
||||||
public class ApplicationUpdateCommand : Command
|
public class ApplicationUpdateCommand : Command
|
||||||
{
|
{
|
||||||
public override bool SendUpdatesToClient => true;
|
public override bool SendUpdatesToClient => true;
|
||||||
|
public override bool IsExclusive => true;
|
||||||
|
|
||||||
public override string CompletionMessage => null;
|
public override string CompletionMessage => null;
|
||||||
}
|
}
|
||||||
|
|
1
test.sh
1
test.sh
|
@ -1,3 +1,4 @@
|
||||||
|
#! /bin/bash
|
||||||
PLATFORM=$1
|
PLATFORM=$1
|
||||||
TYPE=$2
|
TYPE=$2
|
||||||
WHERE="cat != ManualTest"
|
WHERE="cat != ManualTest"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue