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