diff --git a/NzbDrone.Api/Commands/CommandConnection.cs b/NzbDrone.Api/Commands/CommandConnection.cs new file mode 100644 index 000000000..227c69bec --- /dev/null +++ b/NzbDrone.Api/Commands/CommandConnection.cs @@ -0,0 +1,42 @@ +using System; +using Microsoft.AspNet.SignalR; +using Microsoft.AspNet.SignalR.Infrastructure; +using NzbDrone.Api.SignalR; +using NzbDrone.Common.Messaging; +using NzbDrone.Common.Messaging.Events; +using NzbDrone.Common.Messaging.Tracking; + +namespace NzbDrone.Api.Commands +{ + public class CommandConnection : NzbDronePersistentConnection, + IHandleAsync, + IHandleAsync, + IHandleAsync + { + public override string Resource + { + get { return "/Command"; } + } + + public void HandleAsync(CommandStartedEvent message) + { + BroadcastMessage(message.TrackedCommand); + } + + public void HandleAsync(CommandCompletedEvent message) + { + BroadcastMessage(message.TrackedCommand); + } + + public void HandleAsync(CommandFailedEvent message) + { + BroadcastMessage(message.TrackedCommand); + } + + private void BroadcastMessage(TrackedCommand trackedCommand) + { + var context = ((ConnectionManager)GlobalHost.ConnectionManager).GetConnection(GetType()); + context.Connection.Broadcast(trackedCommand); + } + } +} diff --git a/NzbDrone.Api/Commands/CommandModule.cs b/NzbDrone.Api/Commands/CommandModule.cs index 371483cc3..8ad4a5475 100644 --- a/NzbDrone.Api/Commands/CommandModule.cs +++ b/NzbDrone.Api/Commands/CommandModule.cs @@ -2,8 +2,10 @@ using System.Linq; using Nancy; using NzbDrone.Api.Extensions; +using NzbDrone.Api.Mapping; using NzbDrone.Common.Composition; using NzbDrone.Common.Messaging; +using NzbDrone.Common.Messaging.Tracking; namespace NzbDrone.Api.Commands { @@ -11,14 +13,16 @@ namespace NzbDrone.Api.Commands { private readonly IMessageAggregator _messageAggregator; private readonly IContainer _container; + private readonly ITrackCommands _trackCommands; - public CommandModule(IMessageAggregator messageAggregator, IContainer container) + public CommandModule(IMessageAggregator messageAggregator, IContainer container, ITrackCommands trackCommands) { _messageAggregator = messageAggregator; _container = container; + _trackCommands = trackCommands; Post["/"] = x => RunCommand(ReadResourceFromRequest()); - + Get["/"] = x => GetAllCommands(); } private Response RunCommand(CommandResource resource) @@ -29,9 +33,15 @@ namespace NzbDrone.Api.Commands .Equals(resource.Command, StringComparison.InvariantCultureIgnoreCase)); dynamic command = Request.Body.FromJson(commandType); - _messageAggregator.PublishCommand(command); - return resource.AsResponse(HttpStatusCode.Created); + var response = (TrackedCommand) _messageAggregator.PublishCommandAsync(command); + + return response.AsResponse(HttpStatusCode.Created); + } + + private Response GetAllCommands() + { + return _trackCommands.AllTracked().AsResponse(); } } } \ No newline at end of file diff --git a/NzbDrone.Api/Extensions/ReqResExtensions.cs b/NzbDrone.Api/Extensions/ReqResExtensions.cs index fd9980347..1f1d89180 100644 --- a/NzbDrone.Api/Extensions/ReqResExtensions.cs +++ b/NzbDrone.Api/Extensions/ReqResExtensions.cs @@ -12,7 +12,6 @@ namespace NzbDrone.Api.Extensions { private static readonly NancyJsonSerializer NancySerializer = new NancyJsonSerializer(); - public static readonly string LastModified = BuildInfo.BuildDateTime.ToString("r"); public static T FromJson(this Stream body) where T : class, new() @@ -25,7 +24,6 @@ namespace NzbDrone.Api.Extensions return (T)FromJson(body, type); } - public static object FromJson(this Stream body, Type type) { var reader = new StreamReader(body, true); diff --git a/NzbDrone.Api/NancyBootstrapper.cs b/NzbDrone.Api/NancyBootstrapper.cs index 4e481119a..61c311471 100644 --- a/NzbDrone.Api/NancyBootstrapper.cs +++ b/NzbDrone.Api/NancyBootstrapper.cs @@ -10,6 +10,7 @@ using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Messaging; using NzbDrone.Core.Instrumentation; using NzbDrone.Core.Lifecycle; +using NzbDrone.Core.ProgressMessaging; using TinyIoC; namespace NzbDrone.Api @@ -29,14 +30,12 @@ namespace NzbDrone.Api { _logger.Info("Starting NzbDrone API"); - RegisterPipelines(pipelines); container.Resolve().Register(); container.Resolve().Register(pipelines); container.Resolve().PublishEvent(new ApplicationStartedEvent()); - ApplicationPipelines.OnError.AddItemToEndOfPipeline(container.Resolve().HandleException); } @@ -48,10 +47,8 @@ namespace NzbDrone.Api { registerNancyPipeline.Register(pipelines); } - } - protected override TinyIoCContainer GetApplicationContainer() { return _tinyIoCContainer; diff --git a/NzbDrone.Api/NzbDrone.Api.csproj b/NzbDrone.Api/NzbDrone.Api.csproj index d0a03bd17..0be2d8ce9 100644 --- a/NzbDrone.Api/NzbDrone.Api.csproj +++ b/NzbDrone.Api/NzbDrone.Api.csproj @@ -83,8 +83,12 @@ + + + + diff --git a/NzbDrone.Api/ProgressMessaging/ProgressMessageConnection.cs b/NzbDrone.Api/ProgressMessaging/ProgressMessageConnection.cs new file mode 100644 index 000000000..a5b4afdb0 --- /dev/null +++ b/NzbDrone.Api/ProgressMessaging/ProgressMessageConnection.cs @@ -0,0 +1,24 @@ +using System; +using Microsoft.AspNet.SignalR; +using Microsoft.AspNet.SignalR.Infrastructure; +using NzbDrone.Api.SignalR; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.ProgressMessaging; + +namespace NzbDrone.Api.ProgressMessaging +{ + public class ProgressMessageConnection : NzbDronePersistentConnection, + IHandleAsync + { + public override string Resource + { + get { return "/ProgressMessage"; } + } + + public void HandleAsync(NewProgressMessageEvent message) + { + var context = ((ConnectionManager)GlobalHost.ConnectionManager).GetConnection(GetType()); + context.Connection.Broadcast(message.ProgressMessage); + } + } +} diff --git a/NzbDrone.Api/ProgressMessaging/ProgressMessageModule.cs b/NzbDrone.Api/ProgressMessaging/ProgressMessageModule.cs new file mode 100644 index 000000000..a06db9c24 --- /dev/null +++ b/NzbDrone.Api/ProgressMessaging/ProgressMessageModule.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Nancy; +using NzbDrone.Api.Extensions; + +namespace NzbDrone.Api.ProgressMessaging +{ + public class ProgressMessageModule : NzbDroneRestModule + { + public ProgressMessageModule() + { + Get["/"] = x => GetAllMessages(); + } + + private Response GetAllMessages() + { + return new List().AsResponse(); + } + } +} \ No newline at end of file diff --git a/NzbDrone.Api/ProgressMessaging/ProgressMessageResource.cs b/NzbDrone.Api/ProgressMessaging/ProgressMessageResource.cs new file mode 100644 index 000000000..3117eb142 --- /dev/null +++ b/NzbDrone.Api/ProgressMessaging/ProgressMessageResource.cs @@ -0,0 +1,12 @@ +using System; +using NzbDrone.Api.REST; + +namespace NzbDrone.Api.ProgressMessaging +{ + public class ProgressMessageResource : RestResource + { + public DateTime Time { get; set; } + public String CommandId { get; set; } + public String Message { get; set; } + } +} \ No newline at end of file diff --git a/NzbDrone.Common.Test/CacheTests/CachedFixture.cs b/NzbDrone.Common.Test/CacheTests/CachedFixture.cs index 4a1cf9021..1d91558a6 100644 --- a/NzbDrone.Common.Test/CacheTests/CachedFixture.cs +++ b/NzbDrone.Common.Test/CacheTests/CachedFixture.cs @@ -48,6 +48,23 @@ namespace NzbDrone.Common.Test.CacheTests _cachedString.Find("Key").Should().Be("New"); } + + [Test] + public void should_be_able_to_remove_key() + { + _cachedString.Set("Key", "Value"); + + _cachedString.Remove("Key"); + + _cachedString.Find("Key").Should().BeNull(); + } + + [Test] + public void should_be_able_to_remove_non_existing_key() + { + _cachedString.Remove("Key"); + } + [Test] public void should_store_null() { diff --git a/NzbDrone.Common.Test/EventingTests/MessageAggregatorCommandTests.cs b/NzbDrone.Common.Test/EventingTests/MessageAggregatorCommandTests.cs index 1e450ae21..b5c45bef0 100644 --- a/NzbDrone.Common.Test/EventingTests/MessageAggregatorCommandTests.cs +++ b/NzbDrone.Common.Test/EventingTests/MessageAggregatorCommandTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Moq; using NUnit.Framework; using NzbDrone.Common.Messaging; +using NzbDrone.Common.Messaging.Tracking; using NzbDrone.Test.Common; namespace NzbDrone.Common.Test.EventingTests @@ -27,6 +28,13 @@ namespace NzbDrone.Common.Test.EventingTests .Setup(c => c.Build(typeof(IExecute))) .Returns(_executorB.Object); + Mocker.GetMock() + .Setup(c => c.TrackIfNew(It.IsAny())) + .Returns(new TrackedCommand(new CommandA(), ProcessState.Running)); + + Mocker.GetMock() + .Setup(c => c.TrackIfNew(It.IsAny())) + .Returns(new TrackedCommand(new CommandB(), ProcessState.Running)); } [Test] @@ -34,6 +42,10 @@ namespace NzbDrone.Common.Test.EventingTests { var commandA = new CommandA(); + Mocker.GetMock() + .Setup(c => c.TrackIfNew(commandA)) + .Returns(new TrackedCommand(commandA, ProcessState.Running)); + Subject.PublishCommand(commandA); _executorA.Verify(c => c.Execute(commandA), Times.Once()); @@ -55,6 +67,9 @@ namespace NzbDrone.Common.Test.EventingTests { var commandA = new CommandA(); + Mocker.GetMock() + .Setup(c => c.TrackIfNew(commandA)) + .Returns(new TrackedCommand(commandA, ProcessState.Running)); Subject.PublishCommand(commandA); @@ -76,17 +91,23 @@ namespace NzbDrone.Common.Test.EventingTests public class CommandA : ICommand { + public String CommandId { get; private set; } // ReSharper disable UnusedParameter.Local public CommandA(int id = 0) // ReSharper restore UnusedParameter.Local { - + CommandId = HashUtil.GenerateCommandId(); } } public class CommandB : ICommand { + public String CommandId { get; private set; } + public CommandB() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Common.Test/MessagingTests/CommandEqualityComparerFixture.cs b/NzbDrone.Common.Test/MessagingTests/CommandEqualityComparerFixture.cs new file mode 100644 index 000000000..2b2deadea --- /dev/null +++ b/NzbDrone.Common.Test/MessagingTests/CommandEqualityComparerFixture.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using FluentAssertions; +using NUnit.Framework; +using NzbDrone.Common.Messaging; +using NzbDrone.Core.IndexerSearch; +using NzbDrone.Core.MediaFiles.Commands; + +namespace NzbDrone.Common.Test.MessagingTests +{ + [TestFixture] + public class CommandEqualityComparerFixture + { + [Test] + public void should_return_true_when_there_are_no_properties() + { + var command1 = new DownloadedEpisodesScanCommand(); + var command2 = new DownloadedEpisodesScanCommand(); + var comparer = new CommandEqualityComparer(); + + comparer.Equals(command1, command2).Should().BeTrue(); + } + + [Test] + public void should_return_true_when_single_property_matches() + { + var command1 = new EpisodeSearchCommand { EpisodeId = 1 }; + var command2 = new EpisodeSearchCommand { EpisodeId = 1 }; + var comparer = new CommandEqualityComparer(); + + comparer.Equals(command1, command2).Should().BeTrue(); + } + + [Test] + public void should_return_true_when_multiple_properties_match() + { + var command1 = new SeasonSearchCommand { SeriesId = 1, SeasonNumber = 1 }; + var command2 = new SeasonSearchCommand { SeriesId = 1, SeasonNumber = 1 }; + var comparer = new CommandEqualityComparer(); + + comparer.Equals(command1, command2).Should().BeTrue(); + } + + [Test] + public void should_return_false_when_single_property_doesnt_match() + { + var command1 = new EpisodeSearchCommand { EpisodeId = 1 }; + var command2 = new EpisodeSearchCommand { EpisodeId = 2 }; + var comparer = new CommandEqualityComparer(); + + comparer.Equals(command1, command2).Should().BeFalse(); + } + + [Test] + public void should_return_false_when_only_one_property_matches() + { + var command1 = new SeasonSearchCommand { SeriesId = 1, SeasonNumber = 1 }; + var command2 = new SeasonSearchCommand { SeriesId = 1, SeasonNumber = 2 }; + var comparer = new CommandEqualityComparer(); + + comparer.Equals(command1, command2).Should().BeFalse(); + } + + [Test] + public void should_return_false_when_no_properties_match() + { + var command1 = new SeasonSearchCommand { SeriesId = 1, SeasonNumber = 1 }; + var command2 = new SeasonSearchCommand { SeriesId = 2, SeasonNumber = 2 }; + var comparer = new CommandEqualityComparer(); + + comparer.Equals(command1, command2).Should().BeFalse(); + } + } +} diff --git a/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index 4c22a7aeb..8fd34cbf6 100644 --- a/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -69,6 +69,7 @@ + diff --git a/NzbDrone.Common/Cache/CacheManger.cs b/NzbDrone.Common/Cache/CacheManger.cs index d264eeb40..b5da3047c 100644 --- a/NzbDrone.Common/Cache/CacheManger.cs +++ b/NzbDrone.Common/Cache/CacheManger.cs @@ -28,7 +28,6 @@ namespace NzbDrone.Common.Cache return GetCache(host, host.FullName); } - public void Clear() { _cache.Clear(); diff --git a/NzbDrone.Common/Cache/Cached.cs b/NzbDrone.Common/Cache/Cached.cs index eef89f317..7269a9a53 100644 --- a/NzbDrone.Common/Cache/Cached.cs +++ b/NzbDrone.Common/Cache/Cached.cs @@ -61,6 +61,12 @@ namespace NzbDrone.Common.Cache return value.Object; } + public void Remove(string key) + { + CacheItem value; + _store.TryRemove(key, out value); + } + public T Get(string key, Func function, TimeSpan? lifeTime = null) { Ensure.That(() => key).IsNotNullOrWhiteSpace(); @@ -81,7 +87,6 @@ namespace NzbDrone.Common.Cache return value; } - public void Clear() { _store.Clear(); diff --git a/NzbDrone.Common/Cache/ICached.cs b/NzbDrone.Common/Cache/ICached.cs index 3708b72af..1c9e50812 100644 --- a/NzbDrone.Common/Cache/ICached.cs +++ b/NzbDrone.Common/Cache/ICached.cs @@ -13,6 +13,7 @@ namespace NzbDrone.Common.Cache void Set(string key, T value, TimeSpan? lifetime = null); T Get(string key, Func function, TimeSpan? lifeTime = null); T Find(string key); + void Remove(string key); ICollection Values { get; } } diff --git a/NzbDrone.Common/HashUtil.cs b/NzbDrone.Common/HashUtil.cs index 0a127ee63..5777cc724 100644 --- a/NzbDrone.Common/HashUtil.cs +++ b/NzbDrone.Common/HashUtil.cs @@ -34,31 +34,9 @@ namespace NzbDrone.Common return String.Format("{0:x8}", mCrc); } - public static string GenerateUserId() + public static string GenerateCommandId() { - return GenerateId("u"); - } - - public static string GenerateAppId() - { - return GenerateId("a"); - } - - public static string GenerateApiToken() - { - return Guid.NewGuid().ToString().Replace("-", ""); - } - - public static string GenerateSecurityToken(int length) - { - var byteSize = (length / 4) * 3; - - var linkBytes = new byte[byteSize]; - var rngCrypto = new RNGCryptoServiceProvider(); - rngCrypto.GetBytes(linkBytes); - var base64String = Convert.ToBase64String(linkBytes); - - return base64String; + return GenerateId("c"); } private static string GenerateId(string prefix) diff --git a/NzbDrone.Common/Instrumentation/LogEventExtensions.cs b/NzbDrone.Common/Instrumentation/LogEventExtensions.cs index cb2fe44f2..373aa9201 100644 --- a/NzbDrone.Common/Instrumentation/LogEventExtensions.cs +++ b/NzbDrone.Common/Instrumentation/LogEventExtensions.cs @@ -13,7 +13,6 @@ namespace NzbDrone.Common.Instrumentation return HashUtil.CalculateCrc(hashSeed); } - public static string GetFormattedMessage(this LogEventInfo logEvent) { var message = logEvent.FormattedMessage; diff --git a/NzbDrone.Common/Instrumentation/LoggerExtensions.cs b/NzbDrone.Common/Instrumentation/LoggerExtensions.cs new file mode 100644 index 000000000..9816965c7 --- /dev/null +++ b/NzbDrone.Common/Instrumentation/LoggerExtensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Messaging.Tracking; + +namespace NzbDrone.Common.Instrumentation +{ + public static class LoggerExtensions + { + public static void Complete(this Logger logger, string message) + { + var logEvent = new LogEventInfo(LogLevel.Info, logger.Name, message); + logEvent.Properties.Add("Status", ProcessState.Completed); + + logger.Log(logEvent); + } + + public static void Complete(this Logger logger, string message, params object[] args) + { + var formattedMessage = String.Format(message, args); + Complete(logger, formattedMessage); + } + + public static void Failed(this Logger logger, string message) + { + var logEvent = new LogEventInfo(LogLevel.Info, logger.Name, message); + logEvent.Properties.Add("Status", ProcessState.Failed); + + logger.Log(logEvent); + } + + public static void Failed(this Logger logger, string message, params object[] args) + { + var formattedMessage = String.Format(message, args); + Failed(logger, formattedMessage); + } + } +} diff --git a/NzbDrone.Common/Messaging/CommandCompletedEvent.cs b/NzbDrone.Common/Messaging/CommandCompletedEvent.cs deleted file mode 100644 index 613800ae0..000000000 --- a/NzbDrone.Common/Messaging/CommandCompletedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace NzbDrone.Common.Messaging -{ - public class CommandCompletedEvent : IEvent - { - public ICommand Command { get; private set; } - - public CommandCompletedEvent(ICommand command) - { - Command = command; - } - } -} \ No newline at end of file diff --git a/NzbDrone.Common/Messaging/CommandEqualityComparer.cs b/NzbDrone.Common/Messaging/CommandEqualityComparer.cs new file mode 100644 index 000000000..390319704 --- /dev/null +++ b/NzbDrone.Common/Messaging/CommandEqualityComparer.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.EnvironmentInfo; + +namespace NzbDrone.Common.Messaging +{ + public class CommandEqualityComparer : IEqualityComparer + { + public bool Equals(ICommand x, ICommand y) + { + var xProperties = x.GetType().GetProperties(); + var yProperties = y.GetType().GetProperties(); + + foreach (var xProperty in xProperties) + { + if (xProperty.Name == "CommandId") + { + continue; + } + + var yProperty = yProperties.SingleOrDefault(p => p.Name == xProperty.Name); + + if (yProperty == null) + { + continue; + } + + if (!xProperty.GetValue(x, null).Equals(yProperty.GetValue(y, null))) + { + return false; + } + } + + return true; + } + + public int GetHashCode(ICommand obj) + { + return obj.CommandId.GetHashCode(); + } + } +} diff --git a/NzbDrone.Common/Messaging/CommandFailedEvent.cs b/NzbDrone.Common/Messaging/CommandFailedEvent.cs deleted file mode 100644 index d33ab79f8..000000000 --- a/NzbDrone.Common/Messaging/CommandFailedEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; - -namespace NzbDrone.Common.Messaging -{ - public class CommandFailedEvent : IEvent - { - public ICommand Command { get; private set; } - public Exception Exception { get; private set; } - - public CommandFailedEvent(ICommand command, Exception exception) - { - Command = command; - Exception = exception; - } - } -} \ No newline at end of file diff --git a/NzbDrone.Common/Messaging/CommandStartedEvent.cs b/NzbDrone.Common/Messaging/CommandStartedEvent.cs deleted file mode 100644 index 3cb4e7f55..000000000 --- a/NzbDrone.Common/Messaging/CommandStartedEvent.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace NzbDrone.Common.Messaging -{ - public class CommandExecutedEvent : IEvent - { - public ICommand Command { get; private set; } - - public CommandExecutedEvent(ICommand command) - { - Command = command; - } - } -} \ No newline at end of file diff --git a/NzbDrone.Common/Messaging/Events/CommandCompletedEvent.cs b/NzbDrone.Common/Messaging/Events/CommandCompletedEvent.cs new file mode 100644 index 000000000..f4831361e --- /dev/null +++ b/NzbDrone.Common/Messaging/Events/CommandCompletedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging.Tracking; + +namespace NzbDrone.Common.Messaging.Events +{ + public class CommandCompletedEvent : IEvent + { + public TrackedCommand TrackedCommand { get; private set; } + + public CommandCompletedEvent(TrackedCommand trackedCommand) + { + TrackedCommand = trackedCommand; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Messaging/Events/CommandExecutedEvent.cs b/NzbDrone.Common/Messaging/Events/CommandExecutedEvent.cs new file mode 100644 index 000000000..b93a5597c --- /dev/null +++ b/NzbDrone.Common/Messaging/Events/CommandExecutedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging.Tracking; + +namespace NzbDrone.Common.Messaging.Events +{ + public class CommandExecutedEvent : IEvent + { + public TrackedCommand TrackedCommand { get; private set; } + + public CommandExecutedEvent(TrackedCommand trackedCommand) + { + TrackedCommand = trackedCommand; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Messaging/Events/CommandFailedEvent.cs b/NzbDrone.Common/Messaging/Events/CommandFailedEvent.cs new file mode 100644 index 000000000..5749ca00a --- /dev/null +++ b/NzbDrone.Common/Messaging/Events/CommandFailedEvent.cs @@ -0,0 +1,17 @@ +using System; +using NzbDrone.Common.Messaging.Tracking; + +namespace NzbDrone.Common.Messaging.Events +{ + public class CommandFailedEvent : IEvent + { + public TrackedCommand TrackedCommand { get; private set; } + public Exception Exception { get; private set; } + + public CommandFailedEvent(TrackedCommand trackedCommand, Exception exception) + { + TrackedCommand = trackedCommand; + Exception = exception; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Messaging/Events/CommandStartedEvent.cs b/NzbDrone.Common/Messaging/Events/CommandStartedEvent.cs new file mode 100644 index 000000000..9247fbd45 --- /dev/null +++ b/NzbDrone.Common/Messaging/Events/CommandStartedEvent.cs @@ -0,0 +1,14 @@ +using NzbDrone.Common.Messaging.Tracking; + +namespace NzbDrone.Common.Messaging.Events +{ + public class CommandStartedEvent : IEvent + { + public TrackedCommand TrackedCommand { get; private set; } + + public CommandStartedEvent(TrackedCommand trackedCommand) + { + TrackedCommand = trackedCommand; + } + } +} \ No newline at end of file diff --git a/NzbDrone.Common/Messaging/ICommand.cs b/NzbDrone.Common/Messaging/ICommand.cs index d9f8049ba..e93f1ccaa 100644 --- a/NzbDrone.Common/Messaging/ICommand.cs +++ b/NzbDrone.Common/Messaging/ICommand.cs @@ -1,6 +1,10 @@ +using System; +using System.Collections.Generic; + namespace NzbDrone.Common.Messaging { public interface ICommand : IMessage { + String CommandId { get; } } } \ No newline at end of file diff --git a/NzbDrone.Common/Messaging/IMessageAggregator.cs b/NzbDrone.Common/Messaging/IMessageAggregator.cs index 6de5ac3c8..9edd5b165 100644 --- a/NzbDrone.Common/Messaging/IMessageAggregator.cs +++ b/NzbDrone.Common/Messaging/IMessageAggregator.cs @@ -1,4 +1,6 @@ -namespace NzbDrone.Common.Messaging +using NzbDrone.Common.Messaging.Tracking; + +namespace NzbDrone.Common.Messaging { /// /// Enables loosely-coupled publication of events. @@ -7,6 +9,8 @@ { void PublishEvent(TEvent @event) where TEvent : class, IEvent; void PublishCommand(TCommand command) where TCommand : class, ICommand; - void PublishCommand(string commandType); + void PublishCommand(string commandTypeName); + TrackedCommand PublishCommandAsync(TCommand command) where TCommand : class, ICommand; + TrackedCommand PublishCommandAsync(string commandTypeName); } } \ No newline at end of file diff --git a/NzbDrone.Common/Messaging/IProcessMessage.cs b/NzbDrone.Common/Messaging/IProcessMessage.cs index b008d9e9b..d22eeea64 100644 --- a/NzbDrone.Common/Messaging/IProcessMessage.cs +++ b/NzbDrone.Common/Messaging/IProcessMessage.cs @@ -4,7 +4,6 @@ public interface IProcessMessageAsync : IProcessMessage { } - public interface IProcessMessage : IProcessMessage { } public interface IProcessMessageAsync : IProcessMessageAsync { } diff --git a/NzbDrone.Common/Messaging/MessageAggregator.cs b/NzbDrone.Common/Messaging/MessageAggregator.cs index 725f84ccf..b8ebd3690 100644 --- a/NzbDrone.Common/Messaging/MessageAggregator.cs +++ b/NzbDrone.Common/Messaging/MessageAggregator.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Threading.Tasks; using NLog; using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Messaging.Events; +using NzbDrone.Common.Messaging.Tracking; using NzbDrone.Common.Serializer; using NzbDrone.Common.TPL; @@ -13,12 +15,14 @@ namespace NzbDrone.Common.Messaging { private readonly Logger _logger; private readonly IServiceFactory _serviceFactory; + private readonly ITrackCommands _trackCommands; private readonly TaskFactory _taskFactory; - public MessageAggregator(Logger logger, IServiceFactory serviceFactory) + public MessageAggregator(Logger logger, IServiceFactory serviceFactory, ITrackCommands trackCommands) { _logger = logger; _serviceFactory = serviceFactory; + _trackCommands = trackCommands; var scheduler = new LimitedConcurrencyLevelTaskScheduler(2); _taskFactory = new TaskFactory(scheduler); } @@ -60,7 +64,6 @@ namespace NzbDrone.Common.Messaging } } - private static string GetEventName(Type eventType) { if (!eventType.IsGenericType) @@ -71,15 +74,69 @@ namespace NzbDrone.Common.Messaging return string.Format("{0}<{1}>", eventType.Name.Remove(eventType.Name.IndexOf('`')), eventType.GetGenericArguments()[0].Name); } - public void PublishCommand(TCommand command) where TCommand : class, ICommand { Ensure.That(() => command).IsNotNull(); - var handlerContract = typeof(IExecute<>).MakeGenericType(command.GetType()); + _logger.Trace("Publishing {0}", command.GetType().Name); + + var trackedCommand = _trackCommands.TrackIfNew(command); + + if (trackedCommand == null) + { + _logger.Info("Command is already in progress: {0}", command.GetType().Name); + return; + } + + ExecuteCommand(trackedCommand); + } + + public void PublishCommand(string commandTypeName) + { + dynamic command = GetCommand(commandTypeName); + PublishCommand(command); + } + + public TrackedCommand PublishCommandAsync(TCommand command) where TCommand : class, ICommand + { + Ensure.That(() => command).IsNotNull(); _logger.Trace("Publishing {0}", command.GetType().Name); + var existingCommand = _trackCommands.TrackNewOrGet(command); + + if (existingCommand.Existing) + { + _logger.Info("Command is already in progress: {0}", command.GetType().Name); + return existingCommand.TrackedCommand; + } + + _taskFactory.StartNew(() => ExecuteCommand(existingCommand.TrackedCommand) + , TaskCreationOptions.PreferFairness) + .LogExceptions(); + + return existingCommand.TrackedCommand; + } + + public TrackedCommand PublishCommandAsync(string commandTypeName) + { + dynamic command = GetCommand(commandTypeName); + return PublishCommandAsync(command); + } + + private dynamic GetCommand(string commandTypeName) + { + var commandType = _serviceFactory.GetImplementations(typeof(ICommand)) + .Single(c => c.FullName.Equals(commandTypeName, StringComparison.InvariantCultureIgnoreCase)); + + return Json.Deserialize("{}", commandType); + } + + private void ExecuteCommand(TrackedCommand trackedCommand) where TCommand : class, ICommand + { + var command = (TCommand)trackedCommand.Command; + + var handlerContract = typeof(IExecute<>).MakeGenericType(command.GetType()); var handler = (IExecute)_serviceFactory.Build(handlerContract); _logger.Debug("{0} -> {1}", command.GetType().Name, handler.GetType().Name); @@ -88,30 +145,27 @@ namespace NzbDrone.Common.Messaging try { + MappedDiagnosticsContext.Set("CommandId", trackedCommand.Command.CommandId); + + PublishEvent(new CommandStartedEvent(trackedCommand)); handler.Execute(command); sw.Stop(); - PublishEvent(new CommandCompletedEvent(command)); + + _trackCommands.Completed(trackedCommand, sw.Elapsed); + PublishEvent(new CommandCompletedEvent(trackedCommand)); } catch (Exception e) { - PublishEvent(new CommandFailedEvent(command, e)); + _trackCommands.Failed(trackedCommand, e); + PublishEvent(new CommandFailedEvent(trackedCommand, e)); throw; } finally { - PublishEvent(new CommandExecutedEvent(command)); + PublishEvent(new CommandExecutedEvent(trackedCommand)); } _logger.Debug("{0} <- {1} [{2}]", command.GetType().Name, handler.GetType().Name, sw.Elapsed.ToString("")); } - - public void PublishCommand(string commandTypeName) - { - var commandType = _serviceFactory.GetImplementations(typeof(ICommand)) - .Single(c => c.FullName.Equals(commandTypeName, StringComparison.InvariantCultureIgnoreCase)); - - dynamic command = Json.Deserialize("{}", commandType); - PublishCommand(command); - } } } diff --git a/NzbDrone.Common/Messaging/TestCommand.cs b/NzbDrone.Common/Messaging/TestCommand.cs index 1a54d5764..c24a89780 100644 --- a/NzbDrone.Common/Messaging/TestCommand.cs +++ b/NzbDrone.Common/Messaging/TestCommand.cs @@ -1,13 +1,15 @@ -namespace NzbDrone.Common.Messaging +using System; + +namespace NzbDrone.Common.Messaging { public class TestCommand : ICommand { + public int Duration { get; set; } + public String CommandId { get; private set; } + public TestCommand() { Duration = 4000; } - - public int Duration { get; set; } - } } \ No newline at end of file diff --git a/NzbDrone.Common/Messaging/Tracking/CommandTrackingService.cs b/NzbDrone.Common/Messaging/Tracking/CommandTrackingService.cs new file mode 100644 index 000000000..27c0c8bf1 --- /dev/null +++ b/NzbDrone.Common/Messaging/Tracking/CommandTrackingService.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.Remoting; +using NzbDrone.Common.Cache; + +namespace NzbDrone.Common.Messaging.Tracking +{ + public interface ITrackCommands + { + TrackedCommand TrackIfNew(ICommand command); + ExistingCommand TrackNewOrGet(ICommand command); + TrackedCommand Completed(TrackedCommand trackedCommand, TimeSpan runtime); + TrackedCommand Failed(TrackedCommand trackedCommand, Exception e); + List AllTracked(); + Boolean ExistingCommand(ICommand command); + TrackedCommand FindExisting(ICommand command); + } + + public class TrackCommands : ITrackCommands, IExecute + { + private readonly ICached _cache; + + public TrackCommands(ICacheManger cacheManger) + { + _cache = cacheManger.GetCache(GetType()); + } + + public TrackedCommand TrackIfNew(ICommand command) + { + if (ExistingCommand(command)) + { + return null; + } + + var trackedCommand = new TrackedCommand(command, ProcessState.Running); + Store(trackedCommand); + + return trackedCommand; + } + + public ExistingCommand TrackNewOrGet(ICommand command) + { + var trackedCommand = FindExisting(command); + + if (trackedCommand == null) + { + trackedCommand = new TrackedCommand(command, ProcessState.Running); + Store(trackedCommand); + + return new ExistingCommand(false, trackedCommand); + } + + return new ExistingCommand(true, trackedCommand); + } + + public TrackedCommand Completed(TrackedCommand trackedCommand, TimeSpan runtime) + { + trackedCommand.StateChangeTime = DateTime.UtcNow; + trackedCommand.State = ProcessState.Completed; + trackedCommand.Runtime = runtime; + + Store(trackedCommand); + + return trackedCommand; + } + + public TrackedCommand Failed(TrackedCommand trackedCommand, Exception e) + { + trackedCommand.StateChangeTime = DateTime.UtcNow; + trackedCommand.State = ProcessState.Failed; + trackedCommand.Exception = e; + + Store(trackedCommand); + + return trackedCommand; + } + + public List AllTracked() + { + return _cache.Values.ToList(); + } + + public bool ExistingCommand(ICommand command) + { + return FindExisting(command) != null; + } + + public TrackedCommand FindExisting(ICommand command) + { + var comparer = new CommandEqualityComparer(); + return Running(command.GetType()).SingleOrDefault(t => comparer.Equals(t.Command, command)); + } + + private List Running(Type type = null) + { + var running = AllTracked().Where(i => i.State == ProcessState.Running); + + if (type != null) + { + return running.Where(t => t.Type == type.FullName).ToList(); + } + + return running.ToList(); + } + + private void Store(TrackedCommand trackedCommand) + { + if (trackedCommand.Command.GetType() == typeof(TrackedCommandCleanupCommand)) + { + return; + } + + _cache.Set(trackedCommand.Command.CommandId, trackedCommand); + } + + public void Execute(TrackedCommandCleanupCommand message) + { + var old = AllTracked().Where(c => c.State != ProcessState.Running && c.StateChangeTime < DateTime.UtcNow.AddMinutes(-5)); + + foreach (var trackedCommand in old) + { + _cache.Remove(trackedCommand.Command.CommandId); + } + } + } +} diff --git a/NzbDrone.Common/Messaging/Tracking/ExistingCommand.cs b/NzbDrone.Common/Messaging/Tracking/ExistingCommand.cs new file mode 100644 index 000000000..7005585ef --- /dev/null +++ b/NzbDrone.Common/Messaging/Tracking/ExistingCommand.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common.Messaging.Tracking +{ + public class ExistingCommand + { + public Boolean Existing { get; set; } + public TrackedCommand TrackedCommand { get; set; } + + public ExistingCommand(Boolean exisitng, TrackedCommand trackedCommand) + { + Existing = exisitng; + TrackedCommand = trackedCommand; + } + } +} diff --git a/NzbDrone.Common/Messaging/Tracking/ProcessState.cs b/NzbDrone.Common/Messaging/Tracking/ProcessState.cs new file mode 100644 index 000000000..dc79c37f4 --- /dev/null +++ b/NzbDrone.Common/Messaging/Tracking/ProcessState.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common.Messaging.Tracking +{ + public enum ProcessState + { + Running, + Completed, + Failed + } +} diff --git a/NzbDrone.Common/Messaging/Tracking/TrackedCommand.cs b/NzbDrone.Common/Messaging/Tracking/TrackedCommand.cs new file mode 100644 index 000000000..35b0e05ac --- /dev/null +++ b/NzbDrone.Common/Messaging/Tracking/TrackedCommand.cs @@ -0,0 +1,30 @@ +using System; + +namespace NzbDrone.Common.Messaging.Tracking +{ + public class TrackedCommand + { + public String Id { get; private set; } + public String Name { get; private set; } + public String Type { get; private set; } + public ICommand Command { get; private set; } + public ProcessState State { get; set; } + public DateTime StateChangeTime { get; set; } + public TimeSpan Runtime { get; set; } + public Exception Exception { get; set; } + + public TrackedCommand() + { + } + + public TrackedCommand(ICommand command, ProcessState state) + { + Id = command.CommandId; + Name = command.GetType().Name; + Type = command.GetType().FullName; + Command = command; + State = state; + StateChangeTime = DateTime.UtcNow; + } + } +} diff --git a/NzbDrone.Common/Messaging/Tracking/TrackedCommandCleanupCommand.cs b/NzbDrone.Common/Messaging/Tracking/TrackedCommandCleanupCommand.cs new file mode 100644 index 000000000..830ba7fb5 --- /dev/null +++ b/NzbDrone.Common/Messaging/Tracking/TrackedCommandCleanupCommand.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common.Messaging.Tracking +{ + public class TrackedCommandCleanupCommand : ICommand + { + public string CommandId { get; private set; } + + public TrackedCommandCleanupCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } + } +} diff --git a/NzbDrone.Common/NzbDrone.Common.csproj b/NzbDrone.Common/NzbDrone.Common.csproj index e392e51bb..6a4006315 100644 --- a/NzbDrone.Common/NzbDrone.Common.csproj +++ b/NzbDrone.Common/NzbDrone.Common.csproj @@ -94,6 +94,14 @@ + + + + + + + + @@ -104,9 +112,9 @@ - - - + + + diff --git a/NzbDrone.Core.Test/ParserTests/ParserFixture.cs b/NzbDrone.Core.Test/ParserTests/ParserFixture.cs index 192ff7380..ac797bba8 100644 --- a/NzbDrone.Core.Test/ParserTests/ParserFixture.cs +++ b/NzbDrone.Core.Test/ParserTests/ParserFixture.cs @@ -80,6 +80,7 @@ namespace NzbDrone.Core.Test.ParserTests [TestCase("Top_Gear.19x06.720p_HDTV_x264-FoV", "Top Gear", 19, 6)] [TestCase("Portlandia.S03E10.Alexandra.720p.WEB-DL.AAC2.0.H.264-CROM.mkv", "Portlandia", 3, 10)] [TestCase("(Game of Thrones s03 e - \"Game of Thrones Season 3 Episode 10\"", "Game of Thrones", 3, 10)] + [TestCase("House.Hunters.International.S05E607.720p.hdtv.x264", "House.Hunters.International", 5, 607)] public void ParseTitle_single(string postTitle, string title, int seasonNumber, int episodeNumber) { var result = Parser.Parser.ParseTitle(postTitle); diff --git a/NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs b/NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs index 68356ab6e..8b886d009 100644 --- a/NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs +++ b/NzbDrone.Core/DataAugmentation/Scene/UpdateSceneMappingCommand.cs @@ -1,8 +1,16 @@ +using System; +using NzbDrone.Common; using NzbDrone.Common.Messaging; namespace NzbDrone.Core.DataAugmentation.Scene { public class UpdateSceneMappingCommand : ICommand { + public String CommandId { get; private set; } + + public UpdateSceneMappingCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs b/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs index b0dce2fd1..720955178 100644 --- a/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs +++ b/NzbDrone.Core/IndexerSearch/EpisodeSearchCommand.cs @@ -1,9 +1,17 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.IndexerSearch { public class EpisodeSearchCommand : ICommand { + public String CommandId { get; private set; } public int EpisodeId { get; set; } + + public EpisodeSearchCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/IndexerSearch/SeasonSearchCommand.cs b/NzbDrone.Core/IndexerSearch/SeasonSearchCommand.cs index cf9a8ba3e..749fefae9 100644 --- a/NzbDrone.Core/IndexerSearch/SeasonSearchCommand.cs +++ b/NzbDrone.Core/IndexerSearch/SeasonSearchCommand.cs @@ -1,10 +1,18 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.IndexerSearch { public class SeasonSearchCommand : ICommand { + public String CommandId { get; private set; } public int SeriesId { get; set; } public int SeasonNumber { get; set; } + + public SeasonSearchCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/IndexerSearch/SeriesSearchCommand.cs b/NzbDrone.Core/IndexerSearch/SeriesSearchCommand.cs index 4e309fde8..8a8de6184 100644 --- a/NzbDrone.Core/IndexerSearch/SeriesSearchCommand.cs +++ b/NzbDrone.Core/IndexerSearch/SeriesSearchCommand.cs @@ -1,9 +1,17 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.IndexerSearch { public class SeriesSearchCommand : ICommand { + public String CommandId { get; private set; } public int SeriesId { get; set; } + + public SeriesSearchCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/Indexers/RssSyncCommand.cs b/NzbDrone.Core/Indexers/RssSyncCommand.cs index 04a7a123b..0d073c977 100644 --- a/NzbDrone.Core/Indexers/RssSyncCommand.cs +++ b/NzbDrone.Core/Indexers/RssSyncCommand.cs @@ -1,9 +1,16 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Indexers { public class RssSyncCommand : ICommand { + public String CommandId { get; private set; } + public RssSyncCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/Indexers/RssSyncService.cs b/NzbDrone.Core/Indexers/RssSyncService.cs index baf414fac..dcbf340b1 100644 --- a/NzbDrone.Core/Indexers/RssSyncService.cs +++ b/NzbDrone.Core/Indexers/RssSyncService.cs @@ -1,5 +1,6 @@ using System.Linq; using NLog; +using NzbDrone.Common.Instrumentation; using NzbDrone.Common.Messaging; using NzbDrone.Core.DecisionEngine; using NzbDrone.Core.Download; @@ -38,7 +39,7 @@ namespace NzbDrone.Core.Indexers var decisions = _downloadDecisionMaker.GetRssDecision(reports); var qualifiedReports = _downloadApprovedReports.DownloadApproved(decisions); - _logger.Info("RSS Sync Completed. Reports found: {0}, Reports downloaded: {1}", reports.Count, qualifiedReports.Count()); + _logger.Complete("RSS Sync Completed. Reports found: {0}, Reports downloaded: {1}", reports.Count, qualifiedReports.Count()); } public void Execute(RssSyncCommand message) diff --git a/NzbDrone.Core/Instrumentation/Commands/ClearLogCommand.cs b/NzbDrone.Core/Instrumentation/Commands/ClearLogCommand.cs index 19776e76d..cabaffcb5 100644 --- a/NzbDrone.Core/Instrumentation/Commands/ClearLogCommand.cs +++ b/NzbDrone.Core/Instrumentation/Commands/ClearLogCommand.cs @@ -1,8 +1,16 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Instrumentation.Commands { public class ClearLogCommand : ICommand { + public String CommandId { get; private set; } + + public ClearLogCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/Instrumentation/Commands/DeleteLogFilesCommand.cs b/NzbDrone.Core/Instrumentation/Commands/DeleteLogFilesCommand.cs index 5d3228afb..8ae4d03d1 100644 --- a/NzbDrone.Core/Instrumentation/Commands/DeleteLogFilesCommand.cs +++ b/NzbDrone.Core/Instrumentation/Commands/DeleteLogFilesCommand.cs @@ -1,8 +1,16 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Instrumentation.Commands { public class DeleteLogFilesCommand : ICommand { + public String CommandId { get; private set; } + + public DeleteLogFilesCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/Instrumentation/Commands/TrimLogCommand.cs b/NzbDrone.Core/Instrumentation/Commands/TrimLogCommand.cs index c00b27020..7e710f294 100644 --- a/NzbDrone.Core/Instrumentation/Commands/TrimLogCommand.cs +++ b/NzbDrone.Core/Instrumentation/Commands/TrimLogCommand.cs @@ -1,8 +1,16 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Instrumentation.Commands { public class TrimLogCommand : ICommand { + public String CommandId { get; private set; } + + public TrimLogCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/Jobs/TaskManager.cs b/NzbDrone.Core/Jobs/TaskManager.cs index 4c82da90a..4dcfdd56c 100644 --- a/NzbDrone.Core/Jobs/TaskManager.cs +++ b/NzbDrone.Core/Jobs/TaskManager.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using NLog; using NzbDrone.Common.Messaging; +using NzbDrone.Common.Messaging.Events; +using NzbDrone.Common.Messaging.Tracking; using NzbDrone.Core.Configuration; using NzbDrone.Core.Configuration.Events; using NzbDrone.Core.Indexers; @@ -47,7 +49,8 @@ namespace NzbDrone.Core.Jobs new ScheduledTask{ Interval = 12*60, TypeName = typeof(RefreshSeriesCommand).FullName}, new ScheduledTask{ Interval = 1, TypeName = typeof(DownloadedEpisodesScanCommand).FullName}, new ScheduledTask{ Interval = 60, TypeName = typeof(ApplicationUpdateCommand).FullName}, - new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName} + new ScheduledTask{ Interval = 1*60, TypeName = typeof(TrimLogCommand).FullName}, + new ScheduledTask{ Interval = 1, TypeName = typeof(TrackedCommandCleanupCommand).FullName} }; var currentTasks = _scheduledTaskRepository.All(); @@ -76,7 +79,7 @@ namespace NzbDrone.Core.Jobs public void HandleAsync(CommandExecutedEvent message) { - var scheduledTask = _scheduledTaskRepository.All().SingleOrDefault(c => c.TypeName == message.Command.GetType().FullName); + var scheduledTask = _scheduledTaskRepository.All().SingleOrDefault(c => c.TypeName == message.TrackedCommand.Command.GetType().FullName); if (scheduledTask != null) { diff --git a/NzbDrone.Core/Lifecycle/ApplicationStartedEvent.cs b/NzbDrone.Core/Lifecycle/ApplicationStartedEvent.cs index 985986b05..f66622dd3 100644 --- a/NzbDrone.Core/Lifecycle/ApplicationStartedEvent.cs +++ b/NzbDrone.Core/Lifecycle/ApplicationStartedEvent.cs @@ -6,5 +6,4 @@ namespace NzbDrone.Core.Lifecycle { } - } \ No newline at end of file diff --git a/NzbDrone.Core/MediaFiles/Commands/CleanMediaFileDb.cs b/NzbDrone.Core/MediaFiles/Commands/CleanMediaFileDb.cs index b873dcc5f..7a195685a 100644 --- a/NzbDrone.Core/MediaFiles/Commands/CleanMediaFileDb.cs +++ b/NzbDrone.Core/MediaFiles/Commands/CleanMediaFileDb.cs @@ -1,13 +1,22 @@ +using System; +using NzbDrone.Common; using NzbDrone.Common.Messaging; namespace NzbDrone.Core.MediaFiles.Commands { public class CleanMediaFileDb : ICommand { + public String CommandId { get; private set; } public int SeriesId { get; private set; } + public CleanMediaFileDb() + { + CommandId = HashUtil.GenerateCommandId(); + } + public CleanMediaFileDb(int seriesId) { + CommandId = HashUtil.GenerateCommandId(); SeriesId = seriesId; } } diff --git a/NzbDrone.Core/MediaFiles/Commands/CleanUpRecycleBinCommand.cs b/NzbDrone.Core/MediaFiles/Commands/CleanUpRecycleBinCommand.cs index b2d16f231..ef27ad213 100644 --- a/NzbDrone.Core/MediaFiles/Commands/CleanUpRecycleBinCommand.cs +++ b/NzbDrone.Core/MediaFiles/Commands/CleanUpRecycleBinCommand.cs @@ -1,8 +1,16 @@ +using System; +using NzbDrone.Common; using NzbDrone.Common.Messaging; namespace NzbDrone.Core.MediaFiles.Commands { public class CleanUpRecycleBinCommand : ICommand { + public String CommandId { get; private set; } + + public CleanUpRecycleBinCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs b/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs index 0f03f2083..a6caf5bee 100644 --- a/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs +++ b/NzbDrone.Core/MediaFiles/Commands/DownloadedEpisodesScanCommand.cs @@ -1,11 +1,16 @@ +using System; +using NzbDrone.Common; using NzbDrone.Common.Messaging; namespace NzbDrone.Core.MediaFiles.Commands { public class DownloadedEpisodesScanCommand : ICommand { + public String CommandId { get; private set; } + public DownloadedEpisodesScanCommand() { + CommandId = HashUtil.GenerateCommandId(); } } } \ No newline at end of file diff --git a/NzbDrone.Core/MediaFiles/Commands/RenameSeasonCommand.cs b/NzbDrone.Core/MediaFiles/Commands/RenameSeasonCommand.cs index 723c5d74b..5a61e100a 100644 --- a/NzbDrone.Core/MediaFiles/Commands/RenameSeasonCommand.cs +++ b/NzbDrone.Core/MediaFiles/Commands/RenameSeasonCommand.cs @@ -1,14 +1,24 @@ +using System; +using NzbDrone.Common; using NzbDrone.Common.Messaging; namespace NzbDrone.Core.MediaFiles.Commands { public class RenameSeasonCommand : ICommand { - public int SeriesId { get; private set; } - public int SeasonNumber { get; private set; } + public int SeriesId { get; set; } + public int SeasonNumber { get; set; } + + public String CommandId { get; private set; } + + public RenameSeasonCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } public RenameSeasonCommand(int seriesId, int seasonNumber) { + CommandId = HashUtil.GenerateCommandId(); SeriesId = seriesId; SeasonNumber = seasonNumber; } diff --git a/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs b/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs index 7716c43c0..f7e99512f 100644 --- a/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs +++ b/NzbDrone.Core/MediaFiles/Commands/RenameSeriesCommand.cs @@ -1,13 +1,22 @@ +using System; +using NzbDrone.Common; using NzbDrone.Common.Messaging; namespace NzbDrone.Core.MediaFiles.Commands { public class RenameSeriesCommand : ICommand { - public int SeriesId { get; private set; } + public String CommandId { get; private set; } + public int SeriesId { get; set; } + + public RenameSeriesCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } public RenameSeriesCommand(int seriesId) { + CommandId = HashUtil.GenerateCommandId(); SeriesId = seriesId; } } diff --git a/NzbDrone.Core/Notifications/Email/TestEmailCommand.cs b/NzbDrone.Core/Notifications/Email/TestEmailCommand.cs index 258884788..26bf91c94 100644 --- a/NzbDrone.Core/Notifications/Email/TestEmailCommand.cs +++ b/NzbDrone.Core/Notifications/Email/TestEmailCommand.cs @@ -1,9 +1,12 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Notifications.Email { public class TestEmailCommand : ICommand { + public String CommandId { get; private set; } public string Server { get; set; } public int Port { get; set; } public bool Ssl { get; set; } @@ -11,5 +14,10 @@ namespace NzbDrone.Core.Notifications.Email public string Password { get; set; } public string From { get; set; } public string To { get; set; } + + public TestEmailCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } diff --git a/NzbDrone.Core/Notifications/Growl/TestGrowlCommand.cs b/NzbDrone.Core/Notifications/Growl/TestGrowlCommand.cs index 35890fff9..8494e0d9d 100644 --- a/NzbDrone.Core/Notifications/Growl/TestGrowlCommand.cs +++ b/NzbDrone.Core/Notifications/Growl/TestGrowlCommand.cs @@ -1,11 +1,19 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Notifications.Growl { public class TestGrowlCommand : ICommand { + public String CommandId { get; private set; } public string Host { get; set; } public int Port { get; set; } public string Password { get; set; } + + public TestGrowlCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } diff --git a/NzbDrone.Core/Notifications/Plex/TestPlexClientCommand.cs b/NzbDrone.Core/Notifications/Plex/TestPlexClientCommand.cs index 6df162ab4..365add8a2 100644 --- a/NzbDrone.Core/Notifications/Plex/TestPlexClientCommand.cs +++ b/NzbDrone.Core/Notifications/Plex/TestPlexClientCommand.cs @@ -1,12 +1,20 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Notifications.Plex { public class TestPlexClientCommand : ICommand { + public String CommandId { get; private set; } public string Host { get; set; } public int Port { get; set; } public string Username { get; set; } public string Password { get; set; } + + public TestPlexClientCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } diff --git a/NzbDrone.Core/Notifications/Plex/TestPlexServerCommand.cs b/NzbDrone.Core/Notifications/Plex/TestPlexServerCommand.cs index 49089afea..7306e5a10 100644 --- a/NzbDrone.Core/Notifications/Plex/TestPlexServerCommand.cs +++ b/NzbDrone.Core/Notifications/Plex/TestPlexServerCommand.cs @@ -1,10 +1,18 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Notifications.Plex { public class TestPlexServerCommand : ICommand { + public String CommandId { get; private set; } public string Host { get; set; } public int Port { get; set; } + + public TestPlexServerCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } diff --git a/NzbDrone.Core/Notifications/Prowl/TestProwlCommand.cs b/NzbDrone.Core/Notifications/Prowl/TestProwlCommand.cs index e58bf5a9c..869123be0 100644 --- a/NzbDrone.Core/Notifications/Prowl/TestProwlCommand.cs +++ b/NzbDrone.Core/Notifications/Prowl/TestProwlCommand.cs @@ -1,10 +1,18 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Notifications.Prowl { public class TestProwlCommand : ICommand { + public String CommandId { get; private set; } public string ApiKey { get; set; } public int Priority { get; set; } + + public TestProwlCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } diff --git a/NzbDrone.Core/Notifications/Pushover/TestPushoverCommand.cs b/NzbDrone.Core/Notifications/Pushover/TestPushoverCommand.cs index 31bc034f9..0c6ec8912 100644 --- a/NzbDrone.Core/Notifications/Pushover/TestPushoverCommand.cs +++ b/NzbDrone.Core/Notifications/Pushover/TestPushoverCommand.cs @@ -1,10 +1,18 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Notifications.Pushover { public class TestPushoverCommand : ICommand { + public String CommandId { get; private set; } public string UserKey { get; set; } public int Priority { get; set; } + + public TestPushoverCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } diff --git a/NzbDrone.Core/Notifications/Xbmc/TestXbmcCommand.cs b/NzbDrone.Core/Notifications/Xbmc/TestXbmcCommand.cs index 56eb75ee8..02c594e8d 100644 --- a/NzbDrone.Core/Notifications/Xbmc/TestXbmcCommand.cs +++ b/NzbDrone.Core/Notifications/Xbmc/TestXbmcCommand.cs @@ -1,13 +1,21 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Notifications.Xbmc { public class TestXbmcCommand : ICommand { + public String CommandId { get; private set; } public string Host { get; set; } public int Port { get; set; } public string Username { get; set; } public string Password { get; set; } public int DisplayTime { get; set; } + + public TestXbmcCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } diff --git a/NzbDrone.Core/NzbDrone.Core.csproj b/NzbDrone.Core/NzbDrone.Core.csproj index 793db0f0a..2f39373e1 100644 --- a/NzbDrone.Core/NzbDrone.Core.csproj +++ b/NzbDrone.Core/NzbDrone.Core.csproj @@ -219,6 +219,9 @@ + + + diff --git a/NzbDrone.Core/ProgressMessaging/NewProgressMessageEvent.cs b/NzbDrone.Core/ProgressMessaging/NewProgressMessageEvent.cs new file mode 100644 index 000000000..0b2905312 --- /dev/null +++ b/NzbDrone.Core/ProgressMessaging/NewProgressMessageEvent.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Messaging; + +namespace NzbDrone.Core.ProgressMessaging +{ + public class NewProgressMessageEvent : IEvent + { + public ProgressMessage ProgressMessage { get; set; } + + public NewProgressMessageEvent(ProgressMessage progressMessage) + { + ProgressMessage = progressMessage; + } + } +} diff --git a/NzbDrone.Core/ProgressMessaging/ProgressMessage.cs b/NzbDrone.Core/ProgressMessaging/ProgressMessage.cs new file mode 100644 index 000000000..b9bc8fc6d --- /dev/null +++ b/NzbDrone.Core/ProgressMessaging/ProgressMessage.cs @@ -0,0 +1,13 @@ +using System; +using NzbDrone.Common.Messaging.Tracking; + +namespace NzbDrone.Core.ProgressMessaging +{ + public class ProgressMessage + { + public DateTime Time { get; set; } + public String CommandId { get; set; } + public String Message { get; set; } + public ProcessState Status { get; set; } + } +} diff --git a/NzbDrone.Core/ProgressMessaging/ProgressMessageTarget.cs b/NzbDrone.Core/ProgressMessaging/ProgressMessageTarget.cs new file mode 100644 index 000000000..ee99f7599 --- /dev/null +++ b/NzbDrone.Core/ProgressMessaging/ProgressMessageTarget.cs @@ -0,0 +1,86 @@ +using System; +using NLog.Config; +using NLog; +using NLog.Layouts; +using NLog.Targets; +using NzbDrone.Common.Messaging; +using NzbDrone.Common.Messaging.Tracking; +using NzbDrone.Core.Lifecycle; + +namespace NzbDrone.Core.ProgressMessaging +{ + + public class ProgressMessageTarget : TargetWithLayout, IHandle, IHandle + { + private readonly IMessageAggregator _messageAggregator; + public LoggingRule Rule { get; set; } + + public ProgressMessageTarget(IMessageAggregator messageAggregator) + { + _messageAggregator = messageAggregator; + } + + public void Register() + { + Layout = new SimpleLayout("${callsite:className=false:fileName=false:includeSourcePath=false:methodName=true}"); + + Rule = new LoggingRule("*", this); + Rule.EnableLoggingForLevel(LogLevel.Info); + + LogManager.Configuration.AddTarget("ProgressMessagingLogger", this); + LogManager.Configuration.LoggingRules.Add(Rule); + LogManager.ConfigurationReloaded += OnLogManagerOnConfigurationReloaded; + LogManager.ReconfigExistingLoggers(); + } + + public void UnRegister() + { + LogManager.ConfigurationReloaded -= OnLogManagerOnConfigurationReloaded; + LogManager.Configuration.RemoveTarget("ProgressMessagingLogger"); + LogManager.Configuration.LoggingRules.Remove(Rule); + LogManager.ReconfigExistingLoggers(); + Dispose(); + } + + private void OnLogManagerOnConfigurationReloaded(object sender, LoggingConfigurationReloadedEventArgs args) + { + Register(); + } + + protected override void Write(LogEventInfo logEvent) + { + var commandId = MappedDiagnosticsContext.Get("CommandId"); + + if (String.IsNullOrWhiteSpace(commandId)) + { + return; + } + + var status = logEvent.Properties.ContainsKey("Status") ? (ProcessState)logEvent.Properties["Status"] : ProcessState.Running; + + var message = new ProgressMessage(); + message.Time = logEvent.TimeStamp; + message.CommandId = commandId; + message.Message = logEvent.FormattedMessage; + message.Status = status; + + _messageAggregator.PublishEvent(new NewProgressMessageEvent(message)); + } + + public void Handle(ApplicationStartedEvent message) + { + if (!LogManager.Configuration.LoggingRules.Contains(Rule)) + { + Register(); + } + } + + public void Handle(ApplicationShutdownRequested message) + { + if (LogManager.Configuration.LoggingRules.Contains(Rule)) + { + UnRegister(); + } + } + } +} \ No newline at end of file diff --git a/NzbDrone.Core/Providers/UpdateXemMappingsCommand.cs b/NzbDrone.Core/Providers/UpdateXemMappingsCommand.cs index da4b4b53f..0dd28372d 100644 --- a/NzbDrone.Core/Providers/UpdateXemMappingsCommand.cs +++ b/NzbDrone.Core/Providers/UpdateXemMappingsCommand.cs @@ -1,13 +1,17 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Providers { public class UpdateXemMappingsCommand : ICommand { - public int? SeriesId { get; private set; } + public String CommandId { get; private set; } + public int? SeriesId { get; set; } public UpdateXemMappingsCommand(int? seriesId) { + CommandId = HashUtil.GenerateCommandId(); SeriesId = seriesId; } } diff --git a/NzbDrone.Core/Providers/XemProvider.cs b/NzbDrone.Core/Providers/XemProvider.cs index d2abb6776..0cb1aded7 100644 --- a/NzbDrone.Core/Providers/XemProvider.cs +++ b/NzbDrone.Core/Providers/XemProvider.cs @@ -132,8 +132,7 @@ namespace NzbDrone.Core.Providers catch (Exception ex) { - //TODO: We should increase this back to warn when caching is in place - _logger.TraceException("Error updating scene numbering mappings for: " + series, ex); + _logger.ErrorException("Error updating scene numbering mappings for: " + series, ex); } } diff --git a/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs b/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs index f38af963c..a58fca783 100644 --- a/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs +++ b/NzbDrone.Core/Tv/Commands/RefreshSeriesCommand.cs @@ -1,13 +1,23 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Tv.Commands { public class RefreshSeriesCommand : ICommand { - public int? SeriesId { get; private set; } + public String CommandId { get; private set; } + public int? SeriesId { get; set; } + + public RefreshSeriesCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } public RefreshSeriesCommand(int? seriesId) { + CommandId = HashUtil.GenerateCommandId(); + SeriesId = seriesId; } } diff --git a/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs b/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs index bbbf6a1f3..dd1b97d6b 100644 --- a/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs +++ b/NzbDrone.Core/Update/Commands/ApplicationUpdateCommand.cs @@ -1,8 +1,16 @@ -using NzbDrone.Common.Messaging; +using System; +using NzbDrone.Common; +using NzbDrone.Common.Messaging; namespace NzbDrone.Core.Update.Commands { public class ApplicationUpdateCommand : ICommand { + public String CommandId { get; private set; } + + public ApplicationUpdateCommand() + { + CommandId = HashUtil.GenerateCommandId(); + } } } \ No newline at end of file diff --git a/NzbDrone.Integration.Test/CommandIntegerationTests.cs b/NzbDrone.Integration.Test/CommandIntegerationTests.cs index c1e06bac1..af3342c2b 100644 --- a/NzbDrone.Integration.Test/CommandIntegerationTests.cs +++ b/NzbDrone.Integration.Test/CommandIntegerationTests.cs @@ -1,5 +1,10 @@ -using NUnit.Framework; +using System.Net; +using FluentAssertions; +using NUnit.Framework; using NzbDrone.Api.Commands; +using NzbDrone.Common.Messaging.Tracking; +using NzbDrone.Common.Serializer; +using RestSharp; namespace NzbDrone.Integration.Test { @@ -9,7 +14,27 @@ namespace NzbDrone.Integration.Test [Test] public void should_be_able_to_run_rss_sync() { - Commands.Post(new CommandResource {Command = "rsssync"}); + var request = new RestRequest("command") + { + RequestFormat = DataFormat.Json, + Method = Method.POST + }; + + request.AddBody(new CommandResource {Command = "rsssync"}); + + var restClient = new RestClient("http://localhost:8989/api"); + var response = restClient.Execute(request); + + if (response.ErrorException != null) + { + throw response.ErrorException; + } + + response.ErrorMessage.Should().BeBlank(); + response.StatusCode.Should().Be(HttpStatusCode.Created); + + var trackedCommand = Json.Deserialize(response.Content); + trackedCommand.Id.Should().NotBeNullOrEmpty(); } } } \ No newline at end of file diff --git a/UI/Commands/CommandCollection.js b/UI/Commands/CommandCollection.js new file mode 100644 index 000000000..72939381b --- /dev/null +++ b/UI/Commands/CommandCollection.js @@ -0,0 +1,17 @@ +'use strict'; +define( + [ + 'backbone', + 'Commands/CommandModel', + 'Mixins/backbone.signalr.mixin' + ], function (Backbone, CommandModel) { + + var CommandCollection = Backbone.Collection.extend({ + url : window.ApiRoot + '/command', + model: CommandModel + }); + + var collection = new CommandCollection().bindSignalR(); + + return collection; + }); diff --git a/UI/Commands/CommandModel.js b/UI/Commands/CommandModel.js new file mode 100644 index 000000000..33a6217c8 --- /dev/null +++ b/UI/Commands/CommandModel.js @@ -0,0 +1,8 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + }); + }); diff --git a/UI/Episode/Search/Layout.js b/UI/Episode/Search/Layout.js index 15b89f79a..aa24febf5 100644 --- a/UI/Episode/Search/Layout.js +++ b/UI/Episode/Search/Layout.js @@ -9,9 +9,9 @@ define( 'Series/SeriesCollection', 'Shared/LoadingView', 'Shared/Messenger', - 'Commands/CommandController', + 'Shared/Actioneer', 'Shared/FormatHelpers' - ], function (App, Marionette, ButtonsView, ManualSearchLayout, ReleaseCollection, SeriesCollection, LoadingView, Messenger, CommandController, FormatHelpers) { + ], function (App, Marionette, ButtonsView, ManualSearchLayout, ReleaseCollection, SeriesCollection, LoadingView, Messenger, Actioneer, FormatHelpers) { return Marionette.Layout.extend({ template: 'Episode/Search/LayoutTemplate', @@ -39,16 +39,19 @@ define( e.preventDefault(); } - CommandController.Execute('episodeSearch', { episodeId: this.model.get('id') }); - var series = SeriesCollection.get(this.model.get('seriesId')); var seriesTitle = series.get('title'); var season = this.model.get('seasonNumber'); var episode = this.model.get('episodeNumber'); var message = seriesTitle + ' - ' + season + 'x' + FormatHelpers.pad(episode, 2); - Messenger.show({ - message: 'Search started for: ' + message + Actioneer.ExecuteCommand({ + command : 'episodeSearch', + properties : { + episodeId: this.model.get('id') + }, + errorMessage: 'Search failed for: ' + message, + startMessage: 'Search started for: ' + message }); App.vent.trigger(App.Commands.CloseModalCommand); diff --git a/UI/ProgressMessaging/ProgressMessageCollection.js b/UI/ProgressMessaging/ProgressMessageCollection.js new file mode 100644 index 000000000..eb7317b4d --- /dev/null +++ b/UI/ProgressMessaging/ProgressMessageCollection.js @@ -0,0 +1,40 @@ +'use strict'; +define( + [ + 'backbone', + 'ProgressMessaging/ProgressMessageModel', + 'Shared/Messenger', + 'Mixins/backbone.signalr.mixin' + ], function (Backbone, ProgressMessageModel, Messenger) { + + var ProgressMessageCollection = Backbone.Collection.extend({ + url : window.ApiRoot + '/progressmessage', + model: ProgressMessageModel + }); + + var collection = new ProgressMessageCollection().bindSignalR(); + + collection.signalRconnection.received(function (message) { + + var type = getMessengerType(message.status); + + Messenger.show({ + id : message.commandId, + message: message.message, + type : type + }); + }); + + var getMessengerType = function (status) { + switch (status) { + case 'completed': + return 'success'; + case 'failed': + return 'error'; + default: + return 'info'; + } + }; + + return collection; + }); diff --git a/UI/ProgressMessaging/ProgressMessageModel.js b/UI/ProgressMessaging/ProgressMessageModel.js new file mode 100644 index 000000000..33a6217c8 --- /dev/null +++ b/UI/ProgressMessaging/ProgressMessageModel.js @@ -0,0 +1,8 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + }); + }); diff --git a/UI/Router.js b/UI/Router.js index 76513c471..4388c4f69 100644 --- a/UI/Router.js +++ b/UI/Router.js @@ -5,10 +5,12 @@ require( 'marionette', 'Controller', 'Series/SeriesCollection', + 'ProgressMessaging/ProgressMessageCollection', + 'Shared/Actioneer', 'Navbar/NavbarView', 'jQuery/RouteBinder', 'jquery' - ], function (App, Marionette, Controller, SeriesCollection, NavbarView, RouterBinder, $) { + ], function (App, Marionette, Controller, SeriesCollection, ProgressMessageCollection, Actioneer, NavbarView, RouterBinder, $) { var Router = Marionette.AppRouter.extend({ @@ -42,7 +44,7 @@ require( RouterBinder.bind(App.Router); App.navbarRegion.show(new NavbarView()); $('body').addClass('started'); - }) + }); }); return App.Router; diff --git a/UI/SeasonPass/SeriesLayout.js b/UI/SeasonPass/SeriesLayout.js index 5de36f404..fd4d6b985 100644 --- a/UI/SeasonPass/SeriesLayout.js +++ b/UI/SeasonPass/SeriesLayout.js @@ -113,6 +113,8 @@ define( }, _setMonitored: function (seasonNumber) { + //TODO: use Actioneer? + var self = this; var promise = $.ajax({ diff --git a/UI/Series/Details/SeasonLayout.js b/UI/Series/Details/SeasonLayout.js index ea6b76ceb..b5281541f 100644 --- a/UI/Series/Details/SeasonLayout.js +++ b/UI/Series/Details/SeasonLayout.js @@ -8,9 +8,8 @@ define( 'Cells/EpisodeTitleCell', 'Cells/RelativeDateCell', 'Cells/EpisodeStatusCell', - 'Commands/CommandController', 'Shared/Actioneer' - ], function (App, Marionette, Backgrid, ToggleCell, EpisodeTitleCell, RelativeDateCell, EpisodeStatusCell, CommandController, Actioneer) { + ], function (App, Marionette, Backgrid, ToggleCell, EpisodeTitleCell, RelativeDateCell, EpisodeStatusCell, Actioneer) { return Marionette.Layout.extend({ template: 'Series/Details/SeasonLayoutTemplate', @@ -103,9 +102,10 @@ define( seriesId : this.model.get('seriesId'), seasonNumber: this.model.get('seasonNumber') }, - element : this.ui.seasonSearch, - failMessage : 'Search for season {0} failed'.format(this.model.get('seasonNumber')), - startMessage: 'Search for season {0} started'.format(this.model.get('seasonNumber')) + element : this.ui.seasonSearch, + errorMessage : 'Search for season {0} failed'.format(this.model.get('seasonNumber')), + startMessage : 'Search for season {0} started'.format(this.model.get('seasonNumber')), + successMessage: 'Search for season {0} completed'.format(this.model.get('seasonNumber')) }); }, @@ -145,15 +145,15 @@ define( _seasonRename: function () { Actioneer.ExecuteCommand({ - command : 'renameSeason', - properties : { + command : 'renameSeason', + properties : { seriesId : this.model.get('seriesId'), seasonNumber: this.model.get('seasonNumber') }, - element : this.ui.seasonRename, - failMessage: 'Season rename failed', - context : this, - onSuccess : this._afterRename + element : this.ui.seasonRename, + errorMessage: 'Season rename failed', + context : this, + onSuccess : this._afterRename }); }, diff --git a/UI/Series/Details/SeasonMenu/ItemView.js b/UI/Series/Details/SeasonMenu/ItemView.js index 2ffe52418..1ab230734 100644 --- a/UI/Series/Details/SeasonMenu/ItemView.js +++ b/UI/Series/Details/SeasonMenu/ItemView.js @@ -51,7 +51,7 @@ define( seasonNumber: this.model.get('seasonNumber') }, element : this.ui.seasonSearch, - failMessage : 'Search for season {0} failed'.format(this.model.get('seasonNumber')), + errorMessage: 'Search for season {0} failed'.format(this.model.get('seasonNumber')), startMessage: 'Search for season {0} started'.format(this.model.get('seasonNumber')) }); }, diff --git a/UI/Series/Details/SeriesDetailsLayout.js b/UI/Series/Details/SeriesDetailsLayout.js index b241e18b3..6b305e8cd 100644 --- a/UI/Series/Details/SeriesDetailsLayout.js +++ b/UI/Series/Details/SeriesDetailsLayout.js @@ -155,10 +155,10 @@ define( properties : { seriesId: this.model.get('id') }, - element : this.ui.rename, - context : this, - onSuccess : this._refetchEpisodeFiles, - failMessage: 'Series search failed' + element : this.ui.rename, + context : this, + onSuccess : this._refetchEpisodeFiles, + errorMessage: 'Series search failed' }); }, @@ -169,7 +169,7 @@ define( seriesId: this.model.get('id') }, element : this.ui.search, - failMessage : 'Series search failed', + errorMessage: 'Series search failed', startMessage: 'Search for {0} started'.format(this.model.get('title')) }); }, diff --git a/UI/Series/Index/SeriesIndexLayout.js b/UI/Series/Index/SeriesIndexLayout.js index b35234589..dd44e3cb6 100644 --- a/UI/Series/Index/SeriesIndexLayout.js +++ b/UI/Series/Index/SeriesIndexLayout.js @@ -105,7 +105,6 @@ define( title : 'RSS Sync', icon : 'icon-rss', command : 'rsssync', - successMessage: 'RSS Sync Completed', errorMessage : 'RSS Sync Failed!' }, { @@ -140,7 +139,6 @@ define( this._fetchCollection(); }, - initialize: function () { this.seriesCollection = SeriesCollection; @@ -148,7 +146,6 @@ define( this.listenTo(SeriesCollection, 'remove', this._renderView); }, - _renderView: function () { if (SeriesCollection.length === 0) { @@ -164,7 +161,6 @@ define( } }, - onShow: function () { this._showToolbar(); this._renderView(); diff --git a/UI/Settings/Notifications/EditTemplate.html b/UI/Settings/Notifications/EditTemplate.html index dba9e30f9..91e9ecdb7 100644 --- a/UI/Settings/Notifications/EditTemplate.html +++ b/UI/Settings/Notifications/EditTemplate.html @@ -66,7 +66,7 @@ {{/if}} - +
diff --git a/UI/Settings/Notifications/EditView.js b/UI/Settings/Notifications/EditView.js index 312e48b88..be47a7db6 100644 --- a/UI/Settings/Notifications/EditView.js +++ b/UI/Settings/Notifications/EditView.js @@ -6,11 +6,11 @@ define([ 'Settings/Notifications/Model', 'Settings/Notifications/DeleteView', 'Shared/Messenger', - 'Commands/CommandController', + 'Shared/Actioneer', 'Mixins/AsModelBoundView', 'Form/FormBuilder' -], function (App, Marionette, NotificationModel, DeleteView, Messenger, CommandController, AsModelBoundView) { +], function (App, Marionette, NotificationModel, DeleteView, Messenger, Actioneer, AsModelBoundView) { var model = Marionette.ItemView.extend({ template: 'Settings/Notifications/EditTemplate', @@ -70,41 +70,28 @@ define([ var testCommand = this.model.get('testCommand'); if (testCommand) { this.idle = false; - this.ui.testButton.addClass('disabled'); - this.ui.testIcon.addClass('icon-spinner icon-spin'); - var properties = {}; _.each(this.model.get('fields'), function (field) { properties[field.name] = field.value; }); - var self = this; - var commandPromise = CommandController.Execute(testCommand, properties); - commandPromise.done(function () { - Messenger.show({ - message: 'Notification settings tested successfully' - }); - }); - - commandPromise.fail(function (options) { - if (options.readyState === 0 || options.status === 0) { - return; - } - - Messenger.show({ - message: 'Failed to test notification settings', - type : 'error' - }); + Actioneer.ExecuteCommand({ + command : testCommand, + properties : properties, + button : this.ui.testButton, + element : this.ui.testIcon, + errorMessage : 'Failed to test notification settings', + successMessage: 'Notification settings tested successfully', + always : this._testOnAlways, + context : this }); + } + }, - commandPromise.always(function () { - if (!self.isClosed) { - self.ui.testButton.removeClass('disabled'); - self.ui.testIcon.removeClass('icon-spinner icon-spin'); - self.idle = true; - } - }); + _testOnAlways: function () { + if (!this.isClosed) { + this.idle = true; } } }); diff --git a/UI/Shared/Actioneer.js b/UI/Shared/Actioneer.js index 929147ff1..516a0e7b7 100644 --- a/UI/Shared/Actioneer.js +++ b/UI/Shared/Actioneer.js @@ -1,15 +1,30 @@ 'use strict'; -define(['Commands/CommandController', 'Shared/Messenger'], - function(CommandController, Messenger) { - return { +define( + [ + 'Commands/CommandController', + 'Commands/CommandCollection', + 'Shared/Messenger'], + function(CommandController, CommandCollection, Messenger) { + + var actioneer = Marionette.AppRouter.extend({ + + initialize: function () { + this.trackedCommands = []; + CommandCollection.fetch(); + this.listenTo(CommandCollection, 'sync', this._handleCommands); + }, + ExecuteCommand: function (options) { options.iconClass = this._getIconClass(options.element); - this._showStartMessage(options); + if (options.button) { + options.button.addClass('disable'); + } + this._setSpinnerOnElement(options); var promise = CommandController.Execute(options.command, options.properties); - this._handlePromise(promise, options); + this._showStartMessage(options, promise); }, SaveModel: function (options) { @@ -24,15 +39,7 @@ define(['Commands/CommandController', 'Shared/Messenger'], _handlePromise: function (promise, options) { promise.done(function () { - if (options.successMessage) { - Messenger.show({ - message: options.successMessage - }); - } - - if (options.onSuccess) { - options.onSuccess.call(options.context); - } + self._onSuccess(options); }); promise.fail(function (ajaxOptions) { @@ -40,31 +47,46 @@ define(['Commands/CommandController', 'Shared/Messenger'], return; } - if (options.failMessage) { - Messenger.show({ - message: options.failMessage, - type : 'error' - }); - } - - if (options.onError) { - options.onError.call(options.context); - } + self._onError(options); }); promise.always(function () { + self._onComplete(options); + }); + }, + + _handleCommands: function () { + var self = this; + + _.each(this.trackedCommands, function (trackedCommand){ + if (trackedCommand.completed === true) { + return; + } + + var options = trackedCommand.options; + var command = CommandCollection.find({ 'id': trackedCommand.id }); - if (options.leaveIcon) { - options.element.removeClass('icon-spin'); + if (!command) { + trackedCommand.completed = true; + + self._onError(options, trackedCommand.id); + self._onComplete(options); + return; } - else { - options.element.addClass(options.iconClass); - options.element.removeClass('icon-nd-spinner'); + if (command.get('state') === 'completed') { + trackedCommand.completed = true; + + self._onSuccess(options, command.get('id')); + self._onComplete(options); + return; } - if (options.always) { - options.always.call(options.context); + if (command.get('state') === 'failed') { + trackedCommand.completed = true; + + self._onError(options, command.get('id')); + self._onComplete(options); } }); }, @@ -74,6 +96,10 @@ define(['Commands/CommandController', 'Shared/Messenger'], }, _setSpinnerOnElement: function (options) { + if (!options.element) { + return; + } + if (options.leaveIcon) { options.element.addClass('icon-spin'); } @@ -84,12 +110,79 @@ define(['Commands/CommandController', 'Shared/Messenger'], } }, - _showStartMessage: function (options) { - if (options.startMessage) { + _onSuccess: function (options, id) { + if (options.successMessage) { Messenger.show({ - message: options.startMessage + id : id, + message: options.successMessage, + type : 'success' }); } + + if (options.onSuccess) { + options.onSuccess.call(options.context); + } + }, + + _onError: function (options, id) { + if (options.errorMessage) { + Messenger.show({ + id : id, + message: options.errorMessage, + type : 'error' + }); + } + + if (options.onError) { + options.onError.call(options.context); + } + }, + + _onComplete: function (options) { + if (options.button) { + options.button.removeClass('disable'); + } + + if (options.leaveIcon) { + options.element.removeClass('icon-spin'); + } + + else { + options.element.addClass(options.iconClass); + options.element.removeClass('icon-nd-spinner'); + options.element.removeClass('icon-spin'); + } + + if (options.always) { + options.always.call(options.context); + } + }, + + _showStartMessage: function (options, promise) { + var self = this; + + if (!promise) { + if (options.startMessage) { + Messenger.show({ + message: options.startMessage + }); + } + + return; + } + + promise.done(function (data) { + self.trackedCommands.push({ id: data.id, options: options }); + + if (options.startMessage) { + Messenger.show({ + id : data.id, + message: options.startMessage + }); + } + }); } - } + }); + + return new actioneer(); }); diff --git a/UI/Shared/Messenger.js b/UI/Shared/Messenger.js index 8f04009d9..1188e0fe5 100644 --- a/UI/Shared/Messenger.js +++ b/UI/Shared/Messenger.js @@ -13,6 +13,10 @@ define(function () { options.hideAfter = 5; break; + case 'success': + options.hideAfter = 5; + break; + default : options.hideAfter = 0; } @@ -22,11 +26,11 @@ define(function () { message : options.message, type : options.type, showCloseButton: true, - hideAfter : options.hideAfter + hideAfter : options.hideAfter, + id : options.id }); }, - monitor: function (options) { if (!options.promise) { diff --git a/UI/Shared/Toolbar/Button/ButtonView.js b/UI/Shared/Toolbar/Button/ButtonView.js index 32e8b3fbc..04814ccd3 100644 --- a/UI/Shared/Toolbar/Button/ButtonView.js +++ b/UI/Shared/Toolbar/Button/ButtonView.js @@ -3,9 +3,9 @@ define( [ 'app', 'marionette', - 'Commands/CommandController', + 'Shared/Actioneer', 'Shared/Messenger' - ], function (App, Marionette, CommandController, Messenger) { + ], function (App, Marionette, Actioneer, Messenger) { return Marionette.ItemView.extend({ template : 'Shared/Toolbar/ButtonTemplate', @@ -19,7 +19,6 @@ define( icon: '.x-icon' }, - initialize: function () { this.storageKey = this.model.get('menuKey') + ':' + this.model.get('key'); this.idle = true; @@ -45,68 +44,19 @@ define( }, invokeCommand: function () { - //TODO: Use Actioneer to handle icon swapping - var command = this.model.get('command'); if (command) { this.idle = false; - this.$el.addClass('disabled'); - this.ui.icon.addClass('icon-spinner icon-spin'); - - var self = this; - var commandPromise = CommandController.Execute(command); - commandPromise.done(function () { - if (self.model.get('successMessage')) { - Messenger.show({ - message: self.model.get('successMessage') - }); - } - - if (self.model.get('onSuccess')) { - if (!self.model.ownerContext) { - throw 'ownerContext must be set.'; - } - - self.model.get('onSuccess').call(self.model.ownerContext); - } - }); - commandPromise.fail(function (options) { - if (options.readyState === 0 || options.status === 0) { - return; - } - - if (self.model.get('errorMessage')) { - Messenger.show({ - message: self.model.get('errorMessage'), - type : 'error' - }); - } - - if (self.model.get('onError')) { - if (!self.model.ownerContext) { - throw 'ownerContext must be set.'; - } - - self.model.get('onError').call(self.model.ownerContext); - } + Actioneer.ExecuteCommand({ + command : command, + button : this.$el, + element : this.ui.icon, + errorMessage : this.model.get('errorMessage'), + successMessage: this.model.get('successMessage'), + always : this._commandAlways, + context : this }); - - commandPromise.always(function () { - if (!self.isClosed) { - self.$el.removeClass('disabled'); - self.ui.icon.removeClass('icon-spinner icon-spin'); - self.idle = true; - } - }); - - if (self.model.get('always')) { - if (!self.model.ownerContext) { - throw 'ownerContext must be set.'; - } - - self.model.get('always').call(self.model.ownerContext); - } } }, @@ -133,8 +83,13 @@ define( if (callback) { callback.call(this.model.ownerContext); } - } + }, + _commandAlways: function () { + if (!this.isClosed) { + this.idle = true; + } + } }); }); diff --git a/UI/Shared/Toolbar/ToolbarLayout.js b/UI/Shared/Toolbar/ToolbarLayout.js index 701fdf07d..8945337a2 100644 --- a/UI/Shared/Toolbar/ToolbarLayout.js +++ b/UI/Shared/Toolbar/ToolbarLayout.js @@ -30,10 +30,8 @@ define( this.left = options.left; this.right = options.right; this.toolbarContext = options.context; - }, - onShow: function () { if (this.left) { _.each(this.left, this._showToolbarLeft, this); @@ -51,7 +49,6 @@ define( this._showToolbar(element, index, 'right'); }, - _showToolbar: function (buttonGroup, index, position) { var groupCollection = new ButtonCollection();