From 853f25468c286f927aede0c267852a2cbf0477f3 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sat, 20 Oct 2018 13:27:51 -0700 Subject: [PATCH] Preferred words New: Ability to prefer releases based on terms in release title --- src/NzbDrone.Api/Calendar/CalendarModule.cs | 1 + .../EpisodeFiles/EpisodeFileModule.cs | 1 + .../EpisodeFiles/EpisodeFileResource.cs | 1 + src/NzbDrone.Api/Episodes/EpisodeModule.cs | 1 + .../Episodes/EpisodeModuleWithSignalR.cs | 1 + src/NzbDrone.Api/History/HistoryModule.cs | 1 + .../Restrictions/RestrictionModule.cs | 18 ++-- .../Restrictions/RestrictionResource.cs | 15 ++- src/NzbDrone.Api/Wanted/CutoffModule.cs | 1 + src/NzbDrone.Api/Wanted/MissingModule.cs | 1 + .../CutoffSpecificationFixture.cs | 75 +++++++++++---- .../DownloadDecisionMakerFixture.cs | 3 +- .../QueueSpecificationFixture.cs | 49 ++++++++-- ...ReleaseRestrictionsSpecificationFixture.cs | 20 ++-- .../RssSync/DelaySpecificationFixture.cs | 9 +- .../RssSync/HistorySpecificationFixture.cs | 1 + .../RssSync/ProperSpecificationFixture.cs | 2 +- .../SameEpisodesSpecificationFixture.cs | 6 +- ...ture.cs => UpgradeSpecificationFixture.cs} | 44 ++++++--- .../Housekeepers/CleanupUnusedTagsFixture.cs | 6 +- .../MoveEpisodeFileFixture.cs | 4 +- .../NzbDrone.Core.Test.csproj | 3 +- .../PreferredWordService/CalculateFixture.cs | 95 +++++++++++++++++++ .../Migration/127_rename_release_profiles.cs | 15 +++ src/NzbDrone.Core/Datastore/TableMapping.cs | 5 +- .../DownloadDecisionComparer.cs | 8 +- .../DecisionEngine/DownloadDecisionMaker.cs | 10 +- .../Specifications/CutoffSpecification.cs | 16 +++- .../IDecisionEngineSpecification.cs | 2 +- .../Specifications/QueueSpecification.cs | 33 ++++--- .../ReleaseRestrictionsSpecification.cs | 10 +- .../RssSync/DelaySpecification.cs | 33 ++++--- .../RssSync/HistorySpecification.cs | 29 +++++- .../SameEpisodesSpecification.cs | 2 +- .../UpgradableSpecification.cs | 51 +++++++--- .../UpgradeDiskSpecification.cs | 13 ++- .../AggregatePreferredWordScore.cs | 22 +++++ .../Aggregators/IAggregateRemoteEpisode.cs | 9 ++ .../RemoteEpisodeAggregationService.cs | 44 +++++++++ .../Housekeepers/CleanupUnusedTags.cs | 4 +- src/NzbDrone.Core/MediaFiles/EpisodeFile.cs | 5 + .../SameEpisodesImportSpecification.cs | 1 + src/NzbDrone.Core/NzbDrone.Core.csproj | 18 ++-- .../Organizer/FileNameBuilder.cs | 19 +++- .../Organizer/FileNameSampleService.cs | 8 +- .../Parser/Model/RemoteEpisode.cs | 1 + .../Releases}/PerlRegexFactory.cs | 8 +- .../Profiles/Releases/PreferredWordService.cs | 76 +++++++++++++++ .../Profiles/Releases/ReleaseProfile.cs | 21 ++++ .../Releases/ReleaseProfileRepository.cs | 17 ++++ .../Releases/ReleaseProfileService.cs | 65 +++++++++++++ .../Releases}/TermMatcher.cs | 5 +- src/NzbDrone.Core/Restrictions/Restriction.cs | 18 ---- .../Restrictions/RestrictionRepository.cs | 17 ---- .../Restrictions/RestrictionService.cs | 65 ------------- src/NzbDrone.Core/Tags/TagService.cs | 12 +-- src/Sonarr.Api.V3/Calendar/CalendarModule.cs | 1 + .../EpisodeFiles/EpisodeFileModule.cs | 1 + .../EpisodeFiles/EpisodeFileResource.cs | 1 + src/Sonarr.Api.V3/Episodes/EpisodeModule.cs | 1 + .../Episodes/EpisodeModuleWithSignalR.cs | 1 + src/Sonarr.Api.V3/History/HistoryModule.cs | 1 + .../Indexers/ReleaseModuleBase.cs | 14 ++- src/Sonarr.Api.V3/Indexers/ReleaseResource.cs | 4 +- .../Profiles/Release/ReleaseProfileModule.cs | 60 ++++++++++++ .../Release/ReleaseProfileResource.cs} | 29 +++--- .../Restrictions/RestrictionModule.cs | 60 ------------ src/Sonarr.Api.V3/Sonarr.Api.V3.csproj | 4 +- src/Sonarr.Api.V3/Wanted/CutoffModule.cs | 1 + src/Sonarr.Api.V3/Wanted/MissingModule.cs | 1 + 70 files changed, 852 insertions(+), 347 deletions(-) rename src/NzbDrone.Core.Test/DecisionEngineTests/{QualityUpgradeSpecificationFixture.cs => UpgradeSpecificationFixture.cs} (73%) create mode 100644 src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs create mode 100644 src/NzbDrone.Core/Datastore/Migration/127_rename_release_profiles.cs rename src/NzbDrone.Core/DecisionEngine/{ => Specifications}/IDecisionEngineSpecification.cs (85%) rename src/NzbDrone.Core/DecisionEngine/{ => Specifications}/SameEpisodesSpecification.cs (94%) rename src/NzbDrone.Core/DecisionEngine/{ => Specifications}/UpgradableSpecification.cs (71%) create mode 100644 src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs create mode 100644 src/NzbDrone.Core/Download/Aggregation/Aggregators/IAggregateRemoteEpisode.cs create mode 100644 src/NzbDrone.Core/Download/Aggregation/RemoteEpisodeAggregationService.cs rename src/NzbDrone.Core/{Restrictions => Profiles/Releases}/PerlRegexFactory.cs (92%) create mode 100644 src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs create mode 100644 src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs create mode 100644 src/NzbDrone.Core/Profiles/Releases/ReleaseProfileRepository.cs create mode 100644 src/NzbDrone.Core/Profiles/Releases/ReleaseProfileService.cs rename src/NzbDrone.Core/{Restrictions => Profiles/Releases}/TermMatcher.cs (93%) delete mode 100644 src/NzbDrone.Core/Restrictions/Restriction.cs delete mode 100644 src/NzbDrone.Core/Restrictions/RestrictionRepository.cs delete mode 100644 src/NzbDrone.Core/Restrictions/RestrictionService.cs create mode 100644 src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs rename src/Sonarr.Api.V3/{Restrictions/RestrictionResource.cs => Profiles/Release/ReleaseProfileResource.cs} (54%) delete mode 100644 src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs diff --git a/src/NzbDrone.Api/Calendar/CalendarModule.cs b/src/NzbDrone.Api/Calendar/CalendarModule.cs index 9a4d01dd4..8481e2ffb 100644 --- a/src/NzbDrone.Api/Calendar/CalendarModule.cs +++ b/src/NzbDrone.Api/Calendar/CalendarModule.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using NzbDrone.Api.Episodes; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Tv; using NzbDrone.SignalR; diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs index 1fc1603b9..fe6eda838 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileModule.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Exceptions; using NzbDrone.SignalR; using Sonarr.Http; diff --git a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs index af033ab51..38a9a611b 100644 --- a/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs +++ b/src/NzbDrone.Api/EpisodeFiles/EpisodeFileResource.cs @@ -1,6 +1,7 @@ using System; using System.IO; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.MediaFiles; using Sonarr.Http.REST; using NzbDrone.Core.Qualities; diff --git a/src/NzbDrone.Api/Episodes/EpisodeModule.cs b/src/NzbDrone.Api/Episodes/EpisodeModule.cs index 6ee311bfc..1f0794161 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeModule.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeModule.cs @@ -2,6 +2,7 @@ using Sonarr.Http.REST; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.SignalR; namespace NzbDrone.Api.Episodes diff --git a/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs b/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs index 291626eb0..ea6871f7d 100644 --- a/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs +++ b/src/NzbDrone.Api/Episodes/EpisodeModuleWithSignalR.cs @@ -4,6 +4,7 @@ using NzbDrone.Api.Series; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; diff --git a/src/NzbDrone.Api/History/HistoryModule.cs b/src/NzbDrone.Api/History/HistoryModule.cs index a5f4a3584..4e41deee2 100644 --- a/src/NzbDrone.Api/History/HistoryModule.cs +++ b/src/NzbDrone.Api/History/HistoryModule.cs @@ -7,6 +7,7 @@ using Sonarr.Http.Extensions; using NzbDrone.Api.Series; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; using NzbDrone.Core.History; using Sonarr.Http; diff --git a/src/NzbDrone.Api/Restrictions/RestrictionModule.cs b/src/NzbDrone.Api/Restrictions/RestrictionModule.cs index 569b9efd9..7188e57fb 100644 --- a/src/NzbDrone.Api/Restrictions/RestrictionModule.cs +++ b/src/NzbDrone.Api/Restrictions/RestrictionModule.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using FluentValidation.Results; using NzbDrone.Common.Extensions; -using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Profiles.Releases; using Sonarr.Http; using Sonarr.Http.Mapping; @@ -9,12 +9,12 @@ namespace NzbDrone.Api.Restrictions { public class RestrictionModule : SonarrRestModule { - private readonly IRestrictionService _restrictionService; + private readonly IReleaseProfileService _releaseProfileService; - public RestrictionModule(IRestrictionService restrictionService) + public RestrictionModule(IReleaseProfileService releaseProfileService) { - _restrictionService = restrictionService; + _releaseProfileService = releaseProfileService; GetResourceById = GetRestriction; GetResourceAll = GetAllRestrictions; @@ -35,27 +35,27 @@ namespace NzbDrone.Api.Restrictions private RestrictionResource GetRestriction(int id) { - return _restrictionService.Get(id).ToResource(); + return _releaseProfileService.Get(id).ToResource(); } private List GetAllRestrictions() { - return _restrictionService.All().ToResource(); + return _releaseProfileService.All().ToResource(); } private int CreateRestriction(RestrictionResource resource) { - return _restrictionService.Add(resource.ToModel()).Id; + return _releaseProfileService.Add(resource.ToModel()).Id; } private void UpdateRestriction(RestrictionResource resource) { - _restrictionService.Update(resource.ToModel()); + _releaseProfileService.Update(resource.ToModel()); } private void DeleteRestriction(int id) { - _restrictionService.Delete(id); + _releaseProfileService.Delete(id); } } } diff --git a/src/NzbDrone.Api/Restrictions/RestrictionResource.cs b/src/NzbDrone.Api/Restrictions/RestrictionResource.cs index 0e1eddfb1..2849a8d34 100644 --- a/src/NzbDrone.Api/Restrictions/RestrictionResource.cs +++ b/src/NzbDrone.Api/Restrictions/RestrictionResource.cs @@ -1,14 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; +using NzbDrone.Core.Profiles.Releases; using Sonarr.Http.REST; -using NzbDrone.Core.Restrictions; namespace NzbDrone.Api.Restrictions { public class RestrictionResource : RestResource { public string Required { get; set; } - public string Preferred { get; set; } public string Ignored { get; set; } public HashSet Tags { get; set; } @@ -20,7 +19,7 @@ namespace NzbDrone.Api.Restrictions public static class RestrictionResourceMapper { - public static RestrictionResource ToResource(this Restriction model) + public static RestrictionResource ToResource(this ReleaseProfile model) { if (model == null) return null; @@ -29,28 +28,26 @@ namespace NzbDrone.Api.Restrictions Id = model.Id, Required = model.Required, - Preferred = model.Preferred, Ignored = model.Ignored, Tags = new HashSet(model.Tags) }; } - public static Restriction ToModel(this RestrictionResource resource) + public static ReleaseProfile ToModel(this RestrictionResource resource) { if (resource == null) return null; - return new Restriction + return new ReleaseProfile { Id = resource.Id, Required = resource.Required, - Preferred = resource.Preferred, Ignored = resource.Ignored, Tags = new HashSet(resource.Tags) }; } - public static List ToResource(this IEnumerable models) + public static List ToResource(this IEnumerable models) { return models.Select(ToResource).ToList(); } diff --git a/src/NzbDrone.Api/Wanted/CutoffModule.cs b/src/NzbDrone.Api/Wanted/CutoffModule.cs index 901711076..689e6cfc4 100644 --- a/src/NzbDrone.Api/Wanted/CutoffModule.cs +++ b/src/NzbDrone.Api/Wanted/CutoffModule.cs @@ -2,6 +2,7 @@ using System.Linq; using NzbDrone.Api.Episodes; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Tv; using NzbDrone.SignalR; using Sonarr.Http; diff --git a/src/NzbDrone.Api/Wanted/MissingModule.cs b/src/NzbDrone.Api/Wanted/MissingModule.cs index afd5c2730..8d51aacc6 100644 --- a/src/NzbDrone.Api/Wanted/MissingModule.cs +++ b/src/NzbDrone.Api/Wanted/MissingModule.cs @@ -2,6 +2,7 @@ using System.Linq; using NzbDrone.Api.Episodes; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Tv; using NzbDrone.SignalR; using Sonarr.Http; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs index fa37b8b66..feffaf159 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/CutoffSpecificationFixture.cs @@ -1,8 +1,8 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; -using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Languages; using NzbDrone.Core.Profiles.Languages; @@ -13,6 +13,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [TestFixture] public class CutoffSpecificationFixture : CoreTest { + private static readonly int NoPreferredWordScore = 0; + [Test] public void should_return_true_if_current_episode_is_less_than_cutoff() { @@ -27,7 +29,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Languages = LanguageFixture.GetDefaultLanguages(Language.English), Cutoff = Language.English }, - new QualityModel(Quality.DVD, new Revision(version: 2)), Language.English).Should().BeTrue(); + new QualityModel(Quality.DVD, new Revision(version: 2)), + Language.English, + NoPreferredWordScore).Should().BeTrue(); } [Test] @@ -44,7 +48,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Languages = LanguageFixture.GetDefaultLanguages(Language.English), Cutoff = Language.English }, - new QualityModel(Quality.HDTV720p, new Revision(version: 2)), Language.English).Should().BeFalse(); + new QualityModel(Quality.HDTV720p, new Revision(version: 2)), + Language.English, + NoPreferredWordScore).Should().BeFalse(); } [Test] @@ -61,7 +67,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Languages = LanguageFixture.GetDefaultLanguages(Language.English), Cutoff = Language.English }, - new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), Language.English).Should().BeFalse(); + new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), + Language.English, + NoPreferredWordScore).Should().BeFalse(); } [Test] @@ -80,7 +88,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }, new QualityModel(Quality.HDTV720p, new Revision(version: 1)), Language.English, - new QualityModel(Quality.HDTV720p, new Revision(version: 2))).Should().BeTrue(); + NoPreferredWordScore, + new QualityModel(Quality.HDTV720p, new Revision(version: 2)), + NoPreferredWordScore).Should().BeTrue(); } [Test] @@ -99,13 +109,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }, new QualityModel(Quality.HDTV720p, new Revision(version: 2)), Language.English, - new QualityModel(Quality.Bluray1080p, new Revision(version: 2))).Should().BeFalse(); + NoPreferredWordScore, + new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), + NoPreferredWordScore).Should().BeFalse(); } [Test] public void should_return_true_if_quality_cutoff_is_met_and_quality_is_higher_but_language_is_not_met() { - Profile _profile = new Profile { Cutoff = Quality.HDTV720p.Id, @@ -122,13 +133,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _langProfile, new QualityModel(Quality.HDTV720p, new Revision(version: 2)), Language.English, - new QualityModel(Quality.Bluray1080p, new Revision(version: 2))).Should().BeTrue(); + NoPreferredWordScore, + new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), + NoPreferredWordScore).Should().BeTrue(); } [Test] public void should_return_false_if_cutoff_is_met_and_quality_is_higher_and_language_is_met() { - Profile _profile = new Profile { Cutoff = Quality.HDTV720p.Id, @@ -146,13 +158,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _langProfile, new QualityModel(Quality.HDTV720p, new Revision(version: 2)), Language.Spanish, - new QualityModel(Quality.Bluray1080p, new Revision(version: 2))).Should().BeFalse(); + NoPreferredWordScore, + new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), + NoPreferredWordScore).Should().BeFalse(); } [Test] public void should_return_false_if_cutoff_is_met_and_quality_is_higher_and_language_is_higher() { - Profile _profile = new Profile { Cutoff = Quality.HDTV720p.Id, @@ -170,13 +183,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _langProfile, new QualityModel(Quality.HDTV720p, new Revision(version: 2)), Language.French, - new QualityModel(Quality.Bluray1080p, new Revision(version: 2))).Should().BeFalse(); + NoPreferredWordScore, + new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), + NoPreferredWordScore).Should().BeFalse(); } [Test] public void should_return_true_if_cutoff_is_not_met_and_new_quality_is_higher_and_language_is_higher() { - Profile _profile = new Profile { Cutoff = Quality.HDTV720p.Id, @@ -194,13 +208,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _langProfile, new QualityModel(Quality.SDTV, new Revision(version: 2)), Language.French, - new QualityModel(Quality.Bluray1080p, new Revision(version: 2))).Should().BeTrue(); + NoPreferredWordScore, + new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), + NoPreferredWordScore).Should().BeTrue(); } [Test] public void should_return_true_if_cutoff_is_not_met_and_language_is_higher() { - Profile _profile = new Profile { Cutoff = Quality.HDTV720p.Id, @@ -217,7 +232,33 @@ namespace NzbDrone.Core.Test.DecisionEngineTests _profile, _langProfile, new QualityModel(Quality.SDTV, new Revision(version: 2)), - Language.French).Should().BeTrue(); + Language.French, + NoPreferredWordScore).Should().BeTrue(); + } + + [Test] + public void should_return_true_if_cutoffs_are_met_and_score_is_higher() + { + Profile _profile = new Profile + { + Cutoff = Quality.HDTV720p.Id, + Items = Qualities.QualityFixture.GetDefaultQualities(), + }; + + LanguageProfile _langProfile = new LanguageProfile + { + Cutoff = Language.Spanish, + Languages = LanguageFixture.GetDefaultLanguages() + }; + + Subject.CutoffNotMet( + _profile, + _langProfile, + new QualityModel(Quality.HDTV720p, new Revision(version: 2)), + Language.Spanish, + NoPreferredWordScore, + new QualityModel(Quality.Bluray1080p, new Revision(version: 2)), + 10).Should().BeTrue(); } } } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs index c9225540f..31844debe 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/DownloadDecisionMakerFixture.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FluentAssertions; using Moq; @@ -11,6 +11,7 @@ using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; using FizzWare.NBuilder; +using NzbDrone.Core.DecisionEngine.Specifications; namespace NzbDrone.Core.Test.DecisionEngineTests { diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs index 3ffae6ab9..86cfd9d56 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/QueueSpecificationFixture.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; -using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Parser.Model; @@ -26,6 +25,8 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private Series _otherSeries; private Episode _otherEpisode; + private ReleaseInfo _releaseInfo; + [SetUp] public void Setup() { @@ -58,10 +59,14 @@ namespace NzbDrone.Core.Test.DecisionEngineTests .With(e => e.EpisodeNumber = 2) .Build(); + _releaseInfo = Builder.CreateNew() + .Build(); + _remoteEpisode = Builder.CreateNew() .With(r => r.Series = _series) .With(r => r.Episodes = new List { _episode }) .With(r => r.ParsedEpisodeInfo = new ParsedEpisodeInfo { Quality = new QualityModel(Quality.DVD) , Language = Language.Spanish}) + .With(r => r.PreferredWordScore = 0) .Build(); } @@ -95,9 +100,10 @@ namespace NzbDrone.Core.Test.DecisionEngineTests public void should_return_true_when_series_doesnt_match() { var remoteEpisode = Builder.CreateNew() - .With(r => r.Series = _otherSeries) - .With(r => r.Episodes = new List { _episode }) - .Build(); + .With(r => r.Series = _otherSeries) + .With(r => r.Episodes = new List { _episode }) + .With(r => r.Release = _releaseInfo) + .Build(); GivenQueue(new List { remoteEpisode }); Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); @@ -117,6 +123,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Quality = new QualityModel(Quality.SDTV), Language = Language.Spanish }) + .With(r => r.Release = _releaseInfo) .Build(); GivenQueue(new List { remoteEpisode }); @@ -137,6 +144,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Quality = new QualityModel(Quality.SDTV), Language = Language.English }) + .With(r => r.Release = _releaseInfo) .Build(); GivenQueue(new List { remoteEpisode }); @@ -153,12 +161,33 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { Quality = new QualityModel(Quality.DVD) }) + .With(r => r.Release = _releaseInfo) .Build(); GivenQueue(new List { remoteEpisode }); Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); } + [Test] + public void should_return_true_when_qualities_are_the_same_and_languages_are_the_same_with_higher_preferred_word_score() + { + _remoteEpisode.PreferredWordScore = 1; + + var remoteEpisode = Builder.CreateNew() + .With(r => r.Series = _series) + .With(r => r.Episodes = new List { _episode }) + .With(r => r.ParsedEpisodeInfo = new ParsedEpisodeInfo + { + Quality = new QualityModel(Quality.DVD), + Language = Language.Spanish, + }) + .With(r => r.Release = _releaseInfo) + .Build(); + + GivenQueue(new List { remoteEpisode }); + Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); + } + [Test] public void should_return_false_when_qualities_are_the_same_and_languages_are_the_same() { @@ -170,6 +199,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Quality = new QualityModel(Quality.DVD), Language = Language.Spanish, }) + .With(r => r.Release = _releaseInfo) .Build(); GivenQueue(new List { remoteEpisode }); @@ -187,6 +217,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Quality = new QualityModel(Quality.DVD), Language = Language.English, }) + .With(r => r.Release = _releaseInfo) .Build(); GivenQueue(new List { remoteEpisode }); @@ -206,6 +237,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Quality = new QualityModel(Quality.HDTV720p), Language = Language.English }) + .With(r => r.Release = _releaseInfo) .Build(); GivenQueue(new List { remoteEpisode }); @@ -223,6 +255,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Quality = new QualityModel(Quality.HDTV720p), Language = Language.English }) + .With(r => r.Release = _releaseInfo) .Build(); GivenQueue(new List { remoteEpisode }); @@ -240,6 +273,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Quality = new QualityModel(Quality.HDTV720p), Language = Language.English }) + .With(r => r.Release = _releaseInfo) .Build(); _remoteEpisode.Episodes.Add(_otherEpisode); @@ -259,6 +293,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Quality = new QualityModel(Quality.HDTV720p), Language = Language.English }) + .With(r => r.Release = _releaseInfo) .Build(); _remoteEpisode.Episodes.Add(_otherEpisode); @@ -280,6 +315,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Quality.HDTV720p), Language = Language.English }) + .With(r => r.Release = _releaseInfo) .TheFirst(1) .With(r => r.Episodes = new List { _episode }) .TheNext(1) @@ -304,6 +340,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Quality = new QualityModel(Quality.HDTV720p), Language = Language.Spanish }) + .With(r => r.Release = _releaseInfo) .Build(); GivenQueue(new List { remoteEpisode }); @@ -311,4 +348,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs index 255920d39..a79a82d44 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/ReleaseRestrictionsSpecificationFixture.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using FluentAssertions; using Moq; using NUnit.Framework; using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Profiles.Releases; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tv; @@ -35,11 +35,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests private void GivenRestictions(string required, string ignored) { - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.AllForTags(It.IsAny>())) - .Returns(new List + .Returns(new List { - new Restriction + new ReleaseProfile() { Required = required, Ignored = ignored @@ -50,9 +50,9 @@ namespace NzbDrone.Core.Test.DecisionEngineTests [Test] public void should_be_true_when_restrictions_are_empty() { - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.AllForTags(It.IsAny>())) - .Returns(new List()); + .Returns(new List()); Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeTrue(); } @@ -116,11 +116,11 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { _remoteEpisode.Release.Title = "[ www.Speed.cd ] -Whose.Line.is.it.Anyway.US.S10E24.720p.HDTV.x264-BAJSKORV"; - Mocker.GetMock() + Mocker.GetMock() .Setup(s => s.AllForTags(It.IsAny>())) - .Returns(new List + .Returns(new List { - new Restriction { Required = "x264", Ignored = "www.Speed.cd" } + new ReleaseProfile { Required = "x264", Ignored = "www.Speed.cd" } }); Subject.IsSatisfiedBy(_remoteEpisode, null).Accepted.Should().BeFalse(); diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs index 4e212bd97..0a60e0ebf 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/DelaySpecificationFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; @@ -6,7 +6,7 @@ using FluentAssertions; using Marr.Data; using Moq; using NUnit.Framework; -using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.DecisionEngine.Specifications.RssSync; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.Indexers; @@ -86,14 +86,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync _remoteEpisode.Episodes.First().EpisodeFile = new LazyLoaded(new EpisodeFile { Quality = quality, - Language = language + Language = language, + SceneName = "Series.Title.S01E01.720p.HDTV.x264-Sonarr" }); } private void GivenUpgradeForExistingFile() { Mocker.GetMock() - .Setup(s => s.IsUpgradable(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Setup(s => s.IsUpgradable(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(true); } diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs index 2fc8d68c4..59b65f6f0 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/HistorySpecificationFixture.cs @@ -13,6 +13,7 @@ using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Languages; diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs index 351d27663..5479f73d1 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/RssSync/ProperSpecificationFixture.cs @@ -12,7 +12,7 @@ using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; using NzbDrone.Core.DecisionEngine; - +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Test.Framework; namespace NzbDrone.Core.Test.DecisionEngineTests.RssSync diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/SameEpisodesSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/SameEpisodesSpecificationFixture.cs index 183b6cc77..65c14997a 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/SameEpisodesSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/SameEpisodesSpecificationFixture.cs @@ -1,10 +1,10 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; using FluentAssertions; using Moq; using NUnit.Framework; -using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Tv; using NzbDrone.Core.Test.Framework; @@ -73,4 +73,4 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Subject.IsSatisfiedBy(episodes).Should().BeTrue(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs similarity index 73% rename from src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs rename to src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs index 9e8220378..8a23f24bf 100644 --- a/src/NzbDrone.Core.Test/DecisionEngineTests/QualityUpgradeSpecificationFixture.cs +++ b/src/NzbDrone.Core.Test/DecisionEngineTests/UpgradeSpecificationFixture.cs @@ -1,9 +1,9 @@ -using FluentAssertions; +using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Configuration; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; -using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Languages; using NzbDrone.Core.Profiles.Languages; @@ -13,7 +13,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests { [TestFixture] - public class QualityUpgradeSpecificationFixture : CoreTest + public class UpgradeSpecificationFixture : CoreTest { public static object[] IsUpgradeTestCases = { @@ -36,11 +36,7 @@ namespace NzbDrone.Core.Test.DecisionEngineTests new object[] { Quality.WEBDL720p, 1, Language.Spanish, Quality.HDTV720p, 2, Language.French, Quality.WEBDL720p, Language.Spanish, false } }; - [SetUp] - public void Setup() - { - - } + private static readonly int NoPreferredWordScore = 0; private void GivenAutoDownloadPropers(bool autoDownloadPropers) { @@ -66,7 +62,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Cutoff = Language.English }; - Subject.IsUpgradable(profile, langProfile, new QualityModel(current, new Revision(version: currentVersion)), Language.English, new QualityModel(newQuality, new Revision(version: newVersion)), Language.English) + Subject.IsUpgradable( + profile, + langProfile, + new QualityModel(current, new Revision(version: currentVersion)), + Language.English, + NoPreferredWordScore, + new QualityModel(newQuality, new Revision(version: newVersion)), + Language.English, + NoPreferredWordScore) .Should().Be(expected); } @@ -87,7 +91,15 @@ namespace NzbDrone.Core.Test.DecisionEngineTests Cutoff = languageCutoff }; - Subject.IsUpgradable(profile, langProfile, new QualityModel(current, new Revision(version: currentVersion)), currentLanguage, new QualityModel(newQuality, new Revision(version: newVersion)), newLanguage) + Subject.IsUpgradable( + profile, + langProfile, + new QualityModel(current, new Revision(version: currentVersion)), + currentLanguage, + NoPreferredWordScore, + new QualityModel(newQuality, new Revision(version: newVersion)), + newLanguage, + NoPreferredWordScore) .Should().Be(expected); } @@ -108,8 +120,16 @@ namespace NzbDrone.Core.Test.DecisionEngineTests }; - Subject.IsUpgradable(profile, langProfile, new QualityModel(Quality.DVD, new Revision(version: 2)), Language.English, new QualityModel(Quality.DVD, new Revision(version: 1)), Language.English) + Subject.IsUpgradable( + profile, + langProfile, + new QualityModel(Quality.DVD, new Revision(version: 2)), + Language.English, + NoPreferredWordScore, + new QualityModel(Quality.DVD, new Revision(version: 1)), + Language.English, + NoPreferredWordScore) .Should().BeFalse(); } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs index fa7401ef5..70f3582e6 100644 --- a/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs +++ b/src/NzbDrone.Core.Test/Housekeeping/Housekeepers/CleanupUnusedTagsFixture.cs @@ -1,10 +1,10 @@ -using FizzWare.NBuilder; +using FizzWare.NBuilder; using FluentAssertions; using NUnit.Framework; using NzbDrone.Core.Housekeeping.Housekeepers; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.Tags; -using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Profiles.Releases; namespace NzbDrone.Core.Test.Housekeeping.Housekeepers { @@ -27,7 +27,7 @@ namespace NzbDrone.Core.Test.Housekeeping.Housekeepers var tags = Builder.CreateListOfSize(2).BuildList(); Db.InsertMany(tags); - var restrictions = Builder.CreateListOfSize(2) + var restrictions = Builder.CreateListOfSize(2) .All() .With(v => v.Tags.Add(tags[0].Id)) .BuildList(); diff --git a/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs b/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs index 595a19dd4..727f9ae4d 100644 --- a/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs +++ b/src/NzbDrone.Core.Test/MediaFiles/EpisodeFileMovingServiceTests/MoveEpisodeFileFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using FizzWare.NBuilder; @@ -42,7 +42,7 @@ namespace NzbDrone.Core.Test.MediaFiles.EpisodeFileMovingServiceTests .Build(); Mocker.GetMock() - .Setup(s => s.BuildFileName(It.IsAny>(), It.IsAny(), It.IsAny(), null)) + .Setup(s => s.BuildFileName(It.IsAny>(), It.IsAny(), It.IsAny(), null, null)) .Returns("File Name"); Mocker.GetMock() diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 7deabbaa6..f0586f599 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -172,7 +172,7 @@ - + @@ -346,6 +346,7 @@ + diff --git a/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs new file mode 100644 index 000000000..48584950d --- /dev/null +++ b/src/NzbDrone.Core.Test/Profiles/Releases/PreferredWordService/CalculateFixture.cs @@ -0,0 +1,95 @@ +using System.Collections.Generic; +using System.Linq; +using FizzWare.NBuilder; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Core.Profiles.Releases; +using NzbDrone.Core.Test.Framework; +using NzbDrone.Core.Tv; + +namespace NzbDrone.Core.Test.Profiles.Releases.PreferredWordService +{ + [TestFixture] + public class CalculateFixture : CoreTest + { + private Series _series = null; + private List _releaseProfiles = null; + private string _title = "Series.Title.S01E01.720p.HDTV.x264-Sonarr"; + + [SetUp] + public void Setup() + { + _series = Builder.CreateNew() + .With(s => s.Tags = new HashSet(new[] {1, 2})) + .Build(); + + _releaseProfiles = new List(); + + _releaseProfiles.Add(new ReleaseProfile + { + Preferred = new List> + { + new KeyValuePair("x264", 5), + new KeyValuePair("x265", -10) + } + }); + + Mocker.GetMock() + .Setup(s => s.AllForTags(It.IsAny>())) + .Returns(_releaseProfiles); + } + + + private void GivenMatchingTerms(params string[] terms) + { + Mocker.GetMock() + .Setup(s => s.IsMatch(It.IsAny(), _title)) + .Returns((term, title) => terms.Contains(term)); + } + + [Test] + public void should_return_0_when_there_are_no_release_profiles() + { + Mocker.GetMock() + .Setup(s => s.AllForTags(It.IsAny>())) + .Returns(new List()); + + Subject.Calculate(_series, _title).Should().Be(0); + } + + [Test] + public void should_return_0_when_there_are_no_matching_preferred_words() + { + GivenMatchingTerms(); + + Subject.Calculate(_series, _title).Should().Be(0); + } + + [Test] + public void should_calculate_positive_score() + { + GivenMatchingTerms("x264"); + + Subject.Calculate(_series, _title).Should().Be(5); + } + + [Test] + public void should_calculate_negative_score() + { + GivenMatchingTerms("x265"); + + Subject.Calculate(_series, _title).Should().Be(-10); + } + + [Test] + public void should_calculate_using_multiple_profiles() + { + _releaseProfiles.Add(_releaseProfiles.First()); + + GivenMatchingTerms("x264"); + + Subject.Calculate(_series, _title).Should().Be(10); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/Migration/127_rename_release_profiles.cs b/src/NzbDrone.Core/Datastore/Migration/127_rename_release_profiles.cs new file mode 100644 index 000000000..34aa3a798 --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Migration/127_rename_release_profiles.cs @@ -0,0 +1,15 @@ +using FluentMigrator; +using NzbDrone.Core.Datastore.Migration.Framework; + +namespace NzbDrone.Core.Datastore.Migration +{ + [Migration(127)] + public class rename_restrictions_to_release_profiles : NzbDroneMigrationBase + { + protected override void MainDbUpgrade() + { + Rename.Table("Restrictions").To("ReleaseProfiles"); + Alter.Table("ReleaseProfiles").AddColumn("IncludePreferredWhenRenaming").AsBoolean().WithDefaultValue(true); + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 4c675f943..7b3abb56b 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -21,7 +21,6 @@ using NzbDrone.Core.Organizer; using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; -using NzbDrone.Core.Restrictions; using NzbDrone.Core.RootFolders; using NzbDrone.Core.SeriesStats; using NzbDrone.Core.Tags; @@ -37,6 +36,7 @@ using NzbDrone.Core.Extras.Subtitles; using NzbDrone.Core.Messaging.Commands; using NzbDrone.Core.Languages; using NzbDrone.Core.Profiles.Languages; +using NzbDrone.Core.Profiles.Releases; namespace NzbDrone.Core.Datastore { @@ -121,7 +121,7 @@ namespace NzbDrone.Core.Datastore Mapper.Entity().RegisterModel("RemotePathMappings"); Mapper.Entity().RegisterModel("Tags"); - Mapper.Entity().RegisterModel("Restrictions"); + Mapper.Entity().RegisterModel("ReleaseProfiles"); Mapper.Entity().RegisterModel("DelayProfiles"); Mapper.Entity().RegisterModel("Users"); @@ -149,6 +149,7 @@ namespace NzbDrone.Core.Datastore MapRepository.Instance.RegisterTypeConverter(typeof(QualityModel), new EmbeddedDocumentConverter(new QualityIntConverter())); MapRepository.Instance.RegisterTypeConverter(typeof(Dictionary), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(List>), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(Language), new LanguageIntConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(List), new EmbeddedDocumentConverter(new LanguageIntConverter())); diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs index b85ceb606..257a44038 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionComparer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.Indexers; @@ -25,6 +25,7 @@ namespace NzbDrone.Core.DecisionEngine { CompareQuality, CompareLanguage, + ComparePreferredWordScore, CompareProtocol, CompareEpisodeCount, CompareEpisodeNumber, @@ -68,6 +69,11 @@ namespace NzbDrone.Core.DecisionEngine return CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.Series.LanguageProfile.Value.Languages.FindIndex(l => l.Language == remoteEpisode.ParsedEpisodeInfo.Language)); } + private int ComparePreferredWordScore(DownloadDecision x, DownloadDecision y) + { + return CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => remoteEpisode.PreferredWordScore); + } + private int CompareProtocol(DownloadDecision x, DownloadDecision y) { var result = CompareBy(x.RemoteEpisode, y.RemoteEpisode, remoteEpisode => diff --git a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs index eb583499e..16fc82f89 100644 --- a/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs +++ b/src/NzbDrone.Core/DecisionEngine/DownloadDecisionMaker.cs @@ -5,6 +5,8 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation.Extensions; using NzbDrone.Common.Serializer; +using NzbDrone.Core.DecisionEngine.Specifications; +using NzbDrone.Core.Download.Aggregation; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser; using NzbDrone.Core.Parser.Model; @@ -21,12 +23,17 @@ namespace NzbDrone.Core.DecisionEngine { private readonly IEnumerable _specifications; private readonly IParsingService _parsingService; + private readonly IRemoteEpisodeAggregationService _aggregationService; private readonly Logger _logger; - public DownloadDecisionMaker(IEnumerable specifications, IParsingService parsingService, Logger logger) + public DownloadDecisionMaker(IEnumerable specifications, + IParsingService parsingService, + IRemoteEpisodeAggregationService aggregationService, + Logger logger) { _specifications = specifications; _parsingService = parsingService; + _aggregationService = aggregationService; _logger = logger; } @@ -89,6 +96,7 @@ namespace NzbDrone.Core.DecisionEngine } else { + _aggregationService.Augment(remoteEpisode); remoteEpisode.DownloadAllowed = remoteEpisode.Episodes.Any(); decision = GetDecisionForReport(remoteEpisode, searchCriteria); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs index e030d4d2d..dd8af73ff 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/CutoffSpecification.cs @@ -1,18 +1,21 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles.Releases; namespace NzbDrone.Core.DecisionEngine.Specifications { public class CutoffSpecification : IDecisionEngineSpecification { private readonly UpgradableSpecification _upgradableSpecification; + private readonly IPreferredWordService _preferredWordServiceCalculator; private readonly Logger _logger; - public CutoffSpecification(UpgradableSpecification UpgradableSpecification, Logger logger) + public CutoffSpecification(UpgradableSpecification upgradableSpecification, IPreferredWordService preferredWordServiceCalculator, Logger logger) { - _upgradableSpecification = UpgradableSpecification; + _upgradableSpecification = upgradableSpecification; + _preferredWordServiceCalculator = preferredWordServiceCalculator; _logger = logger; } @@ -30,13 +33,16 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("File is no longer available, skipping this file."); continue; } + _logger.Debug("Comparing file quality and language with report. Existing file is {0} - {1}", file.Quality, file.Language); if (!_upgradableSpecification.CutoffNotMet(profile, subject.Series.LanguageProfile, file.Quality, - file.Language, - subject.ParsedEpisodeInfo.Quality)) + file.Language, + _preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName()), + subject.ParsedEpisodeInfo.Quality, + subject.PreferredWordScore)) { _logger.Debug("Cutoff already met, rejecting."); diff --git a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/IDecisionEngineSpecification.cs similarity index 85% rename from src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs rename to src/NzbDrone.Core/DecisionEngine/Specifications/IDecisionEngineSpecification.cs index 08ea4c012..2a1312191 100644 --- a/src/NzbDrone.Core/DecisionEngine/IDecisionEngineSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/IDecisionEngineSpecification.cs @@ -1,7 +1,7 @@ using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; -namespace NzbDrone.Core.DecisionEngine +namespace NzbDrone.Core.DecisionEngine.Specifications { public interface IDecisionEngineSpecification { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs index e024683e4..374056111 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/QueueSpecification.cs @@ -2,6 +2,7 @@ using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles.Releases; using NzbDrone.Core.Queue; namespace NzbDrone.Core.DecisionEngine.Specifications @@ -10,14 +11,17 @@ namespace NzbDrone.Core.DecisionEngine.Specifications { private readonly IQueueService _queueService; private readonly UpgradableSpecification _upgradableSpecification; + private readonly IPreferredWordService _preferredWordServiceCalculator; private readonly Logger _logger; public QueueSpecification(IQueueService queueService, - UpgradableSpecification UpgradableSpecification, - Logger logger) + UpgradableSpecification UpgradableSpecification, + IPreferredWordService preferredWordServiceCalculator, + Logger logger) { _queueService = queueService; _upgradableSpecification = UpgradableSpecification; + _preferredWordServiceCalculator = preferredWordServiceCalculator; _logger = logger; } @@ -26,21 +30,24 @@ namespace NzbDrone.Core.DecisionEngine.Specifications public Decision IsSatisfiedBy(RemoteEpisode subject, SearchCriteriaBase searchCriteria) { - var queue = _queueService.GetQueue() - .Select(q => q.RemoteEpisode).ToList(); - + var queue = _queueService.GetQueue(); var matchingSeries = queue.Where(q => q.Series.Id == subject.Series.Id); - var matchingEpisode = matchingSeries.Where(q => q.Episodes.Select(e => e.Id).Intersect(subject.Episodes.Select(e => e.Id)).Any()); + var matchingEpisode = matchingSeries.Where(q => q.RemoteEpisode.Episodes.Select(e => e.Id).Intersect(subject.Episodes.Select(e => e.Id)).Any()); - foreach (var remoteEpisode in matchingEpisode) + foreach (var queueItem in matchingEpisode) { + var remoteEpisode = queueItem.RemoteEpisode; + _logger.Debug("Checking if existing release in queue meets cutoff. Queued quality is: {0} - {1}", remoteEpisode.ParsedEpisodeInfo.Quality, remoteEpisode.ParsedEpisodeInfo.Language); + var queuedItemPreferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, queueItem.Title); if (!_upgradableSpecification.CutoffNotMet(subject.Series.Profile, subject.Series.LanguageProfile, remoteEpisode.ParsedEpisodeInfo.Quality, - remoteEpisode.ParsedEpisodeInfo.Language, - subject.ParsedEpisodeInfo.Quality)) + remoteEpisode.ParsedEpisodeInfo.Language, + queuedItemPreferredWordScore, + subject.ParsedEpisodeInfo.Quality, + subject.PreferredWordScore)) { return Decision.Reject("Quality for release in queue already meets cutoff: {0} - {1}", remoteEpisode.ParsedEpisodeInfo.Quality, remoteEpisode.ParsedEpisodeInfo.Language); } @@ -50,9 +57,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (!_upgradableSpecification.IsUpgradable(subject.Series.Profile, subject.Series.LanguageProfile, remoteEpisode.ParsedEpisodeInfo.Quality, - remoteEpisode.ParsedEpisodeInfo.Language, - subject.ParsedEpisodeInfo.Quality, - subject.ParsedEpisodeInfo.Language)) + remoteEpisode.ParsedEpisodeInfo.Language, + queuedItemPreferredWordScore, + subject.ParsedEpisodeInfo.Quality, + subject.ParsedEpisodeInfo.Language, + subject.PreferredWordScore)) { return Decision.Reject("Quality for release in queue is of equal or higher preference: {0} - {1}", remoteEpisode.ParsedEpisodeInfo.Quality, remoteEpisode.ParsedEpisodeInfo.Language); } diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs index 4db41230a..7fe907c64 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/ReleaseRestrictionsSpecification.cs @@ -5,20 +5,20 @@ using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; -using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Profiles.Releases; namespace NzbDrone.Core.DecisionEngine.Specifications { public class ReleaseRestrictionsSpecification : IDecisionEngineSpecification { private readonly Logger _logger; - private readonly IRestrictionService _restrictionService; + private readonly IReleaseProfileService _releaseProfileService; private readonly ITermMatcher _termMatcher; - public ReleaseRestrictionsSpecification(ITermMatcher termMatcher, IRestrictionService restrictionService, Logger logger) + public ReleaseRestrictionsSpecification(ITermMatcher termMatcher, IReleaseProfileService releaseProfileService, Logger logger) { _logger = logger; - _restrictionService = restrictionService; + _releaseProfileService = releaseProfileService; _termMatcher = termMatcher; } @@ -30,7 +30,7 @@ namespace NzbDrone.Core.DecisionEngine.Specifications _logger.Debug("Checking if release meets restrictions: {0}", subject); var title = subject.Release.Title; - var restrictions = _restrictionService.AllForTags(subject.Series.Tags); + var restrictions = _releaseProfileService.AllForTags(subject.Series.Tags); var required = restrictions.Where(r => r.Required.IsNotNullOrWhiteSpace()); var ignored = restrictions.Where(r => r.Ignored.IsNotNullOrWhiteSpace()); diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs index 1540cb6f9..5f487d217 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/DelaySpecification.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Core.Download.Pending; using NzbDrone.Core.IndexerSearch.Definitions; @@ -6,6 +6,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Core.Profiles.Delay; using NzbDrone.Core.Qualities; using NzbDrone.Core.Languages; +using NzbDrone.Core.Profiles.Releases; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { @@ -14,16 +15,19 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync private readonly IPendingReleaseService _pendingReleaseService; private readonly IUpgradableSpecification _upgradableSpecification; private readonly IDelayProfileService _delayProfileService; + private readonly IPreferredWordService _preferredWordServiceCalculator; private readonly Logger _logger; public DelaySpecification(IPendingReleaseService pendingReleaseService, - IUpgradableSpecification UpgradableSpecification, + IUpgradableSpecification upgradableSpecification, IDelayProfileService delayProfileService, + IPreferredWordService preferredWordServiceCalculator, Logger logger) { _pendingReleaseService = pendingReleaseService; - _upgradableSpecification = UpgradableSpecification; + _upgradableSpecification = upgradableSpecification; _delayProfileService = delayProfileService; + _preferredWordServiceCalculator = preferredWordServiceCalculator; _logger = logger; } @@ -50,19 +54,22 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync return Decision.Accept(); } - var comparer = new QualityModelComparer(profile); - var comparerLanguage = new LanguageComparer(languageProfile); + var qualityComparer = new QualityModelComparer(profile); + var languageComparer = new LanguageComparer(languageProfile); if (isPreferredProtocol) { foreach (var file in subject.Episodes.Where(c => c.EpisodeFileId != 0).Select(c => c.EpisodeFile.Value)) { - var upgradable = _upgradableSpecification.IsUpgradable(profile, - languageProfile, - file.Quality, - file.Language, - subject.ParsedEpisodeInfo.Quality, - subject.ParsedEpisodeInfo.Language); + var upgradable = _upgradableSpecification.IsUpgradable( + profile, + languageProfile, + file.Quality, + file.Language, + _preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName()), + subject.ParsedEpisodeInfo.Quality, + subject.ParsedEpisodeInfo.Language, + subject.PreferredWordScore); if (upgradable) { @@ -74,8 +81,8 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync // If quality meets or exceeds the best allowed quality in the profile accept it immediately var bestQualityInProfile = profile.LastAllowedQuality(); - var isBestInProfile = comparer.Compare(subject.ParsedEpisodeInfo.Quality.Quality, bestQualityInProfile) >= 0; - var isBestInProfileLanguage = comparerLanguage.Compare(subject.ParsedEpisodeInfo.Language, languageProfile.LastAllowedLanguage()) >= 0; + var isBestInProfile = qualityComparer.Compare(subject.ParsedEpisodeInfo.Quality.Quality, bestQualityInProfile) >= 0; + var isBestInProfileLanguage = languageComparer.Compare(subject.ParsedEpisodeInfo.Language, languageProfile.LastAllowedLanguage()) >= 0; if (isBestInProfile && isBestInProfileLanguage && isPreferredProtocol) { diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs index c05135bf4..f77169941 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/RssSync/HistorySpecification.cs @@ -5,6 +5,7 @@ using NzbDrone.Core.Configuration; using NzbDrone.Core.History; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles.Releases; namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync { @@ -13,16 +14,19 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync private readonly IHistoryService _historyService; private readonly UpgradableSpecification _upgradableSpecification; private readonly IConfigService _configService; + private readonly IPreferredWordService _preferredWordServiceCalculator; private readonly Logger _logger; public HistorySpecification(IHistoryService historyService, UpgradableSpecification upgradableSpecification, IConfigService configService, + IPreferredWordService preferredWordServiceCalculator, Logger logger) { _historyService = historyService; _upgradableSpecification = upgradableSpecification; _configService = configService; + _preferredWordServiceCalculator = preferredWordServiceCalculator; _logger = logger; } @@ -48,8 +52,29 @@ namespace NzbDrone.Core.DecisionEngine.Specifications.RssSync if (mostRecent != null && mostRecent.EventType == HistoryEventType.Grabbed) { var recent = mostRecent.Date.After(DateTime.UtcNow.AddHours(-12)); - var cutoffUnmet = _upgradableSpecification.CutoffNotMet(subject.Series.Profile, subject.Series.LanguageProfile, mostRecent.Quality, mostRecent.Language, subject.ParsedEpisodeInfo.Quality); - var upgradeable = _upgradableSpecification.IsUpgradable(subject.Series.Profile, subject.Series.LanguageProfile, mostRecent.Quality, mostRecent.Language, subject.ParsedEpisodeInfo.Quality, subject.ParsedEpisodeInfo.Language); + + // The series will be the same as the one in history since it's the same episode. + // Instead of fetching the series from the DB reuse the known series. + var preferredWordScore = _preferredWordServiceCalculator.Calculate(subject.Series, mostRecent.SourceTitle); + + var cutoffUnmet = _upgradableSpecification.CutoffNotMet( + subject.Series.Profile, + subject.Series.LanguageProfile, + mostRecent.Quality, + mostRecent.Language, + preferredWordScore, + subject.ParsedEpisodeInfo.Quality, + subject.PreferredWordScore); + + var upgradeable = _upgradableSpecification.IsUpgradable( + subject.Series.Profile, + subject.Series.LanguageProfile, + mostRecent.Quality, + mostRecent.Language, + preferredWordScore, + subject.ParsedEpisodeInfo.Quality, + subject.ParsedEpisodeInfo.Language, + subject.PreferredWordScore); if (!recent && cdhEnabled) { diff --git a/src/NzbDrone.Core/DecisionEngine/SameEpisodesSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesSpecification.cs similarity index 94% rename from src/NzbDrone.Core/DecisionEngine/SameEpisodesSpecification.cs rename to src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesSpecification.cs index 65bf4f1ec..337e2fb6a 100644 --- a/src/NzbDrone.Core/DecisionEngine/SameEpisodesSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/SameEpisodesSpecification.cs @@ -3,7 +3,7 @@ using System.Linq; using NzbDrone.Common.Extensions; using NzbDrone.Core.Tv; -namespace NzbDrone.Core.DecisionEngine +namespace NzbDrone.Core.DecisionEngine.Specifications { public class SameEpisodesSpecification { diff --git a/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs similarity index 71% rename from src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs rename to src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs index 5a1fb1dc2..cbb5486db 100644 --- a/src/NzbDrone.Core/DecisionEngine/UpgradableSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradableSpecification.cs @@ -4,14 +4,14 @@ using NzbDrone.Core.Profiles.Languages; using NzbDrone.Core.Profiles.Qualities; using NzbDrone.Core.Qualities; -namespace NzbDrone.Core.DecisionEngine +namespace NzbDrone.Core.DecisionEngine.Specifications { public interface IUpgradableSpecification { - bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality, Language newLanguage); + bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, int currentScore, QualityModel newQuality, Language newLanguage, int newScore); bool QualityCutoffNotMet(Profile profile, QualityModel currentQuality, QualityModel newQuality = null); bool LanguageCutoffNotMet(LanguageProfile languageProfile, Language currentLanguage); - bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null); + bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, int currentScore, QualityModel newQuality = null, int newScore = 0); bool IsRevisionUpgrade(QualityModel currentQuality, QualityModel newQuality); } @@ -51,19 +51,38 @@ namespace NzbDrone.Core.DecisionEngine return true; } + private bool IsPreferredWordUpgradable(int currentScore, int newScore) + { + return newScore > currentScore; + } - public bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality, Language newLanguage) - { - // If qualities are the same then check language - if (newQuality != null && new QualityModelComparer(profile).Compare(newQuality, currentQuality) == 0) + public bool IsUpgradable(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, int currentScore, QualityModel newQuality, Language newLanguage, int newScore) + { + if (IsQualityUpgradable(profile, currentQuality, newQuality)) { - return IsLanguageUpgradable(languageProfile, currentLanguage, newLanguage); + return true; } - // If quality is worse then always return false - if (!IsQualityUpgradable(profile, currentQuality, newQuality)) + if (new QualityModelComparer(profile).Compare(newQuality, currentQuality) != 0) { - _logger.Debug("existing item has better quality. skipping"); + _logger.Debug("Existing item has better qualitys, skipping"); + return false; + } + + if (IsLanguageUpgradable(languageProfile, currentLanguage, newLanguage)) + { + return true; + } + + if (new LanguageComparer(languageProfile).Compare(newLanguage, currentLanguage) != 0) + { + _logger.Debug("Existing item has better language, skipping"); + return false; + } + + if (!IsPreferredWordUpgradable(currentScore, newScore)) + { + _logger.Debug("Existing item has a better preferred word score, skipping"); return false; } @@ -94,9 +113,10 @@ namespace NzbDrone.Core.DecisionEngine return languageCompare < 0; } - public bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, QualityModel newQuality = null) + public bool CutoffNotMet(Profile profile, LanguageProfile languageProfile, QualityModel currentQuality, Language currentLanguage, int currentScore, QualityModel newQuality = null, int newScore = 0) { - // If we can upgrade the language (it is not the cutoff) then doesn't matter the quality we can always get same quality with prefered language + // If we can upgrade the language (it is not the cutoff) then the quality doesn't + // matter as we can always get same quality with prefered language. if (LanguageCutoffNotMet(languageProfile, currentLanguage)) { return true; @@ -107,6 +127,11 @@ namespace NzbDrone.Core.DecisionEngine return true; } + if (IsPreferredWordUpgradable(currentScore, newScore)) + { + return true; + } + _logger.Debug("Existing item meets cut-off. skipping."); return false; diff --git a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs index 789074268..a3d4e794c 100644 --- a/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs +++ b/src/NzbDrone.Core/DecisionEngine/Specifications/UpgradeDiskSpecification.cs @@ -1,18 +1,21 @@ -using System.Linq; +using System.Linq; using NLog; using NzbDrone.Core.IndexerSearch.Definitions; using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles.Releases; namespace NzbDrone.Core.DecisionEngine.Specifications { public class UpgradeDiskSpecification : IDecisionEngineSpecification { private readonly UpgradableSpecification _upgradableSpecification; + private readonly IPreferredWordService _preferredWordServiceCalculator; private readonly Logger _logger; - public UpgradeDiskSpecification(UpgradableSpecification upgradableSpecification, Logger logger) + public UpgradeDiskSpecification(UpgradableSpecification upgradableSpecification, IPreferredWordService preferredWordServiceCalculator, Logger logger) { _upgradableSpecification = upgradableSpecification; + _preferredWordServiceCalculator = preferredWordServiceCalculator; _logger = logger; } @@ -35,9 +38,11 @@ namespace NzbDrone.Core.DecisionEngine.Specifications if (!_upgradableSpecification.IsUpgradable(subject.Series.Profile, subject.Series.LanguageProfile, file.Quality, - file.Language, + file.Language, + _preferredWordServiceCalculator.Calculate(subject.Series, file.GetSceneOrFileName()), subject.ParsedEpisodeInfo.Quality, - subject.ParsedEpisodeInfo.Language)) + subject.ParsedEpisodeInfo.Language, + subject.PreferredWordScore)) { return Decision.Reject("Quality for existing file on disk is of equal or higher preference: {0} - {1}", file.Quality, file.Language); } diff --git a/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs new file mode 100644 index 000000000..6227ca1e6 --- /dev/null +++ b/src/NzbDrone.Core/Download/Aggregation/Aggregators/AggregatePreferredWordScore.cs @@ -0,0 +1,22 @@ +using NzbDrone.Core.Parser.Model; +using NzbDrone.Core.Profiles.Releases; + +namespace NzbDrone.Core.Download.Aggregation.Aggregators +{ + public class AggregatePreferredWordScore : IAggregateRemoteEpisode + { + private readonly IPreferredWordService _preferredWordServiceCalculator; + + public AggregatePreferredWordScore(IPreferredWordService preferredWordServiceCalculator) + { + _preferredWordServiceCalculator = preferredWordServiceCalculator; + } + + public RemoteEpisode Aggregate(RemoteEpisode remoteEpisode) + { + remoteEpisode.PreferredWordScore = _preferredWordServiceCalculator.Calculate(remoteEpisode.Series, remoteEpisode.Release.Title); + + return remoteEpisode; + } + } +} diff --git a/src/NzbDrone.Core/Download/Aggregation/Aggregators/IAggregateRemoteEpisode.cs b/src/NzbDrone.Core/Download/Aggregation/Aggregators/IAggregateRemoteEpisode.cs new file mode 100644 index 000000000..52092c3d7 --- /dev/null +++ b/src/NzbDrone.Core/Download/Aggregation/Aggregators/IAggregateRemoteEpisode.cs @@ -0,0 +1,9 @@ +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download.Aggregation.Aggregators +{ + public interface IAggregateRemoteEpisode + { + RemoteEpisode Aggregate(RemoteEpisode remoteEpisode); + } +} diff --git a/src/NzbDrone.Core/Download/Aggregation/RemoteEpisodeAggregationService.cs b/src/NzbDrone.Core/Download/Aggregation/RemoteEpisodeAggregationService.cs new file mode 100644 index 000000000..770ac0ae5 --- /dev/null +++ b/src/NzbDrone.Core/Download/Aggregation/RemoteEpisodeAggregationService.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using NLog; +using NzbDrone.Core.Download.Aggregation.Aggregators; +using NzbDrone.Core.Parser.Model; + +namespace NzbDrone.Core.Download.Aggregation +{ + public interface IRemoteEpisodeAggregationService + { + RemoteEpisode Augment(RemoteEpisode remoteEpisode); + } + + public class RemoteEpisodeAggregationService : IRemoteEpisodeAggregationService + { + private readonly IEnumerable _augmenters; + private readonly Logger _logger; + + public RemoteEpisodeAggregationService(IEnumerable augmenters, + Logger logger) + { + _augmenters = augmenters; + _logger = logger; + } + + public RemoteEpisode Augment(RemoteEpisode remoteEpisode) + { + foreach (var augmenter in _augmenters) + { + try + { + augmenter.Aggregate(remoteEpisode); + } + catch (Exception ex) + { + _logger.Warn(ex, ex.Message); + } + } + + + return remoteEpisode; + } + } +} diff --git a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs index 63debb4b7..6ea8a1c5d 100644 --- a/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs +++ b/src/NzbDrone.Core/Housekeeping/Housekeepers/CleanupUnusedTags.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Marr.Data; using NzbDrone.Common.Serializer; @@ -19,7 +19,7 @@ namespace NzbDrone.Core.Housekeeping.Housekeepers { var mapper = _database.GetDataMapper(); - var usedTags = new[] { "Series", "Notifications", "DelayProfiles", "Restrictions" } + var usedTags = new[] { "Series", "Notifications", "DelayProfiles", "ReleaseProfiles" } .SelectMany(v => GetUsedTags(v, mapper)) .Distinct() .ToArray(); diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs index 4f3fd25f9..250bd7eec 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeFile.cs @@ -44,6 +44,11 @@ namespace NzbDrone.Core.MediaFiles return System.IO.Path.GetFileName(RelativePath); } + if (Path.IsNotNullOrWhiteSpace()) + { + return System.IO.Path.GetFileName(Path); + } + return string.Empty; } } diff --git a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs index b13b0f552..b645f500c 100644 --- a/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs +++ b/src/NzbDrone.Core/MediaFiles/EpisodeImport/Specifications/SameEpisodesImportSpecification.cs @@ -1,5 +1,6 @@ using NLog; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; using NzbDrone.Core.Parser.Model; diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 1e201e5ed..3010ae681 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -141,6 +141,7 @@ + @@ -340,12 +341,12 @@ - + - + - + @@ -1005,6 +1006,7 @@ + @@ -1134,11 +1136,11 @@ - - - - - + + + + + diff --git a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs index f96633d1e..0474d635b 100644 --- a/src/NzbDrone.Core/Organizer/FileNameBuilder.cs +++ b/src/NzbDrone.Core/Organizer/FileNameBuilder.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.MediaInfo; +using NzbDrone.Core.Profiles.Releases; using NzbDrone.Core.Qualities; using NzbDrone.Core.Tv; @@ -17,7 +18,7 @@ namespace NzbDrone.Core.Organizer { public interface IBuildFileNames { - string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null); + string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null, List preferredWords = null); string BuildFilePath(Series series, int seasonNumber, string fileName, string extension); string BuildSeasonPath(Series series, int seasonNumber); BasicNamingConfig GetBasicNamingConfig(NamingConfig nameSpec); @@ -30,6 +31,7 @@ namespace NzbDrone.Core.Organizer { private readonly INamingConfigService _namingConfigService; private readonly IQualityDefinitionService _qualityDefinitionService; + private readonly IPreferredWordService _preferredWordService; private readonly ICached _episodeFormatCache; private readonly ICached _absoluteEpisodeFormatCache; private readonly ICached _requiresEpisodeTitleCache; @@ -76,17 +78,19 @@ namespace NzbDrone.Core.Organizer public FileNameBuilder(INamingConfigService namingConfigService, IQualityDefinitionService qualityDefinitionService, ICacheManager cacheManager, + IPreferredWordService preferredWordService, Logger logger) { _namingConfigService = namingConfigService; _qualityDefinitionService = qualityDefinitionService; + _preferredWordService = preferredWordService; _episodeFormatCache = cacheManager.GetCache(GetType(), "episodeFormat"); _absoluteEpisodeFormatCache = cacheManager.GetCache(GetType(), "absoluteEpisodeFormat"); _requiresEpisodeTitleCache = cacheManager.GetCache(GetType(), "requiresEpisodeTitle"); _logger = logger; } - public string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null) + public string BuildFileName(List episodes, Series series, EpisodeFile episodeFile, NamingConfig namingConfig = null, List preferredWords = null) { if (namingConfig == null) { @@ -137,6 +141,7 @@ namespace NzbDrone.Core.Organizer AddEpisodeFileTokens(tokenHandlers, episodeFile); AddQualityTokens(tokenHandlers, series, episodeFile); AddMediaInfoTokens(tokenHandlers, episodeFile); + AddPreferredWords(tokenHandlers, series, episodeFile, preferredWords); var fileName = ReplaceTokens(pattern, tokenHandlers, namingConfig).Trim(); fileName = FileNameCleanupRegex.Replace(fileName, match => match.Captures[0].Value[0].ToString()); @@ -564,6 +569,16 @@ namespace NzbDrone.Core.Organizer tokenHandlers["{TvMazeId}"] = m => series.TvMazeId.ToString(); } + private void AddPreferredWords(Dictionary> tokenHandlers, Series series, EpisodeFile episodeFile, List preferredWords = null) + { + if (preferredWords == null) + { + preferredWords = _preferredWordService.GetMatchingPreferredWords(series, episodeFile.GetSceneOrFileName(), true); + } + + tokenHandlers["{Preferred Words}"] = m => string.Join(" ", preferredWords); + } + private string GetLanguagesToken(string mediaInfoLanguages) { List tokens = new List(); diff --git a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs index 950df08db..06014fc91 100644 --- a/src/NzbDrone.Core/Organizer/FileNameSampleService.cs +++ b/src/NzbDrone.Core/Organizer/FileNameSampleService.cs @@ -33,6 +33,7 @@ namespace NzbDrone.Core.Organizer private static EpisodeFile _dailyEpisodeFile; private static EpisodeFile _animeEpisodeFile; private static EpisodeFile _animeMultiEpisodeFile; + private static List _preferredWords; public FileNameSampleService(IBuildFileNames buildFileNames) { @@ -162,6 +163,11 @@ namespace NzbDrone.Core.Organizer ReleaseGroup = "RlsGrp", MediaInfo = mediaInfoAnime }; + + _preferredWords = new List + { + "iNTERNAL" + }; } public SampleResult GetStandardSample(NamingConfig nameSpec) @@ -243,7 +249,7 @@ namespace NzbDrone.Core.Organizer { try { - return _buildFileNames.BuildFileName(episodes, series, episodeFile, nameSpec); + return _buildFileNames.BuildFileName(episodes, series, episodeFile, nameSpec, _preferredWords); } catch (NamingFormatException) { diff --git a/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs b/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs index 3b99f250c..1952ebc0e 100644 --- a/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs +++ b/src/NzbDrone.Core/Parser/Model/RemoteEpisode.cs @@ -14,6 +14,7 @@ namespace NzbDrone.Core.Parser.Model public List Episodes { get; set; } public bool DownloadAllowed { get; set; } public TorrentSeedConfiguration SeedConfiguration { get; set; } + public int PreferredWordScore { get; set; } public bool IsRecentEpisode() { diff --git a/src/NzbDrone.Core/Restrictions/PerlRegexFactory.cs b/src/NzbDrone.Core/Profiles/Releases/PerlRegexFactory.cs similarity index 92% rename from src/NzbDrone.Core/Restrictions/PerlRegexFactory.cs rename to src/NzbDrone.Core/Profiles/Releases/PerlRegexFactory.cs index 1617bbdcf..b75e80db9 100644 --- a/src/NzbDrone.Core/Restrictions/PerlRegexFactory.cs +++ b/src/NzbDrone.Core/Profiles/Releases/PerlRegexFactory.cs @@ -1,11 +1,7 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using System; using System.Text.RegularExpressions; -using NzbDrone.Common.Exceptions; -namespace NzbDrone.Core.Restrictions +namespace NzbDrone.Core.Profiles.Releases { public static class PerlRegexFactory { diff --git a/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs b/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs new file mode 100644 index 000000000..d8ca43b4d --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Releases/PreferredWordService.cs @@ -0,0 +1,76 @@ +using NLog; +using NzbDrone.Core.Tv; +using System.Collections.Generic; +using System.Linq; + +namespace NzbDrone.Core.Profiles.Releases +{ + public interface IPreferredWordService + { + int Calculate(Series series, string title); + List GetMatchingPreferredWords(Series series, string title, bool isRenaming); + } + + public class PreferredWordService : IPreferredWordService + { + private readonly IReleaseProfileService _releaseProfileService; + private readonly ITermMatcher _termMatcher; + private readonly Logger _logger; + + public PreferredWordService(IReleaseProfileService releaseProfileService, ITermMatcher termMatcher, Logger logger) + { + _releaseProfileService = releaseProfileService; + _termMatcher = termMatcher; + _logger = logger; + } + + public int Calculate(Series series, string title) + { + _logger.Trace("Calculating preferred word score for '{0}'", title); + + var matchingPairs = GetMatchingPairs(series, title, false); + var score = matchingPairs.Sum(p => p.Value); + + _logger.Trace("Calculated preferred word score for '{0}': {1}", title, score); + + return score; + } + + public List GetMatchingPreferredWords(Series series, string title, bool isRenaming) + { + var matchingPairs = GetMatchingPairs(series, title, isRenaming); + + return matchingPairs.OrderByDescending(p => p.Value) + .Select(p => p.Key) + .ToList(); + } + + private List> GetMatchingPairs(Series series, string title, bool isRenaming) + { + var releaseProfiles = _releaseProfileService.AllForTags(series.Tags); + var result = new List>(); + + _logger.Trace("Calculating preferred word score for '{0}'", title); + + foreach (var releaseProfile in releaseProfiles) + { + if (isRenaming && !releaseProfile.IncludePreferredWhenRenaming) + { + continue; + } + + foreach (var preferredPair in releaseProfile.Preferred) + { + var term = preferredPair.Key; + + if (_termMatcher.IsMatch(term, title)) + { + result.Add(preferredPair); + } + } + } + + return result; + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs new file mode 100644 index 000000000..962a946c9 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfile.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using NzbDrone.Core.Datastore; + +namespace NzbDrone.Core.Profiles.Releases +{ + public class ReleaseProfile : ModelBase + { + public string Required { get; set; } + public string Ignored { get; set; } + public List> Preferred { get; set; } + public bool IncludePreferredWhenRenaming { get; set; } + public HashSet Tags { get; set; } + + public ReleaseProfile() + { + Preferred = new List>(); + IncludePreferredWhenRenaming = true; + Tags = new HashSet(); + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Releases/ReleaseProfileRepository.cs b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfileRepository.cs new file mode 100644 index 000000000..b8bcf3b49 --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfileRepository.cs @@ -0,0 +1,17 @@ +using NzbDrone.Core.Datastore; +using NzbDrone.Core.Messaging.Events; + +namespace NzbDrone.Core.Profiles.Releases +{ + public interface IRestrictionRepository : IBasicRepository + { + } + + public class ReleaseProfileRepository : BasicRepository, IRestrictionRepository + { + public ReleaseProfileRepository(IMainDatabase database, IEventAggregator eventAggregator) + : base(database, eventAggregator) + { + } + } +} diff --git a/src/NzbDrone.Core/Profiles/Releases/ReleaseProfileService.cs b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfileService.cs new file mode 100644 index 000000000..7ae1b79eb --- /dev/null +++ b/src/NzbDrone.Core/Profiles/Releases/ReleaseProfileService.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using NLog; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Core.Profiles.Releases +{ + public interface IReleaseProfileService + { + List All(); + List AllForTag(int tagId); + List AllForTags(HashSet tagIds); + ReleaseProfile Get(int id); + void Delete(int id); + ReleaseProfile Add(ReleaseProfile restriction); + ReleaseProfile Update(ReleaseProfile restriction); + } + + public class ReleaseProfileService : IReleaseProfileService + { + private readonly IRestrictionRepository _repo; + private readonly Logger _logger; + + public ReleaseProfileService(IRestrictionRepository repo, Logger logger) + { + _repo = repo; + _logger = logger; + } + + public List All() + { + return _repo.All().ToList(); + } + + public List AllForTag(int tagId) + { + return _repo.All().Where(r => r.Tags.Contains(tagId)).ToList(); + } + + public List AllForTags(HashSet tagIds) + { + return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList(); + } + + public ReleaseProfile Get(int id) + { + return _repo.Get(id); + } + + public void Delete(int id) + { + _repo.Delete(id); + } + + public ReleaseProfile Add(ReleaseProfile restriction) + { + return _repo.Insert(restriction); + } + + public ReleaseProfile Update(ReleaseProfile restriction) + { + return _repo.Update(restriction); + } + } +} diff --git a/src/NzbDrone.Core/Restrictions/TermMatcher.cs b/src/NzbDrone.Core/Profiles/Releases/TermMatcher.cs similarity index 93% rename from src/NzbDrone.Core/Restrictions/TermMatcher.cs rename to src/NzbDrone.Core/Profiles/Releases/TermMatcher.cs index af0c3d1f5..f8c8fd82d 100644 --- a/src/NzbDrone.Core/Restrictions/TermMatcher.cs +++ b/src/NzbDrone.Core/Profiles/Releases/TermMatcher.cs @@ -1,11 +1,8 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; using System.Text.RegularExpressions; using NzbDrone.Common.Cache; -namespace NzbDrone.Core.Restrictions +namespace NzbDrone.Core.Profiles.Releases { public interface ITermMatcher { diff --git a/src/NzbDrone.Core/Restrictions/Restriction.cs b/src/NzbDrone.Core/Restrictions/Restriction.cs deleted file mode 100644 index 9be667d81..000000000 --- a/src/NzbDrone.Core/Restrictions/Restriction.cs +++ /dev/null @@ -1,18 +0,0 @@ -using System.Collections.Generic; -using NzbDrone.Core.Datastore; - -namespace NzbDrone.Core.Restrictions -{ - public class Restriction : ModelBase - { - public string Required { get; set; } - public string Preferred { get; set; } - public string Ignored { get; set; } - public HashSet Tags { get; set; } - - public Restriction() - { - Tags = new HashSet(); - } - } -} diff --git a/src/NzbDrone.Core/Restrictions/RestrictionRepository.cs b/src/NzbDrone.Core/Restrictions/RestrictionRepository.cs deleted file mode 100644 index a88b0e67f..000000000 --- a/src/NzbDrone.Core/Restrictions/RestrictionRepository.cs +++ /dev/null @@ -1,17 +0,0 @@ -using NzbDrone.Core.Datastore; -using NzbDrone.Core.Messaging.Events; - -namespace NzbDrone.Core.Restrictions -{ - public interface IRestrictionRepository : IBasicRepository - { - } - - public class RestrictionRepository : BasicRepository, IRestrictionRepository - { - public RestrictionRepository(IMainDatabase database, IEventAggregator eventAggregator) - : base(database, eventAggregator) - { - } - } -} diff --git a/src/NzbDrone.Core/Restrictions/RestrictionService.cs b/src/NzbDrone.Core/Restrictions/RestrictionService.cs deleted file mode 100644 index 90b9d4a6f..000000000 --- a/src/NzbDrone.Core/Restrictions/RestrictionService.cs +++ /dev/null @@ -1,65 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using NLog; -using NzbDrone.Common.Extensions; - -namespace NzbDrone.Core.Restrictions -{ - public interface IRestrictionService - { - List All(); - List AllForTag(int tagId); - List AllForTags(HashSet tagIds); - Restriction Get(int id); - void Delete(int id); - Restriction Add(Restriction restriction); - Restriction Update(Restriction restriction); - } - - public class RestrictionService : IRestrictionService - { - private readonly IRestrictionRepository _repo; - private readonly Logger _logger; - - public RestrictionService(IRestrictionRepository repo, Logger logger) - { - _repo = repo; - _logger = logger; - } - - public List All() - { - return _repo.All().ToList(); - } - - public List AllForTag(int tagId) - { - return _repo.All().Where(r => r.Tags.Contains(tagId)).ToList(); - } - - public List AllForTags(HashSet tagIds) - { - return _repo.All().Where(r => r.Tags.Intersect(tagIds).Any() || r.Tags.Empty()).ToList(); - } - - public Restriction Get(int id) - { - return _repo.Get(id); - } - - public void Delete(int id) - { - _repo.Delete(id); - } - - public Restriction Add(Restriction restriction) - { - return _repo.Insert(restriction); - } - - public Restriction Update(Restriction restriction) - { - return _repo.Update(restriction); - } - } -} diff --git a/src/NzbDrone.Core/Tags/TagService.cs b/src/NzbDrone.Core/Tags/TagService.cs index 26c390983..6b5a63dc4 100644 --- a/src/NzbDrone.Core/Tags/TagService.cs +++ b/src/NzbDrone.Core/Tags/TagService.cs @@ -3,7 +3,7 @@ using System.Linq; using NzbDrone.Core.Messaging.Events; using NzbDrone.Core.Notifications; using NzbDrone.Core.Profiles.Delay; -using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Profiles.Releases; using NzbDrone.Core.Tv; namespace NzbDrone.Core.Tags @@ -27,21 +27,21 @@ namespace NzbDrone.Core.Tags private readonly IEventAggregator _eventAggregator; private readonly IDelayProfileService _delayProfileService; private readonly INotificationFactory _notificationFactory; - private readonly IRestrictionService _restrictionService; + private readonly IReleaseProfileService _releaseProfileService; private readonly ISeriesService _seriesService; public TagService(ITagRepository repo, IEventAggregator eventAggregator, IDelayProfileService delayProfileService, INotificationFactory notificationFactory, - IRestrictionService restrictionService, + IReleaseProfileService releaseProfileService, ISeriesService seriesService) { _repo = repo; _eventAggregator = eventAggregator; _delayProfileService = delayProfileService; _notificationFactory = notificationFactory; - _restrictionService = restrictionService; + _releaseProfileService = releaseProfileService; _seriesService = seriesService; } @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Tags var tag = GetTag(tagId); var delayProfiles = _delayProfileService.AllForTag(tagId); var notifications = _notificationFactory.AllForTag(tagId); - var restrictions = _restrictionService.AllForTag(tagId); + var restrictions = _releaseProfileService.AllForTag(tagId); var series = _seriesService.AllForTag(tagId); return new TagDetails @@ -91,7 +91,7 @@ namespace NzbDrone.Core.Tags var tags = All(); var delayProfiles = _delayProfileService.All(); var notifications = _notificationFactory.All(); - var restrictions = _restrictionService.All(); + var restrictions = _releaseProfileService.All(); var series = _seriesService.GetAllSeries(); var details = new List(); diff --git a/src/Sonarr.Api.V3/Calendar/CalendarModule.cs b/src/Sonarr.Api.V3/Calendar/CalendarModule.cs index 07092c21c..6440eba44 100644 --- a/src/Sonarr.Api.V3/Calendar/CalendarModule.cs +++ b/src/Sonarr.Api.V3/Calendar/CalendarModule.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Tv; using NzbDrone.SignalR; using Sonarr.Api.V3.Episodes; diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs index 3f5b8e7a0..279195f7c 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileModule.cs @@ -4,6 +4,7 @@ using System.Linq; using Nancy; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Exceptions; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.MediaFiles.Events; diff --git a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs index 7e7783eb3..07a06f7eb 100644 --- a/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs +++ b/src/Sonarr.Api.V3/EpisodeFiles/EpisodeFileResource.cs @@ -1,6 +1,7 @@ using System; using System.IO; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Languages; using NzbDrone.Core.MediaFiles; using NzbDrone.Core.Qualities; diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs b/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs index 57637482d..810662ecd 100644 --- a/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs +++ b/src/Sonarr.Api.V3/Episodes/EpisodeModule.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Nancy; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Tv; using NzbDrone.SignalR; using Sonarr.Http.Extensions; diff --git a/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs b/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs index 004da690b..6271f8f5e 100644 --- a/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs +++ b/src/Sonarr.Api.V3/Episodes/EpisodeModuleWithSignalR.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using NzbDrone.Common.Extensions; using NzbDrone.Core.Datastore.Events; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; using NzbDrone.Core.MediaFiles.Events; using NzbDrone.Core.Messaging.Events; diff --git a/src/Sonarr.Api.V3/History/HistoryModule.cs b/src/Sonarr.Api.V3/History/HistoryModule.cs index 5551febb2..9c46a28cc 100644 --- a/src/Sonarr.Api.V3/History/HistoryModule.cs +++ b/src/Sonarr.Api.V3/History/HistoryModule.cs @@ -4,6 +4,7 @@ using System.Linq; using Nancy; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Download; using NzbDrone.Core.History; using Sonarr.Api.V3.Episodes; diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs b/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs index 58c553862..5c4f117d8 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseModuleBase.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using NzbDrone.Core.DecisionEngine; using Sonarr.Http; @@ -28,9 +28,15 @@ namespace Sonarr.Api.V3.Indexers if (decision.RemoteEpisode.Series != null) { - release.QualityWeight = decision.RemoteEpisode.Series - .Profile.Value - .Items.FindIndex(v => v.Quality == release.Quality.Quality) * 100; + release.QualityWeight = decision.RemoteEpisode + .Series + .Profile.Value + .Items.FindIndex(v => v.Quality == release.Quality.Quality) * 100; + + release.LanguageWeight = decision.RemoteEpisode + .Series + .LanguageProfile.Value + .Languages.FindIndex(v => v.Language == release.Language) * 100; } release.QualityWeight += release.Quality.Revision.Real * 10; diff --git a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs index bf28791d0..006e78285 100644 --- a/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs +++ b/src/Sonarr.Api.V3/Indexers/ReleaseResource.cs @@ -29,6 +29,7 @@ namespace Sonarr.Api.V3.Indexers public bool SceneSource { get; set; } public int SeasonNumber { get; set; } public Language Language { get; set; } + public int LanguageWeight { get; set; } public string AirDate { get; set; } public string SeriesTitle { get; set; } public int[] EpisodeNumbers { get; set; } @@ -45,6 +46,7 @@ namespace Sonarr.Api.V3.Indexers public string InfoUrl { get; set; } public bool DownloadAllowed { get; set; } public int ReleaseWeight { get; set; } + public int PreferredWordScore { get; set; } public string MagnetUrl { get; set; } public string InfoHash { get; set; } @@ -104,7 +106,7 @@ namespace Sonarr.Api.V3.Indexers InfoUrl = releaseInfo.InfoUrl, DownloadAllowed = remoteEpisode.DownloadAllowed, //ReleaseWeight - + PreferredWordScore = remoteEpisode.PreferredWordScore, MagnetUrl = torrentInfo.MagnetUrl, InfoHash = torrentInfo.InfoHash, diff --git a/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs new file mode 100644 index 000000000..932d309fb --- /dev/null +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileModule.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using FluentValidation.Results; +using NzbDrone.Common.Extensions; +using NzbDrone.Core.Profiles.Releases; +using Sonarr.Http; + +namespace Sonarr.Api.V3.Profiles.Release +{ + public class ReleaseProfileModule : SonarrRestModule + { + private readonly IReleaseProfileService _releaseProfileService; + + + public ReleaseProfileModule(IReleaseProfileService releaseProfileService) + { + _releaseProfileService = releaseProfileService; + + GetResourceById = Get; + GetResourceAll = GetAll; + CreateResource = Create; + UpdateResource = Update; + DeleteResource = Delete; + + SharedValidator.Custom(restriction => + { + if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace() && restriction.Preferred.Empty()) + { + return new ValidationFailure("", "'Must contain', 'Must not contain' or 'Preferred' is required"); + } + + return null; + }); + } + + private ReleaseProfileResource Get(int id) + { + return _releaseProfileService.Get(id).ToResource(); + } + + private List GetAll() + { + return _releaseProfileService.All().ToResource(); + } + + private int Create(ReleaseProfileResource resource) + { + return _releaseProfileService.Add(resource.ToModel()).Id; + } + + private void Update(ReleaseProfileResource resource) + { + _releaseProfileService.Update(resource.ToModel()); + } + + private void Delete(int id) + { + _releaseProfileService.Delete(id); + } + } +} diff --git a/src/Sonarr.Api.V3/Restrictions/RestrictionResource.cs b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs similarity index 54% rename from src/Sonarr.Api.V3/Restrictions/RestrictionResource.cs rename to src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs index d30743fe6..28a7e17b9 100644 --- a/src/Sonarr.Api.V3/Restrictions/RestrictionResource.cs +++ b/src/Sonarr.Api.V3/Profiles/Release/ReleaseProfileResource.cs @@ -1,18 +1,19 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; -using NzbDrone.Core.Restrictions; +using NzbDrone.Core.Profiles.Releases; using Sonarr.Http.REST; -namespace Sonarr.Api.V3.Restrictions +namespace Sonarr.Api.V3.Profiles.Release { - public class RestrictionResource : RestResource + public class ReleaseProfileResource : RestResource { public string Required { get; set; } - public string Preferred { get; set; } public string Ignored { get; set; } + public List> Preferred { get; set; } + public bool IncludePreferredWhenRenaming { get; set; } public HashSet Tags { get; set; } - public RestrictionResource() + public ReleaseProfileResource() { Tags = new HashSet(); } @@ -20,37 +21,39 @@ namespace Sonarr.Api.V3.Restrictions public static class RestrictionResourceMapper { - public static RestrictionResource ToResource(this Restriction model) + public static ReleaseProfileResource ToResource(this ReleaseProfile model) { if (model == null) return null; - return new RestrictionResource + return new ReleaseProfileResource { Id = model.Id, Required = model.Required, - Preferred = model.Preferred, Ignored = model.Ignored, + Preferred = model.Preferred, + IncludePreferredWhenRenaming = model.IncludePreferredWhenRenaming, Tags = new HashSet(model.Tags) }; } - public static Restriction ToModel(this RestrictionResource resource) + public static ReleaseProfile ToModel(this ReleaseProfileResource resource) { if (resource == null) return null; - return new Restriction + return new ReleaseProfile { Id = resource.Id, Required = resource.Required, - Preferred = resource.Preferred, Ignored = resource.Ignored, + Preferred = resource.Preferred, + IncludePreferredWhenRenaming = resource.IncludePreferredWhenRenaming, Tags = new HashSet(resource.Tags) }; } - public static List ToResource(this IEnumerable models) + public static List ToResource(this IEnumerable models) { return models.Select(ToResource).ToList(); } diff --git a/src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs b/src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs deleted file mode 100644 index 11bdb55d6..000000000 --- a/src/Sonarr.Api.V3/Restrictions/RestrictionModule.cs +++ /dev/null @@ -1,60 +0,0 @@ -using System.Collections.Generic; -using FluentValidation.Results; -using NzbDrone.Common.Extensions; -using NzbDrone.Core.Restrictions; -using Sonarr.Http; - -namespace Sonarr.Api.V3.Restrictions -{ - public class RestrictionModule : SonarrRestModule - { - private readonly IRestrictionService _restrictionService; - - - public RestrictionModule(IRestrictionService restrictionService) - { - _restrictionService = restrictionService; - - GetResourceById = Get; - GetResourceAll = GetAll; - CreateResource = Create; - UpdateResource = Update; - DeleteResource = Delete; - - SharedValidator.Custom(restriction => - { - if (restriction.Ignored.IsNullOrWhiteSpace() && restriction.Required.IsNullOrWhiteSpace()) - { - return new ValidationFailure("", "Either 'Must contain' or 'Must not contain' is required"); - } - - return null; - }); - } - - private RestrictionResource Get(int id) - { - return _restrictionService.Get(id).ToResource(); - } - - private List GetAll() - { - return _restrictionService.All().ToResource(); - } - - private int Create(RestrictionResource resource) - { - return _restrictionService.Add(resource.ToModel()).Id; - } - - private void Update(RestrictionResource resource) - { - _restrictionService.Update(resource.ToModel()); - } - - private void Delete(int id) - { - _restrictionService.Delete(id); - } - } -} diff --git a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj index a229ff0a9..fe83b8cbe 100644 --- a/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj +++ b/src/Sonarr.Api.V3/Sonarr.Api.V3.csproj @@ -175,8 +175,8 @@ - - + + diff --git a/src/Sonarr.Api.V3/Wanted/CutoffModule.cs b/src/Sonarr.Api.V3/Wanted/CutoffModule.cs index 5cf59f670..d496e8ff3 100644 --- a/src/Sonarr.Api.V3/Wanted/CutoffModule.cs +++ b/src/Sonarr.Api.V3/Wanted/CutoffModule.cs @@ -1,6 +1,7 @@ using System.Linq; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Tv; using NzbDrone.SignalR; using Sonarr.Api.V3.Episodes; diff --git a/src/Sonarr.Api.V3/Wanted/MissingModule.cs b/src/Sonarr.Api.V3/Wanted/MissingModule.cs index 310b0c742..f7b59d0f4 100644 --- a/src/Sonarr.Api.V3/Wanted/MissingModule.cs +++ b/src/Sonarr.Api.V3/Wanted/MissingModule.cs @@ -1,6 +1,7 @@ using System.Linq; using NzbDrone.Core.Datastore; using NzbDrone.Core.DecisionEngine; +using NzbDrone.Core.DecisionEngine.Specifications; using NzbDrone.Core.Tv; using NzbDrone.SignalR; using Sonarr.Api.V3.Episodes;