New: Option to override release and grab New: Option to select download client when multiple of the same type are configured Closes #4526 Closes #4774pull/5540/head
parent
defdc84b7e
commit
07f0fbf9a5
@ -0,0 +1,7 @@
|
|||||||
|
enum DownloadProtocol {
|
||||||
|
Unknown = 'unknown',
|
||||||
|
Usenet = 'usenet',
|
||||||
|
Torrent = 'torrent',
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DownloadProtocol;
|
@ -1,308 +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 Tooltip from 'Components/Tooltip/Tooltip';
|
|
||||||
import EpisodeFormats from 'Episode/EpisodeFormats';
|
|
||||||
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
|
||||||
import formatAge from 'Utilities/Number/formatAge';
|
|
||||||
import formatBytes from 'Utilities/Number/formatBytes';
|
|
||||||
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
|
||||||
import Peers from './Peers';
|
|
||||||
import ReleaseSceneIndicator from './ReleaseSceneIndicator';
|
|
||||||
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 getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
|
||||||
if (isGrabbing) {
|
|
||||||
return '';
|
|
||||||
} else if (isGrabbed) {
|
|
||||||
return 'Added to download queue';
|
|
||||||
} else if (grabError) {
|
|
||||||
return grabError;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Add to download queue';
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
languages,
|
|
||||||
customFormatScore,
|
|
||||||
customFormats,
|
|
||||||
sceneMapping,
|
|
||||||
seasonNumber,
|
|
||||||
episodeNumbers,
|
|
||||||
absoluteEpisodeNumbers,
|
|
||||||
mappedSeasonNumber,
|
|
||||||
mappedEpisodeNumbers,
|
|
||||||
mappedAbsoluteEpisodeNumbers,
|
|
||||||
rejections,
|
|
||||||
episodeRequested,
|
|
||||||
downloadAllowed,
|
|
||||||
isDaily,
|
|
||||||
isGrabbing,
|
|
||||||
isGrabbed,
|
|
||||||
longDateFormat,
|
|
||||||
timeFormat,
|
|
||||||
grabError
|
|
||||||
} = 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>
|
|
||||||
<div className={styles.titleContent}>
|
|
||||||
<Link to={infoUrl}>
|
|
||||||
{title}
|
|
||||||
</Link>
|
|
||||||
<ReleaseSceneIndicator
|
|
||||||
className={styles.sceneMapping}
|
|
||||||
seasonNumber={mappedSeasonNumber}
|
|
||||||
episodeNumbers={mappedEpisodeNumbers}
|
|
||||||
absoluteEpisodeNumbers={mappedAbsoluteEpisodeNumbers}
|
|
||||||
sceneSeasonNumber={seasonNumber}
|
|
||||||
sceneEpisodeNumbers={episodeNumbers}
|
|
||||||
sceneAbsoluteEpisodeNumbers={absoluteEpisodeNumbers}
|
|
||||||
sceneMapping={sceneMapping}
|
|
||||||
episodeRequested={episodeRequested}
|
|
||||||
isDaily={isDaily}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.indexer}>
|
|
||||||
{indexer}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.size}>
|
|
||||||
{formatBytes(size)}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.peers}>
|
|
||||||
{
|
|
||||||
protocol === 'torrent' &&
|
|
||||||
<Peers
|
|
||||||
seeders={seeders}
|
|
||||||
leechers={leechers}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.languages}>
|
|
||||||
<EpisodeLanguages languages={languages} />
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.quality}>
|
|
||||||
<EpisodeQuality quality={quality} />
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.customFormatScore}>
|
|
||||||
<Tooltip
|
|
||||||
anchor={
|
|
||||||
formatPreferredWordScore(customFormatScore, customFormats.length)
|
|
||||||
}
|
|
||||||
tooltip={<EpisodeFormats formats={customFormats} />}
|
|
||||||
position={tooltipPositions.BOTTOM}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.rejected}>
|
|
||||||
{
|
|
||||||
!!rejections.length &&
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<Icon
|
|
||||||
name={icons.DANGER}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
title="Release Rejected"
|
|
||||||
body={
|
|
||||||
<ul>
|
|
||||||
{
|
|
||||||
rejections.map((rejection, index) => {
|
|
||||||
return (
|
|
||||||
<li key={index}>
|
|
||||||
{rejection}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.LEFT}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<TableRowCell className={styles.download}>
|
|
||||||
<SpinnerIconButton
|
|
||||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
|
||||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
|
||||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
|
||||||
isSpinning={isGrabbing}
|
|
||||||
onPress={downloadAllowed ? this.onGrabPress : this.onConfirmGrabPress}
|
|
||||||
/>
|
|
||||||
</TableRowCell>
|
|
||||||
|
|
||||||
<ConfirmModal
|
|
||||||
isOpen={this.state.isConfirmGrabModalOpen}
|
|
||||||
kind={kinds.WARNING}
|
|
||||||
title="Grab Release"
|
|
||||||
message={`Sonarr was unable to determine which series and episode this release was for. Sonarr may be unable to automatically import this release. Do you want to grab '${title}'?`}
|
|
||||||
confirmLabel="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,
|
|
||||||
languages: PropTypes.arrayOf(PropTypes.object).isRequired,
|
|
||||||
customFormats: PropTypes.arrayOf(PropTypes.object),
|
|
||||||
customFormatScore: PropTypes.number.isRequired,
|
|
||||||
sceneMapping: PropTypes.object,
|
|
||||||
seasonNumber: PropTypes.number,
|
|
||||||
episodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
|
||||||
absoluteEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
|
||||||
mappedSeasonNumber: PropTypes.number,
|
|
||||||
mappedEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
|
||||||
mappedAbsoluteEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
|
||||||
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
|
|
||||||
episodeRequested: PropTypes.bool.isRequired,
|
|
||||||
downloadAllowed: PropTypes.bool.isRequired,
|
|
||||||
isDaily: 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
|
|
||||||
};
|
|
||||||
|
|
||||||
InteractiveSearchRow.defaultProps = {
|
|
||||||
rejections: [],
|
|
||||||
isGrabbing: false,
|
|
||||||
isGrabbed: false
|
|
||||||
};
|
|
||||||
|
|
||||||
export default InteractiveSearchRow;
|
|
@ -0,0 +1,314 @@
|
|||||||
|
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 EpisodeFormats from 'Episode/EpisodeFormats';
|
||||||
|
import EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
|
import formatAge from 'Utilities/Number/formatAge';
|
||||||
|
import formatBytes from 'Utilities/Number/formatBytes';
|
||||||
|
import formatPreferredWordScore from 'Utilities/Number/formatPreferredWordScore';
|
||||||
|
import OverrideMatchModal from './OverrideMatch/OverrideMatchModal';
|
||||||
|
import Peers from './Peers';
|
||||||
|
import ReleaseEpisode from './ReleaseEpisode';
|
||||||
|
import ReleaseSceneIndicator from './ReleaseSceneIndicator';
|
||||||
|
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 getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||||
|
if (isGrabbing) {
|
||||||
|
return '';
|
||||||
|
} else if (isGrabbed) {
|
||||||
|
return 'Added to download queue';
|
||||||
|
} else if (grabError) {
|
||||||
|
return grabError;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Add to download queue';
|
||||||
|
}
|
||||||
|
|
||||||
|
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?: object[];
|
||||||
|
customFormatScore: number;
|
||||||
|
sceneMapping?: object;
|
||||||
|
seasonNumber?: number;
|
||||||
|
episodeNumbers?: number[];
|
||||||
|
absoluteEpisodeNumbers?: number[];
|
||||||
|
mappedSeriesId?: number;
|
||||||
|
mappedSeasonNumber?: number;
|
||||||
|
mappedEpisodeNumbers?: number[];
|
||||||
|
mappedAbsoluteEpisodeNumbers?: number[];
|
||||||
|
mappedEpisodeInfo: ReleaseEpisode[];
|
||||||
|
rejections: string[];
|
||||||
|
episodeRequested: boolean;
|
||||||
|
downloadAllowed: boolean;
|
||||||
|
isDaily: boolean;
|
||||||
|
isGrabbing: boolean;
|
||||||
|
isGrabbed: boolean;
|
||||||
|
grabError?: string;
|
||||||
|
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,
|
||||||
|
sceneMapping,
|
||||||
|
seasonNumber,
|
||||||
|
episodeNumbers,
|
||||||
|
absoluteEpisodeNumbers,
|
||||||
|
mappedSeriesId,
|
||||||
|
mappedSeasonNumber,
|
||||||
|
mappedEpisodeNumbers,
|
||||||
|
mappedAbsoluteEpisodeNumbers,
|
||||||
|
mappedEpisodeInfo,
|
||||||
|
rejections,
|
||||||
|
episodeRequested,
|
||||||
|
downloadAllowed,
|
||||||
|
isDaily,
|
||||||
|
isGrabbing,
|
||||||
|
isGrabbed,
|
||||||
|
longDateFormat,
|
||||||
|
timeFormat,
|
||||||
|
grabError,
|
||||||
|
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>
|
||||||
|
<div className={styles.titleContent}>
|
||||||
|
<Link to={infoUrl}>{title}</Link>
|
||||||
|
<ReleaseSceneIndicator
|
||||||
|
className={styles.sceneMapping}
|
||||||
|
seasonNumber={mappedSeasonNumber}
|
||||||
|
episodeNumbers={mappedEpisodeNumbers}
|
||||||
|
absoluteEpisodeNumbers={mappedAbsoluteEpisodeNumbers}
|
||||||
|
sceneSeasonNumber={seasonNumber}
|
||||||
|
sceneEpisodeNumbers={episodeNumbers}
|
||||||
|
sceneAbsoluteEpisodeNumbers={absoluteEpisodeNumbers}
|
||||||
|
sceneMapping={sceneMapping}
|
||||||
|
episodeRequested={episodeRequested}
|
||||||
|
isDaily={isDaily}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.indexer}>{indexer}</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}>
|
||||||
|
<EpisodeLanguages languages={languages} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.quality}>
|
||||||
|
<EpisodeQuality quality={quality} />
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.customFormatScore}>
|
||||||
|
<Tooltip
|
||||||
|
anchor={formatPreferredWordScore(
|
||||||
|
customFormatScore,
|
||||||
|
customFormats.length
|
||||||
|
)}
|
||||||
|
tooltip={<EpisodeFormats formats={customFormats} />}
|
||||||
|
position={tooltipPositions.BOTTOM}
|
||||||
|
/>
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.rejected}>
|
||||||
|
{rejections.length ? (
|
||||||
|
<Popover
|
||||||
|
anchor={<Icon name={icons.DANGER} kind={kinds.DANGER} />}
|
||||||
|
title="Release Rejected"
|
||||||
|
body={
|
||||||
|
<ul>
|
||||||
|
{rejections.map((rejection, index) => {
|
||||||
|
return <li key={index}>{rejection}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.LEFT}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</TableRowCell>
|
||||||
|
|
||||||
|
<TableRowCell className={styles.download}>
|
||||||
|
<SpinnerIconButton
|
||||||
|
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||||
|
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||||
|
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||||
|
isSpinning={isGrabbing}
|
||||||
|
onPress={onGrabPressWrapper}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
title="Override and add to download queue"
|
||||||
|
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>
|
||||||
|
|
||||||
|
<ConfirmModal
|
||||||
|
isOpen={isConfirmGrabModalOpen}
|
||||||
|
kind={kinds.WARNING}
|
||||||
|
title="Grab Release"
|
||||||
|
message={`Sonarr was unable to determine which series and episode this release was for. Sonarr may be unable to automatically import this release. Do you want to grab '${title}'?`}
|
||||||
|
confirmLabel="Grab"
|
||||||
|
onConfirm={onGrabConfirm}
|
||||||
|
onCancel={onGrabCancel}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<OverrideMatchModal
|
||||||
|
isOpen={isOverrideModalOpen}
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
seriesId={mappedSeriesId}
|
||||||
|
seasonNumber={mappedSeasonNumber}
|
||||||
|
episodes={mappedEpisodeInfo}
|
||||||
|
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,68 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
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 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>{modalTitle} - Select Download Client</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
{isFetching ? <LoadingIndicator /> : null}
|
||||||
|
|
||||||
|
{!isFetching && error ? (
|
||||||
|
<div>Unable to load download clients</div>
|
||||||
|
) : 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);
|
||||||
|
}
|
@ -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,31 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import Link from 'Components/Link/Link';
|
||||||
|
import styles from './SelectDownloadClientRow.css';
|
||||||
|
|
||||||
|
interface SelectSeasonRowProps {
|
||||||
|
id: number;
|
||||||
|
name: number;
|
||||||
|
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>Priority: {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);
|
||||||
|
}
|
@ -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,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Modal from 'Components/Modal/Modal';
|
||||||
|
import DownloadProtocol from 'DownloadClient/DownloadProtocol';
|
||||||
|
import { sizes } from 'Helpers/Props';
|
||||||
|
import ReleaseEpisode from 'InteractiveSearch/ReleaseEpisode';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import OverrideMatchModalContent from './OverrideMatchModalContent';
|
||||||
|
|
||||||
|
interface OverrideMatchModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
title: string;
|
||||||
|
indexerId: number;
|
||||||
|
guid: string;
|
||||||
|
seriesId?: number;
|
||||||
|
seasonNumber?: number;
|
||||||
|
episodes: ReleaseEpisode[];
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
isGrabbing: boolean;
|
||||||
|
grabError: string;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverrideMatchModal(props: OverrideMatchModalProps) {
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
title,
|
||||||
|
indexerId,
|
||||||
|
guid,
|
||||||
|
seriesId,
|
||||||
|
seasonNumber,
|
||||||
|
episodes,
|
||||||
|
languages,
|
||||||
|
quality,
|
||||||
|
protocol,
|
||||||
|
isGrabbing,
|
||||||
|
grabError,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} size={sizes.LARGE} onModalClose={onModalClose}>
|
||||||
|
<OverrideMatchModalContent
|
||||||
|
title={title}
|
||||||
|
indexerId={indexerId}
|
||||||
|
guid={guid}
|
||||||
|
seriesId={seriesId}
|
||||||
|
seasonNumber={seasonNumber}
|
||||||
|
episodes={episodes}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -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,391 @@
|
|||||||
|
import React, { useCallback, useEffect, useMemo, 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 EpisodeLanguages from 'Episode/EpisodeLanguages';
|
||||||
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
|
import SelectEpisodeModal from 'InteractiveImport/Episode/SelectEpisodeModal';
|
||||||
|
import SelectLanguageModal from 'InteractiveImport/Language/SelectLanguageModal';
|
||||||
|
import SelectQualityModal from 'InteractiveImport/Quality/SelectQualityModal';
|
||||||
|
import SelectSeasonModal from 'InteractiveImport/Season/SelectSeasonModal';
|
||||||
|
import SelectSeriesModal from 'InteractiveImport/Series/SelectSeriesModal';
|
||||||
|
import ReleaseEpisode from 'InteractiveSearch/ReleaseEpisode';
|
||||||
|
import Language from 'Language/Language';
|
||||||
|
import { QualityModel } from 'Quality/Quality';
|
||||||
|
import Series from 'Series/Series';
|
||||||
|
import { grabRelease } from 'Store/Actions/releaseActions';
|
||||||
|
import { fetchDownloadClients } from 'Store/Actions/settingsActions';
|
||||||
|
import createEnabledDownloadClientsSelector from 'Store/Selectors/createEnabledDownloadClientsSelector';
|
||||||
|
import createSeriesSelector from 'Store/Selectors/createSeriesSelector';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import SelectDownloadClientModal from './DownloadClient/SelectDownloadClientModal';
|
||||||
|
import OverrideMatchData from './OverrideMatchData';
|
||||||
|
import styles from './OverrideMatchModalContent.css';
|
||||||
|
|
||||||
|
type SelectType =
|
||||||
|
| 'select'
|
||||||
|
| 'series'
|
||||||
|
| 'season'
|
||||||
|
| 'episode'
|
||||||
|
| 'quality'
|
||||||
|
| 'language'
|
||||||
|
| 'downloadClient';
|
||||||
|
|
||||||
|
interface OverrideMatchModalContentProps {
|
||||||
|
indexerId: number;
|
||||||
|
title: string;
|
||||||
|
guid: string;
|
||||||
|
seriesId?: number;
|
||||||
|
seasonNumber?: number;
|
||||||
|
episodes: ReleaseEpisode[];
|
||||||
|
languages: Language[];
|
||||||
|
quality: QualityModel;
|
||||||
|
protocol: DownloadProtocol;
|
||||||
|
isGrabbing: boolean;
|
||||||
|
grabError: string;
|
||||||
|
onModalClose(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function OverrideMatchModalContent(props: OverrideMatchModalContentProps) {
|
||||||
|
const modalTitle = 'Manual Grab';
|
||||||
|
const {
|
||||||
|
indexerId,
|
||||||
|
title,
|
||||||
|
guid,
|
||||||
|
protocol,
|
||||||
|
isGrabbing,
|
||||||
|
grabError,
|
||||||
|
onModalClose,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const [seriesId, setSeriesId] = useState(props.seriesId);
|
||||||
|
const [seasonNumber, setSeasonNumber] = useState(props.seasonNumber);
|
||||||
|
const [episodes, setEpisodes] = useState(props.episodes);
|
||||||
|
const [languages, setLanguages] = useState(props.languages);
|
||||||
|
const [quality, setQuality] = useState(props.quality);
|
||||||
|
const [downloadClientId, setDownloadClientId] = useState(null);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [selectModalOpen, setSelectModalOpen] = useState<SelectType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const series: Series = useSelector(createSeriesSelector(seriesId));
|
||||||
|
const { items: downloadClients } = useSelector(
|
||||||
|
createEnabledDownloadClientsSelector(protocol)
|
||||||
|
);
|
||||||
|
|
||||||
|
const episodeInfo = useMemo(() => {
|
||||||
|
return episodes.map((episode) => {
|
||||||
|
return (
|
||||||
|
<div key={episode.id}>
|
||||||
|
{episode.episodeNumber}
|
||||||
|
|
||||||
|
{series.seriesType === 'anime' &&
|
||||||
|
episode.absoluteEpisodeNumber != null
|
||||||
|
? ` (${episode.absoluteEpisodeNumber})`
|
||||||
|
: ''}
|
||||||
|
|
||||||
|
{` - ${episode.title}`}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [series, episodes]);
|
||||||
|
|
||||||
|
const onSelectModalClose = useCallback(() => {
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onSelectSeriesPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('series');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onSeriesSelect = useCallback(
|
||||||
|
(s: Series) => {
|
||||||
|
setSeriesId(s.id);
|
||||||
|
setSeasonNumber(undefined);
|
||||||
|
setEpisodes([]);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setSeriesId, setSeasonNumber, setEpisodes, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectSeasonPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('season');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onSeasonSelect = useCallback(
|
||||||
|
(s: number) => {
|
||||||
|
setSeasonNumber(s);
|
||||||
|
setEpisodes([]);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setSeasonNumber, setEpisodes, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectEpisodePress = useCallback(() => {
|
||||||
|
setSelectModalOpen('episode');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onEpisodesSelect = useCallback(
|
||||||
|
(episodeMap) => {
|
||||||
|
setEpisodes(episodeMap[0].episodes);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setEpisodes, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectQualityPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('quality');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onQualitySelect = useCallback(
|
||||||
|
(quality) => {
|
||||||
|
setQuality(quality);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setQuality, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectLanguagesPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('language');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onLanguagesSelect = useCallback(
|
||||||
|
(languages) => {
|
||||||
|
setLanguages(languages);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setLanguages, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onSelectDownloadClientPress = useCallback(() => {
|
||||||
|
setSelectModalOpen('downloadClient');
|
||||||
|
}, [setSelectModalOpen]);
|
||||||
|
|
||||||
|
const onDownloadClientSelect = useCallback(
|
||||||
|
(downloadClientId) => {
|
||||||
|
setDownloadClientId(downloadClientId);
|
||||||
|
setSelectModalOpen(null);
|
||||||
|
},
|
||||||
|
[setDownloadClientId, setSelectModalOpen]
|
||||||
|
);
|
||||||
|
|
||||||
|
const onGrabPress = useCallback(() => {
|
||||||
|
if (!seriesId) {
|
||||||
|
setError('Series must be selected');
|
||||||
|
return;
|
||||||
|
} else if (!episodes.length) {
|
||||||
|
setError('At least one episode must be selected');
|
||||||
|
return;
|
||||||
|
} else if (!quality) {
|
||||||
|
setError('Quality must be selected');
|
||||||
|
return;
|
||||||
|
} else if (!languages.length) {
|
||||||
|
setError('At least one language must be selected');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(
|
||||||
|
grabRelease({
|
||||||
|
indexerId,
|
||||||
|
guid,
|
||||||
|
seriesId,
|
||||||
|
episodeIds: episodes.map((e) => e.id),
|
||||||
|
quality,
|
||||||
|
languages,
|
||||||
|
downloadClientId,
|
||||||
|
shouldOverride: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}, [
|
||||||
|
indexerId,
|
||||||
|
guid,
|
||||||
|
seriesId,
|
||||||
|
episodes,
|
||||||
|
quality,
|
||||||
|
languages,
|
||||||
|
downloadClientId,
|
||||||
|
setError,
|
||||||
|
dispatch,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
dispatch(fetchDownloadClients());
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ModalContent onModalClose={onModalClose}>
|
||||||
|
<ModalHeader>
|
||||||
|
{translate('Override and Grab - {title}', { title })}
|
||||||
|
</ModalHeader>
|
||||||
|
|
||||||
|
<ModalBody>
|
||||||
|
<DescriptionList>
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('Series')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={series?.title}
|
||||||
|
onPress={onSelectSeriesPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('Season Number')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={seasonNumber}
|
||||||
|
isDisabled={!series}
|
||||||
|
onPress={onSelectSeasonPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('Episodes')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={episodeInfo}
|
||||||
|
isDisabled={!series || isNaN(seasonNumber)}
|
||||||
|
onPress={onSelectEpisodePress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('Quality')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={
|
||||||
|
<EpisodeQuality className={styles.label} quality={quality} />
|
||||||
|
}
|
||||||
|
onPress={onSelectQualityPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('Languages')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={
|
||||||
|
<EpisodeLanguages
|
||||||
|
className={styles.label}
|
||||||
|
languages={languages}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onPress={onSelectLanguagesPress}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{downloadClients.length > 1 ? (
|
||||||
|
<DescriptionListItem
|
||||||
|
className={styles.item}
|
||||||
|
title={translate('Download Client')}
|
||||||
|
data={
|
||||||
|
<OverrideMatchData
|
||||||
|
value={
|
||||||
|
downloadClients.find(
|
||||||
|
(downloadClient) => downloadClient.id === downloadClientId
|
||||||
|
)?.name ?? '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('Grab Release')}
|
||||||
|
</SpinnerErrorButton>
|
||||||
|
</div>
|
||||||
|
</ModalFooter>
|
||||||
|
|
||||||
|
<SelectSeriesModal
|
||||||
|
isOpen={selectModalOpen === 'series'}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onSeriesSelect={onSeriesSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectSeasonModal
|
||||||
|
isOpen={selectModalOpen === 'season'}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
seriesId={seriesId}
|
||||||
|
onSeasonSelect={onSeasonSelect}
|
||||||
|
onModalClose={onSelectModalClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SelectEpisodeModal
|
||||||
|
isOpen={selectModalOpen === 'episode'}
|
||||||
|
selectedIds={[guid]}
|
||||||
|
seriesId={seriesId}
|
||||||
|
isAnime={series.seriesType === 'anime'}
|
||||||
|
seasonNumber={seasonNumber}
|
||||||
|
selectedDetails={title}
|
||||||
|
modalTitle={modalTitle}
|
||||||
|
onEpisodesSelect={onEpisodesSelect}
|
||||||
|
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,10 @@
|
|||||||
|
interface ReleaseEpisode {
|
||||||
|
id: number;
|
||||||
|
episodeFileId: number;
|
||||||
|
seasonNumber: number;
|
||||||
|
episodeNumber: number;
|
||||||
|
absoluteEpisodeNumber?: number;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReleaseEpisode;
|
@ -1,188 +0,0 @@
|
|||||||
import classNames from 'classnames';
|
|
||||||
import _ from 'lodash';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import React from 'react';
|
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
|
||||||
import Icon from 'Components/Icon';
|
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
|
||||||
import { icons, tooltipPositions } from 'Helpers/Props';
|
|
||||||
import styles from './ReleaseSceneIndicator.css';
|
|
||||||
|
|
||||||
function formatReleaseNumber(seasonNumber, episodeNumbers, absoluteEpisodeNumbers) {
|
|
||||||
if (episodeNumbers && episodeNumbers.length) {
|
|
||||||
if (episodeNumbers.length > 1) {
|
|
||||||
return `${seasonNumber}x${episodeNumbers[0]}-${episodeNumbers[episodeNumbers.length - 1]}`;
|
|
||||||
}
|
|
||||||
return `${seasonNumber}x${episodeNumbers[0]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (absoluteEpisodeNumbers && absoluteEpisodeNumbers.length) {
|
|
||||||
if (absoluteEpisodeNumbers.length > 1) {
|
|
||||||
return `${absoluteEpisodeNumbers[0]}-${absoluteEpisodeNumbers[absoluteEpisodeNumbers.length - 1]}`;
|
|
||||||
}
|
|
||||||
return absoluteEpisodeNumbers[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (seasonNumber !== undefined) {
|
|
||||||
return `Season ${seasonNumber}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ReleaseSceneIndicator(props) {
|
|
||||||
const {
|
|
||||||
className,
|
|
||||||
seasonNumber,
|
|
||||||
episodeNumbers,
|
|
||||||
absoluteEpisodeNumbers,
|
|
||||||
sceneSeasonNumber,
|
|
||||||
sceneEpisodeNumbers,
|
|
||||||
sceneAbsoluteEpisodeNumbers,
|
|
||||||
sceneMapping,
|
|
||||||
episodeRequested,
|
|
||||||
isDaily
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const {
|
|
||||||
sceneOrigin,
|
|
||||||
title,
|
|
||||||
comment
|
|
||||||
} = sceneMapping || {};
|
|
||||||
|
|
||||||
if (isDaily) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mappingDifferent = (sceneSeasonNumber !== undefined && seasonNumber !== sceneSeasonNumber);
|
|
||||||
|
|
||||||
if (sceneEpisodeNumbers !== undefined) {
|
|
||||||
mappingDifferent = mappingDifferent || !_.isEqual(sceneEpisodeNumbers, episodeNumbers);
|
|
||||||
} else if (sceneAbsoluteEpisodeNumbers !== undefined) {
|
|
||||||
mappingDifferent = mappingDifferent || !_.isEqual(sceneAbsoluteEpisodeNumbers, absoluteEpisodeNumbers);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sceneMapping && !mappingDifferent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const releaseNumber = formatReleaseNumber(sceneSeasonNumber, sceneEpisodeNumbers, sceneAbsoluteEpisodeNumbers);
|
|
||||||
const mappedNumber = formatReleaseNumber(seasonNumber, episodeNumbers, absoluteEpisodeNumbers);
|
|
||||||
const messages = [];
|
|
||||||
|
|
||||||
const isMixed = (sceneOrigin === 'mixed');
|
|
||||||
const isUnknown = (sceneOrigin === 'unknown' || sceneOrigin === 'unknown:tvdb');
|
|
||||||
|
|
||||||
let level = styles.levelNone;
|
|
||||||
|
|
||||||
if (isMixed) {
|
|
||||||
level = styles.levelMixed;
|
|
||||||
messages.push(<div key="source">{comment ?? 'Source'} releases exist with ambiguous numbering, unable to reliably identify episode.</div>);
|
|
||||||
} else if (isUnknown) {
|
|
||||||
level = styles.levelUnknown;
|
|
||||||
messages.push(<div key="unknown">Numbering varies for this episode and release does not match any known mappings.</div>);
|
|
||||||
if (sceneOrigin === 'unknown') {
|
|
||||||
messages.push(<div key="origin">Assuming Scene numbering.</div>);
|
|
||||||
} else if (sceneOrigin === 'unknown:tvdb') {
|
|
||||||
messages.push(<div key="origin">Assuming TheTVDB numbering.</div>);
|
|
||||||
}
|
|
||||||
} else if (mappingDifferent) {
|
|
||||||
level = styles.levelMapped;
|
|
||||||
} else if (sceneOrigin) {
|
|
||||||
level = styles.levelNormal;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!episodeRequested) {
|
|
||||||
if (!isMixed && !isUnknown) {
|
|
||||||
level = styles.levelNotRequested;
|
|
||||||
}
|
|
||||||
if (mappedNumber) {
|
|
||||||
messages.push(<div key="not-requested">Mapped episode wasn't requested in this search.</div>);
|
|
||||||
} else {
|
|
||||||
messages.push(<div key="unknown-series">Unknown episode or series.</div>);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const table = (
|
|
||||||
<DescriptionList className={styles.descriptionList}>
|
|
||||||
{
|
|
||||||
comment !== undefined &&
|
|
||||||
<DescriptionListItem
|
|
||||||
titleClassName={styles.title}
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title="Mapping"
|
|
||||||
data={comment}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
title !== undefined &&
|
|
||||||
<DescriptionListItem
|
|
||||||
titleClassName={styles.title}
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title="Title"
|
|
||||||
data={title}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
releaseNumber !== undefined &&
|
|
||||||
<DescriptionListItem
|
|
||||||
titleClassName={styles.title}
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title="Release"
|
|
||||||
data={releaseNumber ?? 'unknown'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
releaseNumber !== undefined &&
|
|
||||||
<DescriptionListItem
|
|
||||||
titleClassName={styles.title}
|
|
||||||
descriptionClassName={styles.description}
|
|
||||||
title="TheTVDB"
|
|
||||||
data={mappedNumber ?? 'unknown'}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
</DescriptionList>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Popover
|
|
||||||
anchor={
|
|
||||||
<div className={classNames(level, styles.container, className)}>
|
|
||||||
<Icon name={icons.SCENE_MAPPING} />
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
title="Scene Info"
|
|
||||||
body={
|
|
||||||
<div>
|
|
||||||
{table}
|
|
||||||
{
|
|
||||||
messages.length &&
|
|
||||||
<div className={styles.messages}>
|
|
||||||
{messages}
|
|
||||||
</div> || null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
position={tooltipPositions.RIGHT}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ReleaseSceneIndicator.propTypes = {
|
|
||||||
className: PropTypes.string.isRequired,
|
|
||||||
seasonNumber: PropTypes.number,
|
|
||||||
episodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
|
||||||
absoluteEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
|
||||||
sceneSeasonNumber: PropTypes.number,
|
|
||||||
sceneEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
|
||||||
sceneAbsoluteEpisodeNumbers: PropTypes.arrayOf(PropTypes.number),
|
|
||||||
sceneMapping: PropTypes.object.isRequired,
|
|
||||||
episodeRequested: PropTypes.bool.isRequired,
|
|
||||||
isDaily: PropTypes.bool.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReleaseSceneIndicator;
|
|
@ -0,0 +1,215 @@
|
|||||||
|
import classNames from 'classnames';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import { icons, tooltipPositions } from 'Helpers/Props';
|
||||||
|
import styles from './ReleaseSceneIndicator.css';
|
||||||
|
|
||||||
|
function formatReleaseNumber(
|
||||||
|
seasonNumber,
|
||||||
|
episodeNumbers,
|
||||||
|
absoluteEpisodeNumbers
|
||||||
|
) {
|
||||||
|
if (episodeNumbers && episodeNumbers.length) {
|
||||||
|
if (episodeNumbers.length > 1) {
|
||||||
|
return `${seasonNumber}x${episodeNumbers[0]}-${
|
||||||
|
episodeNumbers[episodeNumbers.length - 1]
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
return `${seasonNumber}x${episodeNumbers[0]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absoluteEpisodeNumbers && absoluteEpisodeNumbers.length) {
|
||||||
|
if (absoluteEpisodeNumbers.length > 1) {
|
||||||
|
return `${absoluteEpisodeNumbers[0]}-${
|
||||||
|
absoluteEpisodeNumbers[absoluteEpisodeNumbers.length - 1]
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
return absoluteEpisodeNumbers[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seasonNumber !== undefined) {
|
||||||
|
return `Season ${seasonNumber}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReleaseSceneIndicatorProps {
|
||||||
|
className: string;
|
||||||
|
seasonNumber?: number;
|
||||||
|
episodeNumbers?: number[];
|
||||||
|
absoluteEpisodeNumbers?: number[];
|
||||||
|
sceneSeasonNumber?: number;
|
||||||
|
sceneEpisodeNumbers?: number[];
|
||||||
|
sceneAbsoluteEpisodeNumbers?: number[];
|
||||||
|
sceneMapping?: {
|
||||||
|
sceneOrigin?: string;
|
||||||
|
title?: string;
|
||||||
|
comment?: string;
|
||||||
|
};
|
||||||
|
episodeRequested: boolean;
|
||||||
|
isDaily: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReleaseSceneIndicator(props: ReleaseSceneIndicatorProps) {
|
||||||
|
const {
|
||||||
|
className,
|
||||||
|
seasonNumber,
|
||||||
|
episodeNumbers,
|
||||||
|
absoluteEpisodeNumbers,
|
||||||
|
sceneSeasonNumber,
|
||||||
|
sceneEpisodeNumbers,
|
||||||
|
sceneAbsoluteEpisodeNumbers,
|
||||||
|
sceneMapping = {},
|
||||||
|
episodeRequested,
|
||||||
|
isDaily,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { sceneOrigin, title, comment } = sceneMapping;
|
||||||
|
|
||||||
|
if (isDaily) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mappingDifferent =
|
||||||
|
sceneSeasonNumber !== undefined && seasonNumber !== sceneSeasonNumber;
|
||||||
|
|
||||||
|
if (sceneEpisodeNumbers !== undefined) {
|
||||||
|
mappingDifferent =
|
||||||
|
mappingDifferent || !_.isEqual(sceneEpisodeNumbers, episodeNumbers);
|
||||||
|
} else if (sceneAbsoluteEpisodeNumbers !== undefined) {
|
||||||
|
mappingDifferent =
|
||||||
|
mappingDifferent ||
|
||||||
|
!_.isEqual(sceneAbsoluteEpisodeNumbers, absoluteEpisodeNumbers);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sceneMapping && !mappingDifferent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const releaseNumber = formatReleaseNumber(
|
||||||
|
sceneSeasonNumber,
|
||||||
|
sceneEpisodeNumbers,
|
||||||
|
sceneAbsoluteEpisodeNumbers
|
||||||
|
);
|
||||||
|
const mappedNumber = formatReleaseNumber(
|
||||||
|
seasonNumber,
|
||||||
|
episodeNumbers,
|
||||||
|
absoluteEpisodeNumbers
|
||||||
|
);
|
||||||
|
const messages = [];
|
||||||
|
|
||||||
|
const isMixed = sceneOrigin === 'mixed';
|
||||||
|
const isUnknown = sceneOrigin === 'unknown' || sceneOrigin === 'unknown:tvdb';
|
||||||
|
|
||||||
|
let level = styles.levelNone;
|
||||||
|
|
||||||
|
if (isMixed) {
|
||||||
|
level = styles.levelMixed;
|
||||||
|
messages.push(
|
||||||
|
<div key="source">
|
||||||
|
{comment ?? 'Source'} releases exist with ambiguous numbering, unable to
|
||||||
|
reliably identify episode.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (isUnknown) {
|
||||||
|
level = styles.levelUnknown;
|
||||||
|
messages.push(
|
||||||
|
<div key="unknown">
|
||||||
|
Numbering varies for this episode and release does not match any known
|
||||||
|
mappings.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
if (sceneOrigin === 'unknown') {
|
||||||
|
messages.push(<div key="origin">Assuming Scene numbering.</div>);
|
||||||
|
} else if (sceneOrigin === 'unknown:tvdb') {
|
||||||
|
messages.push(<div key="origin">Assuming TheTVDB numbering.</div>);
|
||||||
|
}
|
||||||
|
} else if (mappingDifferent) {
|
||||||
|
level = styles.levelMapped;
|
||||||
|
} else if (sceneOrigin) {
|
||||||
|
level = styles.levelNormal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!episodeRequested) {
|
||||||
|
if (!isMixed && !isUnknown) {
|
||||||
|
level = styles.levelNotRequested;
|
||||||
|
}
|
||||||
|
if (mappedNumber) {
|
||||||
|
messages.push(
|
||||||
|
<div key="not-requested">
|
||||||
|
Mapped episode wasn't requested in this search.
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
messages.push(<div key="unknown-series">Unknown episode or series.</div>);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const table = (
|
||||||
|
<DescriptionList className={styles.descriptionList}>
|
||||||
|
{comment !== undefined && (
|
||||||
|
<DescriptionListItem
|
||||||
|
titleClassName={styles.title}
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Mapping"
|
||||||
|
data={comment}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{title !== undefined && (
|
||||||
|
<DescriptionListItem
|
||||||
|
titleClassName={styles.title}
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Title"
|
||||||
|
data={title}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{releaseNumber !== undefined && (
|
||||||
|
<DescriptionListItem
|
||||||
|
titleClassName={styles.title}
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="Release"
|
||||||
|
data={releaseNumber ?? 'unknown'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{releaseNumber !== undefined && (
|
||||||
|
<DescriptionListItem
|
||||||
|
titleClassName={styles.title}
|
||||||
|
descriptionClassName={styles.description}
|
||||||
|
title="TheTVDB"
|
||||||
|
data={mappedNumber ?? 'unknown'}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DescriptionList>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover
|
||||||
|
anchor={
|
||||||
|
<div className={classNames(level, styles.container, className)}>
|
||||||
|
<Icon name={icons.SCENE_MAPPING} />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
title="Scene Info"
|
||||||
|
body={
|
||||||
|
<div>
|
||||||
|
{table}
|
||||||
|
{(messages.length && (
|
||||||
|
<div className={styles.messages}>{messages}</div>
|
||||||
|
)) ||
|
||||||
|
null}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
position={tooltipPositions.RIGHT}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReleaseSceneIndicator;
|
@ -0,0 +1,21 @@
|
|||||||
|
import { createSelector } from 'reselect';
|
||||||
|
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) => {
|
||||||
|
const { isFetching, isPopulated, error, items } = downloadClients;
|
||||||
|
|
||||||
|
const clients = items.filter(
|
||||||
|
(item) => item.protocol === protocol && item.enable
|
||||||
|
);
|
||||||
|
|
||||||
|
return { isFetching, isPopulated, error, items: clients };
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in new issue