New: Searching for episodes with season level scene mapping now possible instead of only via RssSync (Newznab/Torznab only)

This commit is contained in:
Taloth Saldono 2020-04-17 00:24:07 +02:00
parent d6dd13a6be
commit 200aee52f7
13 changed files with 158 additions and 27 deletions

View File

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
import padNumber from 'Utilities/Number/padNumber';
import styles from './SceneInfo.css';
function SceneInfo(props) {
@ -60,6 +61,10 @@ function SceneInfo(props) {
key={alternateTitle.title}
>
{alternateTitle.title}
{
alternateTitle.sceneSeasonNumber !== -1 &&
<span> (S{padNumber(alternateTitle.sceneSeasonNumber, 2)})</span>
}
</div>
);
})

View File

@ -1,6 +1,8 @@
using System;
using FluentAssertions;
using Moq;
using NUnit.Framework;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.DecisionEngine.Specifications.Search;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
@ -23,6 +25,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.Search.SingleEpisodeSearchMatch
_searchCriteria.SeasonNumber = 5;
_searchCriteria.EpisodeNumber = 1;
Mocker.GetMock<ISceneMappingService>()
.Setup(v => v.GetTvdbSeasonNumber(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
.Returns<string, string, int>((s, r, i) => i);
}
private void GivenMapping(int sceneSeasonNumber, int seasonNumber)
{
Mocker.GetMock<ISceneMappingService>()
.Setup(v => v.GetTvdbSeasonNumber(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
.Returns<string, string, int>((s, r, i) => i >= sceneSeasonNumber ? (seasonNumber + i - sceneSeasonNumber) : i);
}
[Test]
@ -33,6 +46,26 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.Search.SingleEpisodeSearchMatch
Subject.IsSatisfiedBy(_remoteEpisode, _searchCriteria).Accepted.Should().BeFalse();
}
[Test]
public void should_return_true_if_season_matches_after_scenemapping()
{
_remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10;
GivenMapping(10, 5);
Subject.IsSatisfiedBy(_remoteEpisode, _searchCriteria).Accepted.Should().BeTrue();
}
[Test]
public void should_return_false_if_season_does_not_match_after_scenemapping()
{
_remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10;
GivenMapping(9, 5);
Subject.IsSatisfiedBy(_remoteEpisode, _searchCriteria).Accepted.Should().BeFalse();
}
[Test]
public void should_return_false_if_full_season_result_for_single_episode_search()
{
@ -44,7 +77,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.Search.SingleEpisodeSearchMatch
[Test]
public void should_return_false_if_episode_number_does_not_match_search_criteria()
{
_remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers = new []{ 2 };
_remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 2 };
Subject.IsSatisfiedBy(_remoteEpisode, _searchCriteria).Accepted.Should().BeFalse();
}

View File

@ -55,6 +55,10 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
Mocker.GetMock<ISeriesService>()
.Setup(s => s.FindByTitle(It.IsAny<string>()))
.Returns(_series);
Mocker.GetMock<ISceneMappingService>()
.Setup(v => v.GetTvdbSeasonNumber(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
.Returns<string, string, int>((s, r, i) => i);
}
private void GivenDailySeries()
@ -324,8 +328,8 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
const int tvdbSeasonNumber = 5;
Mocker.GetMock<ISceneMappingService>()
.Setup(s => s.FindSceneMapping(_parsedEpisodeInfo.SeriesTitle, It.IsAny<string>()))
.Returns(new SceneMapping { SeasonNumber = tvdbSeasonNumber, SceneSeasonNumber = _parsedEpisodeInfo.SeasonNumber });
.Setup(v => v.GetTvdbSeasonNumber(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
.Returns<string, string, int>((s, r, i) => tvdbSeasonNumber);
Subject.GetEpisodes(_parsedEpisodeInfo, _series, true, null);

View File

@ -51,6 +51,10 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
SeasonNumber = _episodes.First().SeasonNumber,
Episodes = _episodes
};
Mocker.GetMock<ISceneMappingService>()
.Setup(v => v.GetTvdbSeasonNumber(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
.Returns<string, string, int>((s, r, i) => i);
}
private void GivenMatchBySeriesTitle()

View File

@ -15,11 +15,13 @@ namespace NzbDrone.Core.DataAugmentation.Scene
public interface ISceneMappingService
{
List<string> GetSceneNames(int tvdbId, List<int> seasonNumbers, List<int> sceneSeasonNumbers);
List<SceneMapping> GetSceneMappings(int tvdbId, List<int> seasonNumbers);
int? FindTvdbId(string sceneTitle, string releaseTitle);
List<SceneMapping> FindByTvdbId(int tvdbId);
SceneMapping FindSceneMapping(string sceneTitle, string releaseTitle);
int? GetSceneSeasonNumber(string seriesTitle, string releaseTitle);
int? GetTvdbSeasonNumber(string seriesTitle, string releaseTitle);
int GetTvdbSeasonNumber(string seriesTitle, string releaseTitle, int sceneSeasonNumber);
int? GetSceneSeasonNumber(int tvdbId, int seasonNumber);
}
@ -66,6 +68,21 @@ namespace NzbDrone.Core.DataAugmentation.Scene
return FilterNonEnglish(names);
}
public List<SceneMapping> GetSceneMappings(int tvdbId, List<int> seasonNumbers)
{
var mappings = FindByTvdbId(tvdbId);
if (mappings == null)
{
return new List<SceneMapping>();
}
return mappings.Where(n => seasonNumbers.Contains(n.SeasonNumber ?? -1) &&
(n.SceneSeasonNumber ?? -1) != -1)
.Where(n => IsEnglish(n.SearchTerm))
.ToList();
}
public int? FindTvdbId(string seriesTitle)
{
return FindTvdbId(seriesTitle, null);
@ -129,6 +146,20 @@ namespace NzbDrone.Core.DataAugmentation.Scene
return FindSceneMapping(seriesTitle, releaseTitle)?.SeasonNumber;
}
public int GetTvdbSeasonNumber(string seriesTitle, string releaseTitle, int sceneSeasonNumber)
{
var sceneMapping = FindSceneMapping(seriesTitle, releaseTitle);
if (sceneMapping != null && sceneMapping.SeasonNumber.HasValue && sceneMapping.SeasonNumber.Value >= 0 &&
sceneMapping.SceneSeasonNumber <= sceneSeasonNumber)
{
var offset = sceneSeasonNumber - sceneMapping.SceneSeasonNumber.Value;
return sceneMapping.SeasonNumber.Value + offset;
}
return sceneSeasonNumber;
}
public int? GetSceneSeasonNumber(int tvdbId, int seasonNumber)
{
var mappings = FindByTvdbId(tvdbId);
@ -266,7 +297,12 @@ namespace NzbDrone.Core.DataAugmentation.Scene
private List<string> FilterNonEnglish(List<string> titles)
{
return titles.Where(title => title.All(c => c <= 255)).ToList();
return titles.Where(IsEnglish).ToList();
}
private bool IsEnglish(string title)
{
return title.All(c => c <= 255);
}
public void Handle(SeriesRefreshStartingEvent message)

View File

@ -104,7 +104,7 @@ namespace NzbDrone.Core.DecisionEngine
}
else if (remoteEpisode.Episodes.Empty())
{
decision = new DownloadDecision(remoteEpisode, new Rejection("Unable to parse episodes from release name"));
decision = new DownloadDecision(remoteEpisode, new Rejection("Unable to identify correct episode(s) using release name and scene mappings"));
}
else
{

View File

@ -1,4 +1,5 @@
using NLog;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
@ -7,10 +8,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
public class SeasonMatchSpecification : IDecisionEngineSpecification
{
private readonly Logger _logger;
private readonly ISceneMappingService _sceneMappingService;
public SeasonMatchSpecification(Logger logger)
public SeasonMatchSpecification(ISceneMappingService sceneMappingService, Logger logger)
{
_logger = logger;
_sceneMappingService = sceneMappingService;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
@ -26,7 +29,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
var singleEpisodeSpec = searchCriteria as SeasonSearchCriteria;
if (singleEpisodeSpec == null) return Decision.Accept();
if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber)
var seasonNumber = _sceneMappingService.GetTvdbSeasonNumber(remoteEpisode.ParsedEpisodeInfo.SeriesTitle,
remoteEpisode.ParsedEpisodeInfo.ReleaseTitle,
remoteEpisode.ParsedEpisodeInfo.SeasonNumber);
if (singleEpisodeSpec.SeasonNumber != seasonNumber)
{
_logger.Debug("Season number does not match searched season number, skipping.");
return Decision.Reject("Wrong season");

View File

@ -1,5 +1,6 @@
using System.Linq;
using NLog;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.IndexerSearch.Definitions;
using NzbDrone.Core.Parser.Model;
@ -8,10 +9,12 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
public class SingleEpisodeSearchMatchSpecification : IDecisionEngineSpecification
{
private readonly Logger _logger;
private readonly ISceneMappingService _sceneMappingService;
public SingleEpisodeSearchMatchSpecification(Logger logger)
public SingleEpisodeSearchMatchSpecification(ISceneMappingService sceneMappingService, Logger logger)
{
_logger = logger;
_sceneMappingService = sceneMappingService;
}
public SpecificationPriority Priority => SpecificationPriority.Default;
@ -35,7 +38,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
private Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SingleEpisodeSearchCriteria singleEpisodeSpec)
{
if (singleEpisodeSpec.SeasonNumber != remoteEpisode.ParsedEpisodeInfo.SeasonNumber)
var seasonNumber = _sceneMappingService.GetTvdbSeasonNumber(remoteEpisode.ParsedEpisodeInfo.SeriesTitle,
remoteEpisode.ParsedEpisodeInfo.ReleaseTitle,
remoteEpisode.ParsedEpisodeInfo.SeasonNumber);
if (singleEpisodeSpec.SeasonNumber != seasonNumber)
{
_logger.Debug("Season number does not match searched season number, skipping.");
return Decision.Reject("Wrong season");

View File

@ -3,6 +3,7 @@ using System.Linq;
using System.Text.RegularExpressions;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.IndexerSearch.Definitions
@ -15,6 +16,7 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public Series Series { get; set; }
public List<string> SceneTitles { get; set; }
public List<SceneMapping> SceneMappings { get; set; }
public List<Episode> Episodes { get; set; }
public virtual bool MonitoredEpisodesOnly { get; set; }
public virtual bool UserInvokedSearch { get; set; }

View File

@ -280,6 +280,9 @@ namespace NzbDrone.Core.IndexerSearch
spec.SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId,
episodes.Select(e => e.SeasonNumber).Distinct().ToList(),
episodes.Select(e => e.SceneSeasonNumber ?? e.SeasonNumber).Distinct().ToList());
spec.SceneMappings = _sceneMapping.GetSceneMappings(series.TvdbId,
episodes.Select(e => e.SeasonNumber).Distinct().ToList());
if (!spec.SceneTitles.Contains(series.Title))
{

View File

@ -32,6 +32,13 @@ namespace NzbDrone.Core.Indexers
_chains.Last().Add(new IndexerPageableRequest(request));
}
public void AddToTier(int tierIndex, IEnumerable<IndexerRequest> request)
{
if (request == null) return;
_chains[tierIndex].Add(new IndexerPageableRequest(request));
}
public void AddTier(IEnumerable<IndexerRequest> request)
{
AddTier();

View File

@ -1,7 +1,9 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.IndexerSearch.Definitions;
namespace NzbDrone.Core.Indexers.Newznab
@ -128,11 +130,16 @@ namespace NzbDrone.Core.Indexers.Newznab
{
var pageableRequests = new IndexerPageableRequestChain();
AddTvIdPageableRequests(pageableRequests, MaxPages, Settings.Categories, searchCriteria,
AddTvIdPageableRequests(pageableRequests, Settings.Categories, searchCriteria,
string.Format("&season={0}&ep={1}",
searchCriteria.SeasonNumber,
searchCriteria.EpisodeNumber));
AddSceneTitlePageableRequests(pageableRequests, Settings.Categories, searchCriteria,
m => string.Format("&season={0}&ep={1}",
m.SceneSeasonNumber,
searchCriteria.EpisodeNumber));
return pageableRequests;
}
@ -140,10 +147,14 @@ namespace NzbDrone.Core.Indexers.Newznab
{
var pageableRequests = new IndexerPageableRequestChain();
AddTvIdPageableRequests(pageableRequests, MaxPages, Settings.Categories, searchCriteria,
AddTvIdPageableRequests(pageableRequests, Settings.Categories, searchCriteria,
string.Format("&season={0}",
searchCriteria.SeasonNumber));
AddSceneTitlePageableRequests(pageableRequests, Settings.Categories, searchCriteria,
m => string.Format("&season={0}",
m.SceneSeasonNumber));
return pageableRequests;
}
@ -151,7 +162,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
var pageableRequests = new IndexerPageableRequestChain();
AddTvIdPageableRequests(pageableRequests, MaxPages, Settings.Categories, searchCriteria,
AddTvIdPageableRequests(pageableRequests, Settings.Categories, searchCriteria,
string.Format("&season={0:yyyy}&ep={0:MM}/{0:dd}",
searchCriteria.AirDate));
@ -162,7 +173,7 @@ namespace NzbDrone.Core.Indexers.Newznab
{
var pageableRequests = new IndexerPageableRequestChain();
AddTvIdPageableRequests(pageableRequests, MaxPages, Settings.Categories, searchCriteria,
AddTvIdPageableRequests(pageableRequests, Settings.Categories, searchCriteria,
string.Format("&season={0}",
searchCriteria.Year));
@ -207,7 +218,7 @@ namespace NzbDrone.Core.Indexers.Newznab
return pageableRequests;
}
private void AddTvIdPageableRequests(IndexerPageableRequestChain chain, int maxPages, IEnumerable<int> categories, SearchCriteriaBase searchCriteria, string parameters)
private void AddTvIdPageableRequests(IndexerPageableRequestChain chain, IEnumerable<int> categories, SearchCriteriaBase searchCriteria, string parameters)
{
var includeTvdbSearch = SupportsTvdbSearch && searchCriteria.Series.TvdbId > 0;
var includeImdbSearch = SupportsImdbSearch && searchCriteria.Series.ImdbId.IsNotNullOrWhiteSpace();
@ -237,29 +248,29 @@ namespace NzbDrone.Core.Indexers.Newznab
ids += "&tvmazeid=" + searchCriteria.Series.TvMazeId;
}
chain.Add(GetPagedRequests(maxPages, categories, "tvsearch", ids + parameters));
chain.Add(GetPagedRequests(MaxPages, categories, "tvsearch", ids + parameters));
}
else
{
if (includeTvdbSearch)
{
chain.Add(GetPagedRequests(maxPages, categories, "tvsearch",
chain.Add(GetPagedRequests(MaxPages, categories, "tvsearch",
string.Format("&tvdbid={0}{1}", searchCriteria.Series.TvdbId, parameters)));
}
else if (includeImdbSearch)
{
chain.Add(GetPagedRequests(maxPages, categories, "tvsearch",
chain.Add(GetPagedRequests(MaxPages, categories, "tvsearch",
string.Format("&imdbid={0}{1}", searchCriteria.Series.ImdbId, parameters)));
}
else if (includeTvRageSearch)
{
chain.Add(GetPagedRequests(maxPages, categories, "tvsearch",
chain.Add(GetPagedRequests(MaxPages, categories, "tvsearch",
string.Format("&rid={0}{1}", searchCriteria.Series.TvRageId, parameters)));
}
else if (includeTvMazeSearch)
{
chain.Add(GetPagedRequests(maxPages, categories, "tvsearch",
chain.Add(GetPagedRequests(MaxPages, categories, "tvsearch",
string.Format("&tvmazeid={0}{1}", searchCriteria.Series.TvMazeId, parameters)));
}
}
@ -277,6 +288,22 @@ namespace NzbDrone.Core.Indexers.Newznab
}
}
private void AddSceneTitlePageableRequests(IndexerPageableRequestChain chain, IEnumerable<int> categories, SearchCriteriaBase searchCriteria, Func<SceneMapping, string> parametersFunc)
{
foreach (var sceneMappingGroup in searchCriteria.SceneMappings.GroupBy(v => v.SceneSeasonNumber))
{
var parameters = parametersFunc(sceneMappingGroup.First());
foreach (var searchTerm in sceneMappingGroup.Select(v => v.SearchTerm).Distinct())
{
chain.AddToTier(0, GetPagedRequests(MaxPages, Settings.Categories, "tvsearch",
string.Format("&q={0}{1}",
NewsnabifyTitle(searchTerm),
parameters)));
}
}
}
private IEnumerable<IndexerRequest> GetPagedRequests(int maxPages, IEnumerable<int> categories, string searchType, string parameters)
{
if (categories.Empty())

View File

@ -402,13 +402,9 @@ namespace NzbDrone.Core.Parser
if (sceneSource)
{
var sceneMapping = _sceneMappingService.FindSceneMapping(parsedEpisodeInfo.SeriesTitle, parsedEpisodeInfo.ReleaseTitle);
if (sceneMapping != null && sceneMapping.SeasonNumber.HasValue && sceneMapping.SeasonNumber.Value >= 0 &&
sceneMapping.SceneSeasonNumber == seasonNumber)
{
seasonNumber = sceneMapping.SeasonNumber.Value;
}
seasonNumber = _sceneMappingService.GetTvdbSeasonNumber(parsedEpisodeInfo.SeriesTitle,
parsedEpisodeInfo.ReleaseTitle,
parsedEpisodeInfo.SeasonNumber);
}
if (parsedEpisodeInfo.EpisodeNumbers == null)