1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2024-11-26 04:33:01 +01:00

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;
}
if (alternateTitle.sceneSeasonNumber === undefined && alternateTitle.sceneOrigin === 'tvdb') {
return true;
}
return seasonNumber === alternateTitle.seasonNumber;
});
}
@ -81,6 +85,8 @@ function EpisodeNumber(props) {
title="Scene Information"
body={
<SceneInfo
seasonNumber={seasonNumber}
episodeNumber={episodeNumber}
sceneSeasonNumber={sceneSeasonNumber}
sceneEpisodeNumber={sceneEpisodeNumber}
sceneAbsoluteEpisodeNumber={sceneAbsoluteEpisodeNumber}

View File

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

View File

@ -7,6 +7,8 @@ import styles from './SceneInfo.css';
function SceneInfo(props) {
const {
seasonNumber,
episodeNumber,
sceneSeasonNumber,
sceneEpisodeNumber,
sceneAbsoluteEpisodeNumber,
@ -56,14 +58,33 @@ function SceneInfo(props) {
<div>
{
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 (
<div
key={alternateTitle.title}
>
{alternateTitle.title}
{
alternateTitle.sceneSeasonNumber !== -1 &&
<span> (S{padNumber(alternateTitle.sceneSeasonNumber, 2)})</span>
suffix &&
<span> ({suffix})</span>
}
{
alternateTitle.comment &&
<span className={styles.comment}> {alternateTitle.comment}</span>
}
</div>
);
@ -78,6 +99,8 @@ function SceneInfo(props) {
}
SceneInfo.propTypes = {
seasonNumber: PropTypes.number,
episodeNumber: PropTypes.number,
sceneSeasonNumber: PropTypes.number,
sceneEpisodeNumber: PropTypes.number,
sceneAbsoluteEpisodeNumber: PropTypes.number,

View File

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

View File

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

View File

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

View File

@ -5,5 +5,7 @@
public string Title { get; set; }
public int? SeasonNumber { 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,
SeasonNumber = v.SeasonNumber,
SceneSeasonNumber = v.SceneSeasonNumber
SceneSeasonNumber = v.SceneSeasonNumber,
SceneOrigin = v.SceneOrigin,
Comment = v.Comment
}).ToList();
}

View File

@ -22,26 +22,17 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.Search.SingleEpisodeSearchMatch
_remoteEpisode.ParsedEpisodeInfo = new ParsedEpisodeInfo();
_remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 5;
_remoteEpisode.ParsedEpisodeInfo.EpisodeNumbers = new[] { 1 };
_remoteEpisode.MappedSeasonNumber = 5;
_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]
public void should_return_false_if_season_does_not_match()
{
_remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10;
_remoteEpisode.MappedSeasonNumber = 10;
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()
{
_remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10;
GivenMapping(10, 5);
_remoteEpisode.MappedSeasonNumber = 5; // 10 -> 5 mapping
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()
{
_remoteEpisode.ParsedEpisodeInfo.SeasonNumber = 10;
GivenMapping(9, 5);
_remoteEpisode.MappedSeasonNumber = 6; // 9 -> 5 mapping
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>>()))
.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>()
.Setup(s => s.GetEpisodes(It.IsAny<ParsedEpisodeInfo>(), _series, true, null))
.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>>()))
.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>()
.Setup(s => s.GetEpisodes(It.IsAny<ParsedEpisodeInfo>(), It.IsAny<Series>(), It.IsAny<bool>(), null))
.Returns(new List<Episode>{ _episode });
@ -84,7 +88,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
}
[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: 2, seasonNumber: 2, episodes: new[] { 1 });
@ -99,7 +103,7 @@ namespace NzbDrone.Core.Test.Download.Pending.PendingReleaseServiceTests
}
[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: 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>>()))
.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>()
.Setup(s => s.GetEpisodes(It.IsAny<ParsedEpisodeInfo>(), _series, true, null))
.Returns(new List<Episode> {_episode});

View File

@ -44,7 +44,8 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
{
SeriesTitle = "TV Series",
SeasonNumber = 1
}
},
MappedSeasonNumber = 1
};
Mocker.GetMock<IParsingService>()
@ -77,6 +78,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
trackedDownload.RemoteEpisode.Series.Id.Should().Be(5);
trackedDownload.RemoteEpisode.Episodes.First().Id.Should().Be(4);
trackedDownload.RemoteEpisode.ParsedEpisodeInfo.SeasonNumber.Should().Be(1);
trackedDownload.RemoteEpisode.MappedSeasonNumber.Should().Be(1);
}
[Test]
@ -91,7 +93,8 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
SeriesTitle = "TV Series",
SeasonNumber = 0,
EpisodeNumbers = new []{ 1 }
}
},
MappedSeasonNumber = 0
};
Mocker.GetMock<IHistoryService>()
@ -139,6 +142,7 @@ namespace NzbDrone.Core.Test.Download.TrackedDownloads
trackedDownload.RemoteEpisode.Series.Id.Should().Be(5);
trackedDownload.RemoteEpisode.Episodes.First().Id.Should().Be(4);
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());
Mocker.GetMock<ISceneMappingService>()
.Setup(s => s.GetSceneNames(It.IsAny<int>(), It.IsAny<List<int>>(), It.IsAny<List<int>>()))
.Returns(new List<string>());
.Setup(s => s.FindByTvdbId(It.IsAny<int>()))
.Returns(new List<SceneMapping>());
}
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 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();
@ -354,7 +354,7 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
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 criteria2 = allCriteria.OfType<DailyEpisodeSearchCriteria>().ToList();
@ -373,7 +373,11 @@ namespace NzbDrone.Core.Test.IndexerSearchTests
Subject.SeasonSearch(_xemSeries.Id, 7, false, false, true, false);
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>()
.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()
@ -328,8 +324,8 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
const int tvdbSeasonNumber = 5;
Mocker.GetMock<ISceneMappingService>()
.Setup(v => v.GetTvdbSeasonNumber(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<int>()))
.Returns<string, string, int>((s, r, i) => tvdbSeasonNumber);
.Setup(v => v.FindSceneMapping(It.IsAny<string>(), It.IsAny<string>()))
.Returns<string, string>((s, r) => new SceneMapping { SceneSeasonNumber = 1, SeasonNumber = tvdbSeasonNumber });
Subject.GetEpisodes(_parsedEpisodeInfo, _series, true, null);
@ -346,8 +342,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 + 100 });
.Setup(v => v.FindSceneMapping(It.IsAny<string>(), It.IsAny<string>()))
.Returns<string, string>((s, r) => new SceneMapping { SceneSeasonNumber = 101, SeasonNumber = tvdbSeasonNumber });
Subject.GetEpisodes(_parsedEpisodeInfo, _series, true, null);

View File

@ -51,10 +51,6 @@ 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()
@ -122,8 +118,8 @@ namespace NzbDrone.Core.Test.ParserTests.ParsingServiceTests
GivenMatchByTvRageId();
Mocker.GetMock<ISceneMappingService>()
.Setup(v => v.FindTvdbId(It.IsAny<string>(), It.IsAny<string>()))
.Returns(10);
.Setup(v => v.FindSceneMapping(It.IsAny<string>(), It.IsAny<string>()))
.Returns(new SceneMapping { TvdbId = 10 });
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 string SceneOrigin { get; set; }
public SearchMode? SearchMode { get; set; }
public string Comment { get; set; }
public string FilterRegex { get; set; }
public string Type { get; set; }

View File

@ -15,14 +15,10 @@ 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);
}
public class SceneMappingService : ISceneMappingService,
@ -60,27 +56,13 @@ namespace NzbDrone.Core.DataAugmentation.Scene
return new List<string>();
}
var names = mappings.Where(n => n.SeasonNumber.HasValue && seasonNumbers.Contains(n.SeasonNumber.Value) ||
n.SceneSeasonNumber.HasValue && sceneSeasonNumbers.Contains(n.SceneSeasonNumber.Value) ||
(n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1)
var names = mappings.Where(n => seasonNumbers.Contains(n.SeasonNumber ?? -1) ||
sceneSeasonNumbers.Contains(n.SceneSeasonNumber ?? -1) ||
(n.SeasonNumber ?? -1) == -1 && (n.SceneSeasonNumber ?? -1) == -1 && n.SceneOrigin != "tvdb")
.Where(n => IsEnglish(n.SearchTerm))
.Select(n => n.SearchTerm).Distinct().ToList();
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();
return names;
}
public int? FindTvdbId(string seriesTitle)
@ -141,44 +123,6 @@ namespace NzbDrone.Core.DataAugmentation.Scene
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()
{
_logger.Info("Updating Scene mappings");
@ -295,11 +239,6 @@ namespace NzbDrone.Core.DataAugmentation.Scene
return normalCandidates;
}
private List<string> FilterNonEnglish(List<string> titles)
{
return titles.Where(IsEnglish).ToList();
}
private bool IsEnglish(string title)
{
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)
{
if (clrValue != null)
if (clrValue != null && clrValue != DBNull.Value)
{
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;
if (singleEpisodeSpec == null) return Decision.Accept();
var seasonNumber = _sceneMappingService.GetTvdbSeasonNumber(remoteEpisode.ParsedEpisodeInfo.SeriesTitle,
remoteEpisode.ParsedEpisodeInfo.ReleaseTitle,
remoteEpisode.ParsedEpisodeInfo.SeasonNumber);
if (singleEpisodeSpec.SeasonNumber != seasonNumber)
if (singleEpisodeSpec.SeasonNumber != remoteEpisode.MappedSeasonNumber)
{
_logger.Debug("Season number does not match searched season number, skipping.");
return Decision.Reject("Wrong season");

View File

@ -38,11 +38,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.Search
private Decision IsSatisfiedBy(RemoteEpisode remoteEpisode, SingleEpisodeSearchCriteria singleEpisodeSpec)
{
var seasonNumber = _sceneMappingService.GetTvdbSeasonNumber(remoteEpisode.ParsedEpisodeInfo.SeriesTitle,
remoteEpisode.ParsedEpisodeInfo.ReleaseTitle,
remoteEpisode.ParsedEpisodeInfo.SeasonNumber);
if (singleEpisodeSpec.SeasonNumber != seasonNumber)
if (singleEpisodeSpec.SeasonNumber != remoteEpisode.MappedSeasonNumber)
{
_logger.Debug("Season number does not match searched season number, skipping.");
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)
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
{
Series = series,
Episodes = episodes,
ParsedEpisodeInfo = release.ParsedEpisodeInfo,
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);
}

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 List<string> SceneTitles { get; set; }
public List<SceneMapping> SceneMappings { get; set; }
public List<Episode> Episodes { get; set; }
public SearchMode SearchMode { get; set; }
public virtual bool MonitoredEpisodesOnly { get; set; }
public virtual bool UserInvokedSearch { 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");
}
return SearchDaily(series, episode, userInvokedSearch, interactiveSearch);
return SearchDaily(series, episode, false, userInvokedSearch, interactiveSearch);
}
if (series.SeriesType == SeriesTypes.Anime)
@ -78,19 +78,19 @@ namespace NzbDrone.Core.IndexerSearch
episode.AbsoluteEpisodeNumber == null)
{
// 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)
{
// 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)
@ -104,77 +104,195 @@ namespace NzbDrone.Core.IndexerSearch
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)
{
var series = _seriesService.GetSeries(seriesId);
if (series.SeriesType == SeriesTypes.Anime)
{
return SearchAnimeSeason(series, episodes, userInvokedSearch, interactiveSearch);
return SearchAnimeSeason(series, episodes, monitoredOnly, userInvokedSearch, interactiveSearch);
}
if (series.SeriesType == SeriesTypes.Daily)
{
return SearchDailySeason(series, episodes, userInvokedSearch, interactiveSearch);
return SearchDailySeason(series, episodes, monitoredOnly, userInvokedSearch, interactiveSearch);
}
if (seasonNumber == 0)
{
// search for special episodes in season 0
return SearchSpecial(series, episodes, userInvokedSearch, interactiveSearch);
}
var mappings = GetSceneSeasonMappings(series, episodes);
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)
{
return v.SceneSeasonNumber.Value;
}
return v.SeasonNumber;
}).Distinct();
// search for special episodes in season 0
downloadDecisions.AddRange(SearchSpecial(series, mapping.Episodes, monitoredOnly, userInvokedSearch, interactiveSearch));
continue;
}
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();
var searchSpec = Get<SingleEpisodeSearchCriteria>(series, sceneSeasonEpisodes.ToList(), userInvokedSearch, interactiveSearch);
Episodes = groupedEpisode.ToList(),
EpisodeMapping = episodeMapping,
SceneTitles = episodeMapping.SceneTitles,
SearchMode = episodeMapping.SearchMode,
SeasonNumber = episodeMapping.SeasonNumber
};
searchSpec.SeasonNumber = sceneSeasonEpisodes.Key;
searchSpec.MonitoredEpisodesOnly = monitoredOnly;
if (episode.SceneSeasonNumber.HasValue && episode.SceneEpisodeNumber.HasValue)
{
searchSpec.EpisodeNumber = episode.SceneEpisodeNumber.Value;
}
else
{
searchSpec.EpisodeNumber = episode.EpisodeNumber;
}
var decisions = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
if (dict.TryGetValue(seasonMapping, out var existing))
{
existing.Episodes.AddRange(seasonMapping.Episodes);
existing.SceneTitles.AddRange(seasonMapping.SceneTitles);
}
else
{
var searchSpec = Get<SeasonSearchCriteria>(series, sceneSeasonEpisodes.ToList(), userInvokedSearch, interactiveSearch);
searchSpec.SeasonNumber = sceneSeasonEpisodes.Key;
searchSpec.MonitoredEpisodesOnly = monitoredOnly;
var decisions = Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec);
downloadDecisions.AddRange(decisions);
dict[seasonMapping] = seasonMapping;
}
}
}
else
foreach (var item in dict)
{
var searchSpec = Get<SeasonSearchCriteria>(series, episodes, userInvokedSearch, interactiveSearch);
searchSpec.SeasonNumber = seasonNumber;
searchSpec.MonitoredEpisodesOnly = monitoredOnly;
item.Value.Episodes = item.Value.Episodes.Distinct().ToList();
item.Value.SceneTitles = item.Value.SceneTitles.Distinct().ToList();
}
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);
downloadDecisions.AddRange(decisions);
@ -183,36 +301,18 @@ namespace NzbDrone.Core.IndexerSearch
return downloadDecisions;
}
private List<DownloadDecision> SearchSingle(Series series, Episode episode, 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)
private List<DownloadDecision> SearchDaily(Series series, Episode episode, bool monitoredOnly, bool userInvokedSearch, bool interactiveSearch)
{
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;
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;
@ -233,11 +333,11 @@ namespace NzbDrone.Core.IndexerSearch
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 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>"
searchSpec.EpisodeQueryTitles = episodes.Where(e => !string.IsNullOrWhiteSpace(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
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;
}
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 episodesToSearch = episodes.Where(e =>
{
// Episode needs to be monitored if it's not an interactive search
if (!interactiveSearch && !e.Monitored)
{
return false;
}
// Ensure episode has an airdate and has already aired
return e.AirDateUtc.HasValue && e.AirDateUtc.Value.Before(DateTime.UtcNow);
});
// Episode needs to be monitored if it's not an interactive search
// and Ensure episode has an airdate and has already aired
var episodesToSearch = episodes
.Where(ep => interactiveSearch || !monitoredOnly || ep.Monitored)
.Where(ep => ep.AirDateUtc.HasValue && ep.AirDateUtc.Value.Before(DateTime.UtcNow))
.ToList();
foreach (var episode in episodesToSearch)
{
downloadDecisions.AddRange(SearchAnime(series, episode, userInvokedSearch, interactiveSearch, true));
downloadDecisions.AddRange(SearchAnime(series, episode, monitoredOnly, userInvokedSearch, interactiveSearch, true));
}
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>();
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();
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;
downloadDecisions.AddRange(Dispatch(indexer => indexer.Fetch(searchSpec), searchSpec));
}
else
{
downloadDecisions.AddRange(SearchDaily(series, yearEpisodes.First(), userInvokedSearch, interactiveSearch));
downloadDecisions.AddRange(SearchDaily(series, yearEpisodes.First(), monitoredOnly, userInvokedSearch, interactiveSearch));
}
}
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();
@ -311,16 +418,41 @@ 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))
{
spec.SceneTitles.Add(series.Title);
}
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.InteractiveSearch = interactiveSearch;

View File

@ -3,6 +3,7 @@ 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.FileList
@ -24,11 +25,22 @@ namespace NzbDrone.Core.Indexers.FileList
{
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();
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;
}
@ -37,11 +49,22 @@ namespace NzbDrone.Core.Indexers.FileList
{
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();
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;
}

View File

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

View File

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

View File

@ -15,6 +15,7 @@ namespace NzbDrone.Core.Parser
{
Series GetSeries(string title);
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);
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);
@ -116,30 +117,12 @@ namespace NzbDrone.Core.Parser
public RemoteEpisode Map(ParsedEpisodeInfo parsedEpisodeInfo, int tvdbId, int tvRageId, SearchCriteriaBase searchCriteria = null)
{
var remoteEpisode = new RemoteEpisode
{
ParsedEpisodeInfo = parsedEpisodeInfo,
};
return Map(parsedEpisodeInfo, tvdbId, tvRageId, null, searchCriteria);
}
var series = GetSeries(parsedEpisodeInfo, tvdbId, tvRageId, searchCriteria);
if (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, Series series)
{
return Map(parsedEpisodeInfo, 0, 0, series, null);
}
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)
{
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)
{
return _episodeService.GetEpisodesBySeason(series.Id, parsedEpisodeInfo.SeasonNumber);
return _episodeService.GetEpisodesBySeason(series.Id, mappedSeasonNumber);
}
if (parsedEpisodeInfo.IsDaily)
@ -173,10 +217,10 @@ namespace NzbDrone.Core.Parser
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)
@ -261,19 +305,18 @@ namespace NzbDrone.Core.Parser
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;
var sceneMappingTvdbId = _sceneMappingService.FindTvdbId(parsedEpisodeInfo.SeriesTitle, parsedEpisodeInfo.ReleaseTitle);
if (sceneMappingTvdbId.HasValue)
if (sceneMapping != null)
{
if (searchCriteria != null && searchCriteria.Series.TvdbId == sceneMappingTvdbId.Value)
if (searchCriteria != null && searchCriteria.Series.TvdbId == sceneMapping.TvdbId)
{
return searchCriteria.Series;
}
series = _seriesService.FindByTvdbId(sceneMappingTvdbId.Value);
series = _seriesService.FindByTvdbId(sceneMapping.TvdbId);
if (series == null)
{
@ -385,7 +428,7 @@ namespace NzbDrone.Core.Parser
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>();
@ -448,17 +491,9 @@ namespace NzbDrone.Core.Parser
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 seasonNumber = parsedEpisodeInfo.SeasonNumber;
if (sceneSource)
{
seasonNumber = _sceneMappingService.GetTvdbSeasonNumber(parsedEpisodeInfo.SeriesTitle,
parsedEpisodeInfo.ReleaseTitle,
parsedEpisodeInfo.SeasonNumber);
}
if (parsedEpisodeInfo.EpisodeNumbers == null)
{
@ -479,7 +514,7 @@ namespace NzbDrone.Core.Parser
if (!episodes.Any())
{
episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, seasonNumber, episodeNumber);
episodes = _episodeService.FindEpisodesBySceneNumbering(series.Id, mappedSeasonNumber, episodeNumber);
}
if (episodes != null && episodes.Any())
@ -499,12 +534,12 @@ namespace NzbDrone.Core.Parser
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)
{
episodeInfo = _episodeService.FindEpisode(series.Id, seasonNumber, episodeNumber);
episodeInfo = _episodeService.FindEpisode(series.Id, mappedSeasonNumber, episodeNumber);
}
if (episodeInfo != null)

View File

@ -5,5 +5,7 @@
public string Title { get; set; }
public int? SeasonNumber { 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;
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)