mirror of
https://github.com/Radarr/Radarr.git
synced 2024-08-18 08:19:38 +02:00
New: Monitor and Process downloads separately
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
parent
f430d1aab2
commit
72caab1b2b
@ -232,6 +232,30 @@ function HistoryDetails(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'downloadIgnored') {
|
||||
const {
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Message"
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
|
@ -23,6 +23,8 @@ function getHeaderTitle(eventType) {
|
||||
return 'Movie File Deleted';
|
||||
case 'movieFileRenamed':
|
||||
return 'Movie File Renamed';
|
||||
case 'downloadIgnored':
|
||||
return 'Download Ignored';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
|
@ -19,6 +19,8 @@ function getIconName(eventType) {
|
||||
return icons.DELETE;
|
||||
case 'movieFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'downloadIgnored':
|
||||
return icons.IGNORE;
|
||||
default:
|
||||
return icons.UNKNOWN;
|
||||
}
|
||||
@ -47,6 +49,8 @@ function getTooltip(eventType, data) {
|
||||
return 'Movie file deleted';
|
||||
case 'movieFileRenamed':
|
||||
return 'Movie file renamed';
|
||||
case 'downloadIgnored':
|
||||
return 'Movie Download Ignored';
|
||||
default:
|
||||
return 'Unknown event';
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ class Queue extends Component {
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'Delay';
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||
});
|
||||
|
||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||
@ -87,8 +87,8 @@ class Queue extends Component {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveSelectedConfirmed = (blacklist) => {
|
||||
this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist);
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
@ -112,7 +112,7 @@ class Queue extends Component {
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
onRefreshPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@ -125,10 +125,11 @@ class Queue extends Component {
|
||||
isPendingSelected
|
||||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isMoviesFetching || isCheckForFinishedDownloadExecuting;
|
||||
const isRefreshing = isFetching || isMoviesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
const isAllPopulated = isPopulated && (isMoviesPopulated || !items.length || items.every((e) => !e.movieId));
|
||||
const hasError = error || moviesError;
|
||||
const selectedCount = this.getSelectedIds().length;
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const selectedCount = selectedIds.length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
return (
|
||||
@ -239,6 +240,13 @@ class Queue extends Component {
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.movieId);
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
@ -259,7 +267,7 @@ Queue.propTypes = {
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired,
|
||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||
|
@ -15,13 +15,13 @@ function createMapStateToProps() {
|
||||
(state) => state.movies,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
|
||||
(movies, options, queue, isCheckForFinishedDownloadExecuting) => {
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||
(movies, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
||||
return {
|
||||
isMoviesFetching: movies.isFetching,
|
||||
isMoviesPopulated: movies.isPopulated,
|
||||
moviesError: movies.error,
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
...options,
|
||||
...queue
|
||||
};
|
||||
@ -113,7 +113,7 @@ class QueueConnector extends Component {
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||
});
|
||||
}
|
||||
|
||||
@ -121,8 +121,8 @@ class QueueConnector extends Component {
|
||||
this.props.grabQueueItems({ ids });
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = (ids, blacklist) => {
|
||||
this.props.removeQueueItems({ ids, blacklist });
|
||||
onRemoveSelectedPress = (payload) => {
|
||||
this.props.removeQueueItems(payload);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -68,6 +68,7 @@ class QueueRow extends Component {
|
||||
title,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
movie,
|
||||
@ -100,8 +101,8 @@ class QueueRow extends Component {
|
||||
} = this.state;
|
||||
|
||||
const progress = 100 - (sizeleft / size * 100);
|
||||
const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning';
|
||||
const isPending = status === 'Delay' || status === 'DownloadClientUnavailable';
|
||||
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
||||
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
@ -129,6 +130,7 @@ class QueueRow extends Component {
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
@ -315,6 +317,7 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!(movie)}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
@ -330,6 +333,7 @@ QueueRow.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
trackedDownloadState: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
movie: PropTypes.object,
|
||||
|
@ -39,8 +39,8 @@ class QueueRowConnector extends Component {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
}
|
||||
|
||||
onRemoveQueueItemPress = (blacklist) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, blacklist });
|
||||
onRemoveQueueItemPress = (payload) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -37,13 +37,14 @@ function QueueStatusCell(props) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus = 'Ok',
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
const hasWarning = trackedDownloadStatus === 'Warning';
|
||||
const hasError = trackedDownloadStatus === 'Error';
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
const hasError = trackedDownloadStatus === 'error';
|
||||
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
@ -54,22 +55,34 @@ function QueueStatusCell(props) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'Paused') {
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = 'Paused';
|
||||
}
|
||||
|
||||
if (status === 'Queued') {
|
||||
if (status === 'queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = 'Queued';
|
||||
}
|
||||
|
||||
if (status === 'Completed') {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = 'Downloaded';
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ' - Waiting to Import';
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ' - Importing';
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ' - Waiting to Process';
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'Delay') {
|
||||
if (status === 'delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = 'Pending';
|
||||
}
|
||||
@ -80,20 +93,20 @@ function QueueStatusCell(props) {
|
||||
title = 'Pending - Download client is unavailable';
|
||||
}
|
||||
|
||||
if (status === 'Failed') {
|
||||
if (status === 'failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = 'Download failed';
|
||||
}
|
||||
|
||||
if (status === 'Warning') {
|
||||
if (status === 'warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'Completed') {
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = `Import failed: ${sourceTitle}`;
|
||||
@ -125,9 +138,15 @@ function QueueStatusCell(props) {
|
||||
QueueStatusCell.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
QueueStatusCell.defaultProps = {
|
||||
trackedDownloadStatus: 'Ok',
|
||||
trackedDownloadState: 'Downloading'
|
||||
};
|
||||
|
||||
export default QueueStatusCell;
|
||||
|
@ -1,4 +0,0 @@
|
||||
.messageRemove {
|
||||
margin-bottom: 30px;
|
||||
color: $dangerColor;
|
||||
}
|
@ -10,7 +10,6 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
@ -21,26 +20,41 @@ class RemoveQueueItemModal extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveQueueItemConfirmed = () => {
|
||||
const blacklist = this.state.blacklist;
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onRemovePress(blacklist);
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ blacklist: false });
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
@ -50,10 +64,11 @@ class RemoveQueueItemModal extends Component {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle
|
||||
sourceTitle,
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const blacklist = this.state.blacklist;
|
||||
const { remove, blacklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -73,9 +88,18 @@ class RemoveQueueItemModal extends Component {
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
<div className={styles.messageRemove}>
|
||||
Removing will remove the download and the file(s) from the download client.
|
||||
</div>
|
||||
<FormGroup>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
@ -83,7 +107,7 @@ class RemoveQueueItemModal extends Component {
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText="Prevents Radarr from automatically grabbing this movie again"
|
||||
helpText="Starts a search for this movie again and prevents this release from being grabbed again"
|
||||
onChange={this.onBlacklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@ -97,7 +121,7 @@ class RemoveQueueItemModal extends Component {
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveQueueItemConfirmed}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
@ -111,6 +135,7 @@ class RemoveQueueItemModal extends Component {
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
@ -21,26 +21,41 @@ class RemoveQueueItemsModal extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveQueueItemConfirmed = () => {
|
||||
const blacklist = this.state.blacklist;
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onRemovePress(blacklist);
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.setState({ blacklist: false });
|
||||
this.resetState();
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
@ -50,10 +65,11 @@ class RemoveQueueItemsModal extends Component {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount
|
||||
selectedCount,
|
||||
canIgnore
|
||||
} = this.props;
|
||||
|
||||
const blacklist = this.state.blacklist;
|
||||
const { remove, blacklist } = this.state;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -74,12 +90,28 @@ class RemoveQueueItemsModal extends Component {
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Blacklist Release{selectedCount > 1 ? 's' : ''}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText="Prevents Radarr from automatically grabbing this release again"
|
||||
helpText="Prevents Radarr from automatically grabbing this movie again"
|
||||
onChange={this.onBlacklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@ -93,7 +125,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveQueueItemConfirmed}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
@ -107,6 +139,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
export const APPLICATION_UPDATE = 'ApplicationUpdate';
|
||||
export const BACKUP = 'Backup';
|
||||
export const CHECK_FOR_FINISHED_DOWNLOAD = 'CheckForFinishedDownload';
|
||||
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
|
||||
export const CLEAR_BLACKLIST = 'ClearBlacklist';
|
||||
export const CLEAR_LOGS = 'ClearLog';
|
||||
export const CUTOFF_UNMET_MOVIES_SEARCH = 'CutoffUnmetMoviesSearch';
|
||||
|
@ -224,7 +224,7 @@ class SignalRConnector extends Component {
|
||||
}
|
||||
|
||||
handleSystemTask = () => {
|
||||
// No-op for now, we may want this later
|
||||
this.props.dispatchFetchCommands();
|
||||
}
|
||||
|
||||
handleRootfolder = () => {
|
||||
|
@ -153,6 +153,7 @@ export const HEALTH = fasMedkit;
|
||||
export const HEART = fasHeart;
|
||||
export const HISTORY = fasHistory;
|
||||
export const HOUSEKEEPING = fasHome;
|
||||
export const IGNORE = fasTimesCircle;
|
||||
export const IN_CINEMAS = fasTicketAlt;
|
||||
export const INFO = fasInfoCircle;
|
||||
export const INTERACTIVE = fasUser;
|
||||
|
@ -148,6 +148,17 @@ export const defaultState = {
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'ignored',
|
||||
label: 'Ignored',
|
||||
filters: [
|
||||
{
|
||||
key: 'eventType',
|
||||
value: '9',
|
||||
type: filterTypes.EQUAL
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -351,13 +351,14 @@ export const actionHandlers = handleThunks({
|
||||
[REMOVE_QUEUE_ITEM]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
id,
|
||||
remove,
|
||||
blacklist
|
||||
} = payload;
|
||||
|
||||
dispatch(updateItem({ section: paged, id, isRemoving: true }));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: `/queue/${id}?blacklist=${blacklist}`,
|
||||
url: `/queue/${id}?removeFromClient=${remove}&blacklist=${blacklist}`,
|
||||
method: 'DELETE'
|
||||
}).request;
|
||||
|
||||
@ -373,6 +374,7 @@ export const actionHandlers = handleThunks({
|
||||
[REMOVE_QUEUE_ITEMS]: function(getState, payload, dispatch) {
|
||||
const {
|
||||
ids,
|
||||
remove,
|
||||
blacklist
|
||||
} = payload;
|
||||
|
||||
@ -389,7 +391,7 @@ export const actionHandlers = handleThunks({
|
||||
]));
|
||||
|
||||
const promise = createAjaxRequest({
|
||||
url: `/queue/bulk?blacklist=${blacklist}`,
|
||||
url: `/queue/bulk?removeFromClient=${remove}&blacklist=${blacklist}`,
|
||||
method: 'DELETE',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify({ ids })
|
||||
|
@ -168,7 +168,7 @@ class QueuedTaskRow extends Component {
|
||||
isCancelConfirmModalOpen
|
||||
} = this.state;
|
||||
|
||||
let triggerIcon = icons.UNKNOWN;
|
||||
let triggerIcon = icons.QUICK;
|
||||
|
||||
if (trigger === 'manual') {
|
||||
triggerIcon = icons.INTERACTIVE;
|
||||
|
@ -86,12 +86,7 @@ private object Remove(int id)
|
||||
|
||||
private object Import()
|
||||
{
|
||||
var resource = Request.Body.FromJson<QueueResource>();
|
||||
var trackedDownload = GetTrackedDownload(resource.Id);
|
||||
|
||||
_completedDownloadService.Process(trackedDownload, true);
|
||||
|
||||
return resource;
|
||||
throw new BadRequestException("No longer available");
|
||||
}
|
||||
|
||||
private object Grab()
|
||||
|
@ -44,7 +44,7 @@ public static QueueResource ToResource(this Core.Queue.Queue model)
|
||||
Timeleft = model.Timeleft,
|
||||
EstimatedCompletionTime = model.EstimatedCompletionTime,
|
||||
Status = model.Status,
|
||||
TrackedDownloadStatus = model.TrackedDownloadStatus,
|
||||
TrackedDownloadStatus = model.TrackedDownloadStatus.ToString(),
|
||||
StatusMessages = model.StatusMessages,
|
||||
DownloadId = model.DownloadId,
|
||||
Protocol = model.Protocol,
|
||||
|
@ -1,4 +1,4 @@
|
||||
using FluentAssertions;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Test.Common;
|
||||
@ -185,6 +185,15 @@ public void should_fix_slashes_unix()
|
||||
osPath.FullPath.Should().Be(@"/just/a/test/to/verify the/slashes/");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_fix_double_slashes_unix()
|
||||
{
|
||||
var osPath = new OsPath(@"/just/a//test////to/verify the/slashes/");
|
||||
|
||||
osPath.Kind.Should().Be(OsPathKind.Unix);
|
||||
osPath.FullPath.Should().Be(@"/just/a/test/to/verify the/slashes/");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_combine_mixed_slashes()
|
||||
{
|
||||
|
@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
@ -85,7 +85,13 @@ private static string FixSlashes(string path, OsPathKind kind)
|
||||
case OsPathKind.Windows:
|
||||
return path.Replace('/', '\\');
|
||||
case OsPathKind.Unix:
|
||||
return path.Replace('\\', '/');
|
||||
path = path.Replace('\\', '/');
|
||||
while (path.Contains("//"))
|
||||
{
|
||||
path = path.Replace("//", "/");
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
return path;
|
||||
|
@ -6,6 +6,7 @@
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles;
|
||||
@ -24,6 +25,8 @@ public class QueueSpecificationFixture : CoreTest<QueueSpecification>
|
||||
|
||||
private Movie _otherMovie;
|
||||
|
||||
private ReleaseInfo _releaseInfo;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
@ -45,6 +48,9 @@ public void Setup()
|
||||
.With(s => s.Id = 2)
|
||||
.Build();
|
||||
|
||||
_releaseInfo = Builder<ReleaseInfo>.CreateNew()
|
||||
.Build();
|
||||
|
||||
_remoteMovie = Builder<RemoteMovie>.CreateNew()
|
||||
.With(r => r.Movie = _movie)
|
||||
.With(r => r.ParsedMovieInfo = new ParsedMovieInfo { Quality = new QualityModel(Quality.DVD) })
|
||||
@ -63,11 +69,12 @@ private void GivenEmptyQueue()
|
||||
.Returns(new List<Queue.Queue>());
|
||||
}
|
||||
|
||||
private void GivenQueue(IEnumerable<RemoteMovie> remoteMovies)
|
||||
private void GivenQueue(IEnumerable<RemoteMovie> remoteMovies, TrackedDownloadState trackedDownloadState = TrackedDownloadState.Downloading)
|
||||
{
|
||||
var queue = remoteMovies.Select(remoteMovie => new Queue.Queue
|
||||
{
|
||||
RemoteMovie = remoteMovie
|
||||
RemoteMovie = remoteMovie,
|
||||
TrackedDownloadState = trackedDownloadState
|
||||
});
|
||||
|
||||
Mocker.GetMock<IQueueService>()
|
||||
@ -178,5 +185,24 @@ public void should_return_false_when_quality_is_better_and_upgrade_allowed_is_fa
|
||||
GivenQueue(new List<RemoteMovie> { remoteMovie });
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_if_everything_is_the_same_for_failed_pending()
|
||||
{
|
||||
_movie.Profile.Cutoff = Quality.Bluray1080p.Id;
|
||||
|
||||
var remoteMovie = Builder<RemoteMovie>.CreateNew()
|
||||
.With(r => r.Movie = _movie)
|
||||
.With(r => r.ParsedMovieInfo = new ParsedMovieInfo
|
||||
{
|
||||
Quality = new QualityModel(Quality.DVD)
|
||||
})
|
||||
.With(r => r.Release = _releaseInfo)
|
||||
.Build();
|
||||
|
||||
GivenQueue(new List<RemoteMovie> { remoteMovie }, TrackedDownloadState.FailedPending);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteMovie, null).Accepted.Should().BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,7 @@
|
||||
namespace NzbDrone.Core.Test.Download
|
||||
{
|
||||
[TestFixture]
|
||||
public class CompletedDownloadServiceFixture : CoreTest<CompletedDownloadService>
|
||||
public class ImportFixture : CoreTest<CompletedDownloadService>
|
||||
{
|
||||
private TrackedDownload _trackedDownload;
|
||||
|
||||
@ -33,12 +33,12 @@ public void Setup()
|
||||
.With(h => h.Title = "Drone.1998")
|
||||
.Build();
|
||||
|
||||
var remoteEpisode = BuildRemoteMovie();
|
||||
var remoteMovie = BuildRemoteMovie();
|
||||
|
||||
_trackedDownload = Builder<TrackedDownload>.CreateNew()
|
||||
.With(c => c.State = TrackedDownloadStage.Downloading)
|
||||
.With(c => c.State = TrackedDownloadState.Downloading)
|
||||
.With(c => c.DownloadItem = completed)
|
||||
.With(c => c.RemoteMovie = remoteEpisode)
|
||||
.With(c => c.RemoteMovie = remoteMovie)
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IDownloadClient>()
|
||||
@ -55,7 +55,7 @@ public void Setup()
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.GetMovie("Drone.1998"))
|
||||
.Returns(remoteEpisode.Movie);
|
||||
.Returns(remoteMovie.Movie);
|
||||
}
|
||||
|
||||
private RemoteMovie BuildRemoteMovie()
|
||||
@ -66,23 +66,6 @@ private RemoteMovie BuildRemoteMovie()
|
||||
};
|
||||
}
|
||||
|
||||
private void GivenNoGrabbedHistory()
|
||||
{
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
|
||||
.Returns((History.History)null);
|
||||
}
|
||||
|
||||
private void GivenSuccessfulImport()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedMovieImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Movie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalMovie() { Path = @"C:\TestPath\Droned.1998.mkv" }))
|
||||
});
|
||||
}
|
||||
|
||||
private void GivenABadlyNamedDownload()
|
||||
{
|
||||
_trackedDownload.DownloadItem.DownloadId = "1234";
|
||||
@ -107,75 +90,6 @@ private void GivenSeriesMatch()
|
||||
.Returns(_trackedDownload.RemoteMovie.Movie);
|
||||
}
|
||||
|
||||
[TestCase(DownloadItemStatus.Downloading)]
|
||||
[TestCase(DownloadItemStatus.Failed)]
|
||||
[TestCase(DownloadItemStatus.Queued)]
|
||||
[TestCase(DownloadItemStatus.Paused)]
|
||||
[TestCase(DownloadItemStatus.Warning)]
|
||||
public void should_not_process_if_download_status_isnt_completed(DownloadItemStatus status)
|
||||
{
|
||||
_trackedDownload.DownloadItem.Status = status;
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertNoAttemptedImport();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_process_if_matching_history_is_not_found_and_no_category_specified()
|
||||
{
|
||||
_trackedDownload.DownloadItem.Category = null;
|
||||
GivenNoGrabbedHistory();
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertNoAttemptedImport();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_process_if_matching_history_is_not_found_but_category_specified()
|
||||
{
|
||||
_trackedDownload.DownloadItem.Category = "tv";
|
||||
GivenNoGrabbedHistory();
|
||||
GivenSeriesMatch();
|
||||
GivenSuccessfulImport();
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertCompletedDownload();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_process_if_output_path_is_empty()
|
||||
{
|
||||
_trackedDownload.DownloadItem.OutputPath = default;
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertNoAttemptedImport();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_mark_as_imported_if_all_episodes_were_imported()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedMovieImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Movie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(
|
||||
new ImportDecision(
|
||||
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" })),
|
||||
|
||||
new ImportResult(
|
||||
new ImportDecision(
|
||||
new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }))
|
||||
});
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertCompletedDownload();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_mark_as_imported_if_all_files_were_rejected()
|
||||
{
|
||||
@ -192,16 +106,16 @@ public void should_not_mark_as_imported_if_all_files_were_rejected()
|
||||
new LocalMovie { Path = @"C:\TestPath\Droned.1999.mkv" }, new Rejection("Rejected!")), "Test Failure")
|
||||
});
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
Subject.Import(_trackedDownload);
|
||||
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent<DownloadCompletedEvent>(It.IsAny<DownloadCompletedEvent>()), Times.Never());
|
||||
|
||||
AssertNoCompletedDownload();
|
||||
AssertNotImported();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_mark_as_imported_if_no_episodes_were_parsed()
|
||||
public void should_not_mark_as_imported_if_no_movies_were_parsed()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedMovieImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Movie>(), It.IsAny<DownloadClientItem>()))
|
||||
@ -218,9 +132,9 @@ public void should_not_mark_as_imported_if_no_episodes_were_parsed()
|
||||
|
||||
_trackedDownload.RemoteMovie.Movie = null;
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
Subject.Import(_trackedDownload);
|
||||
|
||||
AssertNoCompletedDownload();
|
||||
AssertNotImported();
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -234,138 +148,61 @@ public void should_not_mark_as_imported_if_all_files_were_skipped()
|
||||
new ImportResult(new ImportDecision(new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }), "Test Failure")
|
||||
});
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
Subject.Import(_trackedDownload);
|
||||
|
||||
AssertNoCompletedDownload();
|
||||
AssertNotImported();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_mark_as_imported_if_all_episodes_were_imported_but_extra_files_were_not()
|
||||
public void should_mark_as_imported_if_all_movies_were_imported_but_extra_files_were_not()
|
||||
{
|
||||
GivenSeriesMatch();
|
||||
|
||||
_trackedDownload.RemoteMovie.Movie = new Movie();
|
||||
|
||||
Mocker.GetMock<IDownloadedMovieImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Movie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" })),
|
||||
new ImportResult(new ImportDecision(new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }), "Test Failure")
|
||||
});
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalMovie { Path = @"C:\TestPath\Droned.S01E01.mkv", Movie = _trackedDownload.RemoteMovie.Movie })),
|
||||
new ImportResult(new ImportDecision(new LocalMovie { Path = @"C:\TestPath\Droned.S01E01.mkv" }), "Test Failure")
|
||||
});
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
Subject.Import(_trackedDownload);
|
||||
|
||||
AssertCompletedDownload();
|
||||
AssertImported();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_mark_as_imported_if_the_download_can_be_tracked_using_the_source_seriesid()
|
||||
public void should_mark_as_imported_if_the_download_can_be_tracked_using_the_source_movieid()
|
||||
{
|
||||
GivenABadlyNamedDownload();
|
||||
|
||||
Mocker.GetMock<IDownloadedMovieImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Movie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }))
|
||||
});
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalMovie { Path = @"C:\TestPath\Droned.S01E01.mkv", Movie = _trackedDownload.RemoteMovie.Movie }))
|
||||
});
|
||||
|
||||
Mocker.GetMock<IMovieService>()
|
||||
.Setup(v => v.GetMovie(It.IsAny<int>()))
|
||||
.Returns(BuildRemoteMovie().Movie);
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
Subject.Import(_trackedDownload);
|
||||
|
||||
AssertCompletedDownload();
|
||||
AssertImported();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_mark_as_imported_if_the_download_cannot_be_tracked_using_the_source_title_as_it_was_initiated_externally()
|
||||
{
|
||||
GivenABadlyNamedDownload();
|
||||
|
||||
Mocker.GetMock<IDownloadedMovieImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Movie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }))
|
||||
});
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.MostRecentForDownloadId(It.Is<string>(i => i == "1234")));
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertNoCompletedDownload();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_import_when_there_is_a_title_mismatch()
|
||||
{
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.GetMovie("Drone.1998"))
|
||||
.Returns((Movie)null);
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertNoCompletedDownload();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_mark_as_import_title_mismatch_if_ignore_warnings_is_true()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedMovieImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Movie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalMovie { Path = @"C:\TestPath\Droned.1998.mkv" }))
|
||||
});
|
||||
|
||||
Subject.Process(_trackedDownload, true);
|
||||
|
||||
AssertCompletedDownload();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_warn_if_path_is_not_valid_for_windows()
|
||||
{
|
||||
WindowsOnly();
|
||||
|
||||
_trackedDownload.DownloadItem.OutputPath = new OsPath(@"/invalid/Windows/Path");
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertNoAttemptedImport();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_warn_if_path_is_not_valid_for_linux()
|
||||
{
|
||||
PosixOnly();
|
||||
|
||||
_trackedDownload.DownloadItem.OutputPath = new OsPath(@"C:\Invalid\Mono\Path");
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertNoAttemptedImport();
|
||||
}
|
||||
|
||||
private void AssertNoAttemptedImport()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedMovieImportService>()
|
||||
.Verify(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Movie>(), It.IsAny<DownloadClientItem>()), Times.Never());
|
||||
|
||||
AssertNoCompletedDownload();
|
||||
}
|
||||
|
||||
private void AssertNoCompletedDownload()
|
||||
private void AssertNotImported()
|
||||
{
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Never());
|
||||
|
||||
_trackedDownload.State.Should().NotBe(TrackedDownloadStage.Imported);
|
||||
_trackedDownload.State.Should().Be(TrackedDownloadState.ImportPending);
|
||||
}
|
||||
|
||||
private void AssertCompletedDownload()
|
||||
private void AssertImported()
|
||||
{
|
||||
Mocker.GetMock<IDownloadedMovieImportService>()
|
||||
.Verify(v => v.ProcessPath(_trackedDownload.DownloadItem.OutputPath.FullPath, ImportMode.Auto, _trackedDownload.RemoteMovie.Movie, _trackedDownload.DownloadItem), Times.Once());
|
||||
@ -373,7 +210,7 @@ private void AssertCompletedDownload()
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.IsAny<DownloadCompletedEvent>()), Times.Once());
|
||||
|
||||
_trackedDownload.State.Should().Be(TrackedDownloadStage.Imported);
|
||||
_trackedDownload.State.Should().Be(TrackedDownloadState.Imported);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,189 @@
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.MediaFiles;
|
||||
using NzbDrone.Core.MediaFiles.MovieImport;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Download.CompletedDownloadServiceTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ProcessFixture : CoreTest<CompletedDownloadService>
|
||||
{
|
||||
private TrackedDownload _trackedDownload;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var completed = Builder<DownloadClientItem>.CreateNew()
|
||||
.With(h => h.Status = DownloadItemStatus.Completed)
|
||||
.With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic()))
|
||||
.With(h => h.Title = "Drone.S01E01.HDTV")
|
||||
.Build();
|
||||
|
||||
var remoteMovie = BuildRemoteMovie();
|
||||
|
||||
_trackedDownload = Builder<TrackedDownload>.CreateNew()
|
||||
.With(c => c.State = TrackedDownloadState.Downloading)
|
||||
.With(c => c.DownloadItem = completed)
|
||||
.With(c => c.RemoteMovie = remoteMovie)
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IDownloadClient>()
|
||||
.SetupGet(c => c.Definition)
|
||||
.Returns(new DownloadClientDefinition { Id = 1, Name = "testClient" });
|
||||
|
||||
Mocker.GetMock<IProvideDownloadClient>()
|
||||
.Setup(c => c.Get(It.IsAny<int>()))
|
||||
.Returns(Mocker.GetMock<IDownloadClient>().Object);
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
|
||||
.Returns(new History.History());
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.GetMovie("Drone.S01E01.HDTV"))
|
||||
.Returns(remoteMovie.Movie);
|
||||
}
|
||||
|
||||
private RemoteMovie BuildRemoteMovie()
|
||||
{
|
||||
return new RemoteMovie
|
||||
{
|
||||
Movie = new Movie(),
|
||||
};
|
||||
}
|
||||
|
||||
private void GivenNoGrabbedHistory()
|
||||
{
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.MostRecentForDownloadId(_trackedDownload.DownloadItem.DownloadId))
|
||||
.Returns((History.History)null);
|
||||
}
|
||||
|
||||
private void GivenMovieMatch()
|
||||
{
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.GetMovie(It.IsAny<string>()))
|
||||
.Returns(_trackedDownload.RemoteMovie.Movie);
|
||||
}
|
||||
|
||||
private void GivenABadlyNamedDownload()
|
||||
{
|
||||
_trackedDownload.DownloadItem.DownloadId = "1234";
|
||||
_trackedDownload.DownloadItem.Title = "Droned Pilot"; // Set a badly named download
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.MostRecentForDownloadId(It.Is<string>(i => i == "1234")))
|
||||
.Returns(new History.History() { SourceTitle = "Droned S01E01" });
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.GetMovie(It.IsAny<string>()))
|
||||
.Returns((Movie)null);
|
||||
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.GetMovie("Droned S01E01"))
|
||||
.Returns(BuildRemoteMovie().Movie);
|
||||
}
|
||||
|
||||
[TestCase(DownloadItemStatus.Downloading)]
|
||||
[TestCase(DownloadItemStatus.Failed)]
|
||||
[TestCase(DownloadItemStatus.Queued)]
|
||||
[TestCase(DownloadItemStatus.Paused)]
|
||||
[TestCase(DownloadItemStatus.Warning)]
|
||||
public void should_not_process_if_download_status_isnt_completed(DownloadItemStatus status)
|
||||
{
|
||||
_trackedDownload.DownloadItem.Status = status;
|
||||
|
||||
Subject.Check(_trackedDownload);
|
||||
|
||||
AssertNotReadyToImport();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_process_if_matching_history_is_not_found_and_no_category_specified()
|
||||
{
|
||||
_trackedDownload.DownloadItem.Category = null;
|
||||
GivenNoGrabbedHistory();
|
||||
|
||||
Subject.Check(_trackedDownload);
|
||||
|
||||
AssertNotReadyToImport();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_process_if_matching_history_is_not_found_but_category_specified()
|
||||
{
|
||||
_trackedDownload.DownloadItem.Category = "tv";
|
||||
GivenNoGrabbedHistory();
|
||||
GivenMovieMatch();
|
||||
|
||||
Subject.Check(_trackedDownload);
|
||||
|
||||
AssertReadyToImport();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_process_if_output_path_is_empty()
|
||||
{
|
||||
_trackedDownload.DownloadItem.OutputPath = default(OsPath);
|
||||
|
||||
Subject.Check(_trackedDownload);
|
||||
|
||||
AssertNotReadyToImport();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_process_if_the_download_cannot_be_tracked_using_the_source_title_as_it_was_initiated_externally()
|
||||
{
|
||||
GivenABadlyNamedDownload();
|
||||
|
||||
Mocker.GetMock<IDownloadedMovieImportService>()
|
||||
.Setup(v => v.ProcessPath(It.IsAny<string>(), It.IsAny<ImportMode>(), It.IsAny<Movie>(), It.IsAny<DownloadClientItem>()))
|
||||
.Returns(new List<ImportResult>
|
||||
{
|
||||
new ImportResult(new ImportDecision(new LocalMovie { Path = @"C:\TestPath\Droned.S01E01.mkv" }))
|
||||
});
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.MostRecentForDownloadId(It.Is<string>(i => i == "1234")));
|
||||
|
||||
Subject.Check(_trackedDownload);
|
||||
|
||||
AssertNotReadyToImport();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_not_process_when_there_is_a_title_mismatch()
|
||||
{
|
||||
Mocker.GetMock<IParsingService>()
|
||||
.Setup(s => s.GetMovie("Drone.S01E01.HDTV"))
|
||||
.Returns((Movie)null);
|
||||
|
||||
Subject.Check(_trackedDownload);
|
||||
|
||||
AssertNotReadyToImport();
|
||||
}
|
||||
|
||||
private void AssertNotReadyToImport()
|
||||
{
|
||||
_trackedDownload.State.Should().NotBe(TrackedDownloadState.ImportPending);
|
||||
}
|
||||
|
||||
private void AssertReadyToImport()
|
||||
{
|
||||
_trackedDownload.State.Should().Be(TrackedDownloadState.ImportPending);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,100 @@
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Common.Disk;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Download.FailedDownloadServiceTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class ProcessFailedFixture : CoreTest<FailedDownloadService>
|
||||
{
|
||||
private TrackedDownload _trackedDownload;
|
||||
private List<History.History> _grabHistory;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
var completed = Builder<DownloadClientItem>.CreateNew()
|
||||
.With(h => h.Status = DownloadItemStatus.Completed)
|
||||
.With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic()))
|
||||
.With(h => h.Title = "Drone.S01E01.HDTV")
|
||||
.Build();
|
||||
|
||||
_grabHistory = Builder<History.History>.CreateListOfSize(2).BuildList();
|
||||
|
||||
var remoteMovie = new RemoteMovie
|
||||
{
|
||||
Movie = new Movie()
|
||||
};
|
||||
|
||||
_trackedDownload = Builder<TrackedDownload>.CreateNew()
|
||||
.With(c => c.State = TrackedDownloadState.FailedPending)
|
||||
.With(c => c.DownloadItem = completed)
|
||||
.With(c => c.RemoteMovie = remoteMovie)
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
.Setup(s => s.Find(_trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed))
|
||||
.Returns(_grabHistory);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_mark_failed_if_encrypted()
|
||||
{
|
||||
_trackedDownload.DownloadItem.IsEncrypted = true;
|
||||
|
||||
Subject.ProcessFailed(_trackedDownload);
|
||||
|
||||
AssertDownloadFailed();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_mark_failed_if_download_item_is_failed()
|
||||
{
|
||||
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
|
||||
|
||||
Subject.ProcessFailed(_trackedDownload);
|
||||
|
||||
AssertDownloadFailed();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_include_tracked_download_in_message()
|
||||
{
|
||||
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
|
||||
|
||||
Subject.ProcessFailed(_trackedDownload);
|
||||
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.Is<DownloadFailedEvent>(c => c.TrackedDownload != null)), Times.Once());
|
||||
|
||||
AssertDownloadFailed();
|
||||
}
|
||||
|
||||
private void AssertDownloadNotFailed()
|
||||
{
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Never());
|
||||
|
||||
_trackedDownload.State.Should().NotBe(TrackedDownloadState.Failed);
|
||||
}
|
||||
|
||||
private void AssertDownloadFailed()
|
||||
{
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Once());
|
||||
|
||||
_trackedDownload.State.Should().Be(TrackedDownloadState.Failed);
|
||||
}
|
||||
}
|
||||
}
|
@ -13,10 +13,10 @@
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Test.Common;
|
||||
|
||||
namespace NzbDrone.Core.Test.Download
|
||||
namespace NzbDrone.Core.Test.Download.FailedDownloadServiceTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class FailedDownloadServiceFixture : CoreTest<FailedDownloadService>
|
||||
public class ProcessFixture : CoreTest<FailedDownloadService>
|
||||
{
|
||||
private TrackedDownload _trackedDownload;
|
||||
private List<History.History> _grabHistory;
|
||||
@ -32,15 +32,15 @@ public void Setup()
|
||||
|
||||
_grabHistory = Builder<History.History>.CreateListOfSize(2).BuildList();
|
||||
|
||||
var remoteEpisode = new RemoteMovie
|
||||
var remoteMovie = new RemoteMovie
|
||||
{
|
||||
Movie = new Movie(),
|
||||
};
|
||||
|
||||
_trackedDownload = Builder<TrackedDownload>.CreateNew()
|
||||
.With(c => c.State = TrackedDownloadStage.Downloading)
|
||||
.With(c => c.State = TrackedDownloadState.Downloading)
|
||||
.With(c => c.DownloadItem = completed)
|
||||
.With(c => c.RemoteMovie = remoteEpisode)
|
||||
.With(c => c.RemoteMovie = remoteMovie)
|
||||
.Build();
|
||||
|
||||
Mocker.GetMock<IHistoryService>()
|
||||
@ -60,7 +60,7 @@ public void should_not_fail_if_matching_history_is_not_found()
|
||||
{
|
||||
GivenNoGrabbedHistory();
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
Subject.Check(_trackedDownload);
|
||||
|
||||
AssertDownloadNotFailed();
|
||||
}
|
||||
@ -71,7 +71,7 @@ public void should_warn_if_matching_history_is_not_found()
|
||||
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
|
||||
GivenNoGrabbedHistory();
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
Subject.Check(_trackedDownload);
|
||||
|
||||
_trackedDownload.StatusMessages.Should().NotBeEmpty();
|
||||
}
|
||||
@ -82,50 +82,17 @@ public void should_not_warn_if_matching_history_is_not_found_and_not_failed()
|
||||
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
|
||||
GivenNoGrabbedHistory();
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
Subject.Check(_trackedDownload);
|
||||
|
||||
_trackedDownload.StatusMessages.Should().NotBeEmpty();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_mark_failed_if_encrypted()
|
||||
{
|
||||
_trackedDownload.DownloadItem.IsEncrypted = true;
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertDownloadFailed();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_mark_failed_if_download_item_is_failed()
|
||||
{
|
||||
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
AssertDownloadFailed();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_include_tracked_download_in_message()
|
||||
{
|
||||
_trackedDownload.DownloadItem.Status = DownloadItemStatus.Failed;
|
||||
|
||||
Subject.Process(_trackedDownload);
|
||||
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.Is<DownloadFailedEvent>(c => c.TrackedDownload != null)), Times.Once());
|
||||
|
||||
AssertDownloadFailed();
|
||||
}
|
||||
|
||||
private void AssertDownloadNotFailed()
|
||||
{
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Never());
|
||||
|
||||
_trackedDownload.State.Should().NotBe(TrackedDownloadStage.DownloadFailed);
|
||||
_trackedDownload.State.Should().NotBe(TrackedDownloadState.Failed);
|
||||
}
|
||||
|
||||
private void AssertDownloadFailed()
|
||||
@ -133,7 +100,7 @@ private void AssertDownloadFailed()
|
||||
Mocker.GetMock<IEventAggregator>()
|
||||
.Verify(v => v.PublishEvent(It.IsAny<DownloadFailedEvent>()), Times.Once());
|
||||
|
||||
_trackedDownload.State.Should().Be(TrackedDownloadStage.DownloadFailed);
|
||||
_trackedDownload.State.Should().Be(TrackedDownloadState.Failed);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
using System.Collections.Generic;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Movies;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Test.Download.TrackedDownloads
|
||||
{
|
||||
[TestFixture]
|
||||
public class TrackedDownloadAlreadyImportedFixture : CoreTest<TrackedDownloadAlreadyImported>
|
||||
{
|
||||
private Movie _movie;
|
||||
private TrackedDownload _trackedDownload;
|
||||
private List<History.History> _historyItems;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_movie = Builder<Movie>.CreateNew().Build();
|
||||
|
||||
var remoteMovie = Builder<RemoteMovie>.CreateNew()
|
||||
.With(r => r.Movie = _movie)
|
||||
.Build();
|
||||
|
||||
_trackedDownload = Builder<TrackedDownload>.CreateNew()
|
||||
.With(t => t.RemoteMovie = remoteMovie)
|
||||
.Build();
|
||||
|
||||
_historyItems = new List<History.History>();
|
||||
}
|
||||
|
||||
public void GivenHistoryForMovie(Movie movie, params HistoryEventType[] eventTypes)
|
||||
{
|
||||
foreach (var eventType in eventTypes)
|
||||
{
|
||||
_historyItems.Add(
|
||||
Builder<History.History>.CreateNew()
|
||||
.With(h => h.MovieId = movie.Id)
|
||||
.With(h => h.EventType = eventType)
|
||||
.Build());
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_there_is_no_history()
|
||||
{
|
||||
Subject.IsImported(_trackedDownload, _historyItems)
|
||||
.Should()
|
||||
.BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_false_if_single_movie_download_is_not_imported()
|
||||
{
|
||||
GivenHistoryForMovie(_movie, HistoryEventType.Grabbed);
|
||||
|
||||
Subject.IsImported(_trackedDownload, _historyItems)
|
||||
.Should()
|
||||
.BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_return_true_if_single_movie_download_is_imported()
|
||||
{
|
||||
GivenHistoryForMovie(_movie, HistoryEventType.DownloadFolderImported, HistoryEventType.Grabbed);
|
||||
|
||||
Subject.IsImported(_trackedDownload, _historyItems)
|
||||
.Should()
|
||||
.BeTrue();
|
||||
}
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ public void Setup()
|
||||
{
|
||||
DownloadItem = downloadItem,
|
||||
RemoteMovie = remoteMovie,
|
||||
State = TrackedDownloadStage.Downloading
|
||||
State = TrackedDownloadState.Downloading
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -41,7 +41,7 @@ public void Setup()
|
||||
[Test]
|
||||
public void should_not_remove_commands_for_five_minutes_after_they_end()
|
||||
{
|
||||
var command = Subject.Push<CheckForFinishedDownloadCommand>(new CheckForFinishedDownloadCommand());
|
||||
var command = Subject.Push<RefreshMonitoredDownloadsCommand>(new RefreshMonitoredDownloadsCommand());
|
||||
|
||||
// Start the command to mimic CommandQueue's behaviour
|
||||
command.StartedAt = DateTime.Now;
|
||||
|
@ -2,6 +2,7 @@
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.CustomFormats;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Queue;
|
||||
@ -41,6 +42,14 @@ public Decision IsSatisfiedBy(RemoteMovie subject, SearchCriteriaBase searchCrit
|
||||
var remoteMovie = queueItem.RemoteMovie;
|
||||
var qualityProfile = subject.Movie.Profile;
|
||||
|
||||
// To avoid a race make sure it's not FailedPending (failed awaiting removal/search).
|
||||
// Failed items (already searching for a replacement) won't be part of the queue since
|
||||
// it's a copy, of the tracked download, not a reference.
|
||||
if (queueItem.TrackedDownloadState == TrackedDownloadState.FailedPending)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var customFormats = _formatService.ParseCustomFormat(remoteMovie.ParsedMovieInfo);
|
||||
|
||||
_logger.Debug("Checking if existing release in queue meets cutoff. Queued quality is: {0} - {1}",
|
||||
|
@ -4,6 +4,5 @@ namespace NzbDrone.Core.Download
|
||||
{
|
||||
public class CheckForFinishedDownloadCommand : Command
|
||||
{
|
||||
public override bool RequiresDiskAccess => true;
|
||||
}
|
||||
}
|
||||
|
@ -16,103 +16,131 @@ namespace NzbDrone.Core.Download
|
||||
{
|
||||
public interface ICompletedDownloadService
|
||||
{
|
||||
void Process(TrackedDownload trackedDownload, bool ignoreWarnings = false);
|
||||
void Check(TrackedDownload trackedDownload);
|
||||
void Import(TrackedDownload trackedDownload);
|
||||
}
|
||||
|
||||
public class CompletedDownloadService : ICompletedDownloadService
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly IDownloadedMovieImportService _downloadedMovieImportService;
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly IMovieService _movieService;
|
||||
private readonly Logger _logger;
|
||||
private readonly ITrackedDownloadAlreadyImported _trackedDownloadAlreadyImported;
|
||||
|
||||
public CompletedDownloadService(IConfigService configService,
|
||||
IEventAggregator eventAggregator,
|
||||
public CompletedDownloadService(IEventAggregator eventAggregator,
|
||||
IHistoryService historyService,
|
||||
IDownloadedMovieImportService downloadedMovieImportService,
|
||||
IParsingService parsingService,
|
||||
IMovieService movieService,
|
||||
Logger logger)
|
||||
ITrackedDownloadAlreadyImported trackedDownloadAlreadyImported)
|
||||
{
|
||||
_configService = configService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_historyService = historyService;
|
||||
_downloadedMovieImportService = downloadedMovieImportService;
|
||||
_parsingService = parsingService;
|
||||
_movieService = movieService;
|
||||
_logger = logger;
|
||||
_trackedDownloadAlreadyImported = trackedDownloadAlreadyImported;
|
||||
}
|
||||
|
||||
public void Process(TrackedDownload trackedDownload, bool ignoreWarnings = false)
|
||||
public void Check(TrackedDownload trackedDownload)
|
||||
{
|
||||
if (trackedDownload.DownloadItem.Status != DownloadItemStatus.Completed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ignoreWarnings)
|
||||
// Only process tracked downloads that are still downloading
|
||||
if (trackedDownload.State != TrackedDownloadState.Downloading)
|
||||
{
|
||||
var historyItem = _historyService.MostRecentForDownloadId(trackedDownload.DownloadItem.DownloadId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace())
|
||||
var historyItem = _historyService.MostRecentForDownloadId(trackedDownload.DownloadItem.DownloadId);
|
||||
|
||||
if (historyItem == null && trackedDownload.DownloadItem.Category.IsNullOrWhiteSpace())
|
||||
{
|
||||
trackedDownload.Warn("Download wasn't grabbed by Sonarr and not in a category, Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath;
|
||||
|
||||
if (downloadItemOutputPath.IsEmpty)
|
||||
{
|
||||
trackedDownload.Warn("Download doesn't contain intermediate path, Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((OsInfo.IsWindows && !downloadItemOutputPath.IsWindowsPath) ||
|
||||
(OsInfo.IsNotWindows && !downloadItemOutputPath.IsUnixPath))
|
||||
{
|
||||
trackedDownload.Warn("[{0}] is not a valid local path. You may need a Remote Path Mapping.", downloadItemOutputPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var movie = _parsingService.GetMovie(trackedDownload.DownloadItem.Title);
|
||||
|
||||
if (movie == null)
|
||||
{
|
||||
if (historyItem != null)
|
||||
{
|
||||
trackedDownload.Warn("Download wasn't grabbed by Radarr and not in a category, Skipping.");
|
||||
return;
|
||||
movie = _movieService.GetMovie(historyItem.MovieId);
|
||||
}
|
||||
|
||||
var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath;
|
||||
|
||||
if (downloadItemOutputPath.IsEmpty)
|
||||
{
|
||||
trackedDownload.Warn("Download doesn't contain intermediate path, Skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
if ((OsInfo.IsWindows && !downloadItemOutputPath.IsWindowsPath) ||
|
||||
(OsInfo.IsNotWindows && !downloadItemOutputPath.IsUnixPath))
|
||||
{
|
||||
trackedDownload.Warn("[{0}] is not a valid local path. You may need a Remote Path Mapping.", downloadItemOutputPath);
|
||||
return;
|
||||
}
|
||||
|
||||
var movie = _parsingService.GetMovie(trackedDownload.DownloadItem.Title);
|
||||
if (movie == null)
|
||||
{
|
||||
if (historyItem != null)
|
||||
{
|
||||
movie = _movieService.GetMovie(historyItem.MovieId);
|
||||
}
|
||||
|
||||
if (movie == null)
|
||||
{
|
||||
trackedDownload.Warn("Movie title mismatch, automatic import is not possible.");
|
||||
return;
|
||||
}
|
||||
trackedDownload.Warn("Series title mismatch, automatic import is not possible.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Import(trackedDownload);
|
||||
trackedDownload.State = TrackedDownloadState.ImportPending;
|
||||
}
|
||||
|
||||
private void Import(TrackedDownload trackedDownload)
|
||||
public void Import(TrackedDownload trackedDownload)
|
||||
{
|
||||
trackedDownload.State = TrackedDownloadState.Importing;
|
||||
|
||||
var outputPath = trackedDownload.DownloadItem.OutputPath.FullPath;
|
||||
var importResults = _downloadedMovieImportService.ProcessPath(outputPath, ImportMode.Auto, trackedDownload.RemoteMovie.Movie, trackedDownload.DownloadItem);
|
||||
|
||||
var allMoviesImported = importResults.Where(c => c.Result == ImportResultType.Imported)
|
||||
.Select(c => c.ImportDecision.LocalMovie.Movie)
|
||||
.Count() >= 1;
|
||||
|
||||
if (allMoviesImported)
|
||||
{
|
||||
trackedDownload.State = TrackedDownloadState.Imported;
|
||||
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
|
||||
return;
|
||||
}
|
||||
|
||||
// Double check if all episodes were imported by checking the history if at least one
|
||||
// file was imported. This will allow the decision engine to reject already imported
|
||||
// episode files and still mark the download complete when all files are imported.
|
||||
if (importResults.Any(c => c.Result == ImportResultType.Imported))
|
||||
{
|
||||
var historyItems = _historyService.FindByDownloadId(trackedDownload.DownloadItem.DownloadId)
|
||||
.OrderByDescending(h => h.Date)
|
||||
.ToList();
|
||||
|
||||
var allEpisodesImportedInHistory = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems);
|
||||
|
||||
if (allEpisodesImportedInHistory)
|
||||
{
|
||||
trackedDownload.State = TrackedDownloadState.Imported;
|
||||
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
trackedDownload.State = TrackedDownloadState.ImportPending;
|
||||
|
||||
if (importResults.Empty())
|
||||
{
|
||||
trackedDownload.Warn("No files found are eligible for import in {0}", outputPath);
|
||||
return;
|
||||
}
|
||||
|
||||
if (importResults.Count(c => c.Result == ImportResultType.Imported) >= 1)
|
||||
{
|
||||
trackedDownload.State = TrackedDownloadStage.Imported;
|
||||
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
|
||||
return;
|
||||
}
|
||||
|
||||
if (importResults.Any(c => c.Result != ImportResultType.Imported))
|
||||
|
18
src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs
Normal file
18
src/NzbDrone.Core/Download/DownloadIgnoredEvent.cs
Normal file
@ -0,0 +1,18 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Common.Messaging;
|
||||
using NzbDrone.Core.Languages;
|
||||
using NzbDrone.Core.Qualities;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public class DownloadIgnoredEvent : IEvent
|
||||
{
|
||||
public int MovieId { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public QualityModel Quality { get; set; }
|
||||
public string SourceTitle { get; set; }
|
||||
public string DownloadClient { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public string Message { get; set; }
|
||||
}
|
||||
}
|
78
src/NzbDrone.Core/Download/DownloadProcessingService.cs
Normal file
78
src/NzbDrone.Core/Download/DownloadProcessingService.cs
Normal file
@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public class DownloadProcessingService : IExecute<ProcessMonitoredDownloadsCommand>
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly ICompletedDownloadService _completedDownloadService;
|
||||
private readonly IFailedDownloadService _failedDownloadService;
|
||||
private readonly ITrackedDownloadService _trackedDownloadService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public DownloadProcessingService(IConfigService configService,
|
||||
ICompletedDownloadService completedDownloadService,
|
||||
IFailedDownloadService failedDownloadService,
|
||||
ITrackedDownloadService trackedDownloadService,
|
||||
IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
_configService = configService;
|
||||
_completedDownloadService = completedDownloadService;
|
||||
_failedDownloadService = failedDownloadService;
|
||||
_trackedDownloadService = trackedDownloadService;
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
private void RemoveCompletedDownloads(List<TrackedDownload> trackedDownloads)
|
||||
{
|
||||
foreach (var trackedDownload in trackedDownloads.Where(c => c.DownloadItem.CanBeRemoved && c.State == TrackedDownloadState.Imported))
|
||||
{
|
||||
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute(ProcessMonitoredDownloadsCommand message)
|
||||
{
|
||||
var enableCompletedDownloadHandling = _configService.EnableCompletedDownloadHandling;
|
||||
var trackedDownloads = _trackedDownloadService.GetTrackedDownloads();
|
||||
|
||||
foreach (var trackedDownload in trackedDownloads)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (trackedDownload.State == TrackedDownloadState.FailedPending)
|
||||
{
|
||||
_failedDownloadService.ProcessFailed(trackedDownload);
|
||||
}
|
||||
|
||||
if (enableCompletedDownloadHandling && trackedDownload.State == TrackedDownloadState.ImportPending)
|
||||
{
|
||||
_completedDownloadService.Import(trackedDownload);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Debug(e, "Failed to process download: {0}", trackedDownload.DownloadItem.Title);
|
||||
}
|
||||
}
|
||||
|
||||
if (enableCompletedDownloadHandling && _configService.RemoveCompletedDownloads)
|
||||
{
|
||||
// Remove tracked downloads that are now complete
|
||||
RemoveCompletedDownloads(trackedDownloads);
|
||||
}
|
||||
|
||||
_eventAggregator.PublishEvent(new DownloadsProcessedEvent());
|
||||
}
|
||||
}
|
||||
}
|
11
src/NzbDrone.Core/Download/DownloadsProcessedEvent.cs
Normal file
11
src/NzbDrone.Core/Download/DownloadsProcessedEvent.cs
Normal file
@ -0,0 +1,11 @@
|
||||
using NzbDrone.Common.Messaging;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public class DownloadsProcessedEvent : IEvent
|
||||
{
|
||||
public DownloadsProcessedEvent()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,8 @@ public interface IFailedDownloadService
|
||||
{
|
||||
void MarkAsFailed(int historyId);
|
||||
void MarkAsFailed(string downloadId);
|
||||
void Process(TrackedDownload trackedDownload);
|
||||
void Check(TrackedDownload trackedDownload);
|
||||
void ProcessFailed(TrackedDownload trackedDownload);
|
||||
}
|
||||
|
||||
public class FailedDownloadService : IFailedDownloadService
|
||||
@ -52,23 +53,20 @@ public void MarkAsFailed(string downloadId)
|
||||
}
|
||||
}
|
||||
|
||||
public void Process(TrackedDownload trackedDownload)
|
||||
public void Check(TrackedDownload trackedDownload)
|
||||
{
|
||||
string failure = null;
|
||||
|
||||
if (trackedDownload.DownloadItem.IsEncrypted)
|
||||
// Only process tracked downloads that are still downloading
|
||||
if (trackedDownload.State != TrackedDownloadState.Downloading)
|
||||
{
|
||||
failure = "Encrypted download detected";
|
||||
}
|
||||
else if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed)
|
||||
{
|
||||
failure = trackedDownload.DownloadItem.Message ?? "Failed download detected";
|
||||
return;
|
||||
}
|
||||
|
||||
if (failure != null)
|
||||
if (trackedDownload.DownloadItem.IsEncrypted ||
|
||||
trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed)
|
||||
{
|
||||
var grabbedItems = _historyService.Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed)
|
||||
.ToList();
|
||||
var grabbedItems = _historyService
|
||||
.Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed)
|
||||
.ToList();
|
||||
|
||||
if (grabbedItems.Empty())
|
||||
{
|
||||
@ -76,11 +74,41 @@ public void Process(TrackedDownload trackedDownload)
|
||||
return;
|
||||
}
|
||||
|
||||
trackedDownload.State = TrackedDownloadStage.DownloadFailed;
|
||||
PublishDownloadFailedEvent(grabbedItems, failure, trackedDownload);
|
||||
trackedDownload.State = TrackedDownloadState.FailedPending;
|
||||
}
|
||||
}
|
||||
|
||||
public void ProcessFailed(TrackedDownload trackedDownload)
|
||||
{
|
||||
if (trackedDownload.State != TrackedDownloadState.FailedPending)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var grabbedItems = _historyService
|
||||
.Find(trackedDownload.DownloadItem.DownloadId, HistoryEventType.Grabbed)
|
||||
.ToList();
|
||||
|
||||
if (grabbedItems.Empty())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var failure = "Failed download detected";
|
||||
|
||||
if (trackedDownload.DownloadItem.IsEncrypted)
|
||||
{
|
||||
failure = "Encrypted download detected";
|
||||
}
|
||||
else if (trackedDownload.DownloadItem.Status == DownloadItemStatus.Failed && trackedDownload.DownloadItem.Message.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
failure = trackedDownload.DownloadItem.Message;
|
||||
}
|
||||
|
||||
trackedDownload.State = TrackedDownloadState.Failed;
|
||||
PublishDownloadFailedEvent(grabbedItems, failure, trackedDownload);
|
||||
}
|
||||
|
||||
private void PublishDownloadFailedEvent(List<History.History> historyItems, string message, TrackedDownload trackedDownload = null)
|
||||
{
|
||||
var historyItem = historyItems.First();
|
||||
|
49
src/NzbDrone.Core/Download/IgnoredDownloadService.cs
Normal file
49
src/NzbDrone.Core/Download/IgnoredDownloadService.cs
Normal file
@ -0,0 +1,49 @@
|
||||
using NLog;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public interface IIgnoredDownloadService
|
||||
{
|
||||
bool IgnoreDownload(TrackedDownload trackedDownload);
|
||||
}
|
||||
|
||||
public class IgnoredDownloadService : IIgnoredDownloadService
|
||||
{
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public IgnoredDownloadService(IEventAggregator eventAggregator,
|
||||
Logger logger)
|
||||
{
|
||||
_eventAggregator = eventAggregator;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public bool IgnoreDownload(TrackedDownload trackedDownload)
|
||||
{
|
||||
var movie = trackedDownload.RemoteMovie.Movie;
|
||||
|
||||
if (movie == null)
|
||||
{
|
||||
_logger.Warn("Unable to ignore download for unknown movie");
|
||||
return false;
|
||||
}
|
||||
|
||||
var downloadIgnoredEvent = new DownloadIgnoredEvent
|
||||
{
|
||||
MovieId = movie.Id,
|
||||
Languages = trackedDownload.RemoteMovie.ParsedMovieInfo.Languages,
|
||||
Quality = trackedDownload.RemoteMovie.ParsedMovieInfo.Quality,
|
||||
SourceTitle = trackedDownload.DownloadItem.Title,
|
||||
DownloadClient = trackedDownload.DownloadItem.DownloadClient,
|
||||
DownloadId = trackedDownload.DownloadItem.DownloadId,
|
||||
Message = "Manually ignored"
|
||||
};
|
||||
|
||||
_eventAggregator.PublishEvent(downloadIgnoredEvent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public class ProcessMonitoredDownloadsCommand : Command
|
||||
{
|
||||
public override bool RequiresDiskAccess => true;
|
||||
}
|
||||
}
|
@ -2,31 +2,30 @@
|
||||
using NLog;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.IndexerSearch;
|
||||
using NzbDrone.Core.Messaging;
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Movies;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public class RedownloadFailedDownloadService : IHandleAsync<DownloadFailedEvent>
|
||||
public class RedownloadFailedDownloadService : IHandle<DownloadFailedEvent>
|
||||
{
|
||||
private readonly IConfigService _configService;
|
||||
private readonly IMovieService _movieService;
|
||||
private readonly IManageCommandQueue _commandQueueManager;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public RedownloadFailedDownloadService(IConfigService configService,
|
||||
IMovieService movieService,
|
||||
IManageCommandQueue commandQueueManager,
|
||||
Logger logger)
|
||||
{
|
||||
_configService = configService;
|
||||
_movieService = movieService;
|
||||
_commandQueueManager = commandQueueManager;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public void HandleAsync(DownloadFailedEvent message)
|
||||
[EventHandleOrder(EventHandleOrder.Last)]
|
||||
public void Handle(DownloadFailedEvent message)
|
||||
{
|
||||
if (!_configService.AutoRedownloadFailed)
|
||||
{
|
||||
|
8
src/NzbDrone.Core/Download/RefreshDownloadsCommand.cs
Normal file
8
src/NzbDrone.Core/Download/RefreshDownloadsCommand.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using NzbDrone.Core.Messaging.Commands;
|
||||
|
||||
namespace NzbDrone.Core.Download
|
||||
{
|
||||
public class RefreshMonitoredDownloadsCommand : Command
|
||||
{
|
||||
}
|
||||
}
|
@ -11,9 +11,11 @@
|
||||
|
||||
namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
{
|
||||
public class DownloadMonitoringService : IExecute<CheckForFinishedDownloadCommand>,
|
||||
public class DownloadMonitoringService : IExecute<RefreshMonitoredDownloadsCommand>,
|
||||
IExecute<CheckForFinishedDownloadCommand>,
|
||||
IHandle<MovieGrabbedEvent>,
|
||||
IHandle<MovieImportedEvent>,
|
||||
IHandle<DownloadsProcessedEvent>,
|
||||
IHandle<TrackedDownloadsRemovedEvent>
|
||||
{
|
||||
private readonly IProvideDownloadClient _downloadClientProvider;
|
||||
@ -49,7 +51,7 @@ public DownloadMonitoringService(IProvideDownloadClient downloadClientProvider,
|
||||
|
||||
private void QueueRefresh()
|
||||
{
|
||||
_manageCommandQueue.Push(new CheckForFinishedDownloadCommand());
|
||||
_manageCommandQueue.Push(new RefreshMonitoredDownloadsCommand());
|
||||
}
|
||||
|
||||
private void Refresh()
|
||||
@ -70,6 +72,7 @@ private void Refresh()
|
||||
|
||||
_trackedDownloadService.UpdateTrackable(trackedDownloads);
|
||||
_eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads));
|
||||
_manageCommandQueue.Push(new ProcessMonitoredDownloadsCommand());
|
||||
}
|
||||
finally
|
||||
{
|
||||
@ -79,70 +82,54 @@ private void Refresh()
|
||||
|
||||
private List<TrackedDownload> ProcessClientDownloads(IDownloadClient downloadClient)
|
||||
{
|
||||
List<DownloadClientItem> downloadClientHistory = new List<DownloadClientItem>();
|
||||
var downloadClientItems = new List<DownloadClientItem>();
|
||||
var trackedDownloads = new List<TrackedDownload>();
|
||||
|
||||
try
|
||||
{
|
||||
downloadClientHistory = downloadClient.GetItems().ToList();
|
||||
downloadClientItems = downloadClient.GetItems().ToList();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warn(ex, "Unable to retrieve queue and history items from " + downloadClient.Definition.Name);
|
||||
}
|
||||
|
||||
foreach (var downloadItem in downloadClientHistory)
|
||||
foreach (var downloadItem in downloadClientItems)
|
||||
{
|
||||
var newItems = ProcessClientItems(downloadClient, downloadItem);
|
||||
trackedDownloads.AddRange(newItems);
|
||||
}
|
||||
|
||||
if (_configService.EnableCompletedDownloadHandling && _configService.RemoveCompletedDownloads)
|
||||
{
|
||||
RemoveCompletedDownloads(trackedDownloads);
|
||||
var item = ProcessClientItem(downloadClient, downloadItem);
|
||||
trackedDownloads.AddIfNotNull(item);
|
||||
}
|
||||
|
||||
return trackedDownloads;
|
||||
}
|
||||
|
||||
private void RemoveCompletedDownloads(List<TrackedDownload> trackedDownloads)
|
||||
private TrackedDownload ProcessClientItem(IDownloadClient downloadClient, DownloadClientItem downloadItem)
|
||||
{
|
||||
foreach (var trackedDownload in trackedDownloads.Where(c => c.DownloadItem.CanBeRemoved && c.State == TrackedDownloadStage.Imported))
|
||||
{
|
||||
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
|
||||
}
|
||||
}
|
||||
|
||||
private List<TrackedDownload> ProcessClientItems(IDownloadClient downloadClient, DownloadClientItem downloadItem)
|
||||
{
|
||||
var trackedDownloads = new List<TrackedDownload>();
|
||||
try
|
||||
{
|
||||
var trackedDownload = _trackedDownloadService.TrackDownload((DownloadClientDefinition)downloadClient.Definition, downloadItem);
|
||||
if (trackedDownload != null && trackedDownload.State == TrackedDownloadStage.Downloading)
|
||||
if (trackedDownload != null && trackedDownload.State == TrackedDownloadState.Downloading)
|
||||
{
|
||||
_failedDownloadService.Process(trackedDownload);
|
||||
|
||||
if (_configService.EnableCompletedDownloadHandling)
|
||||
{
|
||||
_completedDownloadService.Process(trackedDownload);
|
||||
}
|
||||
_failedDownloadService.Check(trackedDownload);
|
||||
_completedDownloadService.Check(trackedDownload);
|
||||
}
|
||||
|
||||
trackedDownloads.AddIfNotNull(trackedDownload);
|
||||
return trackedDownload;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Error(e, "Couldn't process tracked download {0}", downloadItem.Title);
|
||||
}
|
||||
|
||||
return trackedDownloads;
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool DownloadIsTrackable(TrackedDownload trackedDownload)
|
||||
{
|
||||
// If the download has already been imported or failed don't track it
|
||||
if (trackedDownload.State != TrackedDownloadStage.Downloading)
|
||||
// If the download has already been imported, failed or the user ignored it don't track it
|
||||
if (trackedDownload.State == TrackedDownloadState.Imported ||
|
||||
trackedDownload.State == TrackedDownloadState.Failed ||
|
||||
trackedDownload.State == TrackedDownloadState.Ignored)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@ -156,8 +143,14 @@ private bool DownloadIsTrackable(TrackedDownload trackedDownload)
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Execute(RefreshMonitoredDownloadsCommand message)
|
||||
{
|
||||
Refresh();
|
||||
}
|
||||
|
||||
public void Execute(CheckForFinishedDownloadCommand message)
|
||||
{
|
||||
_logger.Warn("A third party app used the deprecated CheckForFinishedDownload command, it should be updated RefreshMonitoredDownloads instead");
|
||||
Refresh();
|
||||
}
|
||||
|
||||
@ -171,6 +164,13 @@ public void Handle(MovieImportedEvent message)
|
||||
_refreshDebounce.Execute();
|
||||
}
|
||||
|
||||
public void Handle(DownloadsProcessedEvent message)
|
||||
{
|
||||
var trackedDownloads = _trackedDownloadService.GetTrackedDownloads().Where(t => t.IsTrackable && DownloadIsTrackable(t)).ToList();
|
||||
|
||||
_eventAggregator.PublishEvent(new TrackedDownloadRefreshedEvent(trackedDownloads));
|
||||
}
|
||||
|
||||
public void Handle(TrackedDownloadsRemovedEvent message)
|
||||
{
|
||||
var trackedDownloads = _trackedDownloadService.GetTrackedDownloads().Where(t => t.IsTrackable && DownloadIsTrackable(t)).ToList();
|
||||
|
@ -7,7 +7,7 @@ public class TrackedDownload
|
||||
{
|
||||
public int DownloadClient { get; set; }
|
||||
public DownloadClientItem DownloadItem { get; set; }
|
||||
public TrackedDownloadStage State { get; set; }
|
||||
public TrackedDownloadState State { get; set; }
|
||||
public TrackedDownloadStatus Status { get; private set; }
|
||||
public RemoteMovie RemoteMovie { get; set; }
|
||||
public TrackedDownloadStatusMessage[] StatusMessages { get; private set; }
|
||||
@ -33,16 +33,21 @@ public void Warn(params TrackedDownloadStatusMessage[] statusMessages)
|
||||
}
|
||||
}
|
||||
|
||||
public enum TrackedDownloadStage
|
||||
public enum TrackedDownloadState
|
||||
{
|
||||
Downloading,
|
||||
ImportPending,
|
||||
Importing,
|
||||
Imported,
|
||||
DownloadFailed
|
||||
FailedPending,
|
||||
Failed,
|
||||
Ignored
|
||||
}
|
||||
|
||||
public enum TrackedDownloadStatus
|
||||
{
|
||||
Ok,
|
||||
Warning
|
||||
Warning,
|
||||
Error
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.History;
|
||||
|
||||
namespace NzbDrone.Core.Download.TrackedDownloads
|
||||
{
|
||||
public interface ITrackedDownloadAlreadyImported
|
||||
{
|
||||
bool IsImported(TrackedDownload trackedDownload, List<History.History> historyItems);
|
||||
}
|
||||
|
||||
public class TrackedDownloadAlreadyImported : ITrackedDownloadAlreadyImported
|
||||
{
|
||||
public bool IsImported(TrackedDownload trackedDownload, List<History.History> historyItems)
|
||||
{
|
||||
if (historyItems.Empty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var movie = trackedDownload.RemoteMovie.Movie;
|
||||
|
||||
var lastHistoryItem = historyItems.FirstOrDefault(h => h.MovieId == movie.Id);
|
||||
|
||||
if (lastHistoryItem == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var allEpisodesImportedInHistory = lastHistoryItem.EventType == HistoryEventType.DownloadFolderImported;
|
||||
|
||||
return allEpisodesImportedInHistory;
|
||||
}
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@ public class TrackedDownloadService : ITrackedDownloadService
|
||||
private readonly IParsingService _parsingService;
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly ITrackedDownloadAlreadyImported _trackedDownloadAlreadyImported;
|
||||
private readonly IConfigService _config;
|
||||
private readonly ICustomFormatCalculationService _formatCalculator;
|
||||
private readonly Logger _logger;
|
||||
@ -38,6 +39,7 @@ public TrackedDownloadService(IParsingService parsingService,
|
||||
IConfigService config,
|
||||
ICustomFormatCalculationService formatCalculator,
|
||||
IEventAggregator eventAggregator,
|
||||
ITrackedDownloadAlreadyImported trackedDownloadAlreadyImported,
|
||||
Logger logger)
|
||||
{
|
||||
_parsingService = parsingService;
|
||||
@ -46,6 +48,7 @@ public TrackedDownloadService(IParsingService parsingService,
|
||||
_config = config;
|
||||
_formatCalculator = formatCalculator;
|
||||
_eventAggregator = eventAggregator;
|
||||
_trackedDownloadAlreadyImported = trackedDownloadAlreadyImported;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -81,7 +84,7 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do
|
||||
{
|
||||
var existingItem = Find(downloadItem.DownloadId);
|
||||
|
||||
if (existingItem != null && existingItem.State != TrackedDownloadStage.Downloading)
|
||||
if (existingItem != null && existingItem.State != TrackedDownloadState.Downloading)
|
||||
{
|
||||
LogItemChange(existingItem, existingItem.DownloadItem, downloadItem);
|
||||
|
||||
@ -101,9 +104,10 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do
|
||||
|
||||
try
|
||||
{
|
||||
var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId);
|
||||
var grabbedHistoryItem = historyItems.OrderByDescending(h => h.Date).FirstOrDefault(h => h.EventType == HistoryEventType.Grabbed);
|
||||
var firstHistoryItem = historyItems.OrderByDescending(h => h.Date).FirstOrDefault();
|
||||
var historyItems = _historyService.FindByDownloadId(downloadItem.DownloadId)
|
||||
.OrderByDescending(h => h.Date)
|
||||
.ToList();
|
||||
var grabbedHistoryItem = historyItems.FirstOrDefault(h => h.EventType == HistoryEventType.Grabbed);
|
||||
|
||||
//TODO: Create release info from history and use that here, so we don't loose indexer flags!
|
||||
var parsedMovieInfo = _parsingService.ParseMovieInfo(trackedDownload.DownloadItem.Title, new List<object> { grabbedHistoryItem });
|
||||
@ -113,10 +117,27 @@ public TrackedDownload TrackDownload(DownloadClientDefinition downloadClient, Do
|
||||
trackedDownload.RemoteMovie = _parsingService.Map(parsedMovieInfo, "", null).RemoteMovie;
|
||||
}
|
||||
|
||||
if (firstHistoryItem != null)
|
||||
if (historyItems.Any())
|
||||
{
|
||||
trackedDownload.State = GetStateFromHistory(firstHistoryItem.EventType);
|
||||
var firstHistoryItem = historyItems.FirstOrDefault();
|
||||
var state = GetStateFromHistory(firstHistoryItem.EventType);
|
||||
|
||||
trackedDownload.State = state;
|
||||
|
||||
// TODO: Restore check to confirm all files were imported
|
||||
// This will treat partially imported downloads as imported (as it was before), which means a partially imported download after a
|
||||
// restart will get marked as imported without importing the restart of the files.
|
||||
|
||||
//if (state == TrackedDownloadState.Imported)
|
||||
//{
|
||||
// var allImported = _trackedDownloadAlreadyImported.IsImported(trackedDownload, historyItems);
|
||||
|
||||
// trackedDownload.State = allImported ? TrackedDownloadState.Imported : TrackedDownloadState.Downloading;
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// trackedDownload.State = state;
|
||||
//}
|
||||
var grabbedEvent = historyItems.FirstOrDefault(v => v.EventType == HistoryEventType.Grabbed);
|
||||
trackedDownload.Indexer = grabbedEvent?.Data["indexer"];
|
||||
|
||||
@ -190,16 +211,18 @@ private void LogItemChange(TrackedDownload trackedDownload, DownloadClientItem e
|
||||
}
|
||||
}
|
||||
|
||||
private static TrackedDownloadStage GetStateFromHistory(HistoryEventType eventType)
|
||||
private static TrackedDownloadState GetStateFromHistory(HistoryEventType eventType)
|
||||
{
|
||||
switch (eventType)
|
||||
{
|
||||
case HistoryEventType.DownloadFolderImported:
|
||||
return TrackedDownloadStage.Imported;
|
||||
return TrackedDownloadState.Imported;
|
||||
case HistoryEventType.DownloadFailed:
|
||||
return TrackedDownloadStage.DownloadFailed;
|
||||
return TrackedDownloadState.Failed;
|
||||
case HistoryEventType.DownloadIgnored:
|
||||
return TrackedDownloadState.Ignored;
|
||||
default:
|
||||
return TrackedDownloadStage.Downloading;
|
||||
return TrackedDownloadState.Downloading;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,6 +40,7 @@ public enum HistoryEventType
|
||||
// EpisodeFileDeleted = 5, // deprecated
|
||||
MovieFileDeleted = 6,
|
||||
MovieFolderImported = 7, // not used yet
|
||||
MovieFileRenamed = 8
|
||||
MovieFileRenamed = 8,
|
||||
DownloadIgnored = 9
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,8 @@ public class HistoryService : IHistoryService,
|
||||
IHandle<DownloadFailedEvent>,
|
||||
IHandle<MovieFileDeletedEvent>,
|
||||
IHandle<MovieFileRenamedEvent>,
|
||||
IHandle<MovieDeletedEvent>
|
||||
IHandle<MovieDeletedEvent>,
|
||||
IHandle<DownloadIgnoredEvent>
|
||||
{
|
||||
private readonly IHistoryRepository _historyRepository;
|
||||
private readonly Logger _logger;
|
||||
@ -223,6 +224,25 @@ public void Handle(MovieFileRenamedEvent message)
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
|
||||
public void Handle(DownloadIgnoredEvent message)
|
||||
{
|
||||
var history = new History
|
||||
{
|
||||
EventType = HistoryEventType.DownloadIgnored,
|
||||
Date = DateTime.UtcNow,
|
||||
Quality = message.Quality,
|
||||
SourceTitle = message.SourceTitle,
|
||||
MovieId = message.MovieId,
|
||||
DownloadId = message.DownloadId,
|
||||
Languages = message.Languages
|
||||
};
|
||||
|
||||
history.Data.Add("DownloadClient", message.DownloadClient);
|
||||
history.Data.Add("Message", message.Message);
|
||||
|
||||
_historyRepository.Insert(history);
|
||||
}
|
||||
|
||||
public void Handle(MovieDeletedEvent message)
|
||||
{
|
||||
_historyRepository.DeleteForMovie(message.Movie.Id);
|
||||
|
@ -102,7 +102,7 @@ public void Handle(ApplicationStartedEvent message)
|
||||
new ScheduledTask
|
||||
{
|
||||
Interval = Math.Max(_configService.CheckForFinishedDownloadInterval, 1),
|
||||
TypeName = typeof(CheckForFinishedDownloadCommand).FullName
|
||||
TypeName = typeof(RefreshMonitoredDownloadsCommand).FullName
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -83,6 +83,8 @@ public List<ImportResult> ProcessRootFolder(DirectoryInfo directoryInfo)
|
||||
|
||||
public List<ImportResult> ProcessPath(string path, ImportMode importMode = ImportMode.Auto, Movie movie = null, DownloadClientItem downloadClientItem = null)
|
||||
{
|
||||
_logger.Debug("Processing path: {0}", path);
|
||||
|
||||
if (_diskProvider.FolderExists(path))
|
||||
{
|
||||
var directoryInfo = new DirectoryInfo(path);
|
||||
@ -113,34 +115,42 @@ public List<ImportResult> ProcessPath(string path, ImportMode importMode = Impor
|
||||
|
||||
public bool ShouldDeleteFolder(DirectoryInfo directoryInfo, Movie movie)
|
||||
{
|
||||
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
|
||||
var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f).Equals(".rar", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
foreach (var videoFile in videoFiles)
|
||||
{
|
||||
var episodeParseResult =
|
||||
Parser.Parser.ParseMovieTitle(Path.GetFileName(videoFile), _config.ParsingLeniency > 0);
|
||||
|
||||
if (episodeParseResult == null)
|
||||
try
|
||||
{
|
||||
_logger.Warn("Unable to parse file on import: [{0}]", videoFile);
|
||||
var videoFiles = _diskScanService.GetVideoFiles(directoryInfo.FullName);
|
||||
var rarFiles = _diskProvider.GetFiles(directoryInfo.FullName, SearchOption.AllDirectories).Where(f => Path.GetExtension(f).Equals(".rar", StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
foreach (var videoFile in videoFiles)
|
||||
{
|
||||
var movieParseResult =
|
||||
Parser.Parser.ParseMovieTitle(Path.GetFileName(videoFile), _config.ParsingLeniency > 0);
|
||||
|
||||
if (movieParseResult == null)
|
||||
{
|
||||
_logger.Warn("Unable to parse file on import: [{0}]", videoFile);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_detectSample.IsSample(movie, videoFile, false) != DetectSampleResult.Sample)
|
||||
{
|
||||
_logger.Warn("Non-sample file detected: [{0}]", videoFile);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (rarFiles.Any(f => _diskProvider.GetFileSize(f) > 10.Megabytes()))
|
||||
{
|
||||
_logger.Warn("RAR file detected, will require manual cleanup");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_detectSample.IsSample(movie, videoFile, false) != DetectSampleResult.Sample)
|
||||
{
|
||||
_logger.Warn("Non-sample file detected: [{0}]", videoFile);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rarFiles.Any(f => _diskProvider.GetFileSize(f) > 10.Megabytes()))
|
||||
catch (DirectoryNotFoundException e)
|
||||
{
|
||||
_logger.Warn("RAR file detected, will require manual cleanup");
|
||||
_logger.Debug(e, "Folder {0} has already been removed", directoryInfo.FullName);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private List<ImportResult> ProcessFolder(DirectoryInfo directoryInfo, ImportMode importMode, DownloadClientItem downloadClientItem)
|
||||
@ -282,6 +292,12 @@ private void LogInaccessiblePathError(string path)
|
||||
var mounts = _diskProvider.GetMounts();
|
||||
var mount = mounts.FirstOrDefault(m => m.RootDirectory == Path.GetPathRoot(path));
|
||||
|
||||
if (mount == null)
|
||||
{
|
||||
_logger.Error("Import failed, path does not exist or is not accessible by Radarr: {0}. Unable to find a volume mounted for the path. If you're using a mapped network drive see the FAQ for more info", path);
|
||||
return;
|
||||
}
|
||||
|
||||
if (mount.DriveType == DriveType.Network)
|
||||
{
|
||||
_logger.Error("Import failed, path does not exist or is not accessible by Radarr: {0}. It's recommended to avoid mapped network drives when running as a Windows service. See the FAQ for more info", path);
|
||||
|
@ -314,7 +314,7 @@ public void Execute(ManualImportCommand message)
|
||||
//TODO: trackedDownload.RemoteMovie.Movie.Count is always 1?
|
||||
if (groupedTrackedDownload.Select(c => c.ImportResult).Count(c => c.Result == ImportResultType.Imported) >= Math.Max(1, 1))
|
||||
{
|
||||
trackedDownload.State = TrackedDownloadStage.Imported;
|
||||
trackedDownload.State = TrackedDownloadState.Imported;
|
||||
_eventAggregator.PublishEvent(new DownloadCompletedEvent(trackedDownload));
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,65 @@
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.History;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
|
||||
namespace NzbDrone.Core.MediaFiles.MovieImport.Specifications
|
||||
{
|
||||
public class AlreadyImportedSpecification : IImportDecisionEngineSpecification
|
||||
{
|
||||
private readonly IHistoryService _historyService;
|
||||
private readonly Logger _logger;
|
||||
|
||||
public AlreadyImportedSpecification(IHistoryService historyService,
|
||||
Logger logger)
|
||||
{
|
||||
_historyService = historyService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Database;
|
||||
|
||||
public Decision IsSatisfiedBy(LocalMovie localMovie, DownloadClientItem downloadClientItem)
|
||||
{
|
||||
if (downloadClientItem == null)
|
||||
{
|
||||
_logger.Debug("No download client information is available, skipping");
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
var movie = localMovie.Movie;
|
||||
|
||||
if (!movie.HasFile)
|
||||
{
|
||||
_logger.Debug("Skipping already imported check for movie without file");
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
var movieImportedHistory = _historyService.GetByMovieId(movie.Id, null);
|
||||
var lastImported = movieImportedHistory.FirstOrDefault(h => h.EventType == HistoryEventType.DownloadFolderImported);
|
||||
var lastGrabbed = movieImportedHistory.FirstOrDefault(h => h.EventType == HistoryEventType.Grabbed);
|
||||
|
||||
if (lastImported == null)
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
// If the release was grabbed again after importing don't reject it
|
||||
if (lastGrabbed != null && lastGrabbed.Date.After(lastImported.Date))
|
||||
{
|
||||
return Decision.Accept();
|
||||
}
|
||||
|
||||
if (lastImported.DownloadId == downloadClientItem.DownloadId)
|
||||
{
|
||||
_logger.Debug("Movie file previously imported at {0}", lastImported.Date);
|
||||
return Decision.Reject("Movie file already imported at {0}", lastImported.Date);
|
||||
}
|
||||
|
||||
return Decision.Accept();
|
||||
}
|
||||
}
|
||||
}
|
@ -20,16 +20,12 @@ public class EventAggregator : IEventAggregator
|
||||
private class EventSubscribers<TEvent>
|
||||
where TEvent : class, IEvent
|
||||
{
|
||||
private IServiceFactory _serviceFactory;
|
||||
|
||||
public IHandle<TEvent>[] _syncHandlers;
|
||||
public IHandleAsync<TEvent>[] _asyncHandlers;
|
||||
public IHandleAsync<IEvent>[] _globalHandlers;
|
||||
|
||||
public EventSubscribers(IServiceFactory serviceFactory)
|
||||
{
|
||||
_serviceFactory = serviceFactory;
|
||||
|
||||
_syncHandlers = serviceFactory.BuildAll<IHandle<TEvent>>()
|
||||
.OrderBy(GetEventHandleOrder)
|
||||
.ToArray();
|
||||
@ -142,8 +138,7 @@ private static string GetEventName(Type eventType)
|
||||
internal static int GetEventHandleOrder<TEvent>(IHandle<TEvent> eventHandler)
|
||||
where TEvent : class, IEvent
|
||||
{
|
||||
// TODO: Convert "Handle" to nameof(eventHandler.Handle) after .net 4.5
|
||||
var method = eventHandler.GetType().GetMethod("Handle", new Type[] { typeof(TEvent) });
|
||||
var method = eventHandler.GetType().GetMethod(nameof(eventHandler.Handle), new Type[] { typeof(TEvent) });
|
||||
|
||||
if (method == null)
|
||||
{
|
||||
|
@ -21,7 +21,8 @@ public class Queue : ModelBase
|
||||
public TimeSpan? Timeleft { get; set; }
|
||||
public DateTime? EstimatedCompletionTime { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string TrackedDownloadStatus { get; set; }
|
||||
public TrackedDownloadStatus? TrackedDownloadStatus { get; set; }
|
||||
public TrackedDownloadState? TrackedDownloadState { get; set; }
|
||||
public List<TrackedDownloadStatusMessage> StatusMessages { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
public RemoteMovie RemoteMovie { get; set; }
|
||||
|
@ -65,7 +65,8 @@ private Queue MapMovie(TrackedDownload trackedDownload, Movie movie)
|
||||
Sizeleft = trackedDownload.DownloadItem.RemainingSize,
|
||||
Timeleft = trackedDownload.DownloadItem.RemainingTime,
|
||||
Status = trackedDownload.DownloadItem.Status.ToString(),
|
||||
TrackedDownloadStatus = trackedDownload.Status.ToString(),
|
||||
TrackedDownloadStatus = trackedDownload.Status,
|
||||
TrackedDownloadState = trackedDownload.State,
|
||||
StatusMessages = trackedDownload.StatusMessages.ToList(),
|
||||
ErrorMessage = trackedDownload.DownloadItem.Message,
|
||||
RemoteMovie = trackedDownload.RemoteMovie,
|
||||
|
@ -93,7 +93,7 @@ public void should_return_same_instance_of_singletons_by_different_same_interfac
|
||||
public void should_return_same_instance_of_singletons_by_different_interfaces()
|
||||
{
|
||||
var first = _container.ResolveAll<IHandle<MovieGrabbedEvent>>().OfType<DownloadMonitoringService>().Single();
|
||||
var second = (DownloadMonitoringService)_container.Resolve<IExecute<CheckForFinishedDownloadCommand>>();
|
||||
var second = (DownloadMonitoringService)_container.Resolve<IExecute<RefreshMonitoredDownloadsCommand>>();
|
||||
|
||||
first.Should().BeSameAs(second);
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ public class QueueActionModule : RadarrRestModule<QueueResource>
|
||||
private readonly IQueueService _queueService;
|
||||
private readonly ITrackedDownloadService _trackedDownloadService;
|
||||
private readonly IFailedDownloadService _failedDownloadService;
|
||||
private readonly IIgnoredDownloadService _ignoredDownloadService;
|
||||
private readonly IProvideDownloadClient _downloadClientProvider;
|
||||
private readonly IPendingReleaseService _pendingReleaseService;
|
||||
private readonly IDownloadService _downloadService;
|
||||
@ -22,6 +23,7 @@ public class QueueActionModule : RadarrRestModule<QueueResource>
|
||||
public QueueActionModule(IQueueService queueService,
|
||||
ITrackedDownloadService trackedDownloadService,
|
||||
IFailedDownloadService failedDownloadService,
|
||||
IIgnoredDownloadService ignoredDownloadService,
|
||||
IProvideDownloadClient downloadClientProvider,
|
||||
IPendingReleaseService pendingReleaseService,
|
||||
IDownloadService downloadService)
|
||||
@ -29,6 +31,7 @@ public QueueActionModule(IQueueService queueService,
|
||||
_queueService = queueService;
|
||||
_trackedDownloadService = trackedDownloadService;
|
||||
_failedDownloadService = failedDownloadService;
|
||||
_ignoredDownloadService = ignoredDownloadService;
|
||||
_downloadClientProvider = downloadClientProvider;
|
||||
_pendingReleaseService = pendingReleaseService;
|
||||
_downloadService = downloadService;
|
||||
@ -75,9 +78,10 @@ private object Grab()
|
||||
|
||||
private object Remove(int id)
|
||||
{
|
||||
var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true);
|
||||
var blacklist = Request.GetBooleanQueryParameter("blacklist");
|
||||
|
||||
var trackedDownload = Remove(id, blacklist);
|
||||
var trackedDownload = Remove(id, removeFromClient, blacklist);
|
||||
|
||||
if (trackedDownload != null)
|
||||
{
|
||||
@ -89,6 +93,7 @@ private object Remove(int id)
|
||||
|
||||
private object Remove()
|
||||
{
|
||||
var removeFromClient = Request.GetBooleanQueryParameter("removeFromClient", true);
|
||||
var blacklist = Request.GetBooleanQueryParameter("blacklist");
|
||||
|
||||
var resource = Request.Body.FromJson<QueueBulkResource>();
|
||||
@ -96,7 +101,7 @@ private object Remove()
|
||||
|
||||
foreach (var id in resource.Ids)
|
||||
{
|
||||
var trackedDownload = Remove(id, blacklist);
|
||||
var trackedDownload = Remove(id, removeFromClient, blacklist);
|
||||
|
||||
if (trackedDownload != null)
|
||||
{
|
||||
@ -109,7 +114,7 @@ private object Remove()
|
||||
return new object();
|
||||
}
|
||||
|
||||
private TrackedDownload Remove(int id, bool blacklist)
|
||||
private TrackedDownload Remove(int id, bool removeFromClient, bool blacklist)
|
||||
{
|
||||
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
|
||||
|
||||
@ -127,20 +132,31 @@ private TrackedDownload Remove(int id, bool blacklist)
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
|
||||
|
||||
if (downloadClient == null)
|
||||
if (removeFromClient)
|
||||
{
|
||||
throw new BadRequestException();
|
||||
}
|
||||
var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
|
||||
|
||||
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true);
|
||||
if (downloadClient == null)
|
||||
{
|
||||
throw new BadRequestException();
|
||||
}
|
||||
|
||||
downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadId, true);
|
||||
}
|
||||
|
||||
if (blacklist)
|
||||
{
|
||||
_failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId);
|
||||
}
|
||||
|
||||
if (!removeFromClient && !blacklist)
|
||||
{
|
||||
if (!_ignoredDownloadService.IgnoreDownload(trackedDownload))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return trackedDownload;
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Indexers;
|
||||
using NzbDrone.Core.Languages;
|
||||
@ -24,7 +25,8 @@ public class QueueResource : RestResource
|
||||
public TimeSpan? Timeleft { get; set; }
|
||||
public DateTime? EstimatedCompletionTime { get; set; }
|
||||
public string Status { get; set; }
|
||||
public string TrackedDownloadStatus { get; set; }
|
||||
public TrackedDownloadStatus? TrackedDownloadStatus { get; set; }
|
||||
public TrackedDownloadState? TrackedDownloadState { get; set; }
|
||||
public List<TrackedDownloadStatusMessage> StatusMessages { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public string DownloadId { get; set; }
|
||||
@ -56,8 +58,9 @@ public static QueueResource ToResource(this NzbDrone.Core.Queue.Queue model, boo
|
||||
Sizeleft = model.Sizeleft,
|
||||
Timeleft = model.Timeleft,
|
||||
EstimatedCompletionTime = model.EstimatedCompletionTime,
|
||||
Status = model.Status,
|
||||
Status = model.Status.FirstCharToLower(),
|
||||
TrackedDownloadStatus = model.TrackedDownloadStatus,
|
||||
TrackedDownloadState = model.TrackedDownloadState,
|
||||
StatusMessages = model.StatusMessages,
|
||||
ErrorMessage = model.ErrorMessage,
|
||||
DownloadId = model.DownloadId,
|
||||
|
@ -3,6 +3,7 @@
|
||||
using NzbDrone.Common.TPL;
|
||||
using NzbDrone.Core.Datastore.Events;
|
||||
using NzbDrone.Core.Download.Pending;
|
||||
using NzbDrone.Core.Download.TrackedDownloads;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
using NzbDrone.Core.Queue;
|
||||
using NzbDrone.SignalR;
|
||||
@ -45,10 +46,10 @@ private QueueStatusResource GetQueueStatus()
|
||||
TotalCount = queue.Count + pending.Count,
|
||||
Count = queue.Count(q => q.Movie != null) + pending.Count,
|
||||
UnknownCount = queue.Count(q => q.Movie == null),
|
||||
Errors = queue.Any(q => q.Movie != null && q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)),
|
||||
Warnings = queue.Any(q => q.Movie != null && q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase)),
|
||||
UnknownErrors = queue.Any(q => q.Movie == null && q.TrackedDownloadStatus.Equals("Error", StringComparison.InvariantCultureIgnoreCase)),
|
||||
UnknownWarnings = queue.Any(q => q.Movie == null && q.TrackedDownloadStatus.Equals("Warning", StringComparison.InvariantCultureIgnoreCase))
|
||||
Errors = queue.Any(q => q.Movie != null && q.TrackedDownloadStatus == TrackedDownloadStatus.Error),
|
||||
Warnings = queue.Any(q => q.Movie != null && q.TrackedDownloadStatus == TrackedDownloadStatus.Warning),
|
||||
UnknownErrors = queue.Any(q => q.Movie == null && q.TrackedDownloadStatus == TrackedDownloadStatus.Error),
|
||||
UnknownWarnings = queue.Any(q => q.Movie == null && q.TrackedDownloadStatus == TrackedDownloadStatus.Warning)
|
||||
};
|
||||
|
||||
_broadcastDebounce.Resume();
|
||||
|
Loading…
Reference in New Issue
Block a user