From 7b4cb4145d2339935ad8fbf91ed0360183270c2e Mon Sep 17 00:00:00 2001 From: Taloth Saldono Date: Sat, 6 May 2017 14:02:59 +0200 Subject: [PATCH] Ability to blacklist and rename indexer urls via services. --- .../NzbDrone.Core.Test.csproj | 1 + .../SkyhookNotificationServiceFixture.cs | 172 ++++++++++++++++++ src/NzbDrone.Core/Indexers/IndexerFactory.cs | 34 +++- src/NzbDrone.Core/NzbDrone.Core.csproj | 5 + .../SkyhookNotification.cs | 19 ++ .../SkyhookNotificationProxy.cs | 50 +++++ .../SkyhookNotificationService.cs | 77 ++++++++ .../SkyhookNotificationType.cs | 17 ++ .../SubstituteUrlService.cs | 51 ++++++ 9 files changed, 421 insertions(+), 5 deletions(-) create mode 100644 src/NzbDrone.Core.Test/SkyhookNotifications/SkyhookNotificationServiceFixture.cs create mode 100644 src/NzbDrone.Core/SkyhookNotifications/SkyhookNotification.cs create mode 100644 src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationProxy.cs create mode 100644 src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationService.cs create mode 100644 src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationType.cs create mode 100644 src/NzbDrone.Core/SkyhookNotifications/SubstituteUrlService.cs diff --git a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj index 9b325cfb8..c629b4d73 100644 --- a/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj +++ b/src/NzbDrone.Core.Test/NzbDrone.Core.Test.csproj @@ -366,6 +366,7 @@ + diff --git a/src/NzbDrone.Core.Test/SkyhookNotifications/SkyhookNotificationServiceFixture.cs b/src/NzbDrone.Core.Test/SkyhookNotifications/SkyhookNotificationServiceFixture.cs new file mode 100644 index 000000000..b6d0f1b99 --- /dev/null +++ b/src/NzbDrone.Core.Test/SkyhookNotifications/SkyhookNotificationServiceFixture.cs @@ -0,0 +1,172 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Core.SkyhookNotifications; +using NzbDrone.Core.Test.Framework; + +namespace NzbDrone.Core.Test.SkyhookNotifications +{ + public class SkyhookNotificationServiceFixture : CoreTest + { + private static readonly Version _previousVersion = new Version(BuildInfo.Version.Major, BuildInfo.Version.Minor, BuildInfo.Version.Build, BuildInfo.Version.Revision - 1); + private static readonly Version _currentVersion = BuildInfo.Version; + private static readonly Version _nextVersion = new Version(BuildInfo.Version.Major, BuildInfo.Version.Minor, BuildInfo.Version.Build, BuildInfo.Version.Revision + 1); + + private List _expiredNotifications; + private List _currentNotifications; + private List _futureNotifications; + private List _urlNotifications; + + [SetUp] + public void SetUp() + { + _expiredNotifications = new List + { + new SkyhookNotification + { + Id = 1, + Type = SkyhookNotificationType.Notification, + Title = "Expired Notification", + MaximumVersion = _previousVersion.ToString() + } + }; + + _currentNotifications = new List + { + new SkyhookNotification + { + Id = 2, + Type = SkyhookNotificationType.Notification, + Title = "Timeless current Notification" + }, + new SkyhookNotification + { + Id = 2, + Type = SkyhookNotificationType.Notification, + Title = "Ending current Notification", + MaximumVersion = _currentVersion.ToString() + }, + new SkyhookNotification + { + Id = 2, + Type = SkyhookNotificationType.Notification, + Title = "Ending future Notification", + MaximumVersion = _nextVersion.ToString() + }, + new SkyhookNotification + { + Id = 2, + Type = SkyhookNotificationType.Notification, + Title = "Starting previous Notification", + MinimumVersion = _previousVersion.ToString() + }, + new SkyhookNotification + { + Id = 2, + Type = SkyhookNotificationType.Notification, + Title = "Starting current Notification", + MinimumVersion = _currentVersion.ToString() + } + }; + + _futureNotifications = new List + { + new SkyhookNotification + { + Id = 3, + Type = SkyhookNotificationType.Notification, + Title = "Future Notification", + MinimumVersion = _nextVersion.ToString() + } + }; + + _urlNotifications = new List + { + new SkyhookNotification + { + Id = 3, + Type = SkyhookNotificationType.UrlBlacklist, + Title = "Future Notification" + }, + new SkyhookNotification + { + Id = 3, + Type = SkyhookNotificationType.UrlReplace, + Title = "Future Notification" + } + }; + } + + private void GivenNotifications(List notifications) + { + Mocker.GetMock() + .Setup(v => v.GetNotifications()) + .Returns(notifications); + } + + [Test] + public void should_not_return_expired_notifications() + { + GivenNotifications(_expiredNotifications); + + Subject.GetUserNotifications().Should().BeEmpty(); + } + + [Test] + public void should_not_return_future_notifications() + { + GivenNotifications(_futureNotifications); + + Subject.GetUserNotifications().Should().BeEmpty(); + } + + [Test] + public void should_return_current_notifications() + { + GivenNotifications(_currentNotifications); + + Subject.GetUserNotifications().Should().HaveCount(_currentNotifications.Count); + } + + [Test] + public void should_not_return_user_notifications() + { + GivenNotifications(_currentNotifications); + + Subject.GetUrlNotifications().Should().BeEmpty(); + } + + [Test] + public void should_not_return_url_notifications() + { + GivenNotifications(_urlNotifications); + + Subject.GetUserNotifications().Should().BeEmpty(); + } + + [Test] + public void should_return_url_notifications() + { + GivenNotifications(_urlNotifications); + + Subject.GetUrlNotifications().Should().HaveCount(_urlNotifications.Count); + } + + [Test] + public void should_cache_api_result() + { + GivenNotifications(_urlNotifications); + + Subject.GetUrlNotifications(); + Subject.GetUrlNotifications(); + + Mocker.GetMock() + .Verify(v => v.GetNotifications(), Times.Once()); + } + } +} diff --git a/src/NzbDrone.Core/Indexers/IndexerFactory.cs b/src/NzbDrone.Core/Indexers/IndexerFactory.cs index c4ef7ac79..e13c2f8c6 100644 --- a/src/NzbDrone.Core/Indexers/IndexerFactory.cs +++ b/src/NzbDrone.Core/Indexers/IndexerFactory.cs @@ -2,7 +2,9 @@ using System.Linq; using NLog; using NzbDrone.Common.Composition; +using NzbDrone.Common.Extensions; using NzbDrone.Core.Messaging.Events; +using NzbDrone.Core.SkyhookNotifications; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.Indexers @@ -15,17 +17,20 @@ namespace NzbDrone.Core.Indexers public class IndexerFactory : ProviderFactory, IIndexerFactory { + private readonly ISubstituteIndexerUrl _substituteIndexerUrl; private readonly IIndexerStatusService _indexerStatusService; private readonly Logger _logger; - public IndexerFactory(IIndexerStatusService indexerStatusService, + public IndexerFactory(ISubstituteIndexerUrl replaceIndexerUrl, + IIndexerStatusService indexerStatusService, IIndexerRepository providerRepository, IEnumerable providers, - IContainer container, + IContainer container, IEventAggregator eventAggregator, Logger logger) : base(providerRepository, providers, container, eventAggregator, logger) { + _substituteIndexerUrl = replaceIndexerUrl; _indexerStatusService = indexerStatusService; _logger = logger; } @@ -50,7 +55,7 @@ namespace NzbDrone.Core.Indexers if (filterBlockedIndexers) { - return FilterBlockedIndexers(enabledIndexers).ToList(); + return FilterBlockedIndexers(SubstituteIndexerUrls(enabledIndexers)).ToList(); } return enabledIndexers.ToList(); @@ -62,12 +67,31 @@ namespace NzbDrone.Core.Indexers if (filterBlockedIndexers) { - return FilterBlockedIndexers(enabledIndexers).ToList(); + return FilterBlockedIndexers(SubstituteIndexerUrls(enabledIndexers)).ToList(); } return enabledIndexers.ToList(); } + private IEnumerable SubstituteIndexerUrls(IEnumerable indexers) + { + foreach (var indexer in indexers) + { + var settings = (IIndexerSettings)indexer.Definition.Settings; + if (settings.BaseUrl.IsNotNullOrWhiteSpace()) + { + var newBaseUrl = _substituteIndexerUrl.SubstituteUrl(settings.BaseUrl); + if (newBaseUrl != settings.BaseUrl) + { + _logger.Debug("Substituted indexer {0} url {1} with {2} since services blacklisted it.", indexer.Definition.Name, settings.BaseUrl, newBaseUrl); + settings.BaseUrl = newBaseUrl; + } + } + + yield return indexer; + } + } + private IEnumerable FilterBlockedIndexers(IEnumerable indexers) { var blockedIndexers = _indexerStatusService.GetBlockedIndexers().ToDictionary(v => v.ProviderId, v => v); @@ -85,4 +109,4 @@ namespace NzbDrone.Core.Indexers } } } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index 62f7edd61..8b0752941 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -1063,6 +1063,11 @@ + + + + + diff --git a/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotification.cs b/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotification.cs new file mode 100644 index 000000000..26becceea --- /dev/null +++ b/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotification.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Core.SkyhookNotifications +{ + public class SkyhookNotification + { + public int Id { get; set; } + public string MinimumVersion { get; set; } + public string MaximumVersion { get; set; } + public SkyhookNotificationType Type { get; set; } + public string Title { get; set; } + public string Message { get; set; } + public string RegexMatch { get; set; } + public string RegexReplace { get; set; } + } +} diff --git a/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationProxy.cs b/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationProxy.cs new file mode 100644 index 000000000..3158c531b --- /dev/null +++ b/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationProxy.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Cloud; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Http; + +namespace NzbDrone.Core.SkyhookNotifications +{ + public interface ISkyhookNotificationProxy + { + List GetNotifications(); + } + + public class SkyhookNotificationProxy : ISkyhookNotificationProxy + { + private readonly IHttpClient _httpClient; + private readonly IHttpRequestBuilderFactory _requestBuilder; + private readonly Logger _logger; + + public SkyhookNotificationProxy(IHttpClient httpClient, ISonarrCloudRequestBuilder requestBuilder, Logger logger) + { + _httpClient = httpClient; + _requestBuilder = requestBuilder.Services; + _logger = logger; + } + + public List GetNotifications() + { + var notificationsRequest = _requestBuilder.Create() + .Resource("/notifications") + .AddQueryParam("version", BuildInfo.Version) + .AddQueryParam("os", OsInfo.Os.ToString().ToLowerInvariant()) + .Build(); + + try + { + var response = _httpClient.Get>(notificationsRequest); + return response.Resource; + } + catch (Exception ex) + { + _logger.Warn(ex, "Failed to get information update from {0}", notificationsRequest.Url.Host); + return new List(); + } + } + } +} diff --git a/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationService.cs b/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationService.cs new file mode 100644 index 000000000..548acccbf --- /dev/null +++ b/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationService.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Cache; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Core.SkyhookNotifications +{ + public interface ISkyhookNotificationService + { + List GetUserNotifications(); + List GetUrlNotifications(); + } + + public class SkyhookNotificationService : ISkyhookNotificationService + { + private readonly ISkyhookNotificationProxy _proxy; + private readonly Logger _logger; + + private readonly ICached> _cache; + + private readonly TimeSpan CacheExpiry = TimeSpan.FromHours(12); + + public SkyhookNotificationService(ISkyhookNotificationProxy proxy, ICacheManager cacheManager, Logger logger) + { + _proxy = proxy; + _logger = logger; + + _cache = cacheManager.GetCache>(GetType()); + } + + private List GetNotifications(string key) + { + var result = _cache.Find(key); + + if (result == null) + { + var all = _proxy.GetNotifications().Where(FilterVersion).ToList(); + + var user = all.Where(v => v.Type == SkyhookNotificationType.Notification).ToList(); + var url = all.Where(v => v.Type == SkyhookNotificationType.UrlBlacklist || v.Type == SkyhookNotificationType.UrlReplace).ToList(); + _cache.Set("all", all, CacheExpiry); + _cache.Set("user", user, CacheExpiry); + _cache.Set("url", url, CacheExpiry); + } + + return _cache.Find(key); + } + + public List GetUserNotifications() + { + return GetNotifications("user"); + } + + public List GetUrlNotifications() + { + return GetNotifications("url"); + } + + private bool FilterVersion(SkyhookNotification notification) + { + if (notification.MinimumVersion != null && BuildInfo.Version < Version.Parse(notification.MinimumVersion)) + { + return false; + } + + if (notification.MaximumVersion != null && BuildInfo.Version > Version.Parse(notification.MaximumVersion)) + { + return false; + } + + return true; + } + } +} diff --git a/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationType.cs b/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationType.cs new file mode 100644 index 000000000..24b2f8f5f --- /dev/null +++ b/src/NzbDrone.Core/SkyhookNotifications/SkyhookNotificationType.cs @@ -0,0 +1,17 @@ +using System; +using System.Linq; + +namespace NzbDrone.Core.SkyhookNotifications +{ + public enum SkyhookNotificationType + { + // Notification for the user alone. + Notification = 1, + + // Indexer urls matching the RegexMatch are automatically set to temporarily disabled and never contacted. + UrlBlacklist = 2, + + // Indexer urls matching the RegexMatch are replaced with RegexReplace. + UrlReplace = 3 + } +} diff --git a/src/NzbDrone.Core/SkyhookNotifications/SubstituteUrlService.cs b/src/NzbDrone.Core/SkyhookNotifications/SubstituteUrlService.cs new file mode 100644 index 000000000..71298b959 --- /dev/null +++ b/src/NzbDrone.Core/SkyhookNotifications/SubstituteUrlService.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace NzbDrone.Core.SkyhookNotifications +{ + public interface ISubstituteIndexerUrl + { + string SubstituteUrl(string baseUrl); + } + + public class SubstituteUrlService : ISubstituteIndexerUrl + { + private readonly ISkyhookNotificationService _notificationService; + + public SubstituteUrlService(ISkyhookNotificationService notificationService) + { + _notificationService = notificationService; + } + + public string SubstituteUrl(string baseUrl) + { + foreach (var action in _notificationService.GetUrlNotifications()) + { + if (action.Type == SkyhookNotificationType.UrlBlacklist) + { + if (action.RegexMatch == null) continue; + + if (Regex.IsMatch(baseUrl, action.RegexMatch)) + { + return null; + } + } + else if (action.Type == SkyhookNotificationType.UrlReplace) + { + if (action.RegexMatch == null) continue; + if (action.RegexReplace == null) continue; + + if (Regex.IsMatch(baseUrl, action.RegexMatch)) + { + return Regex.Replace(baseUrl, action.RegexMatch, action.RegexReplace); + } + } + } + + return baseUrl; + } + } +}