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

This commit is contained in:
C.J. Manca 2022-08-07 12:43:18 -06:00 committed by GitHub
parent e4c0e80e3e
commit ac7afc351c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 186 additions and 0 deletions

View File

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

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 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;

View File

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

View File

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

View File

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