mirror of
https://github.com/Radarr/Radarr.git
synced 2024-11-20 18:02:44 +01:00
Override release grab modal
New: Option to override release and grab New: Option to select download client when multiple of the same type are configured (cherry picked from commit 07f0fbf9a51d54e44681fd0f74df4e048bff561a)
This commit is contained in:
parent
07b69e665d
commit
9b4f80535e
@ -10,6 +10,7 @@ class DescriptionListItem extends Component {
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
titleClassName,
|
||||
descriptionClassName,
|
||||
title,
|
||||
@ -17,7 +18,7 @@ class DescriptionListItem extends Component {
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className={className}>
|
||||
<DescriptionListItemTitle
|
||||
className={titleClassName}
|
||||
>
|
||||
@ -35,6 +36,7 @@ class DescriptionListItem extends Component {
|
||||
}
|
||||
|
||||
DescriptionListItem.propTypes = {
|
||||
className: PropTypes.string,
|
||||
titleClassName: PropTypes.string,
|
||||
descriptionClassName: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
|
7
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
7
frontend/src/DownloadClient/DownloadProtocol.ts
Normal file
@ -0,0 +1,7 @@
|
||||
enum DownloadProtocol {
|
||||
Unknown = 'unknown',
|
||||
Usenet = 'usenet',
|
||||
Torrent = 'torrent',
|
||||
}
|
||||
|
||||
export default DownloadProtocol;
|
@ -43,6 +43,7 @@ import {
|
||||
faChevronCircleRight as fasChevronCircleRight,
|
||||
faChevronCircleUp as fasChevronCircleUp,
|
||||
faCircle as fasCircle,
|
||||
faCircleDown as fasCircleDown,
|
||||
faCloud as fasCloud,
|
||||
faCloudDownloadAlt as fasCloudDownloadAlt,
|
||||
faCog as fasCog,
|
||||
@ -135,6 +136,7 @@ export const CHECK_INDETERMINATE = fasMinus;
|
||||
export const CHECK_CIRCLE = fasCheckCircle;
|
||||
export const CHECK_SQUARE = fasSquareCheck;
|
||||
export const CIRCLE = fasCircle;
|
||||
export const CIRCLE_DOWN = fasCircleDown;
|
||||
export const CIRCLE_OUTLINE = farCircle;
|
||||
export const CLEAR = fasTrashAlt;
|
||||
export const CLIPBOARD = fasCopy;
|
||||
|
@ -32,7 +32,11 @@ const columns = [
|
||||
},
|
||||
{
|
||||
name: 'rejections',
|
||||
label: React.createElement(Icon, { name: icons.DANGER }),
|
||||
columnLabel: () => translate('Rejections'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.DANGER,
|
||||
title: () => translate('Rejections')
|
||||
}),
|
||||
isSortable: true,
|
||||
fixedSortDirection: sortDirections.ASCENDING,
|
||||
isVisible: true
|
||||
@ -88,6 +92,7 @@ const columns = [
|
||||
},
|
||||
{
|
||||
name: 'customFormatScore',
|
||||
columnLabel: () => translate('CustomFormatScore'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.SCORE,
|
||||
title: () => translate('CustomFormatScore')
|
||||
@ -97,7 +102,11 @@ const columns = [
|
||||
},
|
||||
{
|
||||
name: 'indexerFlags',
|
||||
label: React.createElement(Icon, { name: icons.FLAG }),
|
||||
columnLabel: () => translate('IndexerFlags'),
|
||||
label: React.createElement(Icon, {
|
||||
name: icons.FLAG,
|
||||
title: () => translate('IndexerFlags')
|
||||
}),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
}
|
||||
|
@ -16,11 +16,11 @@
|
||||
|
||||
.quality,
|
||||
.customFormat,
|
||||
.language {
|
||||
.languages {
|
||||
composes: cell;
|
||||
}
|
||||
|
||||
.language {
|
||||
.languages {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
@ -33,8 +33,7 @@
|
||||
}
|
||||
|
||||
.rejected,
|
||||
.indexerFlags,
|
||||
.download {
|
||||
.indexerFlags {
|
||||
composes: cell;
|
||||
|
||||
width: 50px;
|
||||
@ -70,3 +69,39 @@
|
||||
.blocklist {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.download {
|
||||
composes: cell;
|
||||
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.manualDownloadContent {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin: 0 2px;
|
||||
width: 22px;
|
||||
height: 20.39px;
|
||||
vertical-align: middle;
|
||||
line-height: 20.39px;
|
||||
|
||||
&:hover {
|
||||
color: var(--iconButtonHoverColor);
|
||||
}
|
||||
}
|
||||
|
||||
.interactiveIcon {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
left: 0;
|
||||
/* width: 100%; */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.downloadIcon {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 8px;
|
||||
/* width: 100%; */
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -7,10 +7,13 @@ interface CssExports {
|
||||
'customFormat': string;
|
||||
'customFormatScore': string;
|
||||
'download': string;
|
||||
'downloadIcon': string;
|
||||
'history': string;
|
||||
'indexer': string;
|
||||
'indexerFlags': string;
|
||||
'language': string;
|
||||
'interactiveIcon': string;
|
||||
'languages': string;
|
||||
'manualDownloadContent': string;
|
||||
'peers': string;
|
||||
'protocol': string;
|
||||
'quality': string;
|
||||
|
@ -1,365 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import Peers from './Peers';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
|
||||
function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return icons.SPINNER;
|
||||
} else if (isGrabbed) {
|
||||
return icons.DOWNLOADING;
|
||||
} else if (grabError) {
|
||||
return icons.DOWNLOADING;
|
||||
}
|
||||
|
||||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadKind(isGrabbed, grabError) {
|
||||
if (isGrabbed) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (grabError) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return translate('AddedToDownloadQueue');
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return translate('AddToDownloadQueue');
|
||||
}
|
||||
|
||||
class InteractiveSearchRow extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
isConfirmGrabModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onGrabPress = () => {
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId
|
||||
});
|
||||
};
|
||||
|
||||
onConfirmGrabPress = () => {
|
||||
this.setState({ isConfirmGrabModalOpen: true });
|
||||
};
|
||||
|
||||
onGrabConfirm = () => {
|
||||
this.setState({ isConfirmGrabModalOpen: false });
|
||||
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
searchPayload,
|
||||
onGrabPress
|
||||
} = this.props;
|
||||
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId,
|
||||
...searchPayload
|
||||
});
|
||||
};
|
||||
|
||||
onGrabCancel = () => {
|
||||
this.setState({ isConfirmGrabModalOpen: false });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
protocol,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
publishDate,
|
||||
title,
|
||||
infoUrl,
|
||||
indexer,
|
||||
size,
|
||||
seeders,
|
||||
leechers,
|
||||
quality,
|
||||
customFormats,
|
||||
customFormatScore,
|
||||
languages,
|
||||
indexerFlags,
|
||||
rejections,
|
||||
downloadAllowed,
|
||||
isGrabbing,
|
||||
isGrabbed,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
grabError,
|
||||
historyGrabbedData,
|
||||
historyFailedData,
|
||||
blocklistData
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.protocol}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.age}
|
||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, { includeSeconds: true })}
|
||||
>
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.download}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={getDownloadKind(isGrabbed, grabError)}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.rejected}>
|
||||
{
|
||||
!!rejections.length &&
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.DANGER}
|
||||
kind={kinds.DANGER}
|
||||
/>
|
||||
}
|
||||
title={translate('ReleaseRejected')}
|
||||
body={
|
||||
<ul>
|
||||
{
|
||||
rejections.map((rejection, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{rejection}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.title}>
|
||||
<Link
|
||||
to={infoUrl}
|
||||
title={title}
|
||||
>
|
||||
<div>
|
||||
{title}
|
||||
</div>
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexer}>
|
||||
{indexer}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.history}>
|
||||
{
|
||||
historyGrabbedData?.date && !historyFailedData?.date &&
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DEFAULT}
|
||||
title={`${translate('Grabbed')}: ${formatDateTime(historyGrabbedData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
historyFailedData?.date &&
|
||||
<Icon
|
||||
className={styles.failed}
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Failed')}: ${formatDateTime(historyFailedData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
|
||||
/>
|
||||
}
|
||||
|
||||
{
|
||||
blocklistData?.date &&
|
||||
<Icon
|
||||
className={historyGrabbedData || historyFailedData ? styles.blocklist : ''}
|
||||
name={icons.BLOCKLIST}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Blocklisted')}: ${formatDateTime(blocklistData.date, longDateFormat, timeFormat, { includeSeconds: true })}`}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.size}>
|
||||
{formatBytes(size)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.peers}>
|
||||
{
|
||||
protocol === 'torrent' &&
|
||||
<Peers
|
||||
seeders={seeders}
|
||||
leechers={leechers}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.language}>
|
||||
<MovieLanguage
|
||||
languages={languages}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.quality}>
|
||||
<MovieQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormat}>
|
||||
<MovieFormats
|
||||
formats={customFormats}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
{customFormatScore > 0 && `+${customFormatScore}`}
|
||||
{customFormatScore < 0 && customFormatScore}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexerFlags}>
|
||||
{
|
||||
!!indexerFlags.length &&
|
||||
<Popover
|
||||
anchor={
|
||||
<Icon
|
||||
name={icons.FLAG}
|
||||
kind={kinds.PRIMARY}
|
||||
/>
|
||||
}
|
||||
title={translate('IndexerFlags')}
|
||||
body={
|
||||
<ul>
|
||||
{
|
||||
indexerFlags.map((flag, index) => {
|
||||
return (
|
||||
<li key={index}>
|
||||
{flag}
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.BOTTOM}
|
||||
/>
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={this.state.isConfirmGrabModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('GrabRelease')}
|
||||
message={translate('GrabReleaseMessageText', [title])}
|
||||
confirmLabel={translate('Grab')}
|
||||
onConfirm={this.onGrabConfirm}
|
||||
onCancel={this.onGrabCancel}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
InteractiveSearchRow.propTypes = {
|
||||
guid: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
age: PropTypes.number.isRequired,
|
||||
ageHours: PropTypes.number.isRequired,
|
||||
ageMinutes: PropTypes.number.isRequired,
|
||||
publishDate: PropTypes.string.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
infoUrl: PropTypes.string.isRequired,
|
||||
indexerId: PropTypes.number.isRequired,
|
||||
indexer: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
seeders: PropTypes.number,
|
||||
leechers: PropTypes.number,
|
||||
quality: PropTypes.object.isRequired,
|
||||
customFormats: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFormatScore: PropTypes.number.isRequired,
|
||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
indexerFlags: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
downloadAllowed: PropTypes.bool.isRequired,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isGrabbed: PropTypes.bool.isRequired,
|
||||
grabError: PropTypes.string,
|
||||
longDateFormat: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
searchPayload: PropTypes.object.isRequired,
|
||||
onGrabPress: PropTypes.func.isRequired,
|
||||
historyFailedData: PropTypes.object,
|
||||
historyGrabbedData: PropTypes.object,
|
||||
blocklistData: PropTypes.object
|
||||
};
|
||||
|
||||
InteractiveSearchRow.defaultProps = {
|
||||
rejections: [],
|
||||
isGrabbing: false,
|
||||
isGrabbed: false
|
||||
};
|
||||
|
||||
export default InteractiveSearchRow;
|
378
frontend/src/InteractiveSearch/InteractiveSearchRow.tsx
Normal file
378
frontend/src/InteractiveSearch/InteractiveSearchRow.tsx
Normal file
@ -0,0 +1,378 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import SpinnerIconButton from 'Components/Link/SpinnerIconButton';
|
||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRow from 'Components/Table/TableRow';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import type DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import MovieFormats from 'Movie/MovieFormats';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from 'typings/CustomFormat';
|
||||
import MovieBlocklist from 'typings/MovieBlocklist';
|
||||
import MovieHistory from 'typings/MovieHistory';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatAge from 'Utilities/Number/formatAge';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatCustomFormatScore from 'Utilities/Number/formatCustomFormatScore';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
|
||||
import Peers from './Peers';
|
||||
import styles from './InteractiveSearchRow.css';
|
||||
|
||||
function getDownloadIcon(
|
||||
isGrabbing: boolean,
|
||||
isGrabbed: boolean,
|
||||
grabError?: string
|
||||
) {
|
||||
if (isGrabbing) {
|
||||
return icons.SPINNER;
|
||||
} else if (isGrabbed) {
|
||||
return icons.DOWNLOADING;
|
||||
} else if (grabError) {
|
||||
return icons.DOWNLOADING;
|
||||
}
|
||||
|
||||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadKind(isGrabbed: boolean, grabError?: string) {
|
||||
if (isGrabbed) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (grabError) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(
|
||||
isGrabbing: boolean,
|
||||
isGrabbed: boolean,
|
||||
grabError?: string
|
||||
) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
} else if (isGrabbed) {
|
||||
return translate('AddToDownloadQueue');
|
||||
} else if (grabError) {
|
||||
return grabError;
|
||||
}
|
||||
|
||||
return translate('AddedToDownloadQueue');
|
||||
}
|
||||
|
||||
interface InteractiveSearchRowProps {
|
||||
guid: string;
|
||||
protocol: DownloadProtocol;
|
||||
age: number;
|
||||
ageHours: number;
|
||||
ageMinutes: number;
|
||||
publishDate: string;
|
||||
title: string;
|
||||
infoUrl: string;
|
||||
indexerId: number;
|
||||
indexer: string;
|
||||
size: number;
|
||||
seeders?: number;
|
||||
leechers?: number;
|
||||
quality: QualityModel;
|
||||
languages: Language[];
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
mappedMovieId?: number;
|
||||
rejections: string[];
|
||||
indexerFlags: string[];
|
||||
episodeRequested: boolean;
|
||||
downloadAllowed: boolean;
|
||||
isDaily: boolean;
|
||||
isGrabbing: boolean;
|
||||
isGrabbed: boolean;
|
||||
grabError?: string;
|
||||
historyFailedData?: MovieHistory;
|
||||
historyGrabbedData?: MovieHistory;
|
||||
blocklistData?: MovieBlocklist;
|
||||
longDateFormat: string;
|
||||
timeFormat: string;
|
||||
searchPayload: object;
|
||||
onGrabPress(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
function InteractiveSearchRow(props: InteractiveSearchRowProps) {
|
||||
const {
|
||||
guid,
|
||||
indexerId,
|
||||
protocol,
|
||||
age,
|
||||
ageHours,
|
||||
ageMinutes,
|
||||
publishDate,
|
||||
title,
|
||||
infoUrl,
|
||||
indexer,
|
||||
size,
|
||||
seeders,
|
||||
leechers,
|
||||
quality,
|
||||
languages,
|
||||
customFormatScore,
|
||||
customFormats,
|
||||
mappedMovieId,
|
||||
rejections = [],
|
||||
indexerFlags = [],
|
||||
downloadAllowed,
|
||||
isGrabbing = false,
|
||||
isGrabbed = false,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
grabError,
|
||||
historyGrabbedData,
|
||||
historyFailedData,
|
||||
blocklistData,
|
||||
searchPayload,
|
||||
onGrabPress,
|
||||
} = props;
|
||||
|
||||
const [isConfirmGrabModalOpen, setIsConfirmGrabModalOpen] = useState(false);
|
||||
const [isOverrideModalOpen, setIsOverrideModalOpen] = useState(false);
|
||||
|
||||
const onGrabPressWrapper = useCallback(() => {
|
||||
if (downloadAllowed) {
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsConfirmGrabModalOpen(true);
|
||||
}, [
|
||||
guid,
|
||||
indexerId,
|
||||
downloadAllowed,
|
||||
onGrabPress,
|
||||
setIsConfirmGrabModalOpen,
|
||||
]);
|
||||
|
||||
const onGrabConfirm = useCallback(() => {
|
||||
setIsConfirmGrabModalOpen(false);
|
||||
|
||||
onGrabPress({
|
||||
guid,
|
||||
indexerId,
|
||||
...searchPayload,
|
||||
});
|
||||
}, [guid, indexerId, searchPayload, onGrabPress, setIsConfirmGrabModalOpen]);
|
||||
|
||||
const onGrabCancel = useCallback(() => {
|
||||
setIsConfirmGrabModalOpen(false);
|
||||
}, [setIsConfirmGrabModalOpen]);
|
||||
|
||||
const onOverridePress = useCallback(() => {
|
||||
setIsOverrideModalOpen(true);
|
||||
}, [setIsOverrideModalOpen]);
|
||||
|
||||
const onOverrideModalClose = useCallback(() => {
|
||||
setIsOverrideModalOpen(false);
|
||||
}, [setIsOverrideModalOpen]);
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
<TableRowCell className={styles.protocol}>
|
||||
<ProtocolLabel protocol={protocol} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell
|
||||
className={styles.age}
|
||||
title={formatDateTime(publishDate, longDateFormat, timeFormat, {
|
||||
includeSeconds: true,
|
||||
})}
|
||||
>
|
||||
{formatAge(age, ageHours, ageMinutes)}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.download}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={getDownloadKind(isGrabbed, grabError)}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isSpinning={isGrabbing}
|
||||
onPress={onGrabPressWrapper}
|
||||
/>
|
||||
|
||||
<Link
|
||||
className={styles.manualDownloadContent}
|
||||
title={translate('OverrideAndAddToDownloadQueue')}
|
||||
onPress={onOverridePress}
|
||||
>
|
||||
<div className={styles.manualDownloadContent}>
|
||||
<Icon
|
||||
className={styles.interactiveIcon}
|
||||
name={icons.INTERACTIVE}
|
||||
size={12}
|
||||
/>
|
||||
|
||||
<Icon
|
||||
className={styles.downloadIcon}
|
||||
name={icons.CIRCLE_DOWN}
|
||||
size={10}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.rejected}>
|
||||
{rejections.length ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
|
||||
title={translate('ReleaseRejected')}
|
||||
body={
|
||||
<ul>
|
||||
{rejections.map((rejection, index) => {
|
||||
return <li key={index}>{rejection}</li>;
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.RIGHT}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.title}>
|
||||
<Link to={infoUrl} title={title}>
|
||||
<div>{title}</div>
|
||||
</Link>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexer}>{indexer}</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.history}>
|
||||
{historyGrabbedData?.date && !historyFailedData?.date ? (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DEFAULT}
|
||||
title={`${translate('Grabbed')}: ${formatDateTime(
|
||||
historyGrabbedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{historyFailedData?.date ? (
|
||||
<Icon
|
||||
name={icons.DOWNLOADING}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Failed')}: ${formatDateTime(
|
||||
historyFailedData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{blocklistData?.date ? (
|
||||
<Icon
|
||||
className={
|
||||
historyGrabbedData || historyFailedData ? styles.blocklist : ''
|
||||
}
|
||||
name={icons.BLOCKLIST}
|
||||
kind={kinds.DANGER}
|
||||
title={`${translate('Blocklisted')}: ${formatDateTime(
|
||||
blocklistData.date,
|
||||
longDateFormat,
|
||||
timeFormat,
|
||||
{ includeSeconds: true }
|
||||
)}`}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.size}>{formatBytes(size)}</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.peers}>
|
||||
{protocol === 'torrent' ? (
|
||||
<Peers seeders={seeders} leechers={leechers} />
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.languages}>
|
||||
<MovieLanguage languages={languages} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.quality}>
|
||||
<MovieQuality quality={quality} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormat}>
|
||||
<MovieFormats formats={customFormats} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.customFormatScore}>
|
||||
<Tooltip
|
||||
anchor={formatCustomFormatScore(
|
||||
customFormatScore,
|
||||
customFormats.length
|
||||
)}
|
||||
tooltip={<MovieFormats formats={customFormats} />}
|
||||
position={tooltipPositions.TOP}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell className={styles.indexerFlags}>
|
||||
{indexerFlags.length ? (
|
||||
<Popover
|
||||
anchor={<Icon name={icons.FLAG} kind={kinds.PRIMARY} />}
|
||||
title={translate('IndexerFlags')}
|
||||
body={
|
||||
<ul>
|
||||
{indexerFlags.map((flag, index) => {
|
||||
return <li key={index}>{flag}</li>;
|
||||
})}
|
||||
</ul>
|
||||
}
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<ConfirmModal
|
||||
isOpen={isConfirmGrabModalOpen}
|
||||
kind={kinds.WARNING}
|
||||
title={translate('GrabRelease')}
|
||||
message={translate('GrabReleaseMessageText', { title })}
|
||||
confirmLabel={translate('Grab')}
|
||||
onConfirm={onGrabConfirm}
|
||||
onCancel={onGrabCancel}
|
||||
/>
|
||||
|
||||
<OverrideMatchModal
|
||||
isOpen={isOverrideModalOpen}
|
||||
title={title}
|
||||
indexerId={indexerId}
|
||||
guid={guid}
|
||||
movieId={mappedMovieId}
|
||||
languages={languages}
|
||||
quality={quality}
|
||||
protocol={protocol}
|
||||
isGrabbing={isGrabbing}
|
||||
grabError={grabError}
|
||||
onModalClose={onOverrideModalClose}
|
||||
/>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
|
||||
export default InteractiveSearchRow;
|
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import SelectDownloadClientModalContent from './SelectDownloadClientModalContent';
|
||||
|
||||
interface SelectDownloadClientModalProps {
|
||||
isOpen: boolean;
|
||||
protocol: DownloadProtocol;
|
||||
modalTitle: string;
|
||||
onDownloadClientSelect(downloadClientId: number): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SelectDownloadClientModal(props: SelectDownloadClientModalProps) {
|
||||
const { isOpen, protocol, modalTitle, onDownloadClientSelect, onModalClose } =
|
||||
props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onModalClose={onModalClose} size={sizes.MEDIUM}>
|
||||
<SelectDownloadClientModalContent
|
||||
protocol={protocol}
|
||||
modalTitle={modalTitle}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectDownloadClientModal;
|
@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Alert from 'Components/Alert';
|
||||
import Form from 'Components/Form/Form';
|
||||
import Button from 'Components/Link/Button';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
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 DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectDownloadClientRow from './SelectDownloadClientRow';
|
||||
|
||||
interface SelectDownloadClientModalContentProps {
|
||||
protocol: DownloadProtocol;
|
||||
modalTitle: string;
|
||||
onDownloadClientSelect(downloadClientId: number): void;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function SelectDownloadClientModalContent(
|
||||
props: SelectDownloadClientModalContentProps
|
||||
) {
|
||||
const { modalTitle, protocol, onDownloadClientSelect, onModalClose } = props;
|
||||
|
||||
const { isFetching, isPopulated, error, items } = useSelector(
|
||||
createEnabledDownloadClientsSelector(protocol)
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('SelectDownloadClientModalTitle', { modalTitle })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{isFetching ? <LoadingIndicator /> : null}
|
||||
|
||||
{!isFetching && error ? (
|
||||
<Alert kind={kinds.DANGER}>
|
||||
{translate('DownloadClientsLoadError')}
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{isPopulated && !error ? (
|
||||
<Form>
|
||||
{items.map((downloadClient) => {
|
||||
const { id, name, priority } = downloadClient;
|
||||
|
||||
return (
|
||||
<SelectDownloadClientRow
|
||||
key={id}
|
||||
id={id}
|
||||
name={name}
|
||||
priority={priority}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Form>
|
||||
) : null}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectDownloadClientModalContent;
|
@ -0,0 +1,6 @@
|
||||
.downloadClient {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--borderColor);
|
||||
}
|
7
frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientRow.css.d.ts
vendored
Normal file
7
frontend/src/InteractiveSearch/OverrideMatch/DownloadClient/SelectDownloadClientRow.css.d.ts
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'downloadClient': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,32 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectDownloadClientRow.css';
|
||||
|
||||
interface SelectSeasonRowProps {
|
||||
id: number;
|
||||
name: string;
|
||||
priority: number;
|
||||
onDownloadClientSelect(downloadClientId: number): unknown;
|
||||
}
|
||||
|
||||
function SelectDownloadClientRow(props: SelectSeasonRowProps) {
|
||||
const { id, name, priority, onDownloadClientSelect } = props;
|
||||
|
||||
const onSeasonSelectWrapper = useCallback(() => {
|
||||
onDownloadClientSelect(id);
|
||||
}, [id, onDownloadClientSelect]);
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={styles.downloadClient}
|
||||
component="div"
|
||||
onPress={onSeasonSelectWrapper}
|
||||
>
|
||||
<div>{name}</div>
|
||||
<div>{translate('PrioritySettings', { priority })}</div>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectDownloadClientRow;
|
@ -0,0 +1,17 @@
|
||||
.link {
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: inline-block;
|
||||
margin: -2px 0;
|
||||
width: 100%;
|
||||
outline: 2px dashed var(--dangerColor);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.optional {
|
||||
outline: 2px dashed var(--gray);
|
||||
}
|
9
frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchData.css.d.ts
vendored
Normal file
9
frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchData.css.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'link': string;
|
||||
'optional': string;
|
||||
'placeholder': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,35 @@
|
||||
import classNames from 'classnames';
|
||||
import React from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import styles from './OverrideMatchData.css';
|
||||
|
||||
interface OverrideMatchDataProps {
|
||||
value?: string | number | JSX.Element | JSX.Element[];
|
||||
isDisabled?: boolean;
|
||||
isOptional?: boolean;
|
||||
onPress: () => void;
|
||||
}
|
||||
|
||||
function OverrideMatchData(props: OverrideMatchDataProps) {
|
||||
const { value, isDisabled = false, isOptional, onPress } = props;
|
||||
|
||||
return (
|
||||
<Link className={styles.link} isDisabled={isDisabled} onPress={onPress}>
|
||||
{(value == null || (Array.isArray(value) && value.length === 0)) &&
|
||||
!isDisabled ? (
|
||||
<span
|
||||
className={classNames(
|
||||
styles.placeholder,
|
||||
isOptional && styles.optional
|
||||
)}
|
||||
>
|
||||
|
||||
</span>
|
||||
) : (
|
||||
value
|
||||
)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverrideMatchData;
|
@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import OverrideMatchModalContent from './OverrideMatchModalContent';
|
||||
|
||||
interface OverrideMatchModalProps {
|
||||
isOpen: boolean;
|
||||
title: string;
|
||||
indexerId: number;
|
||||
guid: string;
|
||||
movieId?: number;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
protocol: DownloadProtocol;
|
||||
isGrabbing: boolean;
|
||||
grabError?: string;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function OverrideMatchModal(props: OverrideMatchModalProps) {
|
||||
const {
|
||||
isOpen,
|
||||
title,
|
||||
indexerId,
|
||||
guid,
|
||||
movieId,
|
||||
languages,
|
||||
quality,
|
||||
protocol,
|
||||
isGrabbing,
|
||||
grabError,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
|
||||
<OverrideMatchModalContent
|
||||
title={title}
|
||||
indexerId={indexerId}
|
||||
guid={guid}
|
||||
movieId={movieId}
|
||||
languages={languages}
|
||||
quality={quality}
|
||||
protocol={protocol}
|
||||
isGrabbing={isGrabbing}
|
||||
grabError={grabError}
|
||||
onModalClose={onModalClose}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverrideMatchModal;
|
@ -0,0 +1,49 @@
|
||||
.label {
|
||||
composes: label from '~Components/Label.css';
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
composes: modalFooter from '~Components/Modal/ModalFooter.css';
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-right: 20px;
|
||||
color: var(--dangerColor);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.item {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.error {
|
||||
margin-right: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
justify-content: space-between;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
11
frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModalContent.css.d.ts
vendored
Normal file
11
frontend/src/InteractiveSearch/OverrideMatch/OverrideMatchModalContent.css.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'buttons': string;
|
||||
'error': string;
|
||||
'footer': string;
|
||||
'item': string;
|
||||
'label': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
@ -0,0 +1,299 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerErrorButton from 'Components/Link/SpinnerErrorButton';
|
||||
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 DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import usePrevious from 'Helpers/Hooks/usePrevious';
|
||||
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||
import SelectMovieModal from 'InteractiveImport/Movie/SelectMovieModal';
|
||||
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||
import Language from 'Language/Language';
|
||||
import Movie from 'Movie/Movie';
|
||||
import MovieLanguage from 'Movie/MovieLanguage';
|
||||
import MovieQuality from 'Movie/MovieQuality';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import { grabRelease } from 'Store/Actions/releaseActions';
|
||||
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||
import { createMovieSelectorForHook } from 'Store/Selectors/createMovieSelector';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal';
|
||||
import OverrideMatchData from './OverrideMatchData';
|
||||
import styles from './OverrideMatchModalContent.css';
|
||||
|
||||
type SelectType =
|
||||
| 'select'
|
||||
| 'movie'
|
||||
| 'quality'
|
||||
| 'language'
|
||||
| 'downloadClient';
|
||||
|
||||
interface OverrideMatchModalContentProps {
|
||||
indexerId: number;
|
||||
title: string;
|
||||
guid: string;
|
||||
movieId?: number;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
protocol: DownloadProtocol;
|
||||
isGrabbing: boolean;
|
||||
grabError?: string;
|
||||
onModalClose(): void;
|
||||
}
|
||||
|
||||
function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
||||
const modalTitle = translate('ManualGrab');
|
||||
const {
|
||||
indexerId,
|
||||
title,
|
||||
guid,
|
||||
protocol,
|
||||
isGrabbing,
|
||||
grabError,
|
||||
onModalClose,
|
||||
} = props;
|
||||
|
||||
const [movieId, setMovieId] = useState(props.movieId);
|
||||
const [languages, setLanguages] = useState(props.languages);
|
||||
const [quality, setQuality] = useState(props.quality);
|
||||
const [downloadClientId, setDownloadClientId] = useState<number | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||
null
|
||||
);
|
||||
const previousIsGrabbing = usePrevious(isGrabbing);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const movie: Movie | undefined = useSelector(
|
||||
createMovieSelectorForHook(movieId)
|
||||
);
|
||||
const { items: downloadClients } = useSelector(
|
||||
createEnabledDownloadClientsSelector(protocol)
|
||||
);
|
||||
|
||||
const onSelectModalClose = useCallback(() => {
|
||||
setSelectModalOpen(null);
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onSelectMoviePress = useCallback(() => {
|
||||
setSelectModalOpen('movie');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onMovieSelect = useCallback(
|
||||
(m: Movie) => {
|
||||
setMovieId(m.id);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[setMovieId, setSelectModalOpen]
|
||||
);
|
||||
|
||||
const onSelectQualityPress = useCallback(() => {
|
||||
setSelectModalOpen('quality');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onQualitySelect = useCallback(
|
||||
(quality: QualityModel) => {
|
||||
setQuality(quality);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[setQuality, setSelectModalOpen]
|
||||
);
|
||||
|
||||
const onSelectLanguagesPress = useCallback(() => {
|
||||
setSelectModalOpen('language');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onLanguagesSelect = useCallback(
|
||||
(languages: Language[]) => {
|
||||
setLanguages(languages);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[setLanguages, setSelectModalOpen]
|
||||
);
|
||||
|
||||
const onSelectDownloadClientPress = useCallback(() => {
|
||||
setSelectModalOpen('downloadClient');
|
||||
}, [setSelectModalOpen]);
|
||||
|
||||
const onDownloadClientSelect = useCallback(
|
||||
(downloadClientId: number) => {
|
||||
setDownloadClientId(downloadClientId);
|
||||
setSelectModalOpen(null);
|
||||
},
|
||||
[setDownloadClientId, setSelectModalOpen]
|
||||
);
|
||||
|
||||
const onGrabPress = useCallback(() => {
|
||||
if (!movieId) {
|
||||
setError(translate('OverrideGrabNoMovie'));
|
||||
return;
|
||||
} else if (!quality) {
|
||||
setError(translate('OverrideGrabNoQuality'));
|
||||
return;
|
||||
} else if (!languages.length) {
|
||||
setError(translate('OverrideGrabNoLanguage'));
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(
|
||||
grabRelease({
|
||||
indexerId,
|
||||
guid,
|
||||
movieId,
|
||||
quality,
|
||||
languages,
|
||||
downloadClientId,
|
||||
shouldOverride: true,
|
||||
})
|
||||
);
|
||||
}, [
|
||||
indexerId,
|
||||
guid,
|
||||
movieId,
|
||||
quality,
|
||||
languages,
|
||||
downloadClientId,
|
||||
setError,
|
||||
dispatch,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isGrabbing && previousIsGrabbing) {
|
||||
onModalClose();
|
||||
}
|
||||
}, [isGrabbing, previousIsGrabbing, onModalClose]);
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
dispatch(fetchDownloadClients());
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
{translate('OverrideGrabModalTitle', { title })}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
className={styles.item}
|
||||
title={translate('Movie')}
|
||||
data={
|
||||
<OverrideMatchData
|
||||
value={movie?.title}
|
||||
onPress={onSelectMoviePress}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
className={styles.item}
|
||||
title={translate('Quality')}
|
||||
data={
|
||||
<OverrideMatchData
|
||||
value={
|
||||
<MovieQuality className={styles.label} quality={quality} />
|
||||
}
|
||||
onPress={onSelectQualityPress}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
className={styles.item}
|
||||
title={translate('Languages')}
|
||||
data={
|
||||
<OverrideMatchData
|
||||
value={
|
||||
<MovieLanguage
|
||||
className={styles.label}
|
||||
languages={languages}
|
||||
/>
|
||||
}
|
||||
onPress={onSelectLanguagesPress}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
{downloadClients.length > 1 ? (
|
||||
<DescriptionListItem
|
||||
className={styles.item}
|
||||
title={translate('DownloadClient')}
|
||||
data={
|
||||
<OverrideMatchData
|
||||
value={
|
||||
downloadClients.find(
|
||||
(downloadClient) => downloadClient.id === downloadClientId
|
||||
)?.name ?? translate('Default')
|
||||
}
|
||||
onPress={onSelectDownloadClientPress}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</DescriptionList>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.footer}>
|
||||
<div className={styles.error}>{error || grabError}</div>
|
||||
|
||||
<div className={styles.buttons}>
|
||||
<Button onPress={onModalClose}>{translate('Cancel')}</Button>
|
||||
|
||||
<SpinnerErrorButton
|
||||
isSpinning={isGrabbing}
|
||||
error={grabError}
|
||||
onPress={onGrabPress}
|
||||
>
|
||||
{translate('GrabRelease')}
|
||||
</SpinnerErrorButton>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
|
||||
<SelectMovieModal
|
||||
isOpen={selectModalOpen === 'movie'}
|
||||
modalTitle={modalTitle}
|
||||
onMovieSelect={onMovieSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectQualityModal
|
||||
isOpen={selectModalOpen === 'quality'}
|
||||
qualityId={quality ? quality.quality.id : 0}
|
||||
proper={quality ? quality.revision.version > 1 : false}
|
||||
real={quality ? quality.revision.real > 0 : false}
|
||||
modalTitle={modalTitle}
|
||||
onQualitySelect={onQualitySelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectLanguageModal
|
||||
isOpen={selectModalOpen === 'language'}
|
||||
languageIds={languages ? languages.map((l) => l.id) : []}
|
||||
modalTitle={modalTitle}
|
||||
onLanguagesSelect={onLanguagesSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
|
||||
<SelectDownloadClientModal
|
||||
isOpen={selectModalOpen === 'downloadClient'}
|
||||
protocol={protocol}
|
||||
modalTitle={modalTitle}
|
||||
onDownloadClientSelect={onDownloadClientSelect}
|
||||
onModalClose={onSelectModalClose}
|
||||
/>
|
||||
</ModalContent>
|
||||
);
|
||||
}
|
||||
|
||||
export default OverrideMatchModalContent;
|
@ -0,0 +1,22 @@
|
||||
import { createSelector } from 'reselect';
|
||||
import { DownloadClientAppState } from 'App/State/SettingsAppState';
|
||||
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
|
||||
export default function createEnabledDownloadClientsSelector(
|
||||
protocol: DownloadProtocol
|
||||
) {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.downloadClients', sortByName),
|
||||
(downloadClients: DownloadClientAppState) => {
|
||||
const { isFetching, isPopulated, error, items } = downloadClients;
|
||||
|
||||
const clients = items.filter(
|
||||
(item) => item.protocol === protocol && item.enable
|
||||
);
|
||||
|
||||
return { isFetching, isPopulated, error, items: clients };
|
||||
}
|
||||
);
|
||||
}
|
7
frontend/src/typings/MovieBlocklist.ts
Normal file
7
frontend/src/typings/MovieBlocklist.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
interface MovieBlocklist extends ModelBase {
|
||||
date: string;
|
||||
}
|
||||
|
||||
export default MovieBlocklist;
|
7
frontend/src/typings/MovieHistory.ts
Normal file
7
frontend/src/typings/MovieHistory.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import ModelBase from 'App/ModelBase';
|
||||
|
||||
interface MovieHistory extends ModelBase {
|
||||
date: string;
|
||||
}
|
||||
|
||||
export default MovieHistory;
|
@ -80,7 +80,7 @@ public async Task should_download_report_if_movie_was_not_already_downloaded()
|
||||
decisions.Add(new DownloadDecision(remoteMovie));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>()), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -93,7 +93,7 @@ public async Task should_only_download_movie_once()
|
||||
decisions.Add(new DownloadDecision(remoteMovie));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>()), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -110,7 +110,7 @@ public async Task should_not_download_if_any_movie_was_already_downloaded()
|
||||
decisions.Add(new DownloadDecision(remoteMovie2));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>()), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -175,7 +175,7 @@ public async Task should_not_add_to_downloaded_list_when_download_fails()
|
||||
var decisions = new List<DownloadDecision>();
|
||||
decisions.Add(new DownloadDecision(remoteMovie));
|
||||
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteMovie>())).Throws(new Exception());
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteMovie>(), null)).Throws(new Exception());
|
||||
|
||||
var result = await Subject.ProcessDecisions(decisions);
|
||||
|
||||
@ -204,7 +204,7 @@ public async Task should_not_grab_if_pending()
|
||||
decisions.Add(new DownloadDecision(remoteMovie, new Rejection("Failure!", RejectionType.Temporary)));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>()), Times.Never());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), null), Times.Never());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -242,11 +242,11 @@ public async Task should_add_to_failed_if_already_failed_for_that_protocol()
|
||||
decisions.Add(new DownloadDecision(remoteMovie));
|
||||
decisions.Add(new DownloadDecision(remoteMovie));
|
||||
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteMovie>()))
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.IsAny<RemoteMovie>(), null))
|
||||
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>()), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.IsAny<RemoteMovie>(), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -259,12 +259,12 @@ public async Task should_not_add_to_failed_if_failed_for_a_different_protocol()
|
||||
decisions.Add(new DownloadDecision(remoteMovie));
|
||||
decisions.Add(new DownloadDecision(remoteMovie2));
|
||||
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteMovie>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)))
|
||||
Mocker.GetMock<IDownloadService>().Setup(s => s.DownloadReport(It.Is<RemoteMovie>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null))
|
||||
.Throws(new DownloadClientUnavailableException("Download client failed"));
|
||||
|
||||
await Subject.ProcessDecisions(decisions);
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteMovie>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet)), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteMovie>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent)), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteMovie>(r => r.Release.DownloadProtocol == DownloadProtocol.Usenet), null), Times.Once());
|
||||
Mocker.GetMock<IDownloadService>().Verify(v => v.DownloadReport(It.Is<RemoteMovie>(r => r.Release.DownloadProtocol == DownloadProtocol.Torrent), null), Times.Once());
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -276,7 +276,7 @@ public async Task should_add_to_rejected_if_release_unavailable_on_indexer()
|
||||
decisions.Add(new DownloadDecision(remoteMovie));
|
||||
|
||||
Mocker.GetMock<IDownloadService>()
|
||||
.Setup(s => s.DownloadReport(It.IsAny<RemoteMovie>()))
|
||||
.Setup(s => s.DownloadReport(It.IsAny<RemoteMovie>(), null))
|
||||
.Throws(new ReleaseUnavailableException(remoteMovie.Release, "That 404 Error is not just a Quirk"));
|
||||
|
||||
var result = await Subject.ProcessDecisions(decisions);
|
||||
|
@ -76,7 +76,7 @@ public async Task Download_report_should_publish_on_grab_event()
|
||||
var mock = WithUsenetClient();
|
||||
mock.Setup(s => s.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()));
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
VerifyEventPublished<MovieGrabbedEvent>();
|
||||
}
|
||||
@ -87,7 +87,7 @@ public async Task Download_report_should_grab_using_client()
|
||||
var mock = WithUsenetClient();
|
||||
mock.Setup(s => s.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()));
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
mock.Verify(s => s.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()), Times.Once());
|
||||
}
|
||||
@ -99,7 +99,7 @@ public void Download_report_should_not_publish_on_failed_grab_event()
|
||||
mock.Setup(s => s.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()))
|
||||
.Throws(new WebException());
|
||||
|
||||
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<WebException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
VerifyEventNotPublished<MovieGrabbedEvent>();
|
||||
}
|
||||
@ -114,7 +114,7 @@ public void Download_report_should_trigger_indexer_backoff_on_indexer_error()
|
||||
throw new ReleaseDownloadException(v.Release, "Error", new WebException());
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Once());
|
||||
@ -134,7 +134,7 @@ public void Download_report_should_trigger_indexer_backoff_on_http429_with_long_
|
||||
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), TimeSpan.FromMinutes(5.0)), Times.Once());
|
||||
@ -154,7 +154,7 @@ public void Download_report_should_trigger_indexer_backoff_on_http429_based_on_d
|
||||
throw new ReleaseDownloadException(v.Release, "Error", new TooManyRequestsException(request, response));
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseDownloadException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(),
|
||||
@ -168,7 +168,7 @@ public void Download_report_should_not_trigger_indexer_backoff_on_downloadclient
|
||||
mock.Setup(s => s.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()))
|
||||
.Throws(new DownloadClientException("Some Error"));
|
||||
|
||||
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<DownloadClientException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
||||
@ -184,7 +184,7 @@ public void Download_report_should_not_trigger_indexer_backoff_on_indexer_404_er
|
||||
throw new ReleaseUnavailableException(v.Release, "Error", new WebException());
|
||||
});
|
||||
|
||||
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<ReleaseUnavailableException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IIndexerStatusService>()
|
||||
.Verify(v => v.RecordFailure(It.IsAny<int>(), It.IsAny<TimeSpan>()), Times.Never());
|
||||
@ -193,7 +193,7 @@ public void Download_report_should_not_trigger_indexer_backoff_on_indexer_404_er
|
||||
[Test]
|
||||
public void should_not_attempt_download_if_client_isnt_configured()
|
||||
{
|
||||
Assert.ThrowsAsync<DownloadClientUnavailableException>(async () => await Subject.DownloadReport(_parseResult));
|
||||
Assert.ThrowsAsync<DownloadClientUnavailableException>(async () => await Subject.DownloadReport(_parseResult, null));
|
||||
|
||||
Mocker.GetMock<IDownloadClient>().Verify(c => c.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()), Times.Never());
|
||||
VerifyEventNotPublished<MovieGrabbedEvent>();
|
||||
@ -215,7 +215,7 @@ public async Task should_attempt_download_even_if_client_is_disabled()
|
||||
}
|
||||
});
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
Mocker.GetMock<IDownloadClientStatusService>().Verify(c => c.GetBlockedProviders(), Times.Never());
|
||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()), Times.Once());
|
||||
@ -228,7 +228,7 @@ public async Task should_send_download_to_correct_usenet_client()
|
||||
var mockTorrent = WithTorrentClient();
|
||||
var mockUsenet = WithUsenetClient();
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()), Times.Never());
|
||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()), Times.Once());
|
||||
@ -242,7 +242,7 @@ public async Task should_send_download_to_correct_torrent_client()
|
||||
|
||||
_parseResult.Release.DownloadProtocol = DownloadProtocol.Torrent;
|
||||
|
||||
await Subject.DownloadReport(_parseResult);
|
||||
await Subject.DownloadReport(_parseResult, null);
|
||||
|
||||
mockTorrent.Verify(c => c.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()), Times.Once());
|
||||
mockUsenet.Verify(c => c.Download(It.IsAny<RemoteMovie>(), It.IsAny<IIndexer>()), Times.Never());
|
||||
|
@ -17,7 +17,7 @@ namespace NzbDrone.Core.Download
|
||||
{
|
||||
public interface IDownloadService
|
||||
{
|
||||
Task DownloadReport(RemoteMovie remoteMovie);
|
||||
Task DownloadReport(RemoteMovie remoteMovie, int? downloadClientId);
|
||||
}
|
||||
|
||||
public class DownloadService : IDownloadService
|
||||
@ -50,13 +50,15 @@ public DownloadService(IProvideDownloadClient downloadClientProvider,
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task DownloadReport(RemoteMovie remoteMovie)
|
||||
public async Task DownloadReport(RemoteMovie remoteMovie, int? downloadClientId)
|
||||
{
|
||||
var filterBlockedClients = remoteMovie.Release.PendingReleaseReason == PendingReleaseReason.DownloadClientUnavailable;
|
||||
|
||||
var tags = remoteMovie.Movie?.Tags;
|
||||
|
||||
var downloadClient = _downloadClientProvider.GetDownloadClient(remoteMovie.Release.DownloadProtocol, remoteMovie.Release.IndexerId, filterBlockedClients, tags);
|
||||
var downloadClient = downloadClientId.HasValue
|
||||
? _downloadClientProvider.Get(downloadClientId.Value)
|
||||
: _downloadClientProvider.GetDownloadClient(remoteMovie.Release.DownloadProtocol, remoteMovie.Release.IndexerId, filterBlockedClients, tags);
|
||||
|
||||
await DownloadReport(remoteMovie, downloadClient);
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ public async Task<ProcessedDecisions> ProcessDecisions(List<DownloadDecision> de
|
||||
try
|
||||
{
|
||||
_logger.Trace("Grabbing from Indexer {0} at priority {1}.", remoteMovie.Release.Indexer, remoteMovie.Release.IndexerPriority);
|
||||
await _downloadService.DownloadReport(remoteMovie);
|
||||
await _downloadService.DownloadReport(remoteMovie, null);
|
||||
grabbed.Add(report);
|
||||
}
|
||||
catch (ReleaseUnavailableException)
|
||||
|
@ -216,6 +216,7 @@
|
||||
"Day": "Day",
|
||||
"Days": "Days",
|
||||
"Debug": "Debug",
|
||||
"Default": "Default",
|
||||
"DefaultCase": "Default Case",
|
||||
"DefaultDelayProfile": "This is the default profile. It applies to all movies that don't have an explicit profile.",
|
||||
"DelayProfile": "Delay Profile",
|
||||
@ -304,6 +305,7 @@
|
||||
"DownloadClientTagHelpText": "Only use this download client for movies with at least one matching tag. Leave blank to use with all movies.",
|
||||
"DownloadClientUnavailable": "Download client is unavailable",
|
||||
"DownloadClients": "Download Clients",
|
||||
"DownloadClientsLoadError": "Unable to load download clients",
|
||||
"DownloadClientsSettingsSummary": "Download clients, download handling and remote path mappings",
|
||||
"DownloadFailed": "Download failed",
|
||||
"DownloadFailedCheckDownloadClientForMoreDetails": "Download failed: check download client for more details",
|
||||
@ -568,6 +570,7 @@
|
||||
"ManageIndexers": "Manage Indexers",
|
||||
"ManageLists": "Manage Lists",
|
||||
"Manual": "Manual",
|
||||
"ManualGrab": "Manual Grab",
|
||||
"ManualImport": "Manual Import",
|
||||
"ManualImportSelectLanguage": "Manual Import - Select Language",
|
||||
"ManualImportSelectMovie": "Manual Import - Select Movie",
|
||||
@ -756,6 +759,11 @@
|
||||
"OriginalLanguage": "Original Language",
|
||||
"OriginalTitle": "Original Title",
|
||||
"OutputPath": "Output Path",
|
||||
"OverrideAndAddToDownloadQueue": "Override and add to download queue",
|
||||
"OverrideGrabModalTitle": "Override and Grab - {title}",
|
||||
"OverrideGrabNoLanguage": "At least one language must be selected",
|
||||
"OverrideGrabNoMovie": "Movie must be selected",
|
||||
"OverrideGrabNoQuality": "Quality must be selected",
|
||||
"Overview": "Overview",
|
||||
"OverviewOptions": "Overview Options",
|
||||
"PackageVersion": "Package Version",
|
||||
@ -863,6 +871,7 @@
|
||||
"RefreshMovie": "Refresh movie",
|
||||
"RegularExpressionsCanBeTested": "Regular expressions can be tested ",
|
||||
"RejectionCount": "Rejection Count",
|
||||
"Rejections": "Rejections",
|
||||
"RelativePath": "Relative Path",
|
||||
"ReleaseBranchCheckOfficialBranchMessage": "Branch {0} is not a valid Radarr release branch, you will not receive updates",
|
||||
"ReleaseDates": "Release Dates",
|
||||
@ -1002,6 +1011,7 @@
|
||||
"Seeders": "Seeders",
|
||||
"SelectAll": "Select All",
|
||||
"SelectDotDot": "Select...",
|
||||
"SelectDownloadClientModalTitle": "{modalTitle} - Select Download Client",
|
||||
"SelectFolder": "Select Folder",
|
||||
"SelectLanguage": "Select Language",
|
||||
"SelectLanguages": "Select Languages",
|
||||
@ -1181,7 +1191,6 @@
|
||||
"UnableToLoadCustomFormats": "Unable to load Custom Formats",
|
||||
"UnableToLoadDelayProfiles": "Unable to load Delay Profiles",
|
||||
"UnableToLoadDownloadClientOptions": "Unable to load download client options",
|
||||
"UnableToLoadDownloadClients": "Unable to load download clients",
|
||||
"UnableToLoadGeneralSettings": "Unable to load General settings",
|
||||
"UnableToLoadHistory": "Unable to load history",
|
||||
"UnableToLoadIndexerOptions": "Unable to load indexer options",
|
||||
|
@ -5,6 +5,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Cache;
|
||||
using NzbDrone.Common.EnsureThat;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.DecisionEngine;
|
||||
using NzbDrone.Core.Download;
|
||||
using NzbDrone.Core.Exceptions;
|
||||
@ -58,7 +60,8 @@ public ReleaseController(IFetchAndParseRss rssFetcherAndParser,
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public object DownloadRelease(ReleaseResource release)
|
||||
[Consumes("application/json")]
|
||||
public async Task<object> DownloadRelease(ReleaseResource release)
|
||||
{
|
||||
var remoteMovie = _remoteMovieCache.Find(GetCacheKey(release));
|
||||
|
||||
@ -71,6 +74,30 @@ public object DownloadRelease(ReleaseResource release)
|
||||
|
||||
try
|
||||
{
|
||||
if (release.ShouldOverride == true)
|
||||
{
|
||||
Ensure.That(release.MovieId, () => release.MovieId).IsNotNull();
|
||||
Ensure.That(release.Quality, () => release.Quality).IsNotNull();
|
||||
Ensure.That(release.Languages, () => release.Languages).IsNotNull();
|
||||
|
||||
// Clone the remote episode so we don't overwrite anything on the original
|
||||
remoteMovie = new RemoteMovie
|
||||
{
|
||||
Release = remoteMovie.Release,
|
||||
ParsedMovieInfo = remoteMovie.ParsedMovieInfo.JsonClone(),
|
||||
DownloadAllowed = remoteMovie.DownloadAllowed,
|
||||
SeedConfiguration = remoteMovie.SeedConfiguration,
|
||||
CustomFormats = remoteMovie.CustomFormats,
|
||||
CustomFormatScore = remoteMovie.CustomFormatScore,
|
||||
MovieMatchType = remoteMovie.MovieMatchType,
|
||||
ReleaseSource = remoteMovie.ReleaseSource
|
||||
};
|
||||
|
||||
remoteMovie.Movie = _movieService.GetMovie(release.MovieId!.Value);
|
||||
remoteMovie.ParsedMovieInfo.Quality = release.Quality;
|
||||
remoteMovie.Languages = release.Languages;
|
||||
}
|
||||
|
||||
if (remoteMovie.Movie == null)
|
||||
{
|
||||
if (release.MovieId.HasValue)
|
||||
@ -85,7 +112,7 @@ public object DownloadRelease(ReleaseResource release)
|
||||
}
|
||||
}
|
||||
|
||||
_downloadService.DownloadReport(remoteMovie);
|
||||
await _downloadService.DownloadReport(remoteMovie, release.DownloadClientId);
|
||||
}
|
||||
catch (ReleaseDownloadException ex)
|
||||
{
|
||||
@ -97,6 +124,7 @@ public object DownloadRelease(ReleaseResource release)
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public async Task<List<ReleaseResource>> GetReleases(int? movieId)
|
||||
{
|
||||
if (movieId.HasValue)
|
||||
|
@ -32,6 +32,7 @@ public class ReleaseResource : RestResource
|
||||
public bool SceneSource { get; set; }
|
||||
public List<string> MovieTitles { get; set; }
|
||||
public List<Language> Languages { get; set; }
|
||||
public int? MappedMovieId { get; set; }
|
||||
public bool Approved { get; set; }
|
||||
public bool TemporarilyRejected { get; set; }
|
||||
public bool Rejected { get; set; }
|
||||
@ -56,6 +57,12 @@ public class ReleaseResource : RestResource
|
||||
// Sent when queuing an unknown release
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public int? MovieId { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public int? DownloadClientId { get; set; }
|
||||
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||
public bool? ShouldOverride { get; set; }
|
||||
}
|
||||
|
||||
public static class ReleaseResourceMapper
|
||||
@ -88,6 +95,7 @@ public static ReleaseResource ToResource(this DownloadDecision model)
|
||||
Title = releaseInfo.Title,
|
||||
MovieTitles = parsedMovieInfo.MovieTitles,
|
||||
Languages = remoteMovie.Languages,
|
||||
MappedMovieId = remoteMovie.Movie?.Id,
|
||||
Approved = model.Approved,
|
||||
TemporarilyRejected = model.TemporarilyRejected,
|
||||
Rejected = model.Rejected,
|
||||
|
@ -30,7 +30,7 @@ public async Task<object> Grab(int id)
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _downloadService.DownloadReport(pendingRelease.RemoteMovie);
|
||||
await _downloadService.DownloadReport(pendingRelease.RemoteMovie, null);
|
||||
|
||||
return new { };
|
||||
}
|
||||
@ -48,7 +48,7 @@ public async Task<object> Grab([FromBody] QueueBulkResource resource)
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _downloadService.DownloadReport(pendingRelease.RemoteMovie);
|
||||
await _downloadService.DownloadReport(pendingRelease.RemoteMovie, null);
|
||||
}
|
||||
|
||||
return new { };
|
||||
|
Loading…
Reference in New Issue
Block a user