New: Added Scene Info to Interactive Search results to show more about the applied scene/TheXEM mappings
parent
dcda03da4a
commit
5668152d6f
@ -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;
|
@ -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;
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue