New: Added Scene Info to Interactive Search results to show more about the applied scene/TheXEM mappings
This commit is contained in:
parent
dcda03da4a
commit
5668152d6f
|
@ -1,26 +1,13 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Fragment } from 'react';
|
import React, { Fragment } from 'react';
|
||||||
import padNumber from 'Utilities/Number/padNumber';
|
import padNumber from 'Utilities/Number/padNumber';
|
||||||
|
import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles';
|
||||||
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
|
||||||
import Icon from 'Components/Icon';
|
import Icon from 'Components/Icon';
|
||||||
import Popover from 'Components/Tooltip/Popover';
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
import SceneInfo from './SceneInfo';
|
import SceneInfo from './SceneInfo';
|
||||||
import styles from './EpisodeNumber.css';
|
import styles from './EpisodeNumber.css';
|
||||||
|
|
||||||
function getAlternateTitles(seasonNumber, sceneSeasonNumber, alternateTitles) {
|
|
||||||
return alternateTitles.filter((alternateTitle) => {
|
|
||||||
if (sceneSeasonNumber && sceneSeasonNumber === alternateTitle.sceneSeasonNumber) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alternateTitle.sceneSeasonNumber === undefined && alternateTitle.sceneOrigin === 'tvdb') {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return seasonNumber === alternateTitle.seasonNumber;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getWarningMessage(unverifiedSceneNumbering, seriesType, absoluteEpisodeNumber) {
|
function getWarningMessage(unverifiedSceneNumbering, seriesType, absoluteEpisodeNumber) {
|
||||||
const messages = [];
|
const messages = [];
|
||||||
|
|
||||||
|
@ -43,13 +30,14 @@ function EpisodeNumber(props) {
|
||||||
sceneSeasonNumber,
|
sceneSeasonNumber,
|
||||||
sceneEpisodeNumber,
|
sceneEpisodeNumber,
|
||||||
sceneAbsoluteEpisodeNumber,
|
sceneAbsoluteEpisodeNumber,
|
||||||
|
useSceneNumbering,
|
||||||
unverifiedSceneNumbering,
|
unverifiedSceneNumbering,
|
||||||
alternateTitles: seriesAlternateTitles,
|
alternateTitles: seriesAlternateTitles,
|
||||||
seriesType,
|
seriesType,
|
||||||
showSeasonNumber
|
showSeasonNumber
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const alternateTitles = getAlternateTitles(seasonNumber, sceneSeasonNumber, seriesAlternateTitles);
|
const alternateTitles = filterAlternateTitles(seriesAlternateTitles, null, useSceneNumbering, seasonNumber, sceneSeasonNumber);
|
||||||
|
|
||||||
const hasSceneInformation = sceneSeasonNumber !== undefined ||
|
const hasSceneInformation = sceneSeasonNumber !== undefined ||
|
||||||
sceneEpisodeNumber !== undefined ||
|
sceneEpisodeNumber !== undefined ||
|
||||||
|
@ -137,6 +125,7 @@ EpisodeNumber.propTypes = {
|
||||||
sceneSeasonNumber: PropTypes.number,
|
sceneSeasonNumber: PropTypes.number,
|
||||||
sceneEpisodeNumber: PropTypes.number,
|
sceneEpisodeNumber: PropTypes.number,
|
||||||
sceneAbsoluteEpisodeNumber: PropTypes.number,
|
sceneAbsoluteEpisodeNumber: PropTypes.number,
|
||||||
|
useSceneNumbering: PropTypes.bool.isRequired,
|
||||||
unverifiedSceneNumbering: PropTypes.bool.isRequired,
|
unverifiedSceneNumbering: PropTypes.bool.isRequired,
|
||||||
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
seriesType: PropTypes.string,
|
seriesType: PropTypes.string,
|
||||||
|
@ -144,6 +133,7 @@ EpisodeNumber.propTypes = {
|
||||||
};
|
};
|
||||||
|
|
||||||
EpisodeNumber.defaultProps = {
|
EpisodeNumber.defaultProps = {
|
||||||
|
useSceneNumbering: false,
|
||||||
unverifiedSceneNumbering: false,
|
unverifiedSceneNumbering: false,
|
||||||
alternateTitles: [],
|
alternateTitles: [],
|
||||||
showSeasonNumber: false
|
showSeasonNumber: false
|
||||||
|
|
|
@ -179,6 +179,7 @@ export const RESTORE = fasHistory;
|
||||||
export const REORDER = fasBars;
|
export const REORDER = fasBars;
|
||||||
export const RSS = fasRss;
|
export const RSS = fasRss;
|
||||||
export const SAVE = fasSave;
|
export const SAVE = fasSave;
|
||||||
|
export const SCENE_MAPPING = fasSitemap;
|
||||||
export const SCHEDULED = farClock;
|
export const SCHEDULED = farClock;
|
||||||
export const SCORE = fasUserPlus;
|
export const SCORE = fasUserPlus;
|
||||||
export const SEARCH = fasSearch;
|
export const SEARCH = fasSearch;
|
||||||
|
|
|
@ -8,6 +8,13 @@
|
||||||
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
composes: cell from '~Components/Table/Cells/TableRowCell.css';
|
||||||
|
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sceneMapping {
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.indexer {
|
.indexer {
|
||||||
|
|
|
@ -14,6 +14,7 @@ import Popover from 'Components/Tooltip/Popover';
|
||||||
import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
import EpisodeLanguage from 'Episode/EpisodeLanguage';
|
||||||
import EpisodeQuality from 'Episode/EpisodeQuality';
|
import EpisodeQuality from 'Episode/EpisodeQuality';
|
||||||
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
|
||||||
|
import ReleaseSceneIndicator from './ReleaseSceneIndicator';
|
||||||
import Peers from './Peers';
|
import Peers from './Peers';
|
||||||
import styles from './InteractiveSearchRow.css';
|
import styles from './InteractiveSearchRow.css';
|
||||||
|
|
||||||
|
@ -114,8 +115,17 @@ class InteractiveSearchRow extends Component {
|
||||||
quality,
|
quality,
|
||||||
language,
|
language,
|
||||||
preferredWordScore,
|
preferredWordScore,
|
||||||
|
sceneMapping,
|
||||||
|
seasonNumber,
|
||||||
|
episodeNumbers,
|
||||||
|
absoluteEpisodeNumbers,
|
||||||
|
mappedSeasonNumber,
|
||||||
|
mappedEpisodeNumbers,
|
||||||
|
mappedAbsoluteEpisodeNumbers,
|
||||||
rejections,
|
rejections,
|
||||||
|
episodeRequested,
|
||||||
downloadAllowed,
|
downloadAllowed,
|
||||||
|
isDaily,
|
||||||
isGrabbing,
|
isGrabbing,
|
||||||
isGrabbed,
|
isGrabbed,
|
||||||
longDateFormat,
|
longDateFormat,
|
||||||
|
@ -142,6 +152,18 @@ class InteractiveSearchRow extends Component {
|
||||||
<Link to={infoUrl}>
|
<Link to={infoUrl}>
|
||||||
{title}
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
|
<ReleaseSceneIndicator
|
||||||
|
className={styles.sceneMapping}
|
||||||
|
seasonNumber={mappedSeasonNumber}
|
||||||
|
episodeNumbers={mappedEpisodeNumbers}
|
||||||
|
absoluteEpisodeNumbers={mappedAbsoluteEpisodeNumbers}
|
||||||
|
sceneSeasonNumber={seasonNumber}
|
||||||
|
sceneEpisodeNumbers={episodeNumbers}
|
||||||
|
sceneAbsoluteEpisodeNumbers={absoluteEpisodeNumbers}
|
||||||
|
sceneMapping={sceneMapping}
|
||||||
|
episodeRequested={episodeRequested}
|
||||||
|
isDaily={isDaily}
|
||||||
|
/>
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<TableRowCell className={styles.indexer}>
|
<TableRowCell className={styles.indexer}>
|
||||||
|
@ -245,8 +267,17 @@ InteractiveSearchRow.propTypes = {
|
||||||
quality: PropTypes.object.isRequired,
|
quality: PropTypes.object.isRequired,
|
||||||
language: PropTypes.object.isRequired,
|
language: PropTypes.object.isRequired,
|
||||||
preferredWordScore: PropTypes.number.isRequired,
|
preferredWordScore: 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,
|
rejections: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
|
episodeRequested: PropTypes.bool.isRequired,
|
||||||
downloadAllowed: PropTypes.bool.isRequired,
|
downloadAllowed: PropTypes.bool.isRequired,
|
||||||
|
isDaily: PropTypes.bool.isRequired,
|
||||||
isGrabbing: PropTypes.bool.isRequired,
|
isGrabbing: PropTypes.bool.isRequired,
|
||||||
isGrabbed: PropTypes.bool.isRequired,
|
isGrabbed: PropTypes.bool.isRequired,
|
||||||
grabError: PropTypes.string,
|
grabError: PropTypes.string,
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
.container {
|
||||||
|
margin: 2px;
|
||||||
|
border: 1px solid;
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 0 2px;
|
||||||
|
cursor: default;
|
||||||
|
font-size: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.messages {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.descriptionList {
|
||||||
|
composes: descriptionList from '~Components/DescriptionList/DescriptionList.css';
|
||||||
|
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
composes: title from '~Components/DescriptionList/DescriptionListItemTitle.css';
|
||||||
|
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
composes: title from '~Components/DescriptionList/DescriptionListItemDescription.css';
|
||||||
|
|
||||||
|
margin-left: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelMixed {
|
||||||
|
color: $dangerColor;
|
||||||
|
border-color: $dangerColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelUnknown {
|
||||||
|
color: $warningColor;
|
||||||
|
border-color: $warningColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelMapped {
|
||||||
|
color: $textColor;
|
||||||
|
border-color: $textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelNormal {
|
||||||
|
color: $textColor;
|
||||||
|
border-color: $textColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelNone {
|
||||||
|
opacity: 0.2;
|
||||||
|
color: $textColor;
|
||||||
|
border-color: $textColor;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.levelNotRequested {
|
||||||
|
color: $dangerColor;
|
||||||
|
border-color: $dangerColor;
|
||||||
|
}
|
|
@ -0,0 +1,187 @@
|
||||||
|
import _ from 'lodash';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import classNames from 'classNames';
|
||||||
|
import { tooltipPositions, icons, sizes } from 'Helpers/Props';
|
||||||
|
import styles from './ReleaseSceneIndicator.css';
|
||||||
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
|
import Popover from 'Components/Tooltip/Popover';
|
||||||
|
import Icon from 'Components/Icon';
|
||||||
|
|
||||||
|
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>{comment ?? 'Source'} releases exist with ambiguous numbering, unable to reliably identify episode.</div>);
|
||||||
|
} else if (isUnknown) {
|
||||||
|
level = styles.levelUnknown;
|
||||||
|
messages.push(<div>Numbering varies for this episode and release does not match any known mappings.</div>);
|
||||||
|
if (sceneOrigin === 'unknown') {
|
||||||
|
messages.push(<div>Assuming Scene numbering.</div>);
|
||||||
|
} else if (sceneOrigin === 'unknown:tvdb') {
|
||||||
|
messages.push(<div>Assuming TheTVDB numbering.</div>);
|
||||||
|
}
|
||||||
|
} else if (mappingDifferent) {
|
||||||
|
level = styles.levelMapped;
|
||||||
|
} else if (sceneOrigin) {
|
||||||
|
level = styles.levelNormal;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!episodeRequested) {
|
||||||
|
if (!isMixed && !isUnknown) {
|
||||||
|
level = styles.levelNotRequested;
|
||||||
|
}
|
||||||
|
messages.push(<div>Mapped episode wasn't requested in this search.</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}
|
||||||
|
size={sizes.SMALL}
|
||||||
|
/>
|
||||||
|
</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;
|
|
@ -60,6 +60,7 @@ class EpisodeRow extends Component {
|
||||||
sceneAbsoluteEpisodeNumber,
|
sceneAbsoluteEpisodeNumber,
|
||||||
airDateUtc,
|
airDateUtc,
|
||||||
title,
|
title,
|
||||||
|
useSceneNumbering,
|
||||||
unverifiedSceneNumbering,
|
unverifiedSceneNumbering,
|
||||||
isSaving,
|
isSaving,
|
||||||
seriesMonitored,
|
seriesMonitored,
|
||||||
|
@ -110,6 +111,7 @@ class EpisodeRow extends Component {
|
||||||
seasonNumber={seasonNumber}
|
seasonNumber={seasonNumber}
|
||||||
episodeNumber={episodeNumber}
|
episodeNumber={episodeNumber}
|
||||||
absoluteEpisodeNumber={absoluteEpisodeNumber}
|
absoluteEpisodeNumber={absoluteEpisodeNumber}
|
||||||
|
useSceneNumbering={useSceneNumbering}
|
||||||
unverifiedSceneNumbering={unverifiedSceneNumbering}
|
unverifiedSceneNumbering={unverifiedSceneNumbering}
|
||||||
seriesType={seriesType}
|
seriesType={seriesType}
|
||||||
sceneSeasonNumber={sceneSeasonNumber}
|
sceneSeasonNumber={sceneSeasonNumber}
|
||||||
|
@ -265,6 +267,7 @@ EpisodeRow.propTypes = {
|
||||||
airDateUtc: PropTypes.string,
|
airDateUtc: PropTypes.string,
|
||||||
title: PropTypes.string.isRequired,
|
title: PropTypes.string.isRequired,
|
||||||
isSaving: PropTypes.bool,
|
isSaving: PropTypes.bool,
|
||||||
|
useSceneNumbering: PropTypes.bool,
|
||||||
unverifiedSceneNumbering: PropTypes.bool,
|
unverifiedSceneNumbering: PropTypes.bool,
|
||||||
seriesMonitored: PropTypes.bool.isRequired,
|
seriesMonitored: PropTypes.bool.isRequired,
|
||||||
seriesType: PropTypes.string.isRequired,
|
seriesType: PropTypes.string.isRequired,
|
||||||
|
|
|
@ -11,6 +11,7 @@ function createMapStateToProps() {
|
||||||
createEpisodeFileSelector(),
|
createEpisodeFileSelector(),
|
||||||
(series = {}, episodeFile) => {
|
(series = {}, episodeFile) => {
|
||||||
return {
|
return {
|
||||||
|
useSceneNumbering: series.useSceneNumbering,
|
||||||
seriesMonitored: series.monitored,
|
seriesMonitored: series.monitored,
|
||||||
seriesType: series.seriesType,
|
seriesType: series.seriesType,
|
||||||
episodeFilePath: episodeFile ? episodeFile.path : null,
|
episodeFilePath: episodeFile ? episodeFile.path : null,
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
import { findCommand, isCommandExecuting } from 'Utilities/Command';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
|
import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles';
|
||||||
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
|
||||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||||
import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions';
|
import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions';
|
||||||
|
@ -109,15 +110,7 @@ function createMapStateToProps() {
|
||||||
|
|
||||||
const isFetching = isEpisodesFetching || isEpisodeFilesFetching;
|
const isFetching = isEpisodesFetching || isEpisodeFilesFetching;
|
||||||
const isPopulated = isEpisodesPopulated && isEpisodeFilesPopulated;
|
const isPopulated = isEpisodesPopulated && isEpisodeFilesPopulated;
|
||||||
const alternateTitles = _.reduce(series.alternateTitles, (acc, alternateTitle) => {
|
const alternateTitles = filterAlternateTitles(series.alternateTitle, series.title, series.useSceneNumbering);
|
||||||
if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) &&
|
|
||||||
(alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined) &&
|
|
||||||
(alternateTitle.title !== series.title)) {
|
|
||||||
acc.push(alternateTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...series,
|
...series,
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
|
||||||
|
function filterAlternateTitles(alternateTitles, seriesTitle, useSceneNumbering, seasonNumber, sceneSeasonNumber) {
|
||||||
|
const globalTitles = [];
|
||||||
|
const seasonTitles = [];
|
||||||
|
|
||||||
|
if (alternateTitles) {
|
||||||
|
alternateTitles.forEach((alternateTitle) => {
|
||||||
|
if (alternateTitle.sceneOrigin === 'unknown' || alternateTitle.sceneOrigin === 'unknown:tvdb') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alternateTitle.sceneOrigin === 'mixed') {
|
||||||
|
// For now filter out 'mixed' from the UI, the user will get an rejection during manual search.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAltSeasonNumber = (alternateTitle.seasonNumber !== -1 && alternateTitle.seasonNumber !== undefined);
|
||||||
|
const hasAltSceneSeasonNumber = (alternateTitle.sceneSeasonNumber !== -1 && alternateTitle.sceneSeasonNumber !== undefined);
|
||||||
|
|
||||||
|
if (!hasAltSeasonNumber && !hasAltSceneSeasonNumber &&
|
||||||
|
(alternateTitle.title !== seriesTitle) &&
|
||||||
|
(!alternateTitle.sceneOrigin || !useSceneNumbering)) {
|
||||||
|
globalTitles.push(alternateTitle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((sceneSeasonNumber !== undefined && sceneSeasonNumber === alternateTitle.sceneSeasonNumber) ||
|
||||||
|
(seasonNumber !== undefined && seasonNumber === alternateTitle.seasonNumber) ||
|
||||||
|
(!hasAltSeasonNumber && !hasAltSceneSeasonNumber && alternateTitle.sceneOrigin && useSceneNumbering)) {
|
||||||
|
seasonTitles.push(alternateTitle);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seasonNumber === undefined) {
|
||||||
|
return globalTitles;
|
||||||
|
}
|
||||||
|
|
||||||
|
return seasonTitles;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default filterAlternateTitles;
|
|
@ -1,4 +1,6 @@
|
||||||
namespace NzbDrone.Api.Series
|
using NzbDrone.Core.DataAugmentation.Scene;
|
||||||
|
|
||||||
|
namespace NzbDrone.Api.Series
|
||||||
{
|
{
|
||||||
public class AlternateTitleResource
|
public class AlternateTitleResource
|
||||||
{
|
{
|
||||||
|
@ -8,4 +10,24 @@
|
||||||
public string SceneOrigin { get; set; }
|
public string SceneOrigin { get; set; }
|
||||||
public string Comment { get; set; }
|
public string Comment { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class AlternateTitleResourceMapper
|
||||||
|
{
|
||||||
|
public static AlternateTitleResource ToResource(this SceneMapping sceneMapping)
|
||||||
|
{
|
||||||
|
if (sceneMapping == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AlternateTitleResource
|
||||||
|
{
|
||||||
|
Title = sceneMapping.Title,
|
||||||
|
SeasonNumber = sceneMapping.SeasonNumber,
|
||||||
|
SceneSeasonNumber = sceneMapping.SceneSeasonNumber,
|
||||||
|
SceneOrigin = sceneMapping.SceneOrigin,
|
||||||
|
Comment = sceneMapping.Comment
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -222,14 +222,7 @@ namespace NzbDrone.Api.Series
|
||||||
|
|
||||||
if (mappings == null) return;
|
if (mappings == null) return;
|
||||||
|
|
||||||
resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource
|
resource.AlternateTitles = mappings.ConvertAll(AlternateTitleResourceMapper.ToResource);
|
||||||
{
|
|
||||||
Title = v.Title,
|
|
||||||
SeasonNumber = v.SeasonNumber,
|
|
||||||
SceneSeasonNumber = v.SceneSeasonNumber,
|
|
||||||
SceneOrigin = v.SceneOrigin,
|
|
||||||
Comment = v.Comment
|
|
||||||
}).ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Handle(EpisodeImportedEvent message)
|
public void Handle(EpisodeImportedEvent message)
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
using System.Linq;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.DataAugmentation.Scene;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.DecisionEngine.Specifications.Search
|
||||||
|
{
|
||||||
|
public class SceneMappingSpecification : IDecisionEngineSpecification
|
||||||
|
{
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public SceneMappingSpecification(Logger logger)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||||
|
public RejectionType Type => RejectionType.Temporary; // Temporary till there's a mapping
|
||||||
|
|
||||||
|
public Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SearchCriteriaBase searchCriteria)
|
||||||
|
{
|
||||||
|
if (remoteEpisode.SceneMapping == null)
|
||||||
|
{
|
||||||
|
_logger.Debug("No applicable scene mapping, skipping.");
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteEpisode.SceneMapping.SceneOrigin.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
_logger.Debug("No explicit scene origin in scene mapping.");
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var split = remoteEpisode.SceneMapping.SceneOrigin.Split(':');
|
||||||
|
|
||||||
|
var isInteractive = (searchCriteria != null && searchCriteria.InteractiveSearch);
|
||||||
|
|
||||||
|
if (remoteEpisode.SceneMapping.Comment.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
_logger.Debug("SceneMapping has origin {0} with comment '{1}'.", remoteEpisode.SceneMapping.SceneOrigin, remoteEpisode.SceneMapping.Comment);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Debug("SceneMapping has origin {0}.", remoteEpisode.SceneMapping.SceneOrigin);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (split[0] == "mixed")
|
||||||
|
{
|
||||||
|
_logger.Debug("SceneMapping origin is explicitly mixed, this means these were released with multiple unidentifiable numbering schemes.");
|
||||||
|
|
||||||
|
if (remoteEpisode.SceneMapping.Comment.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
return Decision.Reject("{0} has ambiguous numbering");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return Decision.Reject("Ambiguous numbering");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (split[0] == "unknown")
|
||||||
|
{
|
||||||
|
var type = split.Length >= 2 ? split[1] : "scene";
|
||||||
|
|
||||||
|
_logger.Debug("SceneMapping origin is explicitly unknown, unsure what numbering scheme it uses but '{0}' will be assumed. Provide full release title to Sonarr/TheXEM team.", type);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -247,7 +247,7 @@ namespace NzbDrone.Core.IndexerSearch
|
||||||
// By default we do a alt title search in case indexers don't have the release properly indexed. Services can override this behavior.
|
// By default we do a alt title search in case indexers don't have the release properly indexed. Services can override this behavior.
|
||||||
var searchMode = sceneMapping.SearchMode ?? ((sceneMapping.SceneSeasonNumber ?? -1) != -1 ? SearchMode.SearchTitle : SearchMode.Default);
|
var searchMode = sceneMapping.SearchMode ?? ((sceneMapping.SceneSeasonNumber ?? -1) != -1 ? SearchMode.SearchTitle : SearchMode.Default);
|
||||||
|
|
||||||
if (sceneMapping.SceneOrigin == "tvdb")
|
if (sceneMapping.SceneOrigin == "tvdb" || sceneMapping.SceneOrigin == "unknown:tvdb")
|
||||||
{
|
{
|
||||||
yield return new SceneEpisodeMapping
|
yield return new SceneEpisodeMapping
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.DataAugmentation.Scene;
|
||||||
using NzbDrone.Core.Download.Clients;
|
using NzbDrone.Core.Download.Clients;
|
||||||
using NzbDrone.Core.Tv;
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
@ -10,10 +11,12 @@ namespace NzbDrone.Core.Parser.Model
|
||||||
{
|
{
|
||||||
public ReleaseInfo Release { get; set; }
|
public ReleaseInfo Release { get; set; }
|
||||||
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
|
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
|
||||||
|
public SceneMapping SceneMapping { get; set; }
|
||||||
public int MappedSeasonNumber { get; set; }
|
public int MappedSeasonNumber { get; set; }
|
||||||
|
|
||||||
public Series Series { get; set; }
|
public Series Series { get; set; }
|
||||||
public List<Episode> Episodes { get; set; }
|
public List<Episode> Episodes { get; set; }
|
||||||
|
public bool EpisodeRequested { get; set; }
|
||||||
public bool DownloadAllowed { get; set; }
|
public bool DownloadAllowed { get; set; }
|
||||||
public TorrentSeedConfiguration SeedConfiguration { get; set; }
|
public TorrentSeedConfiguration SeedConfiguration { get; set; }
|
||||||
public int PreferredWordScore { get; set; }
|
public int PreferredWordScore { get; set; }
|
||||||
|
|
|
@ -142,6 +142,7 @@ namespace NzbDrone.Core.Parser
|
||||||
var remoteEpisode = new RemoteEpisode
|
var remoteEpisode = new RemoteEpisode
|
||||||
{
|
{
|
||||||
ParsedEpisodeInfo = parsedEpisodeInfo,
|
ParsedEpisodeInfo = parsedEpisodeInfo,
|
||||||
|
SceneMapping = sceneMapping,
|
||||||
MappedSeasonNumber = parsedEpisodeInfo.SeasonNumber
|
MappedSeasonNumber = parsedEpisodeInfo.SeasonNumber
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -181,6 +182,12 @@ namespace NzbDrone.Core.Parser
|
||||||
remoteEpisode.Episodes = new List<Episode>();
|
remoteEpisode.Episodes = new List<Episode>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (searchCriteria != null)
|
||||||
|
{
|
||||||
|
var requestedEpisodes = searchCriteria.Episodes.ToDictionaryIgnoreDuplicates(v => v.Id);
|
||||||
|
remoteEpisode.EpisodeRequested = remoteEpisode.Episodes.Any(v => requestedEpisodes.ContainsKey(v.Id));
|
||||||
|
}
|
||||||
|
|
||||||
return remoteEpisode;
|
return remoteEpisode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.Languages;
|
using NzbDrone.Core.Languages;
|
||||||
using NzbDrone.Core.Parser.Model;
|
using NzbDrone.Core.Parser.Model;
|
||||||
using NzbDrone.Core.Qualities;
|
using NzbDrone.Core.Qualities;
|
||||||
|
using Sonarr.Api.V3.Series;
|
||||||
using Sonarr.Http.REST;
|
using Sonarr.Http.REST;
|
||||||
|
|
||||||
namespace Sonarr.Api.V3.Indexers
|
namespace Sonarr.Api.V3.Indexers
|
||||||
|
@ -35,6 +36,9 @@ namespace Sonarr.Api.V3.Indexers
|
||||||
public string SeriesTitle { get; set; }
|
public string SeriesTitle { get; set; }
|
||||||
public int[] EpisodeNumbers { get; set; }
|
public int[] EpisodeNumbers { get; set; }
|
||||||
public int[] AbsoluteEpisodeNumbers { get; set; }
|
public int[] AbsoluteEpisodeNumbers { get; set; }
|
||||||
|
public int? MappedSeasonNumber { get; set; }
|
||||||
|
public int[] MappedEpisodeNumbers { get; set; }
|
||||||
|
public int[] MappedAbsoluteEpisodeNumbers { get; set; }
|
||||||
public bool Approved { get; set; }
|
public bool Approved { get; set; }
|
||||||
public bool TemporarilyRejected { get; set; }
|
public bool TemporarilyRejected { get; set; }
|
||||||
public bool Rejected { get; set; }
|
public bool Rejected { get; set; }
|
||||||
|
@ -45,9 +49,11 @@ namespace Sonarr.Api.V3.Indexers
|
||||||
public string CommentUrl { get; set; }
|
public string CommentUrl { get; set; }
|
||||||
public string DownloadUrl { get; set; }
|
public string DownloadUrl { get; set; }
|
||||||
public string InfoUrl { get; set; }
|
public string InfoUrl { get; set; }
|
||||||
|
public bool EpisodeRequested { get; set; }
|
||||||
public bool DownloadAllowed { get; set; }
|
public bool DownloadAllowed { get; set; }
|
||||||
public int ReleaseWeight { get; set; }
|
public int ReleaseWeight { get; set; }
|
||||||
public int PreferredWordScore { get; set; }
|
public int PreferredWordScore { get; set; }
|
||||||
|
public AlternateTitleResource SceneMapping { get; set; }
|
||||||
|
|
||||||
public string MagnetUrl { get; set; }
|
public string MagnetUrl { get; set; }
|
||||||
public string InfoHash { get; set; }
|
public string InfoHash { get; set; }
|
||||||
|
@ -102,6 +108,9 @@ namespace Sonarr.Api.V3.Indexers
|
||||||
SeriesTitle = parsedEpisodeInfo.SeriesTitle,
|
SeriesTitle = parsedEpisodeInfo.SeriesTitle,
|
||||||
EpisodeNumbers = parsedEpisodeInfo.EpisodeNumbers,
|
EpisodeNumbers = parsedEpisodeInfo.EpisodeNumbers,
|
||||||
AbsoluteEpisodeNumbers = parsedEpisodeInfo.AbsoluteEpisodeNumbers,
|
AbsoluteEpisodeNumbers = parsedEpisodeInfo.AbsoluteEpisodeNumbers,
|
||||||
|
MappedSeasonNumber = remoteEpisode.Episodes.FirstOrDefault()?.SeasonNumber,
|
||||||
|
MappedEpisodeNumbers = remoteEpisode.Episodes.Select(v => v.EpisodeNumber).ToArray(),
|
||||||
|
MappedAbsoluteEpisodeNumbers = remoteEpisode.Episodes.Where(v => v.AbsoluteEpisodeNumber.HasValue).Select(v => v.AbsoluteEpisodeNumber.Value).ToArray(),
|
||||||
Approved = model.Approved,
|
Approved = model.Approved,
|
||||||
TemporarilyRejected = model.TemporarilyRejected,
|
TemporarilyRejected = model.TemporarilyRejected,
|
||||||
Rejected = model.Rejected,
|
Rejected = model.Rejected,
|
||||||
|
@ -112,9 +121,11 @@ namespace Sonarr.Api.V3.Indexers
|
||||||
CommentUrl = releaseInfo.CommentUrl,
|
CommentUrl = releaseInfo.CommentUrl,
|
||||||
DownloadUrl = releaseInfo.DownloadUrl,
|
DownloadUrl = releaseInfo.DownloadUrl,
|
||||||
InfoUrl = releaseInfo.InfoUrl,
|
InfoUrl = releaseInfo.InfoUrl,
|
||||||
|
EpisodeRequested = remoteEpisode.EpisodeRequested,
|
||||||
DownloadAllowed = remoteEpisode.DownloadAllowed,
|
DownloadAllowed = remoteEpisode.DownloadAllowed,
|
||||||
//ReleaseWeight
|
//ReleaseWeight
|
||||||
PreferredWordScore = remoteEpisode.PreferredWordScore,
|
PreferredWordScore = remoteEpisode.PreferredWordScore,
|
||||||
|
SceneMapping = remoteEpisode.SceneMapping.ToResource(),
|
||||||
|
|
||||||
MagnetUrl = torrentInfo.MagnetUrl,
|
MagnetUrl = torrentInfo.MagnetUrl,
|
||||||
InfoHash = torrentInfo.InfoHash,
|
InfoHash = torrentInfo.InfoHash,
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
namespace Sonarr.Api.V3.Series
|
using NzbDrone.Core.DataAugmentation.Scene;
|
||||||
|
|
||||||
|
namespace Sonarr.Api.V3.Series
|
||||||
{
|
{
|
||||||
public class AlternateTitleResource
|
public class AlternateTitleResource
|
||||||
{
|
{
|
||||||
|
@ -8,4 +10,24 @@
|
||||||
public string SceneOrigin { get; set; }
|
public string SceneOrigin { get; set; }
|
||||||
public string Comment { get; set; }
|
public string Comment { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class AlternateTitleResourceMapper
|
||||||
|
{
|
||||||
|
public static AlternateTitleResource ToResource(this SceneMapping sceneMapping)
|
||||||
|
{
|
||||||
|
if (sceneMapping == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new AlternateTitleResource
|
||||||
|
{
|
||||||
|
Title = sceneMapping.Title,
|
||||||
|
SeasonNumber = sceneMapping.SeasonNumber,
|
||||||
|
SceneSeasonNumber = sceneMapping.SceneSeasonNumber,
|
||||||
|
SceneOrigin = sceneMapping.SceneOrigin,
|
||||||
|
Comment = sceneMapping.Comment
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -240,13 +240,7 @@ namespace Sonarr.Api.V3.Series
|
||||||
|
|
||||||
if (mappings == null) return;
|
if (mappings == null) return;
|
||||||
|
|
||||||
resource.AlternateTitles = mappings.ConvertAll(v => new AlternateTitleResource {
|
resource.AlternateTitles = mappings.ConvertAll(AlternateTitleResourceMapper.ToResource);
|
||||||
Title = v.Title,
|
|
||||||
SeasonNumber = v.SeasonNumber,
|
|
||||||
SceneSeasonNumber = v.SceneSeasonNumber,
|
|
||||||
SceneOrigin = v.SceneOrigin,
|
|
||||||
Comment = v.Comment
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void LinkRootFolderPath(SeriesResource resource)
|
private void LinkRootFolderPath(SeriesResource resource)
|
||||||
|
|
Loading…
Reference in New Issue