mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-05 13:20:20 -05:00
New: Add option to not download before air date to Release Profiles
Closes #969
This commit is contained in:
@@ -39,8 +39,17 @@ function EditReleaseProfileModalContent({
|
||||
saveProvider,
|
||||
} = useManageReleaseProfile(id ?? 0);
|
||||
|
||||
const { name, enabled, required, ignored, indexerIds, tags, excludedTags } =
|
||||
item;
|
||||
const {
|
||||
name,
|
||||
enabled,
|
||||
required,
|
||||
ignored,
|
||||
airDateRestriction,
|
||||
airDateGracePeriod,
|
||||
indexerIds,
|
||||
tags,
|
||||
excludedTags,
|
||||
} = item;
|
||||
|
||||
const wasSaving = usePrevious(isSaving);
|
||||
|
||||
@@ -131,6 +140,33 @@ function EditReleaseProfileModalContent({
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AirDateRestriction')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
{...airDateRestriction}
|
||||
type={inputTypes.CHECK}
|
||||
name="airDateRestriction"
|
||||
helpText={translate('AirDateRestrictionHelpText')}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
{airDateRestriction.value ? (
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('AirDateGracePeriod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
{...airDateGracePeriod}
|
||||
type={inputTypes.NUMBER}
|
||||
unit="days"
|
||||
name="airDateGracePeriod"
|
||||
helpText={translate('AirDateGracePeriodHelpText')}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : null}
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Indexer')}</FormLabel>
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ export interface ReleaseProfileModel extends ModelBase {
|
||||
enabled: boolean;
|
||||
required: string[];
|
||||
ignored: string[];
|
||||
airDateRestriction: boolean;
|
||||
airDateGracePeriod: number;
|
||||
indexerIds: number[];
|
||||
tags: number[];
|
||||
excludedTags: number[];
|
||||
@@ -23,6 +25,8 @@ const NEW_RELEASE_PROFILE: ReleaseProfileModel = {
|
||||
enabled: true,
|
||||
required: [],
|
||||
ignored: [],
|
||||
airDateRestriction: false,
|
||||
airDateGracePeriod: 0,
|
||||
indexerIds: [],
|
||||
tags: [],
|
||||
excludedTags: [],
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using FizzWare.NBuilder;
|
||||
using FluentAssertions;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using NzbDrone.Core.DecisionEngine.Specifications;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
using NzbDrone.Core.Test.Framework;
|
||||
using NzbDrone.Core.Tv;
|
||||
|
||||
namespace NzbDrone.Core.Test.DecisionEngineTests
|
||||
{
|
||||
[TestFixture]
|
||||
public class AirDateSpecificationFixture : CoreTest<AirDateSpecification>
|
||||
{
|
||||
private RemoteEpisode _remoteEpisode;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
_remoteEpisode = new RemoteEpisode
|
||||
{
|
||||
Series = new Series
|
||||
{
|
||||
Tags = new HashSet<int>()
|
||||
},
|
||||
Episodes = Builder<Episode>.CreateListOfSize(1)
|
||||
.All()
|
||||
.With(e => e.AirDateUtc = DateTime.UtcNow)
|
||||
.Build()
|
||||
.ToList(),
|
||||
Release = new ReleaseInfo
|
||||
{
|
||||
PublishDate = DateTime.UtcNow.AddDays(-1)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void GivenSettings(bool airDateRestriction, int gracePeriod)
|
||||
{
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(new List<ReleaseProfile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AirDateRestriction = airDateRestriction,
|
||||
AirDateGracePeriod = gracePeriod
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_if_profile_does_not_enforce_air_date_restriction()
|
||||
{
|
||||
GivenSettings(false, 0);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_if_release_date_is_after_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(1);
|
||||
|
||||
GivenSettings(true, 0);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_if_release_date_with_grace_period_is_after_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(1);
|
||||
|
||||
GivenSettings(true, -2);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_true_if_release_date_is_the_same_as_air_date()
|
||||
{
|
||||
var airDate = DateTime.UtcNow;
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = airDate;
|
||||
_remoteEpisode.Release.PublishDate = airDate;
|
||||
|
||||
GivenSettings(true, 0);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_air_date_is_null()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = null;
|
||||
|
||||
GivenSettings(true, -2);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_release_date_is_before_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
GivenSettings(true, 0);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_release_date_with_grace_period_is_before_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-3);
|
||||
|
||||
GivenSettings(true, -2);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_release_date_is_after_air_date_and_grace_period_is_positive()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(1);
|
||||
|
||||
GivenSettings(true, 2);
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_release_date_with_highest_grace_period_is_before_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(new List<ReleaseProfile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AirDateRestriction = true,
|
||||
AirDateGracePeriod = 0
|
||||
},
|
||||
new()
|
||||
{
|
||||
AirDateRestriction = true,
|
||||
AirDateGracePeriod = -5
|
||||
}
|
||||
});
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void should_be_false_if_one_release_profile_does_not_allow_grabbing_before_air_date()
|
||||
{
|
||||
_remoteEpisode.Episodes.First().AirDateUtc = DateTime.UtcNow;
|
||||
_remoteEpisode.Release.PublishDate = DateTime.UtcNow.AddDays(-1);
|
||||
|
||||
Mocker.GetMock<IReleaseProfileService>()
|
||||
.Setup(s => s.EnabledForTags(It.IsAny<HashSet<int>>(), It.IsAny<int>()))
|
||||
.Returns(new List<ReleaseProfile>
|
||||
{
|
||||
new()
|
||||
{
|
||||
AirDateRestriction = true,
|
||||
AirDateGracePeriod = 0
|
||||
},
|
||||
new()
|
||||
{
|
||||
AirDateRestriction = false,
|
||||
AirDateGracePeriod = 0
|
||||
}
|
||||
});
|
||||
|
||||
Subject.IsSatisfiedBy(_remoteEpisode, new()).Accepted.Should().BeFalse();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
using FluentMigrator;
|
||||
using NzbDrone.Core.Datastore.Migration.Framework;
|
||||
|
||||
namespace NzbDrone.Core.Datastore.Migration;
|
||||
|
||||
[Migration(226)]
|
||||
public class add_air_date_filtering_to_release_profiles : NzbDroneMigrationBase
|
||||
{
|
||||
protected override void MainDbUpgrade()
|
||||
{
|
||||
Alter.Table("ReleaseProfiles").AddColumn("AirDateRestriction").AsBoolean().WithDefaultValue(false);
|
||||
Alter.Table("ReleaseProfiles").AddColumn("AirDateGracePeriod").AsInt32().WithDefaultValue(0);
|
||||
}
|
||||
}
|
||||
@@ -75,5 +75,6 @@ public enum DownloadRejectionReason
|
||||
DiskCustomFormatScore,
|
||||
DiskCustomFormatScoreIncrement,
|
||||
DiskUpgradesNotAllowed,
|
||||
DiskNotUpgrade
|
||||
DiskNotUpgrade,
|
||||
BeforeAirDate
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NLog;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Parser.Model;
|
||||
using NzbDrone.Core.Profiles.Releases;
|
||||
|
||||
namespace NzbDrone.Core.DecisionEngine.Specifications
|
||||
{
|
||||
public class AirDateSpecification : IDownloadDecisionEngineSpecification
|
||||
{
|
||||
private readonly Logger _logger;
|
||||
private readonly IReleaseProfileService _releaseProfileService;
|
||||
private readonly ITermMatcherService _termMatcherService;
|
||||
|
||||
public AirDateSpecification(ITermMatcherService termMatcherService, IReleaseProfileService releaseProfileService, Logger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
_releaseProfileService = releaseProfileService;
|
||||
_termMatcherService = termMatcherService;
|
||||
}
|
||||
|
||||
public SpecificationPriority Priority => SpecificationPriority.Database;
|
||||
public RejectionType Type => RejectionType.Permanent;
|
||||
|
||||
public virtual DownloadSpecDecision IsSatisfiedBy(RemoteEpisode subject, ReleaseDecisionInformation information)
|
||||
{
|
||||
_logger.Debug("Checking if release meets air date restrictions: {0}", subject);
|
||||
|
||||
var releaseProfiles = _releaseProfileService.EnabledForTags(subject.Series.Tags, subject.Release.IndexerId);
|
||||
|
||||
if (releaseProfiles.Empty())
|
||||
{
|
||||
_logger.Debug("No Release Profile, accepting");
|
||||
return DownloadSpecDecision.Accept();
|
||||
}
|
||||
|
||||
var bestProfile = releaseProfiles
|
||||
.OrderByDescending(p => p.AirDateRestriction ? 1 : 0)
|
||||
.ThenByDescending(p => p.AirDateGracePeriod)
|
||||
.First();
|
||||
|
||||
if (!bestProfile.AirDateRestriction)
|
||||
{
|
||||
_logger.Debug("Release Profile does not prevent grabbing before release date, accepting");
|
||||
return DownloadSpecDecision.Accept();
|
||||
}
|
||||
|
||||
var releaseDate = subject.Release.PublishDate;
|
||||
var gracePeriod = bestProfile.AirDateGracePeriod;
|
||||
|
||||
foreach (var episode in subject.Episodes)
|
||||
{
|
||||
var airDate = episode.AirDateUtc;
|
||||
|
||||
if (!airDate.HasValue)
|
||||
{
|
||||
_logger.Debug("No air date available, rejecting");
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.BeforeAirDate, "No air date available");
|
||||
}
|
||||
|
||||
var adjustedAirDate = airDate.Value.AddDays(gracePeriod);
|
||||
|
||||
if (releaseDate < adjustedAirDate)
|
||||
{
|
||||
return DownloadSpecDecision.Reject(DownloadRejectionReason.BeforeAirDate, "Release date {0} is before adjusted air date of {1} (Air Date: {2}. Grace period {3} days)", releaseDate, adjustedAirDate, airDate, gracePeriod);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("All episodes within air date limitations, allowing");
|
||||
return DownloadSpecDecision.Accept();
|
||||
}
|
||||
|
||||
private ReleaseProfile FindBestProfile(List<ReleaseProfile> releaseProfiles)
|
||||
{
|
||||
return releaseProfiles
|
||||
.OrderBy(p => p.AirDateRestriction ? 0 : 1)
|
||||
.ThenBy(p => p.AirDateGracePeriod)
|
||||
.ThenBy(p => p.AirDateRestriction ? 0 : 1)
|
||||
.FirstOrDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,10 @@
|
||||
"AgeWhenGrabbed": "Age (when grabbed)",
|
||||
"Agenda": "Agenda",
|
||||
"AirDate": "Air Date",
|
||||
"AirDateGracePeriod": "Air Date Grace Period",
|
||||
"AirDateGracePeriodHelpText": "Negative values allow grabbing before the air date, positive values prevent grabbing after the air date.",
|
||||
"AirDateRestriction": "Reject Unaired Releases",
|
||||
"AirDateRestrictionHelpText": "Prevents {appName} from grabbing releases that contain episodes that have not yet aired.",
|
||||
"Airs": "Airs",
|
||||
"AirsDateAtTimeOn": "{date} at {time} on {networkLabel}",
|
||||
"AirsTbaOn": "TBA on {networkLabel}",
|
||||
|
||||
@@ -9,6 +9,8 @@ namespace NzbDrone.Core.Profiles.Releases
|
||||
public bool Enabled { get; set; }
|
||||
public List<string> Required { get; set; }
|
||||
public List<string> Ignored { get; set; }
|
||||
public bool AirDateRestriction { get; set; }
|
||||
public int AirDateGracePeriod { get; set; }
|
||||
public List<int> IndexerIds { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public HashSet<int> ExcludedTags { get; set; }
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace Sonarr.Api.V3.Profiles.Release
|
||||
|
||||
SharedValidator.RuleFor(d => d).Custom((restriction, context) =>
|
||||
{
|
||||
if (restriction.MapRequired().Empty() && restriction.MapIgnored().Empty())
|
||||
if (restriction.MapRequired().Empty() && restriction.MapIgnored().Empty() && !restriction.AirDateRestriction)
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfileResource.Required), "'Must contain' or 'Must not contain' is required");
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ namespace Sonarr.Api.V3.Profiles.Release
|
||||
// Is List<string>, string or JArray, we accept 'string' with POST for backward compatibility
|
||||
public object Required { get; set; }
|
||||
public object Ignored { get; set; }
|
||||
public bool AirDateRestriction { get; set; }
|
||||
public int AirDateGracePeriod { get; set; }
|
||||
public int IndexerId { get; set; }
|
||||
public HashSet<int> Tags { get; set; }
|
||||
public HashSet<int> ExcludedTags { get; set; }
|
||||
@@ -42,6 +44,8 @@ namespace Sonarr.Api.V3.Profiles.Release
|
||||
Enabled = model.Enabled,
|
||||
Required = model.Required ?? new List<string>(),
|
||||
Ignored = model.Ignored ?? new List<string>(),
|
||||
AirDateRestriction = model.AirDateRestriction,
|
||||
AirDateGracePeriod = model.AirDateGracePeriod,
|
||||
IndexerId = model.IndexerIds.FirstOrDefault(0),
|
||||
Tags = new HashSet<int>(model.Tags),
|
||||
ExcludedTags = new HashSet<int>(model.ExcludedTags)
|
||||
@@ -62,6 +66,8 @@ namespace Sonarr.Api.V3.Profiles.Release
|
||||
Enabled = resource.Enabled,
|
||||
Required = resource.MapRequired(),
|
||||
Ignored = resource.MapIgnored(),
|
||||
AirDateRestriction = resource.AirDateRestriction,
|
||||
AirDateGracePeriod = resource.AirDateGracePeriod,
|
||||
IndexerIds = new List<int> { resource.IndexerId },
|
||||
Tags = new HashSet<int>(resource.Tags),
|
||||
ExcludedTags = new HashSet<int>(resource.ExcludedTags)
|
||||
|
||||
@@ -21,7 +21,7 @@ public class ReleaseProfileController : RestController<ReleaseProfileResource>
|
||||
|
||||
SharedValidator.RuleFor(d => d).Custom((restriction, context) =>
|
||||
{
|
||||
if (restriction.Required.Empty() && restriction.Ignored.Empty())
|
||||
if (restriction.Required.Empty() && restriction.Ignored.Empty() && !restriction.AirDateRestriction)
|
||||
{
|
||||
context.AddFailure(nameof(ReleaseProfileResource.Required), "'Must contain' or 'Must not contain' is required");
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ public class ReleaseProfileResource : RestResource
|
||||
public bool Enabled { get; set; }
|
||||
public List<string> Required { get; set; } = [];
|
||||
public List<string> Ignored { get; set; } = [];
|
||||
public bool AirDateRestriction { get; set; }
|
||||
public int AirDateGracePeriod { get; set; }
|
||||
public List<int> IndexerIds { get; set; } = [];
|
||||
public HashSet<int> Tags { get; set; } = [];
|
||||
public HashSet<int> ExcludedTags { get; set; } = [];
|
||||
@@ -25,6 +27,8 @@ public static class RestrictionResourceMapper
|
||||
Enabled = model.Enabled,
|
||||
Required = model.Required ?? [],
|
||||
Ignored = model.Ignored ?? [],
|
||||
AirDateRestriction = model.AirDateRestriction,
|
||||
AirDateGracePeriod = model.AirDateGracePeriod,
|
||||
IndexerIds = model.IndexerIds ?? [],
|
||||
Tags = model.Tags ?? [],
|
||||
ExcludedTags = model.ExcludedTags ?? [],
|
||||
@@ -40,6 +44,8 @@ public static class RestrictionResourceMapper
|
||||
Enabled = resource.Enabled,
|
||||
Required = resource.Required,
|
||||
Ignored = resource.Ignored,
|
||||
AirDateRestriction = resource.AirDateRestriction,
|
||||
AirDateGracePeriod = resource.AirDateGracePeriod,
|
||||
IndexerIds = resource.IndexerIds,
|
||||
Tags = resource.Tags,
|
||||
ExcludedTags = resource.ExcludedTags
|
||||
|
||||
Reference in New Issue
Block a user