@@ -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,
diff --git a/frontend/src/InteractiveSearch/ReleaseSceneIndicator.css b/frontend/src/InteractiveSearch/ReleaseSceneIndicator.css
new file mode 100644
index 000000000..d6c21307a
--- /dev/null
+++ b/frontend/src/InteractiveSearch/ReleaseSceneIndicator.css
@@ -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;
+}
diff --git a/frontend/src/InteractiveSearch/ReleaseSceneIndicator.js b/frontend/src/InteractiveSearch/ReleaseSceneIndicator.js
new file mode 100644
index 000000000..c7c7632fe
--- /dev/null
+++ b/frontend/src/InteractiveSearch/ReleaseSceneIndicator.js
@@ -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({comment ?? 'Source'} releases exist with ambiguous numbering, unable to reliably identify episode.
);
+ } else if (isUnknown) {
+ level = styles.levelUnknown;
+ messages.push(Numbering varies for this episode and release does not match any known mappings.
);
+ if (sceneOrigin === 'unknown') {
+ messages.push(Assuming Scene numbering.
);
+ } else if (sceneOrigin === 'unknown:tvdb') {
+ messages.push(Assuming TheTVDB numbering.
);
+ }
+ } else if (mappingDifferent) {
+ level = styles.levelMapped;
+ } else if (sceneOrigin) {
+ level = styles.levelNormal;
+ }
+
+ if (!episodeRequested) {
+ if (!isMixed && !isUnknown) {
+ level = styles.levelNotRequested;
+ }
+ messages.push(Mapped episode wasn't requested in this search.
);
+ }
+
+ const table = (
+
+ {
+ comment !== undefined &&
+
+ }
+
+ {
+ title !== undefined &&
+
+ }
+
+ {
+ releaseNumber !== undefined &&
+
+ }
+
+ {
+ releaseNumber !== undefined &&
+
+ }
+
+ );
+
+ return (
+
+
+
+ }
+ title="Scene Info"
+ body={
+
+ {table}
+ {
+ messages.length &&
+
+ {messages}
+
|| null
+ }
+
+ }
+ 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;
diff --git a/frontend/src/Series/Details/EpisodeRow.js b/frontend/src/Series/Details/EpisodeRow.js
index 03e77ddfd..fcd4a7905 100644
--- a/frontend/src/Series/Details/EpisodeRow.js
+++ b/frontend/src/Series/Details/EpisodeRow.js
@@ -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,
diff --git a/frontend/src/Series/Details/EpisodeRowConnector.js b/frontend/src/Series/Details/EpisodeRowConnector.js
index d8453cef3..6749bb40e 100644
--- a/frontend/src/Series/Details/EpisodeRowConnector.js
+++ b/frontend/src/Series/Details/EpisodeRowConnector.js
@@ -11,6 +11,7 @@ function createMapStateToProps() {
createEpisodeFileSelector(),
(series = {}, episodeFile) => {
return {
+ useSceneNumbering: series.useSceneNumbering,
seriesMonitored: series.monitored,
seriesType: series.seriesType,
episodeFilePath: episodeFile ? episodeFile.path : null,
diff --git a/frontend/src/Series/Details/SeriesDetailsConnector.js b/frontend/src/Series/Details/SeriesDetailsConnector.js
index 28aa2f471..19921eee4 100644
--- a/frontend/src/Series/Details/SeriesDetailsConnector.js
+++ b/frontend/src/Series/Details/SeriesDetailsConnector.js
@@ -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,
diff --git a/frontend/src/Utilities/Series/filterAlternateTitles.js b/frontend/src/Utilities/Series/filterAlternateTitles.js
new file mode 100644
index 000000000..b89ccce2a
--- /dev/null
+++ b/frontend/src/Utilities/Series/filterAlternateTitles.js
@@ -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;
diff --git a/src/NzbDrone.Api/Series/AlternateTitleResource.cs b/src/NzbDrone.Api/Series/AlternateTitleResource.cs
index 0df82ff02..f5ccd9d55 100644
--- a/src/NzbDrone.Api/Series/AlternateTitleResource.cs
+++ b/src/NzbDrone.Api/Series/AlternateTitleResource.cs
@@ -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
+ };
+ }
+ }
}
diff --git a/src/NzbDrone.Api/Series/SeriesModule.cs b/src/NzbDrone.Api/Series/SeriesModule.cs
index d565b7b2a..d3b9a4bbf 100644
--- a/src/NzbDrone.Api/Series/SeriesModule.cs
+++ b/src/NzbDrone.Api/Series/SeriesModule.cs
@@ -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)
diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SceneMappingSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SceneMappingSpecification.cs
new file mode 100644
index 000000000..e06e05365
--- /dev/null
+++ b/src/NzbDrone.Core/DecisionEngine/Specifications/Search/SceneMappingSpecification.cs
@@ -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();
+ }
+ }
+}
diff --git a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs
index 64d4ad1a2..743161a7d 100644
--- a/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs
+++ b/src/NzbDrone.Core/IndexerSearch/NzbSearchService.cs
@@ -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
{
diff --git a/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs b/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs
index 6d4b601a9..7441b76a7 100644
--- a/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs
+++ b/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs
@@ -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 Episodes { get; set; }
+ public bool EpisodeRequested { get; set; }
public bool DownloadAllowed { get; set; }
public TorrentSeedConfiguration SeedConfiguration { get; set; }
public int PreferredWordScore { get; set; }
diff --git a/src/NzbDrone.Core/Parser/ParsingService.cs b/src/NzbDrone.Core/Parser/ParsingService.cs
index d03a012bc..8475b44c7 100644
--- a/src/NzbDrone.Core/Parser/ParsingService.cs
+++ b/src/NzbDrone.Core/Parser/ParsingService.cs
@@ -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();
}
+ if (searchCriteria != null)
+ {
+ var requestedEpisodes = searchCriteria.Episodes.ToDictionaryIgnoreDuplicates(v => v.Id);
+ remoteEpisode.EpisodeRequested = remoteEpisode.Episodes.Any(v => requestedEpisodes.ContainsKey(v.Id));
+ }
+
return remoteEpisode;
}
diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs
index 86e345c1b..8b2e21e19 100644
--- a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs
+++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs
@@ -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,
diff --git a/src/Sonarr.Api.V3/Series/AlternateTitleResource.cs b/src/Sonarr.Api.V3/Series/AlternateTitleResource.cs
index 90648fe95..457cec787 100644
--- a/src/Sonarr.Api.V3/Series/AlternateTitleResource.cs
+++ b/src/Sonarr.Api.V3/Series/AlternateTitleResource.cs
@@ -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
+ };
+ }
+ }
}
diff --git a/src/Sonarr.Api.V3/Series/SeriesModule.cs b/src/Sonarr.Api.V3/Series/SeriesModule.cs
index 7ee24a789..5cdc485ec 100644
--- a/src/Sonarr.Api.V3/Series/SeriesModule.cs
+++ b/src/Sonarr.Api.V3/Series/SeriesModule.cs
@@ -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)