New: Support in services for multiple scene naming/numbering exceptions

This commit is contained in:
Taloth Saldono 2020-12-25 00:26:22 +01:00 committed by Taloth
parent ed2bb0d73a
commit 772448b41b
34 changed files with 604 additions and 339 deletions

View File

@ -13,6 +13,10 @@ function getAlternateTitles(seasonNumber, sceneSeasonNumber, alternateTitles) {
return true; return true;
} }
if (alternateTitle.sceneSeasonNumber === undefined && alternateTitle.sceneOrigin === 'tvdb') {
return true;
}
return seasonNumber === alternateTitle.seasonNumber; return seasonNumber === alternateTitle.seasonNumber;
}); });
} }
@ -81,6 +85,8 @@ function EpisodeNumber(props) {
title="Scene Information" title="Scene Information"
body={ body={
<SceneInfo <SceneInfo
seasonNumber={seasonNumber}
episodeNumber={episodeNumber}
sceneSeasonNumber={sceneSeasonNumber} sceneSeasonNumber={sceneSeasonNumber}
sceneEpisodeNumber={sceneEpisodeNumber} sceneEpisodeNumber={sceneEpisodeNumber}
sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber} sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber}

View File

@ -15,3 +15,8 @@
margin-left: 100px; margin-left: 100px;
} }
.comment {
color: $darkGray;
font-size: $smallFontSize;
}

View File

@ -7,6 +7,8 @@ import styles from './SceneInfo.css';
function SceneInfo(props) { function SceneInfo(props) {
const { const {
seasonNumber,
episodeNumber,
sceneSeasonNumber, sceneSeasonNumber,
sceneEpisodeNumber, sceneEpisodeNumber,
sceneAbsoluteEpisodeNumber, sceneAbsoluteEpisodeNumber,
@ -56,14 +58,33 @@ function SceneInfo(props) {
<div> <div>
{ {
alternateTitles.map((alternateTitle) => { alternateTitles.map((alternateTitle) => {
let suffix = '';
const altSceneSeasonNumber = sceneSeasonNumber === undefined ? seasonNumber : sceneSeasonNumber;
const altSceneEpisodeNumber = sceneEpisodeNumber === undefined ? episodeNumber : sceneEpisodeNumber;
const mappingSeasonNumber = alternateTitle.sceneOrigin === 'tvdb' ? seasonNumber : altSceneSeasonNumber;
const altSeasonNumber = (alternateTitle.sceneSeasonNumber !== -1 && alternateTitle.sceneSeasonNumber !== undefined) ? alternateTitle.sceneSeasonNumber : mappingSeasonNumber;
const altEpisodeNumber = alternateTitle.sceneOrigin === 'tvdb' ? episodeNumber : altSceneEpisodeNumber;
if (altEpisodeNumber !== altSceneEpisodeNumber) {
suffix = `S${padNumber(altSeasonNumber, 2)}E${padNumber(altEpisodeNumber, 2)}`;
} else if (altSeasonNumber !== altSceneSeasonNumber) {
suffix = `S${padNumber(altSeasonNumber, 2)}`;
}
return ( return (
<div <div
key={alternateTitle.title} key={alternateTitle.title}
> >
{alternateTitle.title} {alternateTitle.title}
{ {
alternateTitle.sceneSeasonNumber !== -1 && suffix &&
<span> (S{padNumber(alternateTitle.sceneSeasonNumber, 2)})</span> <span> ({suffix})</span>
}
{
alternateTitle.comment &&
<span className={styles.comment}> {alternateTitle.comment}</span>
} }
</div> </div>
); );
@ -78,6 +99,8 @@ function SceneInfo(props) {
} }
SceneInfo.propTypes = { SceneInfo.propTypes = {
seasonNumber: PropTypes.number,
episodeNumber: PropTypes.number,
sceneSeasonNumber: PropTypes.number, sceneSeasonNumber: PropTypes.number,
sceneEpisodeNumber: PropTypes.number, sceneEpisodeNumber: PropTypes.number,
sceneAbsoluteEpisodeNumber: PropTypes.number, sceneAbsoluteEpisodeNumber: PropTypes.number,

View File

@ -1,3 +1,8 @@
.alternateTitle { .alternateTitle {
white-space: nowrap; white-space: nowrap;
} }
.comment {
color: $darkGray;
font-size: $smallFontSize;
}

View File

@ -9,10 +9,14 @@ function SeriesAlternateTitles({ alternateTitles }) {
alternateTitles.map((alternateTitle) => { alternateTitles.map((alternateTitle) => {
return ( return (
<li <li
key={alternateTitle} key={alternateTitle.title}
className={styles.alternateTitle} className={styles.alternateTitle}
> >
{alternateTitle} {alternateTitle.title}
{
alternateTitle.comment &&
<span className={styles.comment}> {alternateTitle.comment}</span>
}
</li> </li>
); );
}) })

View File

@ -111,8 +111,9 @@ function createMapStateToProps() {
const isPopulated = isEpisodesPopulated && isEpisodeFilesPopulated; const isPopulated = isEpisodesPopulated && isEpisodeFilesPopulated;
const alternateTitles = _.reduce(series.alternateTitles, (acc, alternateTitle) => { const alternateTitles = _.reduce(series.alternateTitles, (acc, alternateTitle) => {
if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) && if ((alternateTitle.seasonNumber === -1 || alternateTitle.seasonNumber === undefined) &&
(alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined)) { (alternateTitle.sceneSeasonNumber === -1 || alternateTitle.sceneSeasonNumber === undefined) &&
acc.push(alternateTitle.title); (alternateTitle.title !== series.title)) {
acc.push(alternateTitle);
} }
return acc; return acc;

View File

@ -5,5 +5,7 @@
public string Title { get; set; } public string Title { get; set; }
public int? SeasonNumber { get; set; } public int? SeasonNumber { get; set; }
public int? SceneSeasonNumber { get; set; } public int? SceneSeasonNumber { get; set; }
public string SceneOrigin { get; set; }
public string Comment { get; set; }
} }
} }

View File

@ -226,7 +226,9 @@ namespace NzbDrone.Api.Series
{ {
Title = v.Title, Title = v.Title,
SeasonNumber = v.SeasonNumber, SeasonNumber = v.SeasonNumber,
SceneSeasonNumber = v.SceneSeasonNumber SceneSeasonNumber = v.SceneSeasonNumber,
SceneOrigin = v.SceneOrigin,
Comment = v.Comment
}).ToList(); }).ToList();
} }

View File

@ -22,26 +22,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.Search.SingleEpisodeSearchMatch
_remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo(); _remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo();
_remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 5; _remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 5;
_remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 }; _remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 };
_remoteEpisode.MappedSeasonNumber = 5;
_searchCriteria.SeasonNumber = 5; _searchCriteria.SeasonNumber = 5;
_searchCriteria.EpisodeNumber = 1; _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] [Test]
public void should_return_false_if_season_does_not_match() public void should_return_false_if_season_does_not_match()
{ {
_remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10; _remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10;
_remoteEpisode.MappedSeasonNumber = 10;
Subject.IsSatisfiedBy(_remoteEpisode, _searchCriteria).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteEpisode, _searchCriteria).Accepted.Should().BeFalse();
} }
@ -50,8 +41,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.Search.SingleEpisodeSearchMatch
public void should_return_true_if_season_matches_after_scenemapping() public void should_return_true_if_season_matches_after_scenemapping()
{ {
_remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10; _remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10;
_remoteEpisode.MappedSeasonNumber = 5; // 10 -> 5 mapping
GivenMapping(10, 5);
Subject.IsSatisfiedBy(_remoteEpisode, _searchCriteria).Accepted.Should().BeTrue(); Subject.IsSatisfiedBy(_remoteEpisode, _searchCriteria).Accepted.Should().BeTrue();
} }
@ -60,8 +50,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.Search.SingleEpisodeSearchMatch
public void should_return_false_if_season_does_not_match_after_scenemapping() public void should_return_false_if_season_does_not_match_after_scenemapping()
{ {
_remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10; _remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10;
_remoteEpisode.MappedSeasonNumber = 6; // 9 -> 5 mapping
GivenMapping(9, 5);
Subject.IsSatisfiedBy(_remoteEpisode, _searchCriteria).Accepted.Should().BeFalse(); Subject.IsSatisfiedBy(_remoteEpisode, _searchCriteria).Accepted.Should().BeFalse();
} }

View File

@ -85,6 +85,10 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
.Setup(s => s.GetSeries(It.IsAny<IEnumerable<int>>())) .Setup(s => s.GetSeries(It.IsAny<IEnumerable<int>>()))
.Returns(new List<Series> { _series }); .Returns(new List<Series> { _series });
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Series>()))
.Returns(new RemoteEpisode { Episodes = new List<Episode> { _episode } });
Mocker.GetMock<IParsingService>() Mocker.GetMock<IParsingService>()
.Setup(s => s.GetEpisodes(It.IsAny<ParsedEpisodeInfo>(), _series, true, null)) .Setup(s => s.GetEpisodes(It.IsAny<ParsedEpisodeInfo>(), _series, true, null))
.Returns(new List<Episode> {_episode}); .Returns(new List<Episode> {_episode});

View File

@ -42,6 +42,10 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
.Setup(s => s.GetSeries(It.IsAny<IEnumerable<int>>())) .Setup(s => s.GetSeries(It.IsAny<IEnumerable<int>>()))
.Returns(new List<Series> { new Series() }); .Returns(new List<Series> { new Series() });
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Series>()))
.Returns(new RemoteEpisode { Episodes = new List<Episode> { _episode } });
Mocker.GetMock<IParsingService>() Mocker.GetMock<IParsingService>()
.Setup(s => s.GetEpisodes(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Series>(), It.IsAny<bool>(), null)) .Setup(s => s.GetEpisodes(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Series>(), It.IsAny<bool>(), null))
.Returns(new List<Episode>{ _episode }); .Returns(new List<Episode>{ _episode });
@ -84,7 +88,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
} }
[Test] [Test]
public void should_not_remove_diffrent_season() public void should_not_remove_different_season()
{ {
AddPending(id: 1, seasonNumber: 2, episodes: new[] { 1 }); AddPending(id: 1, seasonNumber: 2, episodes: new[] { 1 });
AddPending(id: 2, seasonNumber: 2, episodes: new[] { 1 }); AddPending(id: 2, seasonNumber: 2, episodes: new[] { 1 });
@ -99,7 +103,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
} }
[Test] [Test]
public void should_not_remove_diffrent_episodes() public void should_not_remove_different_episodes()
{ {
AddPending(id: 1, seasonNumber: 2, episodes: new[] { 1 }); AddPending(id: 1, seasonNumber: 2, episodes: new[] { 1 });
AddPending(id: 2, seasonNumber: 2, episodes: new[] { 1 }); AddPending(id: 2, seasonNumber: 2, episodes: new[] { 1 });

View File

@ -77,6 +77,10 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
.Setup(s => s.GetSeries(It.IsAny<IEnumerable<int>>())) .Setup(s => s.GetSeries(It.IsAny<IEnumerable<int>>()))
.Returns(new List<Series> { _series }); .Returns(new List<Series> { _series });
Mocker.GetMock<IParsingService>()
.Setup(s => s.Map(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Series>()))
.Returns(new RemoteEpisode { Episodes = new List<Episode> { _episode } });
Mocker.GetMock<IParsingService>() Mocker.GetMock<IParsingService>()
.Setup(s => s.GetEpisodes(It.IsAny<ParsedEpisodeInfo>(), _series, true, null)) .Setup(s => s.GetEpisodes(It.IsAny<ParsedEpisodeInfo>(), _series, true, null))
.Returns(new List<Episode> {_episode}); .Returns(new List<Episode> {_episode});

View File

@ -44,7 +44,8 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
{ {
SeriesTitle = "TV Series", SeriesTitle = "TV Series",
SeasonNumber = 1 SeasonNumber = 1
} },
MappedSeasonNumber = 1
}; };
Mocker.GetMock<IParsingService>() Mocker.GetMock<IParsingService>()
@ -77,6 +78,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
trackedDownload.RemoteEpisode.Series.Id.Should().Be(5); trackedDownload.RemoteEpisode.Series.Id.Should().Be(5);
trackedDownload.RemoteEpisode.Episodes.First().Id.Should().Be(4); trackedDownload.RemoteEpisode.Episodes.First().Id.Should().Be(4);
trackedDownload.RemoteEpisode.ParsedEpisodeInfo.SeasonNumber.Should().Be(1); trackedDownload.RemoteEpisode.ParsedEpisodeInfo.SeasonNumber.Should().Be(1);
trackedDownload.RemoteEpisode.MappedSeasonNumber.Should().Be(1);
} }
[Test] [Test]
@ -91,7 +93,8 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
SeriesTitle = "TV Series", SeriesTitle = "TV Series",
SeasonNumber = 0, SeasonNumber = 0,
EpisodeNumbers = new []{ 1 } EpisodeNumbers = new []{ 1 }
} },
MappedSeasonNumber = 0
}; };
Mocker.GetMock<IHistoryService>() Mocker.GetMock<IHistoryService>()
@ -139,6 +142,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
trackedDownload.RemoteEpisode.Series.Id.Should().Be(5); trackedDownload.RemoteEpisode.Series.Id.Should().Be(5);
trackedDownload.RemoteEpisode.Episodes.First().Id.Should().Be(4); trackedDownload.RemoteEpisode.Episodes.First().Id.Should().Be(4);
trackedDownload.RemoteEpisode.ParsedEpisodeInfo.SeasonNumber.Should().Be(0); trackedDownload.RemoteEpisode.ParsedEpisodeInfo.SeasonNumber.Should().Be(0);
trackedDownload.RemoteEpisode.MappedSeasonNumber.Should().Be(0);
} }
} }
} }

View File

@ -53,8 +53,8 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
.Returns<int, int>((i, j) => _xemEpisodes.Where(d => d.SeasonNumber == j).ToList()); .Returns<int, int>((i, j) => _xemEpisodes.Where(d => d.SeasonNumber == j).ToList());
Mocker.GetMock<ISceneMappingService>() Mocker.GetMock<ISceneMappingService>()
.Setup(s => s.GetSceneNames(It.IsAny<int>(), It.IsAny<List<int>>(), It.IsAny<List<int>>())) .Setup(s => s.FindByTvdbId(It.IsAny<int>()))
.Returns(new List<string>()); .Returns(new List<SceneMapping>());
} }
private void WithEpisode(int seasonNumber, int episodeNumber, int? sceneSeasonNumber, int? sceneEpisodeNumber, string airDate = null) private void WithEpisode(int seasonNumber, int episodeNumber, int? sceneSeasonNumber, int? sceneEpisodeNumber, string airDate = null)
@ -241,7 +241,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
var seasonNumber = 1; var seasonNumber = 1;
var allCriteria = WatchForSearchCriteria(); var allCriteria = WatchForSearchCriteria();
Subject.SeasonSearch(_xemSeries.Id, seasonNumber, false, false, true, false); Subject.SeasonSearch(_xemSeries.Id, seasonNumber, false, true, true, false);
var criteria = allCriteria.OfType<AnimeEpisodeSearchCriteria>().ToList(); var criteria = allCriteria.OfType<AnimeEpisodeSearchCriteria>().ToList();
@ -354,7 +354,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
var allCriteria = WatchForSearchCriteria(); var allCriteria = WatchForSearchCriteria();
Subject.SeasonSearch(_xemSeries.Id, 1, false, false, true, false); Subject.SeasonSearch(_xemSeries.Id, 1, false, true, true, false);
var criteria1 = allCriteria.OfType<DailySeasonSearchCriteria>().ToList(); var criteria1 = allCriteria.OfType<DailySeasonSearchCriteria>().ToList();
var criteria2 = allCriteria.OfType<DailyEpisodeSearchCriteria>().ToList(); var criteria2 = allCriteria.OfType<DailyEpisodeSearchCriteria>().ToList();
@ -373,7 +373,11 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
Subject.SeasonSearch(_xemSeries.Id, 7, false, false, true, false); Subject.SeasonSearch(_xemSeries.Id, 7, false, false, true, false);
Mocker.GetMock<ISceneMappingService>() Mocker.GetMock<ISceneMappingService>()
.Verify(v => v.GetSceneNames(_xemSeries.Id, It.Is<List<int>>(l => l.Contains(7)), It.Is<List<int>>(l => l.Contains(7))), Times.Once()); .Verify(v => v.FindByTvdbId(_xemSeries.Id), Times.Once());
allCriteria.Should().HaveCount(1);
allCriteria.First().Should().BeOfType<SeasonSearchCriteria>();
allCriteria.First().As<SeasonSearchCriteria>().SeasonNumber.Should().Be(7);
} }
} }
} }

View File

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

View File

@ -51,10 +51,6 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
SeasonNumber = _episodes.First().SeasonNumber, SeasonNumber = _episodes.First().SeasonNumber,
Episodes = _episodes 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() private void GivenMatchBySeriesTitle()
@ -122,8 +118,8 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
GivenMatchByTvRageId(); GivenMatchByTvRageId();
Mocker.GetMock<ISceneMappingService>() Mocker.GetMock<ISceneMappingService>()
.Setup(v => v.FindTvdbId(It.IsAny<string>(), It.IsAny<string>())) .Setup(v => v.FindSceneMapping(It.IsAny<string>(), It.IsAny<string>()))
.Returns(10); .Returns(new SceneMapping { TvdbId = 10 });
var result = Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId); var result = Subject.Map(_parsedEpisodeInfo, _series.TvdbId, _series.TvRageId);

View File

@ -18,6 +18,10 @@ namespace NzbDrone.Core.DataAugmentation.Scene
public int? SceneSeasonNumber { get; set; } public int? SceneSeasonNumber { get; set; }
public string SceneOrigin { get; set; }
public SearchMode? SearchMode { get; set; }
public string Comment { get; set; }
public string FilterRegex { get; set; } public string FilterRegex { get; set; }
public string Type { get; set; } public string Type { get; set; }

View File

@ -15,14 +15,10 @@ namespace NzbDrone.Core.DataAugmentation.Scene
public interface ISceneMappingService public interface ISceneMappingService
{ {
List<string> GetSceneNames(int tvdbId, List<int> seasonNumbers, List<int> sceneSeasonNumbers); 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); int? FindTvdbId(string sceneTitle, string releaseTitle);
List<SceneMapping> FindByTvdbId(int tvdbId); List<SceneMapping> FindByTvdbId(int tvdbId);
SceneMapping FindSceneMapping(string sceneTitle, string releaseTitle); SceneMapping FindSceneMapping(string sceneTitle, string releaseTitle);
int? GetSceneSeasonNumber(string seriesTitle, 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);
} }
public class SceneMappingService : ISceneMappingService, public class SceneMappingService : ISceneMappingService,
@ -60,27 +56,13 @@ namespace NzbDrone.Core.DataAugmentation.Scene
return new List<string>(); return new List<string>();
} }
var names = mappings.Where(n => n.SeasonNumber.HasValue && seasonNumbers.Contains(n.SeasonNumber.Value) || var names = mappings.Where(n => seasonNumbers.Contains(n.SeasonNumber ?? -1) ||
n.SceneSeasonNumber.HasValue && sceneSeasonNumbers.Contains(n.SceneSeasonNumber.Value) || sceneSeasonNumbers.Contains(n.SceneSeasonNumber ?? -1) ||
(n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1) (n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1 && n.SceneOrigin != "tvdb")
.Where(n => IsEnglish(n.SearchTerm))
.Select(n => n.SearchTerm).Distinct().ToList(); .Select(n => n.SearchTerm).Distinct().ToList();
return FilterNonEnglish(names); return 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) public int? FindTvdbId(string seriesTitle)
@ -141,44 +123,6 @@ namespace NzbDrone.Core.DataAugmentation.Scene
return FindSceneMapping(seriesTitle, releaseTitle)?.SceneSeasonNumber; return FindSceneMapping(seriesTitle, releaseTitle)?.SceneSeasonNumber;
} }
public int? GetTvdbSeasonNumber(string seriesTitle, string releaseTitle)
{
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);
if (mappings == null)
{
return null;
}
var mapping = mappings.FirstOrDefault(e => e.SeasonNumber == seasonNumber && e.SceneSeasonNumber.HasValue);
if (mapping == null)
{
return null;
}
return mapping.SceneSeasonNumber;
}
private void UpdateMappings() private void UpdateMappings()
{ {
_logger.Info("Updating Scene mappings"); _logger.Info("Updating Scene mappings");
@ -295,11 +239,6 @@ namespace NzbDrone.Core.DataAugmentation.Scene
return normalCandidates; return normalCandidates;
} }
private List<string> FilterNonEnglish(List<string> titles)
{
return titles.Where(IsEnglish).ToList();
}
private bool IsEnglish(string title) private bool IsEnglish(string title)
{ {
return title.All(c => c <= 255); return title.All(c => c <= 255);

View File

@ -0,0 +1,12 @@
using System;
namespace NzbDrone.Core.DataAugmentation.Scene
{
[Flags]
public enum SearchMode
{
Default = 0,
SearchID = 1,
SearchTitle = 2
}
}

View File

@ -25,7 +25,7 @@ namespace NzbDrone.Core.Datastore.Converters
public object ToDB(object clrValue) public object ToDB(object clrValue)
{ {
if (clrValue != null) if (clrValue != null && clrValue != DBNull.Value)
{ {
return (int)clrValue; return (int)clrValue;
} }

View File

@ -0,0 +1,21 @@
using System;
using System.Data;
using FluentMigrator;
using NzbDrone.Common.Disk;
using NzbDrone.Common.Extensions;
using NzbDrone.Core.Datastore.Migration.Framework;
namespace NzbDrone.Core.Datastore.Migration
{
[Migration(150)]
public class add_scene_mapping_origin : NzbDroneMigrationBase
{
protected override void MainDbUpgrade()
{
Alter.Table("SceneMappings")
.AddColumn("SceneOrigin").AsString().Nullable()
.AddColumn("SearchMode").AsInt32().Nullable()
.AddColumn("Comment").AsString().Nullable();
}
}
}

View File

@ -29,11 +29,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
var singleEpisodeSpec = searchCriteria as SeasonSearchCriteria; var singleEpisodeSpec = searchCriteria as SeasonSearchCriteria;
if (singleEpisodeSpec == null) return Decision.Accept(); if (singleEpisodeSpec == null) return Decision.Accept();
var seasonNumber = _sceneMappingService.GetTvdbSeasonNumber(remoteEpisode.ParsedEpisodeInfo.SeriesTitle, if (singleEpisodeSpec.SeasonNumber != remoteEpisode.MappedSeasonNumber)
remoteEpisode.ParsedEpisodeInfo.ReleaseTitle,
remoteEpisode.ParsedEpisodeInfo.SeasonNumber);
if (singleEpisodeSpec.SeasonNumber != seasonNumber)
{ {
_logger.Debug("Season number does not match searched season number, skipping."); _logger.Debug("Season number does not match searched season number, skipping.");
return Decision.Reject("Wrong season"); return Decision.Reject("Wrong season");

View File

@ -38,11 +38,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
private Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SingleEpisodeSearchCriteria singleEpisodeSpec) private Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SingleEpisodeSearchCriteria singleEpisodeSpec)
{ {
var seasonNumber = _sceneMappingService.GetTvdbSeasonNumber(remoteEpisode.ParsedEpisodeInfo.SeriesTitle, if (singleEpisodeSpec.SeasonNumber != remoteEpisode.MappedSeasonNumber)
remoteEpisode.ParsedEpisodeInfo.ReleaseTitle,
remoteEpisode.ParsedEpisodeInfo.SeasonNumber);
if (singleEpisodeSpec.SeasonNumber != seasonNumber)
{ {
_logger.Debug("Season number does not match searched season number, skipping."); _logger.Debug("Season number does not match searched season number, skipping.");
return Decision.Reject("Wrong season"); return Decision.Reject("Wrong season");

View File

@ -291,33 +291,33 @@ namespace NzbDrone.Core.Download.Pending
// Just in case the series was removed, but wasn't cleaned up yet (housekeeper will clean it up) // Just in case the series was removed, but wasn't cleaned up yet (housekeeper will clean it up)
if (series == null) return null; if (series == null) return null;
List<Episode> episodes;
RemoteEpisode knownRemoteEpisode;
if (knownRemoteEpisodes != null && knownRemoteEpisodes.TryGetValue(release.Release.Title, out knownRemoteEpisode))
{
episodes = knownRemoteEpisode.Episodes;
}
else
{
if (ValidateParsedEpisodeInfo.ValidateForSeriesType(release.ParsedEpisodeInfo, series))
{
episodes = _parsingService.GetEpisodes(release.ParsedEpisodeInfo, series, true);
}
else
{
episodes = new List<Episode>();
}
}
release.RemoteEpisode = new RemoteEpisode release.RemoteEpisode = new RemoteEpisode
{ {
Series = series, Series = series,
Episodes = episodes,
ParsedEpisodeInfo = release.ParsedEpisodeInfo, ParsedEpisodeInfo = release.ParsedEpisodeInfo,
Release = release.Release Release = release.Release
}; };
RemoteEpisode knownRemoteEpisode;
if (knownRemoteEpisodes != null && knownRemoteEpisodes.TryGetValue(release.Release.Title, out knownRemoteEpisode))
{
release.RemoteEpisode.MappedSeasonNumber = knownRemoteEpisode.MappedSeasonNumber;
release.RemoteEpisode.Episodes = knownRemoteEpisode.Episodes;
}
else if (ValidateParsedEpisodeInfo.ValidateForSeriesType(release.ParsedEpisodeInfo, series))
{
var remoteEpisode = _parsingService.Map(release.ParsedEpisodeInfo, series);
release.RemoteEpisode.MappedSeasonNumber = remoteEpisode.MappedSeasonNumber;
release.RemoteEpisode.Episodes = remoteEpisode.Episodes;
}
else
{
release.RemoteEpisode.MappedSeasonNumber = release.ParsedEpisodeInfo.SeasonNumber;
release.RemoteEpisode.Episodes = new List<Episode>();
}
result.Add(release); result.Add(release);
} }

View File

@ -0,0 +1,31 @@
using System;
using System.Collections.Generic;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.IndexerSearch.Definitions
{
public class SceneEpisodeMapping
{
public Episode Episode { get; set; }
public SearchMode SearchMode { get; set; }
public List<string> SceneTitles { get; set; }
public int SeasonNumber { get; set; }
public int EpisodeNumber { get; set; }
public int? AbsoluteEpisodeNumber { get; set; }
public override int GetHashCode()
{
return SearchMode.GetHashCode() ^ SeasonNumber.GetHashCode() ^ EpisodeNumber.GetHashCode();
}
public override bool Equals(object obj)
{
var other = obj as SceneEpisodeMapping;
if (object.ReferenceEquals(other, null)) return false;
return SeasonNumber == other.SeasonNumber && EpisodeNumber == other.EpisodeNumber && SearchMode == other.SearchMode;
}
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.Tv;
namespace NzbDrone.Core.IndexerSearch.Definitions
{
public class SceneSeasonMapping
{
public List<Episode> Episodes { get; set; }
public SceneEpisodeMapping EpisodeMapping { get; set; }
public SearchMode SearchMode { get; set; }
public List<string> SceneTitles { get; set; }
public int SeasonNumber { get; set; }
}
}

View File

@ -16,8 +16,8 @@ namespace NzbDrone.Core.IndexerSearch.Definitions
public Series Series { get; set; } public Series Series { get; set; }
public List<string> SceneTitles { get; set; } public List<string> SceneTitles { get; set; }
public List<SceneMapping> SceneMappings { get; set; }
public List<Episode> Episodes { get; set; } public List<Episode> Episodes { get; set; }
public SearchMode SearchMode { get; set; }
public virtual bool MonitoredEpisodesOnly { get; set; } public virtual bool MonitoredEpisodesOnly { get; set; }
public virtual bool UserInvokedSearch { get; set; } public virtual bool UserInvokedSearch { get; set; }
public virtual bool InteractiveSearch { get; set; } public virtual bool InteractiveSearch { get; set; }

View File

@ -68,7 +68,7 @@ namespace NzbDrone.Core.IndexerSearch
throw new SearchFailedException("Air date is missing"); throw new SearchFailedException("Air date is missing");
} }
return SearchDaily(series, episode, userInvokedSearch, interactiveSearch); return SearchDaily(series, episode, false, userInvokedSearch, interactiveSearch);
} }
if (series.SeriesType == SeriesTypes.Anime) if (series.SeriesType == SeriesTypes.Anime)
@ -78,19 +78,19 @@ namespace NzbDrone.Core.IndexerSearch
episode.AbsoluteEpisodeNumber == null) episode.AbsoluteEpisodeNumber == null)
{ {
// Search for special episodes in season 0 that don't have absolute episode numbers // Search for special episodes in season 0 that don't have absolute episode numbers
return SearchSpecial(series, new List<Episode> { episode }, userInvokedSearch, interactiveSearch); return SearchSpecial(series, new List<Episode> { episode }, false, userInvokedSearch, interactiveSearch);
} }
return SearchAnime(series, episode, userInvokedSearch, interactiveSearch); return SearchAnime(series, episode, false, userInvokedSearch, interactiveSearch);
} }
if (episode.SeasonNumber == 0) if (episode.SeasonNumber == 0)
{ {
// Search for special episodes in season 0 // Search for special episodes in season 0
return SearchSpecial(series, new List<Episode> { episode }, userInvokedSearch, interactiveSearch); return SearchSpecial(series, new List<Episode> { episode }, false, userInvokedSearch, interactiveSearch);
} }
return SearchSingle(series, episode, userInvokedSearch, interactiveSearch); return SearchSingle(series, episode, false, userInvokedSearch, interactiveSearch);
} }
public List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber, bool missingOnly, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch) public List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber, bool missingOnly, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch)
@ -104,77 +104,195 @@ namespace NzbDrone.Core.IndexerSearch
return SeasonSearch(seriesId, seasonNumber, episodes, monitoredOnly, userInvokedSearch, interactiveSearch); return SeasonSearch(seriesId, seasonNumber, episodes, monitoredOnly, userInvokedSearch, interactiveSearch);
} }
public List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber, List<Episode> episodes, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch) public List<DownloadDecision> SeasonSearch(int seriesId, int seasonNumber, List<Episode> episodes, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch)
{ {
var series = _seriesService.GetSeries(seriesId); var series = _seriesService.GetSeries(seriesId);
if (series.SeriesType == SeriesTypes.Anime) if (series.SeriesType == SeriesTypes.Anime)
{ {
return SearchAnimeSeason(series, episodes, userInvokedSearch, interactiveSearch); return SearchAnimeSeason(series, episodes, monitoredOnly, userInvokedSearch, interactiveSearch);
} }
if (series.SeriesType == SeriesTypes.Daily) if (series.SeriesType == SeriesTypes.Daily)
{ {
return SearchDailySeason(series, episodes, userInvokedSearch, interactiveSearch); return SearchDailySeason(series, episodes, monitoredOnly, userInvokedSearch, interactiveSearch);
} }
if (seasonNumber == 0) var mappings = GetSceneSeasonMappings(series, episodes);
{
// search for special episodes in season 0
return SearchSpecial(series, episodes, userInvokedSearch, interactiveSearch);
}
var downloadDecisions = new List<DownloadDecision>(); var downloadDecisions = new List<DownloadDecision>();
if (series.UseSceneNumbering) foreach (var mapping in mappings)
{ {
var sceneSeasonGroups = episodes.GroupBy(v => if (mapping.SeasonNumber == 0)
{ {
if (v.SceneSeasonNumber.HasValue && v.SceneEpisodeNumber.HasValue) // search for special episodes in season 0
{ downloadDecisions.AddRange(SearchSpecial(series, mapping.Episodes, monitoredOnly, userInvokedSearch, interactiveSearch));
return v.SceneSeasonNumber.Value; continue;
} }
return v.SeasonNumber;
}).Distinct();
foreach (var sceneSeasonEpisodes in sceneSeasonGroups) if (mapping.Episodes.Count == 1)
{ {
if (sceneSeasonEpisodes.Count() == 1) var searchSpec = Get<SingleEpisodeSearchCriteria>(series, mapping, monitoredOnly, userInvokedSearch, interactiveSearch);
searchSpec.SeasonNumber = mapping.SeasonNumber;
searchSpec.EpisodeNumber = mapping.EpisodeMapping.EpisodeNumber;
var decisions = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
}
else
{
var searchSpec = Get<SeasonSearchCriteria>(series, mapping, monitoredOnly, userInvokedSearch, interactiveSearch);
searchSpec.SeasonNumber = mapping.SeasonNumber;
var decisions = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
}
}
return downloadDecisions;
}
private List<SceneSeasonMapping> GetSceneSeasonMappings(Series series, List<Episode> episodes)
{
var dict = new Dictionary<SceneSeasonMapping, SceneSeasonMapping>();
var sceneMappings = _sceneMapping.FindByTvdbId(series.TvdbId);
// Group the episode by SceneSeasonNumber/SeasonNumber, in 99% of cases this will result in 1 groupedEpisode
var groupedEpisodes = episodes.ToLookup(v => (v.SceneSeasonNumber ?? v.SeasonNumber) * 100000 + v.SeasonNumber);
foreach (var groupedEpisode in groupedEpisodes)
{
var episodeMappings = GetSceneEpisodeMappings(series, groupedEpisode.First(), sceneMappings);
foreach (var episodeMapping in episodeMappings)
{
var seasonMapping = new SceneSeasonMapping
{ {
var episode = sceneSeasonEpisodes.First(); Episodes = groupedEpisode.ToList(),
var searchSpec = Get<SingleEpisodeSearchCriteria>(series, sceneSeasonEpisodes.ToList(), userInvokedSearch, interactiveSearch); EpisodeMapping = episodeMapping,
SceneTitles = episodeMapping.SceneTitles,
SearchMode = episodeMapping.SearchMode,
SeasonNumber = episodeMapping.SeasonNumber
};
searchSpec.SeasonNumber = sceneSeasonEpisodes.Key; if (dict.TryGetValue(seasonMapping, out var existing))
searchSpec.MonitoredEpisodesOnly = monitoredOnly; {
existing.Episodes.AddRange(seasonMapping.Episodes);
if (episode.SceneSeasonNumber.HasValue && episode.SceneEpisodeNumber.HasValue) existing.SceneTitles.AddRange(seasonMapping.SceneTitles);
{
searchSpec.EpisodeNumber = episode.SceneEpisodeNumber.Value;
}
else
{
searchSpec.EpisodeNumber = episode.EpisodeNumber;
}
var decisions = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
} }
else else
{ {
var searchSpec = Get<SeasonSearchCriteria>(series, sceneSeasonEpisodes.ToList(), userInvokedSearch, interactiveSearch); dict[seasonMapping] = seasonMapping;
searchSpec.SeasonNumber = sceneSeasonEpisodes.Key;
searchSpec.MonitoredEpisodesOnly = monitoredOnly;
var decisions = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
} }
} }
} }
else
foreach (var item in dict)
{ {
var searchSpec = Get<SeasonSearchCriteria>(series, episodes, userInvokedSearch, interactiveSearch); item.Value.Episodes = item.Value.Episodes.Distinct().ToList();
searchSpec.SeasonNumber = seasonNumber; item.Value.SceneTitles = item.Value.SceneTitles.Distinct().ToList();
searchSpec.MonitoredEpisodesOnly = monitoredOnly; }
return dict.Values.ToList();
}
private List<SceneEpisodeMapping> GetSceneEpisodeMappings(Series series, Episode episode)
{
var dict = new Dictionary<SceneEpisodeMapping, SceneEpisodeMapping>();
var sceneMappings = _sceneMapping.FindByTvdbId(series.TvdbId);
var episodeMappings = GetSceneEpisodeMappings(series, episode, sceneMappings);
foreach (var episodeMapping in episodeMappings)
{
if (dict.TryGetValue(episodeMapping, out var existing))
{
existing.SceneTitles.AddRange(episodeMapping.SceneTitles);
}
else
{
dict[episodeMapping] = episodeMapping;
}
}
foreach (var item in dict)
{
item.Value.SceneTitles = item.Value.SceneTitles.Distinct().ToList();
}
return dict.Values.ToList();
}
private IEnumerable<SceneEpisodeMapping> GetSceneEpisodeMappings(Series series, Episode episode, List<SceneMapping> sceneMappings)
{
var includeGlobal = true;
foreach (var sceneMapping in sceneMappings)
{
if (sceneMapping.ParseTerm == series.CleanTitle && sceneMapping.FilterRegex.IsNotNullOrWhiteSpace())
{
// Disable the implied mapping if we have an explicit mapping by the same name
includeGlobal = false;
}
// 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")
{
yield return new SceneEpisodeMapping
{
Episode = episode,
SearchMode = searchMode,
SceneTitles = new List<string> { sceneMapping.SearchTerm },
SeasonNumber = (sceneMapping.SceneSeasonNumber ?? -1) == -1 ? episode.SeasonNumber : sceneMapping.SceneSeasonNumber.Value,
EpisodeNumber = episode.EpisodeNumber,
AbsoluteEpisodeNumber = episode.AbsoluteEpisodeNumber
};
}
else
{
yield return new SceneEpisodeMapping
{
Episode = episode,
SearchMode = searchMode,
SceneTitles = new List<string> { sceneMapping.SearchTerm },
SeasonNumber = (sceneMapping.SceneSeasonNumber ?? -1) == -1 ? (episode.SceneSeasonNumber ?? episode.SeasonNumber) : sceneMapping.SceneSeasonNumber.Value,
EpisodeNumber = episode.SceneEpisodeNumber ?? episode.EpisodeNumber,
AbsoluteEpisodeNumber = episode.SceneAbsoluteEpisodeNumber ?? episode.AbsoluteEpisodeNumber
};
}
}
if (includeGlobal)
{
yield return new SceneEpisodeMapping
{
Episode = episode,
SearchMode = SearchMode.Default,
SceneTitles = new List<string> { series.Title },
SeasonNumber = episode.SceneSeasonNumber ?? episode.SeasonNumber,
EpisodeNumber = episode.SceneEpisodeNumber ?? episode.EpisodeNumber,
AbsoluteEpisodeNumber = episode.SceneSeasonNumber ?? episode.AbsoluteEpisodeNumber
};
}
}
private List<DownloadDecision> SearchSingle(Series series, Episode episode, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch)
{
var mappings = GetSceneEpisodeMappings(series, episode);
var downloadDecisions = new List<DownloadDecision>();
foreach (var mapping in mappings)
{
var searchSpec = Get<SingleEpisodeSearchCriteria>(series, mapping, monitoredOnly, userInvokedSearch, interactiveSearch);
searchSpec.SeasonNumber = mapping.SeasonNumber;
searchSpec.EpisodeNumber = mapping.EpisodeNumber;
var decisions = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); var decisions = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
downloadDecisions.AddRange(decisions); downloadDecisions.AddRange(decisions);
@ -183,36 +301,18 @@ namespace NzbDrone.Core.IndexerSearch
return downloadDecisions; return downloadDecisions;
} }
private List<DownloadDecision> SearchSingle(Series series, Episode episode, bool userInvokedSearch, bool interactiveSearch) private List<DownloadDecision> SearchDaily(Series series, Episode episode, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch)
{
var searchSpec = Get<SingleEpisodeSearchCriteria>(series, new List<Episode> { episode }, userInvokedSearch, interactiveSearch);
if (series.UseSceneNumbering && episode.SceneSeasonNumber.HasValue && episode.SceneEpisodeNumber.HasValue)
{
searchSpec.EpisodeNumber = episode.SceneEpisodeNumber.Value;
searchSpec.SeasonNumber = episode.SceneSeasonNumber.Value;
}
else
{
searchSpec.EpisodeNumber = episode.EpisodeNumber;
searchSpec.SeasonNumber = episode.SeasonNumber;
}
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
}
private List<DownloadDecision> SearchDaily(Series series, Episode episode, bool userInvokedSearch, bool interactiveSearch)
{ {
var airDate = DateTime.ParseExact(episode.AirDate, Episode.AIR_DATE_FORMAT, CultureInfo.InvariantCulture); var airDate = DateTime.ParseExact(episode.AirDate, Episode.AIR_DATE_FORMAT, CultureInfo.InvariantCulture);
var searchSpec = Get<DailyEpisodeSearchCriteria>(series, new List<Episode> { episode }, userInvokedSearch, interactiveSearch); var searchSpec = Get<DailyEpisodeSearchCriteria>(series, new List<Episode> { episode }, monitoredOnly, userInvokedSearch, interactiveSearch);
searchSpec.AirDate = airDate; searchSpec.AirDate = airDate;
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
} }
private List<DownloadDecision> SearchAnime(Series series, Episode episode, bool userInvokedSearch, bool interactiveSearch, bool isSeasonSearch = false) private List<DownloadDecision> SearchAnime(Series series, Episode episode, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch, bool isSeasonSearch = false)
{ {
var searchSpec = Get<AnimeEpisodeSearchCriteria>(series, new List<Episode> { episode }, userInvokedSearch, interactiveSearch); var searchSpec = Get<AnimeEpisodeSearchCriteria>(series, new List<Episode> { episode }, monitoredOnly, userInvokedSearch, interactiveSearch);
searchSpec.IsSeasonSearch = isSeasonSearch; searchSpec.IsSeasonSearch = isSeasonSearch;
@ -233,11 +333,11 @@ namespace NzbDrone.Core.IndexerSearch
return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec); return Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
} }
private List<DownloadDecision> SearchSpecial(Series series, List<Episode> episodes, bool userInvokedSearch, bool interactiveSearch) private List<DownloadDecision> SearchSpecial(Series series, List<Episode> episodes,bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch)
{ {
var downloadDecisions = new List<DownloadDecision>(); var downloadDecisions = new List<DownloadDecision>();
var searchSpec = Get<SpecialEpisodeSearchCriteria>(series, episodes, userInvokedSearch, interactiveSearch); var searchSpec = Get<SpecialEpisodeSearchCriteria>(series, episodes, monitoredOnly, userInvokedSearch, interactiveSearch);
// build list of queries for each episode in the form: "<series> <episode-title>" // build list of queries for each episode in the form: "<series> <episode-title>"
searchSpec.EpisodeQueryTitles = episodes.Where(e => !string.IsNullOrWhiteSpace(e.Title)) searchSpec.EpisodeQueryTitles = episodes.Where(e => !string.IsNullOrWhiteSpace(e.Title))
.SelectMany(e => searchSpec.QueryTitles.Select(title => title + " " + SearchCriteriaBase.GetQueryTitle(e.Title))) .SelectMany(e => searchSpec.QueryTitles.Select(title => title + " " + SearchCriteriaBase.GetQueryTitle(e.Title)))
@ -248,62 +348,69 @@ namespace NzbDrone.Core.IndexerSearch
// Search for each episode by season/episode number as well // Search for each episode by season/episode number as well
foreach (var episode in episodes) foreach (var episode in episodes)
{ {
downloadDecisions.AddRange(SearchSingle(series, episode, userInvokedSearch, interactiveSearch)); // Episode needs to be monitored if it's not an interactive search
if (!interactiveSearch && monitoredOnly && !episode.Monitored)
{
continue;
}
downloadDecisions.AddRange(SearchSingle(series, episode, monitoredOnly, userInvokedSearch, interactiveSearch));
} }
return downloadDecisions; return downloadDecisions;
} }
private List<DownloadDecision> SearchAnimeSeason(Series series, List<Episode> episodes, bool userInvokedSearch, bool interactiveSearch) private List<DownloadDecision> SearchAnimeSeason(Series series, List<Episode> episodes, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch)
{ {
var downloadDecisions = new List<DownloadDecision>(); var downloadDecisions = new List<DownloadDecision>();
// Episode needs to be monitored if it's not an interactive search
var episodesToSearch = episodes.Where(e => // and Ensure episode has an airdate and has already aired
{ var episodesToSearch = episodes
// Episode needs to be monitored if it's not an interactive search .Where(ep => interactiveSearch || !monitoredOnly || ep.Monitored)
if (!interactiveSearch && !e.Monitored) .Where(ep => ep.AirDateUtc.HasValue && ep.AirDateUtc.Value.Before(DateTime.UtcNow))
{ .ToList();
return false;
}
// Ensure episode has an airdate and has already aired
return e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow);
});
foreach (var episode in episodesToSearch) foreach (var episode in episodesToSearch)
{ {
downloadDecisions.AddRange(SearchAnime(series, episode, userInvokedSearch, interactiveSearch, true)); downloadDecisions.AddRange(SearchAnime(series, episode, monitoredOnly, userInvokedSearch, interactiveSearch, true));
} }
return downloadDecisions; return downloadDecisions;
} }
private List<DownloadDecision> SearchDailySeason(Series series, List<Episode> episodes, bool userInvokedSearch, bool interactiveSearch) private List<DownloadDecision> SearchDailySeason(Series series, List<Episode> episodes, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch)
{ {
var downloadDecisions = new List<DownloadDecision>(); var downloadDecisions = new List<DownloadDecision>();
foreach (var yearGroup in episodes.Where(v => v.Monitored && v.AirDate.IsNotNullOrWhiteSpace())
.GroupBy(v => DateTime.ParseExact(v.AirDate, Episode.AIR_DATE_FORMAT, CultureInfo.InvariantCulture).Year)) // Episode needs to be monitored if it's not an interactive search
// and Ensure episode has an airdate
var episodesToSearch = episodes
.Where(ep => interactiveSearch || !monitoredOnly || ep.Monitored)
.Where(ep => ep.AirDate.IsNotNullOrWhiteSpace())
.ToList();
foreach (var yearGroup in episodesToSearch.GroupBy(v => DateTime.ParseExact(v.AirDate, Episode.AIR_DATE_FORMAT, CultureInfo.InvariantCulture).Year))
{ {
var yearEpisodes = yearGroup.ToList(); var yearEpisodes = yearGroup.ToList();
if (yearEpisodes.Count > 1) if (yearEpisodes.Count > 1)
{ {
var searchSpec = Get<DailySeasonSearchCriteria>(series, yearEpisodes, userInvokedSearch, interactiveSearch); var searchSpec = Get<DailySeasonSearchCriteria>(series, yearEpisodes, monitoredOnly, userInvokedSearch, interactiveSearch);
searchSpec.Year = yearGroup.Key; searchSpec.Year = yearGroup.Key;
downloadDecisions.AddRange(Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec)); downloadDecisions.AddRange(Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec));
} }
else else
{ {
downloadDecisions.AddRange(SearchDaily(series, yearEpisodes.First(), userInvokedSearch, interactiveSearch)); downloadDecisions.AddRange(SearchDaily(series, yearEpisodes.First(), monitoredOnly, userInvokedSearch, interactiveSearch));
} }
} }
return downloadDecisions; return downloadDecisions;
} }
private TSpec Get<TSpec>(Series series, List<Episode> episodes, bool userInvokedSearch, bool interactiveSearch) where TSpec : SearchCriteriaBase, new() private TSpec Get<TSpec>(Series series, List<Episode> episodes, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch) where TSpec : SearchCriteriaBase, new()
{ {
var spec = new TSpec(); var spec = new TSpec();
@ -311,16 +418,41 @@ namespace NzbDrone.Core.IndexerSearch
spec.SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId, spec.SceneTitles = _sceneMapping.GetSceneNames(series.TvdbId,
episodes.Select(e => e.SeasonNumber).Distinct().ToList(), episodes.Select(e => e.SeasonNumber).Distinct().ToList(),
episodes.Select(e => e.SceneSeasonNumber ?? 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))
{
spec.SceneTitles.Add(series.Title);
}
spec.Episodes = episodes; spec.Episodes = episodes;
spec.MonitoredEpisodesOnly = monitoredOnly;
spec.UserInvokedSearch = userInvokedSearch;
spec.InteractiveSearch = interactiveSearch;
return spec;
}
private TSpec Get<TSpec>(Series series, SceneEpisodeMapping mapping, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch) where TSpec : SearchCriteriaBase, new()
{
var spec = new TSpec();
spec.Series = series;
spec.SceneTitles = mapping.SceneTitles;
spec.SearchMode = mapping.SearchMode;
spec.Episodes = new List<Episode> { mapping.Episode };
spec.MonitoredEpisodesOnly = monitoredOnly;
spec.UserInvokedSearch = userInvokedSearch;
spec.InteractiveSearch = interactiveSearch;
return spec;
}
private TSpec Get<TSpec>(Series series, SceneSeasonMapping mapping, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch) where TSpec : SearchCriteriaBase, new()
{
var spec = new TSpec();
spec.Series = series;
spec.SceneTitles = mapping.SceneTitles;
spec.SearchMode = mapping.SearchMode;
spec.Episodes = mapping.Episodes;
spec.MonitoredEpisodesOnly = monitoredOnly;
spec.UserInvokedSearch = userInvokedSearch; spec.UserInvokedSearch = userInvokedSearch;
spec.InteractiveSearch = interactiveSearch; spec.InteractiveSearch = interactiveSearch;

View File

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NzbDrone.Common.Extensions; using NzbDrone.Common.Extensions;
using NzbDrone.Common.Http; using NzbDrone.Common.Http;
using NzbDrone.Core.DataAugmentation.Scene;
using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.IndexerSearch.Definitions;
namespace NzbDrone.Core.Indexers.FileList namespace NzbDrone.Core.Indexers.FileList
@ -24,11 +25,22 @@ namespace NzbDrone.Core.Indexers.FileList
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
AddImdbRequests(pageableRequests, searchCriteria, "search-torrents", Settings.Categories, $"&season={searchCriteria.SeasonNumber}&episode={searchCriteria.EpisodeNumber}"); if (searchCriteria.SearchMode.HasFlag(SearchMode.SearchID) || searchCriteria.SearchMode == SearchMode.Default)
{
AddImdbRequests(pageableRequests, searchCriteria, "search-torrents", Settings.Categories, $"&season={searchCriteria.SeasonNumber}&episode={searchCriteria.EpisodeNumber}");
}
if (searchCriteria.SearchMode.HasFlag(SearchMode.SearchTitle))
{
AddNameRequests(pageableRequests, searchCriteria, "search-torrents", Settings.Categories, $"&season={searchCriteria.SeasonNumber}&episode={searchCriteria.EpisodeNumber}");
}
pageableRequests.AddTier(); pageableRequests.AddTier();
AddNameRequests(pageableRequests, searchCriteria, "search-torrents", Settings.Categories, $"&season={searchCriteria.SeasonNumber}&episode={searchCriteria.EpisodeNumber}"); if (searchCriteria.SearchMode == SearchMode.Default)
{
AddNameRequests(pageableRequests, searchCriteria, "search-torrents", Settings.Categories, $"&season={searchCriteria.SeasonNumber}&episode={searchCriteria.EpisodeNumber}");
}
return pageableRequests; return pageableRequests;
} }
@ -37,11 +49,22 @@ namespace NzbDrone.Core.Indexers.FileList
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
AddImdbRequests(pageableRequests, searchCriteria, "search-torrents", Settings.Categories, $"&season={searchCriteria.SeasonNumber}"); if (searchCriteria.SearchMode.HasFlag(SearchMode.SearchID) || searchCriteria.SearchMode == SearchMode.Default)
{
AddImdbRequests(pageableRequests, searchCriteria, "search-torrents", Settings.Categories, $"&season={searchCriteria.SeasonNumber}");
}
if (searchCriteria.SearchMode.HasFlag(SearchMode.SearchTitle))
{
AddNameRequests(pageableRequests, searchCriteria, "search-torrents", Settings.Categories, $"&season={searchCriteria.SeasonNumber}");
}
pageableRequests.AddTier(); pageableRequests.AddTier();
AddNameRequests(pageableRequests, searchCriteria, "search-torrents", Settings.Categories, $"&season={searchCriteria.SeasonNumber}"); if (searchCriteria.SearchMode == SearchMode.Default)
{
AddNameRequests(pageableRequests, searchCriteria, "search-torrents", Settings.Categories, $"&season={searchCriteria.SeasonNumber}");
}
return pageableRequests; return pageableRequests;
} }

View File

@ -143,15 +143,31 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
AddTvIdPageableRequests(pageableRequests, Settings.Categories, searchCriteria, if (searchCriteria.SearchMode.HasFlag(SearchMode.SearchID) || searchCriteria.SearchMode == SearchMode.Default)
string.Format("&season={0}&ep={1}", {
searchCriteria.SeasonNumber, AddTvIdPageableRequests(pageableRequests, Settings.Categories, searchCriteria,
searchCriteria.EpisodeNumber)); string.Format("&season={0}&ep={1}",
searchCriteria.SeasonNumber,
searchCriteria.EpisodeNumber));
}
AddSceneTitlePageableRequests(pageableRequests, Settings.Categories, searchCriteria, if (searchCriteria.SearchMode.HasFlag(SearchMode.SearchTitle))
m => string.Format("&season={0}&ep={1}", {
m.SceneSeasonNumber, AddTitlePageableRequests(pageableRequests, Settings.Categories, searchCriteria,
searchCriteria.EpisodeNumber)); string.Format("&season={0}&ep={1}",
searchCriteria.SeasonNumber,
searchCriteria.EpisodeNumber));
}
pageableRequests.AddTier();
if (searchCriteria.SearchMode == SearchMode.Default)
{
AddTitlePageableRequests(pageableRequests, Settings.Categories, searchCriteria,
string.Format("&season={0}&ep={1}",
searchCriteria.SeasonNumber,
searchCriteria.EpisodeNumber));
}
return pageableRequests; return pageableRequests;
} }
@ -160,13 +176,28 @@ namespace NzbDrone.Core.Indexers.Newznab
{ {
var pageableRequests = new IndexerPageableRequestChain(); var pageableRequests = new IndexerPageableRequestChain();
AddTvIdPageableRequests(pageableRequests, Settings.Categories, searchCriteria, if (searchCriteria.SearchMode.HasFlag(SearchMode.SearchID) || searchCriteria.SearchMode == SearchMode.Default)
string.Format("&season={0}", {
searchCriteria.SeasonNumber)); AddTvIdPageableRequests(pageableRequests, Settings.Categories, searchCriteria,
string.Format("&season={0}",
searchCriteria.SeasonNumber));
}
AddSceneTitlePageableRequests(pageableRequests, Settings.Categories, searchCriteria, if (searchCriteria.SearchMode.HasFlag(SearchMode.SearchTitle))
m => string.Format("&season={0}", {
m.SceneSeasonNumber)); AddTitlePageableRequests(pageableRequests, Settings.Categories, searchCriteria,
string.Format("&season={0}",
searchCriteria.SeasonNumber));
}
pageableRequests.AddTier();
if (searchCriteria.SearchMode == SearchMode.Default)
{
AddTitlePageableRequests(pageableRequests, Settings.Categories, searchCriteria,
string.Format("&season={0}",
searchCriteria.SeasonNumber));
}
return pageableRequests; return pageableRequests;
} }
@ -287,11 +318,12 @@ namespace NzbDrone.Core.Indexers.Newznab
string.Format("&tvmazeid={0}{1}", searchCriteria.Series.TvMazeId, parameters))); string.Format("&tvmazeid={0}{1}", searchCriteria.Series.TvMazeId, parameters)));
} }
} }
}
private void AddTitlePageableRequests(IndexerPageableRequestChain chain, IEnumerable<int> categories, SearchCriteriaBase searchCriteria, string parameters)
{
if (SupportsTvTitleSearch) if (SupportsTvTitleSearch)
{ {
chain.AddTier();
foreach (var searchTerm in searchCriteria.SceneTitles) foreach (var searchTerm in searchCriteria.SceneTitles)
{ {
chain.Add(GetPagedRequests(MaxPages, Settings.Categories, "tvsearch", chain.Add(GetPagedRequests(MaxPages, Settings.Categories, "tvsearch",
@ -302,7 +334,6 @@ namespace NzbDrone.Core.Indexers.Newznab
} }
else if (SupportsTvSearch) else if (SupportsTvSearch)
{ {
chain.AddTier();
foreach (var queryTitle in searchCriteria.QueryTitles) foreach (var queryTitle in searchCriteria.QueryTitles)
{ {
chain.Add(GetPagedRequests(MaxPages, Settings.Categories, "tvsearch", chain.Add(GetPagedRequests(MaxPages, Settings.Categories, "tvsearch",
@ -313,35 +344,6 @@ namespace NzbDrone.Core.Indexers.Newznab
} }
} }
private void AddSceneTitlePageableRequests(IndexerPageableRequestChain chain, IEnumerable<int> categories, SearchCriteriaBase searchCriteria, Func<SceneMapping, string> parametersFunc)
{
if (searchCriteria.SceneMappings != null)
{
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())
{
if (SupportsTvTitleSearch)
{
chain.AddToTier(0, GetPagedRequests(MaxPages, Settings.Categories, "tvsearch",
string.Format("&title={0}{1}",
Uri.EscapeDataString(searchTerm),
parameters)));
}
else if (SupportsTvSearch)
{
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) private IEnumerable<IndexerRequest> GetPagedRequests(int maxPages, IEnumerable<int> categories, string searchType, string parameters)
{ {
if (categories.Empty()) if (categories.Empty())

View File

@ -10,6 +10,8 @@ namespace NzbDrone.Core.Parser.Model
{ {
public ReleaseInfo Release { get; set; } public ReleaseInfo Release { get; set; }
public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; } public ParsedEpisodeInfo ParsedEpisodeInfo { get; set; }
public int MappedSeasonNumber { get; set; }
public Series Series { get; set; } public Series Series { get; set; }
public List<Episode> Episodes { get; set; } public List<Episode> Episodes { get; set; }
public bool DownloadAllowed { get; set; } public bool DownloadAllowed { get; set; }

View File

@ -15,6 +15,7 @@ namespace NzbDrone.Core.Parser
{ {
Series GetSeries(string title); Series GetSeries(string title);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Series series);
RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds); RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds);
List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null); List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null);
ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null); ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null);
@ -116,30 +117,12 @@ namespace NzbDrone.Core.Parser
public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null)
{ {
var remoteEpisode = new RemoteEpisode return Map(parsedEpisodeInfo, tvdbId, tvRageId, null, searchCriteria);
{ }
ParsedEpisodeInfo = parsedEpisodeInfo,
};
var series = GetSeries(parsedEpisodeInfo, tvdbId, tvRageId, searchCriteria); public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, Series series)
{
if (series == null) return Map(parsedEpisodeInfo, 0, 0, series, null);
{
return remoteEpisode;
}
remoteEpisode.Series = series;
if (ValidateParsedEpisodeInfo.ValidateForSeriesType(parsedEpisodeInfo, series))
{
remoteEpisode.Episodes = GetEpisodes(parsedEpisodeInfo, series, true, searchCriteria);
}
else
{
remoteEpisode.Episodes = new List<Episode>();
}
return remoteEpisode;
} }
public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds) public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int seriesId, IEnumerable<int> episodeIds)
@ -152,11 +135,72 @@ namespace NzbDrone.Core.Parser
}; };
} }
private RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, Series series, SearchCriteriaBase searchCriteria)
{
var sceneMapping = _sceneMappingService.FindSceneMapping(parsedEpisodeInfo.SeriesTitle, parsedEpisodeInfo.ReleaseTitle);
var remoteEpisode = new RemoteEpisode
{
ParsedEpisodeInfo = parsedEpisodeInfo,
MappedSeasonNumber = parsedEpisodeInfo.SeasonNumber
};
// For now we just detect tvdb vs scene, but we can do multiple 'origins' in the future.
var sceneSource = true;
if (sceneMapping != null)
{
if (sceneMapping.SeasonNumber.HasValue && sceneMapping.SeasonNumber.Value >= 0 &&
sceneMapping.SceneSeasonNumber <= parsedEpisodeInfo.SeasonNumber)
{
remoteEpisode.MappedSeasonNumber += sceneMapping.SeasonNumber.Value - sceneMapping.SceneSeasonNumber.Value;
}
if (sceneMapping.SceneOrigin == "tvdb")
{
sceneSource = false;
}
}
if (series == null)
{
series = GetSeries(parsedEpisodeInfo, tvdbId, tvRageId, sceneMapping, searchCriteria);
}
if (series != null)
{
remoteEpisode.Series = series;
if (ValidateParsedEpisodeInfo.ValidateForSeriesType(parsedEpisodeInfo, series))
{
remoteEpisode.Episodes = GetEpisodes(parsedEpisodeInfo, series, remoteEpisode.MappedSeasonNumber, sceneSource, searchCriteria);
}
}
if (remoteEpisode.Episodes == null)
{
remoteEpisode.Episodes = new List<Episode>();
}
return remoteEpisode;
}
public List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null) public List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, bool sceneSource, SearchCriteriaBase searchCriteria = null)
{
if (sceneSource)
{
var remoteEpisode = Map(parsedEpisodeInfo, 0, 0, series, searchCriteria);
return remoteEpisode.Episodes;
}
return GetEpisodes(parsedEpisodeInfo, series, parsedEpisodeInfo.SeasonNumber, sceneSource, searchCriteria);
}
private List<Episode> GetEpisodes(ParsedEpisodeInfo parsedEpisodeInfo, Series series, int mappedSeasonNumber, bool sceneSource, SearchCriteriaBase searchCriteria)
{ {
if (parsedEpisodeInfo.FullSeason) if (parsedEpisodeInfo.FullSeason)
{ {
return _episodeService.GetEpisodesBySeason(series.Id, parsedEpisodeInfo.SeasonNumber); return _episodeService.GetEpisodesBySeason(series.Id, mappedSeasonNumber);
} }
if (parsedEpisodeInfo.IsDaily) if (parsedEpisodeInfo.IsDaily)
@ -173,10 +217,10 @@ namespace NzbDrone.Core.Parser
if (parsedEpisodeInfo.IsAbsoluteNumbering) if (parsedEpisodeInfo.IsAbsoluteNumbering)
{ {
return GetAnimeEpisodes(series, parsedEpisodeInfo, sceneSource); return GetAnimeEpisodes(series, parsedEpisodeInfo, mappedSeasonNumber, sceneSource, searchCriteria);
} }
return GetStandardEpisodes(series, parsedEpisodeInfo, sceneSource, searchCriteria); return GetStandardEpisodes(series, parsedEpisodeInfo, mappedSeasonNumber, sceneSource, searchCriteria);
} }
public ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null) public ParsedEpisodeInfo ParseSpecialEpisodeTitle(ParsedEpisodeInfo parsedEpisodeInfo, string releaseTitle, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null)
@ -261,19 +305,18 @@ namespace NzbDrone.Core.Parser
return null; return null;
} }
private Series GetSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria) private Series GetSeries(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SceneMapping sceneMapping, SearchCriteriaBase searchCriteria)
{ {
Series series = null; Series series = null;
var sceneMappingTvdbId = _sceneMappingService.FindTvdbId(parsedEpisodeInfo.SeriesTitle, parsedEpisodeInfo.ReleaseTitle); if (sceneMapping != null)
if (sceneMappingTvdbId.HasValue)
{ {
if (searchCriteria != null && searchCriteria.Series.TvdbId == sceneMappingTvdbId.Value) if (searchCriteria != null && searchCriteria.Series.TvdbId == sceneMapping.TvdbId)
{ {
return searchCriteria.Series; return searchCriteria.Series;
} }
series = _seriesService.FindByTvdbId(sceneMappingTvdbId.Value); series = _seriesService.FindByTvdbId(sceneMapping.TvdbId);
if (series == null) if (series == null)
{ {
@ -385,7 +428,7 @@ namespace NzbDrone.Core.Parser
return episodeInfo; return episodeInfo;
} }
private List<Episode> GetAnimeEpisodes(Series series, ParsedEpisodeInfo parsedEpisodeInfo, bool sceneSource) private List<Episode> GetAnimeEpisodes(Series series, ParsedEpisodeInfo parsedEpisodeInfo, int seasonNumber, bool sceneSource, SearchCriteriaBase searchCriteria)
{ {
var result = new List<Episode>(); var result = new List<Episode>();
@ -448,17 +491,9 @@ namespace NzbDrone.Core.Parser
return result; return result;
} }
private List<Episode> GetStandardEpisodes(Series series, ParsedEpisodeInfo parsedEpisodeInfo, bool sceneSource, SearchCriteriaBase searchCriteria) private List<Episode> GetStandardEpisodes(Series series, ParsedEpisodeInfo parsedEpisodeInfo, int mappedSeasonNumber, bool sceneSource, SearchCriteriaBase searchCriteria)
{ {
var result = new List<Episode>(); var result = new List<Episode>();
var seasonNumber = parsedEpisodeInfo.SeasonNumber;
if (sceneSource)
{
seasonNumber = _sceneMappingService.GetTvdbSeasonNumber(parsedEpisodeInfo.SeriesTitle,
parsedEpisodeInfo.ReleaseTitle,
parsedEpisodeInfo.SeasonNumber);
}
if (parsedEpisodeInfo.EpisodeNumbers == null) if (parsedEpisodeInfo.EpisodeNumbers == null)
{ {
@ -479,7 +514,7 @@ namespace NzbDrone.Core.Parser
if (!episodes.Any()) if (!episodes.Any())
{ {
episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, seasonNumber, episodeNumber); episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, mappedSeasonNumber, episodeNumber);
} }
if (episodes != null && episodes.Any()) if (episodes != null && episodes.Any())
@ -499,12 +534,12 @@ namespace NzbDrone.Core.Parser
if (searchCriteria != null) if (searchCriteria != null)
{ {
episodeInfo = searchCriteria.Episodes.SingleOrDefault(e => e.SeasonNumber == seasonNumber && e.EpisodeNumber == episodeNumber); episodeInfo = searchCriteria.Episodes.SingleOrDefault(e => e.SeasonNumber == mappedSeasonNumber && e.EpisodeNumber == episodeNumber);
} }
if (episodeInfo == null) if (episodeInfo == null)
{ {
episodeInfo = _episodeService.FindEpisode(series.Id, seasonNumber, episodeNumber); episodeInfo = _episodeService.FindEpisode(series.Id, mappedSeasonNumber, episodeNumber);
} }
if (episodeInfo != null) if (episodeInfo != null)

View File

@ -5,5 +5,7 @@
public string Title { get; set; } public string Title { get; set; }
public int? SeasonNumber { get; set; } public int? SeasonNumber { get; set; }
public int? SceneSeasonNumber { get; set; } public int? SceneSeasonNumber { get; set; }
public string SceneOrigin { get; set; }
public string Comment { get; set; }
} }
} }

View File

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