New: Added Scene Info to Interactive Search results to show more about the applied scene/TheXEM mappings

This commit is contained in:
Taloth Saldono 2021-01-12 17:59:12 +01:00
parent dcda03da4a
commit 5668152d6f
19 changed files with 490 additions and 42 deletions

View File

@ -1,26 +1,13 @@
import PropTypes from 'prop-types';
import React, { Fragment } from 'react';
import padNumber from 'Utilities/Number/padNumber';
import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles';
import { icons, kinds, tooltipPositions } from 'Helpers/Props';
import Icon from 'Components/Icon';
import Popover from 'Components/Tooltip/Popover';
import SceneInfo from './SceneInfo';
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) {
const messages = [];
@ -43,13 +30,14 @@ function EpisodeNumber(props) {
sceneSeasonNumber,
sceneEpisodeNumber,
sceneAbsoluteEpisodeNumber,
useSceneNumbering,
unverifiedSceneNumbering,
alternateTitles: seriesAlternateTitles,
seriesType,
showSeasonNumber
} = props;
const alternateTitles = getAlternateTitles(seasonNumber, sceneSeasonNumber, seriesAlternateTitles);
const alternateTitles = filterAlternateTitles(seriesAlternateTitles, null, useSceneNumbering, seasonNumber, sceneSeasonNumber);
const hasSceneInformation = sceneSeasonNumber !== undefined ||
sceneEpisodeNumber !== undefined ||
@ -137,6 +125,7 @@ EpisodeNumber.propTypes = {
sceneSeasonNumber: PropTypes.number,
sceneEpisodeNumber: PropTypes.number,
sceneAbsoluteEpisodeNumber: PropTypes.number,
useSceneNumbering: PropTypes.bool.isRequired,
unverifiedSceneNumbering: PropTypes.bool.isRequired,
alternateTitles: PropTypes.arrayOf(PropTypes.object).isRequired,
seriesType: PropTypes.string,
@ -144,6 +133,7 @@ EpisodeNumber.propTypes = {
};
EpisodeNumber.defaultProps = {
useSceneNumbering: false,
unverifiedSceneNumbering: false,
alternateTitles: [],
showSeasonNumber: false

View File

@ -179,6 +179,7 @@ export const RESTORE = fasHistory;
export const REORDER = fasBars;
export const RSS = fasRss;
export const SAVE = fasSave;
export const SCENE_MAPPING = fasSitemap;
export const SCHEDULED = farClock;
export const SCORE = fasUserPlus;
export const SEARCH = fasSearch;

View File

@ -8,6 +8,13 @@
composes: cell from '~Components/Table/Cells/TableRowCell.css';
word-break: break-all;
display: flex;
align-items: center;
justify-content: space-between;
}
.sceneMapping {
flex-shrink: 0;
}
.indexer {

View File

@ -14,6 +14,7 @@ import Popover from 'Components/Tooltip/Popover';
import EpisodeLanguage from 'Episode/EpisodeLanguage';
import EpisodeQuality from 'Episode/EpisodeQuality';
import ProtocolLabel from 'Activity/Queue/ProtocolLabel';
import ReleaseSceneIndicator from './ReleaseSceneIndicator';
import Peers from './Peers';
import styles from './InteractiveSearchRow.css';
@ -114,8 +115,17 @@ class InteractiveSearchRow extends Component {
quality,
language,
preferredWordScore,
sceneMapping,
seasonNumber,
episodeNumbers,
absoluteEpisodeNumbers,
mappedSeasonNumber,
mappedEpisodeNumbers,
mappedAbsoluteEpisodeNumbers,
rejections,
episodeRequested,
downloadAllowed,
isDaily,
isGrabbing,
isGrabbed,
longDateFormat,
@ -142,6 +152,18 @@ class InteractiveSearchRow extends Component {
<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}
/>
</TableRowCell>
<TableRowCell className={styles.indexer}>
@ -245,8 +267,17 @@ InteractiveSearchRow.propTypes = {
quality: PropTypes.object.isRequired,
language: PropTypes.object.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,
episodeRequested: PropTypes.bool.isRequired,
downloadAllowed: PropTypes.bool.isRequired,
isDaily: PropTypes.bool.isRequired,
isGrabbing: PropTypes.bool.isRequired,
isGrabbed: PropTypes.bool.isRequired,
grabError: PropTypes.string,

View File

@ -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;
}

View File

@ -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;

View File

@ -60,6 +60,7 @@ class EpisodeRow extends Component {
sceneAbsoluteEpisodeNumber,
airDateUtc,
title,
useSceneNumbering,
unverifiedSceneNumbering,
isSaving,
seriesMonitored,
@ -110,6 +111,7 @@ class EpisodeRow extends Component {
seasonNumber={seasonNumber}
episodeNumber={episodeNumber}
absoluteEpisodeNumber={absoluteEpisodeNumber}
useSceneNumbering={useSceneNumbering}
unverifiedSceneNumbering={unverifiedSceneNumbering}
seriesType={seriesType}
sceneSeasonNumber={sceneSeasonNumber}
@ -265,6 +267,7 @@ EpisodeRow.propTypes = {
airDateUtc: PropTypes.string,
title: PropTypes.string.isRequired,
isSaving: PropTypes.bool,
useSceneNumbering: PropTypes.bool,
unverifiedSceneNumbering: PropTypes.bool,
seriesMonitored: PropTypes.bool.isRequired,
seriesType: PropTypes.string.isRequired,

View File

@ -11,6 +11,7 @@ function createMapStateToProps() {
createEpisodeFileSelector(),
(series = {}, episodeFile) => {
return {
useSceneNumbering: series.useSceneNumbering,
seriesMonitored: series.monitored,
seriesType: series.seriesType,
episodeFilePath: episodeFile ? episodeFile.path : null,

View File

@ -5,6 +5,7 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { findCommand, isCommandExecuting } from 'Utilities/Command';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import filterAlternateTitles from 'Utilities/Series/filterAlternateTitles';
import createAllSeriesSelector from 'Store/Selectors/createAllSeriesSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import { fetchEpisodes, clearEpisodes } from 'Store/Actions/episodeActions';
@ -109,15 +110,7 @@ function createMapStateToProps() {
const isFetching = isEpisodesFetching || isEpisodeFilesFetching;
const isPopulated = isEpisodesPopulated && isEpisodeFilesPopulated;
const alternateTitles = _.reduce(series.alternateTitles, (acc, alternateTitle) => {
if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) &&
(alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined) &&
(alternateTitle.title !== series.title)) {
acc.push(alternateTitle);
}
return acc;
}, []);
const alternateTitles = filterAlternateTitles(series.alternateTitle, series.title, series.useSceneNumbering);
return {
...series,

View File

@ -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;

View File

@ -1,4 +1,6 @@
namespace NzbDrone.Api.Series
using NzbDrone.Core.DataAugmentation.Scene;
namespace NzbDrone.Api.Series
{
public class AlternateTitleResource
{
@ -8,4 +10,24 @@
public string SceneOrigin { 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
};
}
}
}

View File

@ -222,14 +222,7 @@ namespace NzbDrone.Api.Series
if (mappings == null) return;
resource.AlternateTitles = mappings.Select(v => new AlternateTitleResource
{
Title = v.Title,
SeasonNumber = v.SeasonNumber,
SceneSeasonNumber = v.SceneSeasonNumber,
SceneOrigin = v.SceneOrigin,
Comment = v.Comment
}).ToList();
resource.AlternateTitles = mappings.ConvertAll(AlternateTitleResourceMapper.ToResource);
}
public void Handle(EpisodeImportedEvent message)

View File

@ -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();
}
}
}

View File

@ -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.
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
{

View File

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Download.Clients;
using NzbDrone.Core.Tv;
@ -10,10 +11,12 @@ namespace NzbDrone.Core.Parser.Model
{
public ReleaseInfo Release { get; set; }
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
public SceneMapping SceneMapping { get; set; }
public int MappedSeasonNumber { get; set; }
public Series Series { get; set; }
public List<Episode> Episodes { get; set; }
public bool EpisodeRequested { get; set; }
public bool DownloadAllowed { get; set; }
public TorrentSeedConfiguration SeedConfiguration { get; set; }
public int PreferredWordScore { get; set; }

View File

@ -142,6 +142,7 @@ namespace NzbDrone.Core.Parser
var remoteEpisode = new RemoteEpisode
{
ParsedEpisodeInfo = parsedEpisodeInfo,
SceneMapping = sceneMapping,
MappedSeasonNumber = parsedEpisodeInfo.SeasonNumber
};
@ -181,6 +182,12 @@ namespace NzbDrone.Core.Parser
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;
}

View File

@ -7,6 +7,7 @@ using NzbDrone.Core.Indexers;
using NzbDrone.Core.Languages;
using NzbDrone.Core.Parser.Model;
using NzbDrone.Core.Qualities;
using Sonarr.Api.V3.Series;
using Sonarr.Http.REST;
namespace Sonarr.Api.V3.Indexers
@ -35,6 +36,9 @@ namespace Sonarr.Api.V3.Indexers
public string SeriesTitle { get; set; }
public int[] EpisodeNumbers { 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 TemporarilyRejected { get; set; }
public bool Rejected { get; set; }
@ -45,9 +49,11 @@ namespace Sonarr.Api.V3.Indexers
public string CommentUrl { get; set; }
public string DownloadUrl { get; set; }
public string InfoUrl { get; set; }
public bool EpisodeRequested { get; set; }
public bool DownloadAllowed { get; set; }
public int ReleaseWeight { get; set; }
public int PreferredWordScore { get; set; }
public AlternateTitleResource SceneMapping { get; set; }
public string MagnetUrl { get; set; }
public string InfoHash { get; set; }
@ -102,6 +108,9 @@ namespace Sonarr.Api.V3.Indexers
SeriesTitle = parsedEpisodeInfo.SeriesTitle,
EpisodeNumbers = parsedEpisodeInfo.EpisodeNumbers,
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,
TemporarilyRejected = model.TemporarilyRejected,
Rejected = model.Rejected,
@ -112,9 +121,11 @@ namespace Sonarr.Api.V3.Indexers
CommentUrl = releaseInfo.CommentUrl,
DownloadUrl = releaseInfo.DownloadUrl,
InfoUrl = releaseInfo.InfoUrl,
EpisodeRequested = remoteEpisode.EpisodeRequested,
DownloadAllowed = remoteEpisode.DownloadAllowed,
//ReleaseWeight
PreferredWordScore = remoteEpisode.PreferredWordScore,
SceneMapping = remoteEpisode.SceneMapping.ToResource(),
MagnetUrl = torrentInfo.MagnetUrl,
InfoHash = torrentInfo.InfoHash,

View File

@ -1,4 +1,6 @@
namespace Sonarr.Api.V3.Series
using NzbDrone.Core.DataAugmentation.Scene;
namespace Sonarr.Api.V3.Series
{
public class AlternateTitleResource
{
@ -8,4 +10,24 @@
public string SceneOrigin { 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
};
}
}
}

View File

@ -240,13 +240,7 @@ namespace Sonarr.Api.V3.Series
if (mappings == null) return;
resource.AlternateTitles = mappings.ConvertAll(v => new AlternateTitleResource {
Title = v.Title,
SeasonNumber = v.SeasonNumber,
SceneSeasonNumber = v.SceneSeasonNumber,
SceneOrigin = v.SceneOrigin,
Comment = v.Comment
});
resource.AlternateTitles = mappings.ConvertAll(AlternateTitleResourceMapper.ToResource);
}
private void LinkRootFolderPath(SeriesResource resource)