New: Add maximum single episode age option (per indexer)
This commit is contained in:
parent
e4c0e80e3e
commit
ac7afc351c
|
@ -45,6 +45,7 @@ function EditIndexerModalContent(props) {
|
||||||
tags,
|
tags,
|
||||||
fields,
|
fields,
|
||||||
priority,
|
priority,
|
||||||
|
seasonSearchMaximumSingleEpisodeAge,
|
||||||
protocol,
|
protocol,
|
||||||
downloadClientId
|
downloadClientId
|
||||||
} = item;
|
} = item;
|
||||||
|
@ -153,6 +154,23 @@ function EditIndexerModalContent(props) {
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
advancedSettings={advancedSettings}
|
||||||
|
isAdvanced={true}
|
||||||
|
>
|
||||||
|
<FormLabel>Maximum Single Episode Age</FormLabel>
|
||||||
|
|
||||||
|
<FormInputGroup
|
||||||
|
type={inputTypes.NUMBER}
|
||||||
|
name="seasonSearchMaximumSingleEpisodeAge"
|
||||||
|
helpText="During a full season search only season packs will be allowed when the season's last episode is older than this setting. Standard series only. Use 0 to disable."
|
||||||
|
min={0}
|
||||||
|
unit="days"
|
||||||
|
{...seasonSearchMaximumSingleEpisodeAge}
|
||||||
|
onChange={onInputChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup
|
<FormGroup
|
||||||
advancedSettings={advancedSettings}
|
advancedSettings={advancedSettings}
|
||||||
isAdvanced={true}
|
isAdvanced={true}
|
||||||
|
|
|
@ -0,0 +1,96 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
using NzbDrone.Core.Qualities;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using FluentAssertions;
|
||||||
|
using FizzWare.NBuilder;
|
||||||
|
using NzbDrone.Core.Test.Framework;
|
||||||
|
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class SingleEpisodeAgeDownloadDecisionFixture : CoreTest<SeasonPackOnlySpecification>
|
||||||
|
{
|
||||||
|
private RemoteEpisode parseResultMulti;
|
||||||
|
private RemoteEpisode parseResultSingle;
|
||||||
|
private Series series;
|
||||||
|
private List<Episode> episodes;
|
||||||
|
private SeasonSearchCriteria multiSearch;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
series = Builder<Series>.CreateNew()
|
||||||
|
.With(s => s.Seasons = Builder<Season>.CreateListOfSize(1).Build().ToList())
|
||||||
|
.With(s => s.SeriesType = SeriesTypes.Standard)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
episodes = new List<Episode>();
|
||||||
|
episodes.Add(CreateEpisodeStub(1, 400));
|
||||||
|
episodes.Add(CreateEpisodeStub(2, 370));
|
||||||
|
episodes.Add(CreateEpisodeStub(3, 340));
|
||||||
|
episodes.Add(CreateEpisodeStub(4, 310));
|
||||||
|
|
||||||
|
multiSearch = new SeasonSearchCriteria();
|
||||||
|
multiSearch.Episodes = episodes.ToList();
|
||||||
|
multiSearch.SeasonNumber = 1;
|
||||||
|
|
||||||
|
parseResultMulti = new RemoteEpisode
|
||||||
|
{
|
||||||
|
Series = series,
|
||||||
|
Release = new ReleaseInfo(),
|
||||||
|
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)), FullSeason = true },
|
||||||
|
Episodes = episodes.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
parseResultSingle = new RemoteEpisode
|
||||||
|
{
|
||||||
|
Series = series,
|
||||||
|
Release = new ReleaseInfo(),
|
||||||
|
ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.SDTV, new Revision(version: 2)) },
|
||||||
|
Episodes = new List<Episode>()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Episode CreateEpisodeStub(int number, int age)
|
||||||
|
{
|
||||||
|
return new Episode() {
|
||||||
|
SeasonNumber = 1,
|
||||||
|
EpisodeNumber = number,
|
||||||
|
AirDateUtc = DateTime.UtcNow.AddDays(-age)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1, 200, false)]
|
||||||
|
[TestCase(4, 200, false)]
|
||||||
|
[TestCase(1, 600, true)]
|
||||||
|
[TestCase(1, 365, true)]
|
||||||
|
[TestCase(4, 365, true)]
|
||||||
|
[TestCase(1, 0, true)]
|
||||||
|
public void single_episode_release(int episode, int SeasonSearchMaximumSingleEpisodeAge, bool expectedResult)
|
||||||
|
{
|
||||||
|
parseResultSingle.Release.SeasonSearchMaximumSingleEpisodeAge = SeasonSearchMaximumSingleEpisodeAge;
|
||||||
|
parseResultSingle.Episodes.Clear();
|
||||||
|
parseResultSingle.Episodes.Add(episodes.Find(e => e.EpisodeNumber == episode));
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(parseResultSingle, multiSearch).Accepted.Should().Be(expectedResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
// should always accept all season packs
|
||||||
|
[TestCase(200, true)]
|
||||||
|
[TestCase(600, true)]
|
||||||
|
[TestCase(365, true)]
|
||||||
|
[TestCase(0, true)]
|
||||||
|
public void multi_episode_release(int SeasonSearchMaximumSingleEpisodeAge, bool expectedResult)
|
||||||
|
{
|
||||||
|
parseResultMulti.Release.SeasonSearchMaximumSingleEpisodeAge = SeasonSearchMaximumSingleEpisodeAge;
|
||||||
|
|
||||||
|
Subject.IsSatisfiedBy(parseResultMulti, multiSearch).Accepted.Should().BeTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
using FluentMigrator;
|
||||||
|
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.Datastore.Migration
|
||||||
|
{
|
||||||
|
[Migration(172)]
|
||||||
|
public class add_SeasonSearchMaximumSingleEpisodeAge_to_indexers : NzbDroneMigrationBase
|
||||||
|
{
|
||||||
|
protected override void MainDbUpgrade()
|
||||||
|
{
|
||||||
|
Alter.Table("Indexers").AddColumn("SeasonSearchMaximumSingleEpisodeAge").AsInt32().NotNullable().WithDefaultValue(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
using System;
|
||||||
|
using NLog;
|
||||||
|
using NzbDrone.Core.Configuration;
|
||||||
|
using NzbDrone.Core.IndexerSearch.Definitions;
|
||||||
|
using NzbDrone.Core.Parser.Model;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using System.Linq;
|
||||||
|
using NzbDrone.Core.Tv;
|
||||||
|
|
||||||
|
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||||
|
{
|
||||||
|
public class SeasonPackOnlySpecification : IDecisionEngineSpecification
|
||||||
|
{
|
||||||
|
private readonly IConfigService _configService;
|
||||||
|
private readonly Logger _logger;
|
||||||
|
|
||||||
|
public SeasonPackOnlySpecification(IConfigService configService, Logger logger)
|
||||||
|
{
|
||||||
|
_configService = configService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpecificationPriority Priority => SpecificationPriority.Default;
|
||||||
|
public RejectionType Type => RejectionType.Permanent;
|
||||||
|
|
||||||
|
public virtual Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria)
|
||||||
|
{
|
||||||
|
if (searchCriteria == null || searchCriteria.Episodes.Count == 1)
|
||||||
|
{
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subject.Release.SeasonSearchMaximumSingleEpisodeAge > 0)
|
||||||
|
{
|
||||||
|
if (subject.Series.SeriesType == SeriesTypes.Standard && !subject.ParsedEpisodeInfo.FullSeason && subject.Episodes.Count >= 1)
|
||||||
|
{
|
||||||
|
// test against episodes of the same season in the current search, and make sure they have an air date
|
||||||
|
var subset = searchCriteria.Episodes.Where(e => e.AirDateUtc.HasValue && e.SeasonNumber == subject.Episodes.First().SeasonNumber).ToList();
|
||||||
|
|
||||||
|
if (subset.Count() > 0 && subset.Max(e => e.AirDateUtc).Value.Before(DateTime.UtcNow - TimeSpan.FromDays(subject.Release.SeasonSearchMaximumSingleEpisodeAge)))
|
||||||
|
{
|
||||||
|
_logger.Debug("Release {0}: last episode in this season aired more than {1} days ago, season pack required.", subject.Release.Title, subject.Release.SeasonSearchMaximumSingleEpisodeAge);
|
||||||
|
return Decision.Reject("Last episode in this season aired more than {0} days ago, season pack required.", subject.Release.SeasonSearchMaximumSingleEpisodeAge);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Decision.Accept();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,6 +23,7 @@ namespace NzbDrone.Core.Indexers
|
||||||
public abstract string Name { get; }
|
public abstract string Name { get; }
|
||||||
public abstract DownloadProtocol Protocol { get; }
|
public abstract DownloadProtocol Protocol { get; }
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
|
public int SeasonSearchMaximumSingleEpisodeAge { get; set; }
|
||||||
|
|
||||||
public abstract bool SupportsRss { get; }
|
public abstract bool SupportsRss { get; }
|
||||||
public abstract bool SupportsSearch { get; }
|
public abstract bool SupportsSearch { get; }
|
||||||
|
@ -81,6 +82,7 @@ namespace NzbDrone.Core.Indexers
|
||||||
c.Indexer = Definition.Name;
|
c.Indexer = Definition.Name;
|
||||||
c.DownloadProtocol = Protocol;
|
c.DownloadProtocol = Protocol;
|
||||||
c.IndexerPriority = ((IndexerDefinition)Definition).Priority;
|
c.IndexerPriority = ((IndexerDefinition)Definition).Priority;
|
||||||
|
c.SeasonSearchMaximumSingleEpisodeAge = ((IndexerDefinition)Definition).SeasonSearchMaximumSingleEpisodeAge;
|
||||||
});
|
});
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -12,6 +12,7 @@ namespace NzbDrone.Core.Indexers
|
||||||
public bool SupportsRss { get; set; }
|
public bool SupportsRss { get; set; }
|
||||||
public bool SupportsSearch { get; set; }
|
public bool SupportsSearch { get; set; }
|
||||||
public int Priority { get; set; } = 25;
|
public int Priority { get; set; } = 25;
|
||||||
|
public int SeasonSearchMaximumSingleEpisodeAge { get; set; } = 0;
|
||||||
|
|
||||||
public override bool Enable => EnableRss || EnableAutomaticSearch || EnableInteractiveSearch;
|
public override bool Enable => EnableRss || EnableAutomaticSearch || EnableInteractiveSearch;
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ namespace NzbDrone.Core.Parser.Model
|
||||||
public int IndexerId { get; set; }
|
public int IndexerId { get; set; }
|
||||||
public string Indexer { get; set; }
|
public string Indexer { get; set; }
|
||||||
public int IndexerPriority { get; set; }
|
public int IndexerPriority { get; set; }
|
||||||
|
public int SeasonSearchMaximumSingleEpisodeAge { get; set; }
|
||||||
public DownloadProtocol DownloadProtocol { get; set; }
|
public DownloadProtocol DownloadProtocol { get; set; }
|
||||||
public int TvdbId { get; set; }
|
public int TvdbId { get; set; }
|
||||||
public int TvRageId { get; set; }
|
public int TvRageId { get; set; }
|
||||||
|
|
|
@ -11,6 +11,7 @@ namespace Sonarr.Api.V3.Indexers
|
||||||
public bool SupportsSearch { get; set; }
|
public bool SupportsSearch { get; set; }
|
||||||
public DownloadProtocol Protocol { get; set; }
|
public DownloadProtocol Protocol { get; set; }
|
||||||
public int Priority { get; set; }
|
public int Priority { get; set; }
|
||||||
|
public int SeasonSearchMaximumSingleEpisodeAge { get; set; }
|
||||||
public int DownloadClientId { get; set; }
|
public int DownloadClientId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -29,6 +30,7 @@ namespace Sonarr.Api.V3.Indexers
|
||||||
resource.SupportsSearch = definition.SupportsSearch;
|
resource.SupportsSearch = definition.SupportsSearch;
|
||||||
resource.Protocol = definition.Protocol;
|
resource.Protocol = definition.Protocol;
|
||||||
resource.Priority = definition.Priority;
|
resource.Priority = definition.Priority;
|
||||||
|
resource.SeasonSearchMaximumSingleEpisodeAge = definition.SeasonSearchMaximumSingleEpisodeAge;
|
||||||
resource.DownloadClientId = definition.DownloadClientId;
|
resource.DownloadClientId = definition.DownloadClientId;
|
||||||
|
|
||||||
return resource;
|
return resource;
|
||||||
|
@ -44,6 +46,7 @@ namespace Sonarr.Api.V3.Indexers
|
||||||
definition.EnableAutomaticSearch = resource.EnableAutomaticSearch;
|
definition.EnableAutomaticSearch = resource.EnableAutomaticSearch;
|
||||||
definition.EnableInteractiveSearch = resource.EnableInteractiveSearch;
|
definition.EnableInteractiveSearch = resource.EnableInteractiveSearch;
|
||||||
definition.Priority = resource.Priority;
|
definition.Priority = resource.Priority;
|
||||||
|
definition.SeasonSearchMaximumSingleEpisodeAge = resource.SeasonSearchMaximumSingleEpisodeAge;
|
||||||
definition.DownloadClientId = resource.DownloadClientId;
|
definition.DownloadClientId = resource.DownloadClientId;
|
||||||
|
|
||||||
return definition;
|
return definition;
|
||||||
|
|
Loading…
Reference in New Issue