New: Add maximum single episode age option (per indexer)

(cherry picked from commit ac7afc351ced2a4043fc6038f20495056d05b341)
This commit is contained in:
C.J. Manca 2022-08-07 12:43:18 -06:00 committed by servarr
parent c1dd253bc1
commit 1f23cf7e4c
8 changed files with 186 additions and 0 deletions

View File

@ -45,6 +45,7 @@ function EditIndexerModalContent(props) {
supportsSearch,
fields,
priority,
seasonSearchMaximumSingleEpisodeAge,
protocol,
downloadClientId
} = item;
@ -164,6 +165,23 @@ function EditIndexerModalContent(props) {
/>
</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
advancedSettings={advancedSettings}
isAdvanced={true}

View File

@ -11,6 +11,7 @@ namespace Lidarr.Api.V1.Indexers
public bool SupportsSearch { get; set; }
public DownloadProtocol Protocol { get; set; }
public int Priority { get; set; }
public int SeasonSearchMaximumSingleEpisodeAge { get; set; }
public int DownloadClientId { get; set; }
}
@ -32,6 +33,7 @@ namespace Lidarr.Api.V1.Indexers
resource.SupportsSearch = definition.SupportsSearch;
resource.Protocol = definition.Protocol;
resource.Priority = definition.Priority;
resource.SeasonSearchMaximumSingleEpisodeAge = definition.SeasonSearchMaximumSingleEpisodeAge;
resource.DownloadClientId = definition.DownloadClientId;
return resource;
@ -50,6 +52,7 @@ namespace Lidarr.Api.V1.Indexers
definition.EnableAutomaticSearch = resource.EnableAutomaticSearch;
definition.EnableInteractiveSearch = resource.EnableInteractiveSearch;
definition.Priority = resource.Priority;
definition.SeasonSearchMaximumSingleEpisodeAge = resource.SeasonSearchMaximumSingleEpisodeAge;
definition.DownloadClientId = resource.DownloadClientId;
return definition;

View File

@ -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();
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}
}

View File

@ -23,6 +23,7 @@ namespace NzbDrone.Core.Indexers
public abstract string Name { get; }
public abstract DownloadProtocol Protocol { get; }
public int Priority { get; set; }
public int SeasonSearchMaximumSingleEpisodeAge { get; set; }
public abstract bool SupportsRss { get; }
public abstract bool SupportsSearch { get; }
@ -82,6 +83,7 @@ namespace NzbDrone.Core.Indexers
c.Indexer = Definition.Name;
c.DownloadProtocol = Protocol;
c.IndexerPriority = ((IndexerDefinition)Definition).Priority;
c.SeasonSearchMaximumSingleEpisodeAge = ((IndexerDefinition)Definition).SeasonSearchMaximumSingleEpisodeAge;
});
return result;

View File

@ -12,6 +12,7 @@ namespace NzbDrone.Core.Indexers
public bool SupportsRss { get; set; }
public bool SupportsSearch { get; set; }
public int Priority { get; set; } = 25;
public int SeasonSearchMaximumSingleEpisodeAge { get; set; } = 0;
public override bool Enable => EnableRss || EnableAutomaticSearch || EnableInteractiveSearch;

View File

@ -18,6 +18,7 @@ namespace NzbDrone.Core.Parser.Model
public string Artist { get; set; }
public string Album { get; set; }
public int IndexerPriority { get; set; }
public int SeasonSearchMaximumSingleEpisodeAge { get; set; }
public DownloadProtocol DownloadProtocol { get; set; }
public DateTime PublishDate { get; set; }