From 5668152d6ffafff9aef703319c51d7c692adf29f Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Tue, 12 Jan 2021 17:59:12 +0100 Subject: [PATCH] New: Added Scene Info to Interactive Search results to show more about the applied scene/TheXEM mappings --- frontend/src/Episode/EpisodeNumber.js | 20 +- frontend/src/Helpers/Props/icons.js | 1 + .../InteractiveSearchRow.css | 7 + .../InteractiveSearch/InteractiveSearchRow.js | 31 +++ .../ReleaseSceneIndicator.css | 66 +++++++ .../ReleaseSceneIndicator.js | 187 ++++++++++++++++++ frontend/src/Series/Details/EpisodeRow.js | 3 + .../src/Series/Details/EpisodeRowConnector.js | 1 + .../Series/Details/SeriesDetailsConnector.js | 11 +- .../Utilities/Series/filterAlternateTitles.js | 43 ++++ .../Series/AlternateTitleResource.cs | 24 ++- src/NzbDrone.Api/Series/SeriesModule.cs | 9 +- .../Search/SceneMappingSpecification.cs | 74 +++++++ .../IndexerSearch/NzbSearchService.cs | 2 +- .../Parser/Model/RemoteEpisode.cs | 3 + src/NzbDrone.Core/Parser/ParsingService.cs | 7 + src/Sonarr.Api.V3/Indexers/ReleaseResource.cs | 11 ++ .../Series/AlternateTitleResource.cs | 24 ++- src/Sonarr.Api.V3/Series/SeriesModule.cs | 8 +- 19 files changed, 490 insertions(+), 42 deletions(-) create mode 100644 frontend/src/InteractiveSearch/ReleaseSceneIndicator.css create mode 100644 frontend/src/InteractiveSearch/ReleaseSceneIndicator.js create mode 100644 frontend/src/Utilities/Series/filterAlternateTitles.js create mode 100644 src/NzbDrone.Core/DecisionEngine/Specifications/Search/SceneMappingSpecification.cs diff --git a/frontend/src/Episode/EpisodeNumber.js b/frontend/src/Episode/EpisodeNumber.js index 8e9891dc7..5644a147b 100644 --- a/frontend/src/Episode/EpisodeNumber.js +++ b/frontend/src/Episode/EpisodeNumber.js @@ -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 diff --git a/frontend/src/Helpers/Props/icons.js b/frontend/src/Helpers/Props/icons.js index cad3ef748..8d61b0d4f 100644 --- a/frontend/src/Helpers/Props/icons.js +++ b/frontend/src/Helpers/Props/icons.js @@ -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; diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.css b/frontend/src/InteractiveSearch/InteractiveSearchRow.css index a0dcc6cb1..d352d92d8 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.css +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.css @@ -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 { diff --git a/frontend/src/InteractiveSearch/InteractiveSearchRow.js b/frontend/src/InteractiveSearch/InteractiveSearchRow.js index fe9c9ea73..b86624c02 100644 --- a/frontend/src/InteractiveSearch/InteractiveSearchRow.js +++ b/frontend/src/InteractiveSearch/InteractiveSearchRow.js @@ -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 { {title} + @@ -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)