New: Optionally remove from queue by changing category to 'Post-Import Category' when configured

Closes #6023
This commit is contained in:
Mark McDowall 2024-01-22 20:56:35 -08:00 committed by GitHub
parent 31baed4b2c
commit 345854d0fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 317 additions and 383 deletions

View File

@ -25,7 +25,7 @@ import toggleSelected from 'Utilities/Table/toggleSelected';
import QueueFilterModal from './QueueFilterModal';
import QueueOptionsConnector from './QueueOptionsConnector';
import QueueRowConnector from './QueueRowConnector';
import RemoveQueueItemsModal from './RemoveQueueItemsModal';
import RemoveQueueItemModal from './RemoveQueueItemModal';
class Queue extends Component {
@ -305,9 +305,16 @@ class Queue extends Component {
}
</PageContentBody>
<RemoveQueueItemsModal
<RemoveQueueItemModal
isOpen={isConfirmRemoveModalOpen}
selectedCount={selectedCount}
canChangeCategory={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
return !!(item && item.downloadClientHasPostImportCategory);
})
)}
canIgnore={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);
@ -315,7 +322,7 @@ class Queue extends Component {
return !!(item && item.seriesId && item.episodeId);
})
)}
allPending={isConfirmRemoveModalOpen && (
pending={isConfirmRemoveModalOpen && (
selectedIds.every((id) => {
const item = items.find((i) => i.id === id);

View File

@ -99,6 +99,7 @@ class QueueRow extends Component {
indexer,
outputPath,
downloadClient,
downloadClientHasPostImportCategory,
estimatedCompletionTime,
added,
timeleft,
@ -420,6 +421,7 @@ class QueueRow extends Component {
<RemoveQueueItemModal
isOpen={isRemoveQueueItemModalOpen}
sourceTitle={title}
canChangeCategory={!!downloadClientHasPostImportCategory}
canIgnore={!!series}
isPending={isPending}
onRemovePress={this.onRemoveQueueItemModalConfirmed}
@ -450,6 +452,7 @@ QueueRow.propTypes = {
indexer: PropTypes.string,
outputPath: PropTypes.string,
downloadClient: PropTypes.string,
downloadClientHasPostImportCategory: PropTypes.bool,
estimatedCompletionTime: PropTypes.string,
added: PropTypes.string,
timeleft: PropTypes.string,

View File

@ -1,171 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class RemoveQueueItemModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
remove: true,
blocklist: false,
skipRedownload: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blocklist: false,
skipRedownload: false
});
};
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
//
// Render
render() {
const {
isOpen,
sourceTitle,
canIgnore,
isPending
} = this.props;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={this.onModalClose}
>
<ModalContent
onModalClose={this.onModalClose}
>
<ModalHeader>
{translate('RemoveQueueItem', { sourceTitle })}
</ModalHeader>
<ModalBody>
<div>
{translate('RemoveQueueItemConfirmation', { sourceTitle })}
</div>
{
isPending ?
null :
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>{translate('BlocklistRelease')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
{
blocklist ?
<FormGroup>
<FormLabel>{translate('SkipRedownload')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup> :
null
}
</ModalBody>
<ModalFooter>
<Button onPress={this.onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
RemoveQueueItemModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
sourceTitle: PropTypes.string.isRequired,
canIgnore: PropTypes.bool.isRequired,
isPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RemoveQueueItemModal;

View File

@ -0,0 +1,230 @@
import React, { useCallback, useMemo, useState } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemModal.css';
interface RemovePressProps {
remove: boolean;
changeCategory: boolean;
blocklist: boolean;
skipRedownload: boolean;
}
interface RemoveQueueItemModalProps {
isOpen: boolean;
sourceTitle: string;
canChangeCategory: boolean;
canIgnore: boolean;
isPending: boolean;
selectedCount?: number;
onRemovePress(props: RemovePressProps): void;
onModalClose: () => void;
}
type RemovalMethod = 'removeFromClient' | 'changeCategory' | 'ignore';
type BlocklistMethod =
| 'doNotBlocklist'
| 'blocklistAndSearch'
| 'blocklistOnly';
function RemoveQueueItemModal(props: RemoveQueueItemModalProps) {
const {
isOpen,
sourceTitle,
canIgnore,
canChangeCategory,
isPending,
selectedCount,
onRemovePress,
onModalClose,
} = props;
const multipleSelected = selectedCount && selectedCount > 1;
const [removalMethod, setRemovalMethod] =
useState<RemovalMethod>('removeFromClient');
const [blocklistMethod, setBlocklistMethod] =
useState<BlocklistMethod>('doNotBlocklist');
const { title, message } = useMemo(() => {
if (!selectedCount) {
return {
title: translate('RemoveQueueItem', { sourceTitle }),
message: translate('RemoveQueueItemConfirmation', { sourceTitle }),
};
}
if (selectedCount === 1) {
return {
title: translate('RemoveSelectedItem'),
message: translate('RemoveSelectedItemQueueMessageText'),
};
}
return {
title: translate('RemoveSelectedItems'),
message: translate('RemoveSelectedItemsQueueMessageText', {
selectedCount,
}),
};
}, [sourceTitle, selectedCount]);
const removalMethodOptions = useMemo(() => {
return [
{
key: 'removeFromClient',
value: translate('RemoveFromDownloadClient'),
hint: multipleSelected
? translate('RemoveMultipleFromDownloadClientHint')
: translate('RemoveFromDownloadClientHint'),
},
{
key: 'changeCategory',
value: translate('ChangeCategory'),
isDisabled: !canChangeCategory,
hint: multipleSelected
? translate('ChangeCategoryMultipleHint')
: translate('ChangeCategoryHint'),
},
{
key: 'ignore',
value: multipleSelected
? translate('IgnoreDownloads')
: translate('IgnoreDownload'),
isDisabled: !canIgnore,
hint: multipleSelected
? translate('IgnoreDownloadsHint')
: translate('IgnoreDownloadHint'),
},
];
}, [canChangeCategory, canIgnore, multipleSelected]);
const blocklistMethodOptions = useMemo(() => {
return [
{
key: 'doNotBlocklist',
value: translate('DoNotBlocklist'),
hint: translate('DoNotBlocklistHint'),
},
{
key: 'blocklistAndSearch',
value: translate('BlocklistAndSearch'),
hint: multipleSelected
? translate('BlocklistAndSearchMultipleHint')
: translate('BlocklistAndSearchHint'),
},
{
key: 'blocklistOnly',
value: translate('BlocklistOnly'),
hint: multipleSelected
? translate('BlocklistMultipleOnlyHint')
: translate('BlocklistOnlyHint'),
},
];
}, [multipleSelected]);
const handleRemovalMethodChange = useCallback(
({ value }: { value: RemovalMethod }) => {
setRemovalMethod(value);
},
[setRemovalMethod]
);
const handleBlocklistMethodChange = useCallback(
({ value }: { value: BlocklistMethod }) => {
setBlocklistMethod(value);
},
[setBlocklistMethod]
);
const handleConfirmRemove = useCallback(() => {
onRemovePress({
remove: removalMethod === 'removeFromClient',
changeCategory: removalMethod === 'changeCategory',
blocklist: blocklistMethod !== 'doNotBlocklist',
skipRedownload: blocklistMethod === 'blocklistOnly',
});
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
}, [
removalMethod,
blocklistMethod,
setRemovalMethod,
setBlocklistMethod,
onRemovePress,
]);
const handleModalClose = useCallback(() => {
setRemovalMethod('removeFromClient');
setBlocklistMethod('doNotBlocklist');
onModalClose();
}, [setRemovalMethod, setBlocklistMethod, onModalClose]);
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={handleModalClose}>
<ModalContent onModalClose={handleModalClose}>
<ModalHeader>{title}</ModalHeader>
<ModalBody>
<div className={styles.message}>{message}</div>
{isPending ? null : (
<FormGroup>
<FormLabel>{translate('RemoveQueueItemRemovalMethod')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="removalMethod"
value={removalMethod}
values={removalMethodOptions}
isDisabled={!canChangeCategory && !canIgnore}
helpTextWarning={translate(
'RemoveQueueItemRemovalMethodHelpTextWarning'
)}
onChange={handleRemovalMethodChange}
/>
</FormGroup>
)}
<FormGroup>
<FormLabel>
{multipleSelected
? translate('BlocklistReleases')
: translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="blocklistMethod"
value={blocklistMethod}
values={blocklistMethodOptions}
helpText={translate('BlocklistReleaseHelpText')}
onChange={handleBlocklistMethodChange}
/>
</FormGroup>
</ModalBody>
<ModalFooter>
<Button onPress={handleModalClose}>{translate('Close')}</Button>
<Button kind={kinds.DANGER} onPress={handleConfirmRemove}>
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
export default RemoveQueueItemModal;

View File

@ -1,174 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import FormGroup from 'Components/Form/FormGroup';
import FormInputGroup from 'Components/Form/FormInputGroup';
import FormLabel from 'Components/Form/FormLabel';
import Button from 'Components/Link/Button';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds, sizes } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './RemoveQueueItemsModal.css';
class RemoveQueueItemsModal extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
remove: true,
blocklist: false,
skipRedownload: false
};
}
//
// Control
resetState = function() {
this.setState({
remove: true,
blocklist: false,
skipRedownload: false
});
};
//
// Listeners
onRemoveChange = ({ value }) => {
this.setState({ remove: value });
};
onBlocklistChange = ({ value }) => {
this.setState({ blocklist: value });
};
onSkipRedownloadChange = ({ value }) => {
this.setState({ skipRedownload: value });
};
onRemoveConfirmed = () => {
const state = this.state;
this.resetState();
this.props.onRemovePress(state);
};
onModalClose = () => {
this.resetState();
this.props.onModalClose();
};
//
// Render
render() {
const {
isOpen,
selectedCount,
canIgnore,
allPending
} = this.props;
const { remove, blocklist, skipRedownload } = this.state;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={this.onModalClose}
>
<ModalContent
onModalClose={this.onModalClose}
>
<ModalHeader>
{selectedCount > 1 ? translate('RemoveSelectedItems') : translate('RemoveSelectedItem')}
</ModalHeader>
<ModalBody>
<div className={styles.message}>
{selectedCount > 1 ? translate('RemoveSelectedItemsQueueMessageText', { selectedCount }) : translate('RemoveSelectedItemQueueMessageText')}
</div>
{
allPending ?
null :
<FormGroup>
<FormLabel>{translate('RemoveFromDownloadClient')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="remove"
value={remove}
helpTextWarning={translate('RemoveFromDownloadClientHelpTextWarning')}
isDisabled={!canIgnore}
onChange={this.onRemoveChange}
/>
</FormGroup>
}
<FormGroup>
<FormLabel>
{selectedCount > 1 ? translate('BlocklistReleases') : translate('BlocklistRelease')}
</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="blocklist"
value={blocklist}
helpText={translate('BlocklistReleaseSearchEpisodeAgainHelpText')}
onChange={this.onBlocklistChange}
/>
</FormGroup>
{
blocklist ?
<FormGroup>
<FormLabel>{translate('SkipRedownload')}</FormLabel>
<FormInputGroup
type={inputTypes.CHECK}
name="skipRedownload"
value={skipRedownload}
helpText={translate('SkipRedownloadHelpText')}
onChange={this.onSkipRedownloadChange}
/>
</FormGroup> :
null
}
</ModalBody>
<ModalFooter>
<Button onPress={this.onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onRemoveConfirmed}
>
{translate('Remove')}
</Button>
</ModalFooter>
</ModalContent>
</Modal>
);
}
}
RemoveQueueItemsModal.propTypes = {
isOpen: PropTypes.bool.isRequired,
selectedCount: PropTypes.number.isRequired,
canIgnore: PropTypes.bool.isRequired,
allPending: PropTypes.bool.isRequired,
onRemovePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default RemoveQueueItemsModal;

View File

@ -264,6 +264,7 @@ FormInputGroup.propTypes = {
name: PropTypes.string.isRequired,
value: PropTypes.any,
values: PropTypes.arrayOf(PropTypes.any),
isDisabled: PropTypes.bool,
type: PropTypes.string.isRequired,
kind: PropTypes.oneOf(kinds.all),
min: PropTypes.number,

View File

@ -430,13 +430,14 @@ export const actionHandlers = handleThunks({
id,
remove,
blocklist,
skipRedownload
skipRedownload,
changeCategory
} = payload;
dispatch(updateItem({ section: paged, id, isRemoving: true }));
const promise = createAjaxRequest({
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
url: `/queue/${id}?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
method: 'DELETE'
}).request;
@ -454,7 +455,8 @@ export const actionHandlers = handleThunks({
ids,
remove,
blocklist,
skipRedownload
skipRedownload,
changeCategory
} = payload;
dispatch(batchActions([
@ -470,7 +472,7 @@ export const actionHandlers = handleThunks({
]));
const promise = createAjaxRequest({
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}`,
url: `/queue/bulk?removeFromClient=${remove}&blocklist=${blocklist}&skipRedownload=${skipRedownload}&changeCategory=${changeCategory}`,
method: 'DELETE',
dataType: 'json',
contentType: 'application/json',

View File

@ -134,7 +134,7 @@ namespace NzbDrone.Core.Download.Clients.Aria2
CanMoveFiles = false,
CanBeRemoved = torrent.Status == "complete",
Category = null,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = torrent.InfoHash?.ToUpper(),
IsEncrypted = false,
Message = torrent.ErrorMessage,

View File

@ -91,7 +91,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
{
yield return new DownloadClientItem
{
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = Definition.Name + "_" + item.DownloadId,
Category = "sonarr",
Title = item.Title,

View File

@ -61,7 +61,7 @@ namespace NzbDrone.Core.Download.Clients.Blackhole
{
yield return new DownloadClientItem
{
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = Definition.Name + "_" + item.DownloadId,
Category = "sonarr",
Title = item.Title,

View File

@ -137,7 +137,7 @@ namespace NzbDrone.Core.Download.Clients.Deluge
item.Title = torrent.Name;
item.Category = Settings.TvCategory;
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace());
var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadPath));
item.OutputPath = outputPath + torrent.Name;

View File

@ -91,7 +91,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
var item = new DownloadClientItem()
{
Category = Settings.TvCategory,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = CreateDownloadId(torrent.Id, serialNumber),
Title = torrent.Title,
TotalSize = torrent.Size,

View File

@ -99,7 +99,7 @@ namespace NzbDrone.Core.Download.Clients.DownloadStation
var item = new DownloadClientItem()
{
Category = Settings.TvCategory,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = CreateDownloadId(nzb.Id, serialNumber),
Title = nzb.Title,
TotalSize = nzb.Size,

View File

@ -118,7 +118,7 @@ namespace NzbDrone.Core.Download.Clients.Flood
var item = new DownloadClientItem
{
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = torrent.Key,
Title = properties.Name,
OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(properties.Directory)),

View File

@ -75,7 +75,7 @@ namespace NzbDrone.Core.Download.Clients.FreeboxDownload
Category = Settings.Category,
Title = torrent.Name,
TotalSize = torrent.Size,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
RemainingSize = (long)(torrent.Size * (double)(1 - ((double)torrent.ReceivedPrct / 10000))),
RemainingTime = torrent.Eta <= 0 ? null : TimeSpan.FromSeconds(torrent.Eta),
SeedRatio = torrent.StopRatio <= 0 ? 0 : torrent.StopRatio / 100,

View File

@ -59,7 +59,7 @@ namespace NzbDrone.Core.Download.Clients.Hadouken
var item = new DownloadClientItem
{
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = torrent.InfoHash.ToUpper(),
OutputPath = outputPath + torrent.Name,
RemainingSize = torrent.TotalSize - torrent.DownloadedBytes,

View File

@ -58,7 +58,7 @@ namespace NzbDrone.Core.Download.Clients.NzbVortex
{
var queueItem = new DownloadClientItem();
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
queueItem.DownloadId = vortexQueueItem.AddUUID ?? vortexQueueItem.Id.ToString();
queueItem.Category = vortexQueueItem.GroupName;
queueItem.Title = vortexQueueItem.UiTitle;

View File

@ -73,7 +73,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
queueItem.Title = item.NzbName;
queueItem.TotalSize = totalSize;
queueItem.Category = item.Category;
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
queueItem.CanMoveFiles = true;
queueItem.CanBeRemoved = true;
@ -120,7 +120,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget
var historyItem = new DownloadClientItem();
var itemDir = item.FinalDir.IsNullOrWhiteSpace() ? item.DestDir : item.FinalDir;
historyItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
historyItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
historyItem.DownloadId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString();
historyItem.Title = item.Name;
historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo);

View File

@ -75,7 +75,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic
var historyItem = new DownloadClientItem
{
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = GetDownloadClientId(file),
Title = title,

View File

@ -231,7 +231,7 @@ namespace NzbDrone.Core.Download.Clients.QBittorrent
Category = torrent.Category.IsNotNullOrWhiteSpace() ? torrent.Category : torrent.Label,
Title = torrent.Name,
TotalSize = torrent.Size,
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace()),
RemainingSize = (long)(torrent.Size * (1.0 - torrent.Progress)),
RemainingTime = GetRemainingTime(torrent),
SeedRatio = torrent.Ratio

View File

@ -65,7 +65,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
}
var queueItem = new DownloadClientItem();
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
queueItem.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
queueItem.DownloadId = sabQueueItem.Id;
queueItem.Category = sabQueueItem.Category;
queueItem.Title = sabQueueItem.Title;
@ -120,7 +120,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd
var historyItem = new DownloadClientItem
{
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this),
DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false),
DownloadId = sabHistoryItem.Id,
Category = sabHistoryItem.Category,
Title = sabHistoryItem.Title,

View File

@ -74,7 +74,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission
item.Category = Settings.TvCategory;
item.Title = torrent.Name;
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, false);
item.OutputPath = GetOutputPath(outputPath, torrent);
item.TotalSize = torrent.TotalSize;

View File

@ -148,7 +148,7 @@ namespace NzbDrone.Core.Download.Clients.RTorrent
}
var item = new DownloadClientItem();
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace());
item.Title = torrent.Name;
item.DownloadId = torrent.Hash;
item.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.Path));

View File

@ -122,7 +122,7 @@ namespace NzbDrone.Core.Download.Clients.UTorrent
item.Title = torrent.Name;
item.TotalSize = torrent.Size;
item.Category = torrent.Label;
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this);
item.DownloadClientInfo = DownloadClientItemClientInfo.FromDownloadClient(this, Settings.TvImportedCategory.IsNotNullOrWhiteSpace());
item.RemainingSize = torrent.Remaining;
item.SeedRatio = torrent.Ratio;

View File

@ -37,9 +37,10 @@ namespace NzbDrone.Core.Download
public string Type { get; set; }
public int Id { get; set; }
public string Name { get; set; }
public bool HasPostImportCategory { get; set; }
public static DownloadClientItemClientInfo FromDownloadClient<TSettings>(
DownloadClientBase<TSettings> downloadClient)
DownloadClientBase<TSettings> downloadClient, bool hasPostImportCategory)
where TSettings : IProviderConfig, new()
{
return new DownloadClientItemClientInfo
@ -47,7 +48,8 @@ namespace NzbDrone.Core.Download
Protocol = downloadClient.Protocol,
Type = downloadClient.Name,
Id = downloadClient.Definition.Id,
Name = downloadClient.Definition.Name
Name = downloadClient.Definition.Name,
HasPostImportCategory = hasPostImportCategory
};
}
}

View File

@ -153,9 +153,15 @@
"BlackholeWatchFolder": "Watch Folder",
"BlackholeWatchFolderHelpText": "Folder from which {appName} should import completed downloads",
"Blocklist": "Blocklist",
"BlocklistAndSearch": "Blocklist and Search",
"BlocklistAndSearchHint": "Start a search for a replacement after blocklisting",
"BlocklistAndSearchMultipleHint": "Start searches for replacements after blocklisting",
"BlocklistLoadError": "Unable to load blocklist",
"BlocklistMultipleOnlyHint": "Blocklist without searching for replacements",
"BlocklistOnly": "Blocklist Only",
"BlocklistOnlyHint": "Blocklist without searching for a replacement",
"BlocklistRelease": "Blocklist Release",
"BlocklistReleaseSearchEpisodeAgainHelpText": "Starts a search for this episode again and prevents this release from being grabbed again",
"BlocklistReleaseHelpText": "Blocks this release from being redownloaded by {appName} via RSS or Automatic Search",
"BlocklistReleases": "Blocklist Releases",
"Branch": "Branch",
"BranchUpdate": "Branch to use to update {appName}",
@ -188,6 +194,9 @@
"CertificateValidation": "Certificate Validation",
"CertificateValidationHelpText": "Change how strict HTTPS certification validation is. Do not change unless you understand the risks.",
"Certification": "Certification",
"ChangeCategory": "Change Category",
"ChangeCategoryHint": "Changes download to the 'Post-Import Category' from Download Client",
"ChangeCategoryMultipleHint": "Changes downloads to the 'Post-Import Category' from Download Client",
"ChangeFileDate": "Change File Date",
"ChangeFileDateHelpText": "Change file date on import/rescan",
"CheckDownloadClientForDetails": "check download client for more details",
@ -377,6 +386,8 @@
"DisabledForLocalAddresses": "Disabled for Local Addresses",
"Discord": "Discord",
"DiskSpace": "Disk Space",
"DoNotBlocklist": "Do not Blocklist",
"DoNotBlocklistHint": "Remove without blocklisting",
"DoNotPrefer": "Do not Prefer",
"DoNotUpgradeAutomatically": "Do not Upgrade Automatically",
"Docker": "Docker",
@ -754,6 +765,10 @@
"IconForFinalesHelpText": "Show icon for series/season finales based on available episode information",
"IconForSpecials": "Icon for Specials",
"IconForSpecialsHelpText": "Show icon for special episodes (season 0)",
"IgnoreDownload": "Ignore Download",
"IgnoreDownloads": "Ignore Downloads",
"IgnoreDownloadHint": "Stops {appName} from processing this download further",
"IgnoreDownloadsHint": "Stops {appName} from processing these downloads further",
"Ignored": "Ignored",
"IgnoredAddresses": "Ignored Addresses",
"Images": "Images",
@ -1596,11 +1611,15 @@
"RemoveFailedDownloadsHelpText": "Remove failed downloads from download client history",
"RemoveFilter": "Remove filter",
"RemoveFromBlocklist": "Remove from Blocklist",
"RemoveFromDownloadClient": "Remove From Download Client",
"RemoveFromDownloadClientHelpTextWarning": "Removing will remove the download and the file(s) from the download client.",
"RemoveFromDownloadClient": "Remove from Download Client",
"RemoveFromDownloadClientHint": "Removes download and file(s) from download client",
"RemoveFromQueue": "Remove from queue",
"RemoveMultipleFromDownloadClientHint": "Removes downloads and files from download client",
"RemoveQueueItem": "Remove - {sourceTitle}",
"RemoveQueueItemRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the download and the file(s) from the download client.",
"RemoveQueueItemConfirmation": "Are you sure you want to remove '{sourceTitle}' from the queue?",
"RemoveQueueItemRemovalMethod": "Removal Method",
"RemoveQueueItemsRemovalMethodHelpTextWarning": "'Remove from Download Client' will remove the downloads and the files from the download client.",
"RemoveRootFolder": "Remove root folder",
"RemoveSelected": "Remove Selected",
"RemoveSelectedBlocklistMessageText": "Are you sure you want to remove the selected items from the blocklist?",

View File

@ -30,6 +30,7 @@ namespace NzbDrone.Core.Queue
public RemoteEpisode RemoteEpisode { get; set; }
public DownloadProtocol Protocol { get; set; }
public string DownloadClient { get; set; }
public bool DownloadClientHasPostImportCategory { get; set; }
public string Indexer { get; set; }
public string OutputPath { get; set; }
public string ErrorMessage { get; set; }

View File

@ -80,7 +80,8 @@ namespace NzbDrone.Core.Queue
DownloadClient = trackedDownload.DownloadItem.DownloadClientInfo.Name,
Indexer = trackedDownload.Indexer,
OutputPath = trackedDownload.DownloadItem.OutputPath.ToString(),
Added = trackedDownload.Added
Added = trackedDownload.Added,
DownloadClientHasPostImportCategory = trackedDownload.DownloadItem.DownloadClientInfo.HasPostImportCategory
};
queue.Id = HashConverter.GetHashInt31($"trackedDownload-{trackedDownload.DownloadClient}-{trackedDownload.DownloadItem.DownloadId}-ep{episode?.Id ?? 0}");

View File

@ -71,7 +71,7 @@ namespace Sonarr.Api.V3.Queue
}
[RestDeleteById]
public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false)
public void RemoveAction(int id, bool removeFromClient = true, bool blocklist = false, bool skipRedownload = false, bool changeCategory = false)
{
var pendingRelease = _pendingReleaseService.FindPendingQueueItem(id);
@ -89,12 +89,12 @@ namespace Sonarr.Api.V3.Queue
throw new NotFoundException();
}
Remove(trackedDownload, removeFromClient, blocklist, skipRedownload);
Remove(trackedDownload, removeFromClient, blocklist, skipRedownload, changeCategory);
_trackedDownloadService.StopTracking(trackedDownload.DownloadItem.DownloadId);
}
[HttpDelete("bulk")]
public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false)
public object RemoveMany([FromBody] QueueBulkResource resource, [FromQuery] bool removeFromClient = true, [FromQuery] bool blocklist = false, [FromQuery] bool skipRedownload = false, [FromQuery] bool changeCategory = false)
{
var trackedDownloadIds = new List<string>();
var pendingToRemove = new List<NzbDrone.Core.Queue.Queue>();
@ -125,7 +125,7 @@ namespace Sonarr.Api.V3.Queue
foreach (var trackedDownload in trackedToRemove.DistinctBy(t => t.DownloadItem.DownloadId))
{
Remove(trackedDownload, removeFromClient, blocklist, skipRedownload);
Remove(trackedDownload, removeFromClient, blocklist, skipRedownload, changeCategory);
trackedDownloadIds.Add(trackedDownload.DownloadItem.DownloadId);
}
@ -292,7 +292,7 @@ namespace Sonarr.Api.V3.Queue
_pendingReleaseService.RemovePendingQueueItems(pendingRelease.Id);
}
private TrackedDownload Remove(TrackedDownload trackedDownload, bool removeFromClient, bool blocklist, bool skipRedownload)
private TrackedDownload Remove(TrackedDownload trackedDownload, bool removeFromClient, bool blocklist, bool skipRedownload, bool changeCategory)
{
if (removeFromClient)
{
@ -305,13 +305,24 @@ namespace Sonarr.Api.V3.Queue
downloadClient.RemoveItem(trackedDownload.DownloadItem, true);
}
else if (changeCategory)
{
var downloadClient = _downloadClientProvider.Get(trackedDownload.DownloadClient);
if (downloadClient == null)
{
throw new BadRequestException();
}
downloadClient.MarkItemAsImported(trackedDownload.DownloadItem);
}
if (blocklist)
{
_failedDownloadService.MarkAsFailed(trackedDownload.DownloadItem.DownloadId, skipRedownload);
}
if (!removeFromClient && !blocklist)
if (!removeFromClient && !blocklist && !changeCategory)
{
if (!_ignoredDownloadService.IgnoreDownload(trackedDownload))
{

View File

@ -38,6 +38,7 @@ namespace Sonarr.Api.V3.Queue
public string DownloadId { get; set; }
public DownloadProtocol Protocol { get; set; }
public string DownloadClient { get; set; }
public bool DownloadClientHasPostImportCategory { get; set; }
public string Indexer { get; set; }
public string OutputPath { get; set; }
public bool EpisodeHasFile { get; set; }
@ -81,6 +82,7 @@ namespace Sonarr.Api.V3.Queue
DownloadId = model.DownloadId,
Protocol = model.Protocol,
DownloadClient = model.DownloadClient,
DownloadClientHasPostImportCategory = model.DownloadClientHasPostImportCategory,
Indexer = model.Indexer,
OutputPath = model.OutputPath,
EpisodeHasFile = model.Episode?.HasFile ?? false